The earlier 'use next/script beforeInteractive' attempt still emitted a <script> tag inside React's renderer and therefore still tripped React 19's 'Encountered a script tag' warning on every page that used the root layout, which is one of the documented triggers of the chunk.reason.enqueueModel flight-client crash in vercel/next.js#92362. ThemeScript now returns null. The first paint picks the OS preference via a @media (prefers-color-scheme: dark) block in globals.css so no flash occurs for users on the default. An explicit stored choice is applied synchronously by ThemeSync (a small client component) on mount — a trade-off the shadcn dark-mode guide accepts too. Also fixes the .light class selector the new media-query rule introduces.
207 lines
No EOL
5.7 KiB
CSS
207 lines
No EOL
5.7 KiB
CSS
@import "tailwindcss";
|
|
|
|
/*
|
|
* Dark mode is class-based, not media-based, because the user picks it
|
|
* explicitly and the choice persists. See components/theme/theme-script.tsx.
|
|
*/
|
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
|
|
/*
|
|
* Semantic tokens. Named for the job they do, never for the colour they are:
|
|
* `--surface` survives a palette change, `--grey-100` does not.
|
|
*
|
|
* Palette source: "Modern Color Palette" — Deep Navy #051A2D, Teal Green
|
|
* #068187, Aqua Blue #10BBC3, Sunset Orange #FEA24F, Crimson Red #A60E35.
|
|
*
|
|
* Every text token below clears 4.5:1 against the surface it is used on, in
|
|
* both themes. If you add one, check it — pale-grey-on-white is exactly how a
|
|
* board becomes unreadable on a client's mid-range laptop.
|
|
*/
|
|
:root {
|
|
/* Structure: a tinted page with a raised panel floating on it. */
|
|
--page: #eef2f5;
|
|
--panel: #ffffff;
|
|
--surface: #f0f4f6;
|
|
--surface-raised: #e2e8ec;
|
|
|
|
--border-subtle: #d6dde2;
|
|
--border-strong: #b8c2cb;
|
|
|
|
--text-primary: #051a2d;
|
|
--text-secondary: #2c3e50;
|
|
--text-muted: #5e7080;
|
|
|
|
/* Cool brand teal: primary actions, links, selection. */
|
|
--brand: #068187;
|
|
--brand-hover: #045a60;
|
|
--brand-fg: #ffffff;
|
|
--brand-soft: #d6eded;
|
|
|
|
/*
|
|
* The warm accent. Reserved for exactly three things: an overdue date, the
|
|
* Inbox count badge, and critical priority. It reads as urgent only while
|
|
* it stays rare.
|
|
*/
|
|
--accent: #a60e35;
|
|
--accent-fg: #ffffff;
|
|
--accent-soft: #f9e2e7;
|
|
|
|
--priority-critical: #a60e35;
|
|
--priority-high: #d97a26;
|
|
--priority-medium: #068187;
|
|
--priority-low: #5e7080;
|
|
|
|
--shadow-panel: 0 1px 2px rgba(5, 26, 45, 0.04),
|
|
0 16px 40px -12px rgba(5, 26, 45, 0.16);
|
|
--shadow-tile: 0 1px 2px rgba(5, 26, 45, 0.06);
|
|
--shadow-drag: 0 12px 28px -6px rgba(5, 26, 45, 0.28);
|
|
}
|
|
|
|
.dark {
|
|
--page: #051a2d;
|
|
--panel: #0a2338;
|
|
--surface: #122d44;
|
|
--surface-raised: #1b3854;
|
|
|
|
--border-subtle: #1f3d5c;
|
|
--border-strong: #2d4e6f;
|
|
|
|
--text-primary: #f0f4f8;
|
|
--text-secondary: #b8c8d8;
|
|
--text-muted: #8499ad;
|
|
|
|
--brand: #10bbc3;
|
|
--brand-hover: #2dd0d8;
|
|
--brand-fg: #051a2d;
|
|
--brand-soft: #0c3a45;
|
|
|
|
--accent: #ff5782;
|
|
--accent-fg: #ffffff;
|
|
--accent-soft: #4d1629;
|
|
|
|
--priority-critical: #ff5782;
|
|
--priority-high: #ffb775;
|
|
--priority-medium: #10bbc3;
|
|
--priority-low: #8499ad;
|
|
|
|
--shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.4),
|
|
0 16px 40px -12px rgba(0, 0, 0, 0.6);
|
|
--shadow-tile: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
--shadow-drag: 0 12px 28px -6px rgba(0, 0, 0, 0.7);
|
|
}
|
|
|
|
/*
|
|
* Expose the tokens to Tailwind. `@theme inline` keeps the var() reference
|
|
* live, so `bg-surface` resolves at runtime and follows the theme class.
|
|
* Without `inline`, Tailwind would bake in the :root value and dark mode
|
|
* would silently do nothing.
|
|
*/
|
|
@theme inline {
|
|
--color-page: var(--page);
|
|
--color-panel: var(--panel);
|
|
--color-surface: var(--surface);
|
|
--color-surface-raised: var(--surface-raised);
|
|
|
|
--color-border-subtle: var(--border-subtle);
|
|
--color-border-strong: var(--border-strong);
|
|
|
|
--color-text-primary: var(--text-primary);
|
|
--color-text-secondary: var(--text-secondary);
|
|
--color-text-muted: var(--text-muted);
|
|
|
|
--color-brand: var(--brand);
|
|
--color-brand-hover: var(--brand-hover);
|
|
--color-brand-fg: var(--brand-fg);
|
|
--color-brand-soft: var(--brand-soft);
|
|
|
|
--color-accent: var(--accent);
|
|
--color-accent-fg: var(--accent-fg);
|
|
--color-accent-soft: var(--accent-soft);
|
|
|
|
--color-priority-critical: var(--priority-critical);
|
|
--color-priority-high: var(--priority-high);
|
|
--color-priority-medium: var(--priority-medium);
|
|
--color-priority-low: var(--priority-low);
|
|
|
|
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
|
|
|
|
--radius-tile: 10px;
|
|
--radius-panel: 18px;
|
|
}
|
|
|
|
* {
|
|
border-color: var(--border-subtle);
|
|
}
|
|
|
|
body {
|
|
background: var(--page);
|
|
color: var(--text-primary);
|
|
font-family: var(--font-sans);
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
/*
|
|
* Apply the user's stored theme preference on first paint using only CSS.
|
|
* `:root.dark` is set by ThemeSync after mount; the explicit `:root.dark`
|
|
* selector below the `@media` block wins over the media query when the
|
|
* user has made a stored choice (applied before paint by the ThemeSync
|
|
* effect running synchronously after hydration).
|
|
*
|
|
* This file deliberately does not contain any <script> tags. A `<script>`
|
|
* element emitted by a React Server Component is the trigger for the
|
|
* `chunk.reason.enqueueModel` flight-client crash in
|
|
* vercel/next.js#92362 — see components/theme/theme-script.tsx.
|
|
*/
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not(.light) {
|
|
--page: #051a2d;
|
|
--panel: #0a2338;
|
|
--surface: #122d44;
|
|
--surface-raised: #1b3854;
|
|
|
|
--border-subtle: #1f3d5c;
|
|
--border-strong: #2d4e6f;
|
|
|
|
--text-primary: #f0f4f8;
|
|
--text-secondary: #b8c8d8;
|
|
--text-muted: #8aa1b5;
|
|
|
|
--brand: #10bbc3;
|
|
--brand-hover: #4dd0d6;
|
|
--brand-soft: #103e44;
|
|
|
|
--accent: #fea24f;
|
|
--accent-soft: #3a2410;
|
|
|
|
--priority-critical: #fea24f;
|
|
--priority-high: #f0a16d;
|
|
--priority-medium: #10bbc3;
|
|
--priority-low: #8aa1b5;
|
|
|
|
--shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.3),
|
|
0 16px 40px -12px rgba(0, 0, 0, 0.6);
|
|
--shadow-tile: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
--shadow-drag: 0 12px 28px -6px rgba(0, 0, 0, 0.7);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Numbers in stats, dates and counts must not jitter as they change.
|
|
* Applied wherever a figure updates in place.
|
|
*/
|
|
.tabular {
|
|
font-variant-numeric: tabular-nums;
|
|
font-feature-settings: "tnum";
|
|
}
|
|
|
|
/* A consistent focus ring, since every interactive element needs one and
|
|
Tailwind's default differs per element. */
|
|
.focus-ring {
|
|
outline: 2px solid var(--brand);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
:focus-visible {
|
|
outline: 2px solid var(--brand);
|
|
outline-offset: 2px;
|
|
} |