Skip to content

v4.36 — Tint-Ink Contrast Floor

Same-hue ink on a same-hue tint no longer ships illegible — measured once, across every hue, mode, and theme, instead of re-derived one site at a time.

Summary

The same defect — text or an icon rendered in a hue's own ink color on top of that hue's own tinted background (bg-<hue>/N text-<hue>) — had been fixed one site at a time, three separate times: OBJ-1951 fixed one site, OBJ-2047 closed as a no-op (the "locked pattern" every later ticket inherited had never actually shipped anywhere), and OBJ-2056 fixed one more. Each pass re-derived the fix from scratch and landed on a different answer, because nobody had measured the whole surface at once.

v4.36 did that measurement. Sweeping master and computing real WCAG 2.x contrast — composited over the actual surface token, not an assumed flat backdrop, across both light and dark mode and all 9 supporter themes — found 109 true defects across 66 files, more than three times the stale estimate the milestone was scoped against, plus a more severe class nobody had named yet: text-destructive used as ink measured 1.77:1 in dark mode, because dark --destructive is a fill color, not an ink color, and using it as one is nearly illegible.

The headline finding is that there was never going to be one "locked pattern" to apply everywhere. The correct fix varies by hue, by tint alpha, by mode, and by supporter theme. --warning-accessible was itself a broken token that no call-site edit could fix. The alpha ceiling for an ink swap turned out to be per-hue, not a single global rule. Two hue families — coach and gold — had no accessible ink token at all and were invisible to the original census. A fourth family, rarity-*, turned out to be a duplicate of coach under a different token name and was found only because Desi wrote a script to check for exactly that shape of duplication. So the deliverable is a measured matrix plus a CI guard that asserts it — not a pile of one-off edits — so a fourth re-derivation of this fix never has to happen again.

Goal

This milestone measures the whole surface once, writes the answer down, and installs a guard so the fourth re-derivation never happens.

Scope — What Shipped

  • Token-layer fixes: --warning-accessible darkened so it clears AA as ink on warning tints (Phase 1); --primary-accessible given a supporter-theme floor so the low-alpha ink swap holds over --background, not just --card (Phase 3a); new coach/gold accessible ink tokens, the first either hue has ever had (Phase 6); new rarity-* accessible ink tokens, closing a defect invisible to the original census because --rarity-epic turned out to be byte-identical to --coach-interactive under a different name (Phase 7).
  • Call-site remediation across both apps: text-destructivetext-destructive-accessible at 135 sites (87 ionic_frontend, 10 admin_dashboard — both apps consume the same design tokens and carried the identical live defect); a low-alpha ink swap at 68 sites; 18 high-alpha sites where no ink token clears AA at any value, each given a Desi-specified pattern instead of an alpha drop (5 patterns, 0 new tokens); 31 coach/gold sites; 18 rarity-* sites.
  • The durable artifact: a contrast unit spec (tintInkContrastMatrix.spec.ts) asserting the full (hue, alpha, mode, theme, surface) matrix, and a lint/AST guard (tintInkGuard.spec.ts) that fails CI on a same-hue tint/ink co-occurrence or an above-ceiling alpha. Hues are discovered from design_system/css/tokens.css at runtime rather than hand-listed — a hand-written hue list is exactly what hid coach/gold from the original census, and Phase 7 had to fix the guard's own discovery logic after it shipped blind to every hyphenated hue family, rarity-* included.
  • Docs: DESIGN.md's Palette Intent section gained a new hard-rule section for the matrix, the per-hue alpha ceiling, and the fill-vs-ink distinction; design_system/README.md's contrast contract was extended; docs/development/accessibility.md's App-Side AA-Violation Ledger got the full before/after record.

Phases

PhaseNameStatusPRHighlights
1Warning-accessible token floorShipped#2267Fixed the one broken token no call-site edit could have fixed.
2Dark-mode text-destructive remediationShipped#2269The most severe fix in the milestone — 135 sites across both apps.
3a--primary-accessible supporter-theme floorShipped#2275Added mid-milestone after a --card-only measurement proved false over --background.
3bLow-alpha ink swap batchShipped#227668 mechanical sites once re-bucketed on the corrected per-hue ceiling.
4High-alpha tint patternShipped#228218 sites where the failing axis was hue, not alpha — no ink token could clear AA at any value.
5Guard + fixtures + docsShipped#2290Shipped the durable artifact — with a hue-discovery gap Phase 7 later had to fix.
6coach + gold ink tokensShipped#229431 sites in a hue pair the original census regex never swept at all.
7rarity-* ink tokens + guard fixShipped#2293Added the day it shipped, after Desi's own blindspot-detection script found a fourth hidden hue family.

Promoted to master via PR #2304 (acef4a0f7, 2026-08-11).

Key Decisions

  • There is no single "locked pattern." Every prior pass at this defect class assumed one existed and re-derived a different answer each time. This milestone measured across hue × alpha × mode × theme × surface and found the correct fix genuinely varies on all five axes — the deliverable is the matrix and a guard that enforces it, not a style rule.
  • Alpha is held fixed at the 18 high-alpha sites rather than dropped to /10. Dropping alpha would have been insufficient for primary (fails even at /10) and unnecessary for success/warning/destructive (pass at /20 unaltered) — and would have flattened hover/selected-state visual affordances that alpha itself was carrying. HabitCalendar's selected-state indicator moved to an opaque border plus an accessible glyph instead of an alpha change.
  • Both apps, not just ionic_frontend. The kickoff census that Josh approved covered ionic_frontend only. admin_dashboard shares the same design_system token exports and carried the identical text-destructive defect — shipping the approved scope alone would have closed this milestone with a live 1.77:1 dark-mode defect still in production, in the other app.
  • The promotion kept master's newer token values over this milestone's own. master had independently shipped a strictly-more-accessible --primary-accessible recompute (OBJ-2425) while this milestone was in flight. The promotion conflict was resolved in master's favor on all 7 hunks — taking this milestone's slightly-less-accessible values would have regressed contrast at the exact moment this milestone was meant to end that risk.
  • Hues must be discovered from the token file, never hand-listed — and this rule had to be relearned twice inside one milestone. The original census regex hand-listed hues and missed coach/gold. The guard built specifically to prevent that shape of miss then filtered its own token-file scan through a regex that excluded every hyphenated hue name, missing rarity-*. Phase 7 fixed the guard; a related gap (per-line scan granularity) was found afterward and is tracked as follow-up work (OBJ-2472), not silently absorbed into "done."

Requirements Coverage

6 / 6 success criteria satisfied (this hand-authored milestone has no standalone REQUIREMENTS.md; criteria are drawn from the ROADMAP's own "Success criteria for the milestone" section — see the Milestone Audit for the full per-criterion table).

CategoryCountStatus
Defect closure (both apps, all themes, both surfaces)1Satisfied, with a known guard-visibility gap tracked as follow-up (OBJ-2472)
Token floors2Satisfied — --warning-accessible, --primary-accessible
Specified high-alpha pattern1Satisfied — 18/18 sites
CI guard1Satisfied, amended once post-ship for hue-discovery
Docs1Satisfied

Full requirements/success-criteria detail: v4.36-tint-ink-contrast-floor-ROADMAP.md on GitHub.

Outcomes

Every text or icon element rendered as ink on a same-hue background tint, across both apps, in both modes, the default theme, and all 9 supporter presets, now clears WCAG AA — a live defect class that had shipped, undetected, through three prior remediation attempts. Contributors adding a new tinted surface get a CI guard that fails the build on a reintroduction of the pattern or an above-ceiling alpha, instead of relying on a manual review to catch it. DESIGN.md and design_system/README.md now document the per-hue alpha ceiling and the fill-vs-ink distinction directly, so the next contributor who reaches for -accessible doesn't have to re-measure it.

Tech Debt

  • (Phase 5, found after ship, OBJ-2472) The shipped guard scans source files line by line, so a parent element carrying the tint and a child element carrying the ink on the next line is structurally invisible to it. 109 candidate sites surfaced, untriaged; 2 of 3 spot-checked were real defects. A green guard run does not currently mean zero same-hue defects — it means zero same-line same-hue defects. Backlog, assigned to Roy.
  • (Recorded by the ROADMAP) workorder.mjs, the generator used to census remaining work throughout this milestone, scopes its "0 unowned" reading to a hand-written 7-hue list. Every clean census reading in this milestone's history, including this close's own re-run, is a pass over that 7-hue denominator — accurate about what it measured, but not a whole-codebase guarantee.
  • (Deliberately deferred, recorded by the ROADMAP) 10 hover:bg-<hue>/5 sites are exempt from AA since their rest state already passes — a recorded deferral, not a silent pass.
  • acef4a0f7 — Promotion to master (PR #2304)
  • db424eb91 — Phase 7: rarity-* ink tokens + guard hue-discovery fix (PR #2293)
  • 7607a6fd4 — Phase 6: coach + gold ink tokens (PR #2294)
  • 00205a76a — Phase 5: guard, fixtures, docs (PR #2290)
  • 1a0d90519 — Phase 4: high-alpha tint pattern (PR #2282)
  • 1d5fdda28 — Phase 1: warning-accessible token floor (PR #2267)

Last updated: 2026-08-12 Version: v4.4.6

Loading…