# Accessibility

Everything the system already does for a person who cannot use it the ordinary way, with the way to
check each one. **Nothing here is new.** These are the rulings taken while the tokens, the responsive
ladder and the motion register were built; this file collects them and gives each a named instrument.

**"Confirmed" means a run on 2026-08-23, not a memory that an earlier stage did it.** A stage's
finding is true as of that stage's tree, and this tree has grown since. Where a row could not be run
now, it says **debt** and names the row in `docs/backlog.md` that carries it. **Nothing is repaired
here**: a product edit after the tree was accepted cancels every comparison the acceptance stood on.

**A row whose check cannot be named never gets a status.** A point that cannot be verified is neither
a confirmation nor a plan; it looks like finished work and is not. There are no such rows below.

**AND THE ONE ROW THAT CARRIED A STATUS FROM SOMEBODY ELSE'S RUN WAS WRONG, WHICH IS WHY THE RULE IS
WRITTEN THE WAY IT IS.** (It is confirmed now, by a run; what follows is how it was found.) Row 13 said confirmed by an earlier stage and named the instrument rather
than running it, on the argument that headings are a structural property and this stage does not read
the structure tree. That argument is sound and the status was still a status nobody had earned:
**four documents have no level-one heading at all** and every instrument here passed them. It was
found by a reader with no context building a feature, on 2026-08-23, which is the one instrument that
walks the path nobody who built the package walks any more. The row is a debt now and carries its
measurement. **A status inherited from an earlier tree is a status about that tree.**

---

## The run

Chromium 151 and WebKit 26.5, from disk over `file://`, which is the protocol these documents are
read at and the one that has caught three defects here that a server hid. Nine screens for contrast
and focus, four for the touch floor, seventeen widths at three browser default sizes for the ladder,
one screen for motion. Transition and animation frozen before any probe that writes CSS, and two
animation frames of settle after every viewport change.

**Four controls had to come back positive before any finding was believed.** Each is here because
the equivalent reading has been wrong in this repository before.

| Control | Why | Result |
|---|---|---|
| The theme control: the page ground must DIFFER between the two runs | these screens boot the theme from storage and the boot script REMOVES the attribute when the key is absent, so setting it is not setting the theme; a whole sweep once came back with daylight identical to the Vault | **passed**, the two grounds differ on every screen and in both engines |
| The motion control: a rule the token override cannot reach must be SEEN disobeying | a reading that cannot come back red is a reading of the guard | **passed** in both engines, in both the normal and the reduced run |
| The pointer control: `(pointer:coarse)` asserted IN the page | the touch floor lives entirely inside that query and a headless browser is `pointer:fine`, so a sweep without it measures a product with the floor switched off | **passed**, `true` in every touch run |
| The focus control: an element the system does not style must still show a ring change | proves the probe can see a ring at all, so a clean result is not a blind one | **passed** |

---

## The checklist

| # | The point | Where it is in the code | How to check it | Status |
|---|---|---|---|---|
| 1 | Every focusable control shows a visible focus ring, in both themes | `base.css`, the `:focus-visible` floor, drawn from one role | TAB to one control of each type and read `matches(':focus-visible')` and the computed outline and shadow before and after, in both themes | **confirmed.** Links, buttons, fields and disclosures: all four types answer, in both themes and both engines |
| 2 | The ring is one token, not a colour per component | the focus role in `components/tokens.css`, resolving to one brass primitive | read the role off the root element, then invert it: `map.md` shows it as one of four roles carrying that primitive across 26 components | **confirmed.** (The pack for this stage names this token `--color-focus`; in this system it is the focus-ring role. Same thing, different name.) |
| 3 | Text meets its contrast threshold on the thing that IDENTIFIES the control | every colour pair is a role in `components/tokens.css`, applied by the component | computed `color` against the first painted ancestor ground, WCAG formula, both themes, thresholds by surface: ink, large ink | **confirmed. 12 identifying families, 9 screens, both themes, both engines: 0 under threshold.** Worst in the Vault and worst in Daylight both clear the ink threshold with room |
| 4 | An inactive control is not held to that threshold, and is measured anyway | the held Confirm in the bet panel, `button` primary variant | count inactive controls apart from live ones and report the worst separately | **confirmed.** They are exempt by the criterion itself, and the one in the tree clears the live threshold in both themes regardless |
| 5 | The responsive ladder is in `rem`, so it moves with the reader's own default | the three rungs in `components/tokens.css`, page frame | set the browser's default font size through the devtools protocol, never `html{font-size}`, which a media query ignores by definition; then find the width at which the navigation carrier changes hands | **confirmed, and it moves exactly as declared.** At the ordinary default the handover is at the desk rung; raise the default by half and it lands at the width the ratio predicts. A reader who enlarges type keeps one column for longer instead of being handed a desktop layout at phone width |
| 6 | Exactly one navigation carrier at any width | the bottom bar and the header, each behind one rung | walk the rungs and one pixel either side, count visible carriers at each | **confirmed. 306 renders over 17 widths, 3 default sizes and 6 screens: exactly one carrier at every width and 0 documents scrolling sideways** |
| 7 | Primary navigation is a landmark a screen reader can jump to | the bottom bar carries `nav` with a primary label; the account destinations carry one in the header | list every `nav` and its label at each width, and check one is primary | **confirmed, and it was a debt until 2026-08-23.** Below the desk rung the bottom bar is the primary landmark. Above it the bar goes and the header takes over, and the header's destinations were a `role="menu"` dropdown, which is no landmark: a reader navigating by landmark found categories, the footer columns and legal and no primary. They are a labelled navigation landmark now on 149 documents, present at every width with all six destinations, and the false `role="menuitem"` went with it. `docs/backlog.md` 232 |
| 8 | Reduced motion is answered by re-declaring the tokens, not by a blanket rule | the reduced-motion block in `components/tokens.css` | emulate the setting, read the computed transition and animation durations over the whole document, with a control proving a disobeying rule is visible | **confirmed. Moving elements go to 0, and the control was seen in both runs.** The blanket `*` net was deleted on purpose: under `!important` on `*` a component that reads no token is indistinguishable from one that reads every token, and the check could never fail |
| 9 | A finger target meets the touch floor | one rule in `base.css`, inside the coarse-pointer query, as a `max()` so it raises a short control and never lowers a tall one | emulate touch AND assert the coarse-pointer branch is on in the page, then measure every interactive box, **and measure the element a finger actually hits rather than the one the markup names** | **confirmed, and the debt this row carried was two mismeasurements.** The filter toggle is a checkbox at 1x1 by design and **its label is 44x44**, so the target was always the label. The three icon tiles on the event detail are 28x28 and are one of the four exclusions `base.css` declares by name with its reason: **28 clears the criterion at AA, which asks 24**; 44 is the AAA figure and that question is the one the rule already points at. Nothing was resized. `docs/backlog.md` 233 |
| 10 | The skip link is the first tab stop and becomes visible when focused | `base.css` | focus it and read its colour and box, on every screen | **confirmed on 100 of 119.** One screen has none. The other 19 are the screens that ship with a modal dialog open, which makes the document behind it inert: the skip link correctly cannot be reached there |
| 11 | On a screen that opens with a modal, focus starts inside it and can leave it | the four shared dialogs, plus one outcome dialog on the overlay families | TAB from load and confirm the first stop is inside the dialog; press Escape and confirm it closes; **and test whether an element BEHIND can take focus, which is the question, rather than whether Tab ever reaches `body`, which is the browser chrome** | **confirmed 2026-08-23, and it was already right.** This row said 19 screens; there are **24**, all opened with `showModal()`. Both engines: focus lands inside on 24 of 24, **no element behind can take focus on any of them**, focus wraps back into the dialog after 40 tabs on 24 of 24 in Chromium and 23 in WebKit, and Escape closes on 24 of 24. **The probe manufactured a defect twice before it got there**, which is why the check above names the test that decides it |
| 12 | Green and red carry outcome and never status | the outcome roles in `components/tokens.css` | read the roles and check no error or success surface uses them | **confirmed by construction and by the contrast run**: the error tone is a border role, a mark and a weight on the ordinary ground, so a reader who cannot separate the two hues loses nothing that carries meaning |
| 13 | One heading level one per screen, no level skipped | decided in the structure layer, carried into the paint | count the level-one headings of every document in BOTH trees; the answer is one everywhere or the row is not closed | **confirmed 2026-08-23, and it was a debt for one day.** Four documents had none: the event detail's loading state, signed in and signed out, in both trees, where the heading went out with the question a skeleton replaced. The screen-reader line those pages already carried is the heading now, one string in both roles, with no new class and no new rule. **239 of 239 documents in both trees carry exactly one.** `docs/backlog.md` 240 |

---

## And the package's own two pages were read the same way

**A document that publishes an accessibility checklist is a document somebody will check.** So
`index.html` and `handoff/handoff.html` were put through the same instrument as the product on
2026-08-23, and they failed: the course chrome's muted role measured **3.36** against the page ground
and **3.15** against the plate, under the ink threshold, and it carried the table headers, the small
print and the stage tag on both pages.

**It was one value in a palette that fourteen documents copy**, so fixing two of them would have been
drift by this repository's own definition. All fourteen were swept to a value that clears the
threshold on both grounds, and re-read on both engines: **seven documents are clean now, these two
among them.** The other seven still carry **301 elements** under the criterion and they are not the
palette at all: they are greys written as literals inside individual documents, past their own token.
That is `docs/backlog.md` 244, filed and not swept, because it is a judgement per document rather
than one value.

**This is the only repair this stage made**, and it is here rather than in the product: the course
chrome is not `ui-visual/`, `wireframes/`, `components/` or the stand, it links neither the system
nor its base, and no product rule or product sweep has ever reached it.

---

## The debts, and all four are closed

Filed on 2026-08-23 by this stage, which does not repair the product, and closed later the same day
by a fix pass the user asked for. **Three of the four closed by measurement rather than by an edit**,
which is the more useful half of the record: the instrument had been wrong, not the product.

| What | How it closed |
|---|---|
| Primary navigation had no landmark label at and above the desk rung | **an edit.** The account destinations are a labelled navigation landmark on 149 documents, and the `role="menu"` they carried, which promises arrow-key semantics this product does not implement, went with it |
| Four controls under the touch floor with the coarse branch on | **a measurement.** The filter toggle's target is its label, which is 44 square; the three tiles are 28 and clear the criterion at AA, and are a declared exclusion with its reason written. Nothing resized |
| Focus placement and Escape on the screens that open with a modal | **a measurement.** 24 screens, not 19. Focus lands inside on all of them, nothing behind can take focus, focus wraps back, and Escape closes, on both engines |
| Four documents with no level-one heading | **an edit.** The screen-reader line those pages already carried is the heading now: one string, both roles, no new class |

**The pattern in the three measurements is one sentence.** A checkbox is not the target its label is, a
declared exclusion is not an oversight, and Tab reaching `body` is the browser's chrome and not the
page behind. **Every one of the three would have shipped as a repair to something that was not
broken**, which costs more than the defect it imagines.

## What this file does not own

Which colour is which, in `components/tokens.css` and `DESIGN.md`. What each control IS, in
`ui-kit/docs/inventory.md`. What the product does, in `behaviour.md`. What moves when a token
changes, in `map.md`. The open rows themselves, in `docs/backlog.md`.
