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-accessibledarkened so it clears AA as ink onwarningtints (Phase 1);--primary-accessiblegiven a supporter-theme floor so the low-alpha ink swap holds over--background, not just--card(Phase 3a); newcoach/goldaccessible ink tokens, the first either hue has ever had (Phase 6); newrarity-*accessible ink tokens, closing a defect invisible to the original census because--rarity-epicturned out to be byte-identical to--coach-interactiveunder a different name (Phase 7). - Call-site remediation across both apps:
text-destructive→text-destructive-accessibleat 135 sites (87ionic_frontend, 10admin_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); 31coach/goldsites; 18rarity-*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 fromdesign_system/css/tokens.cssat runtime rather than hand-listed — a hand-written hue list is exactly what hidcoach/goldfrom 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
| Phase | Name | Status | PR | Highlights |
|---|---|---|---|---|
| 1 | Warning-accessible token floor | Shipped | #2267 | Fixed the one broken token no call-site edit could have fixed. |
| 2 | Dark-mode text-destructive remediation | Shipped | #2269 | The most severe fix in the milestone — 135 sites across both apps. |
| 3a | --primary-accessible supporter-theme floor | Shipped | #2275 | Added mid-milestone after a --card-only measurement proved false over --background. |
| 3b | Low-alpha ink swap batch | Shipped | #2276 | 68 mechanical sites once re-bucketed on the corrected per-hue ceiling. |
| 4 | High-alpha tint pattern | Shipped | #2282 | 18 sites where the failing axis was hue, not alpha — no ink token could clear AA at any value. |
| 5 | Guard + fixtures + docs | Shipped | #2290 | Shipped the durable artifact — with a hue-discovery gap Phase 7 later had to fix. |
| 6 | coach + gold ink tokens | Shipped | #2294 | 31 sites in a hue pair the original census regex never swept at all. |
| 7 | rarity-* ink tokens + guard fix | Shipped | #2293 | Added 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 forprimary(fails even at/10) and unnecessary forsuccess/warning/destructive(pass at/20unaltered) — 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 coveredionic_frontendonly.admin_dashboardshares the samedesign_systemtoken exports and carried the identicaltext-destructivedefect — 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.masterhad independently shipped a strictly-more-accessible--primary-accessiblerecompute (OBJ-2425) while this milestone was in flight. The promotion conflict was resolved inmaster'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, missingrarity-*. 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).
| Category | Count | Status |
|---|---|---|
| Defect closure (both apps, all themes, both surfaces) | 1 | Satisfied, with a known guard-visibility gap tracked as follow-up (OBJ-2472) |
| Token floors | 2 | Satisfied — --warning-accessible, --primary-accessible |
| Specified high-alpha pattern | 1 | Satisfied — 18/18 sites |
| CI guard | 1 | Satisfied, amended once post-ship for hue-discovery |
| Docs | 1 | Satisfied |
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>/5sites are exempt from AA since their rest state already passes — a recorded deferral, not a silent pass.
Related Artifacts
- Roadmap: v4.36-tint-ink-contrast-floor-ROADMAP.md
- Milestone Audit: v4.36-tint-ink-contrast-floor-MILESTONE-AUDIT.md
- Contrast spec + guard:
tintInkContrastMatrix.spec.ts,tintInkGuard.spec.ts - Triggering issue: OBJ-2427
- Prior instances of this defect class: OBJ-1951, OBJ-2047 (closed no-op), OBJ-2056
- Follow-up: OBJ-2472 (guard's per-line scan granularity)
- Merge PR: #2304
Related Commits
acef4a0f7— Promotion tomaster(PR #2304)db424eb91— Phase 7:rarity-*ink tokens + guard hue-discovery fix (PR #2293)7607a6fd4— Phase 6:coach+goldink 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