# A11Y.md
## Accessibility doctrine

> Accessibility is a typography problem first, a contrast problem second, a focus problem third, a motion problem fourth. Treat it as a property of the system, never a checklist on the way out.

The full live reference is `preview/a11y.html`. This file is the floor — what every page, screen, and component must satisfy.

---

## §1 · Contrast (WCAG 2.1)

The contract pair `var(--color)` / `var(--backgroundColor)` varies per theme, so contrast ratios aren't fixed numbers — they're per-(theme, mode) outcomes. `tests/theme-contrast.test.mjs` enforces the floor across every (theme, mode) pair in the registry:

| Token | Use | Floor | Enforcement |
|---|---|---|---|
| `var(--color)` on `var(--backgroundColor)` | body, primary text | **4.5:1** WCAG AA | matrix test, no allowlist |
| `var(--grey-500)` on `var(--backgroundColor)` | captions, mono meta | **3.0:1** WCAG AA-Large | matrix test, allowlist for saturated themes |
| `var(--red)` (`#c8102e`) | gap signal, errors, deltas | **3.0:1** AA-Large; large-text only against dark fg | by doctrine — see DOCTRINE.md§3.2 |
| `var(--grey-300)` | hairlines, disabled state | n/a — never used for type | doctrine |

**Rules:**
- Body text uses `var(--color)`. Never lighter. The matrix test fails if any theme misses 4.5:1.
- Captions stop at `var(--grey-500)`. On three saturated themes (`zyg`, `peacock`, `figma`) the 50% sRGB blend can't reach 3.0:1 — caption-weight text on those themes must be sized AA-Large (≥14px bold or ≥18px) or use `var(--color)` directly. CONTRACT.md§Theme Registry lists the allowlist.
- Grey-300 is for hairlines and the disabled state. Never set grey-300 type.
- On dark themes, red mono caps must be ≥18px or ≥14px bold — that is why deck dark slides set them at 18px.
- Color is never the only signal. Every red element carries a label, dot, or icon.

### Themed surfaces — automated contrast floor

`tests/theme-contrast.test.mjs` enforces the matrix on every (theme, mode) pair:

- **Body text** (`--color` vs `--backgroundColor`) ≥ 4.5:1 (WCAG AA) for **every** (theme, mode) pair. No allowlist permitted — any new theme must pass naturally or stay out of the registry.
- **`--grey-500` captions** (50% sRGB mix vs `--backgroundColor`) ≥ 3.0:1 (WCAG AA-Large).

The grey-500 floor has a small allowlist for **saturated chromatic themes** (`zyg`, `peacock`, `figma`) where the darkColor and lightColor sit in similar luminance bands — the 50% mid-blend can't reach 3.0:1 against either endpoint by construction. On those themes:

- **Captions must be AA-Large size** (≥18px or ≥14px bold), or
- **Use `var(--color)` directly** for caption-weight text.

Body text on those themes is unaffected — it uses `var(--color)` and passes 4.5:1.

---

## §2 · Focus

- `:focus-visible` only. `:focus` alone is banned.
- 2px solid red outline, 2px offset.
- Inputs on a hairline use a 2px red bottom shadow instead of a wrap-around outline.
- Disabled = opacity 0.4 + grey-300 stroke. Never grey-500 (still passes AA, looks merely dim).
- Skip link is the first focusable thing on every page. Hidden until focus, then ink chip top-left.

---

## §3 · Motion

Every animation has a `@media (prefers-reduced-motion: reduce)` branch.

| Element | Default | Reduced |
|---|---|---|
| Spinner | 900ms rotate | slows to 4s |
| Live dot | 1.4s pulse | static, full opacity |
| Skeleton | 1.4s shimmer | static block |
| Slide transitions | 280ms ease-out cross-fade | instant cut |
| Chart reveals | 60ms stagger per series | all together |
| Scroll | native browser only | never `scroll-behavior: smooth` |

We never *remove* the affordance — a spinner still spins, just slower. Brand recognizable, strobe gone.

---

## §4 · Semantics

**Required**
- One `h1` per page; `h2`–`h4` nest in order.
- Landmarks on every page: `main`, `nav`, `aside`, `footer`.
- Real `<label for>` on every input. Placeholder is never the label.
- Modals trap focus, Esc closes, focus returns to the trigger.
- Toasts post into `role="status"` (info/success) or `role="alert"` (warn/error).
- Icons: `aria-hidden="true"` when redundant with text; `role="img"` + `<title>` when standalone (close, search, live).

**Banned**
- `div` with a click handler — use `button`.
- `outline: none` without a replacement.
- Scroll-jacking, parallax, autoplay video.
- Tooltips for primary content (keyboard-hostile).
- Placeholders carrying the label.
- `tabindex` values > 0.
- Modals that don't move focus.

---

## §5 · Testing checklist

Before shipping any page, screen, or component:

1. Tab through it with the keyboard alone. Is the tab order logical? Does focus stay visible? Does Esc close every dismissible thing?
2. Toggle "Reduce motion" in the OS. Does anything still strobe, jitter, or auto-play?
3. Zoom to 200%. Does the layout reflow without horizontal scroll?
4. Inspect color tokens. Is any text drawn in grey-300, or in grey-500 on ink?
5. Run the page through a screen reader. Does the heading outline read as the document outline?
6. Check the contrast pairs against §1. Reject any pair not on the list.

If you can't answer all six "yes," the page is not ready.

---

*Companions: `preview/color-system.html` (the palette), `preview/components-feedback.html` (states), `preview/components-overlays.html` (focus traps), `preview/iconography.html` (`aria-hidden` rules).*
