The roles design asset specifies inks that FAIL AA — `--el-text-faint` is 2.39:1 and `--el-text-muted` on `--el-surface` is 4.16:1, so the code had to diverge from the mock
Found mid-run by MOTIR-2265, whose axe sweep over the real route is what measured it. Surfaced rather than absorbed (notes.html #27): the fix landed in the COMPONENTS because the card's acceptance criterion demands zero axe violations, but the ASSET still specifies the failing values, so the mock and the code now disagree and nothing says why.
The measurements, taken by AxeBuilder on /settings/project/roles in the acceptance lane
| Element | Mock's token | Resolved | On | Ratio | AA |
|---|---|---|---|---|---|
.lockchip — the Built-in chip, 11px | --el-text-faint | #a4a097 | #f6f5f4 | 2.39:1 | ✗ |
.rsum .rmembers — the member count, 11.5px | --el-text-faint | #a4a097 | #f6f5f4 | 2.39:1 | ✗ |
.rolerow .rdesc — the role's purpose, 12.5px | --el-text-muted | #787671 | #f6f5f4 | 4.16:1 | ✗ |
#f6f5f4 is --el-surface, which roles-permissions.mock.html paints on .rolelist and .permcard. The same --el-text-muted clears AA on --el-card (#ffffff, 4.93:1) — the ink is not the whole story, the SURFACE is. --el-text-faint clears AA on neither, at any size these screens use.
What the code does now, and why it is not a silent divergence
RoleList / RoleDetail / PermissionGroups paint both containers --el-card (which is what the shipped Card primitive the design's own primitive table names actually renders) and move the two chips off --el-text-faint onto --el-text-secondary (#5d5b54, ≈7.4:1). No colour was invented; every value is an existing --el-* token. A comment in RoleList.tsx records the divergence and points here.
The rule that decides it: motir-core/CLAUDE.md — "AA contrast holds" — which the design notes themselves restate ("colored chips put the hue in the tint BACKGROUND with --el-text-strong text… never tint a page-level surface"). Where a mock's token choice and that rule disagree, the rule wins.
Why this is worth a card rather than a comment
The asset is the source of truth for the next person who builds to it — MOTIR-2257's create page is drawn in the same file, with the same .lockchip and .rsum rules, and will reproduce the failure unless the asset moves. And the shipped a11y sweeps run color-contrast ENABLED with zero exclusions (tests/e2e/shell-a11y-detail.spec.ts says so in as many words), so this is not an accepted state anywhere in the product — it is a defect the roles screens happened to be the first to measure.
⚠️ The wider question this raises, and it is the reason the card is not just "edit three CSS lines": is --el-text-faint usable for TEXT at all? On both --el-card (2.80:1) and --el-surface (2.39:1) it fails AA at every size in use. Either it is a decoration-only token and that should be written down where a component author reads it, or it needs a value that clears AA. Settle that, then sweep — do not simply recolour this one asset and leave the next author to rediscover it.
Acceptance criteria
design/projects/roles-permissions.mock.htmland itsdesign-notes.mdsection carry inks that clear AA on the surface they sit on, measured rather than eyeballed, and the notes state the surface each ratio was measured against.- The three-file asset set stays complete — the
.pngexport is re-rendered from the corrected source, not left stale. - A decision is recorded for
--el-text-faint: DECORATION-ONLY (and the token's own comment in the design system says so, where a component author will see it) or re-valued to clear AA. If re-valued, every current consumer is swept and the change is measured on at least two surfaces. - The components' divergence comment is updated to match whatever the asset settles on — if the asset moves to the same tokens the code already uses, the comment becomes a note that they now agree, not a dangling pointer.
- No colour is invented: every value is an existing
--el-*token or a new Tier-3 token mapped to a--color-*.
Context refs
design/projects/roles-permissions.mock.html—.lockchip,.rsum .rmembers,.rolerow .rdesc,.pgroup, and the.rolelist/.permcardbackground: var(--el-surface)that halves the headroom.packages/design-system/theme.css—--el-text-faint→--color-stone,--el-text-muted→--color-muted-foreground,--el-text-secondary→--color-slate.app/(authed)/settings/project/roles/_components/RoleList.tsx— the divergence and its comment.tests/e2e/shell-a11y-detail.spec.ts— the shipped precedent thatcolor-contrastruns enabled with zero exclusions.tests/e2e/acceptance-roles-permissions.spec.ts— the sweep that measured this.