diff --git a/docs/web-ui/css-vars.md b/docs/web-ui/css-vars.md
index ad140eb6..54f86636 100644
--- a/docs/web-ui/css-vars.md
+++ b/docs/web-ui/css-vars.md
@@ -1,11 +1,22 @@
# CSS custom properties (theme variables)
All colour variables are declared **once** in
-`frontend/packages/shared/src/base.css` under `:root`.
+`frontend/packages/shared/src/theme.css` under `:root`.
Per-page stylesheets (`common.css`, `dashboard.css`, `flow.css`, `logs.css`, `agent.css`) must reference these
-names and must **not** redeclare them.
+names and must **not** redeclare them. (`base.css` holds only the
+shared `body` typography — it references the palette but no longer
+defines it.)
-## Palette (`base.css`)
+`theme.css` is deliberately a **standalone** stylesheet, not `@import`ed
+into the page bundles: each package re-exports it (`src/theme.css` →
+`@import "@hive/shared/theme.css"`) so esbuild emits its own
+`dist/static/theme.css`, and every page links it **first**
+(``) ahead of the page CSS.
+Because the palette lives in its own output file, a theme swap — e.g. a
+stylix-generated variant carrying the same variable names — can replace
+just that one file without rebuilding the rest of the frontend bundle.
+
+## Palette (`theme.css`)
| Variable | Hex | Catppuccin Mocha | Use |
|---|---|---|---|
diff --git a/frontend/packages/agent/build.mjs b/frontend/packages/agent/build.mjs
index 6b231f38..60916de8 100644
--- a/frontend/packages/agent/build.mjs
+++ b/frontend/packages/agent/build.mjs
@@ -43,15 +43,19 @@ await build({
logLevel: 'info',
});
-// Bundle the CSS — the @import lines pull in shared/base.css and
-// shared/terminal.css from the @hive/shared workspace dep.
-await build({
- entryPoints: [src('agent.css')],
- outfile: staticDir('agent.css'),
- bundle: true,
- loader: { '.css': 'css' },
- logLevel: 'info',
-});
+// Bundle the CSS. `theme.css` re-exports the standalone Catppuccin
+// palette (kept its own output file so a theme swap replaces only it,
+// no bundle rebuild); `agent.css`'s @import lines pull in shared
+// base.css + terminal.css from the @hive/shared workspace dep.
+for (const entry of ['theme.css', 'agent.css']) {
+ await build({
+ entryPoints: [src(entry)],
+ outfile: staticDir(entry),
+ bundle: true,
+ loader: { '.css': 'css' },
+ logLevel: 'info',
+ });
+}
for (const html of ['index.html', 'stats.html', 'screen.html']) {
copyFileSync(src(html), dist(html));
diff --git a/frontend/packages/agent/src/index.html b/frontend/packages/agent/src/index.html
index f66045a3..74ac07a7 100644
--- a/frontend/packages/agent/src/index.html
+++ b/frontend/packages/agent/src/index.html
@@ -5,6 +5,7 @@
hyperhive agent
+
diff --git a/frontend/packages/agent/src/screen.html b/frontend/packages/agent/src/screen.html
index 33b41d5c..ed0ce3c1 100644
--- a/frontend/packages/agent/src/screen.html
+++ b/frontend/packages/agent/src/screen.html
@@ -5,6 +5,7 @@
screen
+
diff --git a/frontend/packages/agent/src/stats.html b/frontend/packages/agent/src/stats.html
index 72fcf32a..8fa106f3 100644
--- a/frontend/packages/agent/src/stats.html
+++ b/frontend/packages/agent/src/stats.html
@@ -5,6 +5,7 @@
hyperhive agent — stats
+
diff --git a/frontend/packages/agent/src/theme.css b/frontend/packages/agent/src/theme.css
new file mode 100644
index 00000000..941c4150
--- /dev/null
+++ b/frontend/packages/agent/src/theme.css
@@ -0,0 +1,6 @@
+/* Standalone theme bundle — re-exports the shared Catppuccin palette so
+ esbuild emits it as its own `dist/static/theme.css`, linked first by
+ every agent page. Kept separate from the page bundles so a theme swap
+ (e.g. stylix) replaces only this file. See @hive/shared/theme.css +
+ docs/web-ui/css-vars.md. */
+@import "@hive/shared/theme.css";
diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs
index 6e16cd57..46c2724b 100644
--- a/frontend/packages/dashboard/build.mjs
+++ b/frontend/packages/dashboard/build.mjs
@@ -77,10 +77,11 @@ await build({
});
// Bundle CSS — one entry per page. esbuild resolves @import including
-// the package re-exports from @hive/shared. Each page loads common.css
-// (shared typography, badges, buttons, inbox, side panel) plus its own
-// page-specific bundle.
-for (const entry of ['common.css', 'dashboard.css', 'flow.css', 'logs.css']) {
+// the package re-exports from @hive/shared. Each page loads theme.css
+// (the standalone Catppuccin palette — kept its own file so a theme
+// swap replaces only it) + common.css (shared typography, badges,
+// buttons, inbox, side panel) plus its own page-specific bundle.
+for (const entry of ['theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css']) {
await build({
entryPoints: [src(entry)],
outfile: staticDir(entry),
diff --git a/frontend/packages/dashboard/src/flow.html b/frontend/packages/dashboard/src/flow.html
index aca98872..061a076a 100644
--- a/frontend/packages/dashboard/src/flow.html
+++ b/frontend/packages/dashboard/src/flow.html
@@ -5,6 +5,7 @@
hyperhive // FL0W
+
diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html
index 2b3f8bc1..46a64527 100644
--- a/frontend/packages/dashboard/src/index.html
+++ b/frontend/packages/dashboard/src/index.html
@@ -5,6 +5,7 @@
hyperhive // h1ve-c0re
+
diff --git a/frontend/packages/dashboard/src/logs.html b/frontend/packages/dashboard/src/logs.html
index 5bd4d38a..8945250e 100644
--- a/frontend/packages/dashboard/src/logs.html
+++ b/frontend/packages/dashboard/src/logs.html
@@ -5,6 +5,7 @@
hyperhive // LOGS
+
diff --git a/frontend/packages/dashboard/src/theme.css b/frontend/packages/dashboard/src/theme.css
new file mode 100644
index 00000000..5e71c223
--- /dev/null
+++ b/frontend/packages/dashboard/src/theme.css
@@ -0,0 +1,6 @@
+/* Standalone theme bundle — re-exports the shared Catppuccin palette so
+ esbuild emits it as its own `dist/static/theme.css`, linked first by
+ every dashboard page. Kept separate from the page bundles so a theme
+ swap (e.g. stylix) replaces only this file. See @hive/shared/theme.css
+ + docs/web-ui/css-vars.md. */
+@import "@hive/shared/theme.css";
diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json
index 795ed42c..35a885bd 100644
--- a/frontend/packages/shared/package.json
+++ b/frontend/packages/shared/package.json
@@ -8,6 +8,7 @@
"exports": {
".": "./src/index.js",
"./terminal.js": "./src/terminal.js",
+ "./theme.css": "./src/theme.css",
"./base.css": "./src/base.css",
"./terminal.css": "./src/terminal.css"
},
diff --git a/frontend/packages/shared/src/base.css b/frontend/packages/shared/src/base.css
index 7a35a06a..7136b8eb 100644
--- a/frontend/packages/shared/src/base.css
+++ b/frontend/packages/shared/src/base.css
@@ -1,25 +1,10 @@
-/* Base palette + typography shared by the hive-c0re dashboard and the
- hive-ag3nt web UI. Catppuccin Mocha. Per-page stylesheets append on
- top of this and must NOT redeclare the colour variables — the whole
- point of pulling them out is one source of truth. */
-:root {
- --bg: #1e1e2e; /* base */
- --bg-elev: #181825; /* mantle */
- --crust: #11111b; /* crust — terminal background */
- --fg: #cdd6f4; /* text */
- --muted: #7f849c; /* overlay1 */
- --purple: #cba6f7; /* mauve */
- --purple-dim: #45475a;/* surface1 */
- --cyan: #89dceb; /* sky */
- --blue: #89b4fa; /* blue */
- --pink: #f5c2e7; /* pink */
- --amber: #fab387; /* peach */
- --yellow: #f9e2af; /* yellow */
- --green: #a6e3a1; /* green */
- --red: #f38ba8; /* red */
- --border: #313244; /* surface0 */
- --subtext0: #a6adc8; /* subtext0 — secondary/toolbar text, dimmer than --fg but lighter than --muted */
-}
+/* Base typography shared by the hive-c0re dashboard and the hive-ag3nt
+ web UI. The colour variables it references (`--bg`, `--fg`, …) live in
+ the standalone `theme.css` (Catppuccin Mocha), linked separately by
+ every page BEFORE this file so the `:root` vars resolve — see
+ `theme.css` + docs/web-ui/css-vars.md. Per-page stylesheets append on
+ top of this and must NOT redeclare the colour variables; `theme.css`
+ is the one source of truth. */
body {
background: var(--bg);
color: var(--fg);
diff --git a/frontend/packages/shared/src/theme.css b/frontend/packages/shared/src/theme.css
new file mode 100644
index 00000000..9c59c7e5
--- /dev/null
+++ b/frontend/packages/shared/src/theme.css
@@ -0,0 +1,30 @@
+/* Theme colour variables (Catppuccin Mocha) — the single source of
+ truth for the hive UI palette. Kept in a standalone file, linked as
+ its own `` by every page
+ BEFORE the page CSS, so the `:root` vars are defined for everything
+ that references them (base typography, terminal, dashboard, agent).
+
+ Why standalone rather than `@import`ed into the page bundles: esbuild
+ inlines `@import` at build time, which would bake the palette into
+ every CSS bundle. Emitting `theme.css` as its own output file lets a
+ theme swap (e.g. a stylix-generated variant carrying the same var
+ names) replace just this one file without rebuilding the rest of the
+ frontend. See docs/web-ui/css-vars.md. */
+:root {
+ --bg: #1e1e2e; /* base */
+ --bg-elev: #181825; /* mantle */
+ --crust: #11111b; /* crust — terminal background */
+ --fg: #cdd6f4; /* text */
+ --muted: #7f849c; /* overlay1 */
+ --purple: #cba6f7; /* mauve */
+ --purple-dim: #45475a;/* surface1 */
+ --cyan: #89dceb; /* sky */
+ --blue: #89b4fa; /* blue */
+ --pink: #f5c2e7; /* pink */
+ --amber: #fab387; /* peach */
+ --yellow: #f9e2af; /* yellow */
+ --green: #a6e3a1; /* green */
+ --red: #f38ba8; /* red */
+ --border: #313244; /* surface0 */
+ --subtext0: #a6adc8; /* subtext0 — secondary/toolbar text, dimmer than --fg but lighter than --muted */
+}