Problem
The marketing site (website/) is the framework's flagship dogfood app, but it does not follow the styling and layout guidance WebJs ships to its own users. Three concrete divergences, all verified against the current tree at 4822e0d1:
1. The palettes duplicate every dark value instead of using light-dark().
.agents/skills/webjs/references/styling.md (the "Light and dark, defined once (DRY)" section, L112-142) and packages/cli/lib/create.js (L1245-1330, the generated app/layout.ts token block) both teach one rule: write each colour token ONCE with the native light-dark(LIGHT, DARK) function and let color-scheme pick the side. The website does the opposite, in two places:
website/app/layout.ts L293-323: the dark palette is written TWICE, once under @media (prefers-color-scheme: dark) { :root:not([data-theme='light']) } and again verbatim under :root[data-theme='dark']. 16 tokens, ~30 lines of pure duplication. The two blocks are already byte-equal, so any future edit has to be made in both or the OS-dark and toggle-dark paths silently diverge.
website/public/input.css L152-218: the .ui-preview component-preview palette writes 19 tokens THREE times (base, @media (prefers-color-scheme: dark), :root[data-theme='dark']), ~66 lines.
The prerequisite for light-dark() is already in place: website/app/layout.ts L248 declares color-scheme: light dark on :root, L324 sets :root[data-theme='light'] { color-scheme: light } and L310 sets :root[data-theme='dark'] { color-scheme: dark }. So the three-way selector logic is redundant with a color-scheme cascade that already exists.
2. website/components/ui/ holds 11 accidentally-committed dead files.
website/AGENTS.md states that components/ui/ is "intentionally EMPTY here, left free for webjs ui add to own, exactly as the scaffold expects". It is not empty: commit 235d0fb0 (#1213) added 11 files / 766 lines there. Every one is BYTE-IDENTICAL to its counterpart in the gitignored generated mirror website/modules/ui/components/ (verified with diff on card.ts, kbd.ts, table.ts), and NONE is imported by anything in the app (grep for each basename across app lib modules components returns zero non-self hits).
They leaked because website/.gitignore ignores /modules/ui/components/, /lib/utils/cn.ts, and /lib/utils/dom.ts but NOT /components/ui/.
3. Doc drift in website/AGENTS.md.
The "Announcement banner" section describes a strip the root layout renders "just above the sticky header". No such markup exists in app/layout.ts any more, and L329 explicitly comments --header-h: 59px; /* #610 fixed header offset (no banner) */. The same section calls the header "sticky" when #610 deliberately made it position: fixed.
Design / approach
1. Collapse both palettes onto light-dark().
For app/layout.ts, fold each duplicated dark colour back into its :root declaration as light-dark(<light>, <dark>) and delete both dark blocks, keeping only the color-scheme switches. Per the styling reference's "Edge cases" paragraph, light-dark() is COLOUR-only, so the non-colour and composite tokens must keep an explicit override pair:
--glow-strength (0.16 / 0.08) is a bare number, not a colour.
--shadow-sm / --shadow are full shadow values (geometry + colour), so light-dark() cannot wrap them whole. Either keep the override pair or split the colour out into its own light-dark() token and build the shadow from it (preferred: it puts the per-theme part back under the single-definition rule).
--accent-tint, --cta-surface, --accent-surface, --accent-border are DERIVED via color-mix() from --accent-live; once --accent-live is a light-dark() token they track both themes for free and their overrides can go.
For public/input.css, the .ui-preview block is a scoped container rather than :root, which is fine: color-scheme inherits, so .ui-preview picks up whatever :root resolved and light-dark() inside it selects the matching side with no extra selectors.
2. Delete website/components/ui/*.ts and add /components/ui/ to website/.gitignore, restoring the state website/AGENTS.md already documents and keeping the directory free for webjs ui add.
3. Correct the two stale paragraphs in website/AGENTS.md.
Implementation notes (for the implementing agent)
Where to edit
website/app/layout.ts L247-324: the :root token block, the @media (prefers-color-scheme: dark) block, the :root[data-theme='dark'] block, and the two color-scheme switches. This is inside a template literal in a page/layout, which is legal (a layout never hydrates, so interpolating into <style> is allowed; invariant 9's backtick ban still applies inside the html tag).
website/public/input.css L152-218: the .ui-preview palette and its two dark blocks. Do NOT touch the @custom-variant dark block at L23-28 or the @theme / @theme inline blocks at L30-137; the custom variant is already correctly keyed off [data-theme] rather than the kit's .dark, which is what keeps the previews in step with the page theme.
website/components/ui/*.ts: delete all 11 files.
website/.gitignore: add /components/ui/.
website/AGENTS.md: the "Announcement banner" section and the components/ layout block.
Landmines / gotchas
website/scripts/copy-registry.mjs L44-63 carries a one-time LEGACY migration that DELETES files from components/ui/ whose contents match /from '(\.\/lib\/|\.\.\/\.\.\/lib\/ui\/)/. The 11 committed files have NO imports at all, so they do not match and survive every dev cycle. Deleting them by hand is therefore required; the script will not do it. Leave that migration block alone.
website/public/tailwind.css is a GITIGNORED compiled artifact. Edit public/input.css only, then let webjs.dev.before / webjs.dev.regenerate recompile (see website/AGENTS.md "Run"). Never hand-edit tailwind.css.
- The
.ui-preview palette carries a comment (L144-150) saying it mirrors what webjs ui init writes into a user's globals.css, i.e. packages/ui/packages/registry/themes/index.css. That file is NOT actually mirrored verbatim today: it uses @custom-variant dark (&:is(.dark *)) plus :root / .dark blocks (L55, L66, L102), while the website keys off [data-theme]. If .ui-preview moves to light-dark() and the registry theme does not, update that comment so it stops claiming a byte-for-byte mirror it never was. Converting the registry theme itself is a SCAFFOLD surface change and belongs in a separate issue (it would need the webjs-scaffold-sync skill and a generate-boot-check pass).
- Verify in a REAL browser in all three toggle states (
system with a dark OS, forced light, forced dark) plus the /ui gallery previews. Per the styling reference: "Light mode passing proves nothing about dark." The <theme-toggle> at website/components/theme-toggle.ts writes data-theme on <html>; the no-FOUC bootstrap that reads it back is the inline script at app/layout.ts L128-134.
light-dark() is CSS Color 5 / Baseline 2024. All four current targets (the site's supported browsers) have it, but oklch() values inside it are fine since light-dark() accepts any <color>.
Invariants to respect
- AGENTS.md invariant 7 (light-DOM tag-prefix) does not apply to the layout's
<style>: those rules belong to a layout, not a component. Do not "fix" them.
- AGENTS.md invariant 11 (prose punctuation and
WebJs brand casing) applies to every markdown line touched in website/AGENTS.md.
- The root layout is the ONE file allowed to write its own shell (invariant 8). Do not move the token block anywhere else.
Tests + docs surfaces
website/test/ssr/* is the existing SSR suite; website/test/ssr/ui-gallery.test.ts already pins seed: false. Add an SSR assertion that the rendered root layout emits light-dark( and NO @media (prefers-color-scheme: dark) palette block, with the counterfactual (revert the layout, the test reds).
- Add a test that
website/components/ui/ stays empty, so the accidental re-commit cannot recur silently.
- Run
npx webjs check in website/ (currently clean) and npm test.
- No framework
packages/*/src change here, so the doc gate does not apply; website/AGENTS.md is the doc surface for this one.
Acceptance criteria
Problem
The marketing site (
website/) is the framework's flagship dogfood app, but it does not follow the styling and layout guidance WebJs ships to its own users. Three concrete divergences, all verified against the current tree at4822e0d1:1. The palettes duplicate every dark value instead of using
light-dark()..agents/skills/webjs/references/styling.md(the "Light and dark, defined once (DRY)" section, L112-142) andpackages/cli/lib/create.js(L1245-1330, the generatedapp/layout.tstoken block) both teach one rule: write each colour token ONCE with the nativelight-dark(LIGHT, DARK)function and letcolor-schemepick the side. The website does the opposite, in two places:website/app/layout.tsL293-323: the dark palette is written TWICE, once under@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) }and again verbatim under:root[data-theme='dark']. 16 tokens, ~30 lines of pure duplication. The two blocks are already byte-equal, so any future edit has to be made in both or the OS-dark and toggle-dark paths silently diverge.website/public/input.cssL152-218: the.ui-previewcomponent-preview palette writes 19 tokens THREE times (base,@media (prefers-color-scheme: dark),:root[data-theme='dark']), ~66 lines.The prerequisite for
light-dark()is already in place:website/app/layout.tsL248 declarescolor-scheme: light darkon:root, L324 sets:root[data-theme='light'] { color-scheme: light }and L310 sets:root[data-theme='dark'] { color-scheme: dark }. So the three-way selector logic is redundant with acolor-schemecascade that already exists.2.
website/components/ui/holds 11 accidentally-committed dead files.website/AGENTS.mdstates thatcomponents/ui/is "intentionally EMPTY here, left free forwebjs ui addto own, exactly as the scaffold expects". It is not empty: commit235d0fb0(#1213) added 11 files / 766 lines there. Every one is BYTE-IDENTICAL to its counterpart in the gitignored generated mirrorwebsite/modules/ui/components/(verified withdiffoncard.ts,kbd.ts,table.ts), and NONE is imported by anything in the app (grepfor each basename acrossapp lib modules componentsreturns zero non-self hits).They leaked because
website/.gitignoreignores/modules/ui/components/,/lib/utils/cn.ts, and/lib/utils/dom.tsbut NOT/components/ui/.3. Doc drift in
website/AGENTS.md.The "Announcement banner" section describes a strip the root layout renders "just above the sticky header". No such markup exists in
app/layout.tsany more, and L329 explicitly comments--header-h: 59px; /* #610 fixed header offset (no banner) */. The same section calls the header "sticky" when #610 deliberately made itposition: fixed.Design / approach
1. Collapse both palettes onto
light-dark().For
app/layout.ts, fold each duplicated dark colour back into its:rootdeclaration aslight-dark(<light>, <dark>)and delete both dark blocks, keeping only thecolor-schemeswitches. Per the styling reference's "Edge cases" paragraph,light-dark()is COLOUR-only, so the non-colour and composite tokens must keep an explicit override pair:--glow-strength(0.16/0.08) is a bare number, not a colour.--shadow-sm/--shadoware full shadow values (geometry + colour), solight-dark()cannot wrap them whole. Either keep the override pair or split the colour out into its ownlight-dark()token and build the shadow from it (preferred: it puts the per-theme part back under the single-definition rule).--accent-tint,--cta-surface,--accent-surface,--accent-borderare DERIVED viacolor-mix()from--accent-live; once--accent-liveis alight-dark()token they track both themes for free and their overrides can go.For
public/input.css, the.ui-previewblock is a scoped container rather than:root, which is fine:color-schemeinherits, so.ui-previewpicks up whatever:rootresolved andlight-dark()inside it selects the matching side with no extra selectors.2. Delete
website/components/ui/*.tsand add/components/ui/towebsite/.gitignore, restoring the statewebsite/AGENTS.mdalready documents and keeping the directory free forwebjs ui add.3. Correct the two stale paragraphs in
website/AGENTS.md.Implementation notes (for the implementing agent)
Where to edit
website/app/layout.tsL247-324: the:roottoken block, the@media (prefers-color-scheme: dark)block, the:root[data-theme='dark']block, and the twocolor-schemeswitches. This is inside a template literal in a page/layout, which is legal (a layout never hydrates, so interpolating into<style>is allowed; invariant 9's backtick ban still applies inside thehtmltag).website/public/input.cssL152-218: the.ui-previewpalette and its two dark blocks. Do NOT touch the@custom-variant darkblock at L23-28 or the@theme/@theme inlineblocks at L30-137; the custom variant is already correctly keyed off[data-theme]rather than the kit's.dark, which is what keeps the previews in step with the page theme.website/components/ui/*.ts: delete all 11 files.website/.gitignore: add/components/ui/.website/AGENTS.md: the "Announcement banner" section and thecomponents/layout block.Landmines / gotchas
website/scripts/copy-registry.mjsL44-63 carries a one-time LEGACY migration that DELETES files fromcomponents/ui/whose contents match/from '(\.\/lib\/|\.\.\/\.\.\/lib\/ui\/)/. The 11 committed files have NO imports at all, so they do not match and survive every dev cycle. Deleting them by hand is therefore required; the script will not do it. Leave that migration block alone.website/public/tailwind.cssis a GITIGNORED compiled artifact. Editpublic/input.cssonly, then letwebjs.dev.before/webjs.dev.regeneraterecompile (seewebsite/AGENTS.md"Run"). Never hand-edittailwind.css..ui-previewpalette carries a comment (L144-150) saying it mirrors whatwebjs ui initwrites into a user'sglobals.css, i.e.packages/ui/packages/registry/themes/index.css. That file is NOT actually mirrored verbatim today: it uses@custom-variant dark (&:is(.dark *))plus:root/.darkblocks (L55, L66, L102), while the website keys off[data-theme]. If.ui-previewmoves tolight-dark()and the registry theme does not, update that comment so it stops claiming a byte-for-byte mirror it never was. Converting the registry theme itself is a SCAFFOLD surface change and belongs in a separate issue (it would need thewebjs-scaffold-syncskill and a generate-boot-check pass).systemwith a dark OS, forcedlight, forceddark) plus the/uigallery previews. Per the styling reference: "Light mode passing proves nothing about dark." The<theme-toggle>atwebsite/components/theme-toggle.tswritesdata-themeon<html>; the no-FOUC bootstrap that reads it back is the inline script atapp/layout.tsL128-134.light-dark()is CSS Color 5 / Baseline 2024. All four current targets (the site's supported browsers) have it, butoklch()values inside it are fine sincelight-dark()accepts any<color>.Invariants to respect
<style>: those rules belong to a layout, not a component. Do not "fix" them.WebJsbrand casing) applies to every markdown line touched inwebsite/AGENTS.md.Tests + docs surfaces
website/test/ssr/*is the existing SSR suite;website/test/ssr/ui-gallery.test.tsalready pinsseed: false. Add an SSR assertion that the rendered root layout emitslight-dark(and NO@media (prefers-color-scheme: dark)palette block, with the counterfactual (revert the layout, the test reds).website/components/ui/stays empty, so the accidental re-commit cannot recur silently.npx webjs checkinwebsite/(currently clean) andnpm test.packages/*/srcchange here, so the doc gate does not apply;website/AGENTS.mdis the doc surface for this one.Acceptance criteria
website/app/layout.tsdeclares every per-theme COLOUR token once vialight-dark(); the@media (prefers-color-scheme: dark)and:root[data-theme='dark']palette duplicates are gone, leaving onlycolor-schemeswitches plus the documented non-colour overrideswebsite/public/input.css.ui-previewdeclares its 19 tokens once vialight-dark(); its two dark blocks are gone--glow-strength, the shadows) are handled per the styling reference's edge-case rule, not silently droppedwebsite/components/ui/is empty and/components/ui/is gitignoredwebsite/AGENTS.mdno longer documents a non-existent announcement banner, and describes the header as fixed (dogfood: mobile navbar flickers on forward nav (backdrop-blur sticky header) #610) rather than sticky/docspage, and a/uicomponent pagenpx webjs checkandnpm testpass inwebsite/