Files
design-v3/design-system-v3.css
Joshua KingandClaude Opus 5 d9ed1898b9 Add the Extrudex Design System v3 bundle
Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the
self-hosted JetBrains Mono fonts, and interactive mockups of every screen
across three iterations. Iteration 3 is final: phone, tablet and desktop
tiers, device preview, and "empty spool weight" terminology. The build
scripts regenerate the screens byte-identically from source.

Implementation is tracked in Maestro feature UI Refresh - V3.0
(224b72a1-8d84-4970-97ea-5dafa4d8ea3b).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 11:31:50 -04:00

395 lines
20 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* CubeCraftLabs Design System v3 (proposal): Soft UI / Neumorphism
*
* Built on Design System v2.0 (http://pages.ccl/design-v2.0/, verified
* identical to Extrudex web/src/theme.css at dev 321ce0d on 2026-09-18).
*
* Structure
* 1. Font faces JetBrains Mono, self-hosted woff2, no CDN
* 2. v2.0 base scale kept verbatim, so v2 references still resolve
* 3. v3 theme tokens dark (default, as in v2.0) and light (new)
* 4. Neumorphism surfaces, raised/inset shadow pairs, intensity, radius
* 5. State tokens focus, pressed, disabled, error
* 6. Reference components used by the mockups
*
* Accessibility rules this file enforces:
* - Shadows are decoration. Every control also has a border at 3:1 or more
* against its surface (WCAG 1.4.11), so it stays visible with shadows off.
* - Focus, pressed, disabled and error each change something besides shadow
* direction: the outline, the fill plus a check glyph, a dashed border, or
* a red border plus an icon plus a message.
* - The contrast table is in design-system-v3.md.
*
* Theme selection: dark is the default because v2.0 is dark. Light applies
* when the OS prefers it and nothing is forced, or with data-theme="light" on
* <html>. data-theme="dark" forces dark.
*/
/* ── 1. Font faces ──────────────────────────────────────────────────────── */
/* The same files @fontsource/jetbrains-mono 5.3.0 ships (the app's dependency). */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 400; src: url('./fonts/jetbrains-mono-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 500; src: url('./fonts/jetbrains-mono-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 700; src: url('./fonts/jetbrains-mono-latin-700-normal.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 800; src: url('./fonts/jetbrains-mono-latin-800-normal.woff2') format('woff2'); }
/* ── 2. v2.0 base scale (unchanged) ─────────────────────────────────────── */
:root {
--ccl-charcoal: #2c2c30;
--ccl-charcoal-2: #36363a;
--ccl-charcoal-3: #1b1b1e;
--ccl-gold: #8f7ae0;
--ccl-gold-dim: #7a63cc;
--ccl-text: #f4f4f6;
--ccl-text-muted: #b2b2ba;
--ccl-border: #3d3d44;
--ccl-border-2: #4e4e57;
--ccl-green: #a9d88b;
--ccl-red: #f08b93;
--ccl-white: #ffffff;
--ccl-ink: #2a2431;
--ccl-grey: var(--ccl-charcoal);
--ccl-grey-2: var(--ccl-charcoal-2);
--ccl-grey-3: var(--ccl-charcoal-3);
--ccl-purple: var(--ccl-gold);
--ccl-purple-dim: var(--ccl-gold-dim);
--ccl-purple-wash: rgba(143, 122, 224, 0.12);
/* v3 additions to the base scale: tints derived from the v2.0 hues */
--ccl-gold-light: #a99af0; /* accent lifted to pass 4.5:1 as text on charcoal */
--ccl-violet-deep: #5b43b8; /* accent darkened to pass 4.5:1 on the light surface */
--ccl-charcoal-inset:#232327; /* between charcoal and charcoal-3: dark wells */
--ccl-amber: #e8c170; /* new: warning (v2.0 had none) */
--ccl-blue: #8fc3f0; /* new: informational / provenance */
--ccl-mist: #e4e2ea; /* light page: charcoal's hue, inverted */
--ccl-mist-2: #f1f0f5; /* light high surface */
--ccl-mist-3: #d9d6e1; /* light inset */
/* Typography, spacing and motion: unchanged from v2.0 */
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
--font-body: var(--font-mono);
--font-display: var(--font-mono);
--fw-regular: 400; --fw-medium: 500; --fw-bold: 700; --fw-display: 800;
--fs-caption: 11px; --fs-label: 12px; --fs-body: 14px; --fs-body-lg: 15px;
--fs-h3: 18px; --fs-h2: 22px; --fs-h1: 28px; --fs-display: 40px;
--lh-tight: 1.15; --lh-snug: 1.35; --lh-body: 1.7;
--tracking-wordmark: 0.02em; --tracking-caps: 0.08em; --tracking-caps-wide: 0.14em;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px;
--space-6: 24px; --space-8: 32px; --space-10: 40px; --space-14: 56px; --space-16: 64px;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--dur-fast: 100ms; --dur-base: 160ms;
--dur-press: 60ms; /* v3: pressing reacts faster than hover */
--scrollbar-width: 6px;
/* ── 4a. Radius scale (v3; replaces v2.0's "0 is the rule") ── */
--radius-none: 0px; /* tables, code, grid lines: square stays available */
--radius-xs: 4px; /* chips, swatches, inline code */
--radius-sm: 8px; /* inputs, small buttons */
--radius-md: 12px; /* buttons, list rows */
--radius-lg: 18px; /* cards, panels */
--radius-xl: 26px; /* dialogs, the app shell */
--radius-pill: 999px; /* toggles, progress tracks, badges */
/* ── 4b. Shadow geometry: distance + blur per intensity level ── */
--nm-d-1: 2px; --nm-b-1: 5px; /* level 1: chips, small controls */
--nm-d-2: 4px; --nm-b-2: 10px; /* level 2: buttons, rows */
--nm-d-3: 8px; --nm-b-3: 18px; /* level 3: cards, panels */
--nm-d-4: 14px; --nm-b-4: 30px; /* level 4: dialogs */
/* Focus ring geometry: identical in both themes */
--focus-width: 2px;
--focus-offset: 3px;
--border-control-width: 1px;
--border-error-width: 2px;
}
/* ── 3. Theme tokens: DARK (default) ───────────────────────────────────── */
:root,
:root[data-theme="dark"] {
color-scheme: dark;
/* Surfaces */
--surface-page: var(--ccl-charcoal); /* #2c2c30 */
--surface-raised: var(--ccl-charcoal); /* v3: same as page; the lift comes from the shadow */
--surface-high: var(--ccl-charcoal-2); /* #36363a dialogs, menus */
--surface-inset: var(--ccl-charcoal-inset); /* #232327 inputs, wells, progress tracks */
--surface-code: var(--ccl-charcoal-3);
/* Text */
--text-primary: var(--ccl-text); /* 12.66:1 on page */
--text-secondary: var(--ccl-text-muted); /* 6.60:1 on page */
--text-accent: var(--ccl-gold-light); /* 5.67:1 on page */
--text-on-accent: var(--ccl-ink); /* on --accent-fill */
--text-on-gold: var(--ccl-ink);
/* Accent */
--accent: var(--ccl-gold); /* brand hue, for large marks only */
--accent-hover: var(--ccl-gold-dim);
--accent-fill: var(--ccl-gold-light); /* primary button fill; ink on it: 6.14:1 */
--accent-fill-press: #9785e6;
--accent-wash: rgba(169, 154, 240, 0.10);
/* Status: the v2.0 hues plus two new ones */
--status-success: var(--ccl-green);
--status-error: var(--ccl-red);
--status-warning: var(--ccl-amber);
--status-info: var(--ccl-blue);
--wash-success: rgba(169, 216, 139, 0.10);
--wash-error: rgba(240, 139, 147, 0.10);
--wash-warning: rgba(232, 193, 112, 0.10);
--wash-info: rgba(143, 195, 240, 0.10);
/* Borders */
--border-hairline: var(--ccl-border); /* decorative card edges */
--border-strong: var(--ccl-border-2);
--border-control: #85858f; /* 1.4.11: 3.81:1 page, 4.29:1 inset, 3.29:1 high */
--focus-ring: var(--ccl-gold-light); /* 5.67:1 on page */
/* ── 4c. Neumorphism shadow pairs, DARK ──
Dark soft UI fails when the "dark" shadow is only a darker charcoal:
nothing on #2c2c30 is visibly darker than #1b1b1e. Four rules make it
work here:
a. The dark half is true black at high alpha, not a darker gray.
b. The light half is white at 4–7% alpha. It reads as sheen, not glow.
c. Raised surfaces add a 1px top rim light (inset, white 6%). The rim
stays visible even where the drop shadow merges into the background.
d. Every control has a 3:1 border anyway, so the shadow never carries
structure on its own. */
--nm-shadow-dark: rgba(0, 0, 0, 0.62);
--nm-shadow-light: rgba(255, 255, 255, 0.055);
--nm-rim: inset 0 1px 0 rgba(255, 255, 255, 0.06);
--nm-inset-dark: rgba(0, 0, 0, 0.55);
--nm-inset-light: rgba(255, 255, 255, 0.045);
--nm-raised-gradient: linear-gradient(145deg, #313136, #29292d);
}
/* ── 3. Theme tokens: LIGHT (new in v3) ────────────────────────────────── */
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
color-scheme: light;
--surface-page: var(--ccl-mist);
--surface-raised: var(--ccl-mist);
--surface-high: var(--ccl-mist-2);
--surface-inset: var(--ccl-mist-3);
--surface-code: #d2cfdb;
--text-primary: var(--ccl-ink);
--text-secondary: #55505f;
--text-accent: var(--ccl-violet-deep);
--text-on-accent: var(--ccl-white);
--text-on-gold: var(--ccl-white);
--accent: var(--ccl-gold);
--accent-hover: var(--ccl-gold-dim);
--accent-fill: var(--ccl-violet-deep);
--accent-fill-press: #4d37a3;
--accent-wash: rgba(91, 67, 184, 0.10);
--status-success: #2c6619;
--status-error: #a8202f;
--status-warning: #7c5100;
--status-info: #1d5e94;
--wash-success: rgba(44, 102, 25, 0.10);
--wash-error: rgba(168, 32, 47, 0.10);
--wash-warning: rgba(124, 81, 0, 0.10);
--wash-info: rgba(29, 94, 148, 0.10);
--border-hairline: #cfccd8;
--border-strong: #b9b5c6;
--border-control: #736e86;
--focus-ring: var(--ccl-violet-deep);
--nm-shadow-dark: rgba(88, 80, 120, 0.30);
--nm-shadow-light: rgba(255, 255, 255, 0.95);
--nm-rim: inset 0 1px 0 rgba(255, 255, 255, 0.7);
--nm-inset-dark: rgba(88, 80, 120, 0.28);
--nm-inset-light: rgba(255, 255, 255, 0.9);
--nm-raised-gradient: linear-gradient(145deg, #eceaf1, #dcd9e3);
}
}
:root[data-theme="light"] {
color-scheme: light;
--surface-page: var(--ccl-mist);
--surface-raised: var(--ccl-mist);
--surface-high: var(--ccl-mist-2);
--surface-inset: var(--ccl-mist-3);
--surface-code: #d2cfdb;
--text-primary: var(--ccl-ink);
--text-secondary: #55505f;
--text-accent: var(--ccl-violet-deep);
--text-on-accent: var(--ccl-white);
--text-on-gold: var(--ccl-white);
--accent: var(--ccl-gold);
--accent-hover: var(--ccl-gold-dim);
--accent-fill: var(--ccl-violet-deep);
--accent-fill-press: #4d37a3;
--accent-wash: rgba(91, 67, 184, 0.10);
--status-success: #2c6619;
--status-error: #a8202f;
--status-warning: #7c5100;
--status-info: #1d5e94;
--wash-success: rgba(44, 102, 25, 0.10);
--wash-error: rgba(168, 32, 47, 0.10);
--wash-warning: rgba(124, 81, 0, 0.10);
--wash-info: rgba(29, 94, 148, 0.10);
--border-hairline: #cfccd8;
--border-strong: #b9b5c6;
--border-control: #736e86;
--focus-ring: var(--ccl-violet-deep);
--nm-shadow-dark: rgba(88, 80, 120, 0.30);
--nm-shadow-light: rgba(255, 255, 255, 0.95);
--nm-rim: inset 0 1px 0 rgba(255, 255, 255, 0.7);
--nm-inset-dark: rgba(88, 80, 120, 0.28);
--nm-inset-light: rgba(255, 255, 255, 0.9);
--nm-raised-gradient: linear-gradient(145deg, #eceaf1, #dcd9e3);
}
/* ── 4d. Composed shadows: theme-agnostic, built from the pairs above ── */
:root {
--nm-raised-1: calc(var(--nm-d-1) * -1) calc(var(--nm-d-1) * -1) var(--nm-b-1) var(--nm-shadow-light),
var(--nm-d-1) var(--nm-d-1) var(--nm-b-1) var(--nm-shadow-dark), var(--nm-rim);
--nm-raised-2: calc(var(--nm-d-2) * -1) calc(var(--nm-d-2) * -1) var(--nm-b-2) var(--nm-shadow-light),
var(--nm-d-2) var(--nm-d-2) var(--nm-b-2) var(--nm-shadow-dark), var(--nm-rim);
--nm-raised-3: calc(var(--nm-d-3) * -1) calc(var(--nm-d-3) * -1) var(--nm-b-3) var(--nm-shadow-light),
var(--nm-d-3) var(--nm-d-3) var(--nm-b-3) var(--nm-shadow-dark), var(--nm-rim);
--nm-raised-4: calc(var(--nm-d-4) * -1) calc(var(--nm-d-4) * -1) var(--nm-b-4) var(--nm-shadow-light),
var(--nm-d-4) var(--nm-d-4) var(--nm-b-4) var(--nm-shadow-dark), var(--nm-rim);
--nm-inset-1: inset var(--nm-d-1) var(--nm-d-1) var(--nm-b-1) var(--nm-inset-dark),
inset calc(var(--nm-d-1) * -1) calc(var(--nm-d-1) * -1) var(--nm-b-1) var(--nm-inset-light);
--nm-inset-2: inset var(--nm-d-2) var(--nm-d-2) var(--nm-b-2) var(--nm-inset-dark),
inset calc(var(--nm-d-2) * -1) calc(var(--nm-d-2) * -1) var(--nm-b-2) var(--nm-inset-light);
--nm-flat: none;
/* v2.0 names kept so old rules still resolve */
--shadow-none: none;
--shadow-raised: var(--nm-raised-2);
/* Borders, v2.0 names */
--border-width: 1px;
--border: var(--border-width) solid var(--border-hairline);
--border-strong-rule: var(--border-width) solid var(--border-strong);
--border-accent: 1.5px solid var(--accent);
--scrollbar-thumb: var(--border-strong);
--scrollbar-thumb-hover: var(--accent);
}
/* ── 6. Reference components ────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { background: var(--surface-page); }
body {
margin: 0;
background: var(--surface-page);
color: var(--text-primary);
font-family: var(--font-body);
font-size: var(--fs-body);
line-height: var(--lh-body);
-webkit-font-smoothing: antialiased;
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
/* Surfaces */
.nm-raised { background: var(--nm-raised-gradient); box-shadow: var(--nm-raised-3); border: 1px solid var(--border-hairline); border-radius: var(--radius-lg); }
.nm-raised-sm { background: var(--surface-raised); box-shadow: var(--nm-raised-2); border: 1px solid var(--border-hairline); border-radius: var(--radius-md); }
.nm-inset { background: var(--surface-inset); box-shadow: var(--nm-inset-2); border: 1px solid var(--border-hairline); border-radius: var(--radius-md); }
.nm-dialog { background: var(--surface-high); box-shadow: var(--nm-raised-4); border: 1px solid var(--border-strong); border-radius: var(--radius-xl); }
/* Buttons
* rest raised-2 + 1px control border
* hover border takes the accent color
* pressed inset shadow + accent wash fill + accent border (+ ✓ on toggles)
* focus 2px solid outline, offset 3px, in the focus color
* disabled no shadow, dashed border, secondary text, not-allowed cursor
*/
.btn {
font: var(--fw-medium) var(--fs-body)/1 var(--font-body);
color: var(--text-primary);
background: var(--surface-raised);
border: var(--border-control-width) solid var(--border-control);
border-radius: var(--radius-md);
padding: 10px 16px;
min-height: 40px;
box-shadow: var(--nm-raised-2);
cursor: pointer;
display: inline-flex; align-items: center; gap: var(--space-2);
transition: box-shadow var(--dur-base) var(--ease-out), border-color var(--dur-fast), background var(--dur-fast);
}
.btn:hover { border-color: var(--text-accent); }
.btn:active, .btn[aria-pressed="true"], .btn.is-pressed {
box-shadow: var(--nm-inset-1);
background: var(--accent-wash);
border-color: var(--text-accent);
transition-duration: var(--dur-press);
}
.btn[aria-pressed="true"]::before, .btn.is-pressed::before { content: "✓"; font-weight: var(--fw-bold); color: var(--text-accent); }
.btn:focus-visible, .btn.is-focused, .input:focus-visible, .input.is-focused, .select:focus-visible, .select.is-focused {
outline: var(--focus-width) solid var(--focus-ring);
outline-offset: var(--focus-offset);
}
.btn:disabled, .btn.is-disabled {
box-shadow: none;
border-style: dashed;
color: var(--text-secondary);
background: transparent;
cursor: not-allowed;
}
.btn-primary { background: var(--accent-fill); color: var(--text-on-accent); border-color: var(--accent-fill); font-weight: var(--fw-bold); }
.btn-primary:hover { border-color: var(--text-primary); }
.btn-primary:active, .btn-primary.is-pressed { background: var(--accent-fill-press); color: var(--text-on-accent); border-color: var(--text-primary); }
.btn-primary.is-pressed::before { color: var(--text-on-accent); }
.btn-danger { color: var(--status-error); border-color: var(--status-error); }
.btn-sm { min-height: 32px; padding: 6px 12px; font-size: var(--fs-label); border-radius: var(--radius-sm); box-shadow: var(--nm-raised-1); }
.btn-icon { width: 40px; padding: 0; justify-content: center; }
/* Inputs: always inset, always bordered */
.input, .select {
font: var(--fw-regular) var(--fs-body)/1.4 var(--font-body);
color: var(--text-primary);
background: var(--surface-inset);
border: var(--border-control-width) solid var(--border-control);
border-radius: var(--radius-sm);
box-shadow: var(--nm-inset-1);
padding: 9px 12px;
min-height: 40px;
width: 100%;
}
.input::placeholder { color: var(--text-secondary); }
.input:disabled, .input.is-disabled, .select:disabled {
box-shadow: none; border-style: dashed; color: var(--text-secondary); background: transparent;
}
/* Error: 2px red border + icon in the label + message. Never color alone. */
.field.has-error .input, .field.has-error .select, .input.is-error {
border: var(--border-error-width) solid var(--status-error);
padding: 8px 11px;
}
.field-error {
display: flex; gap: var(--space-2); align-items: flex-start;
color: var(--status-error); font-size: var(--fs-label); line-height: var(--lh-snug); margin-top: var(--space-1);
}
.field-error::before { content: "⚠"; font-weight: var(--fw-bold); }
/* Chips / badges: text + wash + border + glyph */
.chip {
display: inline-flex; align-items: center; gap: 6px;
font-size: var(--fs-caption); font-weight: var(--fw-bold);
letter-spacing: var(--tracking-caps); text-transform: uppercase;
padding: 2px 9px; border-radius: var(--radius-pill);
border: 1px solid currentColor; line-height: 1.6;
white-space: nowrap;
}
.chip-ok { color: var(--status-success); background: var(--wash-success); }
.chip-err { color: var(--status-error); background: var(--wash-error); }
.chip-warn { color: var(--status-warning); background: var(--wash-warning); }
.chip-info { color: var(--status-info); background: var(--wash-info); }
.chip-accent { color: var(--text-accent); background: var(--accent-wash); }
.chip-neutral { color: var(--text-secondary); background: transparent; }
.chip-dashed { border-style: dashed; }
/* Progress track (inset) + fill (raised) */
.track { height: 10px; border-radius: var(--radius-pill); background: var(--surface-inset); box-shadow: var(--nm-inset-1); border: 1px solid var(--border-hairline); overflow: hidden; }
.track > span { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--text-accent); }
.track.is-low > span { background: var(--status-error); }
/* Swatch: 1–3 colors, a hairline so white/black filament shows on either theme */
.swatch { display: inline-flex; width: 26px; height: 26px; border-radius: 50%; overflow: hidden; border: 1px solid var(--border-control); box-shadow: var(--nm-raised-1); flex: none; }
.swatch > i { flex: 1; }
.swatch-sm { width: 18px; height: 18px; }
/* Text helpers */
.muted { color: var(--text-secondary); }
.accent { color: var(--text-accent); }
.caps { text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); font-size: var(--fs-caption); font-weight: var(--fw-bold); }
.num { font-variant-numeric: tabular-nums; }