The automated gates (web/a11y.py in CI, blocking pa11y/axe) check structure and
computed contrast. They cannot check the lived experience: whether the page is
actually operable and comprehensible with a screen reader and a keyboard. This
file is the record of that human step. Until the result table below is filled in
by a person, the demo is accessibility-reviewed by automation only, and the
README and model card say so.
Test the deployed demo (or web/index.html served locally) with at least one
screen reader and keyboard-only, at default zoom and at 400%. Record the date,
the tool and version, and the outcome of each item. File a follow-up issue for
anything that fails and link it here.
Recommended coverage: NVDA + Firefox (Windows) and VoiceOver + Safari (macOS or iOS). One desktop and one mobile pass is the minimum for a phone-first civic audience.
Four pages are public, not one. The checklist below is written against the
chat page, but /embed, /offline, and /guide are equally reachable, and the
last two exist for riders with no signal at the stop or who would rather browse
than type — the audience least able to route around a problem. At minimum, walk
/offline and /guide for heading navigation and reading order (they are long
documents, so heading structure is the whole navigation story) and /embed for
keyboard operability inside an iframe, which is the one context the standalone
page never exercises. As of 2026-08-05 all four are covered by the structural
gate (python -m web.a11y), and as of 2026-08-12 all four are also covered by
the merge-blocking browser gate (axe + HTML CodeSniffer at WCAG2AA), which until
then had only ever loaded the chat page. All four pass both runners. None of
them has had a human pass.
Each item is pass / fail / not-tested with a note. Items marked (auto) are also covered by the static gate; they are listed so the manual pass confirms the automation did not miss the real behavior.
- Every control (text box, Ask, examples, text-size A/A+/A++, high contrast, Yes/No feedback, Start over) is reachable and operable by Tab and Enter.
- Focus order is logical and visible (the focus outline is the 4px ring with a 3px offset).
- No keyboard trap anywhere in the form or transcript.
- After submitting a question, focus moves to the new answer turn so a
keyboard user lands on the response (the page sets
tabindex=-1and callsfocus()on each turn; confirm it actually lands).
- The page title and the single
h1are announced; heading order (h1 then the card h2s) reads sensibly. (auto: structure) - The "What it will not do" list is announced before the input, so the limits are heard, not skipped.
- The status line ("Looking through the published policies…", errors) is
announced through the polite live region without stealing focus. (auto:
role=status,aria-live=politepresent) - A new answer is announced or is reachable immediately after it arrives.
- Citations read as usable source references, not a wall of brackets: each "AGENCY: Title (fetched DATE)" link is announced with its agency and title. (this is the R1a-3 item the static gate cannot verify)
- The "Based on policies published as of DATE" line and the "Fetched N days ago" staleness note are announced and understandable.
- The feedback control announces its purpose ("Was this helpful?", Yes/No
with
aria-label). - Spanish answers carry
lang="es"so the screen reader switches voice.
- Text-size controls (A / A+ / A++) visibly scale the page and persist on
reload; the pressed state is announced (
aria-pressed). - High-contrast toggle increases contrast and persists; pressed state announced.
- The page reflows without horizontal scrolling at 400% zoom (1.4.10). (auto: zoom not disabled)
- Contrast meets AA in both the default and high-contrast themes. (auto: blocking axe/pa11y)
- Target sizes are comfortable on a phone. (auto: 24px minimum in CSS)
This section is a head start for the human pass, not a substitute for it. It
records what could be confirmed by reading web/index.html and running the
static gate (python -m web.a11y, green on this commit). Every row a real
screen reader must judge — whether an announcement is actually heard and makes
sense — is left explicitly for the human pass. Do not promote any "needs
human" row to pass without an actual screen-reader session.
Keyboard
- Verified by code: every control is a native
<button>,<textarea>, or<a>— focusable and Enter/Space-operable without extra ARIA. Focus order follows DOM order, which matches the visual order (banner → h1 → display settings → "will not do" → ask form → examples → status → transcript). The focus ring isoutline: 4px solid #1d4ed8withoutline-offset: 3pxvia:focus-visible. (This note read "3px" until 2026-08-05. The page had since moved to a thicker ring with an offset, which is the stronger result for 2.4.13 Focus Appearance — but a pre-audit that describes code the repo no longer ships is worth nothing to the person doing the human pass, so every claim in this section was re-verified against HEAD on 2026-08-05 and this one was the only drift found.) No script installs a focus trap. On each new answer the code setstabindex=-1on the turn and callsfocus(), so a keyboard user is moved onto the response. - Needs human pass: confirm the focus visibly lands on the new turn and that the ring is perceivable at 400% and in high-contrast mode.
Screen reader
- Verified by code (structure): one
<h1>, card<h2>s, structured-answer<h3>s — no skipped levels. The "What it will not do"<section>precedes the ask form in the DOM. Status isrole=status aria-live=polite. Answer turns carrylang(ans.setAttribute("lang", data.language)) so a Spanish answer can switch voice. Feedback buttons havearia-label("Yes, helpful" / "No, not helpful") beside a "Was this helpful?" label. Citations render as<a>AGENCY: Title</a> (fetched DATE)inside a<ul>. Minor finding (not a failure): the "Sources" caption is aFixed 2026-08-05. It is now an<strong>, not a heading, so it is not a screen-reader heading-nav target.<h3>onweb/index.html(under the "Conversation"<h2>) and an<h2>on/embed(whose only other heading is its<h1>, so an<h3>would skip a level), styled to keep the inline bold caption's appearance. Where an answer came from is exactly what a screen-reader user goes looking for, and heading navigation is how they look; the recorded transcript the independent audit grades already used<h3>Sources</h3>, so the page riders actually use was the less accessible of the two. The human pass should still confirm the sources are easy to find by heading navigation, which is the part only listening can settle.- Needs human pass: whether the moved focus actually announces the new answer;
whether the citation list reads as usable references rather than a bracket
wall; whether the polite live region announces status without stealing focus;
and whether the Spanish
langactually flips the voice.
Low vision / zoom / contrast
- Verified by code: the page is rem/em-based with
html.tsize-large(112.5%) andhtml.tsize-xlarge(125%), toggled witharia-pressedand persisted inlocalStorage; high contrast togglesbody.contrast(which only deepens colours), alsoaria-pressedand persisted. Viewport allows zoom (nouser-scalable=no). Target sizes: primary controlsmin-height: 2.5rem(40px), secondary1.75rem(28px) — both ≥ the 24px 2.5.8 minimum. Static contrast is covered by the blocking axe/pa11y gate. - Needs human pass: reflow with no horizontal scroll at 400%, and a subjective contrast/readability check in both themes on a real device.
| Date | Tool + version | Platform | Pass / fail summary | Follow-ups |
|---|---|---|---|---|
| 2026-07-11 | source inspection + web.a11y static gate |
n/a (code review) | Structure, keyboard wiring, ARIA, lang, target sizes, persistence all correct by inspection; static gate green. Screen-reader listening items not yet performed. |
Human SR pass still required (rows above) |
| 2026-08-05 | source inspection + widened web.a11y static gate |
n/a (code review) | Re-verified the 2026-07-11 claims against HEAD: one drift (focus ring, corrected above). Fixed the open "Sources is a <strong>" finding on both answering surfaces. Widened the gate from web/index.html to all four public pages; /embed, /offline, and /guide were previously unchecked and all three pass. Still no screen reader was used — every listening row is untouched. |
Human SR pass still required, now across four pages |
| 2026-08-12 | pa11y 9.1.1, runners axe + htmlcs (headless Chrome) | n/a (automated, CI) | Widened the browser gate from web/index.html to all four public pages and added HTML CodeSniffer alongside axe, so eight page/runner combinations now gate every PR. /embed, /offline, and /guide had never been loaded in a browser by any check. All four pass both runners with zero violations — no defects to fix, so the value here is regression protection, not remediation. Still no screen reader was used; every listening row is untouched. |
Human SR pass still required across four pages |
| pending | NVDA/Firefox or VoiceOver/Safari | desktop + mobile | not yet performed |
Until a row here records a real screen-reader pass, do not present the demo as production-ready for accessibility.