Accessibility
Vermilion and Moss are never the only signal. Every state has a glyph and a label, every control has a 44px hit area, every animation has a still version. The numbers below are computed from the token file when this page loads.
Milo is named after a golden lion tamarin who had one leg and was the fastest of the tamarins. The belief that follows is circumstance should not define capability — and physical circumstance is the first row of that table.
Which makes Universal Design the centre of this system rather than a compliance step at the end. Everything on this page is the belief expressed as measurements: contrast that is computed rather than eyeballed, state that never depends on colour alone, a keyboard path through every flow, and motion that a person can switch off without losing meaning. 38 Origin states the belief; this page is where it has to survive contact with a real interface.
Token pairs, both modes
Every text pair computed live against WCAG AA; the failures are named, not hidden.
| Mode | Foreground | Background | Sample | Ratio | AA text | AA large / UI |
|---|
Two light-mode pairs fail: see the checklist at the foot of this page for the proposed token changes. AA text = 4.5:1. AA large text and UI components = 3:1. Vermilion 500 on cream is a mark and large-type colour; --milo-agent-ink (600) carries body text.
Map
Every interactive component has a keyboard contract; Tab reaches it, Enter or Space acts, Escape leaves.
Every key, component by component
- Tab / Shift TabMove through every interactive element in reading order. Rows are focusable as a unit; their actions follow.
- ↵Open the focused row, run the selected command, submit Quick Entry.
- SpaceToggle a disclosure, switch, or tab.
- ← →Move between tabs and recipe buttons.
- ↑ ↓Move within a list, menu or palette.
- EscDismiss the pill, Quick Entry, palette, popover or permission — always returns focus to the invoker.
- ⌘ KQuick Entry.
- ⌘ ⇧ DSwitch light / dark.
- ⌘ .Pause the current run.
Focus is visible
Two-pixel Vermilion ring, two-pixel offset, on every focusable element. Try it:
- Queued
The ring left this page and went into the app. Every ring the keyboard draws in offline is Vermilion, 2 px — in the window, the pop-ups, the Mind map and the first run. Measured against the grounds it sits on:
- On cream
- 3.2 : 1 · Vermilion 500
- On carbon
- 6.3 : 1 · Vermilion 400
- WCAG 2.2
- 3 : 1 for a focus indicator — met on both
The product now aims at WCAG 2.2 AAA wherever it can be met: body text at 7:1, 44 px targets, focus clearly visible (2.4.13), no time limits, motion off under reduced motion, every control named for a screen reader (44.6). A field that takes focus turns its border 2 px Vermilion; a row may take a Vermilion bar instead of the ring.
offline-os docs/ACCESSIBILITY.md, "The focus ring (2026-09-26)", held by tests/test_focus_ring_vermilion.py. What a screen reader says on each surface is still not heard by ear in the source. REF: https://www.w3.org/TR/WCAG22/#focus-appearance
Labelling
Glyphs are decorative and hidden; the state word is always in the tree.
Labelling rules
- GlyphsEvery ◉ ● ⌁ △ is
aria-hidden; the adjacent label is the accessible name. - Voice pill
role="status",aria-live="polite". The state word and transcript are announced as they change. - Run indicatorStandalone:
role="img"with "Running, step 3 of 5". In a row: decorative, the .state label speaks. - Progress
role="progressbar"with value, min, max and anaria-labelnaming the work. - DisclosureNative
<details>: expanded state comes for free. - Permission
role="dialog"/"alertdialog", labelled by the ask, focus trapped, Esc returns focus. - ImagesArt and nature carry the artist, title and institution in
alt; Milo interpretations say so.
Reduced motion and font scaling
Durations collapse under reduced motion; the scale is rem-based and survives 150%.
Under prefers-reduced-motion the tokens collapse every duration to ≤120ms, ambient motion stops and the dither transition draws its final frame. Status: …
Font scaling: the whole system is in rem on a 4px baseline. Drag to preview 100–150%.
Preview
What matters now?
- Render Day 1 voice trackRunning
44px hit area
The visual can be small; the target is not.
Visual size may be small. The hit area is not. Buttons extend their hit area 4px in every direction with a pseudo-element; rows, switches and nav items set min-height: 44px.
Dashed Vermilion = hit area (illustrative).
Remove the colour, keep the meaning
Every state pairs colour with a glyph and a label, so the meaning survives greyscale.
Milo is ◉ and a left rule. Offline is ⌁ and a boxed field. Position and glyph carry the ownership before Vermilion or Moss do.
Status
What passes, what is open, and where the evidence lives.
The checklist
- Text contrast ≥ 4.5:1 for primary and secondary ink, both modes (table above)
- UI contrast ≥ 3:1 for hairline-strong, focus ring, error and both accents (marks and large type)
- Keyboard reachable: every control, row, tab, menu, dialog
- Visible focus on every focusable element
- Glyphs hidden from AT; labels present on every state
- Live regions on pill and toasts
- prefers-reduced-motion honoured in CSS and the engine
- Font scaling to 150% without overlap (rem, clamp, 4px baseline)
- Targets ≥ 44px
- Colour never the sole indicator (JEV colorstate gate)
--milo-agent-inkon cream measures 4.46:1 — 0.04 short of AA text. Proposed: darken Vermilion 600 to #C0361A (≈4.9:1). Logged in MILO_OPEN_QUESTIONS.--state-warning#D9A21B on cream is 2.0:1 — it is always paired with △ and a label, but as a colour it fails even the 3:1 UI bar. Proposed: #9A6E05 on light. Logged.- Automated axe scan results are recorded in evidence/accessibility.md; manual VoiceOver pass is an open item