From 2f5eb0e5b99416a4a45887fbc1939a6b58d0c79c Mon Sep 17 00:00:00 2001 From: khatibqa Date: Sun, 27 Sep 2026 23:19:23 -0400 Subject: [PATCH 1/8] feat(theme): add Midnight Blueprint dark mode Adds a real dark-mode token pass on top of the light "paper cream / navy ink / blue accent" theme from the updated-ui redesign, following prefers-color-scheme automatically. Re-tunes every existing hue for a dark navy surface rather than introducing a new palette: - Surface: #070c1a / #0b1226 / #101a35 (was cream/paper) - Ink: the light theme's own paper cream (#f2e9d6), reused as the dark theme's text color instead of a separate near-white - Accent: #3d8fe0 (brightened from the light-mode blue) - Identity: teal #3fae9c, terracotta #e08a54, violet #a08cf0 Two helper custom properties (--paper-rgb, --ink-rgb) let the one-off rgba() literals scattered through app-theme.css (glass nav/sidebar backgrounds, hover washes, texture patterns) flip with the theme instead of staying baked to the light values -- box-shadows are deliberately left alone since a shadow should stay dark-hued in both themes, not invert to a light glow. The logo's recolor filter gets its own dark-mode chain since it's driven by a filter on a black source image, not --ink. Also mirrors the token values into globals.css's plain :root block, which app-theme.css doesn't otherwise shadow for consumers outside .app-page/.auth-page. --workspace-* tokens are untouched -- the embedded IDE surface is already permanently dark by design. Every dark rule is duplicated under both `@media (prefers-color-scheme: dark)` and `[data-theme="dark"]`. Only the media query is live today (no theme-toggle UI exists yet); the data-theme selectors are prepared for whenever one is added, at zero cost if it never is. Verified by inlining the real CSS into a throwaway static harness (not committed) and toggling data-theme -- confirmed to render correctly. Could not verify against the live dev server: middleware fails to build for every route in this environment on a pre-existing, unrelated @azure/core-tracing/@azure/core-lro export mismatch (also hit during the gen2 test run on the control-updates branch), not touched here. Co-Authored-By: Claude Sonnet 5 --- apps/web/app/app-theme.css | 122 ++++++++++++++++++++++++++++++++----- apps/web/app/globals.css | 48 +++++++++++++++ 2 files changed, 155 insertions(+), 15 deletions(-) diff --git a/apps/web/app/app-theme.css b/apps/web/app/app-theme.css index db8534f03..97c009223 100644 --- a/apps/web/app/app-theme.css +++ b/apps/web/app/app-theme.css @@ -10,6 +10,12 @@ * vibrant blue accent, with teal / terracotta / violet for agent identities. * Legacy token names (`--codev-gold-*`, `--codev-black-*`) are kept as slots * so existing rules did not all need renaming. + * + * Dark mode ("Midnight Blueprint"): same identity hues, re-tuned for a dark + * navy surface, with the light theme's paper cream reused as the dark + * theme's ink color instead of a separate near-white. Applies automatically + * from `prefers-color-scheme`; the `[data-theme]` selectors are prepared for + * a manual toggle, though no toggle UI exists yet. */ :root { --codev-black-950: #f2e9d6; @@ -35,6 +41,52 @@ --codev-sage-500: var(--codev-stone-500); --codev-orange-500: var(--codev-terracotta); --codev-orange-400: var(--codev-terracotta); + + /* RGB channel triplets (no alpha) so the one-off rgba() literals below can + flip with the theme instead of staying baked to the light palette. + --paper-rgb is the translucent "glass" panel base (nav, sidebar, cards); + --ink-rgb is the low-opacity ink wash used for hover states and texture + patterns, never for box-shadows -- shadows stay dark in both themes. */ + --paper-rgb: 255, 253, 247; + --ink-rgb: 14, 47, 126; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --codev-black-950: #070c1a; + --codev-black-900: #0b1226; + --codev-black-800: #101a35; + --codev-black-700: #172244; + --codev-beige-100: #f2e9d6; + --codev-beige-200: #c9ba95; + --codev-stone-300: #96918a; + --codev-stone-500: #a08cf0; + --codev-gold-500: #3d8fe0; + --codev-gold-400: #5aa6ec; + --codev-on-gold: #070c1a; + --codev-terracotta: #e08a54; + --codev-teal: #3fae9c; + --paper-rgb: 16, 26, 53; + --ink-rgb: 242, 233, 214; + } +} + +:root[data-theme="dark"] { + --codev-black-950: #070c1a; + --codev-black-900: #0b1226; + --codev-black-800: #101a35; + --codev-black-700: #172244; + --codev-beige-100: #f2e9d6; + --codev-beige-200: #c9ba95; + --codev-stone-300: #96918a; + --codev-stone-500: #a08cf0; + --codev-gold-500: #3d8fe0; + --codev-gold-400: #5aa6ec; + --codev-on-gold: #070c1a; + --codev-terracotta: #e08a54; + --codev-teal: #3fae9c; + --paper-rgb: 16, 26, 53; + --ink-rgb: 242, 233, 214; } .app-page, @@ -79,9 +131,31 @@ font-feature-settings: "ss01" on; } +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .app-page, + :root:not([data-theme="light"]) .auth-page { + --muted-2: #726d64; + --line: rgba(var(--ink-rgb), 0.14); + --line-strong: rgba(var(--ink-rgb), 0.24); + --gold-soft: rgba(61, 143, 224, 0.17); + --teal-soft: rgba(63, 174, 156, 0.16); + color-scheme: dark; + } +} + +[data-theme="dark"] .app-page, +[data-theme="dark"] .auth-page { + --muted-2: #726d64; + --line: rgba(var(--ink-rgb), 0.14); + --line-strong: rgba(var(--ink-rgb), 0.24); + --gold-soft: rgba(61, 143, 224, 0.17); + --teal-soft: rgba(63, 174, 156, 0.16); + color-scheme: dark; +} + .app-page .app-nav { border-color: var(--line); - background: rgba(255, 253, 247, 0.78); + background: rgba(var(--paper-rgb), 0.78); color: var(--ink); backdrop-filter: blur(14px) saturate(160%); } @@ -97,6 +171,24 @@ hue-rotate(210deg) brightness(0.95) contrast(1.05); } +/* The logo asset is recolored from a black source via filter rather than a + mask, so dark mode needs its own chain rather than inheriting --ink. This + one is a close approximation of the cream ink and may want a visual + tweak once seen against the real asset. */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .app-page .brand-image, + :root:not([data-theme="light"]) .auth-page .brand-image { + filter: brightness(0) saturate(100%) invert(92%) sepia(24%) saturate(320%) + hue-rotate(345deg) brightness(1.02) contrast(0.94); + } +} + +[data-theme="dark"] .app-page .brand-image, +[data-theme="dark"] .auth-page .brand-image { + filter: brightness(0) saturate(100%) invert(92%) sepia(24%) saturate(320%) + hue-rotate(345deg) brightness(1.02) contrast(0.94); +} + .auth-page { position: relative; overflow: hidden; @@ -124,8 +216,8 @@ inset: 0; background-image: repeating-linear-gradient( 115deg, - rgba(14, 47, 126, 0.035) 0, - rgba(14, 47, 126, 0.035) 1px, + rgba(var(--ink-rgb), 0.035) 0, + rgba(var(--ink-rgb), 0.035) 1px, transparent 1px, transparent 7px ); @@ -137,7 +229,7 @@ position: relative; z-index: 1; border-color: var(--line); - background: rgba(255, 253, 247, 0.78); + background: rgba(var(--paper-rgb), 0.78); color: var(--ink); backdrop-filter: blur(14px) saturate(160%); } @@ -158,8 +250,8 @@ border-color: var(--line-strong); background: linear-gradient( 145deg, - rgba(255, 253, 247, 0.94), - rgba(255, 253, 247, 0.82) + rgba(var(--paper-rgb), 0.94), + rgba(var(--paper-rgb), 0.82) ); box-shadow: 0 28px 90px rgba(14, 47, 126, 0.12); } @@ -335,8 +427,8 @@ z-index: 0; background-image: repeating-linear-gradient( 0deg, - rgba(14, 47, 126, 0.04) 0, - rgba(14, 47, 126, 0.04) 1px, + rgba(var(--ink-rgb), 0.04) 0, + rgba(var(--ink-rgb), 0.04) 1px, transparent 1px, transparent 14px ); @@ -358,7 +450,7 @@ top: 0; padding: 20px 12px; border-right: 1px solid var(--line); - background: rgba(255, 253, 247, 0.82); + background: rgba(var(--paper-rgb), 0.82); backdrop-filter: blur(14px) saturate(160%); overflow-y: auto; } @@ -390,7 +482,7 @@ } .app-sidebar-link:hover { - background: rgba(14, 47, 126, 0.06); + background: rgba(var(--ink-rgb), 0.06); color: var(--ink); } @@ -426,7 +518,7 @@ } .app-sidebar-footer .profile-menu-trigger:hover { - background: rgba(14, 47, 126, 0.06); + background: rgba(var(--ink-rgb), 0.06); color: var(--ink); } @@ -463,8 +555,8 @@ linear-gradient(118deg, rgba(27, 99, 179, 0.1), transparent 38%), linear-gradient( 145deg, - rgba(255, 253, 247, 0.94), - rgba(255, 253, 247, 0.82) + rgba(var(--paper-rgb), 0.94), + rgba(var(--paper-rgb), 0.82) ); box-shadow: 0 20px 48px rgba(14, 47, 126, 0.1); } @@ -524,7 +616,7 @@ .stat-card { min-width: 145px; border-color: var(--line); - background: rgba(255, 253, 247, 0.72); + background: rgba(var(--paper-rgb), 0.72); } .stat-value-active { @@ -535,7 +627,7 @@ padding: clamp(20px, 3vw, 32px); border: 1px solid var(--line); border-radius: 18px; - background: rgba(255, 253, 247, 0.72); + background: rgba(var(--paper-rgb), 0.72); box-shadow: 0 18px 44px rgba(14, 47, 126, 0.08); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 9f631c8d8..95e28fd7c 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -25,6 +25,54 @@ color-scheme: light; } +/* Dark-mode fallback for the semantic tokens above -- app-theme.css (loaded + after this file) re-declares the same tokens on `.app-page`/`.auth-page` + with a matching dark override and always wins there, so this block only + matters for the rare consumer of these tokens outside those two classes. + Kept in lockstep with app-theme.css's dark values; --workspace-* stays + untouched since the embedded IDE surface is intentionally dark already. */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --ink: #f2e9d6; + --muted: #96918a; + --muted-2: #726d64; + --line: rgba(242, 233, 214, 0.14); + --line-strong: rgba(242, 233, 214, 0.24); + --surface: #070c1a; + --surface-2: #0b1226; + --surface-3: #101a35; + --navy: #f2e9d6; + --gold: #3d8fe0; + --gold-bright: #5aa6ec; + --gold-soft: rgba(61, 143, 224, 0.17); + --teal: #3fae9c; + --teal-soft: rgba(63, 174, 156, 0.16); + --orange: #e08a54; + --violet: #a08cf0; + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + --ink: #f2e9d6; + --muted: #96918a; + --muted-2: #726d64; + --line: rgba(242, 233, 214, 0.14); + --line-strong: rgba(242, 233, 214, 0.24); + --surface: #070c1a; + --surface-2: #0b1226; + --surface-3: #101a35; + --navy: #f2e9d6; + --gold: #3d8fe0; + --gold-bright: #5aa6ec; + --gold-soft: rgba(61, 143, 224, 0.17); + --teal: #3fae9c; + --teal-soft: rgba(63, 174, 156, 0.16); + --orange: #e08a54; + --violet: #a08cf0; + color-scheme: dark; +} + /* Shared product theme. App shell is light cream/navy; workspace IDE stays dark. */ .app-page, From b398c3c099fde81cedfbea26edeaac179c823407 Mon Sep 17 00:00:00 2001 From: khatibqa Date: Mon, 28 Sep 2026 00:06:13 -0400 Subject: [PATCH 2/8] feat(theme): unify dark mode across the app and add a visible toggle Two real bugs, both from the previous commit's dark tokens only reaching app-theme.css's own selectors: 1. globals.css, mission-control.css, team-chat.css and settings/orca-theme.css each hardcode their own copies of the current light-palette hex/rgba values instead of referencing the shared tokens -- over 150 occurrences. Toggling the tokens did nothing for any of them, which is why gen2 and sign-in (styled mostly through app-theme.css directly) went dark while the dashboard, rooms, admin, and mission control did not. Fixed every occurrence that is a bare, unwrapped literal (verified by excluding anything already inside `var(--token, fallback)`, which was already correct and needed no change). Two new RGB-triplet helpers, --accent-rgb and --teal-rgb, join the existing --paper-rgb/--ink-rgb so the many one-off rgba() badges/borders/washes flip too. Modal-backdrop scrims are deliberately left as a literal dark navy wash in both themes -- a dimmer should stay dark, not invert to a light glow when it happens to reuse the ink color. Also fixed `html`/`body { background: #f2e9d6 }` in globals.css, which applied globally and outside any dark-aware token -- almost certainly the dominant reason pages still looked light at their edges regardless of what their own content did. orca-theme.css's Tailwind v4 `@theme` block can't itself be conditional, so its --color-* overrides are written as ordinary selector rules after it, following the same pattern as everywhere else. Left untouched, and flagged rather than guessed at: a second, older layer of literals from at least one earlier redesign (rgba(237,238,240,*), rgba(23,59,45,*), #121417, #1a1d21, plus a few standalone colors like #46c771/#e3c98a/#86590b) that isn't even rendering today's intended light theme correctly -- a pre-existing bug independent of dark mode, not safe to fix by blind substitution. 2. There was no way to switch modes -- dark mode only ever followed the OS preference. Added `ThemeToggle`, a standalone icon button (not a menu item, so it doesn't require opening anything to find) next to the profile menu in both AppChrome layouts and on the sign-in page. Clicking it stamps an explicit `data-theme` on and remembers the choice in localStorage; a blocking inline script in the root layout's re-stamps that choice before first paint so a returning dark-mode visitor never sees a light flash. Until a visitor ever clicks it, the theme still just follows `prefers-color-scheme` with no JS involved. Verified with the same throwaway static-harness technique as the previous commit (real CSS inlined, data-theme flipped directly) since the dev server still can't build on the pre-existing, unrelated @azure/core-tracing/@azure/core-lro dependency conflict -- confirmed the dashboard's stat-card/workspace-browser/workspace-card, previously stuck light, now render correctly in both themes with no regression to the light-mode screenshot from the prior commit. Co-Authored-By: Claude Sonnet 5 --- apps/web/app/app-theme.css | 59 +++++++++++++++ apps/web/app/globals.css | 58 +++++++-------- apps/web/app/layout.tsx | 8 +++ apps/web/app/mission-control.css | 50 ++++++------- apps/web/app/settings/orca-theme.css | 60 ++++++++++++++++ apps/web/app/sign-in/page.tsx | 6 +- apps/web/app/team-chat.css | 50 ++++++------- apps/web/components/shell/app-chrome.tsx | 3 + apps/web/components/shell/theme-toggle.tsx | 83 ++++++++++++++++++++++ 9 files changed, 298 insertions(+), 79 deletions(-) create mode 100644 apps/web/components/shell/theme-toggle.tsx diff --git a/apps/web/app/app-theme.css b/apps/web/app/app-theme.css index 97c009223..92f197c39 100644 --- a/apps/web/app/app-theme.css +++ b/apps/web/app/app-theme.css @@ -49,6 +49,14 @@ patterns, never for box-shadows -- shadows stay dark in both themes. */ --paper-rgb: 255, 253, 247; --ink-rgb: 14, 47, 126; + + /* RGB triplets for the accent and teal identity colors, so the many + one-off rgba(27, 99, 179, X) / rgba(42, 109, 98, X) badges, borders and + glows scattered across globals.css, mission-control.css and + team-chat.css can flip with the theme too, instead of only the + property that happens to read --gold/--teal directly. */ + --accent-rgb: 27, 99, 179; + --teal-rgb: 42, 109, 98; } @media (prefers-color-scheme: dark) { @@ -68,6 +76,8 @@ --codev-teal: #3fae9c; --paper-rgb: 16, 26, 53; --ink-rgb: 242, 233, 214; + --accent-rgb: 61, 143, 224; + --teal-rgb: 63, 174, 156; } } @@ -87,6 +97,8 @@ --codev-teal: #3fae9c; --paper-rgb: 16, 26, 53; --ink-rgb: 242, 233, 214; + --accent-rgb: 61, 143, 224; + --teal-rgb: 63, 174, 156; } .app-page, @@ -391,6 +403,53 @@ box-shadow: 0 18px 50px rgba(14, 47, 126, 0.14); } +/* Theme toggle: a standalone icon button next to the profile menu, not a + menu item -- it should be visible without opening anything. Sized and + shaped like .profile-menu-trigger so the two sit together as one group. */ +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border: 1px solid transparent; + border-radius: 999px; + background: none; + color: var(--muted); + cursor: pointer; +} + +.theme-toggle:hover { + border-color: var(--line-strong); + background: var(--surface-3); + color: var(--ink); +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--gold); + outline-offset: 1px; +} + +.app-sidebar-footer .theme-toggle { + width: 100%; + height: auto; + padding: 8px 6px; + border-radius: 8px; + justify-content: flex-start; + gap: 10px; +} + +.app-sidebar-footer .theme-toggle::after { + content: "Theme"; + font-size: 13px; +} + +.auth-nav-actions { + display: flex; + align-items: center; + gap: 14px; +} + .app-page .profile-menu-heading strong, .app-page .profile-menu-link:hover, .app-page .profile-menu-action:hover { diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 95e28fd7c..a663414bd 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -775,8 +775,8 @@ .workspace-chat-bubble-error { align-self: flex-start; - border: 1px solid rgba(27, 99, 179, 0.35); - background: rgba(27, 99, 179, 0.08); + border: 1px solid rgba(var(--accent-rgb), 0.35); + background: rgba(var(--accent-rgb), 0.08); color: var(--workspace-ink); } @@ -883,7 +883,7 @@ background: radial-gradient( circle at 82% 0, - rgba(27, 99, 179, 0.15), + rgba(var(--accent-rgb), 0.15), transparent 25rem ), var(--surface); @@ -927,7 +927,7 @@ } .auth-card::before { - border-color: rgba(27, 99, 179, 0.18); + border-color: rgba(var(--accent-rgb), 0.18); } .auth-card > p:not(.eyebrow) { @@ -943,8 +943,8 @@ .setup-panel, .inline-alert { - border-color: rgba(27, 99, 179, 0.24); - background: rgba(27, 99, 179, 0.08); + border-color: rgba(var(--accent-rgb), 0.24); + background: rgba(var(--accent-rgb), 0.08); } .setup-panel p { @@ -993,7 +993,7 @@ select, } .workspace-card:hover { - border-color: rgba(27, 99, 179, 0.42); + border-color: rgba(var(--accent-rgb), 0.42); background: var(--surface-2); } @@ -1634,14 +1634,14 @@ select, } html { - background: #f2e9d6; + background: var(--surface); scroll-behavior: smooth; } body { min-width: 320px; margin: 0; - background: #f2e9d6; + background: var(--surface); color: var(--ink); font-family: var(--font-geist-sans), Arial, sans-serif; text-rendering: optimizeLegibility; @@ -1674,7 +1674,7 @@ button { } ::selection { - background: rgba(27, 99, 179, 0.24); + background: rgba(var(--accent-rgb), 0.24); } /* Authenticated product surfaces */ @@ -1685,7 +1685,7 @@ button { background: radial-gradient( circle at 76% 0, - rgba(27, 99, 179, 0.07), + rgba(var(--accent-rgb), 0.07), transparent 28rem ), var(--surface); @@ -1909,7 +1909,7 @@ button { border: 1px solid var(--gold); border-radius: 999px; background: var(--gold); - color: #f7f3e8; + color: var(--codev-on-gold); font-weight: 700; text-decoration: none; cursor: pointer; @@ -1988,9 +1988,9 @@ button { .inline-alert { margin-top: 26px; padding: 16px; - border: 1px solid rgba(27, 99, 179, 0.22); + border: 1px solid rgba(var(--accent-rgb), 0.22); border-radius: 8px; - background: rgba(27, 99, 179, 0.07); + background: rgba(var(--accent-rgb), 0.07); } .setup-panel p { @@ -2018,7 +2018,7 @@ button { height: 7px; border-radius: 50%; background: var(--teal); - box-shadow: 0 0 9px rgba(27, 99, 179, 0.55); + box-shadow: 0 0 9px rgba(var(--accent-rgb), 0.55); } .dot-muted { @@ -2069,7 +2069,7 @@ button { } .secondary-button:hover { - border-color: rgba(27, 99, 179, 0.35); + border-color: rgba(var(--accent-rgb), 0.35); color: var(--ink); } @@ -2502,7 +2502,7 @@ button:disabled { } .workspace-card:hover { - border-color: rgba(27, 99, 179, 0.28); + border-color: rgba(var(--accent-rgb), 0.28); background: var(--surface-2); } @@ -2570,7 +2570,7 @@ button:disabled { padding: 17px; border: 1px solid var(--line); border-radius: 8px; - background: rgba(27, 99, 179, 0.035); + background: rgba(var(--accent-rgb), 0.035); } .security-callout strong { @@ -2596,7 +2596,7 @@ button:disabled { padding: 10px; align-items: center; gap: 12px; - border: 1px solid rgba(27, 99, 179, 0.18); + border: 1px solid rgba(var(--accent-rgb), 0.18); border-radius: 7px; background: var(--teal-soft); } @@ -2820,7 +2820,7 @@ button:disabled { border: 1px solid var(--gold); border-radius: 999px; background: var(--gold); - color: #f7f3e8; + color: var(--codev-on-gold); font-size: 13px; font-weight: 670; transition: @@ -2830,7 +2830,7 @@ button:disabled { .primary-button:hover { transform: translateY(-2px); - box-shadow: 0 10px 30px rgba(27, 99, 179, 0.22); + box-shadow: 0 10px 30px rgba(var(--accent-rgb), 0.22); } .hero-note { @@ -3966,9 +3966,9 @@ footer > span { font-weight: 650; text-transform: uppercase; letter-spacing: 0.05em; - background: var(--gold-soft, rgba(27, 99, 179, 0.12)); + background: var(--gold-soft, rgba(var(--accent-rgb), 0.12)); color: var(--gold, #1b63b3); - border: 1px solid rgba(27, 99, 179, 0.28); + border: 1px solid rgba(var(--accent-rgb), 0.28); margin-bottom: 10px; } @@ -4004,8 +4004,8 @@ footer > span { gap: 4px; padding: 12px 18px; border-radius: 12px; - background: rgba(255, 253, 247, 0.72); - border: 1px solid rgba(14, 47, 126, 0.16); + background: rgba(var(--paper-rgb), 0.72); + border: 1px solid rgba(var(--ink-rgb), 0.16); min-width: 130px; } @@ -4031,9 +4031,9 @@ footer > span { border-radius: 999px; font-size: 13.5px; font-weight: 650; - color: #f7f3e8; + color: var(--codev-on-gold); background: var(--gold, #1b63b3); - box-shadow: 0 4px 12px rgba(27, 99, 179, 0.22); + box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.22); transition: transform 0.15s ease, background-color 0.15s ease; @@ -4042,8 +4042,8 @@ footer > span { .resume-primary-button:hover { transform: translateY(-1px); - background: #2a7fd4; - color: #f7f3e8; + background: var(--gold-bright); + color: var(--codev-on-gold); } .home-section-header { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index e0b929145..fb9c27963 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,6 +8,7 @@ import "./app-theme.css"; import "./team-chat.css"; import { VisitTracker } from "@/components/landing/visit-tracker"; +import { THEME_INIT_SCRIPT } from "@/components/shell/theme-toggle"; const sans = Inter_Tight({ variable: "--font-geist-sans", @@ -54,6 +55,13 @@ export default function RootLayout({ }>) { return ( + + {/* Stamps an explicit stored theme choice before first paint, so a + returning dark-mode visitor never sees a light flash. Nothing to + do when no choice is stored -- the CSS `prefers-color-scheme` + query already handles that case without JS. */} +