11# check-a11y-contrast
22
3- Design-time WCAG 2.2 contrast checker for the Cmdr desktop app.
3+ Design-time contrast checker for the Cmdr desktop app: WCAG 2.2 AA (the primary gate) plus an enforced APCA Lc-45
4+ perceptual floor.
45
56## Why
67
@@ -23,11 +24,11 @@ go run ./scripts/check-a11y-contrast
2324# Via check runner
2425pnpm check a11y-contrast
2526
26- # Verbose (show warnings from unresolvable values)
27+ # Verbose (warnings from unresolvable values + the full APCA advisory detail )
2728go run ./scripts/check-a11y-contrast -- --verbose
2829```
2930
30- Exit code 0 on clean, 1 on any violation.
31+ Exit code 0 on clean, 1 on any violation (a WCAG pair below threshold OR an APCA pair below the Lc-45 floor) .
3132
3233## What it checks
3334
@@ -71,6 +72,24 @@ stripped (those carry old-WebKit hex fallbacks that would otherwise overwrite th
7172more than one — for example a small block carrying the selection-fg fallback rule sits before the main dark token
7273table).
7374
75+ ## APCA floor (perceptual second opinion)
76+
77+ On top of the WCAG gate, the tool runs every evaluated pair through APCA (the Accessible Perceptual Contrast Algorithm,
78+ the method explored for WCAG 3), using the canonical ` apca-w3 ` 0.1.9 (W3) constants. APCA predicts real readability
79+ better than WCAG 2: it's polarity-aware (dark-on-light and light-on-dark differ, which WCAG 2 treats as identical) and
80+ accounts for font size and weight. Output is ` Lc ` ("lightness contrast"), signed, |Lc| ~ 0..108. See ` apca.go ` .
81+
82+ - ** Enforced** : any pair below ** APCA Lc 45** (APCA's "absolute minimum for any text") fails the check, alongside WCAG.
83+ - ** Advisory** (printed only with ` -verbose ` ): the full ` |Lc| ` distribution, a "blast radius if bar X were the gate"
84+ table, a zoom sweep (zoom relaxes the size-based target; it does not change ` Lc ` ), and every pair below its
85+ font-size/weight-aware target. Non-verbose runs print a one-line summary plus the floor verdict.
86+ - ** Why a floor, not the full APCA ladder** : APCA's preferred body level (Lc 75–90) would flag most of the app's
87+ 14px/400 text — that 45–60 band is design intent (de-emphasized placeholders, hints, disabled), not a bug. We gate
88+ only the hard floor and keep the muted band advisory.
89+ - ** Status** (verified 2026-06-30): APCA was removed from the WCAG 3 draft (2023) and now develops independently (ARC);
90+ its core math has been stable since 2022, which is why we enforce one conservative bar and keep the rest advisory.
91+ WCAG 2.2 AA stays the primary, legally-recognized gate.
92+
7493## Output
7594
7695```
@@ -117,7 +136,9 @@ analyzer.go Walks parsed rules per mode, tracks cascade state by
117136 compound class set, emits Finding per (selector, mode) pair.
118137 Uses `evaluateAt` to run worst-case across accent variants
119138 when the pair is accent-sensitive.
120- reporter.go Pretty prints violations and optional warnings.
139+ reporter.go Pretty prints WCAG violations and optional warnings.
140+ apca.go APCA 0.1.9 (W3) Lc math, the font-size/weight target
141+ ladder, the enforced Lc-45 floor, and the advisory report.
121142accent_matrix.go Runtime accent variants (the 8 macOS system accents +
122143 Cmdr gold) and the per-variant VarTable override.
123144size_tiers.go `.size-*` utility classes × known container bgs, since
@@ -140,6 +161,8 @@ Tests:
140161- ` parser_test.go ` : app.css + Svelte parsing, selector extraction.
141162- ` analyzer_test.go ` : cascade inheritance, known false-positive cases.
142163- ` accent_matrix_test.go ` : variant sweep + per-variant resolution.
164+ - ` apca_test.go ` : APCA reference values (black-on-white ≈ Lc 106, white-on-black ≈ −108), polarity asymmetry, target
165+ ladder.
143166
144167Diagnostic helpers (skipped by default; gated on env vars):
145168
@@ -163,7 +186,8 @@ Edit `namedColors` map in `contrast.go`.
163186
164187### Tune thresholds
165188
166- WCAG AA (current) uses 4.5:1 / 3:1. For AAA, change the constants in ` analyzer.evaluate ` (7:1 / 4.5:1).
189+ WCAG AA (current) uses 4.5:1 / 3:1. For AAA, change the constants in ` analyzer.evaluate ` (7:1 / 4.5:1). The enforced
190+ APCA floor is ` apcaFloor ` in ` apca.go ` (Lc 45); the advisory size/weight target ladder is ` apcaTiers ` there.
167191
168192### Add an allowlist for intentional violations
169193
0 commit comments