Accessibility
WCAG 2.2 AA is the floor and it is enforced at the token layer, not by review. Where the system exceeds AA it says so, and where it takes a judgement call it owns it.
One focus style, system-wide
2px --color-border-focus, outline-offset: 2px, :focus-visible only. Never a glow, never a fill. The offset means the ring never sits on the element's own border, so it stays visible on a bordered input.
Contrast floors
| What | Floor | The token that holds it |
|---|---|---|
| Body text | 4.5:1 | text-muted at 4.76:1 is the floor. Nothing lighter carries text. |
| Large text (≥ 24px) | 3:1 | Not used as an excuse anywhere — headings are ink. |
| Control boundaries (1.4.11) | 3:1 | border-strong at 3.06:1 |
| Graphics carrying value | 3:1 | All five rating fills clear it; the empty star is text-muted for the same reason. |
| Hit target | 44px | --spacing-touch. WCAG asks 24px at AA; the system takes the AAA number. |
Measure any pair yourself in the contrast checker.
Never colour alone
Colour, number and word together.
A green bar alone. Nothing here survives greyscale, and nothing is announced.
ARIA patterns in use
| Component | Pattern |
|---|---|
| Rating | Native radio group in a <fieldset>. Not role="radiogroup" with hand-rolled keyboard support — no ARIA beats bad ARIA. |
| ScoreBar | role="meter" with aria-valuetext carrying number and word. |
| Dialog | Focus trapped, Esc closes, focus returns to the trigger. Title is the accessible name. |
| Toast | Polite live region; errors are assertive and are never the only place a failure is reported. |
| Input error | aria-invalid + aria-describedby; post-submit messages use role="alert" so they announce without stealing focus. |
| Steps | aria-current="step" on the current marker. |
Forced colors
In Windows High Contrast Mode every colour we set is discarded. Two consequences the system handles: star state is carried by two different glyphs, not two colours; and selected states use the forced-selected utility, which maps to the system Highlight / HighlightText keywords. System colour keywords cannot be tokens, so they live in a named utility rather than as an arbitrary value on a component.
Motion
The skeleton pulse and every entrance animation sit behind motion-safe:. Nothing in the system animates transform except the bottom sheet and the button spinner, so a reduced-motion user loses decoration and keeps meaning.
theme.css.