Skip to contentWolf-Rayet

Decision records

ADR-0124 The shell paints in roles, and the ramps floor the pairs it uses

Accepted2026-09-22Phase 7

#Context

check-docs.mjs reported 48 of 48 APCA pairs unnameable, each one saying the ramp receipt declares no floor for this ground. That reads as a gap in the ramps. It was not.

The docs shell painted wolf-rayet.css's default surface, whose names are flat: --wr-surface, --wr-text, --wr-text-subtle, --wr-selected, --wr-brand-bold. That sheet answers to data-theme. The semantic tier answers to data-wr-theme. The shell followed neither, so it drew the same #0b0d10 on #ffffff under all four themes, which is a role pair in interior-light by coincidence and in the other three not a pair at all. Sixteen of the 48 failures were that single fact. The shell also kept a hand-written dark pair for --ds-ink and --ds-bar, restated twice, because a fixed colour cannot re-resolve.

Underneath that was a real gap, but a much smaller one. Once the shell painted in roles, four pairs remained that the ramp receipt genuinely declared no floor for:

  • text-accent on surface-page
  • text-accent on accent-subtle
  • text-accent on status-caution-subtle
  • text-primary on surface-inset

The receipt declared four floors in total: text-primary, text-secondary and text-critical on substrate, and text-on-raised on a raised step. Every one of them is text on the page or text on a panel. Nothing floored a link, and nothing floored primary text on an inset panel, although the shell draws both on every page.

#Decision

The shell paints in the semantic roles, and the ramps declare a floor for every pair it paints.

Two halves of one decision, and neither works alone. Roles without floors are unmeasurable; floors without roles have nothing to measure.

--ds-ink becomes text-primary and --ds-bar becomes surface-inset, which retires the two dark blocks that restated them: a role re-resolves per theme and that is the whole reason to take one. Page grounds take surface-page, the greys take surface-inset and surface-hovered, links and the current tab take text-accent, selected states take accent-subtle, and every reading grey takes text-secondary.

--ds-ink survives as a name. ADR-0122 and the shell's own comment say why: a heading on the page should read as the page's rather than as a component's. What changes is that it now holds a role rather than a colour.

The four new floors take the tier the system already declares for that kind of text, per theme, rather than a number chosen here:

  • text-accent is coloured reading text, which is what text-critical is, so it takes that tier: 60, 60, 75, 45 across interior-light, interior-dark, field-day and field-night.
  • text-primary on surface-inset is primary text on a panel, which is what text-on-raised is, so it takes that tier: 90, 90, 90, 55.

All sixteen are met by the ramps as they stand. The generator reports 8 role floor(s), all met, none needed a nudge for every theme, and no token value moves: zero files under packages/tokens/css change, so no fixture and no baseline moves either.

A floor names its ground as a role, not as a ramp step. New floors carry onRole, and the check matches a floor to a measured pair on (role, onRole).

#Consequences

docs:check-site exits 0. APCA reports 36 pairs and no failures, the first time that check has asserted anything: it was reporting 48 of 48 unnameable, which is not a contrast result at all.

The docs site is themed for the first time. Dark resolves through the tier rather than through a second copy of the values, which is also the reason the site can now be swept across all four themes and mean it.

field-day refused one floor and was right to. text-primary on surface-inset at the substrate tier of 100 reads 98.22 even at the theme's darkest bound, short by 1.78, because an inset panel sits off the substrate and costs about 2 Lc. A floor the geometry cannot reach is not a stricter rule, it is an unbuildable one, and the generator's refusal is the mechanism working.

#Rejected

Widen the ramp table so a floor named by ramp and step is unique. Several roles legitimately resolve to one step: interior-dark's neutral/0 is both surface-raised and surface-inset. The ambiguity is in identifying a floor by geometry rather than by the pair it governs, so the fix is onRole.

Lower text-accent to a tier the shell already cleared everywhere. It would have made the check green without asserting anything, which is ADR-0018's mistake with the length ceiling: a number fitted to the output describes it rather than constrains it.

Leave the shell's flat names and widen the check's role table instead. The check would then name pairs that do not re-resolve per theme, so the four-theme sweep would keep measuring interior-light's colours under field-night's name.