v4.33 — Primary Ink Adaptive Swap
Every
--primary-family fill in both frontends now carries adaptive ink (text-primary-foreground) instead of hardcoded white — the last piece of a contrast defect the token layer alone couldn't close, since a consumer can always opt out of the token.
Summary
v4.29 and v4.31 had already brought every --primary/--primary-foreground pair and the --gradient-primary family up to WCAG AA at the token layer. This milestone closes the gap those two left standing: a site can pair a --primary-family fill with a hardcoded text-white instead of the adaptive token, and nothing at the token layer catches that — the token-level guarantee only protects a consumer that actually uses it. text-white on bg-primary measured 2.46:1 in dark mode (AA needs 4.5); the adaptive text-primary-foreground on the identical fill measured 7.69:1. The defect survived three prior remediation milestones because light mode's --primary-foreground is literally white — text-white and text-primary-foreground render the same pixel in light mode, so the wrong code looked correct there and only failed once dark mode flipped the token to near-black.
Orion's kickoff census superseded the source issue's (OBJ-2266) filed list of 11 sites, finding 26 across both frontends — not admin-only, and the admin list itself was incomplete, exactly as the source issue's own closing warning predicted. Each phase then found its own scan technique undercounted by exactly the shapes it couldn't represent: a same-line grep missed fill and ink living on different elements (Phase 2, +7 sites), and even an ancestor-propagating AST guard missed a fill supplied by a non-literal :class expression — a computed property or function call (Phase 3, the design-question count corrected from 2 to 8). The pattern repeated at each layer a stronger tool arrived, and the ROADMAP records the progression rather than treating any one correction as a one-off.
Phase 3's most consequential finding broke the milestone's own founding assumption for its final 8 sites. Measuring all 12 category-cover and team-tone-chip gradients against every candidate ink found that no single ink cleared AA 4.5:1 on 9 of the 12 — those gradients were built at the utility layer from fill tokens that had never declared an ink and structurally could not carry one at any fixed value. The "design tokens are never modified" framing this milestone started with survived in letter (every pre-existing token stayed byte-identical) and was superseded in spirit: 12 new mode-matched --gradient-cover-*/--gradient-tone-* token pairs were added, because a pure consumer-layer swap was the wrong answer for two-thirds of these gradients. All 24 new measurements clear 4.5; the tightest is 4.56.
Goal
Every
--primary-family fill in both frontends carries adaptive ink (text-primary-foreground), so the token-level contrast guarantees v4.29 and v4.31 already enforce actually reach the rendered surface — and a source-scan guard makes the hardcoded pairing unable to come back silently.—
.planning/milestones/v4.33-primary-ink-adaptive-swap-ROADMAP.md
Scope — What Shipped
- 41 sites swapped from hardcoded
text-whiteto adaptivetext-primary-foregroundacrossadmin_dashboardandionic_frontend: 12 flatbg-primarysites in admin (Phase 1), 21 sites in ionic spanning flat fills, gradient fills, and 2 conditional:classbindings where both branches needed independent verification (Phase 2), and 8 category-cover/tone-chip design-ruling sites plus a Phase-2 half-fix onEnneagramQuestionCard.vue(Phase 3). - Two source-scan guards, replacing zero prior coverage for this defect class.
admin_dashboard/tests/unit/primaryInkGuard.spec.ts— a same-attribute regex, sufficient for admin's tree.ionic_frontend/tests/unit/primaryInkGuard.spec.ts— avue/compiler-sfcAST walk with ancestor-fill propagation, built after demonstrating a regex port would miss 10 of 21 real sites (parent-fill/child-ink pairs and:classbindings a same-attribute match can't see). Both allowlists end the milestone holding only permanent structural exclusions — 2 in ionic, 0 in admin. - 12 new CSS token pairs (
--gradient-cover-*× 7,--gradient-tone-*× 5,design_system/css/tokens.css) for the category-cover and team-tone-chip gradients that could not carry any single fixed ink at AA — stops moved on lightness only, hue and saturation byte-identical to the old utility combos, so wayfinding between covers/tones is unaffected. No second ink token was introduced; all 12 pair withtext-primary-foreground. - Two escaped defects found by the new guards, not by either census, closed in the same milestone:
EnneagramQuestionCard.vue's choice-text body copy, half-fixed by this milestone's own Phase 2 (root fill swapped, body copy left ontext-white); and a guard ancestor-fill-propagation gap that false-positived on an opaque-button-shielded icon, fixed and distinguished from a genuinely unfixable z-order-scrim case (CommunityPreviewModal.vue), which is documented as a permanent non-fire rather than "fixed" into a legibility regression. - Rewritten documentation:
docs/development/design-tokens.md's new "The primary-ink rule" section states the rule, the light-mode no-op that let it survive undetected, both guards' blind spots, and the shared-ink ruling for all 12 category-cover/tone-chip gradients — extending the existing v4.29/v4.31 contract sections rather than opening a competing one.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | admin_dashboard ink swap + guard | Shipped | 2 | Swapped all 12 filed sites; a full-tree scan confirmed the census was complete for admin. Shipped the first source-scan guard. Roy's round-1 review caught a hover-only shape (ErrorBoundary.vue:55) the guard's matcher missed. |
| 2 | ionic_frontend mechanical ink swap | Shipped | 3 | Re-censused at dispatch (14 → 21 sites) after a proximity scan found parent-fill/child-ink pairs a same-line grep couldn't see. Shipped an AST-based guard after proving Phase 1's regex would miss 10 of 21 sites, and back-ported it to admin. The new guard immediately found 4 sites no census had. |
| 3 | shared-ink design call, residual swap & docs | Shipped | 5 | Re-censused the design question (2 → 8 sites) after finding both prior censuses were blind to non-literal :class expressions. Desi's measurement disproved the milestone's own premise for 9 of 12 gradients, producing 12 new token pairs instead of a swap. Fixed the EnneagramQuestionCard.vue half-fix and the guard's ancestor-shielding false-positive class. Documented the full rule. |
Key Decisions
- A shared-ink question got measured, not assumed. Rather than defaulting to
text-primary-foregroundfor the 12 category-cover/tone-chip gradients because it worked everywhere else, Desi measured all 12 against every candidate ink and found 9 of 12 could not clear AA at any single fixed value — producing a 12-new-token-pair ruling instead of a wrong swap that would have looked complete. - No second ink token, even though one was considered.
--gradient-foregroundwas evaluated and rejected for the 12 new gradient pairs — a byte-identical sibling ink class would hand a future author two correct-looking answers to the same question, the exact drift this milestone exists to close. All 12 pairs use the existingtext-primary-foreground. - A guard's false positive is not the same defect as a false negative, and neither gets "fixed" by force.
CommunityPreviewModal.vue's two flagged sites sit under an opaque button and a full-bleed scrim — swapping either to near-black ink would have created a real legibility regression. Both are documented permanent non-fires in the guard's allowlist, not silently swapped or removed. - Each phase's guard was built to the shapes actually present, not ported wholesale. Phase 1's same-attribute regex was sufficient for admin (zero parent/child or dynamic-fill sites there); porting it verbatim to ionic would have missed 10 of 21 sites, so Phase 2 built an AST tree-walk instead and back-ported the stronger version to admin.
- No new contrast math anywhere. The adaptive token inherits v4.29's and v4.31's existing token-layer AA guarantees transitively — this milestone is a consumer-layer source-scan problem, not a contrast-measurement problem, except for the 12 gradient pairs that genuinely needed new measurement because no existing token covered them.
Requirements Coverage
9 / 9 requirements satisfied — see the Milestone Audit for the full table.
| Category | Count | Status |
|---|---|---|
| INK | 3 | All satisfied |
| GUARD | 2 | All satisfied |
| SPEC | 1 | All satisfied |
| EVID | 2 | All satisfied |
| DOCS | 1 | All satisfied |
Outcomes
Every --primary-family fill across both frontends — flat bg-primary, bg-gradient-primary, and the 12 category-cover/tone-chip gradients — now pairs its ink with a token that inherits the existing AA 4.5:1 token-layer guarantee. Users get legible ink on every one of these surfaces in both light and dark mode instead of a subset reading near-invisible in dark mode. A future author adding a --primary-family fill anywhere in either app now has a documented rule to follow and two guards that fail loudly — not silently — if the hardcoded pairing returns.
Tech Debt
- (Phase 3, residual risk, not independently verified live) Family B (the 6 team tone-chip sites) was never rendered in a live app during this milestone — the preview smoke account has no team. Coverage rests on Storybook, unit tests, and Desi's rendered mockups; worth a real device pass when a team-bearing account exists.
- (Docs debt, non-blocking, flagged by the milestone audit)
CHANGELOG.md's[Unreleased]v4.33 entry headline reads "33 sites total" but its own itemized breakdown sums to 41, matching this milestone's real total. The itemized content is correct; only the summary headline is stale arithmetic. - (Pre-existing, unaffected by this milestone) OBJ-2257 —
--gradient-accent's Midnight dark-mode gap, carved out of v4.31's scope.
Related Artifacts
- Roadmap: v4.33-primary-ink-adaptive-swap-ROADMAP.md
- Milestone Audit: v4.33-primary-ink-adaptive-swap-MILESTONE-AUDIT.md
- Design tokens contract: docs/development/design-tokens.md § The primary-ink rule
- Triggering issue: OBJ-2266 (deferred out of v4.31 Phase 2 by Codi with Roy's review endorsement, parked until v4.31 reached
master) - Merge PR: #2213
- Follow-ups: OBJ-2257 (
--gradient-accentMidnight gap, pre-existing), OBJ-2311 (escaped-defect adjudication, bundled into Phase 3)
Related Commits
785661f44— Phase 1: admin_dashboard ink swap + guard (PR #2189)c1d962606— Phase 2: ionic_frontend mechanical ink swap (PR #2193)6d929e6e9— Phase 3: shared-ink design call, residual swap & docs (PR #2209)32ec51e4e— Promote v4.33 integration branch tomaster(PR #2213)
Last updated: 2026-08-08 · Version: v4.4.5