ADR-0110 A shadow resolves where it is declared
Amends ADR-0108. Does not supersede it: the decision that each stacking context casts is unchanged, and this is where the cast is declared.
#Context
ADR-0108 emitted two shadow tokens into css/stacking.css, both on :root:
:root {
--wr-stacking-raised-shadow: 0 0.25rem 0.75rem rgb(from var(--wr-scrim) r g b / 0.1);
}The emitter carried a comment saying the colour "resolves in whatever theme the element is rendered in rather than in the theme this file was written for". That is not what CSS does. A var() inside a custom property is substituted when the property is computed, on the element the declaration applies to. The computed value then inherits already substituted. So the scrim was resolved against :root and against nothing else.
--wr-scrim is a theme role and lives on [data-wr-theme="..."]. Whether that selector matches :root is a property of the consuming application, not of the token, and the two consumers differ:
- The docs site puts
data-wr-themeon<html>, so:rootis the themed
element and the shadow resolved. It resolved to the root theme and stayed there: a nested Scope declaring another theme inherited the root theme's scrim. Measured before the fix, a field-night element nested under interior-light carried the interior-light scrim in its shadow.
- The proof app and 268 of the 276 budget fixtures put
data-wr-themeon
<body>, because <body> is the Substrate; none put it on <html>, and the remaining 8 are the themeless Phase 0 set. At :root there, var(--wr-scrim) is undefined, rgb(from var(--wr-scrim) r g b / 0.1) is invalid at computed-value time, and the property computes to the guaranteed-invalid value and inherits empty. Measured before the fix on /incident: four elements carrying data-wr-stacking="raised", all four box-shadow: none.
So elevation was live in the gallery, wrong under a nested theme, and absent from the product and from every scene the budget measures. ADR-0108 reported four modal scenes moving for the shadow. What moved them was the declaration it replaced: modal.css had drawn a hard offset in --wr-modal-mark, a colour that resolves, and that offset was removed in the same change. The cast ADR-0108 decided was never drawn in a measured scene and never priced.
#Decision
- The shadow tokens are emitted on
[data-wr-theme], not on:root. That
puts the declaration on the same element as the scrim it names, so each themed element re-declares the property in its own theme and a nested Scope casts its own theme's colour.
- Borders and
--wr-scrim-alphastay on:root. They are the same number
in every theme, which is ADR-0014's reason for shipping the scrim as a colour and an alpha in the first place. Only the values naming a role move.
- The rule is checked, and checked structurally.
verify-output.jsreads
every emitted stylesheet, brace-matches each :root block, and fails any declaration whose value names a theme role. The check is general: it is about where a role may be resolved, not about these two tokens.
#Consequences
The shadows now render in the fixtures, so the budget prices them for the first time. 35 of the 74 compared scenes moved, between 1.32e-4 and 1.16e-3, in both directions: a near-black cast on a dark ground pulls a Scope toward the substrate and lowers its emission, and the same cast on a lighter ground raises it. Both baselines re-recorded. No scene changed outcome and the two scenes over a theme ceiling are still the two intentional violation fixtures.
Confirmed after the change: on /incident in field-night, the three shell panels cast color(srgb 0.016 0.012 0.011 / 0.1) 0px 4px 12px. region declares a raised context and still casts nothing, which is correct: five components draw a shadow and region takes border-subtle instead. On /components/popover, a nested field-night element now carries the field-night scrim and the root case is unchanged.
#Alternatives
Move the theme to <html> in the proof app. Fixes the app and not the token. It leaves the nested-theme case wrong everywhere, and it asks every future consumer to mount the theme on one specific element or lose elevation silently. The proof app puts the theme on <body> because <body> is its Substrate, which is a reasonable thing for a consumer to do.
Compose the shadow at use, as the scrim background already is. Consistent with the note at the foot of stacking.css. Rejected because a box-shadow is one property: every one of the five consumers would restate the geometry, and the geometry is the part the generator owns.
Emit one shadow token per theme, pre-mixed. Removes the var() entirely. Rejected for ADR-0014's reason: it writes one value per theme for a geometry that does not vary by theme, and the alpha would go with it.