Skip to content

fix(css): scope baseline CSS to cascade layers so imported sheets win, in publisher and canvas - #426

Draft
fosferon wants to merge 2 commits into
CoreBunch:mainfrom
fosferon:fix/css-cascade-layers
Draft

fix(css): scope baseline CSS to cascade layers so imported sheets win, in publisher and canvas#426
fosferon wants to merge 2 commits into
CoreBunch:mainfrom
fosferon:fix/css-cascade-layers

Conversation

@fosferon

@fosferon fosferon commented Aug 24, 2026

Copy link
Copy Markdown

What

Wraps the CMS baseline CSS in named cascade layers (instatic-reset, instatic-framework) in both places it is assembled — the published bundle and the editor canvas.

Why

Unlayered CSS outranks every cascade layer, regardless of specificity or source order. The baseline was emitted unlayered, so it silently overrode any imported stylesheet that uses @layer — which is every Tailwind v4 build, whose entire output is wrapped in @layer theme/base/components/utilities.

For a site brought in through Site Import, the imported CSS shipped intact but inert. Pages rendered with the CMS's own tokens: --primary bleeding through as unexplained coloured blocks, the font stack falling back to system-ui, surfaces and spacing collapsing — while the author's design sat in the bundle, fully present and simply outranked.

The failure is quiet and easy to misread as a broken import. The diagnostic tell is that element and image counts match the source site almost exactly while every page renders dramatically shorter.

Two code paths, same defect

1. Published bundle (server/publish/siteCssBundle.ts) — reset and framework were emitted unlayered alongside a layered userStyles.

2. Editor canvas (src/admin/pages/site/canvas/canvasClassCss.ts) — subtler. ClassStyleInjector wraps everything the builder returns in @layer user-authored, and UserStylesheetInjector puts imported sheets in that same layer. Rules sitting directly in user-authored outrank rules in its nested sublayers, so the unlayered baseline sat at user-authored while Tailwind landed one level deeper at user-authored.theme — and won everything.

The second one matters independently: after fixing only the publisher, the canvas still previewed CMS defaults, so the published page and the editor disagreed — precisely what the canvas exists to prevent. An imported site appeared as near-blank containers in the editor: uneditable, because you cannot edit what you cannot see.

How

reset.css is the first stylesheet on the page (and the canvas emits its order statement first), so layer order is fixed before either layer is populated. Imported sheets declare their own layers afterwards and therefore win.

Deliberately unchanged in both paths:

  • Author class CSS (style / the canvas's class registry output) stays unlayered, so explicit authoring decisions keep beating both the baseline and imported sheets.
  • userStyles is not re-wrapped. Nesting a kept Tailwind sheet inside our layer would re-invert the precedence this fixes.
  • Editor-chrome CSS stays unlayered in the canvas, so selection outlines and interaction affordances still win — that existing behaviour is untouched.

Impact

  • Existing sites with no imported CSS: no visible change — cascade layers only matter when rules compete.
  • Sites with imported stylesheets: imported CSS now applies as authored. Intended, but it is a rendering change for any site that had silently been showing CMS defaults.
  • Module CSS travels inside the framework bundle and is therefore layered too, so an imported stylesheet can now override module defaults. For a CMS that imports whole sites that ordering seems right — flagging it explicitly as a judgement call worth a maintainer's eye.

Measured

A 29-route Astro + Tailwind v4 site imported into a clean install, comparing rendered height against the source site.

Published pages:

Before After
Mean height fidelity 54.7% 95.6%
Routes within 8% of source 24/29 (14 exact)
Inter styled elements 4 2,310
Brand colour occurrences 0 104

Editor canvas (desktop frame, published page is 6403px):

Before After
Frame height 3429px 6503px
Body font system-ui Inter

Verification

  • bun test — 6667 pass. One pre-existing failure in collabRelayIntegration ("refuses a read-only edit AND resets the viewer"), confirmed failing identically on clean main with these changes stashed.
  • bun run build — clean.
  • bun run lint — clean.
  • Seven new gate tests: four in siteCssBundle.test.ts, three in classStyleInjector.test.ts — layer-order statement position, wrappers present, author CSS staying unlayered, userStyles not re-wrapped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FPTjUK1YJcwK83zYoANRbb

fosferon and others added 2 commits August 23, 2026 11:31
…ts win

Unlayered CSS outranks every cascade layer regardless of specificity or
source order. The publisher emitted `reset` and `framework` unlayered, so
the CMS baseline silently overrode any imported stylesheet that uses
`@layer` — which is every Tailwind v4 build, whose entire output is
wrapped in `@layer theme/base/components/utilities`.

The effect on a site brought in through Site Import: the imported CSS
shipped intact but inert. Pages rendered with the CMS's own tokens —
`--primary` bleeding through as unexplained blocks, the font stack
falling back to system-ui, surfaces and spacing collapsing — while the
author's design was present in the bundle and simply outranked.

Wrap the baseline in `instatic-reset` and `instatic-framework`, with the
layer-order statement emitted at the top of `reset` (the first stylesheet
on the page) so relative order is fixed before either layer is populated.
Imported sheets declare their own layers afterwards and therefore win.

Deliberately unchanged:
- `style` (Styles-panel + imported class rules) stays unlayered, so
  explicit authoring decisions keep beating both baseline and imports.
- `userStyles` is not re-wrapped; nesting a kept Tailwind sheet inside
  our layer would re-invert the precedence this fixes.

Measured on a 29-route Astro/Tailwind v4 site imported into a clean
install, comparing rendered height against the source site: mean fidelity
54.7% -> 95.6%, with 24/29 routes landing within 8% of the original and 14
matching exactly. Inter went from 4 styled elements to 2,310; the brand
colour from 0 occurrences to 104.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPTjUK1YJcwK83zYoANRbb
…e too

Same defect as the published bundle, second code path. ClassStyleInjector
wraps everything `buildCanvasClassCSS` returns in `@layer user-authored`,
and UserStylesheetInjector puts imported stylesheets in that same layer.

Rules sitting DIRECTLY in `user-authored` outrank rules in its nested
sublayers. The canvas baseline was emitted unlayered, so once wrapped it
sat directly in `user-authored` while an imported Tailwind sheet — whose
output is entirely `@layer theme/base/utilities` — ended up one level
deeper as `user-authored.theme` etc. The baseline won everything.

Net effect: the editor canvas previewed CMS defaults even after the
published page was fixed. `:where(body) { font-family: system-ui }` beat
the site's own font, sections collapsed, and an imported site rendered as
near-blank containers — uneditable, because you cannot edit what you
cannot see. The published page and the canvas disagreed, which is exactly
what the canvas exists to prevent.

Name the sublayers so the iframe reproduces the published cascade:
reset -> `instatic-reset`, fonts + framework root -> `instatic-framework`,
order statement first. Author class CSS stays unlayered within
`user-authored`, mirroring the published bundle's unlayered `style`.

Measured on a 29-route Tailwind v4 site imported into a clean install,
desktop canvas frame vs the published page (6403px):
  before  3429px, body font system-ui
  after   6503px, body font Inter

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPTjUK1YJcwK83zYoANRbb
@fosferon fosferon changed the title fix(publisher): scope baseline CSS to cascade layers so imported sheets win fix(css): scope baseline CSS to cascade layers so imported sheets win, in publisher and canvas Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant