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

14 KiB
Raw Blame History

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-whitetext-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.