v4.42 — Tint Alpha Governance
A design-system ticket about nine ungoverned tint alphas turned out to be two structural guard gaps and a 104-entry allowlist leak. This milestone closed both, fixed the worst measured contrast defect of any tint-ink milestone to date (1.32:1), and only then shipped the named tint scale OBJ-1986 originally asked for.
Summary
OBJ-1986 was filed during v4.26 as a narrow observation: accent tint backgrounds were hand-picked at nine different alpha values with no governing rule. Sizing it as an epic and re-censusing at kickoff found the real problem sat one layer underneath. Of the 14 hues that declare an -accessible ink token, only 6 carried an enforced contrast ceiling — the other 8 had a fix target and no check. And tintInkGuard.spec.ts's allowlist held 104 entries, 78 of them reasoned against OBJ-2472, an issue that had already shipped done — an open triage against a closed ticket, silently carrying real, in-scope defects with no owner.
Phase 1 closed the first gap by extending the existing measurement harness to all 14 hues rather than building a second one — the milestone's own declared top risk, named at kickoff and avoided in practice. Phases 2 and 3 then triaged all 78 orphaned entries plus 19 hover-deferred sites nobody had re-verified since they were written, dissolving nearly all of them structurally (the guard now computes the exemption instead of listing it) and finding exactly one real defect: a "Change" chip button reading 2.73:1. Phase 4 fixed the three sites Phase 1's new ceiling actually flagged. Phase 5 then widened the guard itself — to raw-CSS alphas, bracket-syntax Tailwind classes, and two more scan roots — which is what surfaced the milestone's real teeth: 52 new findings (Phase 5b) including eight genuine celebration-tier defects, and a -foreground-as-ink scanner blind spot that let BadgeModal.vue's Legendary rarity badge ship at 1.32:1 in dark mode, worse than anything either of the two prior tint-ink milestones found. That blind spot became Phase 5c, inserted mid-milestone and closed before Phase 6 could open, so Phase 6's completion claim would be honest against a lens that could actually see the whole defect class.
Phase 6 shipped OBJ-1986's original ask — but not as filed. A flat, shared alpha scale is exactly what DESIGN.md § v4.36 rule 3 already measured and rejected as "this milestone's own first guess, and it was wrong." The scale that shipped instead is 42 --ink-safe-<hue>-<level> tokens, one triple of wash/fill/max per ceilinged hue, each resolving through that hue's own ceiling rather than a shared number — so --ink-safe-primary-max is 0.10 and --ink-safe-rarity-legendary-max is 0.39, both provably AA-safe in their own right. 50 existing call sites migrated onto it; 233 above-ceiling raw occurrences were deliberately left alone — 37 of them background-role decorative fills and scrims with no ink riding on them, the other 196 border/box-shadow/filter properties that were never eligible for the ink-safe scale regardless of alpha — because migrating them would change nothing about accessibility and everything about intent. tintInkGuard.spec.ts now asserts expect(ALLOWLIST).toEqual([]) directly, the same discipline v4.40 established. The allowlist's arc across the milestone: 104 → 17 → 9 → 7 → 0.
Goal
Two things have to be true at milestone close. One — every hue that has an ink token has an enforced alpha ceiling, and no live site sits above it. Two — the tint guard's allowlist is empty. Only once both hold does the third thing — OBJ-1986's original ask, a governed tint scale — mean anything. A named scale declared over a surface with 78 untriaged defects and 8 unceilinged hues would be a notation change dressed up as governance.
Scope — What Shipped
- Ceiling coverage (Phase 1, OBJ-2814):
SHIPPED_CEILINGextended from 6 to all 14 hues with a declared-accessibleink sibling, measured across 10 themes × 2 modes × both--card/--backgroundusing the existing v4.36 helpers.NO_CEILING_NEEDEDnames the other 25 discovered hues with a written reason. A sum assertion ties both lists todiscoverHues().lengthso a future undeclared hue fails loud instead of silently passing. - Cross-element triage —
primary(Phase 2, OBJ-2815): 52 orphanedCROSS_ELEMENT_ALLOWLISTentries on the thinnest-headroom hue in the system. Desi's triage protocol — computed exemption viaisDecorativeDescendant(), keyed on "carries no accessible name," never on element type — dissolved 51 as decorative and fixed the one real defect,GoalCreate.vue's "Change" chip (2.73:1 → 4.92:1). - Cross-element triage — tail + hover (Phase 3, OBJ-2816): the remaining 26 cross-element entries plus 19 never-re-verified hover-deferred sites. Both buckets went to
[]— the hover exemption made structural (accessibleClearsAaAtAlpha()/restPassesAA()) rather than case-by-case, since every site turned out to carry exclusively accessible ink underneath. - Fix the newly-ceilinged sites (Phase 4, OBJ-2817): the 3 genuine
accentsites Phase 1's new/14ceiling flagged (of ~9 the kickoff census over-estimated), fixed rather than re-allowlisted. - Guard coverage for raw-CSS alphas (Phase 5, OBJ-2818):
scanRawCssTints()extended to real CSS declarations and TS/JS object-literal string values; bracket-syntax Tailwind (bg-<hue>/[N]) matched;SCAN_ROOTSwidened todesign_system/srcandmarketing_landing. 404 raw occurrences reduced to 44 genuine flat-fill-plus-ink candidates by property-context classification. - Raw-CSS co-occurrence triage (Phase 5b, OBJ-2831, inserted): disposed all 52 findings Phase 5's widened lens parked, including 8 real celebration-tier
rarity-*/goldfixes. All four affected allowlist buckets independently re-verified[]three times over (Codi, Riley, Roy) as an anti-whitewash control. - The
-foreground-as-ink blind spot (Phase 5c, OBJ-2837, inserted): closed a scanner gap where a hue's own-foregroundtoken used as ink over that hue's tint matched neither of the guard's two ink tests. Fixed the milestone's worst measured defect (BadgeModal.vue, 1.32:1) plus three related sites (AchievementCard.vue, twoTimelineEvent.vuesites,RankChip.vue). Surfaced and re-specced a shape-based hazard assertion that would have false-positived two correctlydark:-guarded sites. - The governed tint scale, allowlist flip, and close (Phase 6, OBJ-2819): 42
--ink-safe-<hue>-<level>tokens declared indesign_system/css/tokens.css, each asserted at or below its hue's Phase 1 ceiling; 50 call sites migrated across 28 files;expect(ALLOWLIST).toEqual([])plus a live-tree zero-unlisted-violations scan; the-foregroundtoken contract ruled opaque-fill-only in DESIGN.md; all milestone docs landed.
Phases
| Phase | Name | Status | PR(s) | Highlights |
|---|---|---|---|---|
| 1 | Ceiling coverage for every hue with an ink token | Shipped | #2562 | SHIPPED_CEILING 6 → 14 hues; no second measurement path built |
| 2 | Cross-element triage — primary (52 entries) | Shipped | #2563 | 51 decorative-exempt, 1 real fix (GoalCreate.vue chip) |
| 3 | Cross-element triage — tail + hover re-verification | Shipped | #2573 | Remaining 26 + 19 hover sites → [], both made structural |
| 4 | Fix the sites Phase 1's ceilings newly flag | Shipped | #2575 | 3 accent sites fixed, CEILING_ALLOWLIST [] |
| 5 | Guard coverage for raw-CSS alphas and unscanned roots | Shipped | #2576 | Raw-CSS, bracket-syntax, and two new scan roots covered |
| 5b | Raw-CSS co-occurrence and ceiling triage (inserted) | Shipped | #2584 | 52 findings disposed; 8 real celebration-tier fixes |
| 5c | The -foreground-as-ink blind spot (inserted) | Shipped | #2587, #2590, #2594 | Fixed the milestone's worst defect, 1.32:1 → AA |
| 6 | The governed tint scale, allowlist flip, and close | Shipped | #2596, #2595, #2605, #2604, #2612 | 42 tokens declared; toEqual([]) flipped; docs landed |
Key Decisions
- The scale is per-hue-clamped, not a flat shared value — because the flat version was already tried and rejected. DESIGN.md § v4.36 rule 3 measured a flat α ≤ 0.10 rule during an earlier milestone and found it wrong. v4.42's 42 tokens each resolve through their own hue's measured ceiling instead, so
wash/fill/maxmeans something different — and is safe — per hue. - The scale is ink-safe only; above-ceiling sites are documented, not swept. Of 401 raw-CSS tint occurrences, 233 sit above their hue's ceiling. Only 37 of those are background-role — decorative fills, scrims, gradients with no ink riding on them; the remaining 196 are border/box-shadow/filter properties, never a tint under ink and never eligible for the scale regardless of alpha. None is a defect. Migrating either group onto the scale would be a blind sweep of the exact shape DESIGN.md § v4.37 warns against, for zero accessibility gain.
- "Assert measured, not shape-matched" — the milestone's most expensive lesson. Phase 5c's hazard assertion was originally specified as a shape check (no element pairing an opaque fill class with
-foregroundink). It fired on two sites that measure 7.35:1 and carry adark:override precisely to handle the hazard being asked about — a false positive of exactly the class the check existed to remove. Re-specced to assert the resolved contrast ratio directly. - The guard's own re-census always wins over this ROADMAP's prose. Stated at kickoff and proven true twice: Phase 4's site count came in at 3 against an indicative ~9, and Phase 5c's came in at 9 against an indicative 5, each time because the guard saw AST shapes and cross-file structure a hand-written sizing table couldn't.
- Prop-level mode gating that no test enforces is not a structural exemption.
RankChip.vue'sgoldtone looked light-mode-only at every real consumer, but its own Storybook story rendered it unconditionally — an unenforced invariant, not a guarantee. Fixed rather than exempted. - The
-foregroundtoken contract is opaque-fill-only, now written down. Phase 5c's defect happened because a token measured against an opaque fill (--rarity-legendary-foreground, 5.49:1) was read as ink over a 20% tint of the same hue (1.32:1) — the token wasn't wrong, the call site was reading a contract it never offered. DESIGN.md now states this explicitly rather than leaving it implicit. - Real opaque-fill defects found along the way were routed out, not folded in. Three
-foreground-on-solid-fill violations (bg-coach,bg-rarity-epicdark,hover:bg-destructive-interactivelight) surfaced during Phase 6's measurement pass. They're a different rule (Solid-Fill Own-Foreground Floor) than the tint-alpha class this milestone governs — filed as OBJ-2851 rather than expanding this close phase's scope.
Requirements Coverage
20 / 20 requirements satisfied — CEILING-1..3, TRIAGE-1..6, SITE-1..2, GUARD-1..4, SCALE-1..3, DOCS-1..3, per the milestone audit.
| Category | Count | Status |
|---|---|---|
| CEILING-1..3 | 3 | All satisfied |
| TRIAGE-1..6 | 6 | All satisfied |
| SITE-1..2 | 2 | All satisfied |
| GUARD-1..4 | 4 | All satisfied |
| SCALE-1..3 | 3 | All satisfied |
| DOCS-1..3 | 3 | All satisfied |
Full detail: MILESTONE-AUDIT.md on GitHub.
Outcomes
Every hue in the app that carries an ink token now has a measured, enforced AA contrast ceiling — 14 of 14, up from 6 — and the tint guard's allowlist, which held 104 entries at kickoff (78 of them a leak against a closed issue), is [], asserted directly rather than inferred from an empty diff. A call site that needs a same-hue tint under ink no longer hand-derives an alpha against a ceiling it may not know exists — it reaches for var(--ink-safe-<hue>-<level>), which already resolves correctly in every theme and mode. The single worst measured contrast defect either of Objectuve's two prior tint-ink milestones ever found (1.32:1, Legendary rarity badge, dark mode) is fixed. Two of the three guard gaps DESIGN.md's OBJ-2456 note had tracked since a prior milestone are now closed for real, not just marked closed on an issue tracker; the third — a same-hue tint layer stacked on another — is recorded as still open rather than silently dropped.
Tech Debt
- (OBJ-2851, routed out, not fixed here) Three solid-fill
-foregroundviolations found during Phase 6's measurement pass —bg-coach(3.69:1, all 20 theme×mode contexts, 7 call sites includingUiButton.vue:17),bg-rarity-epicdark (3.01:1),hover:bg-destructive-interactivelight (4.32:1). A different rule than tint-alpha governance; filed separately and routed to Roy for triage. - (carried on the accessibility ledger, not fixed here) A stacked-tint-layer scanner gap remains open — a same-hue tint composited on top of another tint layer is still invisible to
tintInkGuard.spec.ts. - (Phase 6, deferred census work)
opaqueFillHueIn()'s variant-prefix regex accepts any prefix includinghover:, so a hover-only opaque fill can technically excuse a resting-state pairing. Provably hiding nothing at milestone close; flagged for future tightening, not fixed here. - (ROADMAP self-correction, recorded by this audit) The milestone ROADMAP cites Phase 6 Task 1's content as landing via commit
18ae4dc02, which is not itself an ancestor ofmaster— the content folded into Task 2's squash-merge (2c62b20ee) instead. No content is missing; the citation is stale.
Related Artifacts
- Roadmap: v4.42-tint-alpha-governance-ROADMAP.md
- Milestone audit: v4.42-tint-alpha-governance-MILESTONE-AUDIT.md
- Phase artifacts: .planning/phases/v4.42-phase-*/ (Phases 2, 5b, 5c, 6 carry a UI-SPEC)
- Guard:
ionic_frontend/tests/unit/tintInkGuard.spec.ts,ionic_frontend/tests/unit/tintInkContrastMatrix.spec.ts - Scale mechanics: docs/development/design-tokens.md § The ink-safe tint scale
- Accessibility ledger: docs/development/accessibility.md § App-Side AA-Violation Ledger, OBJ-1986 entry
- Design rule: DESIGN.md § Accessibility hard rules — v4.42 Governed Tint Scale
- Follow-up issue: OBJ-2851 — Solid-Fill Own-Foreground Floor violations, spun out during this milestone
Related Commits
b462f0775— Phase 1: ceiling coverage extended to 14 hues (#2562)f98b791b5— Phase 2:primarycross-element triage, 52 entries (#2563)0e30b82c5— Phase 3: tail cross-element + hover re-verification (#2573)7335ea10f— Phase 4: fix the newly-ceilingedaccentsites (#2575)712734e6b— Phase 5: raw-CSS + bracket-syntax + widened scan roots (#2576)dd397633f— Phase 5b: raw-CSS co-occurrence triage, 52 findings disposed (#2584)927a573eb— Phase 5c:-foreground-as-ink blind spot closed (#2587, #2590, #2594)2c62b20ee— Phase 6 Task 2: 42 ink-safe tokens declared (#2596)dfd263cca— Phase 6 Task 3:expect(ALLOWLIST).toEqual([])flipped (#2595)07f2e35d7— Phase 6 Task 4: 50 call sites migrated across 28 files (#2605)25e1bec97— Phase 6 Task 5: DESIGN.md rulings recorded (#2604)0d4128320— Phase 6 Task 6: scale mechanics, ledger, CHANGELOG, ROADMAP close (#2612)
Last updated: 2026-08-27