Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/components/themeConfig.ts | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/components/themeTokens.test.ts | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/styles/fonts.ts | 94++++++++++++++++++++-----------------------------------------------------------
Mcommon/styles/tokens.css | 853++++++++++++++++++++++++-------------------------------------------------------
Meditor/app/globals.css | 9+++++----
Mexport/app/globals.css | 9+++++----
Mhomepage/app/globals.css | 9+++++----
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