#18145 batches 1–4 have landed (#18167 tab, #18168 grid, #18170 button, #18172 portal) and its guard is live (#18196). node buildScripts/util/check-theme-value-files.mjs at dev 274b63beac reports 5 files pending, 2 exempt, and all five pending files are Global.scss:
blocks
file
10
resources/scss/theme-neo-light/Global.scss
10
resources/scss/theme-neo-dark/Global.scss
1
resources/scss/theme-light/Global.scss
1
resources/scss/theme-dark/Global.scss
1
resources/scss/theme-cyberpunk/Global.scss
#18145 called this batch out in advance: "largest and least mechanical; likely wants its own ticket once the others land." They have landed. This is that ticket, and the measurement below is why it is not a fifth mechanical batch.
The Problem
The neo blocks are structure that never belonged in a theme file, and the diff proves it.theme-neo-light/Global.scss:14-79 against theme-neo-dark/Global.scss:14-80 is 61 of 66 lines byte-identical. Every h1/h2/h3/i, em declaration already reads var(--core-*) / var(--sem-*) — no literal, no theme decision. Only five declarations diverge:
block
neo-light
neo-dark
p color
var(--sem-color-fg-neutral-contrast)
var(--white-off)
code:not(.hljs) background-color
#E6EDF7
var(--sem-color-surface-primary-background)
h1..h6 code:not(.hljs) color
(absent)
var(--white-off)
mark background-color
var(--green-50)
var(--green-900)
mark color
black
var(--white)
So the batch needs 5 new tokens, not the ~25 the block count suggests.
The classic block is already the answer, written as a selector.theme-light, theme-dark and theme-cyberpunk each carry one identical block:
p {
color : inherit;
font-family: inherit;
font-size : inherit;
font-weight: inherit;
line-height: inherit;
}
Zero values. It is a UA-default reset stating "this family declines to style paragraphs" — the exact statement the move needs those themes to make as tokens.
The Architectural Reality
These are element selectors, so the move drops weight further than any prior batch did.:root .neo-theme-neo-light h1 is (0,2,1); a structural rule in resources/scss/src/Global.scss is bare h1, (0,0,1). Batch 1 shipped a specificity regression precisely because a declaration diff cannot see a cascade change, so this batch was swept for declarations sitting between the two weights on an overlapping property.
One named repaint.resources/scss/src/apps/realworld2/MainContainer.scss:28
is (0,1,1). Today it loses to the theme's (0,2,1) heading block on all four properties. At bare h1 it wins, and realworld2 headings fall back from the design tokens to inherit / 500 / 1.1.
One latent tie the batch found rather than caused.resources/scss/src/apps/portal/home/parts/AiToolchain.scss:56,80,87 — .portal-home-parts-aitoolchain .agent-os-faq h2|h3|p is (0,2,1), an exact tie with :root .neo-theme-neo-* h2|h3|p, overlapping on color / font-size / line-height. Theme sheets load lazily per class, so which rule wins the portal FAQ's typography is source-order-dependent today. That is the non-determinism #18145 exists to remove.
Swept and clear (no overlapping property, so weight is irrelevant): .neo-markdown-component p (margin only) and .neo-app-content-sections-container.neo-container h3 (margins only).
⛔ CORRECTION, 2026-09-08 — this section originally cleared code, .hljs too, and that clearance was FALSE. It read: "code, .hljs in resources/scss/src/util/HighlightJs.scss:7 at (0,0,1), which stays outranked by a moved code:not(.hljs) at (0,1,1)." The specificity arithmetic is correct and the conclusion drawn from it is backwards. Being outranked was treated as being irrelevant. It is the opposite: the structural rule wins, and in a family that declines the token it wins in order to resolve revert — which rolls back the entire author origin, discarding HighlightJs's declaration rather than deferring to it. A winning declaration that resolves to revert is more destructive than one that supplies a value.
Measured after it shipped: classic inline code read monospace where dev reads Menlo, monospace. Falsifier by @neo-gpt-emmy on PR #18454; reproduced in this repository's own component fixture before repair. The fixture could not have caught it — it measured a UA baseline and never composed the author sheet it was competing with, so a correct UA-default control certified a narrower contract than it appeared to.
Repair, first attempt — superseded, recorded because it was wrong in an instructive way.font-family left the structural code rule entirely and moved to util/HighlightJs.scss, which split code, .hljs into code {font-family: var(--global-code-font-family, Menlo, monospace)} with .hljs keeping the literal. That fixed composed-classic and broke uncomposed-neo: it made a lazy-loading sheet the sole owner of an always-needed value, so both neo themes lost their mono token wherever HighlightJs had not loaded. Caught by @neo-gpt-emmy in R2.
Final shape — two readers, one token, positioned by weight.util/HighlightJs.scss keeps the above at (0,0,1), and resources/scss/src/Global.scssalso reads the token at zero specificity, :where(code:not(.hljs)) {font-family: var(--global-code-font-family, revert)}. Because :where() zeroes the selector, HighlightJs outranks Global whenever it is present, so the two never compete: Global speaks only in the uncomposed case. .hljs (0,1,0) still outranks code (0,0,1), so highlighted code is inert throughout, and Menlo, monospace never leaves HighlightJs. Verified across all 5 themes × {with, without HighlightJs}, matching dev in every cell.
The constraint collision that makes this its own ticket
Three rules already on the books meet here:
#18145's rule — the selectors move to resources/scss/src/, and every token they introduce is declared in all themes.
#18145's Out of Scope — "Components with no classic-theme styling at all … a real gap, a different one." Classic themes must not acquire heading/code/mark typography here.
#18141's law — a token some themes omit does not reset at a nested theme boundary; it inherits from the nearest declaring ancestor.
Satisfying all three means the classic families must state the neutral value rather than omit it — which is what their p { …: inherit } block already does, one property set at a time. The batch generalizes that block into tokens instead of deleting it.
Unmeasured, and the implementer must measure it before relying on it: a moved declaration reading var(--core-fontsize-h1) in a family that never declares it is invalid-at-computed-value-time, and IACVT resolves an inherited property to inherit — which would make the move self-neutralizing for classic themes at the document root. Per rule 3 that cannot hold for a classic theme nested inside a neo scope, where the ancestor's token is visible. The discriminating witness is a nested arm (classic scope inside a neo scope) asserting the outer scope resolves a real value before asserting the inner one inherits nothing — the #18141 witness shape, which fails vacuously without the outer assertion.
The Fix
Move the ten neo blocks into resources/scss/src/Global.scss, each declaration reading a token with a revert fallback.
Introduce the --global-* tokens and declare them in all five themes.
Declare, in the three classic themes, every token the moved rules read — as initial, which is how a family states its decline — then delete their p { …: inherit } block, since it has become those declarations.
Shrink buildScripts/util/check-theme-value-files-baseline.json by the five entries in the same commit (the guard fails a baselined file whose count drops).
⚠️ CORRECTION — the token count was 5 and is 14. Both revisions above are measured, and the difference is not scope creep:
5 was right about the neo-theme divergences and wrong about the mechanism. It assumed the already-tokenised declarations could move verbatim. They cannot: in a classic scope nested inside a neo one the --core-* tokens arrive by inheritance, and a var() fallback fires on absence, never on inheritance — so a classic family must be able to un-declare every property, not only the divergent ones.
+8 for declarations that were literals (letter-spacing, the code border/radius/size/padding, the heading-code margin/padding, mark padding). Without a token these reach the classic families with no way to decline.
+1 for --global-code-font-family, from the HighlightJs correction above.
--core-fontfamily-mono is correspondingly no longer read by the structure layer, so its classic un-declaration is retired rather than left as dead ballast.
Sequencing
This batch and #18107 must not run concurrently.#18107 changes the weight a theme sheet declares at (:where(), zero specificity); this changes where the selector lives. Both move the same rules across the same cascade thresholds, and Vega already flagged the attribution cost on D#18182 OQ5: with both in flight, a paint regression cannot be attributed to either. Whichever lands first, the other re-measures. The realworld2 repaint above is weight-drop-shaped, not move-shaped — #18107's :where() sweep hits the identical victim if it reaches Global.scss, so that measurement belongs to both tickets.
Acceptance Criteria
AC-1 — the five Global.scss files contain variable declarations only; the moved selectors live in resources/scss/src/Global.scss.
AC-2 — every token the moved rules read is declared in all five themes, so no nested theme boundary inherits a value it should reset.
AC-3 — a nested-scope witness (classic theme inside a neo scope) asserts the outer scope resolves a real value before asserting the inner one inherits nothing, so it cannot pass vacuously.
AC-4 — resolved as "no repaint occurs", and the premise above is falsified.apps/realworld2/neo-config.json declares themes: ["neo-theme-dark", "neo-theme-light"] — both classic, and classic themes have never carried heading blocks. So no theme heading rule has ever matched realworld2: .rw2-main-container h1 (0,1,1) already won and still outranks the structural (0,0,1), and every token it would read is declared initial by its own themes, reverting to the UA exactly as before. The predicted repaint required a neo theme the app does not load. (Separately observed, not a claim about this batch: realworld2 does not currently boot — Base.on is not a function — on a server where the portal mounts fine on the same branch.)
AC-5 — the AiToolchain FAQ tie is dispositioned: named as pre-existing and left, or resolved, with which rule wins after the batch stated either way.
AC-6 — emitted CSS diffed per theme after node ./buildScripts/build/themes.mjs -f -n -e esm -t all, verified by output rather than exit code (build-themes prompts and exits 0 non-TTY, leaving dist/ stale).
AC-7 — check-theme-value-files.mjs exits 0 with the five entries removed from the baseline.
Contract Ledger Matrix — the consumed token surface
Added at @neo-gpt-emmy's RA-2: this batch introduces a token vocabulary that themes are now required to satisfy, and neither this ticket nor #18145 recorded it. Source of Authority for every row is resources/scss/src/Global.scss (the structure layer that reads them), except the last, which is util/HighlightJs.scss. Every row is verified against the built CSS rather than the source.
The contract in one line: a token's fallback must be the value the next author rule would supply from that rule's own position — so the correct fallback is a property of the declaration's weight, not of the token.
--global-code-font-family is the only row with a competing author sheet, and it is the row that shows why: the same token needs different fallbacks in its two readers. In Global at (0,0,0) the honest fallback is revert, because in the composition where that rule speaks at all (no HighlightJs) there is genuinely no author rule to defer to. In HighlightJs at (0,0,1) it is Menlo, monospace, that sheet's own default. Every other row has one reader and no competitor, so revert is right by default.
An earlier revision of this section said this row "must not fall back to revert". That was true of the intermediate repair, in which Global did not read the token at all, and is false of the shipped one. Corrected after @neo-gpt-emmy read the live ticket against b261233c52.
Retained pre-existing tokens the structure layer still reads — --core-fontfamily-sans, --core-fontsize-h1/h2/h3, --core-fontsize-body, --core-fontweight-regular/semibold, --core-lineheight-headline/paragraph, --sem-color-fg-neutral-contrast. Classic themes un-declare all ten, giving 24 declarations per classic theme (14 --global-* + 10 retained).
Out of Scope
Giving the classic families heading / code / mark typography. #18145 already routed that to its own ticket as a different gap.
The two dialog/Base.scss baseline entries — still exempt, still blocked on the (0,3,0)-over-ui-variant design call.
The SASS-to-plain-CSS question — D#18182, explicitly post-v13.2.
Avoided Traps
Reading the block count as the token count. Ten blocks per neo theme suggests a large token surface; the diff says five diverge and the rest are already tokenized. Sizing this batch from the guard's number would have over-scoped it fivefold.
Proving the move safe with a declaration diff. That instrument certified batch 1 and missed a cascade change it had no term for. The weight table comes first here, and it is what surfaced both findings.
Deleting the classic p reset as dead weight. It carries no values, which reads as removable; it is in fact the only place those three families state that they decline to style paragraphs, and #18141 makes an unstated decline inherit.
Decision Record impact
none — no ADR governs the SCSS structure/skin split; the authority is the operator's 2026-09-02 rule recorded in #18145 and enforced by check-theme-value-files.mjs.
Related
#18145 — parent rule and batches 1–4; this is its batch 5, graduated as that ticket anticipated.
#18141 — the omission-inherits-across-nested-scopes law AC-2 and AC-3 rest on.
#18107 — the :where() weight sweep; see Sequencing.
D#18182 — the SASS/postCSS question, post-v13.2.
unowned-rationale: filed from the nightshift heartbeat, which does not claim work it cannot finish in the same run. The measurement is complete and the batch is unstarted — free for any seat.
Live latest-open sweep: latest 20 open issues at 2026-09-03T21:10Z; no equivalent (#18107 is selector weight, this is selector presence, and they are explicitly sequenced above). A2A claim sweep over the last 12 messages, all read-states: no competing claim on the theme surface. Memory Core rationale sweep surfaced #18145's authoring session and Vega's classic-family weight findings, both cited. Own-assignment sweep: #18145 is mine and is the parent; no other same-surface lane. Structure Map gate: N/A — no .mjs placement decision, and the substrate is resources/scss/src/, whose sibling precedent is the four landed batches.
Retrieval Hint: Global.scss theme value file selectors move to src element selector weight drop realworld2 heading repaint AiToolchain FAQ tie classic themes state inherit
Context
#18145 batches 1–4 have landed (#18167 tab, #18168 grid, #18170 button, #18172 portal) and its guard is live (#18196).
node buildScripts/util/check-theme-value-files.mjsat dev274b63beacreports 5 files pending, 2 exempt, and all five pending files areGlobal.scss:resources/scss/theme-neo-light/Global.scssresources/scss/theme-neo-dark/Global.scssresources/scss/theme-light/Global.scssresources/scss/theme-dark/Global.scssresources/scss/theme-cyberpunk/Global.scss#18145 called this batch out in advance: "largest and least mechanical; likely wants its own ticket once the others land." They have landed. This is that ticket, and the measurement below is why it is not a fifth mechanical batch.
The Problem
The neo blocks are structure that never belonged in a theme file, and the diff proves it.
theme-neo-light/Global.scss:14-79againsttheme-neo-dark/Global.scss:14-80is 61 of 66 lines byte-identical. Everyh1/h2/h3/i, emdeclaration already readsvar(--core-*)/var(--sem-*)— no literal, no theme decision. Only five declarations diverge:pcolorvar(--sem-color-fg-neutral-contrast)var(--white-off)code:not(.hljs)background-color#E6EDF7var(--sem-color-surface-primary-background)h1..h6 code:not(.hljs)colorvar(--white-off)markbackground-colorvar(--green-50)var(--green-900)markcolorblackvar(--white)So the batch needs 5 new tokens, not the ~25 the block count suggests.
The classic block is already the answer, written as a selector.
theme-light,theme-darkandtheme-cyberpunkeach carry one identical block:p { color : inherit; font-family: inherit; font-size : inherit; font-weight: inherit; line-height: inherit; }Zero values. It is a UA-default reset stating "this family declines to style paragraphs" — the exact statement the move needs those themes to make as tokens.
The Architectural Reality
These are element selectors, so the move drops weight further than any prior batch did.
:root .neo-theme-neo-light h1is (0,2,1); a structural rule inresources/scss/src/Global.scssis bareh1, (0,0,1). Batch 1 shipped a specificity regression precisely because a declaration diff cannot see a cascade change, so this batch was swept for declarations sitting between the two weights on an overlapping property.One named repaint.
resources/scss/src/apps/realworld2/MainContainer.scss:28h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { color : inherit; font-family: inherit; font-weight: 500; line-height: 1.1; }is (0,1,1). Today it loses to the theme's (0,2,1) heading block on all four properties. At bare
h1it wins, and realworld2 headings fall back from the design tokens toinherit/500/1.1.One latent tie the batch found rather than caused.
resources/scss/src/apps/portal/home/parts/AiToolchain.scss:56,80,87—.portal-home-parts-aitoolchain .agent-os-faq h2|h3|pis (0,2,1), an exact tie with:root .neo-theme-neo-* h2|h3|p, overlapping oncolor/font-size/line-height. Theme sheets load lazily per class, so which rule wins the portal FAQ's typography is source-order-dependent today. That is the non-determinism #18145 exists to remove.Swept and clear (no overlapping property, so weight is irrelevant):
.neo-markdown-component p(marginonly) and.neo-app-content-sections-container.neo-container h3(margins only).The constraint collision that makes this its own ticket
Three rules already on the books meet here:
resources/scss/src/, and every token they introduce is declared in all themes.marktypography here.Satisfying all three means the classic families must state the neutral value rather than omit it — which is what their
p { …: inherit }block already does, one property set at a time. The batch generalizes that block into tokens instead of deleting it.Unmeasured, and the implementer must measure it before relying on it: a moved declaration reading
var(--core-fontsize-h1)in a family that never declares it is invalid-at-computed-value-time, and IACVT resolves an inherited property toinherit— which would make the move self-neutralizing for classic themes at the document root. Per rule 3 that cannot hold for a classic theme nested inside a neo scope, where the ancestor's token is visible. The discriminating witness is a nested arm (classic scope inside a neo scope) asserting the outer scope resolves a real value before asserting the inner one inherits nothing — the #18141 witness shape, which fails vacuously without the outer assertion.The Fix
resources/scss/src/Global.scss, each declaration reading a token with arevertfallback.--global-*tokens and declare them in all five themes.initial, which is how a family states its decline — then delete theirp { …: inherit }block, since it has become those declarations.buildScripts/util/check-theme-value-files-baseline.jsonby the five entries in the same commit (the guard fails a baselined file whose count drops).Sequencing
This batch and #18107 must not run concurrently. #18107 changes the weight a theme sheet declares at (
:where(), zero specificity); this changes where the selector lives. Both move the same rules across the same cascade thresholds, and Vega already flagged the attribution cost on D#18182 OQ5: with both in flight, a paint regression cannot be attributed to either. Whichever lands first, the other re-measures. The realworld2 repaint above is weight-drop-shaped, not move-shaped — #18107's:where()sweep hits the identical victim if it reachesGlobal.scss, so that measurement belongs to both tickets.Acceptance Criteria
Global.scssfiles contain variable declarations only; the moved selectors live inresources/scss/src/Global.scss.apps/realworld2/neo-config.jsondeclaresthemes: ["neo-theme-dark", "neo-theme-light"]— both classic, and classic themes have never carried heading blocks. So no theme heading rule has ever matched realworld2:.rw2-main-container h1(0,1,1) already won and still outranks the structural (0,0,1), and every token it would read is declaredinitialby its own themes, reverting to the UA exactly as before. The predicted repaint required a neo theme the app does not load. (Separately observed, not a claim about this batch: realworld2 does not currently boot —Base.on is not a function— on a server where the portal mounts fine on the same branch.)node ./buildScripts/build/themes.mjs -f -n -e esm -t all, verified by output rather than exit code (build-themesprompts and exits 0 non-TTY, leavingdist/stale).check-theme-value-files.mjsexits 0 with the five entries removed from the baseline.Contract Ledger Matrix — the consumed token surface
Added at @neo-gpt-emmy's RA-2: this batch introduces a token vocabulary that themes are now required to satisfy, and neither this ticket nor #18145 recorded it. Source of Authority for every row is
resources/scss/src/Global.scss(the structure layer that reads them), except the last, which isutil/HighlightJs.scss. Every row is verified against the built CSS rather than the source.--global-heading-letter-spacingh1,h2,h3letter-spacingrevert→ UA-0.02eminitial-0.96px=-0.02em × 48px--global-paragraph-colorpcolourrevert→ UAinitial--global-code-background-color·--global-code-border·--global-code-border-radius·--global-code-font-size·--global-code-paddingcodeboxrevert→ UAinitial--global-code-font-familycodefont — two readersrevertin Global at(0,0,0);Menlo, monospacein HighlightJs at(0,0,1)var(--core-fontfamily-mono)initialMenlo, monospaceclassic-composed, UAmonospaceclassic-uncomposed,Source Code Proneo either way--global-heading-code-color·--global-heading-code-margin·--global-heading-code-paddingh1..h6 coderevert→ UAinitial--global-mark-background-color·--global-mark-color·--global-mark-paddingmarkrevert→ UAinitialrgb(0,0,0)neo /rgb(255,255,0)classicThe contract in one line: a token's fallback must be the value the next author rule would supply from that rule's own position — so the correct fallback is a property of the declaration's weight, not of the token.
--global-code-font-familyis the only row with a competing author sheet, and it is the row that shows why: the same token needs different fallbacks in its two readers. In Global at(0,0,0)the honest fallback isrevert, because in the composition where that rule speaks at all (no HighlightJs) there is genuinely no author rule to defer to. In HighlightJs at(0,0,1)it isMenlo, monospace, that sheet's own default. Every other row has one reader and no competitor, sorevertis right by default.An earlier revision of this section said this row "must not fall back to
revert". That was true of the intermediate repair, in which Global did not read the token at all, and is false of the shipped one. Corrected after @neo-gpt-emmy read the live ticket againstb261233c52.Retained pre-existing tokens the structure layer still reads —
--core-fontfamily-sans,--core-fontsize-h1/h2/h3,--core-fontsize-body,--core-fontweight-regular/semibold,--core-lineheight-headline/paragraph,--sem-color-fg-neutral-contrast. Classic themes un-declare all ten, giving 24 declarations per classic theme (14--global-*+ 10 retained).Out of Scope
code/marktypography. #18145 already routed that to its own ticket as a different gap.dialog/Base.scssbaseline entries — stillexempt, still blocked on the (0,3,0)-over-ui-variant design call.:where()weight — #18107, and see Sequencing.Avoided Traps
preset as dead weight. It carries no values, which reads as removable; it is in fact the only place those three families state that they decline to style paragraphs, and #18141 makes an unstated decline inherit.Decision Record impact
none— no ADR governs the SCSS structure/skin split; the authority is the operator's 2026-09-02 rule recorded in #18145 and enforced bycheck-theme-value-files.mjs.Related
:where()weight sweep; see Sequencing.unowned-rationale:filed from the nightshift heartbeat, which does not claim work it cannot finish in the same run. The measurement is complete and the batch is unstarted — free for any seat.Live latest-open sweep: latest 20 open issues at 2026-09-03T21:10Z; no equivalent (#18107 is selector weight, this is selector presence, and they are explicitly sequenced above). A2A claim sweep over the last 12 messages, all read-states: no competing claim on the theme surface. Memory Core rationale sweep surfaced #18145's authoring session and Vega's classic-family weight findings, both cited. Own-assignment sweep: #18145 is mine and is the parent; no other same-surface lane. Structure Map gate: N/A — no
.mjsplacement decision, and the substrate isresources/scss/src/, whose sibling precedent is the four landed batches.Origin Session ID: fafaa395-59ad-4b53-afce-076e4a607f36
Retrieval Hint:
Global.scss theme value file selectors move to src element selector weight drop realworld2 heading repaint AiToolchain FAQ tie classic themes state inherit