Components

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;
}