mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:27:14 +01:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4
221 lines
7.1 KiB
CSS
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;
|
|
}
|
|
}
|