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.
14 KiB
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).#0f0e0con#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
#9a5c2eto 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(#0f0e0con#e85f5f= 5.73:1 PASS). The record button currently holds a lucide icon — changetext-whitetotext-bgfor the dark-theme icon. - Option B: darken
--color-danger(dark) from#e85f5fto approximately#c43838to 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-bgtotext-whiteon the EmptyState button. In light modewhite on accent-light= 4.57:1 PASS; in dark modewhite on accent-dark= 2.89:1 (FAIL-1 above). So if this single button changes totext-white, it gains 0.26 headroom in light but joins the FAIL-1 pool in dark. - Option B: Keep
text-bgbut fix FAIL-1 globally by swapping all accent buttons in dark mode totext-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-bgin dark (passing),text-whitein 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.