Shared styles
The shared base CSS every demo builds on. Design tokens and demo-wide primitives (.demo-trigger, .visually-hidden, …) live here rather than in each demo's CSS tab, so when you lift a demo into your own app, copy this shared part alongside the demo's own CSS. Because the demos are written against theme-aware tokens, these shared styles ship both the light and dark token values — copy them and toggle data-theme on your root to switch themes, with no per-component dark CSS.
tokens/colors.css
/*
* Stimeo UI — color tokens.
*
* Theme: "breathe life into your HTML." The primary is a fresh blue-green
* (vital / living), Ruby-on-Rails red is the accent (the framework Stimeo gives
* life to), on a calm slate-neutral canvas.
*
* Two layers:
* 1. Base ramps (--vital-500, --ruby-500, --slate-700, …) — raw palette.
* 2. Semantic aliases (--color-text, --color-accent, --surface-card, …) —
* what UI code should actually reference.
*/
:root {
/* ── Primary · "Vital" blue-green (life) ─────────────────────────── */
--vital-50: #e7fbf5;
--vital-100: #c4f5e7;
--vital-200: #8fe9d2;
--vital-300: #54d8ba;
--vital-400: #21c1a3;
--vital-500: #0ea88f; /* brand primary */
--vital-600: #0a8a78;
--vital-700: #0c6e61;
--vital-800: #0f574e;
--vital-900: #103f3a;
--vital-rgb: 14, 168, 143; /* @kind other */
/* ── Accent · Ruby / Rails red ───────────────────────────────────── */
--ruby-50: #fdecec;
--ruby-100: #fbd0cf;
--ruby-200: #f5a8a5;
--ruby-300: #ec7a76;
--ruby-400: #de524d;
--ruby-500: #cc342d; /* ruby-lang / Rails red */
--ruby-600: #ad2722;
--ruby-700: #8c1f1b;
--ruby-800: #6c1916;
--ruby-900: #4d1311;
--ruby-rgb: 204, 52, 45; /* @kind other */
/* ── Neutral · Slate ─────────────────────────────────────────────── */
--white: #ffffff;
--slate-50: #f8fafc;
--slate-100: #f1f5f9;
--slate-150: #e9eef4;
--slate-200: #e2e8f0;
--slate-300: #cbd5e1;
--slate-400: #94a3b8;
--slate-500: #64748b;
--slate-600: #475569;
--slate-700: #334155;
--slate-800: #1e293b;
--slate-900: #0f172a;
--ink: #111827; /* primary text */
/* ── Functional semantics ────────────────────────────────────────── */
--leaf-500: #2f9e44; /* success — a distinct leaf green, not the teal primary */
--leaf-50: #e9f7ec;
--amber-500: #d98a09; /* warning */
--amber-50: #fbf2e0;
--danger-500: var(--ruby-500); /* danger shares the ruby accent */
--danger-50: var(--ruby-50);
--info-500: var(--vital-600); /* info uses the primary family */
--info-50: var(--vital-50);
/* ── Semantic aliases (reference these in UI) ────────────────────── */
--color-primary: var(--vital-500);
--color-primary-hover: var(--vital-600);
--color-primary-press: var(--vital-700);
--color-primary-soft: var(--vital-50);
--color-on-primary: var(--white);
--color-accent: var(--ruby-500);
--color-accent-hover: var(--ruby-600);
--color-accent-soft: var(--ruby-50);
--color-on-accent: var(--white);
--color-text: var(--ink);
--color-text-muted: var(--slate-500);
--color-text-subtle: var(--slate-400);
--color-text-inverse: var(--white);
--color-link: var(--vital-600);
--surface-page: var(--white);
--surface-subtle: var(--slate-50);
--surface-sidebar: var(--slate-50);
--surface-card: var(--white);
--surface-raised: var(--white);
--surface-code: var(--slate-900);
--on-code: var(--slate-200);
--border-default: var(--slate-200);
--border-strong: var(--slate-300);
--border-interactive: var(--slate-400);
--border-focus: var(--vital-500);
--focus-ring: 0 0 0 3px rgba(var(--vital-rgb), 0.35);
/* Signature "life" gradient — used sparingly (hero accents, marks, pulses). */
--gradient-life: linear-gradient(120deg, var(--vital-500) 0%, #34d6a8 55%, #7fe6b0 100%);
}
/*
* ── Theme: light island reset + dark overrides ──────────────────────
*
* The site chrome re-themes off `data-theme` on <html> (toggled by the
* dogfooded stimeo--theme controller). Only the *surface / text / border*
* semantics flip; the base ramps and the vital primary are theme-constant, so
* the brand reads identically in both.
*
* `[data-theme="light"]` exists so a subtree can pin itself light even while the
* site is dark — the demo stages do this, keeping the ~100 headless demos on
* their authored light canvas without per-demo dark work.
*/
[data-theme="light"] {
--surface-page: var(--white);
--surface-subtle: var(--slate-50);
--surface-sidebar: var(--slate-50);
--surface-card: var(--white);
--surface-raised: var(--white);
--color-text: var(--ink);
--color-text-muted: var(--slate-500);
--color-text-subtle: var(--slate-400);
--color-link: var(--vital-600);
--color-primary-soft: var(--vital-50);
--border-default: var(--slate-200);
--border-strong: var(--slate-300);
--border-interactive: var(--slate-400);
}
[data-theme="dark"] {
--surface-page: #0a1120;
--surface-subtle: #0f1a2e;
--surface-sidebar: #0c1424;
--surface-card: #111d33;
--surface-raised: #16233b;
--color-text: #e8eef6;
--color-text-muted: #93a3b8;
--color-text-subtle: #64748b;
--color-link: #54d8ba;
--color-primary-soft: rgba(var(--vital-rgb), 0.16);
--border-default: #21314c;
--border-strong: #2c3f5e;
--border-interactive: #3a5076;
/* Functional shades that need a brighter step to stay legible on the dark canvas. */
--leaf-500: #3fc06a;
--leaf-50: rgba(63, 192, 106, 0.14);
}
tokens/typography.css
/*
* Stimeo UI — typography tokens.
*
* Hanken Grotesk carries UI + display; JetBrains Mono carries code (the
* `data-*` / `stimeo--*` API is shown in mono everywhere). Scale is a modest
* 1.2-ish ratio tuned for dense documentation reading.
*/
:root {
/* ── Families ────────────────────────────────────────────────────── */
--font-sans: "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display: "Hanken Grotesk", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
/* ── Weights ─────────────────────────────────────────────────────── */
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--weight-extrabold: 800;
/* ── Type scale (rem) ────────────────────────────────────────────── */
--text-xs: 0.75rem; /* 12 — tags, captions, eyebrows */
--text-sm: 0.8125rem; /* 13 — labels, code, table meta */
--text-base: 0.9375rem; /* 15 — body */
--text-md: 1.0625rem; /* 17 — lead body */
--text-lg: 1.25rem; /* 20 — card titles, section h3 */
--text-xl: 1.5rem; /* 24 — h2 */
--text-2xl: 1.875rem; /* 30 — page h1 */
--text-3xl: 2.5rem; /* 40 — hero */
--text-4xl: 3.25rem; /* 52 — display hero */
/* ── Line heights ────────────────────────────────────────────────── */
--leading-tight: 1.15;
--leading-snug: 1.3;
--leading-normal: 1.6;
--leading-relaxed: 1.75;
/* ── Letter spacing ──────────────────────────────────────────────── */
--tracking-tight: -0.02em;
--tracking-snug: -0.01em;
--tracking-normal: 0;
--tracking-wide: 0.04em;
--tracking-eyebrow: 0.08em; /* uppercase eyebrows / category labels */
/* ── Semantic aliases ────────────────────────────────────────────── */
--text-body: var(--text-base);
--text-heading: var(--font-display);
--text-code: var(--font-mono);
}
tokens/spacing.css
/*
* Stimeo UI — spacing, radii, shadows, layout, motion tokens.
*/
:root {
/* ── Spacing scale (4px base) ────────────────────────────────────── */
--space-0: 0;
--space-1: 0.25rem; /* 4 */
--space-2: 0.5rem; /* 8 */
--space-3: 0.75rem; /* 12 */
--space-4: 1rem; /* 16 */
--space-5: 1.25rem; /* 20 */
--space-6: 1.5rem; /* 24 */
--space-8: 2rem; /* 32 */
--space-10: 2.5rem; /* 40 */
--space-12: 3rem; /* 48 */
--space-16: 4rem; /* 64 */
/* ── Radii ───────────────────────────────────────────────────────── */
--radius-xs: 0.25rem; /* 4 — chips, code inline */
--radius-sm: 0.375rem; /* 6 — buttons, inputs */
--radius-md: 0.5rem; /* 8 — cards, code blocks */
--radius-lg: 0.75rem; /* 12 — surfaces, demo cards */
--radius-xl: 1rem; /* 16 — hero panels */
--radius-pill: 999px; /* tags, switches, badges */
/* ── Borders ─────────────────────────────────────────────────────── */
--border-width: 1px;
--border-width-strong: 2px;
/* ── Shadows (soft, low, cool-tinted) ────────────────────────────── */
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 6px -1px rgba(15, 23, 42, 0.06), 0 2px 4px -2px rgba(15, 23, 42, 0.05);
--shadow-lg: 0 12px 24px -8px rgba(15, 23, 42, 0.12), 0 4px 8px -4px rgba(15, 23, 42, 0.06);
--shadow-xl: 0 20px 50px -12px rgba(15, 23, 42, 0.25);
/* Vital-tinted glow — for primary CTAs / "alive" emphasis only. */
--shadow-vital: 0 8px 24px -8px rgba(var(--vital-rgb), 0.45);
/* ── Layout ──────────────────────────────────────────────────────── */
--sidebar-width: 16rem;
--content-max: 56rem;
--reading-max: 42rem;
/* ── Motion (lively but controlled — Stimeo "brings HTML to life") ── */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* @kind other */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* @kind other */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* @kind other */ /* subtle overshoot */
--duration-fast: 120ms; /* @kind other */
--duration-base: 180ms; /* @kind other */
--duration-slow: 280ms; /* @kind other */
}
base/variables.css
/*
* Legacy-token bridge.
*
* The playground's presentation CSS (and the per-demo demo.css sidecars) were
* written against a small set of generic names (--bg, --fg, --accent, …). The
* brand visual layer now lives in tokens/ (vendored from docs/design-system):
* colors.css, typography.css, spacing.css. This file maps the legacy names onto
* the new semantic tokens so the whole site + every demo re-theme at once,
* without editing each consumer.
*
* Direction of travel: as CSS migrates to reference the semantic tokens directly
* (--color-text, --surface-card, --border-default, …), the matching alias below
* can be dropped. Until then this shim keeps the cascade intact.
*
* NB: --sidebar-width now comes from tokens/spacing.css (same 16rem value).
*/
:root {
/* Surfaces / canvas */
--bg: var(--surface-page);
--sidebar-bg: var(--surface-sidebar);
/* Text */
--fg: var(--color-text);
--muted: var(--color-text-muted);
/* Lines */
--border: var(--border-default);
/* Accent — the single legacy accent maps to the brand PRIMARY (vital).
Ruby is introduced deliberately in component CSS for danger / true accent. */
--accent: var(--color-primary);
--accent-rgb: var(--vital-rgb);
/* Code panels */
--code-bg: var(--surface-code);
--code-fg: var(--on-code);
/* Functional dark shades for status TEXT. The design system ships 500 + 50 for
leaf / amber / danger; demos also need an accessible dark shade for body text
on light backgrounds. Danger reuses the ruby ramp; leaf / amber are literals. */
--danger-700: var(--ruby-700);
--leaf-700: #15803d;
--amber-700: #b45309;
}
/* Dark-theme overrides for the functional status TEXT shades. The light values
above are tuned for light backgrounds; brighten them so status text stays
legible on the dark canvas. Declared here — after the :root that defines them —
so it wins the cascade (same specificity, later wins). */
[data-theme="dark"] {
--leaf-700: #4ade80;
--amber-700: #fbbf24;
--danger-700: #f87171;
}
base/demo-primitives.css
/*
* Shared demo primitives (generic looks reused across multiple demos).
*
* Each demo's demo.css holds only that component's own styling; the generic
* classes used across components are defined here exactly once. This structurally
* prevents drift from hand-copying into each demo (e.g. forgetting a button style).
*
* These are published as code on the "shared styles" page (/foundation) alongside
* variables.css, so users can copy them verbatim when taking a demo into their app.
*/
/* Demo trigger / action button (the same plain button look as the dialog etc. demos). */
.demo-trigger {
padding: 0.5rem 1rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--bg);
color: var(--fg);
font: inherit;
cursor: pointer;
transition:
border-color 0.15s ease,
color 0.15s ease;
}
.demo-trigger:hover {
border-color: var(--accent);
}
.demo-trigger:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Demo text inputs (input[type=text/search/...] / textarea). Same plain border and
radius as the trigger button, unifying the look across the input demos (input_mask /
auto_submit / nested_form / reset_before_cache, etc.) in one place. Each demo.css
adds only its state selectors (e.g. [data-mask-complete] / [data-auto-submit-pending]);
the base look stays here to prevent drift from hand-copying. */
.demo-input {
padding: 0.5rem 0.7rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--bg);
color: var(--fg);
font: inherit;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.demo-input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Card surface (background, border, radius, padding, shadow). A shared surface so
the calendar / date_range_picker demos sit in the same framing. Per-demo layout
like width / display stays in each demo.css (only the surface is centralized here,
to prevent drift from hand-copying). */
.demo-card {
background: var(--surface-card);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: 1.25rem;
box-shadow: var(--shadow-sm);
}
/* Visually hidden but still read by screen readers (used for accessible labels, etc.). */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}