# Frontend a11y refactor — changelog 2026-05-01 Implemented per `/opt/sortof/docs/a11y-audit-2026-05-01.md`. No backend changes; no DB migrations; no spec changes. Frontend only. ## Files touched - `/opt/sortof/frontend/index.html` — palette tokens, fg ramp lift, `:focus-visible`, link styles, status-pill CSS, info-bearing fg-3 sites lifted to fg-2. - `/opt/sortof/frontend/sortof-app.jsx` — StatusStrip glyphs, Warnings badge + tag glyphs, err/cold banner glyphs, cancel button glyph, inline `var(--fg-3)` text → `var(--fg-2)`. ## Palette: before → after ### Theme tokens | Token | Before | After | Reason | |---|---|---|---| | `--fg` | oklch(0.95 0.008 240) | oklch(0.95 0.008 240) | unchanged (already AAA) | | `--fg-1` | oklch(0.78 0.008 240) | **oklch(0.85 0.008 240)** | secondary text headroom | | `--fg-2` | oklch(0.60 0.010 240) | **oklch(0.80 0.010 240)** | tertiary text — was borderline AA (5.0:1), now 7.7:1 vs `--bg-1` and ≥5.5:1 against every panel surface | | `--fg-3` | oklch(0.45 0.010 240) | **oklch(0.68 0.010 240)** | decoration only (`.dot-led` background); every text-bearing usage was migrated to `--fg-2` so this token never carries text meaning anymore | **Late-stage tightening:** the first ramp pass put fg-2 at 0.72 and fg-3 at 0.60. That cleared AA against `--bg-1` (0.21) only. On darker panel surfaces (`--bg-2` 0.245, `--bg-3` 0.28, `--bg-hi` 0.32), fg-3 still failed AA, and many sites used fg-3 inside those panels. Fixed by: 1. Lifting fg-2 to 0.80 (passes AAA against bg-1; passes AA against bg-hi worst-case). 2. Lifting fg-3 to 0.68 (decoration only — passes AA hedge). 3. Sed-replacing every `color: var(--fg-3)` to `color: var(--fg-2)` across `index.html` (background uses preserved). Result: 1 remaining `--fg-3` reference (the `.dot-led { background }` rule). | `--acc-green` | oklch(0.78 0.13 195) (teal) | aliased to `--acc-success` | | `--acc-amber` | oklch(0.82 0.15 75) | aliased to `--acc-warn` | | `--acc-red` | oklch(0.68 0.18 30) | aliased to `--acc-error` | | `--acc-blue` | oklch(0.72 0.14 255) (deep) | aliased to `--acc-info` | | `--acc-success` | _new_ | **oklch(0.78 0.13 165)** Okabe-Ito bluish-green | | `--acc-warn` | _new_ | **oklch(0.82 0.15 75)** Okabe-Ito orange/yellow | | `--acc-error` | _new_ | **oklch(0.70 0.18 35)** Okabe-Ito vermillion | | `--acc-info` | _new_ | **oklch(0.78 0.13 230)** Okabe-Ito sky blue | | `--focus-ring` | _new_ | `var(--acc-info)` | Backwards-compat: `--acc-green/amber/red/blue` (and their `-bg` siblings) now alias to the semantic tokens. Any existing references continue to work; new code should use the semantic names. ### Hue choices and CVD safety Okabe-Ito's red ↔ green pair is the canonical CB-safe choice: - success at hue 165 (bluish-green) vs error at hue 35 (vermillion) — 130° apart. Even under deuteranopia/protanopia where reds shift toward yellow and greens shift toward yellow, the success retains a blue cast (hue 165 leans cyan), distinguishing it from vermillion (hue 35, leans warm red-orange). Both at similar L (0.78 vs 0.70) for parity. - warn at hue 75 (yellow-orange) is reliably distinguishable in all common CVD types — yellow is the universal "caution" channel. - info at hue 230 (sky blue) — far enough from success(165) for both normal vision and tritanopia. ### Per-section CSS edits | Selector | Before | After | |---|---|---| | `.tagline` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `footer.app` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `.label-meta` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `.branch-name` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `.branch-deps` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `.branch-pos` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `.status-pill.idle` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `.status-pill.nonmod` | `color: var(--fg-3)` | `color: var(--fg-2)` | | `a` | `color: fg-1; border-bottom: 1px dotted` | `color: var(--acc-info); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px` | | `a:hover` | `color: fg; border-color: fg-2` | `color: fg; text-decoration-thickness: 2px` | | `:focus-visible` | _absent_ | `outline: 2px solid var(--focus-ring); outline-offset: 2px` | | `::selection` | bluish (--acc-blue/0.35 hex) | bluish (--acc-info/0.35) | | `.status-glyph` | _new class_ | min-width 12px, font-weight 600, line-height 1 | | `.status-pill.` rules | colored .dot-led only | recolor `.status-glyph` AND `.dot-led`; old `.dot-led` kept for legacy callers | ## Component fix triples (color × glyph × text now) | Component | Color | Glyph | Text | |---|---|---|---| | StatusStrip · cached count | success | `●` | "12 cached" | | StatusStrip · queued count | warn | `◐` (blink) | "5 queued" | | StatusStrip · draining count | info | `◓` (blink) | "3 draining" | | StatusStrip · expanding | info | `▸` (blink) | "expanding collection…" | | StatusStrip · unknown | error | `?` | "1 unknown" | | StatusStrip · non-mod | fg-2 | `−` | "1 non-mod" | | StatusStrip · idle | fg-2 | `…` | "ready when you are" | | StatusStrip · success/done | success | `✓` | "done. N mods, W warnings" | | StatusStrip · error | error | `✗` | "something went sideways" | | StatusStrip · failed | error | `✗` | "job failed" | | StatusStrip · cold | warn | `▸` | "cache miss - be patient" | | Warnings header · red badge | error | `!` | "{N}" | | Warnings header · amber badge | warn | `⚠` | "{N}" | | Warning row · w-tag | error/warn | `!` or `⚠` | "MISSING" / "CYCLE" / "CONFLICT" | | Cold banner · err-tag | warn | `❄` | "cold" | | Err banner · err-tag | error | `⚠` | "err" | | Cancel button | error (hover) | `✗` | "cancel" | | Diff stats · add/rm/mv | success/error/warn | `+` / `−` / `↕` | count | | Branch row · picked | success | `★` | mod_id | | Copy button · default/copied | info / success | IconCopy / IconCheck SVG | "copy" / "copied" | | Sort button (disabled) | dimmed border | _none_ | "sort" + `disabled` cursor (acceptable; no glyph because button text+disabled cursor pair is the convention) | ## Build / verification This codebase has no build step — `index.html` loads `sortof-app.jsx` directly via `