diff --git a/.github/trigger-deploy b/.github/trigger-deploy
index 4fff828e..566643f7 100644
--- a/.github/trigger-deploy
+++ b/.github/trigger-deploy
@@ -1 +1 @@
-1785992689
+1786027990
diff --git a/.github/trigger-test b/.github/trigger-test
index a23595e0..0302ffd0 100644
--- a/.github/trigger-test
+++ b/.github/trigger-test
@@ -1 +1 @@
-1786026794
+1786029809
diff --git a/scripts/test-theme.js b/scripts/test-theme.js
index 77e8d1b4..1fabb962 100644
--- a/scripts/test-theme.js
+++ b/scripts/test-theme.js
@@ -1,11 +1,23 @@
-// Dark-theme toggle — offline unit tests. No network. Verifies the shared
-// shell ships the pre-paint theme script (no flash of the wrong theme), the
-// moon/sun toggle button, and the dark-mode CSS overrides, and that the
-// palette flip is internally consistent (--ink and --cream flip together so
-// the ~100 solid-ink chips invert cleanly).
+// Dark-only theme — offline unit tests. No network.
+//
+// The site used to ship a light default plus a moon/sun toggle, a pre-paint
+// script reading localStorage, and a :root[data-theme="dark"] override block.
+// That is all gone: dark is now the ONLY theme, set directly on :root.
+//
+// Why that shape rather than just defaulting the toggle to dark: a palette
+// applied through an attribute needs JavaScript to set it, which means a frame
+// of the wrong colours before the script runs, plus a stored preference that
+// can disagree with the markup. Dark values on :root make the first paint
+// already correct with no script at all.
+//
+// These assertions exist because a HALF-removed theme is worse than either
+// state. Reintroducing a light token, an override block, or a toggle should
+// fail here rather than ship a page that is dark in some places and white in
+// others.
//
// node scripts/test-theme.js
import { ledgerShell, LEDGER_CSS } from "../src/ledger-chrome.js";
+import { readFileSync } from "node:fs";
let passed = 0, failed = 0;
const ok = (cond, msg) => {
@@ -18,36 +30,55 @@ const html = ledgerShell({
baseUrl: "https://agent402.tools", body: "hi",
});
-// --- no-flash: theme is applied from storage/prefers BEFORE first paint -------
-const headStart = html.slice(0, html.indexOf(""));
-ok(headStart.includes("localStorage.getItem('a402-theme')"), "pre-paint script reads the saved theme");
-ok(headStart.includes("prefers-color-scheme:dark"), "falls back to the OS preference");
-ok(headStart.includes("setAttribute('data-theme','dark')"), "sets data-theme before paint");
-// the setter script must be in
too, so the button's onclick resolves
-ok(headStart.includes("function a402ToggleTheme"), "toggle function defined in head");
-ok(html.indexOf("function a402ToggleTheme") < html.indexOf(""), "toggle fn is above the body");
-
-// --- the moon/sun control lives in the nav -----------------------------------
-ok(html.includes('class="ml-theme-toggle"'), "nav has the theme toggle button");
-ok(html.includes('onclick="a402ToggleTheme()"'), "button is wired to the toggle");
-ok(html.includes('class="ml-moon"') && html.includes('class="ml-sun"'), "both moon + sun glyphs present");
-ok(/aria-label="Toggle dark mode"/.test(html), "toggle is labelled for a11y");
-
-// --- dark palette exists and flips the paired tokens together ----------------
-ok(LEDGER_CSS.includes(':root[data-theme="dark"]'), "dark-theme CSS block present");
-// --ink (foreground) goes light AND --cream (text on ink chips) goes dark, so
-// every `background:var(--ink);color:var(--cream)` chip becomes light-on-dark.
-const dark = LEDGER_CSS.slice(LEDGER_CSS.indexOf(':root[data-theme="dark"]'));
-const darkBlock = dark.slice(0, dark.indexOf("}"));
-ok(/--ink:\s*#[EeFf]/.test(darkBlock), "dark --ink is light");
-ok(/--cream:\s*#0/.test(darkBlock), "dark --cream is dark (chips invert cleanly)");
-ok(/--paper:\s*#0/.test(darkBlock), "dark --paper is dark");
-// the moon hides in dark, the sun hides in light (CSS-only, no JS needed)
-ok(LEDGER_CSS.includes('.ml-theme-toggle .ml-sun'), "sun hidden by default (light)");
-ok(LEDGER_CSS.includes(':root[data-theme="dark"] .ml-theme-toggle .ml-moon'), "moon hidden in dark");
-
-// light mode stays the default: no data-theme attribute is hardcoded on
-ok(!/]*data-theme=/.test(html), "html has no hardcoded theme (light is the default)");
+// --- the :root palette IS the dark palette ----------------------------------
+const rootBlock = LEDGER_CSS.slice(LEDGER_CSS.indexOf(":root {"), LEDGER_CSS.indexOf("}", LEDGER_CSS.indexOf("--font-mono")));
+const tok = (name) => (rootBlock.match(new RegExp(`${name}:\\s*(\\S+);`)) || [])[1] || "";
+const isDark = (h) => /^#[0-3]/.test(h); // #0E0E10, #171719, #1E1E21…
+const isLight = (h) => /^#[C-Fc-f]/.test(h); // #ECECEA, #F4F4F2…
+
+ok(isDark(tok("--paper")), `page background is dark (--paper ${tok("--paper")})`);
+ok(isLight(tok("--ink")), `foreground is light (--ink ${tok("--ink")})`);
+ok(isDark(tok("--card")), `cards are dark (--card ${tok("--card")})`);
+// --ink and --cream must stay paired: ~100 chips are background:var(--ink)
+// with color:var(--cream), so if only one of them flips they go invisible.
+ok(isDark(tok("--cream")), `--cream is dark so ink chips read light-on-dark (${tok("--cream")})`);
+ok(isLight(tok("--on-dark")), `text on dark surfaces stays light (--on-dark ${tok("--on-dark")})`);
+ok(/color-scheme:\s*dark/.test(LEDGER_CSS), "color-scheme is dark so form controls and scrollbars match");
+ok(!/color-scheme:\s*light/.test(LEDGER_CSS), "no light color-scheme survives");
+
+// --- nothing of the toggle mechanism is left --------------------------------
+ok(!LEDGER_CSS.includes('[data-theme="dark"]'), "no [data-theme] override block in the CSS");
+// Matches the ATTRIBUTE form (`data-theme=`) and the selector form
+// (`[data-theme`), not the bare word: the CSS comment above legitimately
+// explains why the attribute is gone, and a test that fails on its own
+// documentation is testing prose rather than behaviour.
+ok(!/data-theme\s*=/.test(html), "no data-theme attribute is set on any element");
+ok(!/\[data-theme/.test(html.replace(/\/\*[\s\S]*?\*\//g, "")), "no [data-theme] selector outside comments");
+ok(!html.includes("a402ToggleTheme"), "no toggle function ships");
+ok(!html.includes("a402-theme"), "no stored theme preference is read or written");
+ok(!html.includes("prefers-color-scheme"), "the OS preference no longer decides the palette");
+ok(!html.includes("ml-theme-toggle"), "the toggle button is gone from the nav");
+ok(!html.includes("ml-moon") && !html.includes("ml-sun"), "moon and sun glyphs are gone");
+
+// --- no orphaned selector where the toggle rules used to be -----------------
+// The first removal pass matched from `.ml-theme-toggle` to end of line, which
+// stranded `:root[data-theme="dark"] ` in front of the following comment and
+// produced a malformed selector - the kind that silently invalidates the rule
+// after it. Whole-line removal fixed it; this keeps it fixed.
+ok((LEDGER_CSS.match(/\{/g) || []).length === (LEDGER_CSS.match(/\}/g) || []).length,
+ "LEDGER_CSS braces balance");
+const stranded = [...LEDGER_CSS.matchAll(/\}\s*([^\n{}]*)\/\*/g)].map((m) => m[1].trim()).filter(Boolean);
+ok(stranded.length === 0,
+ `no selector text stranded before a comment${stranded.length ? ` (found "${stranded[0].slice(0, 60)}")` : ""}`);
+
+// --- the revenue chart palette had to follow the theme ----------------------
+// Its dark series colours were keyed on [data-theme="dark"]. With the attribute
+// gone that rule could never match, so the chart would have kept LIGHT series
+// colours on a permanently dark page: still legible, easy to miss in review,
+// and wrong.
+const revenueSrc = readFileSync(new URL("../src/revenue-live.js", import.meta.url), "utf8");
+ok(!revenueSrc.includes('[data-theme="dark"]'), "revenue chart has no dead [data-theme] rule");
+ok(/\.rvz\{--s1:#3987e5/.test(revenueSrc), "the chart's dark series palette is the one that ships");
console.log(`\n${failed ? "FAILED" : "OK"}: ${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);
diff --git a/src/ledger-chrome.js b/src/ledger-chrome.js
index c9857755..a7852ee4 100644
--- a/src/ledger-chrome.js
+++ b/src/ledger-chrome.js
@@ -58,40 +58,6 @@ html { overflow-x: clip; }
override on each dark container. Accent-as-background keeps white text
legible on #BF360C (5.8:1); brightening it would BREAK that, so dark
containers only override text, never accent-bg buttons. */
- --accent: #BF360C;
- --accent-lit: #F0522E;
- --paper: #FFFFFF;
- --card: #F7F7F5;
- --card-zebra: #F1F1EF;
- --footer-bg: #F2F2F0;
- --ink: #0B0B0B;
- --ink-panel: #151515;
- --muted: #4A4A4A;
- --faint: #6A6A6A;
- --hairline: #E0E0DE;
- --dash: #C9C9C7;
- --dark-border: #262626;
- --dark-border2: #343434;
- --cream: #FFFFFF;
- --cream2: #F5F5F5;
- --surface: #0B0B0B;
- --on-dark: #FFFFFF;
- --on-dark2: #F5F5F5;
- --dk-muted: #9C9C9C;
- --dk-muted2: #B8B8B8;
- --dk-muted3: #888888;
- --green: #3E9B6E;
- --font-body: 'Archivo', 'Archivo Fallback', system-ui, sans-serif;
- --font-mono: 'Space Mono', 'Space Mono Fallback', monospace;
-}
-/* Dark theme. Only the true foreground/page tokens flip: --ink (text + borders)
- goes light, --paper/--card/--muted/--faint/--hairline/--dash flip to their
- dark values. Dark SURFACES do NOT invert - every card, terminal, and CTA that
- was background:var(--surface) with color:var(--on-dark) stays dark with light
- text in both themes (that split is exactly why --surface / --on-dark exist,
- separate from the dual-use --ink). Applied from localStorage (or
- prefers-color-scheme) before first paint - see ledgerShell. */
-:root[data-theme="dark"] {
--accent: #F0522E;
--accent-lit: #F0522E;
--paper: #0E0E10;
@@ -99,28 +65,34 @@ html { overflow-x: clip; }
--card-zebra: #1E1E21;
--footer-bg: #131315;
--ink: #ECECEA;
+ --ink-panel: #171719;
--muted: #9E9E98;
--faint: #6C6C68;
--hairline: #2A2A30;
--dash: #35353B;
+ --dark-border: #262626;
+ --dark-border2: #343434;
--cream: #0E0E10;
--cream2: #171719;
- /* Dark surfaces (was background:var(--surface)) STAY dark - a card/terminal must
- not invert to light. Their text (--on-dark*) STAYS light. Only foreground
- --ink/--muted and page --paper actually flip. */
--surface: #17171A;
--on-dark: #F4F4F2;
--on-dark2: #CFCFCB;
- --ink-panel: #171719;
+ --dk-muted: #9C9C9C;
+ --dk-muted2: #B8B8B8;
+ --dk-muted3: #888888;
+ --green: #3E9B6E;
+ --font-body: 'Archivo', 'Archivo Fallback', system-ui, sans-serif;
+ --font-mono: 'Space Mono', 'Space Mono Fallback', monospace;
}
-:root { color-scheme: light; }
-:root[data-theme="dark"] { color-scheme: dark; }
+/* Dark is the ONLY theme. The palette above IS the dark palette, set directly
+ on :root rather than behind a [data-theme] attribute, so the first paint is
+ already dark: no flash, no pre-paint script, no stored preference, and
+ nothing to get out of sync. There is deliberately no light mode and no
+ toggle. Dark SURFACES still do not invert - a card or terminal that was
+ background:var(--surface) with color:var(--on-dark) keeps light text, which
+ is why --surface / --on-dark stay separate from the dual-use --ink. */
+:root { color-scheme: dark; }
body { transition: background-color .18s ease, color .18s ease; }
-.ml-theme-toggle { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; padding:0; border:1.5px solid var(--ink); background:transparent; color:var(--ink); cursor:pointer; }
-.ml-theme-toggle:hover { background: var(--card-zebra); }
-.ml-theme-toggle .ml-sun { display:none; }
-:root[data-theme="dark"] .ml-theme-toggle .ml-moon { display:none; }
-:root[data-theme="dark"] .ml-theme-toggle .ml-sun { display:inline-flex; }
/* --- mobile hamburger menu (the hover nav dropdowns don't work on touch, and
the inline links get squeezed to zero on a phone - so ≤880px collapses the
whole nav into a tap menu) --- */
@@ -484,10 +456,7 @@ function nav(activePath) {