# 07 · Accessibility

Glass is pretty, but it is risky for readability. These rules keep the app usable for every owner in every condition. They are part of "done" for every screen.

## Dynamic Type (text size)

- All type tokens scale. Native: map each token to a text style (`02-tokens.md`, `tokens.json → type.*.iosStyle / material`) with `relativeTo:` so custom sizes still scale. Web prototype: sizes are in `rem`, and the dev bar sets 14 / 16 / 19 / 23 / 28px to mimic S, M, L, XXL and AX3.
- **Test at XXL and AX3.** At those sizes:
  - Rows grow taller; nothing truncates to the point of meaninglessness (names may wrap to 2 lines, previews stay 1 line).
  - Stat tiles go from 2 columns to 1 at AX sizes.
  - Action tiles (Message / Call / Directions) stack icon over label, and their labels may wrap.
  - Tab labels **stop growing at Large** (native rule). The icons carry the meaning, and VoiceOver reads the full label.
  - The hero number and display headline cap at about 1.3× so they never push the first action off screen.
  - Buttons grow in height, never clip. Two-button rows stack vertically when they don't fit.

## VoiceOver / TalkBack labels

Each interactive element has one clear, spoken label. Combine row parts into one label instead of letting VoiceOver read them piece by piece.

| Element | Label |
|---|---|
| Conversation row | "Dana Whitfield, SMS, 2 unread, It tripped again just now, 10:02 AM". Add ", Remy paused" / ", opted out" / ", Jess replying" when true |
| Unread badge (standalone) | "3 unread" |
| Call row | "Greg Hollis, voicemail, 38 seconds, 7:09 AM". Call button: "Call Greg Hollis" |
| Voicemail play | "Play voicemail, 38 seconds" / "Pause voicemail". Without audio: "The recording is not available on this phone" (not a button) |
| Waveform | Hidden. The time label is read as "12 seconds of 38" |
| Message bubble | "Dana: Is it safe to keep using the hot tub? 10:02 AM". Owner: "You: …, Delivered". Remy: "Remy: …" |
| Private note | "Private note, not sent to the customer: …" |
| Remy suggestion | "Remy suggests: … Actions: Send, Edit, Dismiss" |
| Auto-send | Announced once: "Remy will send this in 5 seconds. Stop button available." Don't announce every tick |
| Tab | "Inbox, tab, 2 of 5, 5 unread" |
| Remy orb | "Remy: what your assistant is doing" |
| Stage chip | Read as text: "Status: Quoted". Urgent: "Urgent" |
| Job card (pipeline) | "Nina Patel, full rewire, quoted, 9 days in stage, stuck". Custom actions: "Move to…", "Open" |
| Ghost slot | "Remy suggests: Panel quote visit, 12:30 PM, not confirmed. Double-tap to review" |
| Offline pill | Announced once as a status: "You're offline" |
| Toast | Polite live region. Never the only place important information appears |

- Decorative things are hidden: the wallpaper, the glass rims, the Remy gradient, the waveform bars, the emoji in chips.
- Drag-and-drop always has a non-drag alternative (the "Move to…" sheet, plus VoiceOver custom actions).
- Sheets: focus moves to the sheet title when it opens, returns to the opener when it closes, and Escape (or the VoiceOver scrub gesture) closes it.
- Headings: large titles and section labels are headings, so rotor navigation works.

## Touch targets: 44pt minimum

- Every tap target is **at least 44×44pt**, even when it looks smaller. A 36pt small button or segment gets an invisible 44pt hit area (`.btn--sm::after`, `.seg button::after`).
- Leave at least 8pt between separate targets in a row (Call buttons on call rows, chips).
- The whole row is one target. The only exception is one trailing button that keeps its own 44pt area.

## Contrast with glass

- Text is at least 4.5:1 (body) or 3:1 (large, 24px+ or 19px+ bold). Icons are at least 3:1. Check against the **worst** backdrop: the brightest spot of the vivid wallpaper, and for the nav bar, a white bubble scrolling underneath.
- Dense content is on **solid** surfaces (`01-principles.md`), so text contrast never depends on what is behind the glass.
- On glass, use only `--ink` text and `--accent` icons. `--text-2` is not allowed on glass.
- Accent as text = `--accent-text` (#c2410c, 5.2:1). Primary button = `--accent-strong` with white (5.2:1).
- Colour never carries meaning alone: status chips have words, channel dots sit next to a label, the urgent flag says "Urgent", and failed messages say "Not sent".

Verified pairs (light):

| Pair | Ratio |
|---|---|
| `--ink` on white | 19.1:1 |
| `--text-2` (≈ #5c5f6e) on white | 6.3:1 |
| `--accent-text` #c2410c on white | 5.2:1 |
| White on `--accent-strong` #c2410c | 5.2:1 |
| `--ok` #15803d on white | 5.0:1 |
| `--warn` #b45309 on white | 5.0:1 |
| `--danger` #b91c1c on white | 6.5:1 |
| `--info` #1d4ed8 on white | 6.7:1 |

Dark mode uses the lighter variants (`#34d399`, `#fbbf24`, `#f87171`, `#60a5fa`, `#ff8a5c`), which are 6.7–11:1 on the dark surface.

## Reduce Transparency fallback

When the OS setting is on (iOS "Reduce Transparency", Android with blur unavailable, or `prefers-reduced-transparency` on the web):

- Every glass, frosted and solid fill becomes **opaque**: `#f4f5fa` (chrome), `#ffffff` (sheets, cards, surfaces) in light; `#1c1e2b`, `#262938`, `#161824` in dark.
- All backdrop blur is off. The rims become a single 10% hairline. The highlight is gone.
- The calm wallpaper is hidden (plain base colour). The vivid wallpaper stays on Today only, behind opaque cards.
- The layout does not change, only the materials. Test every screen with the "No glass" dev toggle.

## Increase Contrast

`--text-2` goes to 82% (86% dark). Hairlines go to 22% (30% dark). Glass fill goes to 86%. Glass and frosted rims become a visible 35% border. Chips keep their strong text. Test with the "High contrast" dev toggle.

## Reduce Motion

All durations collapse to 0 (`tokens.css`). Push and pop become instant cuts, sheets appear in place, and the lens jumps. The Remy mark and the ghost-slot "breathing" stop, skeletons stop shimmering, and the confetti is skipped. The auto-send countdown still counts down (it is behaviour); only its bar stops animating smoothly.

## Dark mode

- A full token set (`tokens.css`). Test every screen in both modes.
- Dark is not just inverted: glass fills are tinted navy (`rgba(30,32,48,.36)`), shadows are deeper, and accent text lightens to `#ff8a5c`.
- Owner bubbles stay `--accent-strong` with white in both modes. Remy bubbles keep the violet edge.
- Images and photos are never inverted.

## One-handed reach

- Primary actions live in the **bottom half**: the composer, sheet CTAs, the Call back button on the voicemail card, the booking card buttons, and the tab bar.
- The top area holds only navigation (back) and low-frequency actions ("…"). Every top-right action has a reachable twin (the thread's Call is also on the voicemail card and the missed-call card).
- Sheets rise from the bottom, and their primary button is last and full width.
- Pull-to-refresh and the "tap the active tab to scroll up" behaviour both work one-handed.

## Sunlight and the job site

- Outdoors, at the minimum brightness that's still usable, the test is: can you read the name, the preview and the time on a conversation row at arm's length? If not, the surface is too transparent.
- Sizes are raised for this: meta 14 (was 13.5), chips 12 (was 11), tabs 11 (was 10.5).
- The heaviest weights go on the numbers and names that matter. Avoid thin weights (below 450) entirely.
- No information is carried only by a subtle tint, blur difference or glow.
- Gloves and wet fingers: big targets, no tiny × buttons (close buttons are 44pt), no gesture is the only path, and destructive actions are confirmed.
