v4.26 — Light-Mode Contrast Floor
Light mode now clears WCAG AA on the primary CTA, the focus indicator, the celebration card, and accent ink everywhere it appears — and the rules that decide it are written down so the next surface inherits them instead of relitigating them.
Summary
Objectuve's primary call-to-action gradient, its only focus affordance, and its streak-celebration card had all shipped with light-mode contrast failures — not edge cases, but the app's highest-traffic interaction surfaces. The primary CTA gradient measured 4.34:1 at its dark stop and 2.35:1 at its light stop against a 4.5:1 floor, reproduced identically across 49 sites in 35 files. The app-wide focus indicator was already compliant at 4.54:1, but 546 (later measured at 607, as the sweep ran) outline-none declarations across the component tree disabled it in favor of a sub-3:1 tinted ring — the accessibility baseline for every keyboard and switch user, silently opted out of everywhere. The celebration card's secondary label, rendered on a translucent glass surface over a heavy scrim, composited to mid-grey and measured 3.42–3.76:1 depending on backdrop.
A foundation finding reordered the milestone before it started: 8 of the app's 10 supporter-theme presets failed AA on a plain solid primary button, and 2 failed the 3:1 focus-ring floor even at full opacity — meaning neither the CTA fix nor the ring fix could be verified without a preset clamp landing first. That became Phase 1. Two more findings, surfaced by Desi while producing the Phase 1 spec, were folded into the milestone rather than shipped as a follow-up chain: preset accent-fill ink (white text failing AA on 10 of 10 accent-colored badges and chips) and the reverse pairing — accent used as ink directly on the page background, also failing on all 10 presets.
Six phases shipped between 2026-08-01 and 2026-08-02, landing ten PRs on the milestone's integration branch, then promoted to master as a single merge. The five fix phases and the docs-close phase together record six hard rules in DESIGN.md, in the same register as the pre-existing "cyan is decoration-only" and amber-lightness rules — so a future accent, ink, or focus decision on this app has a rule to look up rather than a contrast bug to rediscover.
Goal
Make light mode clear WCAG AA on the app's three highest-traffic interaction surfaces — the primary CTA, the focus indicator, and the celebration card — and record the rules that decide them in
DESIGN.md, so the next surface inherits the decision instead of relitigating it.
Scope — What Shipped
- New design tokens (
design_system/css/tokens.css):--primary-ink/--primary-ink-foreground(text-bearing CTA fill),--focus-ring(aliases--primary-accessible),--primary-accessible(brand ink split out of the overloaded--primary-interactive),--accent-accessible(accent used as ink, completing the-accessiblefamily). - Supporter-theme preset AA clamp — all 10 presets in
ionic_frontend/src/theme/supporter-themes.csshue-preserving lightness-clamped so solid--primary+ foreground clears 4.5:1 and--ringvs--cardclears 3.0:1 in light mode, matching the semanticsderiveCustomTheme()already ran live for custom Supporter colors. - App-wide zero-specificity focus rule — a single
:focus-visiblebase rule wrapped in:where(...)(ionic_frontend/src/theme/tailwind.css) replaces per-component focus styling; 607outline-none/tinted-ring opt-outs deleted across 194 components, with a regression guard (tokenDrift.spec.ts) preventing new opt-outs from reintroducing the gap. - Celebration card surface change —
CelebrationBalanced.vue/CelebrationCinematic.vuemoved from a translucent glass card over a heavy scrim to an opaquehsl(var(--card))surface in both modes, following the existingFeatureTour.vueopaque-surface precedent. - Accent ink, both directions —
--accent-foreground(ink on an accent fill) fixed on 10/10 presets via per-presetbestForeground();--accent-accessible(accent used as ink on the page) fixed via a ~29-anchor call-site sweep across 23 files. A dark-mode cascade bug that silently reverted every preset's dark accent to legacy orange was fixed as part of the same work. DESIGN.mdgained a numbered "Accessibility hard rules — v4.26 Light-Mode Contrast Floor" section (6 rules), cross-linking the CTA rule to the existing OBJ-1426 marketing rule so the app and marketing read as one decision.- Docs —
docs/development/accessibility.md's AA-violation ledger,ionic_frontend/STYLE_GUIDE.md's Tour overlay cross-link, and one consolidatedCHANGELOG.mdentry (fix(a11y), OBJ-1955) covering all six rules. - No schema, no migration, no GraphQL, no feature flag — frontend and design-system tokens only. Rollback is a per-phase PR revert.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | Supporter-theme preset AA clamp | Shipped | 1 | Foundation — all 10 presets clamped to clear both AA floors; gated every later phase |
| 2 | Primary CTA ink rule | Shipped | 1 | New --primary-ink token; 39 anchors migrated across 35 files; extends OBJ-1426's marketing rule to the app |
| 3 | Focus-ring visibility | Shipped | 1 | Zero-specificity app-wide :focus-visible rule; 607 opt-out sites deleted across 194 components |
| 5 | Preset accent ink + interactive-token parity | Shipped | 1 | --accent-foreground fixed on 10/10 presets; --primary-interactive/--primary-accessible role split; dark-cascade bug fixed |
| 6 | Accent-as-ink token + call-site sweep | Shipped | 2 | New --accent-accessible token; 29-anchor sweep across 23 files; only phase to reach 3 fix rounds (stacked-PR trap, resolved by cherry-pick) |
| 4 | Celebration card surface + docs close | Shipped | 2 | Opaque card surface; all six rules recorded in DESIGN.md and cross-linked across three more docs |
Stage order (the execution order) ran 1 → 2 → 5 → 3 → 6 → 4 — Phase 5 started early and merged ahead of Phase 3, which is what produced the one real cross-phase token collision (see Key Decisions). Phase numbers are issue identities and never renumber.
Key Decisions
- Preset clamp shipped first, ahead of the source issue's own ordering — both the CTA ink rule and the focus ring's pass/fail were theme-dependent and unverifiable without a preset clamp landing first (2 of 10 presets failed the ring floor even at full opacity). Reordering cost nothing; skipping it would have shipped a fix nobody could confirm worked on gold or ocean.
- A new
--primary-inktoken, not a reuse of--primary-interactive— the latter fit numerically (5.65:1) but is semantically the hover state; reusing it as the CTA rest fill would have collapsed rest and hover into one color. --primary-accessiblecreated independently by two phases, resolved on measurement, not a coin flip. Phase 2 needed brand ink for itsbg-clip-textruling; Phase 5 was chartered to split the same role out of--primary-interactive. Both sets of values cleared AA in light mode — light mode didn't decide it. Phase 5's dark-modemidnightvalue (4.54:1) was kept over Phase 2's (4.31:1, would have shipped an AA failure), then Phase 2's assertions were re-run against Phase 5's values and passed with headroom.- Accent-as-ink split into its own phase (6) rather than folded into Phase 5 — different token, different binding surface (accent as ink on the page vs. ink on an accent fill), and a call-site sweep instead of a token edit. Folding it in would have roughly doubled a package already touching 105 call sites.
- No feature flag — locked by Josh at kickoff. Contrast is not A/B-able, and a partially-applied focus affordance is worse than either end state. Rollback is a per-phase PR revert, not a flag flip.
- Master promotion is a separate, hand-created step (OBJ-2041), and this milestone verified it before closing — per this repo's documented stranded-branch failure mode (
gsd/v4.15-dashboard-hierarchyshipped every phase and GA'd its flag without ever reachingmaster), this milestone's close explicitly rancheck-branch-promotion.mjs containsand confirmed every file the integration branch created is present inmasterbefore marking anything shipped.
Requirements Coverage
12 / 12 success criteria satisfied (quoted from v4.26-light-mode-contrast-floor-MILESTONE-AUDIT.md). This milestone predates this repo's standalone per-milestone REQUIREMENTS.md convention for hand-authored epics; coverage is derived from the ROADMAP's own 12-item "what must be TRUE at close" list, each independently re-verified against shipped source in the audit.
Full roadmap and success-criteria detail: v4.26-light-mode-contrast-floor-ROADMAP.md on GitHub.
Outcomes
- Every text-bearing primary CTA in the app renders at ≥4.5:1 in light mode, on every supporter theme, at both gradient stops.
- Every keyboard- and switch-reachable interactive element shows a ≥3:1 focus indicator in light mode, on every supporter theme — the indicator can no longer be silently opted out of per-component, and a regression guard prevents a new opt-out from reintroducing the gap.
- The celebration card's secondary label, dismiss hint, and XP chip all clear AA regardless of what's behind the card's scrim, because the card surface is now opaque rather than composited.
- Accent color is AA-safe both as a fill's ink and as ink on the page background, on all 10 supporter themes, in both light and dark mode.
DESIGN.mdcarries six new hard rules a future contributor can look up before shipping a new accent, ink, or focus decision, instead of re-deriving contrast math from scratch.
Tech Debt
- (fast-follow, backlog) OBJ-1985 — revert
MilestoneCard.vue's local contrast workaround now that Phase 6 ships a real--accent-accessibletoken. - (fast-follow, backlog) OBJ-1986 — design-system audit candidate: accent tints found at 9 distinct ungoverned alpha values across the codebase.
- (fast-follow, backlog) OBJ-2038 — axe/Storybook visual spot-check on the
sunset/midnight/slatepresets, an accepted Phase 6 gap (no live browser UAT ran on this phase; CI gates and code review substituted). - (non-blocking, noted at audit) No live keyboard tab-through was performed at any point in the milestone — every hop verified in sandboxes without a browser. Recommended as a human sanity pass, not a merge blocker.
Related Artifacts
- Roadmap: v4.26-light-mode-contrast-floor-ROADMAP.md
- Milestone Audit: v4.26-light-mode-contrast-floor-MILESTONE-AUDIT.md
- Design rules: DESIGN.md § Palette intent § Accessibility hard rules
- Accessibility ledger: docs/development/accessibility.md § App-Side AA-Violation Ledger
- Triggering issue: OBJ-1955
- Coordination anchor: OBJ-1958
- Merge PR (master promotion): #1995
Related Commits
86460a4f1— chore(v4.26): promote Light-Mode Contrast Floor to master (OBJ-2041) (#1995)bd705a70c— Phase 1: supporter-theme preset AA clamp (#1933)8052d99c2— Phase 2: primary CTA ink rule (#1962)42837088c— Phase 3: focus-ring visibility (#1976)e6fb8b9ec— Phase 5: preset accent ink + interactive-token parity (#1968)4dbe9873c/398a6dd62— Phase 6: accent-as-ink token + call-site sweep (#1979 / #1980)fa130e592— Phase 4: celebration card surface + docs close (#1989 / #1993)
Last updated: 2026-08-02 Version: v4.26