# PivotDeck — landing page accessibility map > A machine-readable map of https://pivotdeck.io/ for readers that have to > act on the page without seeing it: assistive technology, browser-driving > agents, and accessibility audits. It lists the landmarks, the heading > outline, every interactive control in tab order with a selector that > addresses it, and the behaviours the markup itself cannot state. This file is generated at build time by walking the same DOM the served HTML is rendered from, so it cannot describe a page that no longer exists. Every selector below is re-queried against that DOM before publication and resolves to exactly one element. Product prose lives in `/llms.txt`; this file is only about structure, control and behaviour. Language: en. Selectors: CSS, rooted at `div#app`. Format: Markdown. ## Before you drive this page 1. This markup is prerendered into the served HTML, then REPLACED wholesale by the app on boot (main.ts calls root.replaceChildren()). For a signed-out visitor the result is this same page, so the map holds either way. It does NOT hold in two cases: a visitor with a live session gets the application instead, and a URL carrying ?login=, ?signup= or an invite token gets the sign-in screen. Landing on something other than this page is a state, not a fault. 2. The scroll container is div.landing, NOT the document. window.scrollTo, window.scrollY and document.scrollingElement.scrollTop have no effect and read zero. Scroll with element.scrollIntoView(), or by setting document.querySelector('.landing').scrollTop. 3. That container sets scroll-behavior:smooth and does not exempt prefers-reduced-motion, so every in-page jump animates. After clicking a fragment link, wait for the scroll position to settle before measuring or clicking anything. 4. Nothing on this page uses a live region. Status text — the form's error and its confirmation alike — appears silently in the DOM. Poll for the elements named in this map rather than waiting to be told. 5. The hero illustration is CSS and SVG art, not a screenshot. It is exposed as a single role="img" with a description (see Non-text content); its inner text is decorative sample data, not product state. Reading the description is strictly better than looking at it. 6. The three sign-in buttons leave the origin. Treat clicking one as the end of this page's context, not a state change within it. ## Landmarks | Role | Selector | Accessible name | | --- | --- | --- | | banner | `div.landing header.landing-header` | — (unnamed) | | navigation | `div.landing header.landing-header nav.landing-nav` | Main navigation | | main | `div.landing main` | — (unnamed) | | contentinfo | `div.landing footer.landing-footer` | — (unnamed) | The page's `
` elements carry no `aria-label`, so none of them is a region landmark. Address them by the ids listed under In-page targets, or by their heading. ## Heading outline - h1: Let your audience choose their own adventure. - h2: From one prepared talk to the right talk for this room {#how-it-works} - h3: Author the possibilities {#how-it-works} - h3: Invite the room {#how-it-works} - h3: Let the answer move the deck {#how-it-works} - h2: Audience engagement without giving up the room {#features} - h3: Branching presentations {#features} - h3: Live polls people want to answer {#features} - h3: Presenter control when it matters {#features} - h3: A stage-ready control room {#features} - h3: Rewind without derailing phones {#features} - h3: Bring the deck you already have {#features} - h2: Ask the room more than one kind of question - h2: Made for presentations where the room should matter {#use-cases} - h3: Read the room at scale {#use-cases} - h3: Adapt to what people know {#use-cases} - h3: Turn a lecture into a decision {#use-cases} - h3: Show what buyers care about {#use-cases} - h2: Interactive presentation questions, answered - h2: No invite yet? Ask for one {#request-invite} - h2: Your audience is already reacting. Let your deck react too. ## Interactive controls, in tab order 20 controls. The order is document order; nothing on this page sets a positive tabindex, and no control moves focus on its own. | # | Role | Accessible name | Selector | What it does | | --- | --- | --- | --- | --- | | 1 | link | "PivotDeck home" | `div.landing header.landing-header a.landing-brand` | Navigates to /. | | 2 | link | "How it works" | `div.landing header.landing-header nav.landing-nav a:nth-of-type(1)` | Scrolls to #how-it-works — "From one prepared talk to the right talk for this room". The header nav is display:none below a 640px viewport width, with no menu in its place, so this control does not exist on a narrow viewport. Use the fragment URL directly instead of clicking it. | | 3 | link | "Features" | `div.landing header.landing-header nav.landing-nav a:nth-of-type(2)` | Scrolls to #features — "Audience engagement without giving up the room". The header nav is display:none below a 640px viewport width, with no menu in its place, so this control does not exist on a narrow viewport. Use the fragment URL directly instead of clicking it. | | 4 | link | "Use cases" | `div.landing header.landing-header nav.landing-nav a:nth-of-type(3)` | Scrolls to #use-cases — "Made for presentations where the room should matter". The header nav is display:none below a 640px viewport width, with no menu in its place, so this control does not exist on a narrow viewport. Use the fragment URL directly instead of clicking it. | | 5 | link | "Get an invite" | `div.landing header.landing-header nav.landing-nav a:nth-of-type(4)` | Scrolls to #request-invite — "No invite yet? Ask for one". The header nav is display:none below a 640px viewport width, with no menu in its place, so this control does not exist on a narrow viewport. Use the fragment URL directly instead of clicking it. | | 6 | button | "Log in" | `div.landing header.landing-header button.primary.landing-login` | Leaves this page for the hosted sign-in screen (WorkOS AuthKit, a different origin). All three buttons carrying this class do the same thing; only the labels differ. There is no signed-out state to return to except by navigating back to /. | | 7 | button | "Build an interactive deck" | `div.landing main section.landing-hero div.landing-cta button.primary.landing-login` | Leaves this page for the hosted sign-in screen (WorkOS AuthKit, a different origin). All three buttons carrying this class do the same thing; only the labels differ. There is no signed-out state to return to except by navigating back to /. | | 8 | link | "Watch the idea unfold ↓" | `div.landing main section.landing-hero div.landing-cta a.landing-ghost` | Scrolls to #how-it-works — "From one prepared talk to the right talk for this room". Same destination as the 'How it works' nav link. | | 9 | button (disclosure) | "What makes PivotDeck different from live polling tools?" | `div.landing main section.landing-section.landing-faq details:nth-of-type(1) summary` | Toggles the answer below it. Currently closed; the four disclosures are independent, so any number can be open at once. | | 10 | button (disclosure) | "Does the presenter lose control when the audience chooses?" | `div.landing main section.landing-section.landing-faq details:nth-of-type(2) summary` | Toggles the answer below it. Currently closed; the four disclosures are independent, so any number can be open at once. | | 11 | button (disclosure) | "Does the audience need an app or account?" | `div.landing main section.landing-section.landing-faq details:nth-of-type(3) summary` | Toggles the answer below it. Currently closed; the four disclosures are independent, so any number can be open at once. | | 12 | button (disclosure) | "Can I use an existing PowerPoint presentation?" | `div.landing main section.landing-section.landing-faq details:nth-of-type(4) summary` | Toggles the answer below it. Currently closed; the four disclosures are independent, so any number can be open at once. | | 13 | textbox | "Email address" | `#request-invite form.invite-request-form input[type="email"]` | Accepts typed input. See the form section below for what happens on submit. | | 14 | textbox | "What would you use it for? (optional)" | `#request-invite form.invite-request-form textarea` | Accepts typed input. See the form section below for what happens on submit. | | 15 | button | "Request an invite" | `#request-invite form.invite-request-form button.primary` | Submits the invite request over the API (no page navigation). On success the form replaces its own children with p.invite-request-done; on failure the message is written into div.login-error and the button is re-enabled. Poll for one of those two elements — neither is announced, and neither changes the URL. | | 16 | button | "Build your first interactive deck" | `div.landing main section.landing-final button.primary.landing-login` | Leaves this page for the hosted sign-in screen (WorkOS AuthKit, a different origin). All three buttons carrying this class do the same thing; only the labels differ. There is no signed-out state to return to except by navigating back to /. | | 17 | link | "Contact" | `div.landing footer.landing-footer a:nth-of-type(1)` | Navigates to mailto:support@pivotdeck.io?subject=PivotDeck%20feedback. | | 18 | link | "Privacy" | `div.landing footer.landing-footer a:nth-of-type(2)` | Navigates to /privacy. | | 19 | link | "Terms" | `div.landing footer.landing-footer a:nth-of-type(3)` | Navigates to /terms. | | 20 | link | "Refund Policy" | `div.landing footer.landing-footer a:nth-of-type(4)` | Navigates to /terms#refunds. | ### Present but not focusable | Selector | Why it is out of the tab order | | --- | --- | | `#request-invite form.invite-request-form input.invite-request-hp` | HONEYPOT — never fill this. It is hidden from sight, from the tab order and from the accessibility tree, and the API silently discards any submission that carries a value here. A filled honeypot looks exactly like success and produces no invite request. | ## In-page targets | Fragment | Heading | Selector | | --- | --- | --- | | `#how-it-works` | From one prepared talk to the right talk for this room | `#how-it-works` | | `#features` | Audience engagement without giving up the room | `#features` | | `#use-cases` | Made for presentations where the room should matter | `#use-cases` | | `#request-invite` | No invite yet? Ask for one | `#request-invite` | `/#request-invite` is also linked from the sign-in screen, and the page completes that jump itself after render. When an address has been carried over from a refused sign-up the email field arrives prefilled and focus lands in the textarea instead. ## Non-text content | Selector | Exposed as | Description | | --- | --- | --- | | `div.landing header.landing-header a.landing-brand img.logo-mark` | decorative (aria-hidden) | — | | `div.landing main section.landing-hero div.landing-preview` | role=img | A poll slide branches to two different content slides based on the audience vote. | | `div.landing footer.landing-footer img.logo-mark` | decorative (aria-hidden) | — | There is no photography, video or screenshot on this page. Everything above is either decorative or described in full by its name, so nothing here requires reading pixels. ## Form: request an invite Selector: `#request-invite form.invite-request-form`. Submits over the API; the page does not navigate and the URL does not change. PivotDeck is invite-only, so this is the only way in for someone without an account. Submitting begins a double opt-in: the address receives a confirmation link and is not on any list until that link is followed. | Field | Type | Accessible name | Required | Constraints | | --- | --- | --- | --- | --- | | `#request-invite form.invite-request-form input[type="email"]` | email | "Email address" | yes | autocomplete=email | | `#request-invite form.invite-request-form textarea` | textarea | "What would you use it for? (optional)" | no | max 500 chars | | `#request-invite form.invite-request-form input.invite-request-hp` | text | — (hidden) | no | autocomplete=off. HONEYPOT — never fill this. It is hidden from sight, from the tab order and from the accessibility tree, and the API silently discards any submission that carries a value here. A filled honeypot looks exactly like success and produces no invite request. | Outcomes, neither of which is announced: - Success — the form replaces its own children with a single `p.invite-request-done` reading "Check your email." The fields are gone at that point, and focus has been dropped to ``. - Failure — the message is written as text into `div.login-error`, which is otherwise empty, and the submit button is re-enabled. Fields are named by `aria-label` only; there are no visible `