The compiled CSS#

The compiler turns tokens and recipes into plain CSS with no runtime: four cascade layers, deliberately low specificity, per-theme diff blocks, and axis rules bounded by @scope. Knowing the shape explains why app CSS always wins, why nested instances of one component do not leak into each other, and why the order of the output is a correctness concern rather than taste.

Four layers, one statement#

CSS
@layer zero.fallback, zero.tokens, zero.recipes, zero.structure;
LayerContents
zero.fallback@sigx/zero/css only: design-system-neutral structural token defaults (radius, size and text ramps, durations, --print-ink, …) so an unstyled page is sane.
zero.tokensThe compiled design-system tokens: :where(:root) defaults, the prefers-color-scheme: dark block, one block per theme, the reduced-motion block.
zero.recipesAll compiled recipe CSS, plus base.css's few structural necessities (the <details> marker removal, cursor: not-allowed).
zero.structureOne rule: [data-scope][data-part][hidden]:not([hidden="until-found" i]) { display: none }.

The statement is declared in @sigx/zero/css and emitted atop every compiled tokens.css, because the first mention of a layer fixes its position: a design-system stylesheet parsed before base.css would otherwise create zero.tokens first and leave the fallbacks above it. Restating the order is idempotent; relying on load order is not.

zero.structure exists because [hidden] otherwise relies on the UA sheet — the weakest declaration in the document — and any unconditional display in a recipe defeats it. A later layer rather than higher specificity (compound selectors can reach (0,8,0)), and never !important, which would also outrank the consumer's unlayered app CSS. The consumer always wins.

Specificity is designed#

Root token defaults are emitted as :where(:root) — specificity (0,0,0) — so any [data-theme="x"] block at (0,1,0) beats them regardless of source order, a nested data-theme re-themes its subtree by inheritance, and unlayered app CSS beats everything.

Theme blocks are diff-only — only what diverges from :root — with two deliberate exceptions restated per theme: scheme-divergent non-colour values (or a data-theme="light" island under a system-dark root would inherit the dark value), and tokens whose values reference colour propertiesvar() in a custom property substitutes where declared, not where used, so a --shadow-md: … var(--color-primary) declared only at :root would capture :root's primary forever.

@property registrations are per design system, emitted above the layers in each compiled tokens.css: every declared colour role (typed <color>, so theme switches animate) plus declared customs carrying a syntax. -soft is unregistered because its value can be color-mix(), invalid as an initial-value.

Axis rules and the @scope donut#

Axis rules are anchored on the carrier partroot, else the first declared part; the trigger for the four rootless scopes. For the carrier itself the rule is flat:

CSS
[data-scope="button"][data-part="root"][data-variant="outline"] { … }

For any other part the attribute is on an ancestor, and a bare descendant selector is unbounded — card-in-card would let the outer instance's axis rules reach the inner one, with source order rather than proximity deciding. The compiler therefore emits a donut scope:

CSS
@scope ([data-scope="tabs"][data-part="root"][data-size="xs"]) to ([data-scope="tabs"][data-part="root"]) {
    [data-scope="tabs"][data-part="tab"] { font-size: var(--text-xs); padding: 0.25rem 0.5rem; }
}

The lower bound is any nested same-scope carrier — its subtree leaves the scope. Two CSS facts make this correct: scoping proximity outranks source order, so each part resolves to its nearest carrier; and an unscoped rule counts as infinitely distant, so the axis refinement still beats the flat base rules.

Defaults are mirrored onto absence. A single-axis rule whose value is the recipe's default is emitted twice: [attr="v"] and :not([attr]). Compound variants take the same treatment as a cross product — without it, a compound naming a defaulted axis would match nothing, since the attribute is simply absent. Compound rules are emitted separately rather than comma-joined, because part-style emission appends pseudo-element suffixes and & substitutions that would bind only to the last selector of a list.

Emission order#

At-rules add no specificity, so conditional buckets are emitted in a fixed tier order — raw conditions, preference queries, breakpoints, reduced-motion, @starting-style — with reduced-motion late so an accessibility override is never overwritten by a wider viewport, and @starting-style after the open-state rules it interpolates from. print sorts with the preference queries: after the flat rules it refines, before any breakpoint. The same prelude reached at two different tiers (a raw @media (min-width: 640px) beside a declared sm) is a hard error, because its position would otherwise depend on visit order.

Everything lands inside @layer zero.recipes; @keyframes are emitted outside the layer.

Reduced motion#

Every declared duration key collapses to 0.01ms under prefers-reduced-motion: reduce — not 0ms, because a zero duration suppresses the transitionend / animationend events presence and exit coordination wait on. The block's selector is :root, [data-theme] at (0,1,0), emitted last inside the layer, so it ties-and-wins against every theme block. @sigx/zero/css carries the same block for the recommended durations only; a design system's own keys are neutralised by its compiled tokens.css, the only place that knows their names.

Interpolation guards#

Every point where an authored string is spliced into emitted CSS is validated, and the policy is a hard error rather than escaping: axis names and values (a value carrying " would close the attribute selector early and emit a second, unrelated selector); property names and a break-out check on declaration values (x;} [data-scope]{color would restyle every scoped element); pseudo-element projections; @keyframes names (a keyframes named none would capture animation: none); theme names into [data-theme="…"]; token keys; and fragment package specifiers (selector injection and path traversal both). A recipe that needs content: '";"' is asked to spell it differently, because an escape hatch here is an injection surface.