# 12 · Accessibility

> Target: **WCAG 2.1 AA**. Not because a regulation says so — because the
> audience is an owner-operator reading a phone in daylight at the end of a long
> day.

---

## 12.1 Contrast

Every ratio below is computed from `tokens/tokens.json` at build time and
rendered live in the [colour explorer](/color/).

### Text on light surfaces

| Pairing | Ratio | Grade |
|---|---|---|
| `ink` on `paper` | 16.14:1 | AAA |
| `ink-body` on `paper` | 13.14:1 | AAA |
| `ink-muted` on `paper` | 5.16:1 | AA |
| `ink-muted` on white | 5.61:1 | AA |
| `ink-muted` on `mist` | 4.87:1 | AA |
| **`accent` `#C4602A` on white** | **4.16:1** | **✗ Fail** |
| `accent-ink` `#A34A1F` on white | 5.90:1 | AA |
| `accent-ink` on `paper` | 5.43:1 | AA |
| `accent-ink` on `accent-light` | 4.73:1 | AA |

### Buttons

| Pairing | Ratio | Grade |
|---|---|---|
| **white on `accent` `#C4602A`** | **4.16:1** | **✗ Fail** |
| white on `accent-ink` `#A34A1F` | 5.90:1 | AA |
| white on `accent-hover` `#A84E22` | 5.56:1 | AA |

### Dark

| Pairing | Ratio | Grade |
|---|---|---|
| `on-ink` on `ink` | 15.60:1 | AAA |
| `accent-lift` `#E07A38` on `ink` | 5.86:1 | AA |
| `accent` `#C4602A` on `ink` | 4.23:1 | ✗ Fail for body |
| `dark.muted` `#98938C` on `ink` | 5.76:1 | AA |
| `#7A7570` on `ink` | 3.85:1 | ✗ Fail for body |

### Status pairings

All six clear AA: success 5.43:1 · attention 4.98:1 · warning 6.25:1 ·
error 8.98:1 · info 7.88:1.

### The two rules that follow

1. **`#C4602A` never carries letterforms on a light surface.** Use `#A34A1F`.
2. **`#C4602A` never carries letterforms on a dark surface either.** Use `#E07A38`.

---

## 12.2 Non-text contrast

WCAG 1.4.11 requires **3:1** for UI component boundaries and meaningful graphics.

| Pairing | Ratio | Verdict |
|---|---|---|
| `border-strong` `#D8D5D0` on white | 1.46:1 | Decorative only |
| `border` `#E5E2DE` on white | 1.29:1 | Decorative only |
| `accent` focus ring on `paper` | 3.82:1 | ✅ Passes |

**Neither border colour identifies a control on its own.** That is acceptable
under the standard *provided* the control is identifiable another way — and in
this system it always is:

- Inputs have a persistent visible label and a 3.82:1 focus ring.
- Buttons have a fill (`primary`, `destructive`) or a text colour clearing 4.5:1 (`secondary`, `ghost`).
- Cards are containers, not controls.

**Do not** ship a control whose only affordance is a `#D8D5D0` hairline. If you
need a boundary to carry meaning by itself, darken it to at least `#949089`.

---

## 12.3 Focus

- One ring everywhere: `2px solid #C4602A`, `outline-offset: 2px`.
- Global in `tokens/since-brand.css` on `*:focus-visible`.
- **Never `outline: none`** without an equivalent replacement in the same rule.
- The ring appears **instantly** — never transition a focus outline.
- Focus moves into a dialog on open and returns to the trigger on close.
- Tab order follows visual order.

---

## 12.4 Semantics

| Requirement | Detail |
|---|---|
| One `h1` per page | The `PageHeader` title. Never skip levels |
| Landmarks | `<main>`, `<nav aria-label>`, `<aside>`, `<header>` |
| Buttons vs links | A link navigates; a button acts. `<div onClick>` is a bug |
| Labels | Every input has a real `<label for>` |
| Tables | `scope="col"`, and a visually hidden `<caption>` |
| Lists | Real `<ul>`/`<ol>`. Stat tiles are `<dl>` with `dt`/`dd` |
| Icons | `aria-hidden="true"`; the label goes on the control |
| Dialogs | `role="dialog"`, `aria-modal="true"`, `aria-label` |
| Current page | `aria-current="page"` on the active nav row and last crumb |
| Live regions | `role="alert"` on error and warning alerts only |
| Skip link | First focusable element on the page |

---

## 12.5 Colour independence

WCAG 1.4.1 — colour is never the only signal.

- Status chips carry a **dot and a word**.
- Chart series are **labelled directly**, not only via a legend swatch.
- Errors carry **text**, not just a red border.
- Required fields are marked in **text**.
- Links in prose are **underlined**, not colour-only.

Test: screenshot the page, desaturate it, and check every state is still
readable.

---

## 12.6 Motion, targets, zoom

- `prefers-reduced-motion: reduce` collapses every transition to 0.01ms. Ships in the token CSS.
- Hit areas are **44×44px minimum**, including padding.
- The layout must survive **200% zoom** and a **375px** viewport without horizontal page scroll. Wide tables scroll inside their own box.
- Text must reflow at 320px CSS width (WCAG 1.4.10).

---

## 12.7 Language and content

- `<html lang="en">`; `lang="fi"` on Finnish passages.
- Link text makes sense out of context — never "click here", never a bare URL.
- `alt` describes the *information*, not the picture. Decorative images get `alt=""`.
- Sentence case is easier to read than Title Case for people with dyslexia — another reason for the rule in [verbal identity](/guide/verbal-identity/).

---

## 12.8 Review checklist

Before shipping any surface:

- [ ] Tab through it. Every control reachable, focus always visible, order sensible
- [ ] Desaturate it. Every state still legible
- [ ] Zoom to 200%. Nothing clipped, no horizontal page scroll
- [ ] 375px viewport. Tables scroll in their box; modals fit
- [ ] Every input has a visible label
- [ ] Every icon-only button has `aria-label`
- [ ] One h1; heading levels do not skip
- [ ] Error messages say what to do
- [ ] Text contrast ≥ 4.5:1; boundaries that carry meaning ≥ 3:1
- [ ] Reduced motion honoured
