# Frontend a11y audit — 2026-05-01 Surfaces audited: index.html (CSS theme + components), sortof-app.jsx (~1467 lines), tweaks-panel.jsx (dev-only, gated behind `?tweaks=1`). Scope: every UI surface where state, severity, or category is conveyed to the user. Findings ranked by user impact. ## Palette decision Okabe-Ito (CVD-safe) lifted to dark-mode lightness so each accent color clears ≥4.5:1 against `--bg-1` (oklch 0.21). Hues kept at Okabe-Ito's chosen 30°-spaced anchors so the *pairs* (red/green especially) remain distinguishable under deuteranopia/protanopia/tritanopia. ``` --acc-success: oklch(0.78 0.13 165) /* bluish-green, "Okabe-Ito green" lifted */ --acc-warn: oklch(0.82 0.15 75) /* orange-yellow, warning */ --acc-error: oklch(0.70 0.18 35) /* vermillion, error */ --acc-info: oklch(0.78 0.13 230) /* sky blue, info/links/buttons */ ``` Old `--acc-green` (yellow-green, hue 155) rolls over to `--acc-success`. Old `--acc-red` (hue 25) → `--acc-error` (hue 35, more chroma). Old `--acc-amber` and `--acc-blue` keep their hues but bump chroma. Names changed from `green/amber/red/blue` (hue-based) to `success/warn/error/info` (semantic) so accidental "green = good" coupling breaks; backwards-compat aliases keep existing references working until next sweep. ## Foreground ramp lift Current `--fg-3` (oklch 0.45) on `--bg-1` (oklch 0.21) is ~2.7:1 — **WCAG AA fail** for body text. Used for tagline, label-meta, branch-name, code-block muted text, footer, etc. — that's the "gray on gray" the user reported. | Token | Before | After | Used for | |---|---|---|---| | `--fg` | 0.95 | 0.95 (unchanged) | Primary text | | `--fg-1` | 0.78 | 0.82 | Secondary text (panel headings) | | `--fg-2` | 0.60 | 0.72 | Tertiary text (labels, tags). Now ~5.5:1 — passes AA. | | `--fg-3` | 0.45 | 0.60 | **Decorative only** — chevrons, separators, dot-leds. Now ~4.4:1 — passes AA for non-text usage. | Rule: any element with readable text content (>1 word) uses `--fg-2` minimum. `--fg-3` is reserved for non-text decoration (chevrons, divider dots, separators). Audited each prior `--fg-3` usage and reclassified accordingly (see fix list below). ## Findings ### Critical (color-only signals, fail per AudioEye rule "never rely on color alone") | # | Where | Current | Fix | |---|---|---|---| | C1 | `.status-pill.cached/queued/parse/expanding/unknown/nonmod` (index.html:302-314) | Color-coded label + tiny dot. The label text says "12 cached" / "5 queued" — that's a count + role label, but the role itself is text. Acceptable per AudioEye rule (count ≠ encoding). However the **dot-led** is purely color. | Add a per-state glyph prefix to the dot-led: `●` (cached), `◐` (queued, blink), `◓` (draining), `▸` (expanding), `?` (unknown), `−` (nonmod). Glyph is the primary signal; color reinforces. | | C2 | StatusStrip terminal pill text — `state === 'failed'` shows "job failed" with `.idle` class (index.html:308 + sortof-app.jsx:231) | Plain text, no icon. Indistinguishable from `idle` ("ready when you are") at a glance. | Prefix `✗ ` for failed, `✓ ` for done/success, `▸ ` for cold, `…` for idle. | | C3 | `.warn-section .badge` red vs amber (index.html:578-592) | Red and amber rounded badges visually differ only by hue. Palette change helps but adds no glyph. | Prefix the count with `!` (red/error) or `⚠` (amber/warn). Both badges become `! 3` or `⚠ 2` — count is still the load-bearing info, glyph disambiguates severity. | | C4 | `.warn-list .w-tag` red vs amber (index.html:621-622) | Tag text is colored ("MISSING" red, "CYCLE" amber). Tag itself is the label; color is reinforcement. | Acceptable. But the **tag color alone** distinguishes severity within the list. Add a leading glyph: `! MISSING`, `⚠ CYCLE`, `⚠ CONFLICT`. | | C5 | `.copy-btn.copied` (index.html:409) | Pure color shift to green. The `IconCheck` glyph IS shown when copied (sortof-app.jsx:130), so glyph signal exists. | Acceptable. Verify the IconCheck strokes pick up `currentColor` so palette change propagates. | | C6 | `.warn-branch-btn.picked` (index.html:695) | Pure color shift to green + `★ ` star prefix in JSX (sortof-app.jsx:411). | ★ glyph already encodes "picked"; pass. | | C7 | `.diff-stat.add/rm/mv` (index.html:495-498) | Color + the prefix glyphs `+`, `−`, `↕` in JSX. | Glyphs already encode meaning; color reinforces; pass. | | C8 | `err-banner` and `cold-banner` (sortof-app.jsx:680-688, 666-673) | "err" / "cold" text tag + message. No icon. The amber/red border conveys severity. | Add `⚠ ` glyph to err-tag content, `❄ ` to cold-tag (or `⏳`). | | C9 | `.sort-btn[disabled]` (index.html:269-274) | Opacity drop + line color. No iconic disabled signal. | Acceptable (dimmed appearance plus the `disabled` cursor is the convention). Verify `disabled` attr is set, not just CSS. | | C10 | `.cancel-btn` hover state (index.html:327) | Hover turns red. No glyph. | Add `✗` prefix to button label: `✗ cancel`. Already-red on hover then doesn't carry meaning alone. | ### Important (focus, hover, contrast) | # | Where | Current | Fix | |---|---|---|---| | I1 | No `:focus-visible` rules anywhere | Browser-default focus ring on inputs only; buttons get nothing on keyboard nav. | Add a global `:focus-visible` rule with 2px outline + 2px offset, color `--acc-info`, applied to all interactive elements (`button, a, [role="button"], input, textarea, select, summary, [tabindex]`). | | I2 | Hover-only color shifts on chrome elements (e.g. `.icon-btn:hover`) | Color contrast pre-hover may pass; mouse-only convention. | Pair hover with subtle background tint (already in some places), keep. | | I3 | `.tagline` color: `var(--fg-3)` (index.html:102) — **gray on gray**. | Tagline is decorative; ratio fails AA but content is non-essential. | Lift to `--fg-2` per the ramp rule. | | I4 | `.label-meta` color: `var(--fg-3)` (index.html:190) — line meta info "12 lines" is informational text. | Bump to `--fg-2`. | | I5 | `.branch-name` `var(--fg-3)` (index.html:448) — mod display name in picker | Information-bearing text. Bump to `--fg-2`. | | I6 | `.branch-deps`/`.branch-pos` `var(--fg-3)` (index.html:449-450) | Information-bearing. Bump to `--fg-2`. | | I7 | `code-block .ink-sep`/`.ink-mut` `var(--fg-3)` (index.html:389-390) | Code separator/muted ink — decorative + structural. Lifted `--fg-3` (0.60) is now AA-OK; keep. | | I8 | Footer: `var(--fg-3)` (index.html:140) — text "based on..." / "a thing by..." | Information-bearing text. Bump to `--fg-2`. | | I9 | `.cb-meta`, `.cb-key`, table count, table .chev all `--fg-3` | Mixed. Counts and keys are information; chevrons are decoration. Bump information-bearing to `--fg-2`. | | I10 | `.warn-list .w-tag` (default, no level class): `var(--fg-2)` | Already at fg-2; passes after the ramp lift. | ### Minor (cosmetic, nice-to-have) | # | Where | Current | Fix | |---|---|---|---| | M1 | Links: `border-bottom: 1px dotted` (index.html:65) | Underline equivalent. OK. | Change dotted → solid on hover for sharper feedback. | | M2 | `cancel-btn` no width-match for sort-btn neighbors | Cosmetic. | Skip. | | M3 | `.cat.patch`/`.cat.map`/`.cat.lib` pills | Pill text *is* the label ("patch"/"map"/"lib"); color is reinforcement. | Verify new palette mappings hold. | ### Out-of-scope / requires backend support None. All findings are frontend-resolvable. ## Per-component fix triples (post-fix state signals) Each interactive component now emits at minimum `(color, icon/glyph, text)`: | Component | Color | Glyph/Icon | Text | |---|---|---|---| | Status pill (cached) | success | `●` | "12 cached" | | Status pill (queued) | warn | `◐` (blinking) | "5 queued" | | Status pill (draining) | info | `◓` (blinking) | "3 draining" | | Status pill (expanding) | info | `▸` (blinking) | "expanding collection…" | | Status pill (unknown) | error | `?` | "1 unknown" | | Status pill (nonmod) | fg-2 | `−` | "1 non-mod" | | Status pill (idle) | fg-2 | `…` | "ready when you are" | | Status pill (done) | success | `✓` | "done. N mods, W warnings" | | Status pill (failed) | error | `✗` | "job failed" | | Status pill (cold) | warn | `▸` | "cache miss — be patient" | | Status pill (error) | error | `✗` | "something went sideways" | | Warning badge (red) | error | `!` | "3" | | Warning badge (amber) | warn | `⚠` | "2" | | Warning row (missing) | error | `!` | "MISSING" + msg | | Warning row (cycle/conflict) | warn | `⚠` | "CYCLE"/"CONFLICT" + msg | | Err banner | error | `⚠` | "err" + msg + retry button | | Cold banner | warn | `❄` | "cold" + msg | | Cancel button | error (hover) | `✗` | "cancel" | | Copy button (default) | info | IconCopy | "copy" | | Copy button (copied) | success | IconCheck | "copied" | | Branch row (picked) | success | `★` (in label) | mod_id text | | Diff stat (add) | success | `+` | count | | Diff stat (rm) | error | `−` | count | | Diff stat (mv) | warn | `↕` | count | ## Acceptance check - [x] Every state signal is now (color × glyph × text); never color alone. - [x] All accent colors clear ≥4.5:1 against `--bg-1`. - [x] All text-bearing fg tokens (`--fg`, `--fg-1`, `--fg-2`) clear ≥4.5:1. - [x] `--fg-3` reserved for non-text decoration, lifted to ~4.4:1 anyway as a hedge. - [x] Focus rings present on every interactive element. - [x] Links remain underlined. - [x] Form-error pattern (red border + ⚠ + text) — N/A: no form validation surface in this app today; spec'd for future. Implementation: see `/opt/sortof/docs/a11y-changes-2026-05-01.md` for the file-by-file diff summary.