utils/web_ui/dist/tokens.css
2026-11-01 19:46:05 +01:00

221 lines
7.1 KiB
CSS

/*
* The frozen design tokens, shared by every one of kuhy's web repos.
*
* Source of truth: ~/src/utils/unified-design-system/tokens.md (values) and
* README.md (the CSS custom-property names). The same numbers ship for Dart in
* ~/src/utils/design_system, so a change here is a change there — do not edit one
* side alone, and do not introduce a value that is not on a scale below.
*
* Dark is the default; light is an override, matching the README.
*/
:root {
/* Neutrals ------------------------------------------------------------- */
--bg: #211d1b;
--surface-1: #2b2624;
--surface-2: #38312e;
--border: #463e3a;
--text: #eceae9;
--text-muted: #aaa09a;
/* Accent + semantic roles ---------------------------------------------- */
--accent: #b8862e;
--info: #b8862e; /* = accent */
--success: #8a9a3c;
--warning: #e0a63c;
--danger: #e2585f;
/* Text/icons ON a filled accent/success/warning/danger surface. Never
* --text: every semantic fill sits in the same mid-light band, where
* near-white measures 2.2:1-3.6:1 against a 4.5:1 floor. */
--on-fill: #211d1b;
/* Text/icons on a dark translucent scrim/overlay — fixed, not theme-tied,
* because the scrim itself is always dark. */
--on-scrim: #eceae9;
/* Categorical ramp ------------------------------------------------------ */
/* For charts, tags and category dots — N mutually distinguishable colours
* with no inherent meaning. NEVER for semantic state; that stays
* success/warning/danger. Use them in ramp order for ordered data and by
* position for unordered data — never "the green one because green means
* good", which is the semantic vocabulary leaking back in.
*
* These deliberately leave the warm wedge every other token sits in: a
* categorical ramp has no meaning to lean on, so hue separation IS the
* requirement. Chosen by search under a >=20 CIE dE floor across normal
* vision and all three CVD types, >=3:1 on BOTH backgrounds, and a monotonic
* lightness staircase (58 -> 44) so the ramp survives greyscale. Do not edit
* a value here without re-running unified-design-system/scripts/ramp_check.py.
* They do not flip with prefers-color-scheme — they must hold on both. */
--cat-1: #c57293;
--cat-2: #398fc0;
--cat-3: #c85a32;
--cat-4: #228736;
--cat-5: #8d58bb;
--cat-6: #686d2c;
/* Corner radius --------------------------------------------------------- */
--radius-sm: 8px; /* buttons, inputs, chips */
--radius-md: 12px; /* cards */
--radius-lg: 16px; /* sheets, dialogs */
/* Spacing (4px base) ---------------------------------------------------- */
--space-1: 4px;
--space-2: 8px;
--space-3: 16px;
--space-4: 24px;
--space-5: 32px;
--space-6: 48px;
/* Typography ------------------------------------------------------------ */
--font-sans: -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
--font-mono: ui-monospace, "Cascadia Code", monospace;
--text-display: 32px;
--text-title: 24px;
--text-subtitle: 20px;
--text-body: 16px; /* the floor for anything a user actually reads */
--text-label: 14px; /* UI chrome only */
--text-caption: 12px; /* UI chrome only */
--tracking-display: -0.2px;
--tracking-title: -0.1px;
--tracking-label: 0.05em;
--tracking-caption: 0.1em;
/* Prose line-length cap (rule 21): ~65-70 characters. */
--measure: 40rem;
/* Shadows: light surfaces only, and only on floating/overlay elements —
* never inline cards or list rows. Blur = 2x offset, tinted from ink. */
--shadow-1: 0 2px 4px rgba(33, 29, 27, 0.12); /* dropdown */
--shadow-2: 0 4px 8px rgba(33, 29, 27, 0.16); /* modal */
/* Focus ring: the platform default is black, invisible on --bg. */
--focus-ring: #b8862e; /* = accent */
/* Motion ----------------------------------------------------------------- */
/* Frozen in ~/src/utils/unified-design-system/motion.md, which carries the
* rationale per step. The same numbers ship for Dart as AppDuration /
* AppCurve — a change here is a change there. Four steps only: a longer
* duration vocabulary gets picked from at random, which is drift wearing a
* token's name. */
--duration-instant: 0ms; /* the reduced-motion collapse target */
--duration-fast: 120ms; /* hover, press, checkbox — already under the cursor */
--duration-base: 200ms; /* default: enters/leaves/moves within the view */
--duration-slow: 320ms; /* sheets, dialogs, page transitions */
/* Named for what the motion does at its END — the part the eye reads. Never
* ease-in-out on something that enters or exits: symmetric easing makes an
* arrival look like it is braking and a departure look reluctant. */
--ease-standard: cubic-bezier(0.2, 0, 0, 1); /* starts and ends on screen */
--ease-decelerate: cubic-bezier(0, 0, 0, 1); /* entering: full speed -> rest */
--ease-accelerate: cubic-bezier(0.3, 0, 1, 1); /* leaving: no settle */
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f4f3;
--surface-1: #fcfbfb;
--surface-2: #fcfbfb;
--border: #e0dad7;
--text: #211d1b;
--text-muted: #70625b;
/* --on-fill / --on-scrim stay fixed — the fills and the scrim do not
* change with page theme. */
}
}
/* Dark surfaces take no shadow at all; elevation is a fill step. Light
* surfaces opt in explicitly via --shadow-1 / --shadow-2. */
@media (prefers-color-scheme: dark) {
:root {
--shadow-1: none;
--shadow-2: none;
}
}
/* Mandatory, and it collapses the tokens themselves rather than asking every
* consumer to branch. Easing is irrelevant at zero duration, so the curves are
* left alone. Sound and haptics are NOT motion and are deliberately untouched
* here: a user who suppressed animation did not ask to lose confirmation that
* a tap landed. The sound opt-out is a separate, user-facing setting. */
@media (prefers-reduced-motion: reduce) {
:root {
--duration-fast: 0ms;
--duration-base: 0ms;
--duration-slow: 0ms;
}
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: var(--text-body);
}
/* Ships here rather than being left to each consumer: an unstyled focus ring
* is the accessibility regression this layer exists to prevent, and several of
* these apps are keyboard-only by necessity. */
:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 1px;
}
/* Print default (brennenputh.me/blog/printable-websites). Colour tokens are
* dropped for paper: black on white, links spelled out, code boxed. Elements
* marked .noPrint (nav, header, controls) vanish; .onlyPrint is the inverse
* and is hidden on screen by the rule below the block. */
@media print {
.noPrint,
nav.noPrint,
header.noPrint {
display: none;
}
body {
background: white;
color: black;
font-size: 12pt;
}
h1 {
font-size: 16pt;
}
h2 {
font-size: 14pt;
}
h3 {
font-size: 13pt;
}
pre,
code {
border: 1px solid black;
font-size: 11pt;
break-inside: avoid;
}
figure,
img {
break-inside: avoid;
}
a {
color: black;
}
a[href^="http"]::after {
content: " (" attr(href) ")";
}
}
@media screen {
.onlyPrint {
display: none;
}
}