# Accessibility The stance is committed in 00-vision.md: **accessibility is a requirement of "native to the bone," not an afterthought.** This document is the design iteration that stance promised — VoiceOver correctness on the custom board, text scaling, and the system visual accommodations. The keyboard-first contract (04-interactions.md) is the floor this builds on. ## Principles - **The keyboard path is the accessibility path.** Every function's drag-free guarantee is its keyboard/menu path (04-interactions.md's contract: every board function has a menu item — the menu home is the keyboard path, with or without a default chord), and VoiceOver rides those same paths. Bespoke accessibility-only UI is a last resort — if a function needs a drag-free alternative, it belongs in the context menu or menu bar, where *all* users get it. Concretely: **card movement is the clipboard** (⌘X, select target, ⌘V — 04's paste-lands-after-anchor/appends-to-lane rules make cut/paste a complete move story), not a parallel set of "Move to lane" accessibility actions. - **Logical order over visual geometry.** Assistive traversal follows the board's semantic order (lane `order`, then card `order`), never rendering artifacts like masonry column position. - **Announce what the files do.** A live board that changes under external writers must say so — silence about a mutating board is a lie to a VoiceOver user. ## The board through VoiceOver - **Tree shape**: window → lanes (accessibility containers, in lane `order`) → cards (leaf elements, in card `order`). A lane container is labeled "⟨title⟩, lane, N cards" — the count reads the search filter like the visible badge (04-interactions.md). The lane header's new-card button is a labeled child ("New card in ⟨lane⟩"). A card is **one flattened element**: label = title (or the untitled placeholder), value carries the attachment count when present, selected state via trait. Face icon and chips are decorative — folded into the element, never separately focusable. The sole-selection **attachment carousel** (03-board-ui.md) is decorative too — page dots and paging included, nothing focusable: the flattened element already carries the attachment count in its value, and the accessible attachment surface is the card window's keyboard-native section (below). - **Logical order, not masonry position** (decided): within a wide lane, VoiceOver reads cards by `order` — the interior grid columns are presentation only. This deliberately diverges from on-screen geometry; the spatial arrow-key model (04-interactions.md) remains available alongside, since board keyboard navigation keeps working with VoiceOver running. - **VO cursor and app selection are independent** (Finder-style): moving the VoiceOver cursor never mutates selection. VO-Space on a card toggles its selection (the ⌘-click analogue — a toggle, never plain click's replace, 04-interactions.md ▸ Selection); ⌘↩ opens the card window; arrow keys and ⇧-arrows drive selection exactly as without VoiceOver. Selection state is always readable from the element (trait), and cut cards expose their dimmed pending state in the value ("cut, pending paste"). - **Actions come from the context menu.** Context menus are the single inventory of per-item actions (Open, Rename, Delete, Put Back, Delete Immediately, width stepper, …), reachable the standard way (VO-⇧-M); where SwiftUI additionally surfaces menu items as custom accessibility actions, that's free improvement, not a separate design surface. - **Rotor**: lane titles are headings, so the headings rotor jumps lane-to-lane — on a one-dimensional board that *is* structural navigation; no custom rotors unless practice shows the need. - **Trash quasi-lane**: when shown (View ▸ Show Trash), it is the last container, labeled as Trash with its count; toggling visibility is announced. Tombstoned cards read their deletion state and expose Put Back / Delete Immediately via the context menu; there is no Open (03-board-ui.md's no-editing-in-the-trash). A tombstoned lane's single entry reads "⟨title⟩, deleted lane, N cards" and exposes the same actions; keyboard reachability follows 04-interactions.md's homogeneous-by-kind trash rules. - **Search**: filtered-out cards leave layout and the accessibility tree together — the filter is the single source of truth for "what's on the board" (04-interactions.md), and the tree is one of its readers. Lane counts announced reflect the filter. ## Moving without dragging - **Cards**: clipboard. ⌘X the selection, move selection to the destination (arrows), ⌘V — between lanes, within a lane (paste lands after the anchor card), and across boards (04-interactions.md's staged clipboard). This is the committed drag-free move story; it needs no VoiceOver-specific machinery because selection and paste targeting are already keyboard-native. - **Lanes**: the defect this doc originally named (lanes had no keyboard-move path) is closed by the keyboard map — with a lane selected, ⌘←/⌘→ move it (Board ▸ Move Left / Move Right, 04-interactions.md); cards gain ⌥⌘↑/⌥⌘↓ within-lane sorting, and cross lanes drag-free via cut/paste (04-interactions.md's clipboard rules). Lanes carry the clipboard too (resettled, 04-interactions.md ▸ Clipboard), so cross-board lane copy/move — once drag-only, the contract's last gap — is ⌘C/⌘X, then ⌘V with the destination board frontmost. - **Lane resize**: the header context menu's width stepper (03-board-ui.md) — and its keyboard face, the Increase/Decrease Lane Width menu items (⌥⌘→/⌥⌘←, 11-command-nexus.md) — is the accessible path; edge drag is enhancement only. - **Attachments**: the card window's sidebar items expose Open / Reveal in Finder / Remove via context menu, and the section is keyboard-navigable outright (arrows, Space-QuickLook, Return, ⌫ — 05-card-window.md); adding files drag-free is File ▸ Add Attachment… (⇧⌘A, 11-command-nexus.md) alongside Finder-drop. ## Live board announcements - **Foreign changes announce, app-mediated echoes never do.** The auto-committer already classifies every observed change as app-mediated or foreign and synthesizes diff summaries for commit messages (06-history-undo.md); announcements reuse that summarizer — one polite (non-interrupting) digest per reload debounce ("Board changed: 2 cards edited, 1 card added"), never per-file chatter. On no-git boards the same classifier runs without the committer — announcements don't depend on git mode. - **A vanishing focus is called out specifically.** If the selected or VO-focused card disappears in a reload (deleted externally, or hidden by a lane tombstone), the announcement names it ("Card 'Fix login' was deleted externally") and focus recovers to the card's lane (mirroring selection's reload-survival rules, 02-architecture.md). - **Bracketed operations announce once, at completion** ("Pulled 3 commits", "Switched to branch 'redesign'") — never their internal churn (02-architecture.md's bracketing). The live-reload-resilience banner (02-architecture.md) is an accessibility element and is announced when it appears and when it clears — including the read-only lock after a failed bracketed reload. ## Card window, welcome, template chooser, popover - **Card window**: standard controls, standard labels. The attributes sidebar is a labeled container of labeled sections; attachment rows are elements labeled by filename; the Details section's unknown-key rows read as static text ("⟨key⟩, ⟨value⟩"); the bottom actions are ordinary buttons. **Preview renders to the accessibility tree as structured text** — headings navigable by rotor, lists and tables read as such; task-list checkboxes are real accessible checkboxes, toggleable without the pointer (05-card-window.md's live checkboxes); body images use Markdown alt text when present, else the filename. Edit and the raw-source outlet are ordinary accessible text editors; the Preview/Edit toggle (⌘E) announces its state. - **Welcome window**: recents rows are elements labeled "⟨name⟩, ⟨location⟩, N lanes, M cards" (registry-cached counts — 02-architecture.md); row actions (Open / Reveal in Finder / Forget) via context menu; unavailable rows say so ("unavailable — board not found"). - **Template chooser**: templates are elements labeled by title; the mini per-lane previews are decorative and hidden from the tree. - **Board popover**: labeled controls throughout; the ahead/behind indicator's information — counts, queued pushes, last error — must be readable as text, never conveyed by color or shape alone. - **Style editor** (card sidebar section, board popover, Style… popover — 03-board-ui.md ▸ Styling ▸ Controls): grids are arrow-navigable, every well Tab-reachable and labeled by name (palette color, symbol name; leading wells "None" / "Default"); the current value is stated by trait, and a batch selection's mixed state reads as "mixed", never conveyed by highlight alone. ## Text scaling & visual accommodations - **Full relative scaling** (decided): relative text styles everywhere, no fixed point sizes. Card face, lane header, and masonry metrics derive from font metrics, so layout survives the largest system text sizes; the no-horizontal-scroll invariant is untouched (lane count is the user's choice; lanes scroll vertically), and 03-board-ui.md's graceful-truncation rules apply at every scale. - **Contrast is pinned to WCAG AA.** Lane and card colors render as edge accents (03-board-ui.md's top-edge band / left-edge stripe), so text never sits on them — they are supplementary decoration, never the sole carrier of information, and carry no text-contrast obligation. The ≥ 4.5:1 automatic-contrast rule binds where text does sit on a user-chosen color: the **board** background (palette pairs verified at design time; arbitrary hex computes its text color at runtime against that threshold). An `#RRGGBBAA` background with alpha computes against the color **composited over its effective backdrop** in the active appearance (the board's over the window background; light and dark resolve differently), recomputed on appearance change. Increase Contrast strengthens borders and the selection indicator. - **State is never color-alone**: selection is a ring plus trait, cut-pending is dim plus stated value, the trash header is hatched plus labeled — all already patterned; kept as a rule. - **Reduce Motion**: reflow-on-drag, search animate-out, the drag replica, the lane-resize rubber-band feedback (03-board-ui.md ▸ Lane — the selection marquee never animates and needs no variant), and trash animations all get reduced variants (crossfade or instant). **Reduce Transparency**: glass underlays (carousel page dots) go solid. - **Full Keyboard Access** (independent of VoiceOver): the board is one tab stop with arrow-key navigation within; every control — lane buttons, popover, card window, welcome — is Tab-reachable. ## Verification - **Automated audits are test failures**: Xcode's accessibility audit (`performAccessibilityAudit`) runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit, raw source), welcome, template chooser, board popover. - **A manual VoiceOver smoke script** lives with the test plan: create lane → create card → rename → cut/paste to another lane → external edit lands (announcement heard) → delete → Put Back → Empty Trash. Run per release; it is the canonical "does the board actually work blind" check. ## Changes from Kanban - All new ground — the pathfinder never had an accessibility pass. Nothing carries over. ## Open questions None currently — the lane-move gap and the Add Attachment menu path are both closed by the keyboard map (04-interactions.md's contract, inventoried in 11-command-nexus.md).