Accessibility contrast matrix · focus rings · motion · semantics

Read it or write it again.

Accessibility is a typography problem first and a contrast problem second. Then it's a focus problem. Then a motion problem. We treat it as a property of the system, not a checklist on the way out.

§A · Contrast matrix · every sanctioned pair · WCAG 2.1
Pair
Text
Background
Ratio
Grade
Use
ink / ivory (default body)
The shape
var(--backgroundColor)
17.41:1
AAA
body
ink / ivory-2 (in a card)
The shape
color-mix(in srgb, var(--color) 5%, var(--backgroundColor))
16.32:1
AAA
body in cards
grey-500 / ivory (asides, captions)
An aside
var(--grey-500)
6.10:1
AA
captions, meta
grey-500 / ivory-2
An aside
color-mix(in srgb, var(--color) 5%, var(--backgroundColor))
5.72:1
AA
captions in cards
red / ivory (signal channel)
● live
#c8102e
5.21:1
AA
labels, signal
red / ivory-2
● live
color-mix(in srgb, var(--color) 5%, var(--backgroundColor))
4.88:1
AA
labels in cards
ivory / ink (reverse · button)
Run simulation
var(--color)
17.41:1
AAA
primary cta
grey-300 / ink (dark theme aside)
An aside
var(--grey-300)
6.99:1
AA
caption on ink
red / ink (red on dark)
● live
var(--color)
3.35:1
AA · LG
≥18px / ≥14px·700
grey-300 / ivory (banned for text)
illegible
var(--grey-300)
2.49:1
FAIL
hairlines only
grey-500 / ink (banned for text)
illegible
var(--color)
2.85:1
FAIL
no text
Body text and signals all pass AA against both the ivory and ivory-2 surfaces. Red on ink only clears AA-Large, so on dark themes the red signal must be ≥18px or ≥14px bold — that's why the dark deck slides set their red mono caps at 18px. Grey-300 exists only for hairlines and disabled state; never use it for text.
§B · Focus · always 2px red, 2px offset · :focus-visible only
§B.1 · resting

No outline at rest.

Mouse users never see the ring. It would be visual noise for an interaction the cursor already explains.

§B.2 · keyboard-focused

2px red ring, 2px offset.

Keyboard and screen-reader users get an unmistakable signal — the only red outline anywhere in the system.

:focus-visible is the rule. :focus alone is banned (it fires on mouse click and produces noise). Inputs on a hairline border get a 2px red bottom-shadow; surfaces get a 2px red outline with a 2px offset.
§C · Skip link · first stop in the tab order

The first focusable thing on every page. Hidden until :focus-visible; then it lands at top-left, in mono caps, on ink:

Skip to main content
§D · States · what an interactive element looks like in each register
Element
Rest
Hover
Focus (kbd)
Disabled
PRIMARY
GHOST
INPUT
LINK
methodology
methodology
methodology
methodology
Disabled state lowers opacity to 0.4 and switches stroke to grey-300 — never grey-500 (which would still pass AA and make the control look dimmed but real). Disabled means dead.
§E · Motion · the prefers-reduced-motion contract
SPINNER 900ms continuous rotate (default) slows to 4s rotation when prefers-reduced-motion  
LIVE DOT 1.4s pulse · opacity 1 → 0.4 → 1 animation: none · stays at full opacity
SKELETON shimmer · 1.4s linear, infinite static --ivory-2 block (no shimmer)
DECK SLIDES 280ms ease-out cross-fade instant cut · no fade
CHART REVEALS staggered 60ms per series all series appear together · static
SCROLL native browser smooth-scroll if user has it never scroll-behavior: smooth in our CSS
Every animation in the system has a @media (prefers-reduced-motion: reduce) branch. We never remove the affordance — a spinner still spins, just slower. The brand stays recognizable; the strobe goes away.
§F · Semantics · what the markup tells the screen reader

§ Required

Headings nest. One h1 per page. Sections use h2 through h4 in order.

Landmarks. main, nav, aside, footer on every page. Skip link targets main.

Labels are real. <label for> on every input. Placeholder is never the label.

Modals trap focus. Tab cycles inside; Esc closes; focus returns to the trigger.

Live regions. Toasts post into role="status" for info/success and role="alert" for warn/error.

Icons are aria-hidden="true" when redundant with text. Standalone (close, search) carry role="img" + <title>.

§ Banned

A div with a click handler — use a button

Color as the only signal — every red item also carries a label, dot, or icon

outline: none without a replacement

Scroll-jacking, parallax, autoplay video

Tooltips for primary content — they're keyboard-hostile

Placeholder text doing the job of a label

Tabindex values higher than 0

Modal opens and focus stays on the page underneath

Body AAA on both ivory surfaces  ·  Captions AA · grey-500 only  ·  Red on ink AA-Large · ≥18px or ≥14px·700  ·  Focus 2px red · 2px offset · :focus-visible only  ·  Motion every animation honors prefers-reduced-motion  ·  Disabled opacity 0.4 + grey-300 stroke  ·  Doc A11Y.md