# Lumotia v0.1 WCAG-AA Contrast Spot-Check **Date:** 2026-05-14 **Auditor:** automated (Claude Code subagent) **Scope:** WCAG 2.1 AA spot-check only — not a full audit. Normal text threshold: 4.5:1. Large text (≥ 18 pt / 14 pt bold) threshold: 3:1. Non-text UI components: 3:1 (WCAG 1.4.11). **Theme coverage:** default dark (`:root`) + light (`[data-theme="light"]`) + three zone variants (cave / energy / reset). --- ## Tokens Audited ### Dark theme (`:root`) | Token | Value | |---|---| | `--color-text` (text-primary) | `#f0ece4` | | `--color-text-secondary` | `#9a9486` | | `--color-text-tertiary` | `#8c8678` | | `--color-bg` (bg-page) | `#0f0e0c` | | `--color-bg-card` | `#1b1a17` | | `--color-bg-elevated` | `#171614` | | `--color-accent` | `#d68450` | | `--color-danger` | `#e85f5f` | | `--color-success` | `#5fc28a` | | `--color-warning` | `#e8be4a` | ### Light theme (`[data-theme="light"]`) | Token | Value | |---|---| | `--color-text` (text-primary) | `#1a1816` | | `--color-text-secondary` | `#5c574d` | | `--color-text-tertiary` | `#6b6557` | | `--color-bg` (bg-page) | `#faf8f5` | | `--color-bg-card` | `#ffffff` | | `--color-bg-elevated` | `#f3f0eb` | | `--color-accent` | `#a3683a` | | `--color-danger` | `#b32626` | | `--color-success` | `#1f7344` | | `--color-warning` | `#a08a1f` | --- ## Pairs Tested and Results All ratios computed via the WCAG relative-luminance formula (IEC 61966-2-1 sRGB linearisation + `(L1+0.05)/(L2+0.05)`). ### Body text and UI label text | Pair | Dark ratio | Dark | Light ratio | Light | |---|---|---|---|---| | text-primary on bg-page | 16.38:1 | PASS | 16.70:1 | PASS | | text-primary on bg-card | 14.77:1 | PASS | 17.70:1 | PASS | | text-secondary on bg-page | 6.39:1 | PASS | 6.77:1 | PASS | | text-secondary on bg-card | 5.76:1 | PASS | 7.18:1 | PASS | | text-tertiary on bg-card | 4.80:1 | PASS | 5.79:1 | PASS | | text-tertiary on bg-page | 5.33:1 | PASS | 5.47:1 | PASS | All body/label text pairs clear AA with comfortable headroom. Phase 10a/10b token adjustments documented in `app.css` are confirmed effective. ### Filled buttons — white or dark text on semantic background | Pair | Ratio | Result | Usage | |---|---|---|---| | **DARK** `#ffffff` on accent `#d68450` | 2.89:1 | **FAIL** | `bg-accent text-white` buttons throughout | | **DARK** `#0f0e0c` on accent `#d68450` | 6.68:1 | PASS | `bg-accent text-bg` (EmptyState) | | **DARK** `#ffffff` on danger `#e85f5f` | 3.37:1 | **FAIL** | DictationPage record button (recording state) | | **DARK** `#ffffff` on success `#5fc28a` | 2.19:1 | **FAIL** | (no solid success button found in codebase — hypothetical pair) | | **DARK** `#ffffff` on warning `#e8be4a` | 1.77:1 | **FAIL** | DictationPage record button (model-loading state, 60% opacity, `cursor-wait`) | | **LIGHT** `#ffffff` on accent `#a3683a` | 4.57:1 | PASS | All `bg-accent text-white` buttons in light mode | | **LIGHT** `#ffffff` on danger `#b32626` | 6.52:1 | PASS | FirstRunPage cancel button | | **LIGHT** `#ffffff` on success `#1f7344` | 5.84:1 | PASS | (no solid success button) | | **LIGHT** `#ffffff` on warning `#a08a1f` | 3.41:1 | **FAIL** | DictationPage record button loading state (light) | | **LIGHT** `#faf8f5` (bg-page) on accent `#a3683a` | 4.31:1 | **FAIL** | EmptyState `text-bg` button — light mode only | ### Semantic text labels on card backgrounds | Pair | Dark ratio | Dark | Light ratio | Light | |---|---|---|---|---| | success text on bg-card | 7.93:1 | PASS | 5.84:1 | PASS | | danger text on bg-card | 5.17:1 | PASS | 6.52:1 | PASS | | warning text on bg-card | 9.86:1 | PASS | 3.41:1 | **FAIL** | ### Semantic text on tinted backgrounds (10% alpha fills) | Pair | Ratio | Result | Usage | |---|---|---|---| | **DARK** warning text on `bg-warning/10` over card | 8.07:1 | PASS | EnergyChip, MicroSteps | | **DARK** danger text on `bg-danger/10` over card | 4.58:1 | PASS | Card danger variant | | **DARK** success text on `bg-success/20` over card | 5.38:1 | PASS | MicroSteps completed row | | **LIGHT** warning text on `bg-warning/10` over white | 3.08:1 | **FAIL** | EnergyChip light mode | | **LIGHT** warning text on `bg-warning/10` over page bg | 2.92:1 | **FAIL** | EnergyChip on page bg | ### StatusPill label text on pill background The StatusPill label uses `text-secondary` on a near-opaque pill bg (`bg-elevated` + ~6% white glow). The 6px coloured dots are decorative (always accompanied by visible text label); they are evaluated against the non-text 3:1 threshold (WCAG 1.4.11). | Pair | Ratio | Result | |---|---|---| | **DARK** text-secondary on pill bg (~`#1d1c1b`) | 5.63:1 | PASS | | **LIGHT** text-secondary on pill bg (`#f3f0eb`) | 6.32:1 | PASS | **Non-text contrast — StatusPill/LlmStatusChip coloured dots (3:1 threshold):** | Dot colour on pill bg | Dark | Light | |---|---|---| | danger dot | 5.05:1 PASS | 5.73:1 PASS | | success dot | 7.75:1 PASS | 5.14:1 PASS | | warning dot | 9.64:1 PASS | 3.00:1 PASS *(borderline)* | | accent dot | 5.90:1 PASS | 4.02:1 PASS | All decorative dots pass non-text contrast (3:1). Light warning dot is exactly 3.00:1 — right at the threshold; no action required. ### Zone variant bg-card surfaces (dark text on zone-coloured card) | Pair | Ratio | Result | |---|---|---| | **DARK** text-primary on cave bg-card (`#14222b`) | 13.78:1 | PASS | | **DARK** text-primary on energy bg-card (`#261913`) | 14.48:1 | PASS | | **DARK** text-primary on reset bg-card (`#161f15`) | 14.36:1 | PASS | | **LIGHT** text-primary on cave bg-card (`#f5fafc`) | 16.83:1 | PASS | | **LIGHT** text-primary on energy bg-card (`#fff8ef`) | 16.80:1 | PASS | | **LIGHT** text-primary on reset bg-card (`#f7fcf5`) | 17.03:1 | PASS | All zone surfaces pass with substantial headroom. --- ## Summary | Category | PASS | FAIL | |---|---|---| | Body / label text on surfaces | 12 | 0 | | Filled buttons — text on semantic bg | 8 | 5 | | Semantic text on card / tinted bg | 7 | 3 | | StatusPill text labels | 2 | 0 | | Non-text dots (3:1 threshold) | 8 | 0 | | Zone variants | 6 | 0 | | **Total** | **43** | **8** | **8 pairs fail WCAG AA.** --- ## Fail Analysis and Recommendations ### FAIL-1 · DARK `white on accent (#d68450)` — 2.89:1 **Affected components:** FirstRunPage (multiple CTA buttons), FilesPage (Browse button), ModelDownloader (Download button), ShutdownRitualPage (finish button), DictationPage (idle record button, badge, paste-confirm button), MorningTriageModal (selected chip and confirm button), ViewerPage (play button selected state). **Root cause:** Dark accent was lightened in Phase 10b (chroma bump to `#d68450`) for brand warmth. That bump moved the token further from the luminance ceiling that allows white text to pass (`L ≤ 0.183`). The current luminance of `#d68450` is 0.314 — nearly twice the ceiling. **Alternatives:** - **Option A (recommended): swap these buttons to `text-bg` (dark-mode page background).** `#0f0e0c` on `#d68450` = **6.68:1 PASS**. Already used in EmptyState. Zero token change needed — just a class change per component (`text-white` → `text-bg`). - **Option B:** darken dark-theme accent to approximately `#9a5c2e` to bring luminance under 0.183. Ratio would reach ~4.6:1. This is a brand token change; requires designer sign-off. - **Option C (scope-limit):** accept the fail for v0.1, document it, and fix in v0.1.1. The dark-mode record button is large (80px), which might qualify as Large UI Component under some interpretations, but WCAG does not grant leniency for size on non-text contrast when text accompanies it. **Nudge required to fix Option B:** darken `--color-accent` in dark theme from `#d68450` to approximately `#9b5e30` (–20% lightness). **This is a brand decision — do not apply without explicit approval.** --- ### FAIL-2 · DARK `white on danger (#e85f5f)` — 3.37:1 **Affected:** DictationPage record button in recording state (`bg-danger text-white`). FirstRunPage cancel-model button. **Root cause:** `#e85f5f` is a medium-luminance red (L=0.262), too bright for white text at AA. **Recommendation:** - **Option A (recommended):** swap the icon/label in the record button to `text-bg` (`#0f0e0c` on `#e85f5f` = **5.73:1 PASS**). The record button currently holds a lucide icon — change `text-white` to `text-bg` for the dark-theme icon. - **Option B:** darken `--color-danger` (dark) from `#e85f5f` to approximately `#c43838` to bring below the white-text ceiling. Ratio would reach ~4.6:1. --- ### FAIL-3 · DARK `white on warning (#e8be4a)` — 1.77:1 (record button loading state) **Affected:** DictationPage record button when `modelLoading = true` — rendered as `bg-warning opacity-60 cursor-wait`. This is a transient disabled state, not an interactive element. The button shows a spinner and the `cursor-wait` cursor; the 80px yellow ring is the only affordance. **Severity: lower.** This state is non-interactive (user cannot click) and lasts only during model warm-up (typically 2–10 seconds on first load). The `opacity-60` actually worsens the contrast ratio further against surrounding content. **Recommendation:** Replace the loading state presentation with a neutral palette: `bg-bg-elevated text-text-tertiary` (already used for the `!tauriRuntimeAvailable` state). Eliminates the white-on-yellow problem entirely and is more semantically consistent (disabled = neutral, not warning). **Propose for v0.1.1 — low user-facing impact given the transience and non-interactivity of the state.** --- ### FAIL-4 · DARK `white on success (#5fc28a)` — 2.19:1 **Affected:** No solid `bg-success text-white` element found in the current codebase. All success uses are either 6px decorative dots or `text-success` labels on tinted backgrounds. This pair is included for completeness — **no action required for v0.1**. --- ### FAIL-5 · LIGHT `white on warning (#a08a1f)` — 3.41:1 **Affected:** DictationPage record button loading state in light mode (same as FAIL-3 above, light variant). Same recommendation applies — swap loading state to neutral palette. --- ### FAIL-6 · LIGHT `warning text (#a08a1f) on bg-card (#ffffff)` — 3.41:1 **Affected:** Any component that renders `text-warning` label text directly on a white card surface in light mode. Grep shows `text-warning` is used in EnergyChip (as `text-warning border-warning bg-warning/10` — the tinted case, not plain white). The plain-card case is the theoretical worst case. **EnergyChip specific:** `warning text on bg-warning/10 over white card` = 3.08:1 — also fails. The tint barely lightens white, so warning text effectively sits on near-white. **Recommendation for light warning text:** Darken `--color-warning` in light theme from `#a08a1f` to approximately `#7d6b10` to reach 4.5:1 on white. Alternatively, for EnergyChip specifically, pair the warning label with `text-text` (dark primary) and rely on the warning colour only for the border/dot — this avoids the contrast problem without token surgery. **This is a design decision; propose for v0.1.1.** --- ### FAIL-7 · LIGHT `bg-page (#faf8f5) on accent (#a3683a)` — 4.31:1 (EmptyState button) **Affected:** `EmptyState.svelte` uses `bg-accent text-bg`. In light mode, `text-bg` resolves to `--color-bg` = `#faf8f5`. Ratio = 4.31:1 — just 0.19 below AA. **Fix options:** - **Option A (minimal, recommended):** change `text-bg` to `text-white` on the EmptyState button. In light mode `white on accent-light` = **4.57:1 PASS**; in dark mode `white on accent-dark` = 2.89:1 (FAIL-1 above). So if this single button changes to `text-white`, it gains 0.26 headroom in light but joins the FAIL-1 pool in dark. - **Option B:** Keep `text-bg` but fix FAIL-1 globally by swapping all accent buttons in dark mode to `text-bg`. That gives EmptyState 6.68:1 in dark and 4.31:1 in light — light still fails marginally. - **Option C (cleanest):** Use a conditional: `text-bg` in dark (passing), `text-white` in light (passing). Requires a theme-conditional class. - **Option D:** Accept the 4.31:1 fail for v0.1 (EmptyState appears only when no recordings exist — it is rarely seen by returning users). Residual for v0.1.1. --- ## Fixes Applied Inline **None.** Per task constraints, no colour tokens were modified. All fails are reported for dispatcher review. --- ## Residual Items for v0.1.1 The following are documented as known contrast gaps. The app ships honest about them; none affect primary reading text. | ID | Fail | Impact | Recommended action | |---|---|---|---| | CA-1 | DARK white on accent buttons | HIGH — multiple primary CTAs in dark mode | Swap buttons to `text-bg` (no token change) or await brand decision on accent darkening | | CA-2 | DARK white on danger record button | MEDIUM — primary CTA in recording state | Swap icon/label to `text-bg` in dark mode | | CA-3 | DARK/LIGHT white on warning loading state | LOW — transient, non-interactive, 2–10 s | Replace loading state with neutral palette | | CA-4 | LIGHT warning text on white surfaces | MEDIUM — EnergyChip and any bare `text-warning` on white card | Darken light `--color-warning` or swap EnergyChip to `text-text` | | CA-5 | LIGHT bg-page on accent (EmptyState) | LOW — empty state screen, rare | Use `text-white` conditionally or fix CA-1 globally first | --- ## What Passes (Confirmed) - All body text, secondary text, and tertiary text on all surfaces (dark + light + all zones): **12/12 PASS** - All StatusPill and LlmStatusChip label text: **PASS** in both themes - All 6px/7px coloured decorative dots (non-text 3:1 threshold): **8/8 PASS** - All zone variant bg-card surfaces: **6/6 PASS** - Light-theme white on accent (all accent buttons in light mode): **PASS** at 4.57:1 - Light-theme white on danger: **PASS** at 6.52:1 - Light-theme white on success: **PASS** at 5.84:1 - All semantic text labels (`text-success`, `text-danger`, `text-warning`) on tinted (`/10`, `/20`) backgrounds in dark mode: **PASS** --- ## Methodology Note Contrast ratios were computed in Python using the WCAG 2.1 relative luminance formula (sRGB linearisation at threshold 0.03928, gamma 2.4). Alpha-composited colours (e.g. `bg-warning/10` Tailwind utilities) were blended against the documented underlying surface before ratio computation. No browser rendering was used; values assume full opacity unless stated.