{
  "audit": {
    "name": "Family welcome and onboarding accessibility audit",
    "standard": "WCAG 2.2 Level AA",
    "method": "WCAG-EM-informed representative evaluation",
    "date_started": "2026-08-25",
    "environment": "local development",
    "commit": "7383dd338",
    "branch": "main",
    "status": "complete_with_limitations",
    "primary_implementation": "React /v2/onboarding/*",
    "working_report": "../family-onboarding-wcag-2.2-aa-2026-08-25.md",
    "evidence_root": "~/screenshots/family-onboarding-a11y",
    "date_completed": "2026-08-25",
    "finding_count": 25,
    "severity_counts": {
      "critical": 0,
      "serious": 16,
      "moderate": 9,
      "minor": 0
    }
  },
  "tools": [
    {
      "name": "agent-browser",
      "version": "0.9.0"
    },
    {
      "name": "Playwright",
      "version": "1.56.1"
    },
    {
      "name": "axe-core",
      "version": "4.13.0",
      "integrity_verified": true
    }
  ],
  "findings": [
    {
      "id": "A11Y-001",
      "title": "React onboarding views have empty document titles",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "2.4.2"
      ],
      "status": "open",
      "source": [
        "portal/app/root.tsx:30-34"
      ],
      "screenshots": [
        "01-welcome-desktop.png",
        "02-identity-gate-desktop.png",
        "06-concerns-desktop.png",
        "09-consents-required-desktop.png"
      ],
      "affected_task": "Identify each onboarding step",
      "affected_functional_ability": "screen-reader navigation; orientation and memory",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; multiple /v2/onboarding/:signedId/* routes",
      "reproduction": [
        "Open representative React onboarding steps.",
        "Inspect document.title on initial load and after client-side navigation."
      ],
      "observed": "`document.title` was empty on welcome, identity verification, concerns, service line, address, card upload, consents, and invalid-link states. Axe reported `document-title` on every scanned state.",
      "expected": "Each page has a unique localized title describing the current step.",
      "impact": "Browser tabs, history, and assistive-technology page identification do not distinguish onboarding steps.",
      "recommended_correction": "Define a localized title for the onboarding layout or each step and verify it changes with route navigation.",
      "verification": "Assert the exact title on initial load and after each client-side step transition.",
      "shared_owner_root_cause": "Onboarding routes export no route metadata; `portal/app/root.tsx:30-34` renders `<Meta />` but receives no onboarding title."
    },
    {
      "id": "A11Y-002",
      "title": "Spanish and Portuguese onboarding content is declared as English",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "3.1.1"
      ],
      "status": "open",
      "source": [
        "portal/app/root.tsx:29"
      ],
      "screenshots": [
        "15-welcome-spanish.png",
        "35-welcome-portuguese.png"
      ],
      "affected_task": "Use onboarding in Spanish or Portuguese",
      "affected_functional_ability": "language comprehension; screen-reader pronunciation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; welcome?locale=es and welcome?locale=pt",
      "reproduction": [
        "Open welcome with locale=es and locale=pt.",
        "Compare visible language with the html lang value."
      ],
      "observed": "The Spanish and Portuguese welcome pages rendered localized copy while `<html lang=\"en\">` remained unchanged.",
      "expected": "The page language matches the resolved locale and updates with locale changes.",
      "impact": "Screen readers and other language-aware tools may apply English pronunciation and processing rules to Spanish and Portuguese content.",
      "recommended_correction": "Set the document language from the resolved application locale and update it when locale changes.",
      "verification": "Assert `es`, `pt`, and `en` on representative localized routes.",
      "shared_owner_root_cause": "`portal/app/root.tsx:29` hardcodes `lang=\"en\"`."
    },
    {
      "id": "A11Y-003",
      "title": "Optional consent choices expose no selected state or consent-specific name",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "1.3.1",
        "1.4.1",
        "4.1.2"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-consents-screen.tsx:185-217"
      ],
      "screenshots": [
        "10-consents-optional-desktop.png"
      ],
      "affected_task": "Answer optional consent questions",
      "affected_functional_ability": "screen-reader operation; color perception",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; consents optional panel",
      "reproduction": [
        "Advance from required to optional consents.",
        "Inspect repeated Yes/No names and select a response.",
        "Inspect role and selected state in the accessibility tree."
      ],
      "observed": "Each consent uses repeated ordinary buttons named only \u201cYes\u201d and \u201cNo.\u201d Selecting one changes colors but adds no `aria-pressed`, `aria-checked`, radio role, or programmatic association with the consent heading.",
      "expected": "Each consent is a named single-choice group with a programmatically exposed selection, not color alone.",
      "impact": "A screen-reader user cannot reliably determine which repeated choice belongs to which consent or which answer is selected.",
      "recommended_correction": "Model each pair as a labeled radio group or another standard single-selection control with a programmatic group name and selected value.",
      "verification": "Inspect the accessibility tree and complete all choices using keyboard and screen reader.",
      "shared_owner_root_cause": "portal/app/components/onboarding-consents-screen.tsx:185-217"
    },
    {
      "id": "A11Y-004",
      "title": "Consent Yes/No buttons have no visible keyboard focus indicator",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "2.4.7"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-consents-screen.tsx:197-215"
      ],
      "screenshots": [
        "11-consents-yes-keyboard-focus.png"
      ],
      "affected_task": "Answer optional consent questions by keyboard",
      "affected_functional_ability": "keyboard and low-vision operation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; consents optional panel",
      "reproduction": [
        "Tab to a consent Yes or No button.",
        "Observe focus without activating it."
      ],
      "observed": "A focused Yes button computed to `outline: none` and `box-shadow: none`. Space activated it, but focus location was not visually apparent.",
      "expected": "Every focused consent choice has a persistent visible focus indicator in standard and forced colors.",
      "impact": "Keyboard users cannot track which legal decision control will be activated.",
      "recommended_correction": "Use the shared focus-visible treatment with adequate contrast on both selected and unselected states.",
      "verification": "Tab through every consent choice in default and forced-colors modes.",
      "shared_owner_root_cause": "portal/app/components/onboarding-consents-screen.tsx:197-215"
    },
    {
      "id": "A11Y-005",
      "title": "Insurance-card photo controls cannot be reached by keyboard",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "1.3.1",
        "2.1.1",
        "2.5.3"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-insurance-card-upload-screen.tsx:168-186"
      ],
      "screenshots": [
        "13-insurance-card-upload-desktop.png"
      ],
      "affected_task": "Upload insurance-card images",
      "affected_functional_ability": "keyboard and speech-input operation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; insurance/card-upload",
      "reproduction": [
        "Open insurance card upload.",
        "Tab from the preceding control through both visible upload targets.",
        "Compare visible card-side text with computed control names."
      ],
      "observed": "Both file inputs use `display: none`; the clickable labels are not focusable. A real Tab sequence skipped both upload controls. The visible \u201cFront of card\u201d and \u201cBack of card (optional)\u201d text sits outside each label and is absent from the input's accessible name.",
      "expected": "Both uploads are keyboard operable and their names include the visible side/required context.",
      "impact": "Keyboard users cannot upload insurance images and assistive-technology users do not receive the card-side or required-state context. Manual entry is available but does not make the upload function keyboard operable.",
      "recommended_correction": "Use a visually hidden, focusable file input or keyboard-operable button/label pattern; include side, required state, and helper text in the accessible relationship.",
      "verification": "Tab to each upload, open the file chooser from the keyboard, and inspect names/descriptions.",
      "shared_owner_root_cause": "portal/app/components/onboarding-insurance-card-upload-screen.tsx:168-186"
    },
    {
      "id": "A11Y-006",
      "title": "Legal-terms modal is an unnamed dialog",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "4.1.2"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-consents-screen.tsx:251-287"
      ],
      "screenshots": [
        "19-unnamed-legal-terms-dialog.png"
      ],
      "affected_task": "Read full legal consent terms",
      "affected_functional_ability": "screen-reader dialog navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; consents legal-terms dialog",
      "reproduction": [
        "Open optional consents.",
        "Activate Read full terms.",
        "Inspect the dialog role and computed name."
      ],
      "observed": "The accessibility tree exposed `dialog` with no name. The popup has no `Dialog.Title`, `aria-label`, or `aria-labelledby`.",
      "expected": "The dialog has a visible, programmatically connected title and correct focus behavior.",
      "impact": "Screen-reader users entering high-consequence legal text are not told what dialog opened.",
      "recommended_correction": "Add a visible `Dialog.Title` connected to the dialog. Give loading and failure replacements appropriate status semantics.",
      "verification": "Confirm the dialog's computed accessible name; verify initial focus, Escape, focus containment, and focus restoration. Escape and restoration to \u201cRead full terms\u201d passed in this sample.",
      "shared_owner_root_cause": "portal/app/components/onboarding-consents-screen.tsx:251-287"
    },
    {
      "id": "A11Y-007",
      "title": "SPA step changes discard focus and provide no new-view context",
      "severity": "serious",
      "evidence": "verified_observation_needs_at_confirmation",
      "criteria": [
        "2.4.3"
      ],
      "status": "open",
      "source": [
        "portal/app/routes/_client.onboarding.$signedId.tsx:5-36"
      ],
      "screenshots": [
        "02-identity-gate-desktop.png",
        "06-concerns-desktop.png"
      ],
      "affected_task": "Move between onboarding steps and consent panels",
      "affected_functional_ability": "keyboard and screen-reader orientation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; multiple SPA transitions and consent panels",
      "reproduction": [
        "Activate Continue on representative route and internal panel transitions.",
        "After the new content settles, inspect document.activeElement and new-view announcement."
      ],
      "observed": "Welcome \u2192 identity, verification \u2192 concerns, and concerns \u2192 service line all left `document.activeElement` on `<body>` after the new view settled. Titles also remained empty.",
      "expected": "Focus moves to a deliberate target and the new context is announced once.",
      "impact": "Keyboard and screen-reader users lose their interaction point and receive no deterministic indication of the new step.",
      "recommended_correction": "Establish one route-change strategy that updates the localized title and moves focus to the new primary heading or a deliberate route-status target without causing duplicate announcements.",
      "verification": "Test browser Back/Forward and every step transition with keyboard, VoiceOver/Safari, and NVDA/Chrome or Firefox.",
      "shared_owner_root_cause": "portal/app/routes/_client.onboarding.$signedId.tsx:5-36"
    },
    {
      "id": "A11Y-008",
      "title": "Address validation leaves focus on Continue and does not associate error help",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "3.3.1",
        "3.3.3",
        "4.1.3"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-address-screen.tsx:140-158",
        "portal/app/components/onboarding-address-screen.tsx:202-218"
      ],
      "screenshots": [
        "17-address-errors.png"
      ],
      "affected_task": "Correct an invalid address submission",
      "affected_functional_ability": "keyboard and screen-reader error recovery",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; address invalid submission",
      "reproduction": [
        "Open Address and submit all required fields empty.",
        "Inspect focus, field descriptions, and live regions."
      ],
      "observed": "Empty submit marked fields `aria-invalid=\"true\"` but left focus on Continue. The generic \u201cPlease fill in all required fields\u201d paragraph had no live-region semantics and was not referenced by any field through `aria-describedby`. No field-specific correction was exposed.",
      "expected": "Submission identifies and associates each error, announces failure, and focuses a useful error target.",
      "impact": "Users must rediscover all failed fields; screen-reader users may not hear that submission failed.",
      "recommended_correction": "Provide field-associated errors, announce or focus an error summary, and move focus to the summary or first invalid control after submit.",
      "verification": "Submit combinations of missing and malformed values and verify focus, announcement, associations, and value preservation.",
      "shared_owner_root_cause": "portal/app/components/onboarding-address-screen.tsx:140-158"
    },
    {
      "id": "A11Y-009",
      "title": "Invalid or expired links produce a terminal with no heading",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "1.3.1"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-error-screen.tsx:19-29"
      ],
      "screenshots": [
        "14-invalid-link-error.png"
      ],
      "affected_task": "Recover from an invalid or expired link",
      "affected_functional_ability": "heading navigation and comprehension",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; invalid-link terminal",
      "reproduction": [
        "Open an invalid signed-link state.",
        "Inspect the title, landmarks, and heading outline."
      ],
      "observed": "The invalid-link state contained only paragraphs and links: no `h1`\u2013`h6`, main landmark, or document title.",
      "expected": "The terminal has a descriptive title, primary heading, and understandable recovery path.",
      "impact": "Heading navigation cannot locate or identify the terminal error, and the page lacks a concise programmatic statement of purpose.",
      "recommended_correction": "Render a clear `h1`, useful recovery copy, and a descriptive document title.",
      "verification": "Inspect invalid, expired, and feature-disabled variants.",
      "shared_owner_root_cause": "portal/app/components/onboarding-error-screen.tsx:19-29"
    },
    {
      "id": "A11Y-010",
      "title": "Insurance carrier search opens an unnamed dialog",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "4.1.2"
      ],
      "status": "open",
      "source": [
        "packages/components/src/forms-v2/search-select.tsx:115-155"
      ],
      "screenshots": [
        "21-insurance-combobox-open.png"
      ],
      "affected_task": "Choose an insurance carrier or matching specialty",
      "affected_functional_ability": "screen-reader popup navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; insurance carrier and matching specialty popups",
      "reproduction": [
        "Open the carrier search and matching Specialty filter.",
        "Inspect each popup role and computed name."
      ],
      "observed": "Opening \u201cInsurance carrier\u201d exposed a popup with `role=\"dialog\"` and no `aria-label` or `aria-labelledby`. The search combobox inside was named, but the containing dialog was not.",
      "expected": "Every dialog popup has a name derived from its visible heading or field label.",
      "impact": "Screen-reader users are not given a programmatic name for the popup context they enter.",
      "recommended_correction": "Give the popup a visible or programmatic name derived from its field label, while preserving the names and relationships of the trigger, search input, and listbox.",
      "verification": "Inspect every shared `SearchableSelect` usage and verify popup naming, Escape, keyboard selection, and focus restoration with AT.",
      "shared_owner_root_cause": "packages/components/src/forms-v2/search-select.tsx:115-155"
    },
    {
      "id": "A11Y-011",
      "title": "Availability cells lose their focus indicator in forced-colors mode",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "2.4.7"
      ],
      "status": "open",
      "source": [
        "packages/components/src/weekly-availability-grid.tsx:107-126"
      ],
      "screenshots": [
        "23-waitlist-desktop.png",
        "24-waitlist-forced-colors.png",
        "26-waitlist-320px.png"
      ],
      "affected_task": "Choose waitlist availability in forced colors",
      "affected_functional_ability": "keyboard and low-vision operation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; waitlist under forced colors",
      "reproduction": [
        "Enable forced-colors mode.",
        "Tab to a waitlist time cell and select it.",
        "Inspect computed outline, box-shadow, border, and selected state."
      ],
      "observed": "A focused waitlist availability cell computed to `outline: none` and `box-shadow: none` in Chromium forced-colors mode because the implementation removes the outline and relies on a Tailwind ring. The grid places all 91 cells in the Tab order.",
      "expected": "Focused cells retain a visible system-color indicator and selected state.",
      "impact": "Keyboard users with Windows High Contrast or another forced-colors configuration cannot reliably see which time slot has focus; reaching controls after the grid also requires excessive tabbing.",
      "recommended_correction": "Preserve a system-color outline under forced colors. Consider a composite grid or roving-tabindex interaction so arrow keys traverse cells while the grid contributes one Tab stop.",
      "verification": "Check selected and unselected cells with keyboard in Windows High Contrast themes and standard color modes.",
      "shared_owner_root_cause": "packages/components/src/weekly-availability-grid.tsx:107-126"
    },
    {
      "id": "A11Y-012",
      "title": "Payment handoff page has no declared language",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "3.1.1"
      ],
      "status": "open",
      "source": [],
      "screenshots": [
        "27-payment-handoff.png",
        "28-stripe-checkout-desktop.png"
      ],
      "affected_task": "Enter payment information",
      "affected_functional_ability": "screen-reader pronunciation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; Rails payment host",
      "reproduction": [
        "Navigate from React onboarding to the Rails payment host.",
        "Inspect the host html lang and the Stripe iframe language."
      ],
      "observed": "The Rails-rendered payment host page had no `lang` attribute on `<html>`. Axe reported `html-has-lang`. Stripe's embedded document separately declared `lang=\"en\"`.",
      "expected": "Host and embedded documents declare the resolved locale.",
      "impact": "Assistive technology cannot reliably select the correct language rules for Cartwheel's payment instructions surrounding the embedded checkout.",
      "recommended_correction": "Set the payment layout's document language from the resolved locale and preserve language across the React-to-Rails handoff.",
      "verification": "Assert the host and embedded-document language for English, Spanish, and Portuguese payment routes.",
      "shared_owner_root_cause": "Shared onboarding document/layout"
    },
    {
      "id": "A11Y-013",
      "title": "Payment handoff has a generic document title",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "2.4.2"
      ],
      "status": "open",
      "source": [],
      "screenshots": [
        "27-payment-handoff.png"
      ],
      "affected_task": "Identify the payment step",
      "affected_functional_ability": "screen-reader and browser-tab orientation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; Rails payment host",
      "reproduction": [
        "Open the payment handoff.",
        "Inspect the host document title."
      ],
      "observed": "The payment host page title was only \u201cCartwheel,\u201d despite the page purpose being payment-information collection.",
      "expected": "The title identifies Cartwheel and the payment-information step.",
      "impact": "Browser tabs, history, and assistive-technology page lists do not identify this onboarding step.",
      "recommended_correction": "Provide a localized title that identifies both Cartwheel and the payment-information step.",
      "verification": "Assert the exact host-page title in each supported locale.",
      "shared_owner_root_cause": "Shared onboarding document/layout"
    },
    {
      "id": "A11Y-014",
      "title": "Match-search progress exposes contradictory states and does not announce completion",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "1.3.1",
        "4.1.2",
        "4.1.3"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-finding-matches-screen.tsx:90-219",
        "portal/app/components/onboarding-finding-matches-screen.tsx:240-280"
      ],
      "screenshots": [
        "29-finding-matches.png"
      ],
      "affected_task": "Wait for clinician matching results",
      "affected_functional_ability": "screen-reader status monitoring",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; finding-matches",
      "reproduction": [
        "Open Finding matches.",
        "Inspect the full accessibility tree during and after the animation.",
        "Observe whether completion and Continue availability are announced."
      ],
      "observed": "Completed checklist rows simultaneously exposed images named \u201cLoading\u201d and \u201cDone\u201d because CSS opacity and animation changed only the visual presentation. The active row later changed visually and a Continue button appeared, but the checklist had no live or status semantics. The hardcoded English image labels were also exposed in localized flows.",
      "expected": "Each row exposes one current state and completion is announced concisely once.",
      "impact": "Screen-reader users receive contradictory progress states and may not be told when matching is ready or when a new action becomes available.",
      "recommended_correction": "Represent each row with one programmatic status at a time, hide replaced decorative graphics, localize any meaningful state labels, and announce concise progress/completion without replaying the whole checklist.",
      "verification": "Observe the complete success, delayed, fallback, and error sequences with VoiceOver and NVDA; ensure each stage has one current state and completion is announced once.",
      "shared_owner_root_cause": "portal/app/components/onboarding-finding-matches-screen.tsx:90-219"
    },
    {
      "id": "A11Y-015",
      "title": "Appointment time controls omit date, clinician, and selected-state context",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "1.3.1",
        "4.1.2"
      ],
      "status": "open",
      "source": [
        "packages/components/src/clinician-available-times.tsx:32-57",
        "packages/components/src/time-chip.tsx:20-39"
      ],
      "screenshots": [
        "30-matching-results-desktop.png",
        "31-schedule-profile-selected-time.png"
      ],
      "affected_task": "Select and confirm an appointment time",
      "affected_functional_ability": "screen-reader form navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; matching and schedule",
      "reproduction": [
        "Open matching results and navigate by buttons.",
        "Open one clinician schedule with a selected slot.",
        "Inspect each time name, grouping, and selected state."
      ],
      "observed": "Matching results exposed many repeated buttons named only \u201c7:00 am,\u201d \u201c7:15 am,\u201d and similar values without a programmatic relationship to the visual clinician card or date label. On the clinician schedule, a preselected time changed only border/background classes; it exposed no `aria-pressed`, `aria-checked`, or equivalent selected state.",
      "expected": "Every time has unique date/clinician context and the current selection is programmatically exposed.",
      "impact": "Screen-reader button navigation cannot distinguish appointment choices, and users cannot determine which date/time is currently selected before continuing.",
      "recommended_correction": "Give each time control a name or description containing the full date and relevant clinician context, group controls under programmatic date/clinician labels, and expose selection with a standard radio or toggle pattern.",
      "verification": "Navigate matching and schedule controls by buttons, groups, and form controls in VoiceOver and NVDA; confirm every choice is unique and the selected slot is announced.",
      "shared_owner_root_cause": "packages/components/src/clinician-available-times.tsx:32-57"
    },
    {
      "id": "A11Y-016",
      "title": "Clinician-profile disclosure controls have ambiguous names and no expanded state",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "2.4.6",
        "4.1.2"
      ],
      "status": "open",
      "source": [
        "portal/app/routes/_client.onboarding.$signedId.schedule.$clinicianId/route.tsx:348-425",
        "packages/components/src/badge.tsx"
      ],
      "screenshots": [
        "31-schedule-profile-selected-time.png"
      ],
      "affected_task": "Expand clinician languages and specialties",
      "affected_functional_ability": "screen-reader button navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; schedule clinician profile",
      "reproduction": [
        "Open a clinician profile.",
        "Inspect +N more and Show less names/states before and after activation."
      ],
      "observed": "Language and specialty disclosures were named only \u201c+14 more,\u201d \u201c+13 more,\u201d or \u201cShow less.\u201d They exposed no `aria-expanded` or `aria-controls`, and repeated \u201cShow less\u201d labels did not identify which content would collapse.",
      "expected": "Each disclosure names its content, exposes expanded state, and references its controlled region.",
      "impact": "Screen-reader users navigating by buttons cannot identify the disclosure's purpose or determine whether its related content is expanded.",
      "recommended_correction": "Include the controlled content in each accessible name, expose `aria-expanded`, and connect each button to a stable content container with `aria-controls`.",
      "verification": "Expand and collapse languages and specialties with keyboard and screen reader; confirm purpose, state, and controlled region are announced.",
      "shared_owner_root_cause": "portal/app/routes/_client.onboarding.$signedId.schedule.$clinicianId/route.tsx:348-425"
    },
    {
      "id": "A11Y-017",
      "title": "Shared wizard pages can begin at heading level 2",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "1.3.1"
      ],
      "status": "open",
      "source": [
        "packages/components/src/layouts/wizard-page.tsx:39-60"
      ],
      "screenshots": [
        "32-confirm-booking.png",
        "34-waitlist-confirmed.png",
        "34-financial-aid-submitted.png"
      ],
      "affected_task": "Understand booking and pending terminal structure",
      "affected_functional_ability": "heading and landmark navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; confirm and pending terminals",
      "reproduction": [
        "Open booking confirmation and representative pending terminals.",
        "Inspect h1/h2 outline and main landmarks."
      ],
      "observed": "\u201cConfirm your booking\u201d was the page's primary heading but rendered as `h2`; the page contained no `h1`. \u201cHow it works\u201d was another `h2`, making both appear as peer sections rather than exposing the first as the page title. The shared wizard layout also provided no `main` landmark.",
      "expected": "Each page starts with one semantic h1 and nests subsection headings under it.",
      "impact": "Heading navigation communicates a misleading document hierarchy and makes the primary purpose less predictable.",
      "recommended_correction": "Let each route supply a semantic `h1` for its page title and use lower levels only for subsections. Add one `main` landmark around the primary page content.",
      "verification": "Inspect heading outlines and landmarks for every route using the shared wizard layout, including terminal and error states.",
      "shared_owner_root_cause": "packages/components/src/layouts/wizard-page.tsx:39-60"
    },
    {
      "id": "A11Y-018",
      "title": "Timed interstitials advance after three seconds without user control",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "2.2.1"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-reassurance-screen.tsx:5-47",
        "portal/app/routes/_client.onboarding.$signedId.insurance._index/route.tsx"
      ],
      "screenshots": [
        "18-reassurance-reduced-motion.png"
      ],
      "affected_task": "Read timed reassurance/interstitial content",
      "affected_functional_ability": "reading, cognition, magnification",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; reassurance and insurance interstitial",
      "reproduction": [
        "Open reassurance and the post-insurance interstitial.",
        "Wait without interaction and observe route timing.",
        "Repeat with reduced motion."
      ],
      "observed": "The reassurance screen and post-insurance \u201cYou're in the right place\u201d interstitial each navigated away after three seconds. Users could not turn off, adjust, extend, pause, or manually advance the limit. Reduced-motion preference did not stop the reassurance animation or timeout.",
      "expected": "Content remains until user action or offers a conforming pause/extend mechanism.",
      "impact": "Users who need more time to read, understand, magnify, or translate content can lose it before finishing and may be disoriented by the unsolicited route change.",
      "recommended_correction": "Remove the automatic limit and provide a clear Continue action, or offer a conforming mechanism to pause/extend it. Respect reduced-motion for nonessential animation independently.",
      "verification": "Confirm content remains until the user advances it; test keyboard, magnification, reduced motion, and screen readers.",
      "shared_owner_root_cause": "portal/app/components/onboarding-reassurance-screen.tsx:5-47"
    },
    {
      "id": "A11Y-019",
      "title": "Several option sets have no programmatic group name",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "1.3.1"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-joint-custody-screen.tsx:147-192",
        "portal/app/components/onboarding-insurance-self-report-screen.tsx:61-94",
        "portal/app/components/onboarding-service-line-screen.tsx:85-126",
        "portal/app/components/onboarding-consents-screen.tsx"
      ],
      "screenshots": [
        "10-consents-optional-desktop.png"
      ],
      "affected_task": "Answer joint custody, service line, insurance, and consent questions",
      "affected_functional_ability": "screen-reader form navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; joint-custody, insurance-self-report, service-line, consents",
      "reproduction": [
        "Open representative option-set screens.",
        "Navigate by radio, checkbox, or form controls.",
        "Inspect each group name."
      ],
      "observed": "Joint-custody, insurance self-report, service-line, and consent choices expose individually named controls but do not connect their visual question to the radio, checkbox, or button group with `fieldset`/`legend`, `aria-labelledby`, or an equivalent name. Rendered accessibility trees showed unnamed radio groups.",
      "expected": "Every related option set has a programmatic name matching its visible question.",
      "impact": "Screen-reader users navigating by form controls lose the question that gives repeated answers such as Yes, No, or No preference their meaning.",
      "recommended_correction": "Use native `fieldset`/`legend` where practical or give each group a stable accessible name derived from its visible prompt.",
      "verification": "Navigate each set by form controls with VoiceOver and NVDA and confirm the question is announced with the options.",
      "shared_owner_root_cause": "portal/app/components/onboarding-joint-custody-screen.tsx:147-192"
    },
    {
      "id": "A11Y-020",
      "title": "Manual-insurance errors are not associated, focused, or announced",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "3.3.1",
        "3.3.3",
        "4.1.3"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-insurance-manual-entry-screen.tsx:104-167",
        "packages/components/src/announcement.tsx:79-105",
        "packages/components/src/forms-v2/announcement.tsx:48-57"
      ],
      "screenshots": [
        "41-insurance-manual-entry-empty.png",
        "43-insurance-manual-entry-member-error.png"
      ],
      "affected_task": "Correct manual insurance and network errors",
      "affected_functional_ability": "keyboard and screen-reader error recovery",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; insurance manual entry invalid/error",
      "reproduction": [
        "Select an insurance company and submit Member ID empty.",
        "Inspect focus, aria-invalid, descriptions, and live regions."
      ],
      "observed": "Submitting manual insurance with an empty Member ID displayed \u201cPlease fill in all required fields,\u201d left focus on Get cost estimate, and set `aria-invalid` without connecting the input to the visible error. The error had no alert/live semantics. Shared Announcement implementations likewise provide no status role for mutation failures.",
      "expected": "Errors identify the field and correction, are associated and announced, and receive useful focus.",
      "impact": "Screen-reader and keyboard users may not know submission failed, which field needs correction, or how to recover.",
      "recommended_correction": "Add field-specific associated messages, announce or focus an error summary, and move focus to the summary or first invalid field. Give asynchronously inserted failures appropriate alert/status semantics.",
      "verification": "Trigger required-field, mutation, upload, and network failures and verify focus, associations, announcement, and retained input.",
      "shared_owner_root_cause": "portal/app/components/onboarding-insurance-manual-entry-screen.tsx:104-167"
    },
    {
      "id": "A11Y-021",
      "title": "Localized flows expose hardcoded English content and accessible names",
      "severity": "serious",
      "evidence": "verified",
      "criteria": [
        "3.1.2"
      ],
      "status": "open",
      "source": [
        "portal/app/routes/_client.onboarding.$signedId.welcome/route.tsx:184-194",
        "portal/app/routes/_client.onboarding.$signedId.matching.$serviceLineId/route.tsx:232-339",
        "portal/app/routes/_client.onboarding.$signedId.complete/route.tsx:24-46",
        "packages/components/src/loading-view.tsx",
        "packages/components/src/spinner.tsx"
      ],
      "screenshots": [
        "54-welcome-es-faq.png",
        "44-insurance-manual-entry-polling.png"
      ],
      "affected_task": "Complete onboarding in Spanish or Portuguese",
      "affected_functional_ability": "language comprehension and screen-reader pronunciation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; localized welcome, loading, matching, completion",
      "reproduction": [
        "Open Spanish welcome FAQ and inspect Close.",
        "Review localized matching/loading/completion source and rendered states.",
        "Compare dates, control names, and status text with active locale."
      ],
      "observed": "The Spanish welcome FAQ exposed its close control as \u201cClose.\u201d Matching filters, loading/error text, and date/time formatters include hardcoded English; shared loading status uses \u201cLoading...\u201d instead of the localized visible context. Completion formatters force `en-US` output in Spanish and Portuguese flows.",
      "expected": "All content and accessible names use the active locale or declare intentional language changes.",
      "impact": "Families using Spanish or Portuguese can encounter controls and essential scheduling information they cannot understand or that assistive technology pronounces using the wrong language rules.",
      "recommended_correction": "Route all user-facing and accessible strings through i18n, format dates/times with the active locale, and use `lang` only for intentionally retained foreign-language parts.",
      "verification": "Complete matching, scheduling, consent, payment, and completion in Spanish and Portuguese while inspecting visible copy and computed names.",
      "shared_owner_root_cause": "portal/app/routes/_client.onboarding.$signedId.welcome/route.tsx:184-194"
    },
    {
      "id": "A11Y-022",
      "title": "Signer-name input does not identify its autocomplete purpose",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "1.3.5"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-consents-screen.tsx:684-698"
      ],
      "screenshots": [
        "10-consents-optional-desktop.png"
      ],
      "affected_task": "Autofill the legal signer name",
      "affected_functional_ability": "cognitive assistance and personalization",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; consent signer field",
      "reproduction": [
        "Open the signer-name consent state.",
        "Inspect the rendered autocomplete attribute."
      ],
      "observed": "The legal signer's full-name input collects a standard personal-data purpose but has no `autocomplete=\"name\"` token.",
      "expected": "The standard autocomplete=name purpose is programmatically identified.",
      "impact": "Browser autofill and assistive personalization cannot reliably identify or supply the signer's name.",
      "recommended_correction": "Add the standards-defined `name` autocomplete token, confirming that the field represents the person currently completing the consent.",
      "verification": "Inspect the rendered token and test browser autofill without changing legal validation behavior.",
      "shared_owner_root_cause": "portal/app/components/onboarding-consents-screen.tsx:684-698"
    },
    {
      "id": "A11Y-023",
      "title": "Completion decoration is exposed as an unnamed image",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "1.1.1"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-complete-screen.tsx:102-104"
      ],
      "screenshots": [
        "33-complete-terminal.png"
      ],
      "affected_task": "Understand onboarding completion",
      "affected_functional_ability": "screen-reader non-text-content navigation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; complete",
      "reproduction": [
        "Open the booked completion terminal.",
        "Run axe and inspect the celebration SVG in the accessibility tree."
      ],
      "observed": "Axe reported `svg-img-alt` for the full-card celebration SVG, which renders `role=\"img\"` with an empty `aria-label`.",
      "expected": "Decorative art is hidden, or meaningful art has approved alternative text.",
      "impact": "Assistive technology encounters an unexplained image in the completion outcome.",
      "recommended_correction": "Treat the celebration as decorative with `aria-hidden=\"true\"` and no image role, or provide approved meaningful alternative text if product determines it conveys information.",
      "verification": "Re-run axe and inspect the completion accessibility tree.",
      "shared_owner_root_cause": "portal/app/components/onboarding-complete-screen.tsx:102-104"
    },
    {
      "id": "A11Y-024",
      "title": "Completion instruction text misses minimum contrast",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "1.4.3"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-complete-screen.tsx:249-257"
      ],
      "screenshots": [
        "33-complete-terminal.png"
      ],
      "affected_task": "Read post-booking instructions",
      "affected_functional_ability": "low-vision and contrast perception",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; complete",
      "reproduction": [
        "Open the booked completion terminal.",
        "Measure the email/Healthie instruction foreground and background contrast."
      ],
      "observed": "Axe measured the 12 px email/Healthie instruction text at 4.44:1 (`#697073` on `#f3f0ed`), below the required 4.5:1 for normal text.",
      "expected": "Normal text has at least 4.5:1 contrast with safety margin.",
      "impact": "Users with low vision or reduced contrast sensitivity may have difficulty reading required post-booking instructions.",
      "recommended_correction": "Use a foreground/background pair that clears 4.5:1 with margin for rendering variation.",
      "verification": "Re-measure the rendered state and check default, zoomed, and forced-colors presentation.",
      "shared_owner_root_cause": "portal/app/components/onboarding-complete-screen.tsx:249-257"
    },
    {
      "id": "A11Y-025",
      "title": "Insurance special options navigate immediately when selected",
      "severity": "moderate",
      "evidence": "verified",
      "criteria": [
        "3.2.2"
      ],
      "status": "open",
      "source": [
        "portal/app/components/onboarding-insurance-zip-lookup-screen.tsx:118-137"
      ],
      "screenshots": [
        "21-insurance-combobox-open.png"
      ],
      "affected_task": "Choose insurance payment/coverage path",
      "affected_functional_ability": "keyboard, screen-reader, and cognitive orientation",
      "environment_state": "Local Chromium, React/Rails development build 7383dd338; insurance carrier lookup",
      "reproduction": [
        "Open the carrier combobox.",
        "Select I do not have insurance or Self-pay without pressing Continue.",
        "Observe the immediate screen/route change."
      ],
      "observed": "Selecting \u201cI don't have insurance\u201d or \u201cSelf-pay\u201d inside the Insurance carrier combobox immediately changes screen or route rather than setting a value for the existing Continue action. A keyboard search for a carrier can therefore navigate when a pinned special option is selected.",
      "expected": "Changing a selection does not change context unless users are advised before the control.",
      "impact": "Keyboard, screen-reader, and cognitive-access users can be disoriented by an unexpected context change during selection.",
      "recommended_correction": "Store the selection and require the existing Continue action, or clearly advise users before the control that selecting those options changes context immediately.",
      "verification": "Select every carrier and special option with pointer, keyboard, and screen reader and confirm context changes only as communicated.",
      "shared_owner_root_cause": "portal/app/components/onboarding-insurance-zip-lookup-screen.tsx:118-137"
    }
  ],
  "coverage": [
    {
      "id": "welcome-en",
      "status": "sampled"
    },
    {
      "id": "welcome-es",
      "status": "sampled"
    },
    {
      "id": "identity-initial-errors",
      "status": "sampled"
    },
    {
      "id": "concerns",
      "status": "sampled"
    },
    {
      "id": "service-line",
      "status": "sampled"
    },
    {
      "id": "address-errors",
      "status": "sampled"
    },
    {
      "id": "insurance-card-upload",
      "status": "sampled"
    },
    {
      "id": "consents-required-optional",
      "status": "sampled"
    },
    {
      "id": "legal-terms-dialog",
      "status": "sampled"
    },
    {
      "id": "reassurance",
      "status": "sampled"
    },
    {
      "id": "invalid-link",
      "status": "sampled"
    },
    {
      "id": "matching",
      "status": "sampled"
    },
    {
      "id": "scheduling-confirmation",
      "status": "partial"
    },
    {
      "id": "insurance-lookup-polling",
      "status": "pending"
    },
    {
      "id": "welcome-pt",
      "status": "sampled"
    },
    {
      "id": "forced-colors",
      "status": "partial"
    },
    {
      "id": "screen-readers",
      "status": "human_required"
    },
    {
      "id": "legacy-parity",
      "status": "not_exercised"
    },
    {
      "id": "insurance-lookup-combobox",
      "status": "sampled"
    },
    {
      "id": "financial-aid",
      "status": "sampled"
    },
    {
      "id": "waitlist",
      "status": "sampled"
    },
    {
      "id": "payment-stripe",
      "status": "sampled"
    },
    {
      "id": "completion-and-pending-terminals",
      "status": "sampled"
    },
    {
      "id": "insurance-manual-entry",
      "status": "sampled"
    },
    {
      "id": "insurance-polling",
      "status": "sampled"
    },
    {
      "id": "safety-review-terminal",
      "status": "sampled"
    },
    {
      "id": "rte-review-terminal",
      "status": "sampled"
    },
    {
      "id": "red-flag-terminal",
      "status": "sampled"
    },
    {
      "id": "no-cost-terminal",
      "status": "sampled"
    },
    {
      "id": "matching-filters-responsive",
      "status": "sampled"
    },
    {
      "id": "landscape",
      "status": "sampled"
    },
    {
      "id": "localized-downstream",
      "status": "partial"
    },
    {
      "id": "browser-zoom-200",
      "status": "not_exercised"
    },
    {
      "id": "voiceover",
      "status": "human_required"
    },
    {
      "id": "nvda",
      "status": "human_required"
    }
  ],
  "evidence": [
    {
      "file": "01-welcome-desktop.png",
      "caption": "English welcome page, desktop",
      "finding_ids": [
        "A11Y-001"
      ]
    },
    {
      "file": "02-identity-gate-desktop.png",
      "caption": "Identity verification page, initial state",
      "finding_ids": [
        "A11Y-001",
        "A11Y-007"
      ]
    },
    {
      "file": "03-identity-empty-errors.png",
      "caption": "Identity verification after empty submission",
      "finding_ids": []
    },
    {
      "file": "04-identity-errors-320px.png",
      "caption": "Identity errors at 320 CSS px",
      "finding_ids": []
    },
    {
      "file": "05-identity-text-spacing-320px.png",
      "caption": "Identity page with WCAG text-spacing overrides at 320 CSS px",
      "finding_ids": []
    },
    {
      "file": "06-concerns-desktop.png",
      "caption": "Concern selection page, desktop",
      "finding_ids": [
        "A11Y-001",
        "A11Y-007"
      ]
    },
    {
      "file": "07-concerns-keyboard-selection.png",
      "caption": "Concern card selected from the keyboard",
      "finding_ids": []
    },
    {
      "file": "09-consents-required-desktop.png",
      "caption": "Required-consent introduction",
      "finding_ids": [
        "A11Y-001"
      ]
    },
    {
      "file": "10-consents-optional-desktop.png",
      "caption": "Optional consent choices",
      "finding_ids": [
        "A11Y-003"
      ]
    },
    {
      "file": "11-consents-yes-keyboard-focus.png",
      "caption": "Consent Yes button during keyboard focus check",
      "finding_ids": [
        "A11Y-004"
      ]
    },
    {
      "file": "12-consents-optional-320px.png",
      "caption": "Optional consents at 320 CSS px",
      "finding_ids": []
    },
    {
      "file": "13-insurance-card-upload-desktop.png",
      "caption": "Insurance card upload controls",
      "finding_ids": [
        "A11Y-005"
      ]
    },
    {
      "file": "14-invalid-link-error.png",
      "caption": "Invalid or expired onboarding-link terminal",
      "finding_ids": [
        "A11Y-009"
      ]
    },
    {
      "file": "15-welcome-spanish.png",
      "caption": "Spanish welcome content while document language remains English",
      "finding_ids": [
        "A11Y-002"
      ]
    },
    {
      "file": "16-address-desktop.png",
      "caption": "Address form, initial state",
      "finding_ids": []
    },
    {
      "file": "17-address-errors.png",
      "caption": "Address form after empty submission",
      "finding_ids": [
        "A11Y-008"
      ]
    },
    {
      "file": "18-reassurance-reduced-motion.png",
      "caption": "Timed reassurance state under reduced-motion preference",
      "finding_ids": [
        "A11Y-018"
      ]
    },
    {
      "file": "19-unnamed-legal-terms-dialog.png",
      "caption": "Legal terms dialog without an accessible name",
      "finding_ids": [
        "A11Y-006"
      ]
    },
    {
      "file": "20-insurance-lookup-desktop.png",
      "caption": "Insurance carrier lookup, initial state",
      "finding_ids": []
    },
    {
      "file": "21-insurance-combobox-open.png",
      "caption": "Open carrier-search popup without a dialog name",
      "finding_ids": [
        "A11Y-010"
      ]
    },
    {
      "file": "22-financial-aid-desktop.png",
      "caption": "Financial-aid request state",
      "finding_ids": []
    },
    {
      "file": "23-waitlist-desktop.png",
      "caption": "Waitlist availability grid with 91 time-slot buttons",
      "finding_ids": [
        "A11Y-011"
      ]
    },
    {
      "file": "24-waitlist-forced-colors.png",
      "caption": "Selected and focused waitlist slot in forced-colors mode",
      "finding_ids": [
        "A11Y-011"
      ]
    },
    {
      "file": "25-consents-forced-colors.png",
      "caption": "Optional consent controls in forced-colors mode",
      "finding_ids": [
        "A11Y-003",
        "A11Y-004"
      ]
    },
    {
      "file": "26-waitlist-320px.png",
      "caption": "Waitlist availability grid at 320 CSS px",
      "finding_ids": [
        "A11Y-011"
      ]
    },
    {
      "file": "27-payment-handoff.png",
      "caption": "Cartwheel payment host and embedded Stripe handoff",
      "finding_ids": [
        "A11Y-012",
        "A11Y-013"
      ]
    },
    {
      "file": "28-stripe-checkout-desktop.png",
      "caption": "Loaded Stripe embedded checkout, desktop",
      "finding_ids": [
        "A11Y-012"
      ]
    },
    {
      "file": "29-finding-matches.png",
      "caption": "Finding-matches checklist with visual progress animation",
      "finding_ids": [
        "A11Y-014"
      ]
    },
    {
      "file": "30-matching-results-desktop.png",
      "caption": "Matching results with repeated time-only appointment buttons",
      "finding_ids": [
        "A11Y-015"
      ]
    },
    {
      "file": "31-schedule-profile-selected-time.png",
      "caption": "Clinician schedule with a visually selected time and profile disclosures",
      "finding_ids": [
        "A11Y-015",
        "A11Y-016"
      ]
    },
    {
      "file": "32-confirm-booking.png",
      "caption": "Booking confirmation beginning with an h2 and no h1",
      "finding_ids": [
        "A11Y-017"
      ]
    },
    {
      "file": "33-complete-terminal.png",
      "caption": "Completed onboarding terminal with booked session details",
      "finding_ids": []
    },
    {
      "file": "34-financial-aid-submitted.png",
      "caption": "Financial-aid submission terminal beginning at heading level 2",
      "finding_ids": [
        "A11Y-017"
      ]
    },
    {
      "file": "34-session-held.png",
      "caption": "Session-held terminal state",
      "finding_ids": []
    },
    {
      "file": "34-waitlist-confirmed.png",
      "caption": "Waitlist confirmation terminal beginning at heading level 2",
      "finding_ids": [
        "A11Y-017"
      ]
    },
    {
      "file": "35-welcome-portuguese.png",
      "caption": "Portuguese welcome content while the document language remains English",
      "finding_ids": [
        "A11Y-002"
      ]
    },
    {
      "file": "36-safety-review-pending-desktop.png",
      "caption": "Safety-review pending terminal beginning at heading level 2",
      "finding_ids": [
        "A11Y-017"
      ]
    },
    {
      "file": "37-rte-manual-review-pending-desktop.png",
      "caption": "Insurance manual-review terminal",
      "finding_ids": []
    },
    {
      "file": "38-red-flag-approved-desktop.png",
      "caption": "Red-flag approval re-entry state",
      "finding_ids": []
    },
    {
      "file": "39-no-cost-confirmed-desktop.png",
      "caption": "No-cost confirmation state",
      "finding_ids": []
    },
    {
      "file": "40-insurance-unverified-desktop.png",
      "caption": "Insurance unverified recovery state",
      "finding_ids": []
    },
    {
      "file": "41-insurance-manual-entry-empty.png",
      "caption": "Manual insurance entry initial state",
      "finding_ids": [
        "A11Y-020"
      ]
    },
    {
      "file": "42-insurance-manual-entry-combobox-keyboard.png",
      "caption": "Manual insurance carrier combobox under keyboard operation",
      "finding_ids": []
    },
    {
      "file": "43-insurance-manual-entry-member-error.png",
      "caption": "Manual insurance required-field error",
      "finding_ids": [
        "A11Y-020"
      ]
    },
    {
      "file": "44-insurance-manual-entry-polling.png",
      "caption": "Manual insurance polling transition",
      "finding_ids": [
        "A11Y-021"
      ]
    },
    {
      "file": "45-insurance-polling-dom-sample.png",
      "caption": "Insurance polling progress state",
      "finding_ids": []
    },
    {
      "file": "46-matching-filters-desktop.png",
      "caption": "Matching results filter controls",
      "finding_ids": []
    },
    {
      "file": "47-matching-specialty-filter-open.png",
      "caption": "Matching specialty filter popup without a dialog name",
      "finding_ids": [
        "A11Y-010"
      ]
    },
    {
      "file": "48-matching-filter-updating.png",
      "caption": "Matching results updating after filter selection",
      "finding_ids": []
    },
    {
      "file": "49-matching-landscape-667x375.png",
      "caption": "Matching results in landscape orientation",
      "finding_ids": []
    },
    {
      "file": "50-matching-narrow-320px.png",
      "caption": "Matching results at 320 CSS px",
      "finding_ids": []
    },
    {
      "file": "51-no-cost-landscape-667x375.png",
      "caption": "No-cost confirmation in landscape orientation",
      "finding_ids": []
    },
    {
      "file": "52-no-cost-landscape-focused-button.png",
      "caption": "Focused no-cost action scrolled into view in landscape",
      "finding_ids": []
    },
    {
      "file": "54-welcome-es-faq.png",
      "caption": "Spanish FAQ with an English Close accessible name",
      "finding_ids": [
        "A11Y-021"
      ]
    }
  ],
  "raw_results": [
    {
      "file": "raw/axe-complete.json",
      "description": "axe result: complete"
    },
    {
      "file": "raw/axe-concerns.json",
      "description": "axe result: concerns"
    },
    {
      "file": "raw/axe-consents-optional.json",
      "description": "axe result: consents optional"
    },
    {
      "file": "raw/axe-financial-aid-submitted.json",
      "description": "axe result: financial aid submitted"
    },
    {
      "file": "raw/axe-finding-matches.json",
      "description": "axe result for the finding-matches state"
    },
    {
      "file": "raw/axe-identity-errors.json",
      "description": "axe result: identity errors"
    },
    {
      "file": "raw/axe-insurance-combobox-open.json",
      "description": "axe result with the carrier popup open"
    },
    {
      "file": "raw/axe-matching-results.json",
      "description": "axe result for matching results"
    },
    {
      "file": "raw/axe-no-cost-confirmed.json",
      "description": "axe result: no cost confirmed"
    },
    {
      "file": "raw/axe-payment-host.json",
      "description": "axe result for the Rails payment host"
    },
    {
      "file": "raw/axe-red-flag-approved.json",
      "description": "axe result: red flag approved"
    },
    {
      "file": "raw/axe-rte-manual-review-pending.json",
      "description": "axe result: rte manual review pending"
    },
    {
      "file": "raw/axe-safety-review-pending.json",
      "description": "axe result: safety review pending"
    },
    {
      "file": "raw/axe-schedule-profile.json",
      "description": "axe result for clinician schedule/profile"
    },
    {
      "file": "raw/axe-waitlist-confirmed.json",
      "description": "axe result: waitlist confirmed"
    },
    {
      "file": "raw/axe-welcome.json",
      "description": "axe result: welcome"
    },
    {
      "file": "raw/forced-colors-observations.json",
      "description": "Computed-state observations under forced colors"
    },
    {
      "file": "raw/stripe-frame-inspection-and-axe.json",
      "description": "Stripe iframe DOM summary and iframe-scoped axe result"
    }
  ],
  "criterion_results": [
    {
      "criterion": "1.1.1",
      "name": "Non-text Content",
      "level": "A",
      "result": "fail",
      "note": "A11Y-023"
    },
    {
      "criterion": "1.2.1",
      "name": "Audio-only and Video-only (Prerecorded)",
      "level": "A",
      "result": "not_applicable",
      "note": "No prerecorded audio/video"
    },
    {
      "criterion": "1.2.2",
      "name": "Captions (Prerecorded)",
      "level": "A",
      "result": "not_applicable",
      "note": "No prerecorded media"
    },
    {
      "criterion": "1.2.3",
      "name": "Audio Description or Media Alternative (Prerecorded)",
      "level": "A",
      "result": "not_applicable",
      "note": "No prerecorded media"
    },
    {
      "criterion": "1.2.4",
      "name": "Captions (Live)",
      "level": "AA",
      "result": "not_applicable",
      "note": "No live media"
    },
    {
      "criterion": "1.2.5",
      "name": "Audio Description (Prerecorded)",
      "level": "AA",
      "result": "not_applicable",
      "note": "No prerecorded media"
    },
    {
      "criterion": "1.3.1",
      "name": "Info and Relationships",
      "level": "A",
      "result": "fail",
      "note": "A11Y-003, 005, 009, 014, 015, 017, 019"
    },
    {
      "criterion": "1.3.2",
      "name": "Meaningful Sequence",
      "level": "A",
      "result": "pass",
      "note": "Sampled reading order remained meaningful"
    },
    {
      "criterion": "1.3.3",
      "name": "Sensory Characteristics",
      "level": "A",
      "result": "pass",
      "note": "No sampled instruction relied only on sensory characteristics"
    },
    {
      "criterion": "1.3.4",
      "name": "Orientation",
      "level": "AA",
      "result": "pass",
      "note": "Matching and no-cost states worked in 667\u00d7375 landscape"
    },
    {
      "criterion": "1.3.5",
      "name": "Identify Input Purpose",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-022"
    },
    {
      "criterion": "1.4.1",
      "name": "Use of Color",
      "level": "A",
      "result": "fail",
      "note": "A11Y-003"
    },
    {
      "criterion": "1.4.2",
      "name": "Audio Control",
      "level": "A",
      "result": "not_applicable",
      "note": "No automatically playing audio"
    },
    {
      "criterion": "1.4.3",
      "name": "Contrast (Minimum)",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-024"
    },
    {
      "criterion": "1.4.4",
      "name": "Resize Text",
      "level": "AA",
      "result": "undetermined",
      "note": "Actual 200% browser zoom not verified"
    },
    {
      "criterion": "1.4.5",
      "name": "Images of Text",
      "level": "AA",
      "result": "not_applicable",
      "note": "No required images of text beyond branding"
    },
    {
      "criterion": "1.4.10",
      "name": "Reflow",
      "level": "AA",
      "result": "pass",
      "note": "Representative states passed at 320 CSS px"
    },
    {
      "criterion": "1.4.11",
      "name": "Non-text Contrast",
      "level": "AA",
      "result": "undetermined",
      "note": "Complete visual contrast inventory remains open"
    },
    {
      "criterion": "1.4.12",
      "name": "Text Spacing",
      "level": "AA",
      "result": "pass",
      "note": "Representative identity state passed spacing overrides"
    },
    {
      "criterion": "1.4.13",
      "name": "Content on Hover or Focus",
      "level": "AA",
      "result": "not_applicable",
      "note": "No triggered content in sampled task"
    },
    {
      "criterion": "2.1.1",
      "name": "Keyboard",
      "level": "A",
      "result": "fail",
      "note": "A11Y-005"
    },
    {
      "criterion": "2.1.2",
      "name": "No Keyboard Trap",
      "level": "A",
      "result": "pass",
      "note": "No trap found; sampled dialogs supported Escape/restoration"
    },
    {
      "criterion": "2.1.4",
      "name": "Character Key Shortcuts",
      "level": "A",
      "result": "not_applicable",
      "note": "No character shortcuts"
    },
    {
      "criterion": "2.2.1",
      "name": "Timing Adjustable",
      "level": "A",
      "result": "fail",
      "note": "A11Y-018"
    },
    {
      "criterion": "2.2.2",
      "name": "Pause, Stop, Hide",
      "level": "A",
      "result": "not_applicable",
      "note": "No qualifying moving content over five seconds"
    },
    {
      "criterion": "2.3.1",
      "name": "Three Flashes or Below Threshold",
      "level": "A",
      "result": "pass",
      "note": "No flashing content observed"
    },
    {
      "criterion": "2.4.1",
      "name": "Bypass Blocks",
      "level": "A",
      "result": "not_applicable",
      "note": "No repeated navigation before the primary task"
    },
    {
      "criterion": "2.4.2",
      "name": "Page Titled",
      "level": "A",
      "result": "fail",
      "note": "A11Y-001, 013"
    },
    {
      "criterion": "2.4.3",
      "name": "Focus Order",
      "level": "A",
      "result": "fail",
      "note": "A11Y-007"
    },
    {
      "criterion": "2.4.4",
      "name": "Link Purpose (In Context)",
      "level": "A",
      "result": "pass",
      "note": "Sampled links had understandable purpose"
    },
    {
      "criterion": "2.4.5",
      "name": "Multiple Ways",
      "level": "AA",
      "result": "not_applicable",
      "note": "Step-by-step process exception"
    },
    {
      "criterion": "2.4.6",
      "name": "Headings and Labels",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-015, 016"
    },
    {
      "criterion": "2.4.7",
      "name": "Focus Visible",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-004, 011"
    },
    {
      "criterion": "2.4.11",
      "name": "Focus Not Obscured (Minimum)",
      "level": "AA",
      "result": "pass",
      "note": "Sampled focused actions remained visible or scrolled into view"
    },
    {
      "criterion": "2.5.1",
      "name": "Pointer Gestures",
      "level": "A",
      "result": "not_applicable",
      "note": "No multipoint/path gesture"
    },
    {
      "criterion": "2.5.2",
      "name": "Pointer Cancellation",
      "level": "A",
      "result": "undetermined",
      "note": "Pointer event timing not exhaustively traced"
    },
    {
      "criterion": "2.5.3",
      "name": "Label in Name",
      "level": "A",
      "result": "fail",
      "note": "A11Y-005"
    },
    {
      "criterion": "2.5.4",
      "name": "Motion Actuation",
      "level": "A",
      "result": "not_applicable",
      "note": "No motion-actuated function"
    },
    {
      "criterion": "2.5.7",
      "name": "Dragging Movements",
      "level": "AA",
      "result": "not_applicable",
      "note": "No drag-only interaction"
    },
    {
      "criterion": "2.5.8",
      "name": "Target Size (Minimum)",
      "level": "AA",
      "result": "pass",
      "note": "30\u00d730 waitlist cells; undersized Back icons met spacing exception"
    },
    {
      "criterion": "3.1.1",
      "name": "Language of Page",
      "level": "A",
      "result": "fail",
      "note": "A11Y-002, 012"
    },
    {
      "criterion": "3.1.2",
      "name": "Language of Parts",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-021"
    },
    {
      "criterion": "3.2.1",
      "name": "On Focus",
      "level": "A",
      "result": "pass",
      "note": "Focus alone caused no sampled context change"
    },
    {
      "criterion": "3.2.2",
      "name": "On Input",
      "level": "A",
      "result": "fail",
      "note": "A11Y-025"
    },
    {
      "criterion": "3.2.3",
      "name": "Consistent Navigation",
      "level": "AA",
      "result": "pass",
      "note": "Repeated navigation retained relative placement"
    },
    {
      "criterion": "3.2.4",
      "name": "Consistent Identification",
      "level": "AA",
      "result": "pass",
      "note": "Repeated controls were consistently identified"
    },
    {
      "criterion": "3.2.6",
      "name": "Consistent Help",
      "level": "A",
      "result": "pass",
      "note": "Repeated help appeared after primary content"
    },
    {
      "criterion": "3.3.1",
      "name": "Error Identification",
      "level": "A",
      "result": "fail",
      "note": "A11Y-008, 020"
    },
    {
      "criterion": "3.3.2",
      "name": "Labels or Instructions",
      "level": "A",
      "result": "pass",
      "note": "Sampled inputs retained visible labels/instructions"
    },
    {
      "criterion": "3.3.3",
      "name": "Error Suggestion",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-008, 020"
    },
    {
      "criterion": "3.3.4",
      "name": "Error Prevention (Legal, Financial, Data)",
      "level": "AA",
      "result": "pass",
      "note": "Consent and booking used review/confirmation"
    },
    {
      "criterion": "3.3.7",
      "name": "Redundant Entry",
      "level": "A",
      "result": "pass",
      "note": "No unnecessary re-entry verified"
    },
    {
      "criterion": "3.3.8",
      "name": "Accessible Authentication (Minimum)",
      "level": "AA",
      "result": "pass",
      "note": "Paste/input assistance allowed; no puzzle/object recognition"
    },
    {
      "criterion": "4.1.2",
      "name": "Name, Role, Value",
      "level": "A",
      "result": "fail",
      "note": "A11Y-003, 006, 010, 014, 015, 016"
    },
    {
      "criterion": "4.1.3",
      "name": "Status Messages",
      "level": "AA",
      "result": "fail",
      "note": "A11Y-008, 014, 020"
    }
  ]
}
