@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Literata:opsz,wght@7..72,400;7..72,500;7..72,600&display=swap');

/* ---- sesebonui/styles.css ---- */
/* ============================================================
   SesebonUI — global entry point.
   Consumers link THIS file. Import list only; no rules here.
   ============================================================ */


/* ---- tokens/fonts.css ---- */
/* ============================================================
   SesebonUI · FONTS
   NOTE: webfonts are loaded from Google Fonts here as a
   convenience. For production / offline, vendor the .woff2
   binaries into assets/fonts/ and replace these @imports with
   local @font-face rules. (Flagged to the user.)
   ============================================================ */



/* ------------------------------------------------------------
   THE THIRD FAMILY — Literata, editorial only.

   The two-family rule (Space Grotesk for UI, JetBrains Mono for
   data) governs the PRODUCT surface and is unchanged. Literata
   is a third ROLE, not a third UI font: it is permitted only
   inside `.sb-prose` — essays, articles, documentation pages.
   It must never appear in a panel, a control, a label or a
   metric.

   Why a text face at all: Space Grotesk is a geometric display
   sans. It carries the 13.5px conversation prose well, and it
   gets tiring across 2,000 words at 17px. Literata is drawn for
   continuous reading, and its sturdy low-contrast strokes hold
   up on a near-black ground where a high-contrast serif blooms.

   It ships with an optical-size axis (7..72), so headings and
   body text in the same family are optically corrected rather
   than merely scaled.
   ------------------------------------------------------------ */


/* ---- tokens/colors.css ---- */
/* ============================================================
   SesebonUI · COLOR SYSTEM
   ------------------------------------------------------------
   Three layers:
   1. PRIMITIVES  — raw palette. Never referenced by components.
   2. SEMANTIC    — role-based aliases. THE CONTRACT. Components
                    read ONLY these (var(--surface-raised), etc).
   3. THEMES/MOTIFS — re-bind semantic tokens under a scope.
                    A reskin = override the semantic layer only.

   SKINNING RULE: a motif may move surfaces / text / accent /
   textures. It may NEVER move --status-alert (safety red stays
   red at Christmas). Keep that invariant and skins can't break
   the brand's meaning.
   ============================================================ */

:root {
  /* ---------- 1. PRIMITIVES (raw — do not consume directly) ---------- */
  --sb-black:      #000000;
  --sb-obsidian:   #0A0A0A;
  --sb-pit:        #070707;
  --sb-chrome:     #050505;
  --sb-line:       #1f1f1f;
  --sb-hair:       #141414;
  --sb-track:      #171717;

  --sb-white:      #fafafa;
  --sb-fog:        #f2f2f2;
  --sb-gray-1:     #8a8a8a;
  --sb-gray-2:     #5a5a5a;
  --sb-gray-3:     #3d3d3d;
  --sb-gray-4:     #2a2a2a;

  --sb-amber:      #FFB000;
  --sb-amber-ink:  #1a1200;
  --sb-red:        #FF3B30;

  --sb-jade:       #5BC8A0;
  --sb-iris:       #9D8DF1;
  --sb-sky:        #7CC4FF;

  /* ---------- 2. SEMANTIC (the contract — components read these) ---------- */

  /* Surfaces — back-to-front depth */
  --surface-base:    var(--sb-black);     /* page / canvas background        */
  --surface-raised:  var(--sb-obsidian);  /* cards, panels                   */
  --surface-sunken:  var(--sb-pit);       /* insets, rows, wells             */
  --surface-chrome:  var(--sb-chrome);    /* top bar, terminal, manifest     */
  --surface-track:   var(--sb-track);     /* progress / bar backgrounds      */

  /* Borders */
  --border-default:  var(--sb-line);      /* standard 1px hairline           */
  --border-subtle:   var(--sb-hair);      /* inner dividers, quieter          */

  /* Text — descending emphasis */
  --text-primary:    var(--sb-white);     /* headings, hero values            */
  --text-body:       var(--sb-fog);       /* default copy                     */
  --text-secondary:  var(--sb-gray-1);    /* supporting / values              */
  --text-tertiary:   var(--sb-gray-2);    /* micro-labels, captions           */
  --text-ghost:      var(--sb-gray-3);    /* deprioritized / placeholder      */

  /* Accent — the single brand interactive color */
  --accent:          var(--sb-amber);
  --accent-ink:      var(--sb-amber-ink); /* text/icon on an accent fill      */
  --accent-wash:     rgba(255,176,0,.08); /* faint accent fill                */
  --accent-line:     rgba(255,176,0,.18); /* accent at hairline strength      */
  --accent-dim:      #6b5300;             /* accent at ~40% luma — journal step index (SPEC §6) */

  /* Status — INVARIANT across motifs. Do not re-theme. */
  --status-alert:    var(--sb-red);       /* threats, overages, errors        */
  --status-active:   var(--sb-amber);     /* on / live / armed-ok             */
  --status-idle:     var(--sb-gray-2);    /* off / clear / passive            */
  --alert-wash:      rgba(255,59,48,.08); /* faint alert fill — mirror of --accent-wash (CodeBlock diff removed-row, Toast alert) */

  /* Data / category encodings (charts, tags) */
  --data-pos:        var(--sb-amber);
  --data-warn:       var(--sb-red);
  --data-neutral:    var(--sb-fog);
  --cat-health:      var(--sb-jade);
  --cat-financial:   var(--sb-amber);
  --cat-preference:  var(--sb-iris);
  --cat-biographical:var(--sb-sky);
  --cat-general:     var(--sb-gray-1);

  /* Longform / code / mermaid (conversation surface — SPEC §3/§6) */
  --code-string:         #e8c15a;                /* code literals / string amber   */
  --mermaid-node-fill:   var(--surface-raised);
  --mermaid-node-stroke: var(--border-default);
  --mermaid-text:        var(--text-secondary);
  --mermaid-decision:    var(--accent);          /* decision nodes                 */
  --mermaid-alert:       var(--status-alert);    /* alert paths                    */
  --mermaid-edge:        var(--text-tertiary);

  /* Decoration (motif texture/ambient hooks — empty by default) */
  --motif-texture:   none;
  --motif-ambient:   transparent;
}

/* ============================================================
   THEME: graphite — a cool-neutral alternate base.
   Bind with <html data-theme="graphite"> or [data-theme].
   ============================================================ */
[data-theme="graphite"] {
  --surface-base:    #0c0d10;
  --surface-raised:  #15171c;
  --surface-sunken:  #101116;
  --surface-chrome:  #0f1014;
  --border-default:  #2a2d35;
  --border-subtle:   #20232a;
  --text-primary:    #ffffff;
  --text-body:       #eef1f5;
  --text-secondary:  #9aa0ab;
  --text-tertiary:   #646b76;
  --text-ghost:      #454b55;
  --status-alert:    #FF5247;  /* still red — invariant role honored */
}


/* ---- tokens/typography.css ---- */
/* ============================================================
   SesebonUI · TYPOGRAPHY
   Two families, strict division of labor:
   - UI / display  → Space Grotesk
   - ALL numbers, metrics, logs, micro-labels, code → JetBrains Mono
   Monospace for data is a load-bearing brand rule, not a vibe.
   ============================================================ */

:root {
  /* Families */
  --font-ui:   'Space Grotesk', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;

  /* Weights */
  --fw-regular: 400;
  --fw-medium:  500;
  --fw-semibold:600;
  --fw-bold:    700;

  /* Fluid type scale (clamp: min, vw, max) */
  --fs-micro:  10px;                          /* mono micro-labels          */
  --fs-label:  clamp(10px, .7vw, 11px);       /* field / section labels      */
  --fs-caption:clamp(9px, .65vw, 10px);
  --fs-body:   clamp(12px, .9vw, 13px);
  --fs-value:  clamp(18px, 1.5vw, 21px);      /* standard metric             */
  --fs-value-lg: clamp(22px, 2.2vw, 28px);
  --fs-title:  clamp(14px, 1.2vw, 17px);      /* panel titles                */
  --fs-hero:   clamp(28px, 3vw, 40px);        /* hero metric                 */
  --fs-display:clamp(15px, 1.4vw, 18px);      /* wordmark                    */

  /* Line heights */
  --lh-tight:  1;
  --lh-snug:   1.2;
  --lh-body:   1.5;

  /* Letter-spacing — mono labels run wide; this is signature */
  --ls-label:  .14em;
  --ls-micro:  .16em;
  --ls-wide:   .1em;
  --ls-normal: .01em;

  /* Longform / conversation surface (added for the ConversationLog — SPEC §6) */
  --fs-prose:  13.5px;   /* assistant prose body                              */
  --lh-prose:  1.65;     /* prose line-height                                 */
  --fw-light:  300;      /* display numerals ONLY (the Zen clock) — nowhere else */
}

/* ============================================================
   EDITORIAL TIER — long-form pages (essays, articles, docs).

   DISTINCT FROM --fs-prose. There are two long-form contexts on
   this system and conflating them breaks one of them:

     CONVERSATION prose  --fs-prose 13.5px, Space Grotesk.
                         Assistant replies inside a panel. Sized
                         to sit next to UI chrome. UNCHANGED.

     EDITORIAL prose     the tokens below, Literata. A full page
                         whose only job is to be read. Sized for
                         continuous reading, not for density.

   The brand's density rule is deliberately suspended here and
   nowhere else. A dashboard earns its tightness because the
   reader is scanning; an essay is the opposite task.
   ============================================================ */

:root {
  /* The editorial family. Swap this one token to re-cast every
     long-form page on the system. */
  --font-read:      'Literata', Georgia, 'Times New Roman', serif;

  /* Reading scale. Fixed, not fluid — a text column should not
     resize with the viewport once it has reached its measure. */
  --fs-read:        17px;
  --fs-read-sm:     14.5px;   /* captions, table cells, figure labels */
  --fs-read-lead:   19.5px;   /* standfirst / summary                 */
  --lh-read:        1.72;

  /* Editorial headings. These are Space Grotesk, not Literata:
     the heading register stays with the UI family so a page
     still reads as this system's page. Only BODY changes face. */
  --fs-read-h1:     clamp(2rem, 5vw, 3.1rem);
  --fs-read-h2:     clamp(1.35rem, 2.8vw, 1.7rem);
  --fs-read-h3:     1.12rem;

  /* Measure — the most load-bearing number on a text page. */
  --measure-read:   66ch;
  --measure-page:   860px;
}


/* ---- tokens/spacing.css ---- */
/* ============================================================
   SesebonUI · SPACING & LAYOUT
   4px base grid. Brutalist density — tight, deliberate.
   These are brand-permanent: motifs never change spacing.
   ============================================================ */

:root {
  --space-0:  0px;
  --space-1:  4px;
  --space-2:  6px;
  --space-3:  8px;
  --space-4:  10px;
  --space-5:  12px;
  --space-6:  14px;
  --space-7:  16px;
  --space-8:  18px;
  --space-9:  20px;
  --space-10: 24px;

  /* Component rhythm */
  --pad-card:      18px 20px;   /* panel padding              */
  --pad-cell:      12px 13px;   /* sub-cell padding           */
  --pad-row:       10px 12px;   /* list row padding           */
  --gap-board:     14px;        /* between panels             */
  --gap-tight:     10px;        /* within a panel             */

  /* Canvas */
  --canvas-max:    1560px;
  --canvas-pad:    16px;

  /* Chrome heights */
  --h-topbar:      46px;
  --h-manifest:    30px;
  --h-input:       42px;

  /* Overlays */
  --drawer-w:      420px;       /* right-edge Drawer fixed width          */
  --toast-max:     3;           /* max toasts visible; older collapse +n  */
}

/* ------------------------------------------------------------
   EDITORIAL RHYTHM — vertical spacing for long-form pages.
   Built from the same 4px grid; the grid is brand-permanent and
   this does not change it. These are larger steps than the
   component rhythm above because a reading column needs air
   that a dashboard panel does not.
   ------------------------------------------------------------ */
:root {
  --stack-para:    24px;   /* paragraph to paragraph            */
  --stack-block:   36px;   /* body to a heading / figure / rule  */
  --stack-section: 72px;   /* major section break                */
  --pad-read:      28px 32px;  /* figure / callout padding       */
}


/* ---- tokens/effects.css ---- */
/* ============================================================
   SesebonUI · EFFECTS — borders, radius, shadows, motion
   Defining trait: ZERO radius, hairline borders, (almost) no
   shadow. Depth comes from surface tints + 1px lines, not blur.
   ============================================================ */

:root {
  /* Border width + radius (radius is 0 everywhere — on purpose) */
  --bw:        1px;
  --radius:    0px;

  /* The one sanctioned shadow — floating overlays only (payload
     inspector, dialogs). Flat surfaces get NO shadow. */
  --shadow-overlay: 0 8px 40px rgba(0,0,0,.6);

  /* Emphasis ring — replaces shadow for "this panel is focused" */
  --ring-accent: inset 0 0 0 1px var(--accent);
  --ring-alert:  inset 0 0 0 1px var(--status-alert);

  /* Motion — fast, mechanical, cubic. No bounce, no spring. */
  --ease:        cubic-bezier(.4, 0, .1, 1);
  --dur-fast:    .18s;
  --dur-med:     .35s;
  --dur-layout:  .55s;   /* column re-composition               */

  /* Scanline / striped placeholder fills (camera + media tiles) */
  --fill-stripe: repeating-linear-gradient(135deg, #0d0d0d 0, #0d0d0d 7px, #070707 7px, #070707 14px);
  --fill-scan:   repeating-linear-gradient(0deg, rgba(255,255,255,.02) 0, rgba(255,255,255,.02) 1px, transparent 1px, transparent 3px);
}

/* Keyframes shipped with the system */
@keyframes sb-blink { 0%,49%{opacity:1} 50%,100%{opacity:0} }
@keyframes sb-pulse { 0%,100%{opacity:1} 50%{opacity:.25} }
@keyframes sb-snow  { 0%{transform:translateY(-10%)} 100%{transform:translateY(110%)} }
@keyframes sb-fade  { from{opacity:0} to{opacity:1} }
@keyframes sb-modal { from{opacity:0; transform:translateY(6px) scale(.99)} to{opacity:1; transform:none} }
@keyframes sb-spin  { to{transform:rotate(360deg)} }
@keyframes sb-tick-sweep { 0%,100%{opacity:.12} 8%{opacity:1} 32%{opacity:.12} }  /* Mark thinking sweep */
@keyframes sb-tts-bar    { 0%,100%{transform:scaleY(.25)} 50%{transform:scaleY(1)} }  /* TTS level meter */
@keyframes sb-deplete    { from{transform:scaleX(1)} to{transform:scaleX(0)} }  /* Toast auto-dismiss countdown line */
@keyframes sb-drawer     { from{opacity:0; transform:translateX(14px)} to{opacity:1; transform:none} }  /* Drawer slide-in */


/* Patterns that ship as CSS rather than as a React component,
   because their consumers are documents, not applications. */

/* ---- patterns/prose.css ---- */
/* ============================================================
   SesebonUI · PROSE — the editorial long-form surface.

   Scope: everything inside `.sb-prose`. This is the ONLY place
   --font-read is permitted. Panels, controls, labels and metrics
   keep the two-family rule (Space Grotesk + JetBrains Mono).

   Relationship to the `Markdown` component (Conversation-Surface
   SPEC §3): same rule set, different context. `Markdown` renders
   assistant replies INSIDE a panel at --fs-prose. This renders a
   PAGE at --fs-read. The rules below are inherited from that spec
   deliberately, so a reader moving between a chat reply and an
   article sees one system, not two:

     - h1–h3 are Space Grotesk. h4–h6 collapse to the mono label
       register: past h3 the register shifts from heading to label.
     - Blockquote is a 2px left rule and italic, with NO accent.
       Accent is interaction, not decoration.
     - Lists use en-dash and zero-padded ordinals, not bullets.
     - Tables get the full hairline grid, mono uppercase headers,
       and right-aligned numeric columns.

   KNOWN DIVERGENCE from SPEC §3, stated rather than hidden:
   the spec swaps every numeral to mono at −1px. That is a
   per-character substitution, which the React `Markdown`
   component does by wrapping spans. CSS cannot do it per
   character without a unicode-range @font-face pinned to a
   font binary, and the Google Fonts URLs are not stable enough
   to pin. So mono is applied here wherever numerals are DATA —
   tables, figures, dates, metrics, code — and prose numerals
   stay in the reading face with tabular-nums. If the numerals
   are ever vendored locally, the unicode-range trick closes
   this gap and this note comes out.
   ============================================================ */

.sb-prose {
  font-family: var(--font-read);
  font-size: var(--fs-read);
  line-height: var(--lh-read);
  color: var(--text-body);
  max-width: var(--measure-read);
}

.sb-prose > * { max-width: var(--measure-read); }

.sb-prose p { margin: 0 0 var(--stack-para); }

.sb-prose > :first-child { margin-top: 0; }
.sb-prose > :last-child  { margin-bottom: 0; }

/* ---------- headings: h1–h3 stay in the UI family ---------- */

.sb-prose h1,
.sb-prose h2,
.sb-prose h3 {
  font-family: var(--font-ui);
  color: var(--text-primary);
  text-wrap: balance;
}

.sb-prose h1 {
  font-size: var(--fs-read-h1);
  font-weight: var(--fw-bold);
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin: 0 0 var(--stack-block);
}

.sb-prose h2 {
  font-size: var(--fs-read-h2);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  letter-spacing: -0.01em;
  margin: var(--stack-section) 0 var(--stack-block);
}

.sb-prose h3 {
  font-size: var(--fs-read-h3);
  font-weight: var(--fw-semibold);
  line-height: 1.3;
  margin: var(--stack-block) 0 var(--space-7);
}

/* Past h3 the register shifts from heading to label (SPEC §3). */
.sb-prose h4,
.sb-prose h5,
.sb-prose h6 {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: var(--stack-block) 0 var(--space-5);
}

/* ---------- inline ---------- */

.sb-prose strong { color: var(--text-primary); font-weight: var(--fw-semibold); }
.sb-prose em     { font-style: italic; }

.sb-prose a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.sb-prose a:hover { background: var(--accent-wash); }

/* Data is mono. Load-bearing brand rule, not a vibe. */
.sb-prose code,
.sb-prose time,
.sb-prose .sb-num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.sb-prose code {
  font-size: 0.85em;
  color: var(--code-string);
  background: var(--surface-sunken);
  border: var(--bw) solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 1px 5px;
}

/* Prose numerals do not change face, but they do align. */
.sb-prose p, .sb-prose li { font-variant-numeric: tabular-nums; }

/* ---------- lists: en-dash and zero-padded ordinals ---------- */

.sb-prose ul,
.sb-prose ol {
  margin: 0 0 var(--stack-para);
  padding-left: var(--space-9);
}
.sb-prose li { margin-bottom: var(--space-5); }
.sb-prose li::marker {
  color: var(--text-tertiary);
  font-family: var(--font-mono);
  font-size: 0.85em;
}
.sb-prose ul { list-style: none; padding-left: var(--space-10); }
.sb-prose ul > li { position: relative; }
.sb-prose ul > li::before {
  content: "–";
  position: absolute;
  left: calc(var(--space-10) * -1);
  color: var(--text-tertiary);
  font-family: var(--font-mono);
}
.sb-prose ol { list-style: decimal-leading-zero; }

.sb-prose li > ul,
.sb-prose li > ol { margin-top: var(--space-5); margin-bottom: 0; }

/* ---------- blockquote: 2px rule, italic, NO accent ---------- */

.sb-prose blockquote {
  margin: var(--stack-block) 0;
  padding: var(--space-5) 0 var(--space-5) var(--space-9);
  border-left: 2px solid var(--border-default);
  font-style: italic;
  font-size: var(--fs-read-lead);
  line-height: 1.45;
  color: var(--text-primary);
}
.sb-prose blockquote p:last-child { margin-bottom: 0; }

/* ---------- rules ---------- */

.sb-prose hr {
  border: 0;
  border-top: var(--bw) solid var(--border-subtle);
  margin: var(--stack-section) 0;
  max-width: var(--measure-read);
}

/* ---------- code blocks ---------- */

.sb-prose pre {
  font-family: var(--font-mono);
  font-size: var(--fs-read-sm);
  line-height: 1.6;
  color: var(--text-body);
  background: var(--surface-sunken);
  border: var(--bw) solid var(--border-default);
  border-radius: var(--radius);
  padding: var(--space-9) var(--space-10);
  overflow-x: auto;
  margin: 0 0 var(--stack-para);
  max-width: 100%;
}
.sb-prose pre code {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font-size: inherit;
}

/* ---------- tables: hairline grid, mono uppercase heads ---------- */

.sb-prose .sb-tablewrap {
  overflow-x: auto;
  margin: 0 0 var(--stack-para);
  max-width: 100%;
  border: var(--bw) solid var(--border-default);
}
.sb-prose table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: var(--fs-read-sm);
}
.sb-prose thead th {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-tertiary);
  text-align: left;
  background: var(--surface-sunken);
  border-bottom: var(--bw) solid var(--border-default);
  padding: var(--pad-row);
  white-space: nowrap;
}
.sb-prose tbody td {
  padding: var(--pad-row);
  border-bottom: var(--bw) solid var(--border-subtle);
  border-right: var(--bw) solid var(--border-subtle);
  color: var(--text-secondary);
  vertical-align: top;
}
.sb-prose tbody td:last-child,
.sb-prose thead th:last-child { border-right: 0; }
.sb-prose tbody tr:last-child td { border-bottom: 0; }

/* Numeric columns are mono and right-aligned. Mark with
   <td class="sb-num"> — or let a generator detect it. */
.sb-prose td.sb-num,
.sb-prose th.sb-num {
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--text-body);
  white-space: nowrap;
}

/* ---------- media ---------- */

.sb-prose img {
  max-width: 100%;
  height: auto;
  border: var(--bw) solid var(--border-default);
}
.sb-prose figure { margin: 0 0 var(--stack-para); max-width: 100%; }
.sb-prose figcaption {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  letter-spacing: var(--ls-wide);
  color: var(--text-ghost);
  margin-top: var(--space-3);
}

/* ---------- lead paragraph ---------- */

.sb-prose .sb-lead {
  font-size: var(--fs-read-lead);
  line-height: 1.5;
  color: var(--text-secondary);
}

@media (max-width: 640px) {
  .sb-prose { font-size: 16px; }
  .sb-prose h2 { margin-top: var(--stack-block); }
}

/* ============================================================
   ICONS — see components/core/icons.svg for the symbol set and
   the reasoning behind geometric glyphs over brand logos.
   Stroke inherits currentColor, so an icon takes the colour of
   whatever text register it sits in, and every semantic token
   applies to it without a per-icon rule.
   ============================================================ */

.sb-icon {
  width: 20px;
  height: 20px;
  flex: none;
  display: inline-block;
  vertical-align: middle;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: butt;
  stroke-linejoin: miter;
}
.sb-icon-sm { width: 16px; height: 16px; }

/* The inlined symbol sprite. Hidden by class, never by an inline style —
   a CSP without 'unsafe-inline' blocks style attributes outright. */
.sb-icon-sprite { display: none; }


/* The pixel owl. Base rule only — the per-animation rules need a
   URL, which differs per consumer, so each generates them from
   assets/sprite/sprites.json. See the file header for the
   contract a generator must honour. */

/* ---- patterns/sprite.css ---- */
/* ============================================================
   SesebonUI · SPRITE — the pixel owl.

   The owl is the system's one figurative asset. It signifies
   wisdom, which is why it is allowed to exist at all in a
   language that otherwise bans decorative illustration: it
   carries a meaning the geometry cannot.

   IT IS NOT THE BRAND MARK. Mark stays the identity — top bar,
   favicon, finished and cancelled states. That split is a
   LICENCE CONSTRAINT, not a preference: the source art's licence
   permits commercial use in unlimited projects with no
   attribution, but explicitly forbids building a logo or a
   trademark from it. The owl may animate a state. It may never
   be the wordmark, the favicon, or the face of the product.
   The same licence forbids AI/ML training use — keep these
   masks out of any model pipeline, including a visual-memory
   capture of a UI that renders them.

   ------------------------------------------------------------
   WHY MASKS, NOT PICTURES

   Every file in assets/sprite/ is a 1-bit ALPHA mask. It holds
   no palette. The element supplies the colour, so a sprite is
   amber under the default theme and frost under motifs/winter
   from ONE asset — it re-skins exactly the way Mark does. A
   coloured PNG would have been the single visual in the system
   that a motif swap could not reach.

   ------------------------------------------------------------
   WHAT LIVES HERE vs WHAT A CONSUMER GENERATES

   This file holds only what needs no URL. The per-animation
   rules do need one, and the right URL differs per consumer —
   a content-hashed path from a static build, a bundler import
   in the app — so each consumer generates them from
   assets/sprite/sprites.json. See site/build.mjs for a
   reference implementation.

   A generator MUST honour three things:

   1. Emit the natural size as `:where(.sprite.<name>)`.
      :where() contributes ZERO specificity, so a placement rule
      like `.herofig .sprite` can override it. Declared normally
      it ties with `.sprite.<name>` at (0,2,0) and wins on source
      order, silently killing every size override downstream.

   2. Derive width, height, mask-size and the keyframe travel
      from --sprite-cell, never from fixed pixels. One override
      then resizes a sprite correctly. Halving the 64px cell
      lands back on the art's native 32px, so scaled-down
      sprites stay pixel-exact.

   3. Emit `.once` AFTER every per-animation rule. Those rules
      set the `animation` SHORTHAND, which resets
      iteration-count and fill-mode — so `.sprite.once` declared
      earlier ties at (0,2,0) and loses, and a one-shot death
      animation loops forever.

   Frames advance on mask-position in PIXELS, never percentages.
   At mask-size 400% a 4-frame sheet under steps(4) lands on
   0/25/50/75%, which is BETWEEN cells and renders as two half
   characters.

   ------------------------------------------------------------
   MOTION

   Consumers are responsible for honouring
   prefers-reduced-motion. Freezing the animation leaves a sprite
   at mask-position 0 — frame one, a complete pose rather than a
   broken cell — so `animation: none` is a safe reduction.
   ============================================================ */

.sprite {
  display: block;
  flex: none;
  background-color: var(--accent);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* Belt and braces. The masks are pre-scaled 2x with nearest
     neighbour at generation time precisely because engines
     disagree about honouring image-rendering for MASK images,
     and a smoothed pixel sprite reads as a bug, not a style. */
  image-rendering: pixelated;
}


/* Motifs (opt-in skins). Activate with e.g. <html data-motif="winter">. */

/* ---- motifs/winter.css ---- */
/* ============================================================
   SesebonUI · MOTIF — Winter
   ------------------------------------------------------------
   Proof that a seasonal reskin is a ONE-FILE token drop.
   Activate: <html data-motif="winter">
   Rules honored:
   - Only surfaces / text / accent / decoration are re-bound.
   - --status-alert is NOT touched (safety red stays red).
   - Spacing, radius, type are untouched (structure is permanent).
   ============================================================ */

[data-motif="winter"] {
  /* Cool-shifted surfaces */
  --surface-base:    #04070b;
  --surface-raised:  #0a0f15;
  --surface-sunken:  #070b10;
  --surface-chrome:  #05080c;
  --border-default:  #16202b;
  --border-subtle:   #101822;

  /* Frost accent replaces amber */
  --accent:          #8FD3FF;
  --accent-ink:      #04141f;
  --accent-wash:     rgba(143,211,255,.08);
  --accent-line:     rgba(143,211,255,.20);

  /* Status roles that ALSO read accent shift with it… */
  --status-active:   #8FD3FF;
  /* …but the alert role stays its invariant red. (Intentionally absent.) */

  /* Decoration hooks — host reads these to mount ambient layers */
  --motif-ambient:   rgba(143,211,255,.06);
}


/* ---- site ---- */
/* ============================================================
   SESEBON SITE — layout only.

   Everything typographic now lives in the design system: the
   editorial tier in tokens/typography.css, the reading surface
   in components/patterns/prose.css. This file holds ONLY what
   is specific to this site's page furniture — chrome, hero,
   cards, listings, CV rows.

   If a rule here starts describing how text reads rather than
   where a box sits, it belongs upstream in SesebonUI instead.

   Rules inherited and kept:
   - Semantic tokens only. No --sb-* primitive is touched.
   - --status-alert is never re-bound.
   - Accent is interaction, not decoration. It appears on links,
     hover states and DATA encodings (--data-*), never as an
     ornamental rule or marker.
   - Radius is 0 and shadows are absent. Depth is surface tint
     plus a 1px line.
   ============================================================ */

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface-base);
  color: var(--text-body);
  font-family: var(--font-ui);
  font-size: var(--fs-read-sm);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

.skip { position: absolute; left: -9999px; top: 0; background: var(--accent); color: var(--accent-ink); padding: var(--space-3) var(--space-7); font-family: var(--font-mono); font-size: var(--fs-label); z-index: 10; }
.skip:focus { left: 0; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------- shared atoms ---------- */

.mono-label {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.mono-label.accent { color: var(--accent); }
.mono-label.muted  { color: var(--text-ghost); }

a { color: var(--accent); text-underline-offset: 3px; }

/* ---------- chrome ---------- */

.topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-7);
  padding: 0 var(--canvas-pad);
  min-height: var(--h-topbar);
  border-bottom: var(--bw) solid var(--border-default);
  background: var(--surface-chrome);
  position: sticky; top: 0; z-index: 5;
}

.wordmark {
  font-family: var(--font-mono);
  font-size: var(--fs-display);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  color: var(--text-primary);
  text-decoration: none;
}
.wordmark::before {
  content: ""; display: inline-block;
  width: 9px; height: 9px;
  margin-right: var(--space-3);
  border: 2px solid var(--accent);
}

.topbar nav { display: flex; gap: var(--space-9); }
.topbar nav a {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-secondary);
  text-decoration: none;
}
.topbar nav a:hover, .topbar nav a[aria-current="page"] { color: var(--accent); }

main {
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: 0 var(--canvas-pad) var(--stack-section);
}

.sitefoot {
  max-width: var(--measure-page);
  margin: 0 auto;
  padding: var(--space-9) var(--canvas-pad) var(--space-10);
  border-top: var(--bw) solid var(--border-subtle);
  display: flex; flex-wrap: wrap;
  gap: var(--space-5) var(--space-10);
  justify-content: space-between;
}

/* ---------- hero / page heads ---------- */

.hero, .pagehead {
  padding: var(--stack-section) 0 var(--stack-block);
  border-bottom: var(--bw) solid var(--border-subtle);
  margin-bottom: var(--stack-block);
}

.hero h1, .pagehead h1 {
  font-family: var(--font-ui);
  font-size: var(--fs-read-h1);
  font-weight: var(--fw-bold);
  line-height: 1.04;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin: var(--space-7) 0 var(--space-9);
  max-width: 20ch;
  text-wrap: balance;
}

.hero-sub {
  font-family: var(--font-read);
  font-size: var(--fs-read-lead);
  line-height: 1.5;
  color: var(--text-secondary);
  max-width: 52ch;
  margin: 0;
}

.hero-meta { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-10); margin-top: var(--space-10); }

/* ---------- bands ---------- */

.band { padding: var(--stack-block) 0; }
.band > h2 { margin: 0 0 var(--space-9); }

.more { margin: var(--space-9) 0 0; }
.more a, .postnav a {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  text-decoration: none;
}
.more a:hover, .postnav a:hover { text-decoration: underline; }

/* ---------- work cards ---------- */

.workgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--gap-board); }

.workcard {
  background: var(--surface-raised);
  border: var(--bw) solid var(--border-default);
  padding: var(--pad-card);
  display: flex; flex-direction: column; gap: var(--space-5);
}
/* baseline, not center: a title that wraps to three lines would otherwise float
   the owl halfway down it. The sprite stands on the title's FIRST line. (This
   rule used to hold the status pill here too; the pill now lives in the tag row
   with the other chips, so only the sprite needs aligning.) */
.workcard header { display: flex; align-items: baseline; gap: var(--space-5); }
.workcard h3 { margin: 0; font-size: var(--fs-read-h3); font-weight: var(--fw-semibold); color: var(--text-primary); }
.workcard p { margin: 0; font-family: var(--font-read); font-size: var(--fs-read-sm); line-height: 1.55; color: var(--text-secondary); }

.pill {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  padding: 3px var(--space-3);
  border: var(--bw) solid var(--border-default);
  color: var(--text-tertiary);
  white-space: nowrap;
}
.pill.live    { color: var(--status-active); border-color: var(--accent-line); background: var(--accent-wash); }
.pill.build   { color: var(--cat-preference); }
.pill.shipped { color: var(--cat-health); }

.tags { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; padding: 0; }
/* :not(.pill) so the status chip keeps its own border and colour: the pill now
   shares this row, and a bare `.tags li` outranks `.pill` on specificity, which
   would have repainted every status the same neutral grey. */
.tags li:not(.pill) { border: var(--bw) solid var(--border-subtle); padding: 2px var(--space-3); }

/* ---------- post lists ---------- */

.postlist { list-style: none; margin: 0; padding: 0; }
.postlist li { border-top: var(--bw) solid var(--border-subtle); }
.postlist li:last-child { border-bottom: var(--bw) solid var(--border-subtle); }

.postlist a {
  display: grid; grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: var(--space-3) var(--space-9);
  align-items: baseline;
  padding: var(--space-9) 0;
  text-decoration: none; color: inherit;
}
.postlist a:hover .postlink-t { color: var(--accent); }

.postlink-t { font-size: var(--fs-read-lead); font-weight: var(--fw-semibold); color: var(--text-primary); line-height: 1.3; }
.postlink-s { grid-column: 2; font-family: var(--font-read); font-size: var(--fs-read-sm); color: var(--text-tertiary); line-height: 1.5; max-width: 60ch; }

/* ---------- link list ---------- */

.linkgroups { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--gap-board); }
.linkgroup > h3 { margin: 0 0 var(--space-5); }
.linklist { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 2px; }
.extlink { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-7); background: var(--surface-sunken); border: var(--bw) solid var(--border-subtle); text-decoration: none; }
.extlink:hover { border-color: var(--accent-line); background: var(--accent-wash); }
.extlink-v { font-family: var(--font-mono); font-size: var(--fs-read-sm); color: var(--text-body); overflow-wrap: anywhere; }

/* ---------- post head ---------- */

.posthead {
  padding: var(--stack-section) 0 var(--stack-block);
  border-bottom: var(--bw) solid var(--border-subtle);
  margin-bottom: var(--stack-block);
}
.posthead h1 {
  font-family: var(--font-ui);
  font-size: var(--fs-read-h1);
  font-weight: var(--fw-bold);
  line-height: 1.06;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin: var(--space-7) 0 var(--space-9);
  max-width: 22ch;
  text-wrap: balance;
}

.postnav { margin-top: var(--stack-block); padding-top: var(--space-9); border-top: var(--bw) solid var(--border-subtle); }

/* ---------- CV ---------- */

.cvsection { padding: var(--stack-block) 0; border-top: var(--bw) solid var(--border-subtle); }
.cvsection > h2 { margin: 0 0 var(--space-10); }

.role { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); gap: var(--space-5) var(--space-10); padding-bottom: var(--stack-block); }
.role:last-child { padding-bottom: 0; }
.role .when { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-wide); color: var(--text-tertiary); line-height: 1.5; }
.role h3 { margin: 0 0 var(--space-2); font-size: var(--fs-read-h3); font-weight: var(--fw-semibold); color: var(--text-primary); }
.role .where { font-family: var(--font-mono); font-size: var(--fs-caption); letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--accent); display: block; margin-bottom: var(--space-5); }
.role ul { margin: 0; padding-left: var(--space-9); font-family: var(--font-read); font-size: var(--fs-read-sm); line-height: 1.6; color: var(--text-secondary); }
.role li { margin-bottom: var(--space-3); }
.role li::marker { color: var(--text-ghost); }
.role > div > p { margin: 0 0 var(--space-5); font-family: var(--font-read); font-size: var(--fs-read-sm); color: var(--text-secondary); }

.skillgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 2px; }
.skillcard { background: var(--surface-sunken); border: var(--bw) solid var(--border-subtle); padding: var(--space-7); }
.skillcard h3 { margin: 0 0 var(--space-5); }
.skillcard ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.skillcard li { font-family: var(--font-mono); font-size: var(--fs-caption); letter-spacing: var(--ls-normal); color: var(--text-secondary); border: var(--bw) solid var(--border-subtle); padding: 3px var(--space-3); }

/* ---------- figures embedded in posts (data encodings) ---------- */

.sb-prose .figure { background: var(--surface-raised); border: var(--bw) solid var(--border-default); padding: var(--pad-read); margin: 0 0 var(--stack-para); max-width: 100%; }
.sb-prose .figure .fig-t { font-family: var(--font-ui); font-size: var(--fs-read-sm); font-weight: var(--fw-semibold); color: var(--text-primary); margin: 0 0 var(--space-2); }
.sb-prose .figure .fig-s { font-family: var(--font-ui); font-size: var(--fs-read-sm); color: var(--text-tertiary); margin: 0 0 var(--space-9); max-width: none; }
.sb-prose .figure .frow { display: grid; grid-template-columns: minmax(90px, 150px) 1fr 3rem; align-items: center; gap: var(--space-5); margin-bottom: var(--space-3); }
.sb-prose .figure .flbl { font-family: var(--font-ui); font-size: var(--fs-read-sm); line-height: 1.25; color: var(--text-secondary); }
.sb-prose .figure .flbl em { font-style: normal; display: block; font-family: var(--font-mono); font-size: var(--fs-caption); color: var(--text-ghost); letter-spacing: var(--ls-wide); }
.sb-prose .figure .ftrack { position: relative; height: 14px; background: var(--surface-track); }
.sb-prose .figure .fbar { position: absolute; inset: 0 auto 0 0; background: var(--data-neutral); opacity: .45; }
.sb-prose .figure .fbar.over { background: var(--data-warn); opacity: 1; }
.sb-prose .figure .fthresh { position: absolute; top: -4px; bottom: -4px; width: 2px; background: var(--data-pos); }
.sb-prose .figure .fval { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-read-sm); text-align: right; color: var(--text-body); }
.sb-prose .figure .fkey { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-9); margin-top: var(--space-7); padding-top: var(--space-5); border-top: var(--bw) solid var(--border-subtle); }
.sb-prose .figure .fkey span { display: inline-flex; align-items: center; gap: var(--space-2); font-family: var(--font-mono); font-size: var(--fs-caption); color: var(--text-ghost); letter-spacing: var(--ls-wide); }
.sb-prose .figure .fkey i { width: 12px; height: 7px; display: inline-block; }

/* ---------- narrow ---------- */

@media (max-width: 640px) {
  .postlist a { grid-template-columns: 1fr; gap: var(--space-3); }
  .postlist .postlink-s { grid-column: 1; }
  .role { grid-template-columns: 1fr; gap: var(--space-3); }
  .sb-prose .figure .frow { grid-template-columns: 1fr 3rem; }
  .sb-prose .figure .flbl { grid-column: 1 / -1; }
  .topbar { position: static; }
}

@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }

/* Link cards: icon + label on one row, value beneath. */
.extlink-h { display: flex; align-items: center; gap: var(--space-3); color: var(--text-tertiary); }
.extlink:hover .extlink-h { color: var(--accent); }

/* "Load-bearing": not shipped, not merely live. Other people's work rests on
   it. Sky reads as structural next to amber (active) and jade (shipped),
   and stays clear of accent, which is reserved for interaction. */
.pill.bearing { color: var(--cat-biographical); border-color: var(--border-default); }


.hero > .sprite { margin-bottom: var(--space-5); }

/* ---------- sprite placements ----------
   Each placement has to justify itself: the sprite either does a job the page
   was already trying to do, or it does not go there.

   SIZING is a single --sprite-cell override; the generated rules derive the
   sheet width and the keyframe travel from it, so nothing here restates a frame
   count. Halving the 64px cell lands back on the art's native 32px, so the
   small ones stay pixel-exact.

   HOVER is carried by the parent (.sw-jump, .sw-run, .sw-hurt-death), never by
   the sprite: a 28px owl is a poor hover target, and the trigger belongs to the
   card or the hero it decorates, not to the decoration.

   OPTICAL OFFSETS. The art is bottom-anchored in its cell — roughly 22% empty
   above the character, none below, and 28% empty to its right. Centring the BOX
   therefore renders the character low and leaves a hole between it and whatever
   follows. The trims below are fractions of --sprite-cell, so they hold at any
   size, and they are transforms and negative margins rather than layout, so
   they move the picture without moving the box. */

/* Hero. The owl shares a ROW with the headline rather than sitting in the hero's
   own grid: the empty block beside a two-line headline is the space it is meant
   to occupy, and aligning it to the hero instead put it level with the subhead,
   a line it has nothing to do with. Sharing the row means it tracks the
   headline if the copy ever grows to three lines.

   The eyebrow and subhead stay full-width above and below, so the owl reads as
   set into the headline block, not as a third column running the hero's height.

   .sw-jump is on the SECTION, so the jump fires from anywhere in the hero —
   a 150px sprite is a fine hover target, but the hero is the thing being
   pointed at. The right trim closes the cell's dead margin (~28% of the cell
   sits empty to the character's right) so the owl aligns to the measure's right
   edge rather than to the edge of its transparent box. */
.headrow { display: flex; align-items: center; gap: var(--space-9); }
.headrow h1 { flex: 0 1 auto; }

/* Sized in em off the HEADLINE's own scale, not in pixels. --fs-read-h1 is
   clamp(2rem, 5vw, 3.1rem), so a two-line headline stands 2.08em tall and
   anything from 67px to 103px depending on viewport. A fixed 150px owl
   overflowed that block at both ends at every width. 2.1em tracks the headline
   through the whole clamp, so the sprite stays the height of the thing it sits
   beside instead of dictating the row.

   The lift centres the CHARACTER rather than its box: ~22% of the cell is empty
   above the head and none below, so a box centred on the headline renders the
   owl noticeably low. */
/* Both trims exist because the cell is not the character. Measured on the art:
   ~12% of the cell is empty to its left and ~28% to its right. Untrimmed, the
   owl would sit a visible step inside the page margin the headline aligns to,
   and the gap to the text would be `gap` PLUS a quarter-cell of nothing —
   which is what makes an untrimmed sprite look pasted on rather than set. */
.herofig { flex: none; font-size: var(--fs-read-h1); }
.herofig .sprite {
  --sprite-cell: 2.1em;
  margin-left: calc(var(--sprite-cell) * -0.125);
  margin-right: calc(var(--sprite-cell) * -0.25);
  transform: translateY(calc(var(--sprite-cell) * -0.11));
}

/* Work cards. One per header, standing on the title's baseline — the same
   alignment the status pill already uses, so a three-line title keeps all three
   elements on its first line. Idle at rest, running while the card is hovered. */
.workcard header .sprite { --sprite-cell: 26px; flex: none; }
.workcard header h3 { flex: 1 1 auto; }

/* End mark. Where a printer would set a fleuron: the article is over, and the
   back-link below is a choice rather than the only thing left on the page. */
.endmark { display: flex; justify-content: center; margin: var(--stack-section) 0 0; }
.endmark .sprite { --sprite-cell: 32px; }

/* 404 — the only screen reached by accident. The owl idles until you hover it,
   then takes the hit and dies once, holding the last frame. Looping that would
   turn an apology into a taunt. */
.notfound { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-10); align-items: center; padding: var(--stack-section) 0; }
.notfound-fig { display: flex; justify-content: center; }
.notfound-fig .sprite { --sprite-cell: 144px; }
.notfound h1 {
  font-family: var(--font-ui);
  font-size: var(--fs-read-h1);
  font-weight: var(--fw-bold);
  line-height: 1.04;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin: var(--space-7) 0 var(--space-9);
  max-width: 20ch;
  text-wrap: balance;
}
.notfound-nav { display: flex; flex-wrap: wrap; gap: var(--space-10); margin-top: var(--stack-block); font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: var(--ls-label); text-transform: uppercase; }
.notfound-nav a { color: var(--text-tertiary); text-decoration: none; border-bottom: var(--bw) solid var(--border-subtle); padding-bottom: var(--space-1); }
.notfound-nav a:hover { color: var(--accent); border-color: var(--accent-line); }

/* Narrow: the figure stops being a column and sits under the text, smaller. A
   176px owl beside a 30ch headline on a phone would own the screen. */
@media (max-width: 720px) {
  .headrow { flex-direction: column; align-items: flex-start; gap: var(--space-7); }
  .notfound { grid-template-columns: minmax(0, 1fr); }
  .notfound-fig { justify-content: flex-start; }
  /* No size override needed — the em sizing already follows --fs-read-h1 down
     its clamp. The left trim goes: stacked, the owl is the first thing on the
     line and a negative left margin would hang it outside the page margin. */
  .herofig .sprite { margin-left: 0; }
  .notfound-fig .sprite { --sprite-cell: 96px; }
}

/* Motif switch (SesebonUI card only). Two radios and :has() — no script,
   because the site sends script-src 'none' and this had to work without an
   exception to it. The state lives in the form control; CSS reads it.

   The winter token block and BOTH swatch colours are generated into the bundle
   from motifs/winter.css and tokens/colors.css, so nothing about the demo can
   drift from the motif it demonstrates. What lives here is only the control. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.motifdemo { display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap; }
.motifswitch { display: flex; gap: var(--space-2); }
.motifswitch label {
  display: inline-flex; align-items: center; gap: var(--space-3);
  font-family: var(--font-mono); font-size: var(--fs-micro);
  letter-spacing: var(--ls-micro); text-transform: uppercase;
  color: var(--text-tertiary);
  border: var(--bw) solid var(--border-subtle);
  padding: 2px var(--space-3);
  cursor: pointer;
}
.motifswitch label:hover { color: var(--accent); border-color: var(--accent-line); }
.swatch { width: 9px; height: 9px; flex: none; }

/* The selected chip. :has() again rather than `:checked + label`, so the rule
   states which control it depends on instead of relying on markup order. */
.motifswitch:has(#motif-obsidian:checked) label[for="motif-obsidian"],
.motifswitch:has(#motif-winter:checked) label[for="motif-winter"] {
  color: var(--text-primary);
  border-color: var(--accent-line);
  background: var(--accent-wash);
}

/* The radio is visually hidden but still focusable, so its focus ring has to be
   drawn on the label instead — otherwise the control is keyboard-operable with
   no indication of where you are. */
.motifswitch input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- sprites (generated — see src/sprite/sprites.json) ---- */
:where(.sprite.idle) { --sprite-cell: 64px; }
.sprite.idle {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-idle.7a0e551c40d3.png);
          mask-image: url(/assets/sprite/owlet-idle.7a0e551c40d3.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
  animation: sb-sprite-idle 1s steps(4) infinite;
}
@keyframes sb-sprite-idle {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; }
}
:where(.sprite.walk) { --sprite-cell: 64px; }
.sprite.walk {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-walk.58cf83542bf0.png);
          mask-image: url(/assets/sprite/owlet-walk.58cf83542bf0.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-walk 0.72s steps(6) infinite;
}
@keyframes sb-sprite-walk {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; }
}
:where(.sprite.run) { --sprite-cell: 64px; }
.sprite.run {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-run.7d4f44b790cb.png);
          mask-image: url(/assets/sprite/owlet-run.7d4f44b790cb.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-run 0.5s steps(6) infinite;
}
@keyframes sb-sprite-run {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; }
}
:where(.sprite.jump) { --sprite-cell: 64px; }
.sprite.jump {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-jump.4b8597992989.png);
          mask-image: url(/assets/sprite/owlet-jump.4b8597992989.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 8) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 8) var(--sprite-cell);
  animation: sb-sprite-jump 0.9s steps(8) infinite;
}
@keyframes sb-sprite-jump {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 8) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 8) * -1) 0; }
}
:where(.sprite.climb) { --sprite-cell: 64px; }
.sprite.climb {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-climb.0945c3156f59.png);
          mask-image: url(/assets/sprite/owlet-climb.0945c3156f59.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
  animation: sb-sprite-climb 0.6s steps(4) infinite;
}
@keyframes sb-sprite-climb {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; }
}
:where(.sprite.push) { --sprite-cell: 64px; }
.sprite.push {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-push.ac60883c8860.png);
          mask-image: url(/assets/sprite/owlet-push.ac60883c8860.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-push 0.8s steps(6) infinite;
}
@keyframes sb-sprite-push {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; }
}
:where(.sprite.throw) { --sprite-cell: 64px; }
.sprite.throw {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-throw.ec48ea35929d.png);
          mask-image: url(/assets/sprite/owlet-throw.ec48ea35929d.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
  animation: sb-sprite-throw 0.45s steps(4) infinite;
}
@keyframes sb-sprite-throw {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; }
}
:where(.sprite.attack) { --sprite-cell: 64px; }
.sprite.attack {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-attack.eae2a42415a7.png);
          mask-image: url(/assets/sprite/owlet-attack.eae2a42415a7.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
  animation: sb-sprite-attack 0.4s steps(4) infinite;
}
@keyframes sb-sprite-attack {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; }
}
:where(.sprite.attack-two) { --sprite-cell: 64px; }
.sprite.attack-two {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-attack-two.7c9c80cdca24.png);
          mask-image: url(/assets/sprite/owlet-attack-two.7c9c80cdca24.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-attack-two 0.55s steps(6) infinite;
}
@keyframes sb-sprite-attack-two {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; }
}
:where(.sprite.walk-attack) { --sprite-cell: 64px; }
.sprite.walk-attack {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-walk-attack.a4599a53308c.png);
          mask-image: url(/assets/sprite/owlet-walk-attack.a4599a53308c.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-walk-attack 0.72s steps(6) infinite;
}
@keyframes sb-sprite-walk-attack {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; }
}
:where(.sprite.hurt) { --sprite-cell: 64px; }
.sprite.hurt {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-hurt.554c207a7c15.png);
          mask-image: url(/assets/sprite/owlet-hurt.554c207a7c15.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 4) var(--sprite-cell);
  animation: sb-sprite-hurt 0.5s steps(4) infinite;
}
@keyframes sb-sprite-hurt {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 4) * -1) 0; }
}
:where(.sprite.death) { --sprite-cell: 64px; }
.sprite.death {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-death.26c34c689080.png);
          mask-image: url(/assets/sprite/owlet-death.26c34c689080.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 8) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 8) var(--sprite-cell);
  animation: sb-sprite-death 1.1s steps(8) infinite;
}
@keyframes sb-sprite-death {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 8) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 8) * -1) 0; }
}
:where(.sprite.dust-jump) { --sprite-cell: 64px; }
.sprite.dust-jump {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-dust-jump.0862dcecd56c.png);
          mask-image: url(/assets/sprite/owlet-dust-jump.0862dcecd56c.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 5) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 5) var(--sprite-cell);
  animation: sb-sprite-dust-jump 0.5s steps(5) infinite;
}
@keyframes sb-sprite-dust-jump {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 5) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 5) * -1) 0; }
}
:where(.sprite.dust-walk) { --sprite-cell: 64px; }
.sprite.dust-walk {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-dust-walk.86cfff0cbbf3.png);
          mask-image: url(/assets/sprite/owlet-dust-walk.86cfff0cbbf3.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-dust-walk 0.6s steps(6) infinite;
}
@keyframes sb-sprite-dust-walk {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 6) * -1) 0; }
}
:where(.sprite.hurt-death) { --sprite-cell: 64px; }
.sprite.hurt-death {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-hurt-death.9fa571d65aa1.png);
          mask-image: url(/assets/sprite/owlet-hurt-death.9fa571d65aa1.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 12) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 12) var(--sprite-cell);
  animation: sb-sprite-hurt-death 1.6s steps(12) infinite;
}
@keyframes sb-sprite-hurt-death {
  to { -webkit-mask-position: calc(calc(var(--sprite-cell) * 12) * -1) 0; mask-position: calc(calc(var(--sprite-cell) * 12) * -1) 0; }
}
:where(.sprite.rock-small) { --sprite-cell: 16px; }
.sprite.rock-small {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-rock-small.7bb6540bfe82.png);
          mask-image: url(/assets/sprite/owlet-rock-small.7bb6540bfe82.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 1) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 1) var(--sprite-cell);
}
:where(.sprite.rock-large) { --sprite-cell: 32px; }
.sprite.rock-large {
  width: var(--sprite-cell);
  height: var(--sprite-cell);
  -webkit-mask-image: url(/assets/sprite/owlet-rock-large.598ea060da1b.png);
          mask-image: url(/assets/sprite/owlet-rock-large.598ea060da1b.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 1) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 1) var(--sprite-cell);
}
.sw-jump:hover .sprite,
.sw-jump:focus-within .sprite {
  -webkit-mask-image: url(/assets/sprite/owlet-jump.4b8597992989.png);
          mask-image: url(/assets/sprite/owlet-jump.4b8597992989.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 8) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 8) var(--sprite-cell);
  animation: sb-sprite-jump 0.9s steps(8) infinite;
}
.sw-run:hover .sprite,
.sw-run:focus-within .sprite {
  -webkit-mask-image: url(/assets/sprite/owlet-run.7d4f44b790cb.png);
          mask-image: url(/assets/sprite/owlet-run.7d4f44b790cb.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 6) var(--sprite-cell);
  animation: sb-sprite-run 0.5s steps(6) infinite;
}
.sw-hurt-death:hover .sprite,
.sw-hurt-death:focus-within .sprite {
  -webkit-mask-image: url(/assets/sprite/owlet-hurt-death.9fa571d65aa1.png);
          mask-image: url(/assets/sprite/owlet-hurt-death.9fa571d65aa1.png);
  -webkit-mask-size: calc(var(--sprite-cell) * 12) var(--sprite-cell);
          mask-size: calc(var(--sprite-cell) * 12) var(--sprite-cell);
  animation: sb-sprite-hurt-death 1.6s steps(12) 1 forwards;
}
.sprite.once {
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
}

/* ---- motif demo (generated — see motifs/winter.css) ---- */
/* Lifted verbatim from motifs/winter.css at build time. */
.workcard:has(#motif-winter:checked) {
  /* Cool-shifted surfaces */
  --surface-base:    #04070b;
  --surface-raised:  #0a0f15;
  --surface-sunken:  #070b10;
  --surface-chrome:  #05080c;
  --border-default:  #16202b;
  --border-subtle:   #101822;

  /* Frost accent replaces amber */
  --accent:          #8FD3FF;
  --accent-ink:      #04141f;
  --accent-wash:     rgba(143,211,255,.08);
  --accent-line:     rgba(143,211,255,.20);

  /* Status roles that ALSO read accent shift with it… */
  --status-active:   #8FD3FF;
  /* …but the alert role stays its invariant red. (Intentionally absent.) */

  /* Decoration hooks — host reads these to mount ambient layers */
  --motif-ambient:   rgba(143,211,255,.06);
}
.swatch.obsidian { background: #FFB000; }
.swatch.winter { background: #8FD3FF; }
