commit b4808e594369298f5de75897e12e610418748d27
parent c0e6bbbabb2c835d10460b6f93ed333ac16a2a3d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 21:37:05 -0400
common: tokens.css is three bases × seven accents; fonts are Archivo, Plex Sans, Plex Mono (brand S2)
The five theme families (base, archive, selenized, swiss, archilyzer), their
.dark block and their per-family type/radius rules are gone. In their place:
`:root, html[data-base="light"]`, `html[data-base="sepia"]` and
`html[data-base="dark"]`, each declaring all 49 colour tokens plus
color-scheme, `--swatch-<id>` for the seven accents (lib/brand.ts values on
that ground) and `--swatch-custom`; then one `html[data-accent=…]` rule per
accent (and `custom`) setting `--brand` and `--brand-mark`. `--primary` is
neutral on every base, `--ring` is the accent. Sepia is new (its status and
chart colours are the archive paper block's, darkened for the ground); dark is
the archive ink block plus the destructive-soft and state-gone it lacked.
Charts are fixed per base (blue, green, violet, amber, magenta), validated with
the dataviz skill's validate_palette.js: adjacent and all pairs pass on every
base, chart-3 vs --state-gone passes, chart-1 is not Signal.
One radius (0.375rem) and one type voice: Archivo (wdth) as --font-display
with `.font-display { font-stretch: 112% }`, IBM Plex Sans 400–700 normal +
italic, IBM Plex Mono. The runtime that sets data-base/data-accent follows in
the next commit; until then every page reads the light block.
themeTokens.test.ts parses the sheet: every base declares every token, the
swatches and --brand-mark equal ACCENTS, the accent rules follow the bases,
text tokens read at 4.5:1. REQUIRED_TOKENS joins themeConfig.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
7 files changed, 556 insertions(+), 680 deletions(-)
diff --git a/common/components/themeConfig.ts b/common/components/themeConfig.ts
@@ -35,3 +35,61 @@ export const THEME_MODES: { id: ThemeMode; label: string }[] = [
{ id: "dark", label: "Dark" },
{ id: "system", label: "System" },
];
+
+// Every colour token a base block in tokens.css must declare. The failure this
+// guards is silent: a base that omits a token inherits the light block's value
+// (`:root` always matches), so a half-declared palette looks "a bit off" rather
+// than broken, and only on the base nobody checked. The unit test
+// (themeTokens.test.ts) parses tokens.css against this list; the homepage e2e
+// reads every one off the computed style of each base.
+export const REQUIRED_TOKENS = [
+ "--background",
+ "--foreground",
+ "--card",
+ "--card-foreground",
+ "--popover",
+ "--popover-foreground",
+ "--primary",
+ "--primary-foreground",
+ "--secondary",
+ "--secondary-foreground",
+ "--muted",
+ "--muted-foreground",
+ "--accent",
+ "--accent-foreground",
+ "--destructive",
+ "--destructive-foreground",
+ "--destructive-soft",
+ "--border",
+ "--border-strong",
+ "--input",
+ "--ring",
+ "--surface",
+ "--faint",
+ "--panel",
+ "--panel-2",
+ "--success",
+ "--success-foreground",
+ "--success-soft",
+ "--warning",
+ "--warning-foreground",
+ "--warning-soft",
+ "--info",
+ "--info-foreground",
+ "--info-soft",
+ "--brand",
+ "--brand-strong",
+ "--brand-soft",
+ "--brand-ink",
+ "--state-gone",
+ "--state-gone-soft",
+ "--chart-1",
+ "--chart-2",
+ "--chart-3",
+ "--chart-4",
+ "--chart-5",
+ "--chart-surface",
+ "--chart-grid",
+ "--chart-axis",
+ "--chart-tooltip-bg",
+] as const;
diff --git a/common/components/themeTokens.test.ts b/common/components/themeTokens.test.ts
@@ -0,0 +1,204 @@
+// tokens.css completeness and parity. The token sheet is hand-written CSS and
+// lib/brand.ts is the palette as data; this test is what keeps the two equal,
+// and what catches a base that forgets a token (which fails SILENTLY in a
+// browser: it inherits the light block's value, since `:root` always matches).
+
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import {
+ ACCENTS,
+ ACCENT_IDS,
+ BASE_GROUND_IDS,
+ BASE_GROUNDS,
+ contrastRatio,
+ type BaseGround,
+} from "../lib/brand";
+import { customAccentVars } from "../lib/accent";
+import { REQUIRED_TOKENS } from "./themeConfig";
+
+const CSS = readFileSync(
+ fileURLToPath(new URL("../styles/tokens.css", import.meta.url)),
+ "utf8",
+);
+
+type Rule = { selector: string; decls: Map<string, string>; index: number };
+
+// Top-level rules only (the sheet has no nesting beyond `@theme inline`, which
+// is an at-rule and skipped). Comments are stripped first.
+function parseRules(css: string): Rule[] {
+ const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
+ const rules: Rule[] = [];
+ let i = 0;
+ while (i < src.length) {
+ const open = src.indexOf("{", i);
+ if (open < 0) break;
+ const selector = src.slice(i, open).replace(/^[\s;]*(@import[^;]*;|@custom-variant[^;]*;)?/g, "").trim();
+ let depth = 1;
+ let j = open + 1;
+ while (j < src.length && depth > 0) {
+ if (src[j] === "{") depth++;
+ else if (src[j] === "}") depth--;
+ j++;
+ }
+ const body = src.slice(open + 1, j - 1);
+ const decls = new Map<string, string>();
+ if (!selector.startsWith("@")) {
+ for (const part of body.split(";")) {
+ const k = part.indexOf(":");
+ if (k < 0) continue;
+ const name = part.slice(0, k).trim();
+ if (name) decls.set(name, part.slice(k + 1).trim());
+ }
+ }
+ rules.push({ selector: selector.replace(/\s+/g, " "), decls, index: rules.length });
+ i = j;
+ }
+ return rules;
+}
+
+const RULES = parseRules(CSS);
+
+function rule(selector: string): Rule {
+ const r = RULES.find((x) => x.selector === selector);
+ assert.ok(r, `tokens.css has no rule for ${selector}`);
+ return r;
+}
+
+const BASE_SELECTOR: Record<BaseGround, string> = {
+ light: ':root, html[data-base="light"]',
+ sepia: 'html[data-base="sepia"]',
+ dark: 'html[data-base="dark"]',
+};
+
+const ON: Record<BaseGround, "onLight" | "onSepia" | "onDark"> = {
+ light: "onLight",
+ sepia: "onSepia",
+ dark: "onDark",
+};
+
+test("every base declares every required token, color-scheme and every swatch", () => {
+ for (const base of BASE_GROUND_IDS) {
+ const r = rule(BASE_SELECTOR[base]);
+ for (const t of REQUIRED_TOKENS) {
+ assert.ok(r.decls.has(t), `${base} is missing ${t}`);
+ assert.notEqual(r.decls.get(t), "", `${base} declares ${t} empty`);
+ }
+ assert.equal(r.decls.get("color-scheme"), base === "dark" ? "dark" : "light");
+ for (const id of ACCENT_IDS) assert.ok(r.decls.has(`--swatch-${id}`), `${base} is missing --swatch-${id}`);
+ assert.ok(r.decls.has("--swatch-custom"), `${base} is missing --swatch-custom`);
+ }
+});
+
+test("REQUIRED_TOKENS is the 49 colour tokens, each once", () => {
+ assert.equal(REQUIRED_TOKENS.length, 49);
+ assert.equal(new Set(REQUIRED_TOKENS).size, REQUIRED_TOKENS.length);
+});
+
+test("--background is each base's ground (lib/brand.ts BASE_GROUNDS)", () => {
+ for (const base of BASE_GROUND_IDS) {
+ assert.equal(rule(BASE_SELECTOR[base]).decls.get("--background"), BASE_GROUNDS[base]);
+ }
+});
+
+test("each base's --swatch-<id> is that accent's value on that ground (ACCENTS)", () => {
+ for (const base of BASE_GROUND_IDS) {
+ const r = rule(BASE_SELECTOR[base]);
+ for (const id of ACCENT_IDS) {
+ assert.equal(r.decls.get(`--swatch-${id}`), ACCENTS[id][ON[base]], `${base} --swatch-${id}`);
+ }
+ }
+});
+
+test("--swatch-custom reads the inline var customAccentVars writes for that base", () => {
+ const vars = customAccentVars("#cc3366");
+ assert.ok(vars);
+ for (const base of BASE_GROUND_IDS) {
+ const name = `--accent-custom-${base}`;
+ assert.ok(name in vars, `customAccentVars has no ${name}`);
+ assert.equal(
+ rule(BASE_SELECTOR[base]).decls.get("--swatch-custom"),
+ `var(${name}, var(--swatch-signal))`,
+ );
+ }
+});
+
+test("each base defaults --brand to Signal, keeps --primary neutral and rings in the accent", () => {
+ for (const base of BASE_GROUND_IDS) {
+ const r = rule(BASE_SELECTOR[base]);
+ assert.equal(r.decls.get("--brand"), "var(--swatch-signal)");
+ assert.equal(r.decls.get("--ring"), "var(--brand)");
+ assert.doesNotMatch(r.decls.get("--primary") ?? "", /brand|swatch/);
+ assert.equal(r.decls.get("--primary"), base === "dark" ? "#efe7d8" : base === "sepia" ? "#33281a" : "#202a31");
+ assert.equal(r.decls.get("--brand-ink"), base === "dark" ? BASE_GROUNDS.dark : "#ffffff");
+ assert.match(r.decls.get("--brand-strong") ?? "", base === "dark" ? /var\(--brand\).*white/ : /var\(--brand\).*black/);
+ assert.match(r.decls.get("--brand-soft") ?? "", base === "dark" ? /var\(--brand\) 16%/ : /var\(--brand\) 12%/);
+ }
+});
+
+test("an accent rule per id sets --brand to its swatch and --brand-mark to its on-dark value", () => {
+ for (const id of ACCENT_IDS) {
+ const r = rule(`html[data-accent="${id}"]`);
+ assert.equal(r.decls.get("--brand"), `var(--swatch-${id})`);
+ assert.equal(r.decls.get("--brand-mark"), ACCENTS[id].onDark);
+ }
+ const custom = rule('html[data-accent="custom"]');
+ assert.equal(custom.decls.get("--brand"), "var(--swatch-custom)");
+ assert.match(custom.decls.get("--brand-mark") ?? "", /^var\(--accent-custom-dark\b/);
+});
+
+test("the accent rules come after every base block (same specificity, later wins)", () => {
+ const lastBase = Math.max(...BASE_GROUND_IDS.map((b) => rule(BASE_SELECTOR[b]).index));
+ for (const id of [...ACCENT_IDS, "custom"]) {
+ assert.ok(rule(`html[data-accent="${id}"]`).index > lastBase, `${id} precedes a base block`);
+ }
+});
+
+test(":root sets the one radius and a Signal --brand-mark default; .font-display runs wide", () => {
+ const root = rule(":root");
+ assert.equal(root.decls.get("--radius"), "0.375rem");
+ assert.equal(root.decls.get("--brand-mark"), ACCENTS.signal.onDark);
+ assert.equal(rule(".font-display").decls.get("font-stretch"), "112%");
+});
+
+test("no retired theme family is left in the sheet", () => {
+ assert.doesNotMatch(CSS, /data-theme/);
+ assert.doesNotMatch(CSS, /--font-(grotesk|plex|code)/);
+ assert.ok(!RULES.some((r) => r.selector === ".dark"), "a .dark token block is back");
+});
+
+test("charts stay off the accent and off the gone colour", () => {
+ for (const base of BASE_GROUND_IDS) {
+ const r = rule(BASE_SELECTOR[base]);
+ for (let n = 1; n <= 5; n++) assert.match(r.decls.get(`--chart-${n}`) ?? "", /^#[0-9a-f]{6}$/);
+ // FACTS.md ~6442: chart-3 once WAS the gone hex; the palette itself was
+ // re-validated with the dataviz validator (plans/brand-and-themes.md, S2).
+ assert.notEqual(r.decls.get("--chart-3"), r.decls.get("--state-gone"));
+ assert.notEqual(r.decls.get("--chart-1"), ACCENTS.signal[ON[base]]);
+ assert.notEqual(r.decls.get("--chart-1"), r.decls.get("--swatch-signal"));
+ }
+});
+
+test("text tokens read at 4.5:1 on each base's ground and card", () => {
+ const TEXT = [
+ "--foreground",
+ "--muted-foreground",
+ "--destructive",
+ "--success",
+ "--warning",
+ "--info",
+ "--state-gone",
+ ];
+ for (const base of BASE_GROUND_IDS) {
+ const r = rule(BASE_SELECTOR[base]);
+ for (const bg of ["--background", "--card"]) {
+ for (const t of TEXT) {
+ const ratio = contrastRatio(r.decls.get(t)!, r.decls.get(bg)!);
+ assert.ok(ratio >= 4.5, `${base}: ${t} on ${bg} is ${ratio.toFixed(2)}:1`);
+ }
+ }
+ const inkOn = contrastRatio(r.decls.get("--primary-foreground")!, r.decls.get("--primary")!);
+ assert.ok(inkOn >= 4.5, `${base}: primary-foreground on primary is ${inkOn.toFixed(2)}:1`);
+ }
+});
diff --git a/common/styles/fonts.ts b/common/styles/fonts.ts
@@ -1,93 +1,47 @@
// Shared font source of truth for the whole family. One `next/font/google` call
-// site, exposing CSS-variable tokens consumed by tokens.css's @theme:
-// --font-display (serif: wordmark, headline numerals)
-// --font-sans (body / UI)
-// --font-mono (queries, timestamps, IDs — the "power layer")
+// site, exposing the CSS-variable tokens tokens.css's @theme consumes:
+// --font-display Archivo — the wordmark, headings, headline numerals
+// --font-sans IBM Plex Sans — body and UI
+// --font-mono IBM Plex Mono — queries, timestamps, counts, IDs
//
-// Each app's root layout imports `fontVars` and spreads it onto <html>. This
-// collapses the previous three divergent setups (export=Geist, editor=system,
-// homepage=Source) onto one Adobe Source super-family voice — the DEFAULT/base +
-// archive voice.
+// Three faces, one voice on every base (plans/brand-and-themes.md, "Type and
+// radius"). The theme families that each loaded a voice of their own (Source
+// Serif/Sans/Code, JetBrains Mono) retired with the base × accent themes.
//
-// THEME-FAMILY TYPE VOICES: some families reassign --font-display/sans/mono to a
-// different loaded face (see tokens.css, `html[data-theme="…"]` blocks) so a
-// theme feels distinct beyond color. Those faces are ALSO loaded here (their
-// `.variable` classes must be present on <html> for the @font-face to be active,
-// which is why every face is joined into `fontVars`), but only referenced when a
-// family opts in:
-// • swiss → Archivo (a neo-grotesque; International Typographic Style)
-// • selenized → JetBrains Mono display+mono over IBM Plex Sans body (terminal)
-// • archilyzer → Archivo at 125% width (expanded, poster-scale display) over
-// IBM Plex Sans body + IBM Plex Mono data (the instrument face)
+// Each app's root layout imports `fontVars` and puts it on <html>, so the
+// variables are defined from the root down.
//
// `next/font` is transformed by Next's SWC loader across the whole compiled
// module graph, and `transpilePackages: ["yt-dlp-transcript-common"]` puts this
// module in that graph for every app — so a shared call site works. If a build
// ever balks, the fallback is to duplicate these calls per layout using the same
// variable names; the CSS token contract is the real invariant.
-import {
- Source_Serif_4,
- Source_Sans_3,
- Source_Code_Pro,
- Archivo,
- JetBrains_Mono,
- IBM_Plex_Sans,
- IBM_Plex_Mono,
-} from "next/font/google";
+import { Archivo, IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google";
-// --- Base / archive voice (the default super-family) ------------------------
-export const fontDisplay = Source_Serif_4({
+// Archivo carries its WIDTH axis: tokens.css runs `.font-display` at
+// `font-stretch: 112%`, and the wordmark (common/components/Wordmark) wider
+// still. Variable weight, so the wordmark's 720 / 380 need no extra files.
+export const fontDisplay = Archivo({
variable: "--font-display",
subsets: ["latin"],
- style: ["normal", "italic"],
-});
-
-export const fontSans = Source_Sans_3({
- variable: "--font-sans",
- subsets: ["latin"],
-});
-
-export const fontMono = Source_Code_Pro({
- variable: "--font-mono",
- subsets: ["latin"],
-});
-
-// --- Per-family voices (opted into by tokens.css [data-theme] blocks) --------
-// Swiss: a neo-grotesque for both display and body — tight, objective, Helvetica-
-// adjacent. Used for --font-display + --font-sans under [data-theme="swiss"].
-// Archilyzer additionally drives Archivo's WIDTH axis, so `font-stretch: 125%`
-// yields a genuinely expanded cut for poster-scale display type — visibly a
-// different animal from the default-width Archivo that swiss uses. Declaring
-// the axis here (rather than loading Archivo a second time) costs swiss
-// nothing: wdth defaults to 100%, so its rendering is unchanged.
-export const fontGrotesk = Archivo({
- variable: "--font-grotesk",
- subsets: ["latin"],
axes: ["wdth"],
});
-// Selenized: a code face for display + mono (the "terminal" voice).
-export const fontCode = JetBrains_Mono({
- variable: "--font-code",
- subsets: ["latin"],
-});
-
-// Selenized + Archilyzer: a humanist sans body. Institutional, built for long
-// documentation — which is half of what the project site is.
-export const fontPlex = IBM_Plex_Sans({
- variable: "--font-plex",
+// A humanist sans built for long documentation — which is most of what the
+// archives and the project site are.
+export const fontSans = IBM_Plex_Sans({
+ variable: "--font-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
+ style: ["normal", "italic"],
});
-// Archilyzer: tabular figures for counts, timecodes and recording states. The
-// data layer is a first-class voice in the instrument face, not an accent.
-export const fontPlexMono = IBM_Plex_Mono({
- variable: "--font-plex-mono",
+// Tabular figures for counts, timecodes and recording states.
+export const fontMono = IBM_Plex_Mono({
+ variable: "--font-mono",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
-// Space-joined `.variable` classes for <html className={...}>. EVERY face must be
-// here so its @font-face is registered even if only some families reference it.
-export const fontVars = `${fontDisplay.variable} ${fontSans.variable} ${fontMono.variable} ${fontGrotesk.variable} ${fontCode.variable} ${fontPlex.variable} ${fontPlexMono.variable}`;
+// Space-joined `.variable` classes for <html className={...}>.
+export const fontVars = `${fontDisplay.variable} ${fontSans.variable} ${fontMono.variable}`;
diff --git a/common/styles/tokens.css b/common/styles/tokens.css
@@ -4,23 +4,34 @@
Imported by every app's globals.css right after `@import "tailwindcss"`:
@import "../../common/styles/tokens.css";
- Two orthogonal axes:
- • `html[data-theme="…"]` selects the THEME FAMILY (palette personality).
- • `html.dark` selects light/dark MODE within that family.
- The `@custom-variant dark` below makes Tailwind's `dark:` utilities follow the
- `.dark` class (not the OS media query), so a pre-paint script can set both axes
- from localStorage. Default (no data-theme) = the neutral "base" family, which
- reproduces the previous zinc look so editor/export keep visual parity.
-
- TOKEN NAMING (Phase 1 — shadcn kit landed):
+ A reader theme is TWO independent choices (plans/brand-and-themes.md):
+ • `html[data-base="light|sepia|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="<id>|custom"]` selects the ACCENT — `--brand`. Each
+ site server-renders its own; a reader may pick another.
+ `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), so sepia styles as a light ground.
+
+ `:root` always matches <html>, 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 sepia and dark blocks are `html[data-base=…]` (0,1,1) and
+ win over it; the accent rules come AFTER all three 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.
- • The FAMILY BRAND color (brass on archive, blue on base) lives under the
- `--brand*` namespace (--brand, --brand-strong, --brand-soft, --brand-ink)
- so it never collides with shadcn's neutral `--accent`. A per-user/per-site
- accent override (ThemeScript / per-site SSR) sets `--brand`.
+ 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-<id>` is each named accent's value ON THIS BASE (lib/brand.ts
+ ACCENTS — themeTokens.test.ts keeps the two equal), so a picker can
+ show the colour a choice will actually paint.
========================================================================== */
@import "tw-animate-css";
@@ -52,7 +63,7 @@
/* 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 in each family. */
+ 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);
@@ -63,16 +74,15 @@
--color-info-foreground: var(--info-foreground);
--color-info-soft: var(--info-soft);
- /* Family brand (brass / blue) — power-layer accent, distinct from shadcn accent */
+ /* 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 family surfaces/lines (previously only some families defined these and
- none were exposed as utilities). Now every family sets them, so components
- can use `bg-surface`, `border-border-strong`, `text-faint`, `text-brand-ink`
- etc. and stay theme-aware. */
+ /* 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);
@@ -97,232 +107,193 @@
}
/* ---------------------------------------------------------------------------
- Per-family NON-COLOR axes — corner radius + type voice. Colour is the main way
- families differ; these add a second dimension so Swiss/Selenized don't just
- feel like recoloured Archive. Written with `html[data-theme="…"]` (specificity
- 0,1,1) so the --font-* reassignments reliably beat next/font's `.variable`
- class, which sets --font-display/sans/mono on <html> at specificity 0,1,0.
- Archive keeps the default soft radius and the full Source super-family voice.
- Base keeps the soft radius but is all-sans (see below) to match the pre-theme
- export/editor look, which had no serif.
+ Type and radius — one voice on every base. The faces come from
+ common/styles/fonts.ts (next/font sets --font-display/-sans/-mono on <html>):
+ 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.
--------------------------------------------------------------------------- */
-/* Base family (no data-theme): all-sans like the pre-theme site. `:not([data-theme])`
- (specificity 0,1,1) targets base only and beats next/font's `.variable` class
- (0,1,0) without leaking into archive, which declares no --font-* and would
- otherwise inherit this from :root. */
-html:not([data-theme]) {
- --font-display: var(--font-sans); /* Source Sans 3 instead of Source Serif 4 */
-}
-html[data-theme="selenized"] {
- --radius: 0.375rem; /* calm, modest rounding — a code editor's corners */
- --font-display: var(--font-code); /* JetBrains Mono wordmark/headings */
- --font-sans: var(--font-plex); /* IBM Plex Sans body */
- --font-mono: var(--font-code);
-}
-html[data-theme="swiss"] {
- --radius: 0px; /* hard 90° corners — International Typographic Style */
- --font-display: var(--font-grotesk); /* Archivo neo-grotesque */
- --font-sans: var(--font-grotesk);
-}
-html[data-theme="archilyzer"] {
- --radius: 0.125rem; /* 2px — a faceplate's edge break, not a rounded card */
- --font-display: var(--font-grotesk); /* Archivo, widened below */
- --font-sans: var(--font-plex); /* IBM Plex Sans — documentation body */
- --font-mono: var(--font-plex-mono); /* IBM Plex Mono — counts, states, time */
+: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;
}
-/* Archivo carries a `wdth` axis (declared in common/styles/fonts.ts). A modest
- widening is what separates this family's headline voice from the same
- neo-grotesque swiss uses at default width. Held at 110% rather than the full
- 125%: at poster sizes the widest cut reads as shouting, and the point of the
- instrument face is composure. Scoped to the display utility so body copy is
- unaffected. */
-html[data-theme="archilyzer"] .font-display {
- font-stretch: 110%;
+
+.font-display {
+ font-stretch: 112%;
}
/* ---------------------------------------------------------------------------
- BASE family (default — no data-theme). Neutral zinc; blue brand accent.
- Reproduces the prior editor/export look so the kit lands at visual parity.
- Chart axis/grid/surface/tooltip tokens live here too (they previously existed
- only on the homepage), so editor/export charts get proper strokes.
+ 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 {
- --radius: 0.625rem;
+:root,
+html[data-base="light"] {
+ color-scheme: light;
- --background: #ffffff;
- --foreground: #171717;
+ --background: #f3f6f7;
+ --surface: #e8edef;
+ --foreground: #161c21;
--card: #ffffff;
- --card-foreground: #171717;
+ --card-foreground: #161c21;
--popover: #ffffff;
- --popover-foreground: #171717;
- --primary: #18181b;
- --primary-foreground: #fafafa;
- --secondary: #f4f4f5;
- --secondary-foreground: #18181b;
- --muted: #f4f4f5;
- --muted-foreground: #71717a;
- --accent: #f4f4f5;
- --accent-foreground: #18181b;
- --destructive: #dc2626;
- --destructive-foreground: #fafafa;
- --destructive-soft: rgba(220, 38, 38, 0.12);
- --border: #e4e4e7;
- --border-strong: #d4d4d8;
- --input: #e4e4e7;
- --ring: #a1a1aa;
- --surface: #f7f7f8;
- --faint: #a1a1aa;
-
- --success: #15803d;
+ --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);
+
+ --success: #1f7a4d;
--success-foreground: #ffffff;
- --success-soft: rgba(22, 163, 74, 0.12);
- --warning: #b45309;
+ --success-soft: rgba(31, 122, 77, 0.12);
+ --warning: #96650b;
--warning-foreground: #ffffff;
- --warning-soft: rgba(217, 119, 6, 0.14);
- --info: #1d4ed8;
+ --warning-soft: rgba(150, 101, 11, 0.14);
+ --info: #1c5f96;
--info-foreground: #ffffff;
- --info-soft: rgba(37, 99, 235, 0.12);
+ --info-soft: rgba(28, 95, 150, 0.12);
- --brand: #2563eb;
- --brand-strong: #1d4ed8;
- --brand-soft: rgba(37, 99, 235, 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 <html>); Signal if it is missing. */
+ --swatch-custom: var(--accent-custom-light, var(--swatch-signal));
+
+ --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;
- --chart-1: #2563eb;
- --chart-2: #16a34a;
- --chart-3: #ea580c;
- --chart-4: #9333ea;
- --chart-5: #db2777;
+ /* 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). Validated
+ with the dataviz skill's validate_palette.js on this base's
+ --chart-surface: adjacent AND all pairs pass (lightness band, chroma, CVD
+ ΔE ≥ 8, normal-vision ΔE ≥ 15, ≥ 3:1). 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-1: #3a7de0;
+ --chart-2: #2f8a57;
+ --chart-3: #5e3aa8;
+ --chart-4: #a8741a;
+ --chart-5: #c24a8a;
--chart-surface: #ffffff;
- --chart-grid: rgba(0, 0, 0, 0.08);
- --chart-axis: #71717a;
+ --chart-grid: rgba(22, 28, 33, 0.09);
+ --chart-axis: #55646e;
--chart-tooltip-bg: #ffffff;
-
- /* Recording state: "this no longer exists on its platform". Declared on the
- base family (and on .dark below) rather than in each family block, so every
- family inherits a usable value — a family that redeclares it (archilyzer)
- simply wins on the same element. Without this, switching the project site
- to another family via the ThemeMenu would leave state chips uncoloured. */
- --state-gone: #dc2626;
- --state-gone-soft: rgba(220, 38, 38, 0.12);
-}
-
-.dark {
- --background: #0a0a0a;
- --foreground: #ededed;
- --card: #18181b;
- --card-foreground: #ededed;
- --popover: #18181b;
- --popover-foreground: #ededed;
- --primary: #fafafa;
- --primary-foreground: #18181b;
- --secondary: #27272a;
- --secondary-foreground: #fafafa;
- --muted: #27272a;
- --muted-foreground: #a1a1aa;
- --accent: #27272a;
- --accent-foreground: #fafafa;
- --destructive: #f87171;
- --destructive-foreground: #fafafa;
- --destructive-soft: rgba(248, 113, 113, 0.16);
- --border: #27272a;
- --border-strong: #3f3f46;
- --input: rgba(255, 255, 255, 0.12);
- --ring: #71717a;
- --surface: #141416;
- --faint: #52525b;
-
- --success: #4ade80;
- --success-foreground: #052e16;
- --success-soft: rgba(74, 222, 128, 0.16);
- --warning: #fbbf24;
- --warning-foreground: #2a1a02;
- --warning-soft: rgba(251, 191, 36, 0.16);
- --info: #60a5fa;
- --info-foreground: #04122e;
- --info-soft: rgba(96, 165, 250, 0.16);
-
- --brand: #60a5fa;
- --brand-strong: #93c5fd;
- --brand-soft: rgba(96, 165, 250, 0.16);
- --brand-ink: #04122e;
-
- --chart-1: #60a5fa;
- --chart-2: #4ade80;
- --chart-3: #fb923c;
- --chart-4: #c084fc;
- --chart-5: #f472b6;
- --chart-surface: #0a0a0a;
- --chart-grid: rgba(255, 255, 255, 0.08);
- --chart-axis: #a1a1aa;
- --chart-tooltip-bg: #18181b;
-
- /* See :root — the dark default every family inherits unless it overrides. */
- --state-gone: #f87171;
- --state-gone-soft: rgba(248, 113, 113, 0.16);
}
/* ---------------------------------------------------------------------------
- ARCHIVE family — the public flagship "Reading Room". Brass brand accent.
- Ships paper (light) + ink (dark). The ink values are the homepage's existing
- warm-ink palette, hoisted here verbatim so the homepage is pixel-parity when
- it defaults to data-theme="archive" + dark.
+ SEPIA — warm paper for long reading. New with the base × accent themes; its
+ status, panel and chart colours derive from the retired archive "paper"
+ block, darkened for this darker ground (every text colour ≥ 4.5:1 on it).
--------------------------------------------------------------------------- */
-[data-theme="archive"] {
- /* Paper (light) — warm aged-paper ground, darkened brass for contrast. */
- --background: #faf6ec;
- --surface: #f4ecdb;
- --foreground: #211a0e;
- --card: #fbf7ef;
- --card-foreground: #211a0e;
- --popover: #fbf7ef;
- --popover-foreground: #211a0e;
- --primary: #95661a;
- --primary-foreground: #fff7e7;
- --secondary: #efe6d3;
- --secondary-foreground: #211a0e;
- --muted: #efe6d3;
- --muted-foreground: #6b6250;
- --accent: #efe6d3;
- --accent-foreground: #211a0e;
+html[data-base="sepia"] {
+ color-scheme: light;
+
+ --background: #f4ecd8;
+ --surface: #ece2ca;
+ --foreground: #33281a;
+ --card: #faf4e6;
+ --card-foreground: #33281a;
+ --popover: #faf4e6;
+ --popover-foreground: #33281a;
+ --primary: #33281a;
+ --primary-foreground: #f4ecd8;
+ --secondary: #e9dec3;
+ --secondary-foreground: #33281a;
+ --muted: #ece2ca;
+ --muted-foreground: #6b5c43;
+ --accent: #e4d7b8;
+ --accent-foreground: #33281a;
--destructive: #b3261e;
- --destructive-foreground: #fff7e7;
- --border: rgba(40, 30, 12, 0.12);
- --border-strong: rgba(40, 30, 12, 0.2);
- --input: rgba(40, 30, 12, 0.18);
- --ring: #95661a;
- --faint: #948b78;
- --panel: rgba(255, 252, 244, 0.7);
- --panel-2: rgba(244, 236, 219, 0.7);
-
- --success: #2e7d32;
- --success-foreground: #fff7e7;
- --success-soft: rgba(46, 125, 50, 0.14);
- --warning: #a85b00;
- --warning-foreground: #fff7e7;
- --warning-soft: rgba(168, 91, 0, 0.14);
- --info: #1f6feb;
- --info-foreground: #fff7e7;
- --info-soft: rgba(31, 111, 235, 0.12);
-
- --brand: #95661a;
- --brand-strong: #714d10;
- --brand-soft: rgba(149, 102, 26, 0.14);
- --brand-ink: #fff7e7;
-
- --chart-1: #2563eb;
- --chart-2: #0f9d6b;
- --chart-3: #c2410c;
- --chart-4: #7c3aed;
- --chart-5: #be185d;
- --chart-surface: #fbf7ef;
- --chart-grid: rgba(40, 30, 12, 0.08);
- --chart-axis: #8a7f66;
+ --destructive-foreground: #ffffff;
+ --destructive-soft: rgba(179, 38, 30, 0.12);
+ --border: #dccdaa;
+ --border-strong: #c4b187;
+ --input: #d3c29c;
+ --ring: var(--brand);
+ --faint: #857a64;
+ --panel: rgba(250, 244, 230, 0.72);
+ --panel-2: rgba(236, 226, 202, 0.72);
+
+ --success: #276c2b;
+ --success-foreground: #ffffff;
+ --success-soft: rgba(39, 108, 43, 0.14);
+ --warning: #945000;
+ --warning-foreground: #ffffff;
+ --warning-soft: rgba(148, 80, 0, 0.14);
+ --info: #1a5cc4;
+ --info-foreground: #ffffff;
+ --info-soft: rgba(26, 92, 196, 0.12);
+
+ /* Each named accent's on-sepia value (lib/brand.ts ACCENTS.onSepia). */
+ --swatch-signal: #2b756e;
+ --swatch-brass: #8e6119;
+ --swatch-vermilion: #b3431f;
+ --swatch-violet: #6a4bc4;
+ --swatch-sakura: #a83a6a;
+ --swatch-blue: #2d5fb8;
+ --swatch-green: #3d772b;
+ --swatch-custom: var(--accent-custom-sepia, var(--swatch-signal));
+
+ --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;
+
+ --state-gone: #a3392a;
+ --state-gone-soft: rgba(163, 57, 42, 0.12);
+
+ /* Same hue order as light, stepped for the paper surface; adjacent and all
+ pairs pass the validator. chart-3 vs --state-gone: ΔE 23.7 normal. */
+ --chart-1: #3574d6;
+ --chart-2: #2a7d4f;
+ --chart-3: #5e3aa8;
+ --chart-4: #a8741a;
+ --chart-5: #bb4585;
+ --chart-surface: #faf4e6;
+ --chart-grid: rgba(51, 40, 26, 0.09);
+ --chart-axis: #6b5c43;
--chart-tooltip-bg: #fffaf0;
}
-[data-theme="archive"].dark {
- /* Ink (dark) — the homepage's warm near-black "archive room". */
+/* ---------------------------------------------------------------------------
+ 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;
@@ -330,8 +301,8 @@ html[data-theme="archilyzer"] .font-display {
--card-foreground: #efe7d8;
--popover: #1b150d;
--popover-foreground: #efe7d8;
- --primary: #e3b15c;
- --primary-foreground: #1a1305;
+ --primary: #efe7d8;
+ --primary-foreground: #0c0a08;
--secondary: #241d12;
--secondary-foreground: #efe7d8;
--muted: #1c1710;
@@ -340,10 +311,11 @@ html[data-theme="archilyzer"] .font-display {
--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: #e3b15c;
+ --ring: var(--brand);
--faint: #6f6757;
--panel: rgba(28, 23, 15, 0.55);
--panel-2: rgba(40, 33, 21, 0.5);
@@ -358,16 +330,31 @@ html[data-theme="archilyzer"] .font-display {
--info-foreground: #04122e;
--info-soft: rgba(110, 168, 255, 0.16);
- --brand: #e3b15c;
- --brand-strong: #f3c977;
- --brand-soft: rgba(227, 177, 92, 0.14);
- --brand-ink: #1a1305;
-
- --chart-1: #6ea8ff;
- --chart-2: #54d6a0;
- --chart-3: #f2935b;
- --chart-4: #c08cf0;
- --chart-5: #f178b6;
+ /* 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));
+
+ --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.16);
+
+ /* Same hue order, stepped into the validator's dark lightness band;
+ adjacent and all pairs pass. chart-3 vs --state-gone: ΔE 22.9 normal. */
+ --chart-1: #3561c8;
+ --chart-2: #3fa577;
+ --chart-3: #9a7ee6;
+ --chart-4: #b98a2a;
+ --chart-5: #c24c8a;
--chart-surface: #16110a;
--chart-grid: rgba(233, 220, 197, 0.08);
--chart-axis: #8a8170;
@@ -375,374 +362,44 @@ html[data-theme="archilyzer"] .font-display {
}
/* ---------------------------------------------------------------------------
- SELENIZED family — the Solarized successor (Jan Warchoł's "selenized"). The
- signature warm-tan ground (#fbf3db) / deep teal-slate ground (#103c48) with a
- cyan brand and the calibrated Selenized accent set. A distinct, unmistakable
- color identity; the terminal/code voice (JetBrains Mono + IBM Plex Sans) is
- applied via the per-family type block near the top of this file.
+ 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).
--------------------------------------------------------------------------- */
-[data-theme="selenized"] {
- /* Selenized light — teal-slate ink on the signature warm tan. */
- --background: #fbf3db;
- --surface: #ece3cc;
- --foreground: #3a4d53;
- --card: #fefbf0;
- --card-foreground: #3a4d53;
- --popover: #fefbf0;
- --popover-foreground: #3a4d53;
- --primary: #009c8f;
- --primary-foreground: #fbf3db;
- --secondary: #ece3cc;
- --secondary-foreground: #3a4d53;
- --muted: #ece3cc;
- --muted-foreground: #63757a;
- --accent: #d5cdb6;
- --accent-foreground: #3a4d53;
- --destructive: #d2212d;
- --destructive-foreground: #fbf3db;
- --destructive-soft: rgba(210, 33, 45, 0.12);
- --border: rgba(58, 77, 83, 0.14);
- --border-strong: rgba(58, 77, 83, 0.28);
- --input: rgba(58, 77, 83, 0.18);
- --ring: #009c8f;
- --faint: #909995;
- --panel: rgba(254, 251, 240, 0.7);
- --panel-2: rgba(236, 227, 204, 0.7);
-
- --success: #489100;
- --success-foreground: #fbf3db;
- --success-soft: rgba(72, 145, 0, 0.14);
- --warning: #ad8900;
- --warning-foreground: #fbf3db;
- --warning-soft: rgba(173, 137, 0, 0.14);
- --info: #0072d4;
- --info-foreground: #fbf3db;
- --info-soft: rgba(0, 114, 212, 0.12);
-
- --brand: #009c8f;
- --brand-strong: #00887c;
- --brand-soft: rgba(0, 156, 143, 0.14);
- --brand-ink: #fbf3db;
-
- --chart-1: #009c8f;
- --chart-2: #489100;
- --chart-3: #0072d4;
- --chart-4: #8762c6;
- --chart-5: #c25d1e;
- --chart-surface: #fefbf0;
- --chart-grid: rgba(58, 77, 83, 0.1);
- --chart-axis: #63757a;
- --chart-tooltip-bg: #fefbf0;
+html[data-accent="signal"] {
+ --brand: var(--swatch-signal);
+ --brand-mark: #5fa8a0;
}
-
-[data-theme="selenized"].dark {
- /* Selenized dark — bright cyan/teal on the deep teal-slate ground. */
- --background: #103c48;
- --surface: #0e3640;
- --foreground: #adbcbc;
- --card: #184956;
- --card-foreground: #cad8d9;
- --popover: #1c5162;
- --popover-foreground: #cad8d9;
- --primary: #41c7b9;
- --primary-foreground: #103c48;
- --secondary: #2d5b69;
- --secondary-foreground: #cad8d9;
- --muted: #184956;
- --muted-foreground: #72898f;
- --accent: #2d5b69;
- --accent-foreground: #cad8d9;
- --destructive: #ff665c;
- --destructive-foreground: #103c48;
- --destructive-soft: rgba(250, 87, 80, 0.16);
- --border: rgba(202, 216, 217, 0.12);
- --border-strong: rgba(202, 216, 217, 0.24);
- --input: rgba(202, 216, 217, 0.16);
- --ring: #41c7b9;
- --faint: #5c7278;
- --panel: rgba(24, 73, 86, 0.6);
- --panel-2: rgba(45, 91, 105, 0.5);
-
- --success: #84c747;
- --success-foreground: #0e2f14;
- --success-soft: rgba(132, 199, 71, 0.16);
- --warning: #ebc13d;
- --warning-foreground: #103c48;
- --warning-soft: rgba(235, 193, 61, 0.16);
- --info: #58a3ff;
- --info-foreground: #041022;
- --info-soft: rgba(88, 163, 255, 0.16);
-
- --brand: #41c7b9;
- --brand-strong: #53d6c7;
- --brand-soft: rgba(65, 199, 185, 0.16);
- --brand-ink: #103c48;
-
- --chart-1: #41c7b9;
- --chart-2: #84c747;
- --chart-3: #58a3ff;
- --chart-4: #bd96fa;
- --chart-5: #fd9456;
- --chart-surface: #184956;
- --chart-grid: rgba(202, 216, 217, 0.1);
- --chart-axis: #72898f;
- --chart-tooltip-bg: #1c5162;
+html[data-accent="brass"] {
+ --brand: var(--swatch-brass);
+ --brand-mark: #e3b15c;
}
-
-/* ---------------------------------------------------------------------------
- SWISS family — International Typographic Style. Red + black + white, strong
- hairline rules. Light-first; dark = white-on-black. Pure token swap.
- --------------------------------------------------------------------------- */
-[data-theme="swiss"] {
- /* Light — white ground, black primary, international red brand. */
- --background: #ffffff;
- --surface: #f5f5f5;
- --foreground: #0a0a0a;
- --card: #ffffff;
- --card-foreground: #0a0a0a;
- --popover: #ffffff;
- --popover-foreground: #0a0a0a;
- --primary: #0a0a0a;
- --primary-foreground: #ffffff;
- --secondary: #f0f0f0;
- --secondary-foreground: #0a0a0a;
- --muted: #f0f0f0;
- --muted-foreground: #525252;
- --accent: #ececec;
- --accent-foreground: #0a0a0a;
- --destructive: #b00016;
- --destructive-foreground: #ffffff;
- --border: #d4d4d4;
- --border-strong: #0a0a0a;
- --input: #c8c8c8;
- --ring: #0a0a0a;
- --faint: #8a8a8a;
- --panel: rgba(255, 255, 255, 0.85);
- --panel-2: rgba(245, 245, 245, 0.85);
-
- --success: #1a7d3a;
- --success-foreground: #ffffff;
- --success-soft: rgba(26, 125, 58, 0.12);
- --warning: #9a6700;
- --warning-foreground: #ffffff;
- --warning-soft: rgba(154, 103, 0, 0.12);
- --info: #1d4ed8;
- --info-foreground: #ffffff;
- --info-soft: rgba(29, 78, 216, 0.1);
-
- --brand: #d4001a;
- --brand-strong: #b00016;
- --brand-soft: rgba(212, 0, 26, 0.1);
- --brand-ink: #ffffff;
-
- --chart-1: #d4001a;
- --chart-2: #0a0a0a;
- --chart-3: #6b7280;
- --chart-4: #1d4ed8;
- --chart-5: #b8a589;
- --chart-surface: #ffffff;
- --chart-grid: rgba(10, 10, 10, 0.1);
- --chart-axis: #525252;
- --chart-tooltip-bg: #ffffff;
+html[data-accent="vermilion"] {
+ --brand: var(--swatch-vermilion);
+ --brand-mark: #ec7a52;
}
-
-[data-theme="swiss"].dark {
- /* Dark — black ground, white primary, brighter red brand. */
- --background: #0a0a0a;
- --surface: #141414;
- --foreground: #fafafa;
- --card: #141414;
- --card-foreground: #fafafa;
- --popover: #161616;
- --popover-foreground: #fafafa;
- --primary: #fafafa;
- --primary-foreground: #0a0a0a;
- --secondary: #1f1f1f;
- --secondary-foreground: #fafafa;
- --muted: #1c1c1c;
- --muted-foreground: #a3a3a3;
- --accent: #222222;
- --accent-foreground: #fafafa;
- --destructive: #ff5a6a;
- --destructive-foreground: #0a0a0a;
- --border: rgba(250, 250, 250, 0.14);
- --border-strong: rgba(250, 250, 250, 0.4);
- --input: rgba(250, 250, 250, 0.18);
- --ring: #fafafa;
- --faint: #6b6b6b;
- --panel: rgba(20, 20, 20, 0.6);
- --panel-2: rgba(31, 31, 31, 0.5);
-
- --success: #46c46a;
- --success-foreground: #0a0a0a;
- --success-soft: rgba(70, 196, 106, 0.16);
- --warning: #d9a13a;
- --warning-foreground: #0a0a0a;
- --warning-soft: rgba(217, 161, 58, 0.16);
- --info: #60a5fa;
- --info-foreground: #0a0a0a;
- --info-soft: rgba(96, 165, 250, 0.16);
-
- --brand: #ff2b3e;
- --brand-strong: #ff5a6a;
- --brand-soft: rgba(255, 43, 62, 0.16);
- --brand-ink: #0a0a0a;
-
- --chart-1: #ff2b3e;
- --chart-2: #fafafa;
- --chart-3: #9ca3af;
- --chart-4: #60a5fa;
- --chart-5: #cbb89a;
- --chart-surface: #141414;
- --chart-grid: rgba(250, 250, 250, 0.1);
- --chart-axis: #a3a3a3;
- --chart-tooltip-bg: #161616;
+html[data-accent="violet"] {
+ --brand: var(--swatch-violet);
+ --brand-mark: #b49cf2;
}
-
-/* ---------------------------------------------------------------------------
- ARCHILYZER family — the instrument face. This is the TOOL's voice, and it is
- deliberately not the archives' voice: the warm brass "reading room" belongs
- to the things Archilyzer builds, and dressing the instrument as its own
- output flattens a hierarchy that should read instantly.
-
- THE ARGUMENT, IN TOKENS: chrome is achromatic — graphite and bone, no
- decorative brand hue anywhere. Colour appears in exactly two places and both
- carry information: a recording's STATE (--state-gone, the one hot hue, worn
- only by recordings that are gone), and the archives themselves (each site
- card wears its own accent from site.json). The tool has no colour; the
- recordings do.
-
- --brand is therefore a cool signal, near-unused in chrome. It exists because
- every family must set it — ThemeScript can override it per-user, shadcn
- components reach for it — not because this family wants an accent. CTAs are
- bone-on-graphite solid fills, not coloured buttons.
-
- The ground is a LIFTED cool slate, not near-black: painted equipment under
- light, rather than the void that every "dark mode with an acid accent"
- landing page reaches for.
- --------------------------------------------------------------------------- */
-[data-theme="archilyzer"] {
- /* Light — bone ground, graphite ink. Anodized rather than papery. */
- --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: #2f7f77;
- --faint: #78868f;
- --panel: rgba(255, 255, 255, 0.78);
- --panel-2: rgba(232, 237, 239, 0.78);
-
- --success: #1f7a4d;
- --success-foreground: #ffffff;
- --success-soft: rgba(31, 122, 77, 0.12);
- --warning: #96650b;
- --warning-foreground: #ffffff;
- --warning-soft: rgba(150, 101, 11, 0.14);
- --info: #1c5f96;
- --info-foreground: #ffffff;
- --info-soft: rgba(28, 95, 150, 0.12);
-
- --brand: #2f7f77;
- --brand-strong: #226058;
- --brand-soft: rgba(47, 127, 119, 0.12);
- --brand-ink: #ffffff;
-
- /* Recording state. The ONLY hues the chrome permits, and they are readable as
- a claim: "gone" is worn exclusively by recordings re-checked and found
- absent from their platform. "kept" is deliberately achromatic — a recording
- still being there is the unremarkable case, and colouring it would imply a
- guarantee nobody re-verified. */
- --state-gone: #a8412d;
- --state-gone-soft: rgba(168, 65, 45, 0.12);
-
- --chart-1: #2f7f77;
- --chart-2: #1c5f96;
- /* --chart-3 is a series hue, not --state-gone: it used to be the same hex,
- so the third archive in a chart read as "gone". A green, lightness-stepped
- away from the gone red so it stays apart under deutan/protan simulation
- too (OKLab dE vs gone 25.9 normal / 11.2 deutan; vs chart-2 26.9, chart-4
- 30.3; 3.29:1 on --chart-surface). */
- --chart-3: #5a9e3a;
- --chart-4: #6a4f9c;
- --chart-5: #8a6a1f;
- --chart-surface: #ffffff;
- --chart-grid: rgba(22, 28, 33, 0.09);
- --chart-axis: #55646e;
- --chart-tooltip-bg: #ffffff;
+html[data-accent="sakura"] {
+ --brand: var(--swatch-sakura);
+ --brand-mark: #ee8fb5;
}
-
-[data-theme="archilyzer"].dark {
- /* Dark — the primary look. Lifted cool slate, bone text, hairline geometry. */
- --background: #151b20;
- --surface: #1c242b;
- --foreground: #e7edf1;
- --card: #1c242b;
- --card-foreground: #e7edf1;
- --popover: #202932;
- --popover-foreground: #e7edf1;
- --primary: #e7edf1;
- --primary-foreground: #151b20;
- --secondary: #242e37;
- --secondary-foreground: #e7edf1;
- --muted: #1c242b;
- --muted-foreground: #a4b3bd;
- --accent: #242e37;
- --accent-foreground: #e7edf1;
- --destructive: #c4553f;
- --destructive-foreground: #f7eae7;
- --destructive-soft: rgba(196, 85, 63, 0.18);
- --border: #2a343c;
- --border-strong: #3d4a54;
- --input: rgba(231, 237, 241, 0.16);
- --ring: #5fa8a0;
- --faint: #8496a2;
- --panel: rgba(28, 36, 43, 0.72);
- --panel-2: rgba(36, 46, 55, 0.6);
-
- --success: #5ab97f;
- --success-foreground: #0c1f14;
- --success-soft: rgba(90, 185, 127, 0.16);
- --warning: #d3a03f;
- --warning-foreground: #1d1605;
- --warning-soft: rgba(211, 160, 63, 0.16);
- --info: #6aa5d8;
- --info-foreground: #071723;
- --info-soft: rgba(106, 165, 216, 0.16);
-
- --brand: #5fa8a0;
- --brand-strong: #7cc2ba;
- --brand-soft: rgba(95, 168, 160, 0.16);
- --brand-ink: #0d1a19;
-
- --state-gone: #c4553f;
- --state-gone-soft: rgba(196, 85, 63, 0.16);
-
- --chart-1: #5fa8a0;
- --chart-2: #6aa5d8;
- /* Same green, dark step (dE vs gone 29.1 normal / 17.1 deutan; vs chart-2
- 20.8, chart-4 26.6; 8.03:1 on --chart-surface). */
- --chart-3: #86c86a;
- --chart-4: #a98ede;
- --chart-5: #d3a03f;
- --chart-surface: #1a2229;
- --chart-grid: rgba(231, 237, 241, 0.08);
- --chart-axis: #8496a2;
- --chart-tooltip-bg: #202932;
+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|sepia|dark`
+ inline on <html>. */
+html[data-accent="custom"] {
+ --brand: var(--swatch-custom);
+ --brand-mark: var(--accent-custom-dark, #5fa8a0);
}
diff --git a/editor/app/globals.css b/editor/app/globals.css
@@ -2,10 +2,11 @@
@import "../../common/styles/tokens.css";
@source "../../common/components";
-/* Design tokens, the `dark` variant, and theme palettes live in
- common/styles/tokens.css. The editor runs the neutral "base" family as a
- compact, command-first cockpit (shell + dashboard + palette on the shared
- kit); per-page internals migrate incrementally. */
+/* Design tokens, the `dark` variant, the three bases (light / sepia / dark) and
+ the accents live in common/styles/tokens.css; the faces in
+ common/styles/fonts.ts. The editor follows the operator's system base with
+ the Signal accent, as a compact, command-first cockpit (shell + dashboard +
+ palette on the shared kit); per-page internals migrate incrementally. */
body {
background: var(--background);
diff --git a/export/app/globals.css b/export/app/globals.css
@@ -2,10 +2,11 @@
@import "../../common/styles/tokens.css";
@source "../../common/components";
-/* Design tokens, the `dark` variant, and theme palettes live in
- common/styles/tokens.css. Export defaults to the neutral "base" family (the
- pre-theme zinc/white look, all-sans); the archive/selenized/swiss families
- remain available via the ThemeMenu picker. */
+/* Design tokens, the `dark` variant, the three bases (light / sepia / dark) and
+ the accents live in common/styles/tokens.css; the faces in
+ common/styles/fonts.ts. A site opens on the reader's system base in its own
+ accent (site.json, rendered as `html[data-accent]`); the hub opens dark in
+ Signal. The ThemeMenu picks any base and any accent. */
body {
background: var(--background);
diff --git a/homepage/app/globals.css b/homepage/app/globals.css
@@ -3,10 +3,11 @@
@source "../../common/components";
/* This is the PROJECT's site — the instrument, not one of the archives it
- builds. It commits to the "archilyzer" family (graphite chrome, achromatic,
- colour reserved for recording state and for the archives' own accents).
- Palettes and the `dark` variant live in common/styles/tokens.css; only the
- faceplate geometry and the page-load reveal remain here.
+ builds. It opens on the dark base in Signal, the family's own accent, and
+ keeps its chrome achromatic: colour is reserved for recording state and for
+ the archives' own accents. The bases, the accents and the `dark` variant
+ live in common/styles/tokens.css; only the faceplate geometry and the
+ page-load reveal remain here.
The warm radial glow + film grain that used to live here is deliberately
GONE. It was archive-room dressing, correct back when this page was a shelf