v4.44 — Gradient Stop Ink Contract
Every stop of every gradient token now carries the same measured ink contract as its first stop, and a guard fails the build the moment a new one doesn't.
Summary
Before this milestone, no guard in the tree had ever looked at a gradient token's individual stops. --gradient-gold's second stop — a raw HSL literal, hsl(38 92% 50%) — had never been measured against the ink painted on top of it, and scored 3.97:1: below the 4.5:1 AA floor, in all 20 theme×mode contexts, every day since the token was authored. The filed defect (OBJ-2868) named that one instance. The milestone's real finding, from Phase 1's census, was that the gap was never gold-shaped: design_system/css/tokens.css carried 40 --gradient-* declarations across 22 token names, and the tint-ink guard saw zero of them. A raw-literal gradient stop declared in :root matches neither the guard's hue-keyed value shape nor its same-block ink-co-location model — the two together made every gradient stop in the token file structurally invisible, not merely unmeasured.
Six phases (five planned, Phase 3 split into 3a/3b mid-milestone by Phase 1's own sizing) shipped in three calendar days, gated for two of those days behind v4.42 Tint Alpha Governance closing (Phase 4 extends the same two files v4.42 was actively rewriting). Phase 1 resolved the one prerequisite question — which of two four-way-divergent --gradient-gold declarations the shipped app actually serves — from a real vite build artifact rather than reasoning about Tailwind's layer handling, then built a call-site-first measurement harness that overturned two of its own upstream naming-convention assumptions in both directions: --gradient-coach pairs with literal text-white at zero --coach-foreground call sites, and --gradient-primary has four text-white outliers hitting 1.66:1 in dark mode — the worst fixable number in the whole census. Phase 2 applied Desi's ruling for gold's stop B (hsl(43 96% 66%), holding hue and saturation byte-identical to stop A — the same shape --gradient-primary and --gradient-accent already travel in) and deleted four dead shadow declarations in variables.css that Phase 1 proved were already unreachable in every mode. Phases 3a and 3b ran in parallel, sweeping the gold consumers and repairing the coach/primary ink pairings respectively, with deliberately asymmetric fix shapes — a background swap for coach CTAs, an ink-class swap for primary outliers — that the plan explicitly warned not to generalize from one to the other. Phase 4 built the guard, catching itself before implementation: a pre-dispatch addendum found that the parser extension alone would produce zero behaviour change, because tokens.css was never inside the scanned root list and every declaration there is a custom property the guard's background-property check rejects by construction. The real scope — widen the scan surface, add a custom-property-aware path, build the cross-file token→call-site ink model the same-block guard structurally lacked — was proven non-vacuous with a red-then-green control before any allowlist assertion was trusted. Phase 5 wrote the rule into DESIGN.md, the honest deferral into the AA-violation ledger, and closed the milestone.
Goal
Every stop of every gradient token carries the same ink contract as its first stop, that contract is measured rather than assumed, and a guard fails the build when a new gradient stop is written that nothing has measured.
Scope — What Shipped
- Token repair:
--gradient-goldstop B re-valued fromhsl(38 92% 50%)tohsl(43 96% 66%)(design_system/css/tokens.css:220) — 3.97:1 → 5.63:1 against its own ink, worst number now 5.02:1 at the untouched stop A.variables.css's four dead shadow declarations (--gradient-gold/--gradient-primary/--gradient-accent/--gradient-coach) deleted;DESIGN.md's recorded value moved with the token in the same commit. - Call-site repairs: the coach gradient's four CTA buttons (
CoachRoadmapIntro.vue,CoachGoalHero.vue,GoalQualityCheck.vue,CoachSuggestSheet.vue) repointed at a newly-ported--gradient-coach-inkbackground token (3.96/4.31:1 → 7.01/7.24:1); four--gradient-primarysites (GoalEventForm.vue×2,RoadmapComposer.vue,AllyLookupModal.vue) swapped from literaltext-whitetotext-primary-foreground(worst 1.66:1 dark-mode fixed). - Guard:
ionic_frontend/tests/unit/tintInkGuard.spec.tsgained a gradient-stop scanner —design_system/cssadded toSCAN_ROOTS, a custom-property-aware declaration path, one-levelvar()-alias resolution, and a cross-file token→call-site ink model built from Phase 1's hand-verified pairing ledger. Completion gate is a 9-entry, 6-token stop-keyed allowlist (GRADIENT_INK_ALLOWLIST), proven non-vacuous with a deliberately-broken-then-repaired control stop. - Docs:
DESIGN.md§ Accessibility hard rules — v4.44 Gradient Stop Ink Contract (new);docs/development/accessibility.md§ App-Side AA-Violation Ledger (new OBJ-2868/OBJ-2869/OBJ-2876 entry);docs/reference/desi-design-context.md§ Anti-patterns (new bullet: don't assume a gradient token's ink is its-foregroundsibling without checking the real call site);CHANGELOG.md[Unreleased]. - Filed, not built here: OBJ-2876 (six supporter-theme
--gradient-cover-*/--gradient-tone-*pairs below the 3:1 glyph floor, worst 1.64:1 — architectural, allowlisted with an expiry condition), OBJ-2928 (--gradient-hero's two inked consumers outside Phase 1's census scope), OBJ-2930 (crew git-identity instability, surfaced twice this milestone as--adminmerge bypasses — explicitly not v4.44 scope).
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | Resolve the effective token, census every gradient stop | Shipped | 1 | Proved tokens.css is the effective declaration from a real build artifact; overturned two naming-convention assumptions in both directions; Desi ruled gold's repair value. |
| 2 | Repair --gradient-gold, delete the shadow declarations | Shipped | 1 | Applied the ruled stop B; deleted four dead shadow declarations; caught and fixed a harness self-check that had baked in the pre-fix value as "known-good." |
| 3a | Sweep the gold consumers | Shipped | 1 | Propagated the repaired stop B across mockups, marketing CSS, and a design-system value that had drifted into its own hardcoded string literal; fixed a generator hardcoded to a personal laptop path. |
| 3b | Port --gradient-coach-ink, repair coach and primary ink pairings | Shipped | 1 | Zero deviations from plan. Background swap for coach CTAs, ink-class swap for primary outliers — deliberately asymmetric fix shapes. |
| 4 | Teach the tint-ink scanners raw-literal gradient stops | Shipped | 1 | Found the parser-only fix would be vacuous before writing it; widened the scan surface and built a cross-file ink model; proved the guard non-vacuous with a red-then-green control. |
| 5 | The rule, the docs, and close | Shipped | 1 | Wrote the DESIGN.md clause, the honest AA-ledger deferral entry, and closed the milestone — including recovering from a self-inflicted close-gate sequencing deadlock. |
Key Decisions
- Gold's stop B moves on lightness only, matching its sibling gradients — Desi ruled
hsl(43 96% 66%)over OBJ-2868's own filed recommendation (hsl(38 92% 59%), which cleared by only 0.03 and kept an off-hue drift that violated DESIGN.md's existing v4.33 lightness-only rule). The ruled value holds hue and saturation byte-identical to stop A and travels +10 L — the same shape--gradient-primaryand--gradient-accentalready use. - The four-way token drift resolves by deletion, not convergence —
variables.css's shadow declarations were proven dead code in every mode (Tailwind v3 flattens@layer base, sotokens.css's source-order-later declaration always wins) andmarketing_landing/was measured to contain zero references to the token. Converging the dead values instead of deleting them would have left more places for drift to restart. - Coach CTAs get a background swap; primary outliers get an ink-class swap — not the same fix generalized twice. The coach defect was in the gradient itself (a darker
-inkvariant token already existed on the marketing site and had simply never been ported to the app); the primary defect was in the literaltext-whiteclass overriding the correcttext-primary-foregroundthat thirty-four other call sites already used correctly. - The guard's real scope was found before implementation, not discovered by a passing-but-vacuous test suite. A pre-dispatch addendum ran the real scanner against the real token file and found the named parser fix alone changes nothing —
tokens.csswas outside the scan surface, and every declaration in it is a custom property the guard's background-property check rejects. The phase's estimate and scope were both revised from that measurement before any code was written. - The one unrepaired gap (supporter-theme cover/tone pairs) is architectural, not a missed sweep — those gradients are tuned against the default theme's
--primary-foreground; supporter light themes redeclare that ink near-black while leaving the gradients unchanged. Fixing it re-opens v4.33 Phase 3's token architecture, which is a milestone-sized problem of its own. Named, owned, allowlisted, and given an explicit expiry condition (OBJ-2876) rather than silently absorbed or left to look like an oversight.
Requirements Coverage
3 / 3 goal clauses satisfied, all six phases' acceptance criteria satisfied (see .planning/milestones/v4.44-gradient-stop-ink-contract-MILESTONE-AUDIT.md).
| Goal clause | Status |
|---|---|
| Every stop of every gradient token carries the same ink contract as its first stop | ✅ |
| That contract is measured rather than assumed | ✅ |
| A guard fails the build when a new gradient stop is written that nothing has measured | ✅ |
Outcomes
Every gradient token declared in design_system/css/tokens.css — 22 token names, 40 declarations — now has its stops measured against the real ink painted at its real call sites, not against a guessed -foreground sibling. Three real AA-contrast defects are fixed and independently re-verified exact in all 20 theme×mode contexts: the gold achievement gradient (XP chips, capstone banners, the tour outro badge), the coach CTA gradient (four buttons), and four text-white outliers on the primary gradient. A guard now fails the build the moment a new gradient stop is written with no measured ink contract behind it — proven non-vacuous, not merely proven to pass. One honest exception (six supporter-theme pairs, architectural, not a stop-tuning problem) is named, allowlisted, and carries an expiry condition rather than being rounded up as done.
Tech Debt
- (OBJ-2876) Six supporter-theme
--gradient-cover-*/--gradient-tone-*token×stop pairs remain below the 3:1 glyph floor under supporter light themes (worst 1.64:1). Allowlisted in the guard; expires the moment a new--gradient-cover-*/--gradient-tone-*token is added without being scoped first. - (OBJ-2928)
--gradient-herohas two live inked consumers indesign_system/src/pages/BrandOverview.vueoutside Phase 1'sionic_frontend/src-scoped census. Not a Phase 4 defect — the design-system reference app is not the shipped product — but named rather than left implicit. - (OBJ-2930, not v4.44 scope) Crew git commit-author identity drifted across three domains in three consecutive merges this milestone, defeating a GitHub ruleset and forcing
--adminmerge bypasses twice. Filed as a standing crew-infrastructure defect, explicitly excluded from this milestone's close.
Related Artifacts
- Roadmap: v4.44-gradient-stop-ink-contract-ROADMAP.md
- Milestone Audit: v4.44-gradient-stop-ink-contract-MILESTONE-AUDIT.md
- Source issue: OBJ-2868 — "gradient-gold token has no ink contract for stop B (3.99:1, blast radius beyond OBJ-2663)," filed by Desi while fixing OBJ-2663 (Legendary gradient chip ink contrast) and deliberately scoped out as "a real defect in a shared token needing its own phase, not a drive-by fix."
- Coordination anchor: OBJ-2869 (parent milestone issue)
- Follow-ups filed: OBJ-2876 (supporter-theme cover/tone deferral), OBJ-2928 (
--gradient-heroscope gap), OBJ-2930 (crew git-identity instability)
Related Commits
10061ee69— Phase 1: Resolve the effective token, census every gradient stop (PR #2610)ac747f3a0— Phase 2: Repair--gradient-gold, delete the shadow declarations (PR #2630)6c9289cb1— Phase 3b: Port--gradient-coach-ink, repair coach and primary ink pairings (PR #2629)fb44b865d— Phase 3a: Sweep the gold consumers (PR #2633)da8cb6a1e— Phase 4: Teach the tint-ink scanners raw-literal gradient stops (PR #2635)26c2508b2— Phase 5: The rule, the docs, and close (PR #2638)
Last updated: 2026-08-26