Contrast was measured, not eyeballed: 41 foreground/background pairs across both themes and the terminal surface, each against the darkest (light) or lightest (dark) surface it actually sits on. Five failed. Links on manila read 3.64:1 and the footer's "No telemetry. Ever." — a claim the site goes out of its way to make — sat at 2.86:1. The accent needed splitting rather than moving. #c1502e clears 4.5:1 as a button fill behind white but only 3.99:1 as an 11px eyebrow on vinyl, so --accent keeps the brand hue for fills and a new --accent-text carries the readable cut. The terminal gets the same treatment dark emulators give their palettes: alarm and oxide brighten inside .tui-content so "Failed" is legible next to its glyph. Reduced motion no longer nukes everything. A blanket 0.01ms also kills the transitions that answer a click, which is feedback rather than movement; looping and travelling animation stops, the rest caps at 80ms. The TUI already skipped its opening run and still does. The hero reserves its box with aspect-ratio from the first paint. It was prerendered at full unscaled height and collapsed once JS measured, moving everything below it by ~145px. Measured shift is now 0. Also: the webfont loads non-blocking (display=swap meant the first frame never waited for it anyway), footer link groups nest under a heading instead of sitting level with page sections, and eight tokens nothing referenced stop shipping.
298 lines
8.9 KiB
CSS
298 lines
8.9 KiB
CSS
@import 'tailwindcss';
|
|
|
|
/*
|
|
* soulseek-rs — design tokens.
|
|
*
|
|
* Ported verbatim from the design system's tokens/{colors,typography,spacing}.css.
|
|
* The same values back the TUI in soulseek-rs/src/ui/styles.rs, so the terminal
|
|
* and the site stay one product. Dark is the native theme; light re-cuts the
|
|
* semantic hues ~10% darker because phosphor and tape read on vinyl but wash
|
|
* out on manila.
|
|
*
|
|
* Theme switching is driven by [data-theme] on <html>, matching the design's
|
|
* own selector, rather than Tailwind's default `.dark` class.
|
|
*/
|
|
@custom-variant light (&:where([data-theme='light'], [data-theme='light'] *));
|
|
|
|
@theme {
|
|
/* ---- Raw palette (dark-native names) ---- */
|
|
--color-sleeve: #14110f; /* warm off-black — cardboard shadow, never #000 */
|
|
--color-vinyl: #1e1a17; /* panels / surfaces, one step up from base */
|
|
--color-vinyl-2: #26211d; /* raised surface / hover on panels */
|
|
--color-dust: #8a8178; /* muted warm grey — secondary text, borders */
|
|
--color-paper: #e8e1d6; /* aged insert card — primary text, never #FFF */
|
|
--color-paper-dim: #c9c1b4;
|
|
|
|
--color-oxide: #c1502e; /* iron-oxide accent, deeper than Rust's own #CE422B */
|
|
--color-tape: #d8a657; /* cassette-shell amber — warnings, queued */
|
|
--color-phosphor: #7fb685; /* desaturated LCD green — success, connected */
|
|
--color-signal: #6e9dc9; /* softened windows-classic blue — links, info */
|
|
--color-alarm: #b33a3a; /* error red, distinguishable from oxide */
|
|
|
|
--color-manila: #f2ede4;
|
|
--color-manila-2: #e8e1d6;
|
|
--color-manila-3: #ded5c7;
|
|
|
|
/* ---- Semantic aliases, wired to the runtime vars below ---- */
|
|
--color-base: var(--bg-base);
|
|
--color-panel: var(--bg-panel);
|
|
--color-raised: var(--bg-raised);
|
|
|
|
--color-primary: var(--text-primary);
|
|
--color-secondary: var(--text-secondary);
|
|
--color-muted: var(--text-muted);
|
|
|
|
--color-hairline: var(--border-hairline);
|
|
--color-line: var(--border-default);
|
|
--color-line-strong: var(--border-strong);
|
|
|
|
/*
|
|
* --accent fills; --accent-text writes.
|
|
*
|
|
* The oxide accent is a mid-tone: it clears 4.5:1 as a background behind
|
|
* white, but not as 11px text on vinyl. Splitting the two uses lets the
|
|
* fill keep the brand hue while the text cut earns its contrast.
|
|
*/
|
|
--color-accent: var(--accent);
|
|
--color-accent-text: var(--accent-text);
|
|
--color-accent-hover: var(--accent-hover);
|
|
|
|
--color-success: var(--status-success);
|
|
--color-warning: var(--status-warning);
|
|
--color-info: var(--status-info);
|
|
|
|
--color-link: var(--link);
|
|
--color-link-hover: var(--link-hover);
|
|
|
|
/* ---- Typography: monospace, non-negotiable. It's a terminal application. ---- */
|
|
--font-mono:
|
|
'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
/* the "earned wink" — the era's forum font. One surface only. */
|
|
--font-forum: Verdana, Tahoma, sans-serif;
|
|
|
|
/* Scale, from the brandbook specimen (48/56, 32/40, 20/28, 14/22, 12/18) */
|
|
--text-display: 48px;
|
|
--text-display--line-height: 56px;
|
|
--text-title: 32px;
|
|
--text-title--line-height: 40px;
|
|
--text-heading: 20px;
|
|
--text-heading--line-height: 28px;
|
|
--text-body: 14px;
|
|
--text-body--line-height: 22px;
|
|
--text-label: 11px;
|
|
--text-label--line-height: 14px;
|
|
|
|
--tracking-label: 0.06em;
|
|
|
|
--radius-md: 8px;
|
|
|
|
--animate-cursor-blink: ss-cursor-blink 1.06s steps(1) infinite;
|
|
|
|
@keyframes ss-cursor-blink {
|
|
0%,
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
50.01%,
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---- DARK is the default theme ---- */
|
|
:root {
|
|
--bg-base: var(--color-sleeve);
|
|
--bg-panel: var(--color-vinyl);
|
|
--bg-raised: var(--color-vinyl-2);
|
|
--bg-selection: rgb(110 157 201 / 0.22);
|
|
|
|
/* Brightened from the raw palette for web body contrast — still warm. */
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #e4dfd8;
|
|
--text-muted: #bdb4a9;
|
|
|
|
--border-hairline: rgb(138 129 120 / 0.35);
|
|
--border-default: rgb(138 129 120 / 0.7);
|
|
--border-strong: var(--color-dust);
|
|
|
|
--accent: var(--color-oxide);
|
|
/* Raised off #c1502e, which reads 3.99:1 as small text on vinyl. */
|
|
--accent-text: #cd7155;
|
|
--accent-hover: #d05f3c;
|
|
|
|
--status-warning: var(--color-tape);
|
|
--status-success: var(--color-phosphor);
|
|
--status-info: var(--color-signal);
|
|
|
|
--link: var(--color-signal);
|
|
--link-hover: #8bb4dc;
|
|
|
|
/* Terminal surface — a real terminal, darker than the page around it. */
|
|
--tm-bg: #1e1a17;
|
|
--tm-hd: #181513;
|
|
--tm-fg: #ffffff;
|
|
--tm-dim: #bdb4a9;
|
|
--tm-bd: rgb(138 129 120 / 0.32);
|
|
--tm-bd2: rgb(138 129 120 / 0.22);
|
|
--tm-bd3: rgb(138 129 120 / 0.6);
|
|
--tm-pr: #c1502e;
|
|
--tm-cm: #aba298;
|
|
--tm-ok: #7fb685;
|
|
--cmd-bg: #0e0c0a;
|
|
--tui-track: #2a2622;
|
|
--tui-bezel: #000000;
|
|
|
|
color-scheme: dark;
|
|
}
|
|
|
|
/* ---- LIGHT: base becomes manila, sleeve becomes ink, accents darken ~10% ---- */
|
|
[data-theme='light'] {
|
|
--bg-base: var(--color-manila);
|
|
--bg-panel: var(--color-manila-2);
|
|
--bg-raised: var(--color-manila-3);
|
|
--bg-selection: rgb(76 127 174 / 0.2);
|
|
|
|
/*
|
|
* Every value below clears 4.5:1 against --bg-raised, the darkest of the
|
|
* three light surfaces, so each one holds on base, panel and raised alike.
|
|
* The design's original cuts were tuned against the base only, which left
|
|
* links at 3.64:1 and the success green at 2.86:1.
|
|
*/
|
|
--text-primary: var(--color-sleeve);
|
|
--text-secondary: #574e45;
|
|
--text-muted: #645b52;
|
|
|
|
--border-hairline: rgb(20 17 15 / 0.15);
|
|
--border-default: rgb(20 17 15 / 0.28);
|
|
--border-strong: #6b6259;
|
|
|
|
--accent: #ae4728;
|
|
--accent-text: #9c3f23;
|
|
--accent-hover: #c1502e;
|
|
|
|
--status-warning: #755725;
|
|
--status-success: #3e6543;
|
|
--status-info: #395f82;
|
|
|
|
--link: #395f82;
|
|
--link-hover: #24486a;
|
|
|
|
--tm-bg: #e9e2d7;
|
|
--tm-hd: #dcd3c4;
|
|
--tm-fg: #14110f;
|
|
--tm-dim: #574e45;
|
|
--tm-bd: rgb(20 17 15 / 0.2);
|
|
--tm-bd2: rgb(20 17 15 / 0.13);
|
|
--tm-bd3: rgb(20 17 15 / 0.42);
|
|
--tm-pr: #9e3f22;
|
|
--tm-cm: #6b6259;
|
|
--tm-ok: #3f7147;
|
|
--cmd-bg: #dcd3c4;
|
|
--tui-track: #cfc5b5;
|
|
--tui-bezel: rgb(20 17 15 / 0.28);
|
|
|
|
color-scheme: light;
|
|
}
|
|
|
|
/*
|
|
* Only the cut that is actually shown gets fetched; a hidden image element
|
|
* would still be downloaded. url() is relative to this stylesheet, which
|
|
* Vite rebases against `base`, so the deploy path stays in vite.config.ts.
|
|
*/
|
|
.logo-mark {
|
|
background-image: url('/logo-ink.svg');
|
|
}
|
|
|
|
[data-theme='light'] .logo-mark {
|
|
background-image: url('/logo-light.svg');
|
|
}
|
|
|
|
/*
|
|
* The TUI panes paint from the raw palette (paper, dust, oxide…) because that
|
|
* is what the Rust client does. Those names do not flip with the theme, so
|
|
* each theme re-cuts them, scoped to the terminal's content.
|
|
*
|
|
* Dark brightens two of them the way a real emulator brightens its palette
|
|
* against black: #b33a3a reads 3.21:1 on vinyl and #c1502e 3.99:1 — fine for
|
|
* a glyph, not for the "Failed" and progress text beside it.
|
|
*/
|
|
.tui-content {
|
|
--color-alarm: #c26060;
|
|
--color-oxide: #c65f40;
|
|
}
|
|
|
|
/* Light would otherwise leave paper-on-paper: the whole set re-cuts dark. */
|
|
[data-theme='light'] .tui-content {
|
|
--color-paper: #14110f;
|
|
--color-paper-dim: #3a332c;
|
|
--color-dust: #645b52;
|
|
--color-oxide: #9e3f22;
|
|
--color-tape: #8d621f; /* 4.62:1 on manila; #8f6420 sat at 4.49 */
|
|
--color-phosphor: #3f7147;
|
|
--color-signal: #35648f;
|
|
--color-alarm: #8f2626;
|
|
}
|
|
|
|
@layer base {
|
|
html {
|
|
scroll-behavior: smooth;
|
|
/* Anchored headings clear the sticky nav instead of hiding under it. */
|
|
scroll-padding-top: 5rem;
|
|
-webkit-text-size-adjust: 100%;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg-base);
|
|
color: var(--text-primary);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-body);
|
|
line-height: var(--text-body--line-height);
|
|
/* Tabular figures EVERYWHERE — speeds, sizes, queue positions, ETAs. */
|
|
font-variant-numeric: tabular-nums;
|
|
-webkit-font-smoothing: antialiased;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
|
|
::selection {
|
|
background: var(--bg-selection);
|
|
}
|
|
|
|
/* The design system's own focus treatment, applied once globally. */
|
|
:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/*
|
|
* Reduced motion, scoped rather than nuked.
|
|
*
|
|
* A blanket `*{animation-duration:0.01ms}` also kills the transitions that
|
|
* carry meaning — a button acknowledging a copy, a card answering a hover.
|
|
* Those are state changes a reduced-motion reader still wants; what they
|
|
* asked not to see is travel and looping.
|
|
*
|
|
* So: stop the looping cursor and smooth scrolling, cap the rest at a
|
|
* duration short enough to read as instant feedback rather than movement.
|
|
* The TUI goes further and skips its opening run entirely — see
|
|
* prefersReducedMotion() in components/tui/hero-terminal.tsx.
|
|
*/
|
|
@media (prefers-reduced-motion: reduce) {
|
|
html {
|
|
scroll-behavior: auto;
|
|
}
|
|
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 80ms !important;
|
|
}
|
|
|
|
/* Looping or travelling motion has no reduced-motion equivalent. */
|
|
[class*='animate-'] {
|
|
animation: none !important;
|
|
}
|
|
}
|
|
}
|