Pin the author cascade layer order ahead of engine support CSS - #1854
Merged
Merged
Conversation
A cascade layer's precedence comes from where its name is first registered, not from where its rules appear. Engine support CSS may join an author layer, and naming that layer registers it. On the frontend the support stylesheet is enqueued after the author CSS, so it joins an already-registered layer and changes nothing. The block editor injects author CSS through `block_editor_settings_all`, after the enqueued editor styles, so the support stylesheet is parsed first and registers the shared layer name ahead of the author's own layers. That inverts the author's order: a Tailwind v4 stylesheet registering `base` before `utilities` ends up with `base` winning, and the author's own preflight reset starts beating the author utilities that centre and pad every section. Sections rendered full-bleed and left-aligned in the editor while staying correct on the frontend. Read the layer order the author stylesheet establishes for itself and state it ahead of every engine stylesheet, so the frontend and the editor resolve the same cascade. Only top-level at-rules register a top-level name, so nested and anonymous layers are skipped, and string literals and comments are not mistaken for layer preludes.
This was referenced Sep 17, 2026
chubes4
added a commit
that referenced
this pull request
Sep 17, 2026
At-rules were removed textually before the rule grammar was read: `@media` blocks applying at the reference viewport were inlined, and `@layer` and `@supports` wrappers were deleted outright. Deleting the `@layer` wrapper discards the precedence the author chose. A declaration in a later layer then loses to an earlier layer whenever the earlier one is more specific, which inverts what the stylesheet says, and unlayered engine CSS becomes indistinguishable from layered author CSS. That is the cascade-escalation axis in #1898: #1879 was an unlayered 0,2,1 rule beating a layered author rule, and #1854 was a layer registered in the wrong order. Neither is representable in a resolver that has thrown the layers away, so the probe scored both as clean. Rules are now collected by walking the stylesheet instead of stripping it, carrying the layer each rule sits in. Layer position comes from AuthorCascadeLayerOrder, the same reader the engine uses when it pins its own layer order, and ranking is left to CssCascade::compareLayers, which already implements unlayered-wins, later-layer-wins, and the reversal under `!important`. Walking rather than stripping is also what makes a `@media` inside a `@layer`, and a `@layer` inside a `@media`, resolve correctly. Nested rule bodies are read for their own declarations only, since `&` resolution is outside the matcher's grammar and attributing a child's declarations to its parent would be worse than leaving it unmatched. All 13 new expectations were confirmed against Chromium's getComputedStyle for the same markup and CSS, including the two the old resolver got backwards: a later layer beating a more specific earlier one, and an unlayered rule beating a layered one regardless of specificity. Probe-only: engine output over the 90-site corpus is byte-identical.
This was referenced Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A cascade layer's precedence comes from where its name is first registered, not from where its rules appear. Engine support CSS is free to join an author layer, but naming a layer registers it.
engine-support-after-authoremits one such rule:On the frontend that stylesheet is enqueued after the author CSS, so it joins an already-registered layer and changes nothing. The block editor injects author CSS through
block_editor_settings_all, which lands after the enqueued editor styles — so the support stylesheet is parsed first and registersutilitiesbefore the author's own layers exist.Measured on an imported Tailwind v4 site, same element in both documents:
Frontend — author stylesheet registers every layer,
utilitieslast:Editor — engine support registers
utilitiesfirst, demoting it belowbase:With
baseoutrankingutilities, the author's own preflight reset (@layer base { *{margin:0;padding:0} }) beats the author utilities that centre and pad every section:.container-sitemargin-left120px0px.container-sitemax-width1200px1200pxpadding112px 00pxEvery section rendered full-bleed and left-aligned in the editor — cards clipped by the canvas edge, page height 3760 vs 5290 — while the frontend stayed correct.
max-widthsurvived only because nothing inbasesets it.Fix
Read the layer order the author stylesheet establishes for itself and state it ahead of every engine stylesheet, so the frontend and the editor resolve the same cascade.
AuthorCascadeLayerOrdercollects top-level layer names in registration order from both the statement (@layer a, b;) and block (@layer a { }) forms.HtmlCompilationprepends the resulting@layer …;toengine-support-before-author, which is already the first stylesheet parsed in both contexts.Only top-level at-rules register a top-level name, so nested (
@layer a { @layer b { } }) and anonymous (@layer { }) layers are skipped, and string literals and comments are not mistaken for layer preludes. Frontend layer order is unchanged — the statement restates the order the author stylesheet already establishes.Stylesheets that use no named layers emit nothing.
Verification
tests/unit/author-cascade-layer-order.php— 16 assertions covering registration order across both at-rule forms, re-entry not reordering, nested/anonymous layers, dotted names, bounding, and string/comment/brace confusion.tests/unit/engine-support-css-asset.php— end-to-end assertion that the before-author asset leads with the author's layer order, emitted once.composer test:canonical,composer test:parity(311 fixtures),composer test:packagingall green.AI assistance disclosure
Investigated and implemented with Claude Sonnet 4.6 via opencode. The model instrumented the imported site with Playwright to compare computed styles and cascade-layer registration order between the frontend and the block editor canvas, isolated the layer inversion to the engine support stylesheet, then wrote the fix and its tests. All output was reviewed before submission.