Scoped design previews are not ISOLATED — 69 `[data-style='X'] <descendant>` material rules cross the nesting boundary, so 10 of 11 style previews wear the ancestor's style
Carved out of MOTIR-3933 by the re-plan its own run submitted. That card frames the style half as a base-entry token leak with a one-block remedy; measured, it is an isolation problem across 69 rules and it needs a mechanism DECIDED before any of them is touched.
The measurement
packages/design-system/theme.css carries 69 descendant rules of the form [data-style='X'] [data-surface='…'] (and [data-style='X'] body, [data-style='X'] .border, …). A descendant combinator does not stop at a nested data-style, so every one of them matches inside a scoped StyleVignette.
Rendered in headless chromium — eleven <div data-style="ID"><div data-surface="card"> tiles under one <html data-style="ACTIVE">, each compared against what that same style renders when it is the ACTIVE one:
<html> data-style | tiles wearing their OWN material |
|---|---|
warm-editorial (the base — no block, so nothing to leak) | 11 / 11 |
aurora | 3 / 11 |
glassmorphism | 1 / 11 |
Under glassmorphism, a tile labelled Neo-Brutalism computes backdrop-filter: blur(18px) saturate(1.8) and a translucent fill. The tokens are right and the rendering is the ancestor's.
This is why a base token block is the wrong remedy. It would make MOTIR-3933's criterion 3 pass — the resolved --radius-card would be correct — on a tile that still paints as glass. The assertion would be green over a wrong page, which is the failure mode the whole card exists to close.
The decision to make
StyleVignette's own header already names it, as an unowned follow-up: "Full nested-base isolation (a reset block or an iframe) is a noted follow-up if a future consumer needs it." That future consumer arrived (motir.co/design, and the onboarding Style gallery has the same shape). The candidates, to be weighed against the 69 rules rather than in the abstract:
- Scope every material rule so it cannot cross a boundary — e.g.
[data-style='X'] :not([data-style]) [data-surface='…'], or a:where()-based barrier. Cheap per rule, 69 of them, and it changes the meaning of every existing selector. - Render each scoped preview in an isolated tree — an iframe or a shadow root carrying its own copy of the token layer. One change, no selector churn, but it costs a document per tile and changes how the component is consumed.
- Emit the material rules from a scope-anchored selector rather than a descendant one, so scoping is structural instead of a per-rule opt-out.
Acceptance criteria
- An ADR under
motir-core/docs/decisions/naming the chosen mechanism, the two it rejected, and the measured table above as the evidence. It states explicitly what a nested preview is and is not isolated from, so the next reader does not re-derive it. - The ADR is checked against BOTH known consumers — the onboarding Style gallery and
TokensSpecimen's galleries — and says what each has to change. - The build cards are AUTHORED in this pass, not deferred to prose: one per unit of work the chosen mechanism implies, each sized, each
blocked_bythis card. A sentence saying "the implementation is a later card" with no key is what this criterion exists to forbid. - No behaviour change ships from this card. It decides and it plans; the selectors are touched by the cards it authors.
Boundary
Does not touch the PALETTE or TYPE axes — those are MOTIR-3933, which is bounded, measured, and independently shippable. Does not touch motir-marketing. Does not publish the package.