diff --git a/agents/auth/src/agent.ts b/agents/auth/src/agent.ts
index 93be0f6..6a0732d 100644
--- a/agents/auth/src/agent.ts
+++ b/agents/auth/src/agent.ts
@@ -794,7 +794,7 @@ export class AuthAgent extends Agent {
// choosing inside a mail our own verified sender delivers. The plain
// text part needs no escaping; the subject is stripped of the line
// breaks a header injection would need.
- const html = `${escapeHtml(action)} ${escapeHtml(intro)}
${escapeHtml(action)}
If you did not request this, you can ignore this email.
`;
+ const html = `${escapeHtml(action)} ${escapeHtml(intro)}
${escapeHtml(action)}
If you did not request this, you can ignore this email.
`;
try {
await this.deliverEmail(message.to, headerSafe(`${action} · Frostbase`), text, html);
diff --git a/package.json b/package.json
index 0497d6d..691048a 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"demo:video": "node scripts/demo-video.mjs",
"demo:live": "node scripts/demo-video.mjs --live",
"build": "vite build",
+ "brand:generate": "node scripts/brand-generate.mjs",
"preview": "npm run build && wrangler dev",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
diff --git a/scripts/brand-generate.mjs b/scripts/brand-generate.mjs
new file mode 100644
index 0000000..5911829
--- /dev/null
+++ b/scripts/brand-generate.mjs
@@ -0,0 +1,60 @@
+// Renders every raster brand asset from the SVG sources in static/, so a
+// colour or wordmark change is one edit plus `npm run brand:generate`.
+//
+// static/favicon.svg -> favicon-16x16, favicon-32x32, icon-192, icon-512
+// (transparent: the cube's faces are punched out)
+// static/brand/mark.svg -> apple-touch-icon (cube on the dark tile) and
+// brand/app-icon (cube on white, dark faces)
+// static/brand/github-header.svg, social-preview.svg -> same-named PNGs
+import { readFile } from 'node:fs/promises';
+import sharp from 'sharp';
+
+const DARK = '#0b0c0f';
+const FACES = '#13151a';
+
+const favicon = await readFile('static/favicon.svg', 'utf8');
+const mark = await readFile('static/brand/mark.svg', 'utf8');
+
+/** The mark's inner body (defs + paths), for re-placing on a tile. */
+const markBody = mark
+ .replace(/^[\s\S]*?]*>/, '')
+ .replace(/<\/svg>\s*$/, '')
+ .replace(/[\s\S]*?<\/title>/, '')
+ .replace(//g, '');
+
+/** The cube centred on a square tile, filling `scale` of its width. */
+function tile(size, background, scale, faces) {
+ const inset = (64 / scale - 64) / 2;
+ const view = 64 + inset * 2;
+ const fill = faces ? ` ` : '';
+ return `
+
+ ${fill}${markBody} `;
+}
+
+const jobs = [
+ ...[16, 32].map((size) => [favicon, size, `static/favicon-${size}x${size}.png`]),
+ ...[192, 512].map((size) => [favicon, size, `static/icon-${size}.png`]),
+ [tile(180, DARK, 0.72), 180, 'static/apple-touch-icon.png'],
+ [tile(1254, '#ffffff', 0.66, FACES), 1254, 'static/brand/app-icon.png'],
+ [
+ await readFile('static/brand/github-header.svg', 'utf8'),
+ null,
+ 'static/brand/github-header.png'
+ ],
+ [
+ await readFile('static/brand/social-preview.svg', 'utf8'),
+ null,
+ 'static/brand/social-preview.png'
+ ]
+];
+
+for (const [svg, size, out] of jobs) {
+ // Render dense for crisp edges, then land on the exact target: the square
+ // size for icons, the SVG's own width/height for the banners.
+ const width = size ?? Number(svg.match(/]*\swidth="(\d+)"/)?.[1]);
+ const height = size ?? Number(svg.match(/]*\sheight="(\d+)"/)?.[1]);
+ const image = sharp(Buffer.from(svg), { density: 384 }).resize(width, height);
+ await image.png({ compressionLevel: 9 }).toFile(out);
+ console.log(`wrote ${out}`);
+}
diff --git a/src/lib/components/console-shell.svelte b/src/lib/components/console-shell.svelte
index d9ebd9b..99d4885 100644
--- a/src/lib/components/console-shell.svelte
+++ b/src/lib/components/console-shell.svelte
@@ -13,7 +13,7 @@
* carries the positioning rather than decoration.
*
* The panel follows the theme; only the terminal card inside it is
- * theme-stable espresso, via a scoped `dark` class that makes the shadcn
+ * theme-stable glacier-night, via a scoped `dark` class that makes the shadcn
* tokens inside the card resolve dark in both themes (a terminal is
* naturally dark). Never touches the root theme state.
*
@@ -67,9 +67,9 @@
+ card glacier-night in both themes while the panel follows the theme. -->
$ npx frostbase init
$ frostbase key create server --env-file
diff --git a/src/routes/(app)/login/+page.svelte b/src/routes/(app)/login/+page.svelte
index c08eb88..cdf8900 100644
--- a/src/routes/(app)/login/+page.svelte
+++ b/src/routes/(app)/login/+page.svelte
@@ -582,7 +582,7 @@
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index a964850..5263d51 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -90,8 +90,8 @@
-
-
+
+
diff --git a/src/routes/layout.css b/src/routes/layout.css
index 2410b27..06c7945 100644
--- a/src/routes/layout.css
+++ b/src/routes/layout.css
@@ -8,88 +8,90 @@
@plugin '@tailwindcss/typography';
/*
- * Cloudflare-coherent palette on a warm "linen" paper ground.
- * Brand anchors: orange #F6821F → oklch(0.7163 0.1706 53.45)
- * gold #FAAD3F → oklch(0.8056 0.1493 72.15)
- * blue #0051C3 → oklch(0.45 0.19 262) (dashboard/link blue)
- * Light mode sits on a warm "linen" off-white ground (hue ~76–85, chroma
- * ≤0.022) instead of pure white; cards are *lighter* than the background so
- * elevation still reads on tinted paper. `accent` stays a *subtle* hover
- * surface (shadcn semantics), tinted warm - never the loud gold (charts only).
+ * Frost palette on a cool "ice" paper ground.
+ * Brand anchors: Glacier #2F8FE0 → oklch(0.6341 0.1498 248.54) (primary)
+ * ice #22B3E8 → oklch(0.719 0.1357 229.56)
+ * deep #2463EB → oklch(0.5459 0.2154 262.75)
+ * Light mode sits on a cool off-white ground (hue ~240-250, chroma <=0.02)
+ * instead of pure white; cards are *lighter* than the background so elevation
+ * still reads on tinted paper. `accent` stays a *subtle* hover surface (shadcn
+ * semantics), tinted cool - never a loud brand fill. Charts keep one warm
+ * series (amber) so five series stay distinguishable on an all-blue brand.
*/
:root {
- --background: oklch(0.976 0.008 80);
- --foreground: oklch(0.155 0.008 55);
- --card: oklch(0.993 0.005 85);
- --card-foreground: oklch(0.155 0.008 55);
- --popover: oklch(0.993 0.005 85);
- --popover-foreground: oklch(0.155 0.008 55);
- --primary: oklch(0.7163 0.1706 53.45);
+ --background: oklch(0.976 0.006 240);
+ --foreground: oklch(0.16 0.014 255);
+ --card: oklch(0.994 0.003 240);
+ --card-foreground: oklch(0.16 0.014 255);
+ --popover: oklch(0.994 0.003 240);
+ --popover-foreground: oklch(0.16 0.014 255);
+ --primary: oklch(0.6341 0.1498 248.54);
--primary-foreground: oklch(1 0 0);
- --secondary: oklch(0.942 0.012 78);
- --secondary-foreground: oklch(0.24 0.012 55);
- --muted: oklch(0.945 0.01 78);
- --muted-foreground: oklch(0.53 0.016 60);
- --accent: oklch(0.928 0.022 70);
- --accent-foreground: oklch(0.24 0.026 50);
+ --secondary: oklch(0.944 0.01 242);
+ --secondary-foreground: oklch(0.24 0.016 255);
+ --muted: oklch(0.946 0.009 242);
+ --muted-foreground: oklch(0.52 0.02 252);
+ --accent: oklch(0.93 0.022 240);
+ --accent-foreground: oklch(0.24 0.03 255);
--destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.893 0.013 76);
- --input: oklch(0.893 0.013 76);
- --ring: oklch(0.7163 0.1706 53.45);
- --chart-1: oklch(0.7163 0.1706 53.45);
- --chart-2: oklch(0.8056 0.1493 72.15);
- --chart-3: oklch(0.45 0.19 262);
+ --border: oklch(0.895 0.012 245);
+ --input: oklch(0.895 0.012 245);
+ --ring: oklch(0.6341 0.1498 248.54);
+ --chart-1: oklch(0.6341 0.1498 248.54);
+ --chart-2: oklch(0.719 0.1357 229.56);
+ --chart-3: oklch(0.5459 0.2154 262.75);
--chart-4: oklch(0.35 0.07 260);
- --chart-5: oklch(0.47 0.157 37.304);
+ --chart-5: oklch(0.78 0.15 75);
--radius: 0.625rem;
- --sidebar: oklch(0.962 0.01 78);
- --sidebar-foreground: oklch(0.155 0.008 55);
- --sidebar-primary: oklch(0.7163 0.1706 53.45);
+ --sidebar: oklch(0.962 0.009 242);
+ --sidebar-foreground: oklch(0.16 0.014 255);
+ --sidebar-primary: oklch(0.6341 0.1498 248.54);
--sidebar-primary-foreground: oklch(1 0 0);
- --sidebar-accent: oklch(0.928 0.022 70);
- --sidebar-accent-foreground: oklch(0.24 0.026 50);
- --sidebar-border: oklch(0.893 0.013 76);
- --sidebar-ring: oklch(0.7163 0.1706 53.45);
+ --sidebar-accent: oklch(0.93 0.022 240);
+ --sidebar-accent-foreground: oklch(0.24 0.03 255);
+ --sidebar-border: oklch(0.895 0.012 245);
+ --sidebar-ring: oklch(0.6341 0.1498 248.54);
}
/*
- * Dark linen: the same warm hue family as the light ground (~58–80), pushed
- * dark - espresso rather than gray. Deeper background with cards lifted
- * further off it and brighter muted text for contrast; borders are warm
- * paper at low alpha so hairlines never read cold.
+ * Dark frost: the same cool hue family as the light ground, pushed dark -
+ * deep glacier-night rather than neutral gray. Cards lift further off the
+ * background, muted text brightens for contrast, and borders are pale ice at
+ * low alpha so hairlines stay cool. Primary brightens slightly so the accent
+ * keeps its contrast on the darker ground.
*/
.dark {
- --background: oklch(0.132 0.008 65);
- --foreground: oklch(0.982 0.005 80);
- --card: oklch(0.205 0.012 68);
- --card-foreground: oklch(0.982 0.005 80);
- --popover: oklch(0.205 0.012 68);
- --popover-foreground: oklch(0.982 0.005 80);
- --primary: oklch(0.7163 0.1706 53.45);
+ --background: oklch(0.14 0.014 255);
+ --foreground: oklch(0.982 0.005 240);
+ --card: oklch(0.205 0.018 255);
+ --card-foreground: oklch(0.982 0.005 240);
+ --popover: oklch(0.205 0.018 255);
+ --popover-foreground: oklch(0.982 0.005 240);
+ --primary: oklch(0.68 0.145 246);
--primary-foreground: oklch(1 0 0);
- --secondary: oklch(0.275 0.014 66);
- --secondary-foreground: oklch(0.982 0.005 80);
- --muted: oklch(0.268 0.012 66);
- --muted-foreground: oklch(0.735 0.018 72);
- --accent: oklch(0.298 0.028 58);
- --accent-foreground: oklch(0.985 0.008 75);
+ --secondary: oklch(0.275 0.02 255);
+ --secondary-foreground: oklch(0.982 0.005 240);
+ --muted: oklch(0.268 0.018 255);
+ --muted-foreground: oklch(0.74 0.022 245);
+ --accent: oklch(0.3 0.035 250);
+ --accent-foreground: oklch(0.985 0.008 240);
--destructive: oklch(0.704 0.191 22.216);
- --border: oklch(0.93 0.03 80 / 12%);
- --input: oklch(0.93 0.03 80 / 16%);
- --ring: oklch(0.7163 0.1706 53.45);
- --chart-1: oklch(0.7163 0.1706 53.45);
- --chart-2: oklch(0.8056 0.1493 72.15);
- --chart-3: oklch(0.62 0.17 258);
- --chart-4: oklch(0.75 0.09 250);
- --chart-5: oklch(0.65 0.14 40);
- --sidebar: oklch(0.185 0.011 66);
- --sidebar-foreground: oklch(0.982 0.005 80);
- --sidebar-primary: oklch(0.7163 0.1706 53.45);
+ --border: oklch(0.93 0.03 240 / 12%);
+ --input: oklch(0.93 0.03 240 / 16%);
+ --ring: oklch(0.68 0.145 246);
+ --chart-1: oklch(0.68 0.145 246);
+ --chart-2: oklch(0.78 0.12 228);
+ --chart-3: oklch(0.62 0.19 262);
+ --chart-4: oklch(0.75 0.06 250);
+ --chart-5: oklch(0.8 0.14 75);
+ --sidebar: oklch(0.185 0.016 255);
+ --sidebar-foreground: oklch(0.982 0.005 240);
+ --sidebar-primary: oklch(0.68 0.145 246);
--sidebar-primary-foreground: oklch(1 0 0);
- --sidebar-accent: oklch(0.298 0.028 58);
- --sidebar-accent-foreground: oklch(0.985 0.008 75);
- --sidebar-border: oklch(0.93 0.03 80 / 12%);
- --sidebar-ring: oklch(0.7163 0.1706 53.45);
+ --sidebar-accent: oklch(0.3 0.035 250);
+ --sidebar-accent-foreground: oklch(0.985 0.008 240);
+ --sidebar-border: oklch(0.93 0.03 240 / 12%);
+ --sidebar-ring: oklch(0.68 0.145 246);
}
@theme inline {
diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png
index 781fd24..ada2297 100644
Binary files a/static/apple-touch-icon.png and b/static/apple-touch-icon.png differ
diff --git a/static/brand/app-icon.png b/static/brand/app-icon.png
index 5334be1..d7389a9 100644
Binary files a/static/brand/app-icon.png and b/static/brand/app-icon.png differ
diff --git a/static/brand/github-header.png b/static/brand/github-header.png
index 54f12b4..b6ef156 100644
Binary files a/static/brand/github-header.png and b/static/brand/github-header.png differ
diff --git a/static/brand/github-header.svg b/static/brand/github-header.svg
index d7dfe48..f856540 100644
--- a/static/brand/github-header.svg
+++ b/static/brand/github-header.svg
@@ -1,11 +1,11 @@
-
+
-
+
- Cloudflarebase
+ Frostbase
The open-source backend built for Cloudflare
diff --git a/static/brand/mark.svg b/static/brand/mark.svg
index f3b5314..2c7bc4b 100644
--- a/static/brand/mark.svg
+++ b/static/brand/mark.svg
@@ -4,8 +4,8 @@
background - the dark fill it used to carry only ever matched one. -->
-
+
diff --git a/static/brand/social-preview.png b/static/brand/social-preview.png
index 0561c71..aa14c63 100644
Binary files a/static/brand/social-preview.png and b/static/brand/social-preview.png differ
diff --git a/static/brand/social-preview.svg b/static/brand/social-preview.svg
index e4f77ca..2397d82 100644
--- a/static/brand/social-preview.svg
+++ b/static/brand/social-preview.svg
@@ -1,11 +1,11 @@
-
+
-
+
- Cloudflarebase
+ Frostbase
The open-source backend built for Cloudflare
diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png
index 2b4e3e2..cc039b4 100644
Binary files a/static/favicon-16x16.png and b/static/favicon-16x16.png differ
diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png
index 5644e88..c6f23f4 100644
Binary files a/static/favicon-32x32.png and b/static/favicon-32x32.png differ
diff --git a/static/favicon.svg b/static/favicon.svg
index c41bbf9..cc40547 100644
--- a/static/favicon.svg
+++ b/static/favicon.svg
@@ -5,8 +5,8 @@
colour the browser tab, bookmark bar, or OS theme happens to be. -->
-
+
diff --git a/static/icon-192.png b/static/icon-192.png
index 551250a..fb12bb3 100644
Binary files a/static/icon-192.png and b/static/icon-192.png differ
diff --git a/static/icon-512.png b/static/icon-512.png
index 5b0a0de..d853d7b 100644
Binary files a/static/icon-512.png and b/static/icon-512.png differ