Skip to content

Commit cea04b3

Browse files
committed
feat(web): add light and dark themes with a header toggle
Every page follows the OS setting until the visitor picks a theme; the choice is saved and applied before first paint. Colour tokens are light-dark() pairs, and data-theme="dark" keeps terminals dark in both themes.
1 parent 475f4b9 commit cea04b3

10 files changed

Lines changed: 241 additions & 155 deletions

File tree

‎web/app/globals.css‎

Lines changed: 115 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
11
@import "tailwindcss";
22

3+
/* Colour tokens are light-dark() pairs: the OS setting picks one until the
4+
header toggle sets data-theme on <html>. data-theme="dark" on any element
5+
keeps that region dark in both themes (terminals, consoles). The pairs are
6+
declared on every [data-theme] scope too, because the light-dark() fallback
7+
resolves where a token is declared, not where it is used. */
38
@theme {
49
--color-white: #ffffff;
510
--color-black: #000000;
6-
--color-surface: #11131c;
7-
--color-surface-container-lowest: #0c0e17;
8-
--color-surface-container-low: #191b24;
9-
--color-surface-container: #1d1f29;
10-
--color-surface-container-high: #282933;
11-
--color-on-surface: #e1e1ef;
12-
--color-on-surface-variant: #c1c9bb;
13-
--color-on-surface-dim: #8a8f9e;
14-
--color-outline: #6a6e7d;
15-
--color-primary: #b3f6ab;
16-
--color-on-primary: #003909;
17-
--color-primary-strong: #96d68f;
18-
--color-secondary: #bac3ff;
19-
--color-signal: #8fd694;
20-
--color-console-cyan: #7dcfff;
21-
--color-link: #2a3e9a;
2211
--color-gray-50: #fafafa;
2312
--color-gray-100: #f5f5f5;
2413
--color-gray-200: #e5e5e5;
@@ -33,8 +22,24 @@
3322
--font-mono: var(--font-jetbrains), ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace;
3423
}
3524

36-
/* Page chrome (body, header, footer) follows the page: dark on the homepage. */
3725
@theme inline {
26+
--color-surface: var(--surface);
27+
--color-surface-container-lowest: var(--surface-container-lowest);
28+
--color-surface-container-low: var(--surface-container-low);
29+
--color-surface-container: var(--surface-container);
30+
--color-surface-container-high: var(--surface-container-high);
31+
--color-on-surface: var(--on-surface);
32+
--color-on-surface-variant: var(--on-surface-variant);
33+
--color-on-surface-dim: var(--on-surface-dim);
34+
--color-outline: var(--outline);
35+
--color-primary: var(--primary);
36+
--color-on-primary: var(--on-primary);
37+
--color-primary-strong: var(--primary-strong);
38+
--color-secondary: var(--secondary);
39+
--color-signal: var(--signal);
40+
--color-caution: var(--caution);
41+
--color-console-cyan: var(--console-cyan);
42+
--color-link: var(--link);
3843
--color-canvas: var(--canvas);
3944
--color-fg: var(--fg);
4045
--color-fg-muted: var(--fg-muted);
@@ -44,28 +49,51 @@
4449
}
4550

4651
:root {
47-
--canvas: #ffffff;
48-
--fg: #2e303a;
49-
--fg-muted: #515367;
50-
--fg-strong: #11131c;
51-
--rule: #dde3da;
52-
--tint: #f3f5ff;
53-
}
54-
55-
/* Header logo: dark-text mark on light pages, light-text mark on the dark homepage. */
52+
color-scheme: light dark;
53+
}
54+
55+
:root,
56+
[data-theme] {
57+
--surface: light-dark(#f6f7f4, #11131c);
58+
--surface-container-lowest: light-dark(#ffffff, #0c0e17);
59+
--surface-container-low: light-dark(#f1f3ee, #191b24);
60+
--surface-container: light-dark(#e6e8e3, #1d1f29);
61+
--surface-container-high: light-dark(#dde3da, #282933);
62+
--on-surface: light-dark(#11131c, #e1e1ef);
63+
--on-surface-variant: light-dark(#44475a, #c1c9bb);
64+
--on-surface-dim: light-dark(#5c6070, #8a8f9e);
65+
--outline: light-dark(#8a8f9e, #6a6e7d);
66+
--primary: light-dark(#1d6b2c, #b3f6ab);
67+
--on-primary: light-dark(#ffffff, #003909);
68+
--primary-strong: light-dark(#155222, #96d68f);
69+
--secondary: light-dark(#2a3e9a, #bac3ff);
70+
--signal: light-dark(#1d6b2c, #8fd694);
71+
--caution: light-dark(#7a4f00, #e0af68);
72+
--console-cyan: light-dark(#0b6a8f, #7dcfff);
73+
--link: light-dark(#2a3e9a, #bac3ff);
74+
75+
/* Page chrome (body, header, footer) */
76+
--canvas: light-dark(#ffffff, #0c0e17);
77+
--fg: light-dark(#2e303a, #e1e1ef);
78+
--fg-muted: light-dark(#515367, #a8adbb);
79+
--fg-strong: light-dark(#11131c, #e1e1ef);
80+
--rule: light-dark(#dde3da, #1d1f29);
81+
--tint: light-dark(#f3f5ff, #1d1f29);
82+
}
83+
84+
[data-theme="light"] { color-scheme: light; }
85+
[data-theme="dark"] { color-scheme: dark; }
86+
87+
/* Header logo and logo silhouettes can't use light-dark(), so they key off
88+
the same choice: an explicit data-theme, else the OS setting. */
5689
.logo-on-dark { display: none; }
57-
:root:has(.page-home) .logo-on-light { display: none; }
58-
:root:has(.page-home) .logo-on-dark { display: block; }
90+
:root[data-theme="dark"] .logo-on-light { display: none; }
91+
:root[data-theme="dark"] .logo-on-dark { display: block; }
5992

60-
:root:has(.page-home) {
61-
--canvas: #0c0e17;
62-
--fg: #e1e1ef;
63-
--fg-muted: #a8adbb;
64-
--fg-strong: #e1e1ef;
65-
--rule: #1d1f29;
66-
--tint: #1d1f29;
93+
@media (prefers-color-scheme: dark) {
94+
:root:not([data-theme="light"]) .logo-on-light { display: none; }
95+
:root:not([data-theme="light"]) .logo-on-dark { display: block; }
6796
}
68-
6997
/* In the base layer so utilities win over these element defaults. */
7098
@layer base {
7199
html {
@@ -129,7 +157,7 @@
129157
}
130158

131159
*:focus-visible {
132-
outline: 2px solid var(--color-secondary);
160+
outline: 2px solid var(--secondary);
133161
outline-offset: 2px;
134162
}
135163

@@ -152,37 +180,37 @@
152180
font-size: 0.95rem;
153181
border-radius: 0.5rem;
154182
overflow: hidden;
155-
border: 1px solid #d5d9e8;
183+
border: 1px solid var(--rule);
156184
}
157185

158186
article thead {
159-
background: #f3f5ff;
187+
background: var(--tint);
160188
}
161189

162190
article th {
163191
padding: 0.875rem 1rem;
164192
text-align: left;
165193
font-weight: 600;
166-
color: #2e303a;
167-
border-bottom: 2px solid #d5d9e8;
194+
color: var(--fg-strong);
195+
border-bottom: 2px solid var(--rule);
168196
}
169197

170198
article td {
171199
padding: 0.75rem 1rem;
172-
border-bottom: 1px solid #d5d9e8;
173-
color: #515367;
200+
border-bottom: 1px solid var(--rule);
201+
color: var(--fg-muted);
174202
}
175203

176204
article tbody tr:last-child td {
177205
border-bottom: none;
178206
}
179207

180208
article tbody tr:hover {
181-
background: #f8faf7;
209+
background: var(--tint);
182210
}
183211

184212
article td code {
185-
background: #f3f5ff;
213+
background: var(--tint);
186214
padding: 0.15em 0.4em;
187215
border-radius: 4px;
188216
font-size: 0.875em;
@@ -201,15 +229,32 @@
201229
}
202230
}
203231

204-
/* One light silhouette, so dark marks (Cursor, Codex) show on the dark strip. */
232+
/* One flat silhouette per theme, so dark marks (Cursor, Codex) still show. */
205233
.agent-logo {
206-
filter: brightness(0) invert(0.8);
234+
filter: brightness(0) invert(0.25);
207235
transition: filter 200ms ease;
208236
}
209237

210238
a:hover .agent-logo,
211239
a:focus-visible .agent-logo {
212-
filter: brightness(0) invert(0.95);
240+
filter: brightness(0) invert(0.05);
241+
}
242+
243+
.company-logo {
244+
filter: brightness(0);
245+
opacity: 0.6;
246+
}
247+
248+
:root[data-theme="dark"] .agent-logo { filter: brightness(0) invert(0.8); }
249+
:root[data-theme="dark"] a:hover .agent-logo,
250+
:root[data-theme="dark"] a:focus-visible .agent-logo { filter: brightness(0) invert(0.95); }
251+
:root[data-theme="dark"] .company-logo { filter: brightness(0) invert(1); opacity: 0.7; }
252+
253+
@media (prefers-color-scheme: dark) {
254+
:root:not([data-theme="light"]) .agent-logo { filter: brightness(0) invert(0.8); }
255+
:root:not([data-theme="light"]) a:hover .agent-logo,
256+
:root:not([data-theme="light"]) a:focus-visible .agent-logo { filter: brightness(0) invert(0.95); }
257+
:root:not([data-theme="light"]) .company-logo { filter: brightness(0) invert(1); opacity: 0.7; }
213258
}
214259

215260
@media (prefers-reduced-motion: no-preference) {
@@ -231,63 +276,30 @@ a:focus-visible .agent-logo {
231276

232277
/* ------------------------------------------------------------------
233278
Docs: "Paper & Prompt" light reading surface, "Control Plane" dark
234-
theme from the OS setting. Code always sits in a dark terminal.
279+
theme. Code always sits in a dark terminal.
235280
------------------------------------------------------------------ */
236281

237-
:root:has(.page-docs) {
238-
--d-ink: #11131c;
239-
--d-body: #2e303a;
240-
--d-muted: #515367;
241-
--d-rule: #e6e8e3;
242-
--d-rule-strong: #8a8f9e;
243-
--d-card: #ffffff;
244-
--d-subtle: #f6f7f4;
245-
--d-active: #1d6b2c;
246-
--d-active-bg: #eef8ec;
247-
--d-link: #2a3e9a;
248-
--d-focus: #2a3e9a;
249-
--d-code-inline: #11131c;
250-
--d-code-inline-bg: #f1f3ee;
251-
--d-term: #0c0e17;
252-
--d-term-border: #0c0e17;
253-
--d-target: rgba(179, 246, 171, 0.35);
254-
--d-button: #11131c;
255-
--d-button-text: #ffffff;
256-
--d-backdrop: rgba(12, 14, 23, 0.45);
257-
}
258-
259-
@media (prefers-color-scheme: dark) {
260-
:root:has(.page-docs) {
261-
color-scheme: dark;
262-
--canvas: #0c0e17;
263-
--fg: #e1e1ef;
264-
--fg-muted: #a8adbb;
265-
--fg-strong: #e1e1ef;
266-
--rule: #1d1f29;
267-
--tint: #1d1f29;
268-
--d-ink: #e1e1ef;
269-
--d-body: #d4d6e1;
270-
--d-muted: #a8adbb;
271-
--d-rule: #1d1f29;
272-
--d-rule-strong: #6a6e7d;
273-
--d-card: #11131c;
274-
--d-subtle: #11131c;
275-
--d-active: #b3f6ab;
276-
--d-active-bg: #1d1f29;
277-
--d-link: #bac3ff;
278-
--d-focus: #bac3ff;
279-
--d-code-inline: #7dcfff;
280-
--d-code-inline-bg: #1d1f29;
281-
--d-term: #191b24;
282-
--d-term-border: #282933;
283-
--d-target: rgba(179, 246, 171, 0.12);
284-
--d-button: #b3f6ab;
285-
--d-button-text: #003909;
286-
--d-backdrop: rgba(5, 6, 10, 0.7);
287-
}
288-
289-
:root:has(.page-docs) .logo-on-light { display: none; }
290-
:root:has(.page-docs) .logo-on-dark { display: block; }
282+
:root,
283+
[data-theme] {
284+
--d-ink: light-dark(#11131c, #e1e1ef);
285+
--d-body: light-dark(#2e303a, #d4d6e1);
286+
--d-muted: light-dark(#515367, #a8adbb);
287+
--d-rule: light-dark(#e6e8e3, #1d1f29);
288+
--d-rule-strong: light-dark(#8a8f9e, #6a6e7d);
289+
--d-card: light-dark(#ffffff, #11131c);
290+
--d-subtle: light-dark(#f6f7f4, #11131c);
291+
--d-active: light-dark(#1d6b2c, #b3f6ab);
292+
--d-active-bg: light-dark(#eef8ec, #1d1f29);
293+
--d-link: light-dark(#2a3e9a, #bac3ff);
294+
--d-focus: light-dark(#2a3e9a, #bac3ff);
295+
--d-code-inline: light-dark(#11131c, #7dcfff);
296+
--d-code-inline-bg: light-dark(#f1f3ee, #1d1f29);
297+
--d-term: light-dark(#0c0e17, #191b24);
298+
--d-term-border: light-dark(#0c0e17, #282933);
299+
--d-target: light-dark(rgba(179, 246, 171, 0.35), rgba(179, 246, 171, 0.12));
300+
--d-button: light-dark(#11131c, #b3f6ab);
301+
--d-button-text: light-dark(#ffffff, #003909);
302+
--d-backdrop: light-dark(rgba(12, 14, 23, 0.45), rgba(5, 6, 10, 0.7));
291303
}
292304

293305
.page-docs {

‎web/app/layout.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,16 @@ export default function RootLayout({
143143

144144
return (
145145
// Font variables must be on <html>: theme tokens resolve at :root.
146-
<html lang="en" className={`${geist.variable} ${jetbrainsMono.variable}`}>
146+
// suppressHydrationWarning: the theme script sets data-theme before React loads.
147+
<html lang="en" className={`${geist.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
148+
<head>
149+
{/* Applies a saved theme before first paint; without one, the OS setting decides. */}
150+
<script
151+
dangerouslySetInnerHTML={{
152+
__html: `try{var t=localStorage.getItem('theme');if(t==='light'||t==='dark')document.documentElement.dataset.theme=t}catch(e){}`,
153+
}}
154+
/>
155+
</head>
147156
<body className="flex flex-col min-h-screen">
148157
<Script
149158
async

0 commit comments

Comments
 (0)