/* ============================================================================= Shared design tokens — the single source of truth for the whole family. Imported by every app's globals.css right after `@import "tailwindcss"`: @import "../../common/styles/tokens.css"; A reader theme is TWO independent choices (plans/brand-and-themes.md): • `html[data-base="light|dark"]` selects the BASE — the ground, text, lines, status and chart colours. "System" is not a block: the pre-paint ThemeScript resolves it to light or dark before first paint. • `html[data-accent="|custom"]` selects the ACCENT — `--brand`. Each site server-renders its own, and a reader does not pick one. `html.dark` is set iff the resolved base is dark. The `@custom-variant dark` below makes Tailwind's `dark:` utilities follow that class (not the OS media query). `:root` always matches , so the light block below is also the fallback for a page no script has touched (no-JS, the editor before its script runs). The dark block is `html[data-base=…]` (0,1,1) and wins over it; the accent rules come AFTER both at the same specificity, so a `data-accent` wins `--brand` on every base. TOKEN NAMING: • The shadcn "new-york" contract owns the unprefixed names: --primary, --secondary, --accent (a NEUTRAL subtle-hover bg), --muted, --card, --popover, --destructive, --border, --input, --ring, --radius, --chart-*. Each has a *-foreground partner where shadcn expects one. --primary is NEUTRAL on every base (ink on the ground), never the accent. • The ACCENT lives under `--brand*` (--brand, --brand-strong, --brand-soft, --brand-ink, and --brand-mark, the header mark's on-dark value) so it never collides with shadcn's neutral `--accent`. • `--swatch-` is each named accent's value ON THIS BASE (lib/brand.ts ACCENTS — themeTokens.test.ts keeps the two equal), so a component can name an accent's colour on the ground in force. • `--base-light` and `--base-dark` are 1 on their own base and 0 on the other. They let a colour that differs per base but is known only to a component be ONE CSS value: each channel a calc() over the two (lib/siteColor.ts perBaseColor — a custom-hex site's card on the homepage and the hub, fitted to each ground). ========================================================================== */ @import "tw-animate-css"; @custom-variant dark (&:where(.dark, .dark *)); @theme inline { /* shadcn surface + text contract */ --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-destructive-soft: var(--destructive-soft); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); /* Semantic status — success / warning / info, each with -foreground + -soft. Soft = a low-alpha tint of the hue for badge/alert fills (like --brand-soft); the base color is tuned to read as TEXT on that soft fill on each base. */ --color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-success-soft: var(--success-soft); --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); --color-warning-soft: var(--warning-soft); --color-info: var(--info); --color-info-foreground: var(--info-foreground); --color-info-soft: var(--info-soft); /* The accent (html[data-accent]) — distinct from shadcn's neutral --accent */ --color-brand: var(--brand); --color-brand-strong: var(--brand-strong); --color-brand-soft: var(--brand-soft); --color-brand-ink: var(--brand-ink); /* Extra surfaces/lines. Every base sets them, so components can use `bg-surface`, `border-border-strong`, `text-faint`, `text-brand-ink` etc. and stay theme-aware. */ --color-surface: var(--surface); --color-border-strong: var(--border-strong); --color-faint: var(--faint); /* Charts */ --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); --color-chart-6: var(--chart-6); /* Type system */ --font-display: var(--font-display); --font-sans: var(--font-sans); --font-mono: var(--font-mono); /* Radius scale derived from --radius (declared once on :root below) */ --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); } /* --------------------------------------------------------------------------- Type and radius — one voice on every base. The faces come from common/styles/fonts.ts (next/font sets --font-display/-sans/-mono on ): Archivo (display, with its `wdth` axis), IBM Plex Sans (body), IBM Plex Mono (data). The display face runs slightly wide; the wordmark sets its own, wider stretch. --------------------------------------------------------------------------- */ :root { --radius: 0.375rem; /* The header mark's lit line: the accent's ON-DARK value (the mark sits on an ink tile on every base). Signal until a data-accent rule below says otherwise — the editor renders no data-accent at all. */ --brand-mark: #5fa8a0; } .font-display { font-stretch: 112%; } /* Fractional glyph advances. IBM Plex Sans, as Google Fonts serves it, has no TrueType instructions but a `gasp` table that asks for grid-fitting at every size, so FreeType hints it on Linux and Chromium rounds each advance to a whole CSS pixel: at 14px the `g` of "Light" or "Signal" is 9px against its 7.41px design width, at every device pixel ratio. At 1x and 1.25x the hinted glyph fills that advance; from 1.5x up (150 % and 175 % desktop scaling too) it is drawn finer than it is spaced, and the words read "Lig ht" and "Sig nal". geometricPrecision turns hinting off and lays text out at the design widths, so it is scoped to 1.5dppx and up, and 1x screens keep the hinting that makes 11–14px text crisp. It changes nothing where text is not hinted (macOS, iOS, Android's subpixel layout). Form controls are named too: the UA sheet resets their text-rendering to auto, so a button's "Results" would still read "Res ults". */ @media (min-resolution: 1.5dppx) { html, button, input, select, textarea { text-rendering: geometricPrecision; } } /* --------------------------------------------------------------------------- LIGHT — bone ground, graphite ink. The former Archilyzer light face, with a neutral primary and the accent from data-accent. Also the `:root` fallback. --------------------------------------------------------------------------- */ :root, html[data-base="light"] { color-scheme: light; --background: #f3f6f7; --surface: #e8edef; --foreground: #161c21; --card: #ffffff; --card-foreground: #161c21; --popover: #ffffff; --popover-foreground: #161c21; --primary: #202a31; --primary-foreground: #f3f6f7; --secondary: #e2e8eb; --secondary-foreground: #202a31; --muted: #e8edef; --muted-foreground: #55646e; --accent: #dde5e8; --accent-foreground: #161c21; --destructive: #a8412d; --destructive-foreground: #ffffff; --destructive-soft: rgba(168, 65, 45, 0.12); --border: #d2dade; --border-strong: #aeb9c0; --input: #c4ced4; --ring: var(--brand); --faint: #78868f; --panel: rgba(255, 255, 255, 0.78); --panel-2: rgba(232, 237, 239, 0.78); /* Each deep enough to read at 4.5:1 on its OWN soft fill over the ground and the card, as the @theme note promises (themeTokens.test.ts): success 4.74 / 5.12, warning 4.75 / 5.14, info 5.20 / 5.62. */ --success: #1c7046; --success-foreground: #ffffff; --success-soft: rgba(28, 112, 70, 0.12); --warning: #825809; --warning-foreground: #ffffff; --warning-soft: rgba(130, 88, 9, 0.14); --info: #1c5f96; --info-foreground: #ffffff; --info-soft: rgba(28, 95, 150, 0.12); /* Each named accent's on-light value (lib/brand.ts ACCENTS.onLight). */ --swatch-signal: #2e7b73; --swatch-brass: #95661a; --swatch-vermilion: #b3431f; --swatch-violet: #6a4bc4; --swatch-sakura: #a83a6a; --swatch-blue: #2d5fb8; --swatch-green: #3f7a2c; /* A custom-hex site's value fitted to this ground (lib/accent.ts customAccentVars, inline on ); Signal if it is missing. */ --swatch-custom: var(--accent-custom-light, var(--swatch-signal)); /* Which base this is, as numbers (the header's note; lib/siteColor.ts). */ --base-light: 1; --base-dark: 0; --brand: var(--swatch-signal); --brand-strong: color-mix(in oklab, var(--brand) 78%, black); --brand-soft: color-mix(in srgb, var(--brand) 12%, transparent); --brand-ink: #ffffff; /* Recording state: "this no longer exists on its platform". Worn only by recordings re-checked and found absent — "kept" is deliberately achromatic, since still being there is the unremarkable case. */ --state-gone: #a8412d; --state-gone-soft: rgba(168, 65, 45, 0.12); /* Charts do not follow the accent: a fixed categorical order per base, in one hue order everywhere (blue, green, violet, amber, magenta). A site's SLOT is its accent's hue family (lib/siteColor.ts siteChartColors), the values stay these. Validated with the dataviz skill's validate_palette.js on this base's --chart-surface: adjacent pairs pass every check (lightness band, chroma, CVD ΔE ≥ 8, normal-vision ΔE ≥ 15, ≥ 3:1). All pairs pass too, with the non-adjacent green ↔ amber in the CVD 6–8 floor band (6.2): legal only with a secondary encoding, a legend or direct labels. chart-3 is a violet, well apart from --state-gone (ΔE 24.3 normal, 21.7 protan): it once WAS the gone hex, so the third archive in a chart read as "gone". chart-1 is a blue, not Signal (ΔE 17.3 from it). chart-6 (release 11) is a rust, Vermilion's family: the sixth site's colour. The only hue family that clears every pair with chart-1..5 on both bases is the red-browns (OKLCH h ≈ 15–60); this one's worst pair with them is CVD ΔE 12.5 deutan and normal 16.6, 8.1:1 on the chart surface and 7.4:1 on the ground. It sits near --state-gone (ΔE 8.4 normal), which is only ever labelled text, never a chart mark. */ --chart-1: #3a7de0; --chart-2: #2f8a57; --chart-3: #5e3aa8; --chart-4: #a8741a; --chart-5: #c24a8a; --chart-6: #823c10; --chart-surface: #ffffff; --chart-grid: rgba(22, 28, 33, 0.09); --chart-axis: #55646e; /* OTHER: the homepage growth chart's band for the sites it groups (homepage/app/lib/growthGaps.ts), a near-neutral slate at the foot of the palette's lightness band (OKLCH L 0.43, C 0.03). 7.36:1 on the ground, 7.99:1 on the chart surface. Against each slot it can sit on (the dataviz validator, normal / worst CVD): blue 21.9 / 21.4, green 17.4 / 15.2, violet 16.2 / 13.4, amber 22.1 / 18.2, magenta 24.5 / 9.1, rust 14.1 / 10.5 — every CVD pair clears 8; rust is the one normal pair under 15. */ --chart-other: #3e545c; --chart-tooltip-bg: #ffffff; } /* --------------------------------------------------------------------------- DARK — warm ink. The former archive "ink" face (the homepage's archive room), with a neutral primary, and the --destructive-soft and --state-gone(-soft) it never declared. --------------------------------------------------------------------------- */ html[data-base="dark"] { color-scheme: dark; --background: #0c0a08; --surface: #141009; --foreground: #efe7d8; --card: #16110a; --card-foreground: #efe7d8; --popover: #1b150d; --popover-foreground: #efe7d8; --primary: #efe7d8; --primary-foreground: #0c0a08; --secondary: #241d12; --secondary-foreground: #efe7d8; --muted: #1c1710; --muted-foreground: #a39a86; --accent: #241d12; --accent-foreground: #efe7d8; --destructive: #f0857a; --destructive-foreground: #1a1305; --destructive-soft: rgba(240, 133, 122, 0.16); --border: rgba(233, 220, 197, 0.1); --border-strong: rgba(233, 220, 197, 0.18); --input: rgba(233, 220, 197, 0.14); --ring: var(--brand); --faint: #6f6757; --panel: rgba(28, 23, 15, 0.55); --panel-2: rgba(40, 33, 21, 0.5); --success: #6cc777; --success-foreground: #0c1a0d; --success-soft: rgba(108, 199, 119, 0.16); --warning: #f0a93c; --warning-foreground: #1a1305; --warning-soft: rgba(240, 169, 60, 0.16); --info: #6ea8ff; --info-foreground: #04122e; --info-soft: rgba(110, 168, 255, 0.16); /* Each named accent's on-dark value (lib/brand.ts ACCENTS.onDark). */ --swatch-signal: #5fa8a0; --swatch-brass: #e3b15c; --swatch-vermilion: #ec7a52; --swatch-violet: #b49cf2; --swatch-sakura: #ee8fb5; --swatch-blue: #74a9f2; --swatch-green: #7cc46a; --swatch-custom: var(--accent-custom-dark, var(--swatch-signal)); /* Which base this is, as numbers (the header's note; lib/siteColor.ts). */ --base-light: 0; --base-dark: 1; --brand: var(--swatch-signal); --brand-strong: color-mix(in oklab, var(--brand) 78%, white); --brand-soft: color-mix(in srgb, var(--brand) 16%, transparent); --brand-ink: #0c0a08; --state-gone: #d9644f; --state-gone-soft: rgba(217, 100, 79, 0.12); /* Same hue order, stepped into the validator's dark lightness band. Adjacent pairs pass every check; all pairs pass with green ↔ magenta in the CVD floor band (6.9). chart-3 vs --state-gone: ΔE 22.9 normal. chart-6, the rust stepped into the band: worst pair CVD 13.9, normal 16.3, 3.2:1 on the surface. */ --chart-1: #3561c8; --chart-2: #3fa577; --chart-3: #9a7ee6; --chart-4: #b98a2a; --chart-5: #c24c8a; --chart-6: #a54a08; --chart-surface: #16110a; --chart-grid: rgba(233, 220, 197, 0.08); --chart-axis: #8a8170; /* OTHER, on this base: a near-neutral grey (OKLCH L 0.49, C 0.01), the dimmest chart mark. 3.22:1 on the ground, 3.06:1 on the chart surface (the slots: 3.37–6.46:1 on the ground). Against each slot (normal / worst CVD): blue 17.8 / 17.9, green 19.2 / 15.7, violet 23.4 / 21.5, amber 20.3 / 18.1, magenta 19.5 / 7.3, rust 13.5 / 9.3 — magenta in the CVD 6–8 floor band (legal with the legend, the table and Other's place on top), rust the one normal pair under 15. */ --chart-other: #62625c; --chart-tooltip-bg: #1b150d; } /* --------------------------------------------------------------------------- ACCENTS — after every base block, at the same specificity, so they win `--brand` whichever base is set. `--brand` reads this base's swatch, so an accent is always its contrast-corrected value for the ground it is on; --brand-strong / -soft / --ring follow it through var(). `--brand-mark` is the on-dark value on every base (the mark sits on an ink tile). --------------------------------------------------------------------------- */ html[data-accent="signal"] { --brand: var(--swatch-signal); --brand-mark: #5fa8a0; } html[data-accent="brass"] { --brand: var(--swatch-brass); --brand-mark: #e3b15c; } html[data-accent="vermilion"] { --brand: var(--swatch-vermilion); --brand-mark: #ec7a52; } html[data-accent="violet"] { --brand: var(--swatch-violet); --brand-mark: #b49cf2; } html[data-accent="sakura"] { --brand: var(--swatch-sakura); --brand-mark: #ee8fb5; } html[data-accent="blue"] { --brand: var(--swatch-blue); --brand-mark: #74a9f2; } html[data-accent="green"] { --brand: var(--swatch-green); --brand-mark: #7cc46a; } /* A site whose site.json accent is its own hex: the layout renders `data-accent="custom"` plus the fitted `--accent-custom-light|dark` inline on . */ html[data-accent="custom"] { --brand: var(--swatch-custom); --brand-mark: var(--accent-custom-dark, #5fa8a0); } /* --------------------------------------------------------------------------- THE CHART GAP — the colour that parts touching chart marks, the marks spec's surface gap (common/components/charts/surfaceGap.ts): the chart surface on every base (a chart card is `--card`, the same value), and the reader's Canvas in forced colours. --------------------------------------------------------------------------- */ :root { --chart-gap: var(--chart-surface); } @media (forced-colors: active) { :root { --chart-gap: Canvas; } }