Files
Lumotia/docs/release/v0.1-contrast-audit.md
Jake 3770815fbf
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia — v0.1 release-completion run
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.
2026-05-15 06:59:08 +01:00

254 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 210 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, 210 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.