Summary
On published pages, Instatic's own reset-<hash>.css and framework-<hash>.css are emitted outside any cascade layer, while a user stylesheet that uses @layer keeps its layers (correctly preserved verbatim by "Keep as stylesheet" import mode, or by authoring in the code editor). Per CSS Cascade 5, unlayered author styles take precedence over all layered author styles for normal declarations — before specificity is considered. The result: the runtime reset silently overrides the entire user design system on every conflicting property.
This inverts the documented cascade contract in docs/features/publisher.md:
class registry, which wins over framework, which wins over reset
For layered user CSS, reset wins over everything instead.
Reproduction
- Import a static site whose stylesheets are organized with
@layer (e.g. @layer reset, base, layout, chrome, components; @layer base { body { font-family: "Some Font", sans-serif } } @layer layout { .lede { margin-block-start: 3rem } }), choosing Keep as stylesheet in the Review step (correct choice, since @layer can't be converted — as documented in docs/features/site-import.md).
- Publish.
- Inspect any published page.
Expected: the kept stylesheet's rules style the page (they load last, and the docs say user styles win over framework/reset).
Actual: every property that the runtime reset also declares is taken from the reset, regardless of the user rules' specificity:
:where(*) { margin: 0; padding: 0 } zeroes all layered margins/paddings → vertical rhythm collapses. Note the :where() wrapper's zero specificity does not help here — layer ordering is resolved before specificity, and unlayered wins.
:where(body) { font-family: system-ui, ... } beats a layered body { font-family: ... } → whole site falls back to system fonts.
:where(button) { background: none; border: 0 } beats layered component styling → controls render unstyled.
Only rules the reset doesn't conflict with (backgrounds, colors, WebGL canvas positioning) survive, so the page looks almost right — which makes the failure easy to miss in QA. Observed on a source build at 2af2ecd1 (post-0.0.12).
Root cause
CSS Cascade 5 origin/layer sorting: within the author origin, normal declarations in the implicit outer (unlayered) layer beat all named-layer declarations. The published <head> loads reset → framework → userStyles; the first two are unlayered, so any @layer-scoped user CSS — no matter how specific or how late-loaded — loses every direct property conflict.
Suggested fix
Wrap the runtime bundles in named layers declared before any user CSS, e.g. emit at the very top of the style head:
@layer instatic.reset, instatic.framework;
and emit PUBLISHER_RESET_CSS inside @layer instatic.reset { ... }, framework CSS inside @layer instatic.framework { ... } (also the module/class CSS if it should stay below user styles). Then:
- unlayered user CSS still wins (unchanged behavior for the common case);
@layer-scoped user CSS now also wins, because user layers are declared later than the instatic.* layers — restoring the documented "user beats framework beats reset" ordering for both authoring styles.
The editor canvas's user-stylesheet pipeline would need the same treatment so preview and publish agree.
Workaround (for anyone hitting this today)
Flatten @layer out of stylesheets before import: emit the rules unlayered, ordered by the declared layer precedence (so equal-specificity conflicts still resolve like the layer order), then import. Downgrades the layer-order-beats-specificity semantics to specificity+source-order, but in practice restores the design faithfully — verified with computed-style probes on a real site.
Related docs
docs/features/site-import.md — documents @layer as unconvertible and "Keep as stylesheet" as the escape hatch; the kept file then hits this at publish time.
docs/features/publisher.md — the cascade table this behavior contradicts.
Summary
On published pages, Instatic's own
reset-<hash>.cssandframework-<hash>.cssare emitted outside any cascade layer, while a user stylesheet that uses@layerkeeps its layers (correctly preserved verbatim by "Keep as stylesheet" import mode, or by authoring in the code editor). Per CSS Cascade 5, unlayered author styles take precedence over all layered author styles for normal declarations — before specificity is considered. The result: the runtime reset silently overrides the entire user design system on every conflicting property.This inverts the documented cascade contract in
docs/features/publisher.md:For layered user CSS, reset wins over everything instead.
Reproduction
@layer(e.g.@layer reset, base, layout, chrome, components; @layer base { body { font-family: "Some Font", sans-serif } } @layer layout { .lede { margin-block-start: 3rem } }), choosing Keep as stylesheet in the Review step (correct choice, since@layercan't be converted — as documented indocs/features/site-import.md).Expected: the kept stylesheet's rules style the page (they load last, and the docs say user styles win over framework/reset).
Actual: every property that the runtime reset also declares is taken from the reset, regardless of the user rules' specificity:
:where(*) { margin: 0; padding: 0 }zeroes all layered margins/paddings → vertical rhythm collapses. Note the:where()wrapper's zero specificity does not help here — layer ordering is resolved before specificity, and unlayered wins.:where(body) { font-family: system-ui, ... }beats a layeredbody { font-family: ... }→ whole site falls back to system fonts.:where(button) { background: none; border: 0 }beats layered component styling → controls render unstyled.Only rules the reset doesn't conflict with (backgrounds, colors, WebGL canvas positioning) survive, so the page looks almost right — which makes the failure easy to miss in QA. Observed on a source build at
2af2ecd1(post-0.0.12).Root cause
CSS Cascade 5 origin/layer sorting: within the author origin, normal declarations in the implicit outer (unlayered) layer beat all named-layer declarations. The published
<head>loadsreset→framework→userStyles; the first two are unlayered, so any@layer-scoped user CSS — no matter how specific or how late-loaded — loses every direct property conflict.Suggested fix
Wrap the runtime bundles in named layers declared before any user CSS, e.g. emit at the very top of the style head:
and emit
PUBLISHER_RESET_CSSinside@layer instatic.reset { ... }, framework CSS inside@layer instatic.framework { ... }(also the module/class CSS if it should stay below user styles). Then:@layer-scoped user CSS now also wins, because user layers are declared later than theinstatic.*layers — restoring the documented "user beats framework beats reset" ordering for both authoring styles.The editor canvas's user-stylesheet pipeline would need the same treatment so preview and publish agree.
Workaround (for anyone hitting this today)
Flatten
@layerout of stylesheets before import: emit the rules unlayered, ordered by the declared layer precedence (so equal-specificity conflicts still resolve like the layer order), then import. Downgrades the layer-order-beats-specificity semantics to specificity+source-order, but in practice restores the design faithfully — verified with computed-style probes on a real site.Related docs
docs/features/site-import.md— documents@layeras unconvertible and "Keep as stylesheet" as the escape hatch; the kept file then hits this at publish time.docs/features/publisher.md— the cascade table this behavior contradicts.