Closes the code-side v0.1 ship gate. All quality gates green: cargo fmt/clippy/test (~327 tests), npm check (0/0), vitest 13/13, scripts/dogfood-rebrand-drill.sh 8/8. Phase F — first-run onboarding promoted to v0.1 - FirstRunPage with skip-to-main + failure recovery + event recording - Six onboarding commands (record/list/has-completed + lumotia_events) - Storage migration v17 (onboarding_events + lumotia_events tables) UI hardening (in-scope items from v0.1-ui-hardening.md) - StatusPill + PostCaptureCard components, 21st preview entry - Sidebar recording-as-sacred-state (opacity + aria-disabled, reduced-motion) - Settings 6-section regroup + Help section + Activation log + Privacy toggle - Error-state copy sweep (DictationPage + SettingsPage, plain-language) - Global :focus-visible rule, textarea outlines restored - Ctrl+K / Ctrl+, / Escape bindings in +layout LLM resilience - rule_based_extract_tasks (regex-free imperative-verb extractor) + extract_tasks_with_fallback wrapper — task extraction never returns zero - tokio::time::timeout(120s) wraps cleanup/tags/tasks commands Release artefacts - LICENSE (canonical AGPL-3.0), CHANGELOG (Keep-a-Changelog format) - v0.1-release-notes, privacy-and-ai-use, install-warnings, tester-onboarding-kit, tester-acceptance-runbook, code-signing-setup, apple-silicon-rb08-runbook, virtual-audio-setup, v0.1-contrast-audit - Workspace versioning + AGPL spdx; npm exact-pin (10 ranges removed) - AppImage SHA-256 sidecar in build.yml - README v0.1 section + Reporting-issues; canonical repo slug Closure pass — items moved from human-required to code-complete - KI-02 Linux idle inhibit: zbus 5 → org.freedesktop.login1.Manager.Inhibit - KI-03 Windows sleep prevention: SetThreadExecutionState(ES_CONTINUOUS|...) - acquire/release_idle_inhibit Tauri commands, wired in DictationPage - Diagnostic-bundle frontend wire-up (Settings → Help button) - WCAG-AA contrast fix via .btn-filled-text utility (no token changes) - 8 destructive-action sites wrapped in plain-language confirm() guards - KNOWN-ISSUES.md + v0.1-known-limitations.md updated (KI-02/03 fixed) Scripts - pre-tag-verify.sh, tag-day.sh, smoke-linux + driver - parse-diagnostic-bundle.sh, parse-activation-log.py Per-item audit trail: docs/release/v0.1-completion-status.md Remaining: W-01…W-08 (signing certs, hardware probes, smoke matrix, tester recruitment) — see docs/release/v0.1-known-limitations.md.
254 lines
14 KiB
Markdown
254 lines
14 KiB
Markdown
# 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.
|