Skip to content

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 -accessible family).
  • Supporter-theme preset AA clamp — all 10 presets in ionic_frontend/src/theme/supporter-themes.css hue-preserving lightness-clamped so solid --primary + foreground clears 4.5:1 and --ring vs --card clears 3.0:1 in light mode, matching the semantics deriveCustomTheme() already ran live for custom Supporter colors.
  • App-wide zero-specificity focus rule — a single :focus-visible base rule wrapped in :where(...) (ionic_frontend/src/theme/tailwind.css) replaces per-component focus styling; 607 outline-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 changeCelebrationBalanced.vue / CelebrationCinematic.vue moved from a translucent glass card over a heavy scrim to an opaque hsl(var(--card)) surface in both modes, following the existing FeatureTour.vue opaque-surface precedent.
  • Accent ink, both directions--accent-foreground (ink on an accent fill) fixed on 10/10 presets via per-preset bestForeground(); --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.md gained 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.
  • Docsdocs/development/accessibility.md's AA-violation ledger, ionic_frontend/STYLE_GUIDE.md's Tour overlay cross-link, and one consolidated CHANGELOG.md entry (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

PhaseNameStatusPlansHighlights
1Supporter-theme preset AA clampShipped1Foundation — all 10 presets clamped to clear both AA floors; gated every later phase
2Primary CTA ink ruleShipped1New --primary-ink token; 39 anchors migrated across 35 files; extends OBJ-1426's marketing rule to the app
3Focus-ring visibilityShipped1Zero-specificity app-wide :focus-visible rule; 607 opt-out sites deleted across 194 components
5Preset accent ink + interactive-token parityShipped1--accent-foreground fixed on 10/10 presets; --primary-interactive/--primary-accessible role split; dark-cascade bug fixed
6Accent-as-ink token + call-site sweepShipped2New --accent-accessible token; 29-anchor sweep across 23 files; only phase to reach 3 fix rounds (stacked-PR trap, resolved by cherry-pick)
4Celebration card surface + docs closeShipped2Opaque 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-ink token, 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-accessible created independently by two phases, resolved on measurement, not a coin flip. Phase 2 needed brand ink for its bg-clip-text ruling; 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-mode midnight value (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-hierarchy shipped every phase and GA'd its flag without ever reaching master), this milestone's close explicitly ran check-branch-promotion.mjs contains and confirmed every file the integration branch created is present in master before 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.md carries 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-accessible token.
  • (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/slate presets, 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.
  • 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

Loading…