/* GENERATED FILE - do not edit.
   Assembled from build/css/*.css in filename order.
   Rebuild: python3 build/gen.py */

@font-face { font-family: "Inter Display"; font-style: normal; font-weight: 400; font-display: swap; src: url("https://framerusercontent.com/assets/bHYNJqzTyl2lqvmMiRRS6Y16Es.woff2") format("woff2"); }
@font-face { font-family: "Inter Display"; font-style: normal; font-weight: 500; font-display: swap; src: url("https://framerusercontent.com/assets/iwWTDc49ENF2tCHbqlNARXw6Ug.woff2") format("woff2"); }
@font-face { font-family: "Inter Display"; font-style: normal; font-weight: 600; font-display: swap; src: url("https://framerusercontent.com/assets/PfdOpgzFf7N2Uye9JX7xRKYTgSc.woff2") format("woff2"); }
@font-face { font-family: "Inter Display"; font-style: normal; font-weight: 700; font-display: swap; src: url("https://framerusercontent.com/assets/qITWJ2WdG0wrgQPDb8lvnYnTXDg.woff2") format("woff2"); }
/* ============================================================
   Xiaoyang Hu — portfolio
   Portfolio typography and surfaces follow xiaoyanghu.com.
   ============================================================ */

:root {
  /* Matched to xiaoyanghu.com, so the Helio work and the AKOOL and Siemens
     case studies read as one portfolio. Values sampled off the live pages
     rather than eyeballed. */
  --bg: #f8fafc;                    /* pale cool canvas */
  --fg: #0c131b;                    /* primary text */
  --paper: #ffffff;                 /* card and figure surfaces */

  --a1: #f8fafc;  --a2: #e6e8ec;  --a3: rgba(12, 19, 27, 0.38);  --a4: rgba(12, 19, 27, 0.45);
  --a5: rgba(12, 19, 27, 0.60);  --a6: rgba(12, 19, 27, 0.75);
  --a7: rgba(12, 19, 27, 0.72);  --a8: #0c131b;

  --text-body:  rgba(12, 19, 27, 0.72);
  --text-muted: rgba(12, 19, 27, 0.64);
  --border:     #dfe1e4;
  --hairline:   rgba(12, 19, 27, 0.10);
  --surface:    #ffffff;

  /* One project accent, the way each case study on the main site carries its
     own. Helio's is its own brand orange rather than a colour picked here. */
  --accent:       #f97316;   /* Helio's brand orange, used as it is */
  --accent-solid: #f97316;
  --accent-60:    rgba(249, 115, 22, 0.60);
  --accent-wash:  rgba(249, 115, 22, 0.12);
  /* Same value Helio's own `--destructive` carries: a darkened cinnabar in
     the accent's family, so an error reads as related to the brand rather
     than as a signal red. Restated here because Helio scopes its tokens to
     `.helio-ui`, which the page's own chrome sits outside of. */
  --danger:       oklch(0.525 0.190 32);

  --sans: "Inter Display", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --hand: "Mynerve", "Bradley Hand", "Segoe Print", cursive;
  /* The old skin set every small label in a monospace face; the main site sets
     them in the interface face at a smaller size. Rather than rewrite fifty
     declarations, --mono becomes that face and real code keeps a real
     monospace under its own name. */
  --mono: "Inter Display", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --code: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --page: 63rem;
  --card-radius: 24px;
  --media-radius: 16px;
  --read: 40rem;

  /* Light only, deliberately. Helio's dark theme was never tuned, and a
     portfolio that flips with the reader's OS would show it at its worst.
     This also stops the UA rendering form controls and scrollbars dark. */
  color-scheme: light;
}


* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text-body);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.wrap { max-width: var(--page); margin: 0 auto; padding: 0 1.5rem; }

img, video { max-width: 100%; }

a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--a3); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--fg); }
a:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; border-radius: 2px; }

code {
  font-family: var(--code); font-size: 0.875em; color: var(--fg);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 4px; padding: 0.05rem 0.3rem;
}


h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }
html { scroll-padding-top: 6rem; }
/*
 * Helio design tokens, vendored.
 *
 * Lifted verbatim from ui/helio-tokens/src/tokens.css in the product repo,
 * which is Xiaoyang's own colour foundation: six hand-written anchors and
 * every other value a color-mix() recipe off them. Copied here because the
 * portfolio has to keep looking like Helio after access to that repo ends.
 *
 * Two changes from the source, both mechanical:
 *   - `:root` becomes `.helio-ui`, so the product palette applies only inside
 *     a surface that opts in. The portfolio's own skin is black and white and
 *     shares two variable names with this file (--border, --text-body); at
 *     :root the two systems would fight.
 *   - The dark-theme block and the Tailwind `@theme` directives are dropped.
 *     The site ships light only, and `@theme` means nothing outside a
 *     Tailwind build.
 *
 * Helio's rem base is 14px, not 16. Anything built against these tokens sets
 * its own font-size rather than inheriting the page's.
 */

/*
 * Helio Desktop — design tokens (6-color OKLCH base, warm-first)
 *
 * Philosophy:
 *   The whole system derives from 6 base colors. Every "muted" / "secondary" /
 *   "border" / "hover" / "paper" / "stone" is a color-mix() recipe, never a
 *   standalone hex value. Warm and cool surfaces are mathematical mixes of
 *   ink into canvas (warm) or info into canvas (cool).
 *
 *   Rule of thumb: 61.8% warm surface (canvas + paper + ink tints), 38.2%
 *   cool surface (stone + info tints), <3% accent paint.
 *
 * References:
 *   - craft-agents-oss did the heavy lifting validating this pattern
 *   - helio.im marketing site picked the warm ivory + cinnabar anchors
 */

.helio-ui {
  /* ═══════════════════════════════════════════════════════════════
   * §1 · THE SIX BASE COLORS
   * Everything else derives from these. To theme the app, overwrite
   * these six. Nothing else.
   * ═══════════════════════════════════════════════════════════════ */

  --canvas: oklch(0.997 0.002 80);          /* warm near-white (≈ #FEFDFB) — content surface across chat panel / inbox / tasks / settings. Bumped from 0.99 → 0.997 to brighten the chat surface so soft-fill chips (AIBadge etc.) read with more contrast. Still under `--overlay` (1.0) so composer / popover lift remains visible. Hue 80 + very low chroma keeps the warm cinnabar-family tone without re-introducing the original cool-blue cast. */
  --ink: oklch(0.120 0.014 50);             /* warm charcoal (≈ #1A1108) — hue 50 matches `--accent`'s cinnabar family. At L=0.12 the hue is barely visible on text itself (still reads as deep dark), but it propagates through `--ink-X` mixes (borders, hovers, paper-mid recipes elsewhere) so every neutral grey carries a hint of brand warmth instead of the previous cool blue-grey (hue 245). */
  --accent: oklch(0.705 0.187 50);          /* warm orange (resolves to #F87500 — the same cinnabar the brand assets fill with; the marketing site's own landing accent is the neighbouring #F97316). Dark mode uses the same value (the brand color is identical across themes — see dark block below). */
  --info: oklch(0.520 0.070 248);           /* cool slate — the 38.2% counterpoint */
  --success: oklch(0.515 0.085 152);        /* muted forest */
  --warning: oklch(0.560 0.130 50);         /* burnt sienna — in-family with accent */
  --destructive: oklch(0.525 0.190 32);     /* darkened cinnabar — errors feel related to accent */

  /* RGB parallels for use inside rgba() — keep in sync with oklch above.
   * Main process uses BACKGROUND_HEX (src/config/theme.ts) for BrowserWindow. */
  --ink-rgb: 43, 33, 26;                    /* RGB equivalent of warm `--ink` at L≈0.16 — used by `rgba()` shadow / rim recipes that need a slightly brighter base than the L=0.12 text ink for the shadow to actually show against a near-white surface. Flipped channel order (R>G>B) vs the prior cool 28,35,43 (R<G<B). */
  --accent-rgb: 249, 115, 22;
  --info-rgb: 90, 110, 150;
  --success-rgb: 58, 125, 90;
  --warning-rgb: 170, 105, 40;
  --destructive-rgb: 198, 55, 10;

  /* ═══════════════════════════════════════════════════════════════
   * §2 · INK TONAL SCALE
   * Solid mixes of ink toward canvas. Use for text tiers, borders,
   * hover backgrounds, subtle surfaces. Named by percentage of ink.
   * ═══════════════════════════════════════════════════════════════ */

  --ink-2:  color-mix(in oklch, var(--ink)  2%, var(--canvas));
  --ink-3:  color-mix(in oklch, var(--ink)  3%, var(--canvas));
  --ink-4:  color-mix(in oklch, var(--ink)  4%, var(--canvas));
  --ink-5:  color-mix(in oklch, var(--ink)  5%, var(--canvas));
  --ink-8:  color-mix(in oklch, var(--ink)  8%, var(--canvas));
  --ink-12: color-mix(in oklch, var(--ink) 12%, var(--canvas));
  --ink-20: color-mix(in oklch, var(--ink) 20%, var(--canvas));
  --ink-30: color-mix(in oklch, var(--ink) 30%, var(--canvas));
  --ink-40: color-mix(in oklch, var(--ink) 40%, var(--canvas));
  --ink-50: color-mix(in oklch, var(--ink) 50%, var(--canvas));
  --ink-55: color-mix(in oklch, var(--ink) 55%, var(--canvas));
  --ink-57: color-mix(in oklch, var(--ink) 57%, var(--canvas));
  --ink-60: color-mix(in oklch, var(--ink) 60%, var(--canvas));
  --ink-70: color-mix(in oklch, var(--ink) 70%, var(--canvas));
  --ink-85: color-mix(in oklch, var(--ink) 85%, var(--canvas));
  --ink-95: color-mix(in oklch, var(--ink) 95%, var(--canvas));

  /* ═══════════════════════════════════════════════════════════════
   * §3 · SEMANTIC TEXT (accent/success/warning/destructive tinted toward ink)
   * Use for text that carries semantic meaning without overpowering the row.
   * Mix is in oklab for perceptually uniform blending.
   * ═══════════════════════════════════════════════════════════════ */

  --accent-text:      color-mix(in oklab, var(--accent)      50%, var(--ink));
  --success-text:     color-mix(in oklab, var(--success)     50%, var(--ink));
  --warning-text:     color-mix(in oklab, var(--warning)     50%, var(--ink));
  --destructive-text: color-mix(in oklab, var(--destructive) 50%, var(--ink));
  --info-text:        color-mix(in oklab, var(--info)        60%, var(--ink));

  /* Tinted accent surfaces (small paint, big meaning).
   *
   * Mix space is `oklab`, not `oklch`, so the source hue stays honest
   * when canvas is in a different hue family. `oklch` interpolates hue
   * in polar coordinates, which makes an 8% mix of warm accent against
   * a cool canvas resolve to a canvas-dominated hue (result reads as
   * "pale blue with a hint of chroma" instead of "pale warm accent").
   *
   * `--accent-soft` blends against a hand-picked warm base
   * (`oklch(0.99 0.006 95)`, a yellow-biased near-white) rather than
   * the cool Slate canvas, and at a very low 2.5% mix ratio.
   * Three things conspire to land on "pale butter cream":
   *   1. Cool canvas's negative b subtracts more from accent's b than
   *      from its a → mixed hue drifts from 35° cinnabar toward 27°
   *      (pink territory). Mixing against a warm base instead of
   *      canvas prevents this.
   *   2. The mixed hue leans toward whichever side has the larger b/a
   *      ratio in the mix. At low accent %, the base's ratio dominates
   *      — picking a yellow-biased base (hue 95°, so b ≫ a) pulls the
   *      result into the yellow-orange zone (~65°) instead of stopping
   *      at peach (~47°).
   *   3. Dropping accent to 2.5% keeps chroma low (≈0.010, roughly half
   *      of a visible 5% tint) and raises L (≈0.982, only 0.003 below
   *      canvas) so the glow reads as a "whispered warm ivory" — a
   *      transient flash that doesn't compete with persistent selection.
   * Net result: hue ≈ 65°, chroma ≈ 0.010, L ≈ 0.982 — light butter.
   *
   * `--selected` is the heavier "mine / active" surface — four times
   * the accent % so it fills a full row with visible warmth (self
   * bubbles, sidebar active row, inbox selected row). Same warm base
   * prevents the pink-shift. Dark mode overrides both below.
   *
   * Other semantic softs follow the default recipe — `warning` and
   * `destructive` are close enough to accent in hue to tolerate the
   * same b-compression, `success` / `info` don't have a warm identity
   * to preserve.
   */
  --accent-soft:      color-mix(in oklab, var(--accent)      2.5%, oklch(0.99 0.006 95));
  --selected:         color-mix(in oklab, var(--accent)      10%,  oklch(0.99 0.006 95));
  --success-soft:     color-mix(in oklab, var(--success)     8%, var(--canvas));
  --warning-soft:     color-mix(in oklab, var(--warning)     8%, var(--canvas));
  --destructive-soft: color-mix(in oklab, var(--destructive) 8%, var(--canvas));
  --info-soft:        color-mix(in oklab, var(--info)        6%, var(--canvas));
  --accent-hover:     color-mix(in oklab, var(--accent)      92%, var(--hover));
  --destructive-hover: color-mix(in oklab, var(--destructive) 92%, var(--hover));

  /* ═══════════════════════════════════════════════════════════════
   * §4 · SEMANTIC SURFACE ALIASES
   * Components reference these — not the raw ink-N scale. If you
   * change the visual of "paper", you change it once here.
   * ═══════════════════════════════════════════════════════════════ */

  /* Warm family (61.8% of surface area).
   *
   * Back on `--ink-X` mixes now that `--ink` itself is hue 50 (warm
   * cinnabar family). Each ink-X step automatically carries the
   * brand-warm character, so paper / paper-mid / borders / hovers
   * all read as the same warm-grey family without per-token tuning. */
  --paper: var(--ink-2);                                /* elevated panel / card — 2% warm ink + canvas = quiet warm off-white */
  --paper-mid: var(--ink-8);                            /* active row / quoted surface — one step darker */
  /* Chrome shell frame · the clean near-white band the sidebar bleeds into
     when MainLayout runs in `mode="chrome"` (default for the desktop
     app and the DS catalog).
     Direct OKLCH value rather than color-mix:
       · L=0.975 — enough contrast from white content to preserve the
         navigation hierarchy without making the rail feel heavy.
       · C=0.003 / hue=55 — a restrained orange-family warmth that ties the
         chrome to Helio's accent without turning the rail beige or pink.
     Tune knobs:
       · Want more separation → lower L
       · Want softer separation → raise L toward the content surface
       · Want more brand warmth → raise C, while keeping it below 0.006. */
  --chrome-frame: oklch(0.975 0.003 55);
  /* Main panel background · the surface that hosts the channel chat /
     DM / inbox / tasks content — what the user thinks of as "the main
     window" of the app. Pure white in light mode. Kept distinct from
     `--paper` (now used by floating UI: dialogs / popovers / banners /
     command palette) and from `--overlay` (the brightest floating-UI
     surface) so the main panel can be tuned without dragging those
     along. Dark mode override below. */
  --app-bg: oklch(1 0 0);
  /* App background roles. MainLayout owns these structural shell materials;
     glass remains reserved for overlays and floating panes. */
  --bg-app-frame: var(--chrome-frame);                   /* window material for rail-less chrome rows and host-owned areas */
  --bg-app-rail: var(--chrome-frame);                    /* primary navigation rail — a quiet warm near-white tied to the brand accent */
  /* Route list columns (Messages, Members, Inbox, Calendar) · the MIDDLE
     of the shell's three tiers. The window reads frame → list → content,
     darkest to lightest (L 0.975 → 0.989 → 1.0), so a list column is
     legible as its own surface without a border doing the work. This is
     deliberately subtler than the rail: its job is to group the browse
     list, not create another dark sidebar. Keep the three monotonic:
     collapse any two and the shell flattens back to a bordered box. */
  --bg-app-list: oklch(0.989 0.002 55);
  --bg-app-content: var(--app-bg);                       /* main content background — the bright full-height reading pane */
  --bg-chat-content: var(--app-bg);                      /* chat surface (header + message stream + composer) — pure white in light. Inverts the surface ⇄ bubble polarity: surface stays the bright "page" tone, and member bubbles use a slightly warm grey via `--bg-chat-bubble` so the bubble itself becomes the colored element. Slack-style. Dark-mode override below keeps the surface at canvas so dark-mode bubbles still lift via being LIGHTER than the surface (the inverse only works in light mode). */
  --bg-chat-bubble: oklch(0.975 0.002 80);                /* member / AI receiver bubble fill (light). Very light warm-neutral grey — barely perceptible against white, flat, no shadow. */
  --bg-chat-bubble-self: var(--selected); /* self-sender bubble fill — aliased to --selected so it matches sidebar active + inbox row selected. Single source of truth for the "mine / active" warm tint. */
  --bg-app-surface: var(--overlay);                      /* message/composer/card surface */
  --bg-app-surface-alt: var(--paper-mid);                /* selected rows / quoted groups */
  --bg-app-floating: var(--overlay);                     /* hover toolbar / popover / menu */
  --overlay: oklch(1 0 0);                              /* pure white in light mode — popover / dropdown / context-menu / floating-toolbar / composer surfaces. Brighter than canvas/paper so floating UI reads as "lifted off the page" regardless of the underlying surface tone. Dark mode overrides this below; unlike `--paper` / `--hover` / `--border` (recipes of ink+canvas that auto-flip), `--overlay` is a literal so it needs an explicit dark restatement. */
  --parchment: color-mix(in oklab, var(--warning) 5%, var(--canvas)); /* warm feature panel */
  --hover: var(--ink-3);                                /* interactive hover bg — one tonal step deeper than --paper so the state remains visible on cards and canvas */
  --row-hover: var(--ink-3);                            /* full-width list-row hover bg on canvas — kept as its own dial so canvas rows can retune without dragging --hover or the sidebar along; currently the same step as --hover */
  --sidebar-hover: var(--ink-8);                        /* sidebar row hover bg — one tonal step deeper than the ink-4 sidebar surface so hover stays visible on the rail */
  --sidebar-selected: var(--overlay);                   /* sidebar selected-row pill — v06 lifts the active row as a white pill off the ink-4 sidebar surface. Literal-backed (--overlay), so it carries a dark override below. */
  --border: var(--ink-12);                              /* default hairline */
  --border-strong: var(--ink-20);

  /* Glass material · translucent chrome that keeps the content behind it
   * perceptible without making callers assemble opacity / blur / rim values.
   * Use for structural overlays (title bars, detail panes, floating chrome),
   * not as a replacement for every opaque card.
   *
   * The background and two edge colors are deliberately separate:
   *   · surface   — the translucent fill;
   *   · border    — the full perimeter hairline;
   *   · highlight — the inset top edge that catches light.
   * `--glass-shadow` composes those edges with the existing grounded panel
   * shadow geometry, while `--glass-backdrop-blur` is the single blur knob. */
  --glass-surface: color-mix(in oklab, var(--overlay) 78%, transparent);
  --glass-border: color-mix(in oklab, var(--ink) 10%, transparent);
  --glass-highlight: color-mix(in oklab, var(--overlay) 78%, transparent);
  --glass-backdrop-blur: 20px;
  --glass-shadow:
    inset 0 1px 0 var(--glass-highlight),
    0 0 0 1px var(--glass-border),
    0 1px 1px -0.5px rgba(var(--shadow-drop-rgb), var(--shadow-blur-opacity)),
    0 3px 3px -1.5px rgba(var(--shadow-drop-rgb), var(--shadow-blur-opacity));

  /* Cool family (38.2% of surface area) */
  --stone: var(--info-soft);                            /* cool panel — tables, dense lists */
  --stone-mid: color-mix(in oklch, var(--info) 10%, var(--canvas));
  --border-cool: color-mix(in oklch, var(--info) 18%, var(--canvas));

  /* Text tiers
   *
   * Contract (WCAG-aware):
   *   --text-primary    ≥14:1 on canvas — body, primary readable content.
   *   --text-secondary  ~9.8:1 — section labels, secondary UI control text.
   *   --text-tertiary   ~5.6:1 light / ~4.9:1 dark — secondary metadata
   *                     (timestamps, hint text, "x ago" stamps, inline
   *                     labels next to primary content). Clears AA 4.5:1
   *                     in both themes.
   *   --text-placeholder ~2.3:1 — placeholder text only (WCAG explicitly
   *                     exempts placeholder).
   */
  --text-primary: var(--ink);
  --text-secondary: var(--ink-70);
  --text-tertiary: var(--ink-55);
  --text-placeholder: var(--ink-30);
  --text-cool: var(--info-text);                        /* for IDs, timestamps, mono metadata */

  /* Icon tiers
   *
   * Icons have their own semantic palette instead of borrowing text
   * tokens:
   *   --icon-primary   pairs with --text-primary — one tonal step
   *                    quieter than its label (ink-60 vs ink).
   *   --icon-secondary pairs with --text-secondary — the icon sits
   *                    lighter (ink-55 vs ink-70) so 16px glyphs
   *                    stay subordinate to their paired label.
   *
   * Controls own these colors so hover / selected state can promote an
   * icon without callers styling individual SVGs. Semantic status icons
   * continue to use success / warning / destructive directly.
   */
  --icon-primary: var(--ink-60);
  /* Figma's majority value. A step lighter than `--text-tertiary`, which
     stops at ink-55 to clear the 4.5:1 text floor in dark; icons answer to
     the 3:1 non-text floor, which ink-50 clears in both themes. */
  --icon-secondary: var(--ink-50);
  /* Solid fills need maximum contrast; icons share the fixed near-white
   * foreground used by their labels instead of receiving a muted tint. */
  --icon-on-accent: var(--text-on-accent);
  --icon-on-success: var(--text-on-accent);
  --icon-on-destructive: var(--text-on-destructive);

  /* Task progress palette
   *
   * Progress marks are tiny, repeated wayfinding signals rather than body
   * copy. They use a brighter product palette than the restrained global
   * info / warning / success colors so the circular states remain distinct
   * at 12–16px in dense lists and menus. */
  --task-progress-open: var(--ink-30);
  --task-progress-in-progress: oklch(0.720 0.150 220);
  --task-progress-blocked: oklch(0.800 0.165 82);
  --task-progress-in-review: oklch(0.680 0.160 300);
  /* Text-tier sibling of --task-progress-in-review: mixed toward ink like the
     --info-text/--warning-text family so it clears AA on body-size labels —
     the raw icon token is too light to use as a foreground text color. */
  --task-progress-in-review-text: color-mix(in oklab, var(--task-progress-in-review) 55%, var(--ink));
  --task-progress-done: oklch(0.700 0.175 145);
  --task-progress-cancelled: var(--ink-30);

  /* Task priority
   *
   * Low / normal / high use neutral icon roles and communicate rank through
   * their bar geometry. Only urgent receives a semantic warning color. */
  --task-priority-urgent: oklch(0.640 0.200 25);

  /* ═══════════════════════════════════════════════════════════════
   * §4b · INVERSE + "ON-PAINT" TOKENS
   * Two distinct semantics that are easy to confuse:
   *
   *   --surface-inverse / --text-on-inverse — the "opposite of body"
   *     pair. Used for tooltip surfaces, filled "complete" steps — any
   *     chip that wants to read as "inverted/high-contrast floating
   *     chrome." Both sides FLIP when light↔dark switches. In light
   *     mode: dark bg, light text. In dark: light bg, dark text.
   *
   *   --text-on-accent / --text-on-destructive — text sitting on the
   *     cinnabar accent (or destructive) fill. The accent color itself
   *     barely shifts between light and dark (we want cinnabar to read
   *     "like cinnabar" in both), so the paired text color must be
   *     FIXED near-white. Flipping it would make the check/badge label
   *     disappear into the orange in dark mode.
   * ═══════════════════════════════════════════════════════════════ */

  --surface-inverse:   var(--ink);       /* flips with theme */
  --text-on-inverse:   var(--canvas);    /* flips with theme */
  --text-on-accent:    oklch(0.985 0.005 85);  /* fixed warm near-white */
  --text-on-destructive: oklch(0.985 0.005 85);

  /* ═══════════════════════════════════════════════════════════════
   * §4c · IDENTITY PALETTE
   * 12 evenly-spaced hues for per-user / per-workspace avatar fills.
   * Real product: assign by hash(user_id) % 12 so the same id always
   * lands on the same slot. Mock data in the design system references
   * these tokens so themes can re-tint identity slots without touching
   * demo code.
   * ═══════════════════════════════════════════════════════════════ */

  --identity-1:  oklch(0.62 0.18 250);   /* blue */
  --identity-2:  oklch(0.68 0.18 15);    /* red */
  --identity-3:  oklch(0.70 0.12 100);   /* yellow-green */
  --identity-4:  oklch(0.60 0.16 180);   /* teal */
  --identity-5:  oklch(0.70 0.10 50);    /* warm gold */
  --identity-6:  oklch(0.65 0.17 320);   /* magenta */
  --identity-7:  oklch(0.70 0.10 200);   /* cyan */
  --identity-8:  oklch(0.60 0.18 100);   /* green */
  --identity-9:  oklch(0.65 0.16 290);   /* purple */
  --identity-10: oklch(0.70 0.14 60);    /* orange */
  --identity-11: oklch(0.70 0.10 0);     /* pink */
  --identity-12: oklch(0.70 0.10 130);   /* sage */

  /* ═══════════════════════════════════════════════════════════════
   * §4c-2 · LABEL PALETTE (chip backgrounds)
   * 12 evenly-spaced hues for user-created task labels. Tuned to
   * Helio's warm + low-chroma palette: uniform L≈0.96 (very pale),
   * uniform chroma ≈0.02-0.025 so the 12 colors read as one family
   * sitting at the same altitude rather than as a vibrant rainbow.
   *
   * Why not Tailwind 100? Tailwind's 100-scale picks hues for generic
   * UI — wider chroma swing and full color-wheel coverage including
   * saturated cool blues / purples that conflict with Helio's warm
   * cinnabar brand. These slots match the warmth and muted weight of
   * the existing `*-soft` brand tokens (accent-soft, success-soft).
   *
   * Text on top always stays at `--text-primary` — only the bg
   * carries the color identity. Dark mode re-states all 12 slots in
   * §7b at L≈0.32 / C≈0.05 so light text reads cleanly against the
   * chip background (HEL-554 regression: white-on-pastel text was
   * invisible in dark mode when these literals leaked through).
   * ═══════════════════════════════════════════════════════════════ */

  --label-red-soft:    oklch(0.96 0.025 20);
  --label-orange-soft: oklch(0.96 0.025 55);
  --label-amber-soft:  oklch(0.96 0.025 80);
  --label-yellow-soft: oklch(0.97 0.030 100);
  --label-lime-soft:   oklch(0.97 0.025 130);
  --label-green-soft:  oklch(0.96 0.022 160);
  --label-teal-soft:   oklch(0.96 0.022 195);
  --label-cyan-soft:   oklch(0.96 0.020 220);
  --label-blue-soft:   oklch(0.96 0.022 250);
  --label-indigo-soft: oklch(0.96 0.022 275);
  --label-purple-soft: oklch(0.96 0.022 305);
  --label-pink-soft:   oklch(0.96 0.025 350);

  /* ═══════════════════════════════════════════════════════════════
   * §4d · BRAND ART TOKENS
   * Brand-specific values that live outside the systemic ink/canvas
   * scale. Splash animation and identity marks use these; normal
   * product UI should NOT reference them.
   * ═══════════════════════════════════════════════════════════════ */

  --brand-void: #000000;          /* cosmos background (pure black, intentionally outside warm scale) */

  /* Dot portrait backgrounds are fixed brand-art colors. They do not flip
   * with the app theme: saved avatars must preserve the same identity. */
  --brand-dot-avatar-mint:      #CDE8DE;
  --brand-dot-avatar-sky:       #CFE0F5;
  --brand-dot-avatar-lilac:     #DED4F4;
  --brand-dot-avatar-rose:      #F2D3DF;
  --brand-dot-avatar-sage:      #D7E2C6;
  --brand-dot-avatar-peach:     #F3D4BF;
  --brand-dot-avatar-yellow:    #F3E4AE;
  --brand-dot-avatar-blue-gray: #D6DFE9;

  /* Third-party connector brands, for the provenance chip on a mirrored
   * conversation. These are facts about someone else's logo, not points on
   * our scale: they never flip with the theme and never move when our
   * palette does. Feishu shares Lark's blue — same product, CN tenant. */
  --brand-connector-slack:  #4A154B;
  --brand-connector-lark:   #3370FF;
  --brand-connector-wechat: #07C160;

  /* The readable foreground ON each fill. White is not a given: it clears
   * 14:1 on Slack's aubergine but only 2.38:1 on WeChat's green, which no
   * amount of good intent makes legible at this chip's size. Paired with
   * the fill so the two can never be chosen apart. */
  --brand-connector-slack-ink:  #FFFFFF;  /* 14.00:1 */
  --brand-connector-lark-ink:   #FFFFFF;  /*  4.28:1 */
  --brand-connector-wechat-ink: #06301A;  /*  6.09:1 */

  /* ═══════════════════════════════════════════════════════════════
   * §4e · TERMINAL SURFACE
   * Terminal emulation insists on its own dark palette regardless of
   * Helio theme, so it feels "like a terminal" rather than like a
   * panel. Two-color pair; does not flip with light/dark.
   * ═══════════════════════════════════════════════════════════════ */

  --terminal-bg: #1A1816;
  --terminal-fg: #E8E4DB;
  --terminal-fg-dim: #A0A099;                  /* secondary terminal output */

  /* ═══════════════════════════════════════════════════════════════
   * §4f · PREVIEW SCALE
   * Fixed dimensions for catalog miniature tiles (GalleryTile preview
   * slots, DemoBlock/Playground preview viewports). These are NOT
   * product UI dimensions — they're the design-system's own scale for
   * "show a component at a legible miniature size in a catalog grid".
   *
   * Chosen so neighbouring tiers differ by ≥60px (perceptible) and the
   * full range spans ~200–720px. Consolidates 14 historical distinct
   * values into 8 tiers. If a future demo needs a value between tiers,
   * pick the nearer one — don't add a mid-tier.
   * ═══════════════════════════════════════════════════════════════ */

  --preview-w-2xs:     200px;
  --preview-w-xs:      220px;
  --preview-w-sm:      240px;
  --preview-w-md:      320px;
  --preview-w-md-plus: 380px;
  --preview-w-lg:      480px;
  --preview-w-xl:      520px;
  --preview-w-2xl:     560px;
  --preview-w-3xl:     720px;

  --preview-w-sidebar: 120px;   /* sidebar mini-preview */

  --preview-h-xs:  140px;
  --preview-h-sm:  260px;
  --preview-h-md:  360px;
  --preview-h-lg:  440px;
  --preview-h-xl:  480px;
  --preview-h-2xl: 560px;

  /* ═══════════════════════════════════════════════════════════════
   * §4g · PROSE + PAGE
   * Readability cap on description paragraphs + catalog page outer
   * width. Same namespace because both are text-column-ish constraints
   * that sit outside the preview-tile scale.
   * ═══════════════════════════════════════════════════════════════ */

  --prose-width: 560px;
  --page-width: 880px;          /* catalog page outer container */
  --fullscreen-content-width: 960px;  /* fullscreen overlay content cap */

  /* Shell geometry. The desktop shell is one composition; these are the
     dimensions its parts must agree on, so they live here rather than in
     each surface. In rem: Helio draws these bands, so they follow the
     root font-size tier along with the text and controls inside them. */
  --shell-header-h: calc(44rem/14);
  /* Helio-drawn macOS traffic lights: 9px origin + 48px cluster + 12px gap
     at the base tier. */
  --traffic-light-clear: calc(69rem/14);

  /* ═══════════════════════════════════════════════════════════════
   * §5 · RADIUS, MOTION, FONTS
   * ═══════════════════════════════════════════════════════════════ */

  /* In rem so a corner keeps its proportion to the box it rounds as the
     font-size tier scales that box. */
  --radius-xs: calc(3rem/14);
  --radius-sm: calc(6rem/14);
  --radius-md: calc(8rem/14);
  --radius-lg: calc(10rem/14);
  --radius-xl: calc(12rem/14);

  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --dur-fast: 100ms;
  --dur-base: 150ms;
  --dur-slow: 200ms;
  /* Atmosphere tier — glow/halo fade-ins that should feel like light, not
   * UI (composer focus glow). Never for interactive state changes. */
  --dur-slower: 450ms;

  --font-system-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
  --font-sans: var(--font-system-sans), sans-serif;
  --font-mono: 'SF Mono', ui-monospace, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;

  /* === Document token line (design 204 §9) ===========================
   * Long-form documents are their OWN surface — they do NOT inherit app
   * chrome's single-font / weight≤500 / size-driven discipline. The
   * human style layer (color / bg / font-size / line-height / font-family
   * / text-align) may only pick from THESE tokens, never arbitrary CSS;
   * values live in Yjs/JSON, never in markdown, and AI edits preserve
   * them in place (§9). Placeholder values — ui/helio-design owns the
   * final palette/scale as a separate effort; this block is the contract
   * (a fixed, themeable token set) the style-layer selectors bind to.
   * Colors reference theme-flipping base tokens so they adapt in dark. */
  --doc-font-serif: 'Charter', 'Georgia', 'Cambria', 'Times New Roman', serif;
  --doc-font-sans: var(--font-sans);
  --doc-font-mono: var(--font-mono);
  /* text palette */
  --doc-color-default: var(--text-primary);
  --doc-color-muted:   var(--ink-50);
  --doc-color-accent:  var(--accent-text);
  --doc-color-rust:    var(--warning);
  --doc-color-blue:    var(--info);
  --doc-color-green:   var(--success);
  /* highlight (background) palette */
  --doc-bg-yellow: color-mix(in oklab, var(--warning) 18%, var(--canvas));
  --doc-bg-rust:   color-mix(in oklab, var(--accent)  12%, var(--canvas));
  --doc-bg-green:  color-mix(in oklab, var(--success) 14%, var(--canvas));
  /* font-size scale (em-relative to the document base measure) */
  --doc-size-sm:   0.85em;
  --doc-size-base: 1em;
  --doc-size-lg:   1.25em;
  --doc-size-xl:   1.5em;
  /* line-height options */
  --doc-leading-tight:  1.4;
  --doc-leading-normal: 1.7;
  --doc-leading-loose:  2;

  /* ═══════════════════════════════════════════════════════════════
   * §6 · Z-INDEX SCALE
   * Every stacked layer has a name. No magic numbers in components.
   *
   * The scale splits into two tiers:
   *
   *   Persistent layers   (panel / modal / overlay / fullscreen) —
   *     structural surfaces the user directs their attention at.
   *   Ephemeral overlays  (popover / dropdown / tooltip) — transient
   *     helpers summoned from within ANY persistent layer. They must
   *     always render on top, including inside modals and fullscreens.
   *
   * Layer              Value  Use case
   * ─────────────────────────────────────────────────────────
   * base                 0    Default content
   * local               10    Sticky icons inside a row
   * sticky              20    Sticky section headers
   * titlebar            40    Window chrome drag region
   * panel               50    App panels (sidebar, detail)
   * modal              200    Dialogs, drawers
   * overlay            300    Full-screen overlays
   * fullscreen         350    Fullscreen content viewers
   * popover            400    Popovers (above fullscreen)
   * dropdown           400    Dropdowns / select menus (above fullscreen)
   * tooltip            450    Tooltips (above popover)
   * tour               500    Onboarding tour spotlight (above tooltips so
   *                           it can dim chrome regardless of ephemeral
   *                           overlays already open)
   * splash             600    Splash / onboarding
   * devtools          9000    DS catalog dev-tools (draggable Playground
   *                           panel). Deliberately far above every product
   *                           layer so it floats over any demo it inspects;
   *                           still below the InspectOverlay highlighter
   *                           (z-[9999]) which must sit on top of the panel
   *                           itself. Catalog-only — product UI never uses it.
   * window-chrome    10000    Helio-drawn macOS traffic lights. Window
   *                           furniture, so it must outrank every layer
   *                           above INCLUDING the pre-React boot splash
   *                           (`ui/helio-desktop/src/index.html`'s
   *                           `#boot-splash`, hardcoded z-index:9999 since
   *                           it paints before tokens.css loads) and the
   *                           catalog's dev-only InspectOverlay (z-[9999]).
   * ═══════════════════════════════════════════════════════════════ */

  --z-base: 0;
  --z-local: 10;
  --z-sticky: 20;
  --z-titlebar: 40;
  --z-panel: 50;
  --z-overlay: 300;
  --z-fullscreen: 350;
  --z-modal: 380;
  --z-popover: 400;
  --z-dropdown: 400;
  --z-tooltip: 450;
  --z-tour: 500;
  --z-splash: 600;
  --z-devtools: 9000;
  /* Helio-drawn macOS traffic lights: window furniture, above every app
     layer including the boot splash — where the native cluster sat. Must
     stay above 9999: the boot splash and the catalog's InspectOverlay are
     both hardcoded at that value outside the token cascade (see table). */
  --z-window-chrome: 10000;

  /* ═══════════════════════════════════════════════════════════════
   * §7 · SHADOW SYSTEM
   *
   * Two RGB knobs drive every shadow:
   *   --shadow-rim-rgb  — the 1px hairline border component. In light
   *     mode that's ink (a subtle dark rim around cards); in dark mode
   *     it flips to a slightly-lighter-than-canvas "top highlight" rim
   *     so elevation still reads against the darker body.
   *   --shadow-drop-rgb — the color of the blurred drop shadow layers.
   *     In light mode black (with low opacity). In dark mode stays
   *     black so the drop still grounds the surface; higher blur
   *     opacity compensates for the reduced contrast against a dark bg.
   *
   * Opacity vars tune the two layers independently — dark mode bumps
   * drop-opacity up and rim-opacity up (see the dark override below).
   * ═══════════════════════════════════════════════════════════════ */

  --shadow-rim-rgb: var(--ink-rgb);
  --shadow-drop-rgb: 0, 0, 0;
  /* Warm-ink shadow channel for surfaces that sit AGAINST accent light
   * (composer glow): cool grey next to cinnabar de-saturates both — the
   * grey goes dirty, the orange goes muddy. */
  --shadow-warm-rgb: 80, 52, 20;
  --shadow-border-opacity: 0.08;
  --shadow-blur-opacity: 0.06;

  --shadow-minimal:
    0 0 0 1px rgba(var(--shadow-rim-rgb), calc(var(--shadow-border-opacity) * 0.75)),
    0 1px 1px -0.5px rgba(var(--shadow-drop-rgb), var(--shadow-blur-opacity));

  --shadow-panel:
    0 0 0 1px rgba(var(--shadow-rim-rgb), var(--shadow-border-opacity)),
    0 1px 1px -0.5px rgba(var(--shadow-drop-rgb), var(--shadow-blur-opacity)),
    0 3px 3px -1.5px rgba(var(--shadow-drop-rgb), var(--shadow-blur-opacity));

  --shadow-drop-panel:
    0 1px 2px 0 rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.8)),
    0 4px 12px -4px rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.9));

  /* Home landing elevation. These three roles intentionally keep separate
   * stacks: the composer has a tight downward lift, the suggestions card a
   * broader drop, and the detail takeover casts only toward the content it
   * covers. Values mirror the canonical Figma surfaces exactly. */
  --shadow-home-composer:
    0 32px 9px rgba(194, 194, 194, 0),
    0 20px 8px rgba(194, 194, 194, 0.01),
    0 11px 7px rgba(194, 194, 194, 0.05),
    0 5px 5px rgba(194, 194, 194, 0.09),
    0 1px 3px rgba(194, 194, 194, 0.1);

  --shadow-home-suggestions:
    0 32px 9px rgba(194, 194, 194, 0),
    0 20px 8px rgba(194, 194, 194, 0.01),
    0 11px 7px rgba(194, 194, 194, 0.05),
    0 5px 5px rgba(194, 194, 194, 0.09),
    0 1px 3px rgba(194, 194, 194, 0.1);

  --shadow-home-suggestion-detail:
    -62px 0 17px rgba(209, 209, 209, 0),
    -39px 0 16px rgba(209, 209, 209, 0.01),
    -22px 0 13px rgba(209, 209, 209, 0.05),
    -10px 0 10px rgba(209, 209, 209, 0.09),
    -2px 0 5px rgba(209, 209, 209, 0.1);

  --shadow-message: none;

  /* Bottom-sheet overlay — a single upward drop (negative y) so a sheet
   * anchored to the bottom edge lifts off the content it covers. Used by
   * mobile bottom sheets (channel detail, task filters). */
  --shadow-sheet: 0 -12px 40px -12px rgba(var(--shadow-drop-rgb), 0.4);

  --shadow-pop:
    0 0 0 1px rgba(var(--shadow-rim-rgb), var(--shadow-border-opacity)),
    0 1px 1px -0.5px rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.67)),
    0 3px 3px 0 rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.67)),
    0 6px 6px 0 rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.33)),
    0 12px 12px 0 rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.33)),
    0 24px 24px 0 rgba(var(--shadow-drop-rgb), calc(var(--shadow-blur-opacity) * 0.33));

  /* Active-tab / selected-link underline: 1px accent bar flush against the
   * bottom edge. Used where the "selected" treatment is an underline rather
   * than a filled pill. */
  --shadow-underline: inset 0 -1px 0 var(--accent);
}
/* Portfolio navigation */
nav.top { position: sticky; top: 0; z-index: 20; background: var(--bg); }
:root { --demo-nav-height: 74px; }
nav.top .wrap { height: var(--demo-nav-height); display: flex; align-items: center; gap: 1.5rem; padding-block: 1rem; font-size: 0.875rem; }
nav.top a { color: var(--text-muted); text-decoration: none; }
nav.top a:hover { color: var(--fg); }
nav.top .me { margin-right: auto; color: var(--fg); font-weight: 500; }
nav.top .me::before { content: "←"; display: inline-grid; place-items: center; width: 1.75rem; height: 1.75rem; margin-right: 0.6rem; border: 1px solid var(--border); border-radius: 50%; }

header.hero { padding: clamp(2.5rem, 5vw, 4rem) 0 0; }
header.hero h1, header.hero .lede, header.hero .claim, header.hero .whereami { max-width: var(--read); }
/* Headings here are written as sentences rather than fitted to the measure, so
   without balancing each one ends on a line of its own last word. */
h1 { margin: 0; color: var(--fg); font-size: clamp(2rem, 4.5vw, 2.5rem); font-weight: 500; line-height: 1.12; letter-spacing: -0.04em; text-wrap: balance; }
.lede { margin: 1rem 0 0; font-size: 1.25rem; line-height: 1.45; letter-spacing: -0.015em; color: var(--text-muted); text-wrap: balance; }
header.hero .claim { margin: 1.75rem 0 0; font-size: 1.125rem; line-height: 1.6; }

/* The rule belongs to the item on its right, and every item carries one. The
   row is pulled left half a channel and the line clips, so whichever item
   happens to start a line loses its rule to the clip. `span + span` draws one
   on the first item of every line after the first, hanging in the margin, and
   which item wraps depends on the strings. */
.whereami { --rule-channel: 1.25rem; overflow: hidden; margin: 1.75rem 0 0; font-size: 0.8125rem; color: var(--text-muted); }
.whereami > span { display: flex; flex-wrap: wrap; align-items: baseline; row-gap: 0.4rem; margin-left: calc(var(--rule-channel) / -2); }
.whereami > span > span { padding: 0 calc(var(--rule-channel) / 2); border-left: 1px solid var(--border); }
.nav-links { display: flex; align-items: center; gap: 1.5rem; }
nav.top .nav-demo { display: inline-flex; align-items: center; gap: 0.7rem; padding: 0.5rem 0.9rem; border: 1px solid var(--border); border-radius: 999px; color: var(--fg); background: var(--paper); }
nav.top .nav-demo:hover { border-color: var(--a3); }
.nav-external { display: inline-flex; align-items: center; gap: 0.3rem; }
section#live { padding-top: 2.75rem; }
@media (max-width: 40rem) {
  :root { --demo-nav-height: 128px; }
  nav.top .wrap { flex-wrap: wrap; row-gap: 0.75rem; }
  .nav-links { width: 100%; justify-content: space-between; gap: 0.75rem; font-size: 0.8125rem; }
  html { scroll-padding-top: 8rem; }
}
section.band { padding-top: clamp(3.5rem, 7vw, 5rem); }
h2 { margin: 0 0 0.6rem; color: var(--fg); font-size: 1.5rem; font-weight: 500; line-height: 1.3; letter-spacing: -0.025em; text-wrap: balance; }
.band-sub { margin: 0 0 1.75rem; font-size: 1rem; color: var(--text-muted); }
#live > .wrap > h2, #live > .wrap > .band-sub { max-width: var(--read); margin-inline: 0; }
.cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem; }
a.card { display: flex; flex-direction: column; height: 100%; overflow: hidden; color: inherit; text-decoration: none; background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--card-radius); box-shadow: inset 0 1px 4px rgba(12,19,27,0.04); transition: transform 180ms ease-out, border-color 180ms ease-out; }
a.card .thumb { order: 2; display: block; padding: 1rem 1.5rem 0; aspect-ratio: 16 / 10; overflow: hidden; background: linear-gradient(180deg, white, var(--bg)); }
a.card .thumb img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: center; border-radius: 8px 8px 0 0; }
a.card .card-body { display: block; padding: 1.75rem 1.75rem 0.5rem; text-align: left; }
.card-meta { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.65rem; }
.card-featured { display: inline-flex; align-items: center; padding: 0 0.6rem; border: 1px solid var(--border); border-radius: 999px; color: var(--text-muted); font-size: 0.75rem; }
.card-no { display: inline-flex; align-items: center; justify-content: center; min-width: 2rem; border: 1px solid var(--border); border-radius: 999px; color: var(--text-muted); font-size: 0.75rem; font-variant-numeric: tabular-nums; }
a.card h3 { margin: 0 0 0.65rem; color: var(--fg); font-weight: 500; font-size: 1.375rem; line-height: 1.25; letter-spacing: -0.025em; text-wrap: balance; }
a.card p { margin: 0; color: var(--text-muted); font-size: 1rem; line-height: 1.5; }
a.card:hover { transform: translateY(-3px); border-color: var(--a3); }
a.card:focus-visible { outline: 2px solid var(--fg); outline-offset: 4px; }
a.card--hero .card-body { max-width: 44rem; margin-inline: 0; padding-top: 2.25rem; }
a.card--hero h3 { font-size: 1.75rem; }
a.card--hero .thumb { aspect-ratio: 2 / 1; padding: 1.75rem 2.5rem 0; }
.demo-scroll-track { height: calc(150svh - var(--demo-nav-height)); }
.hero-live { height: calc(100svh - var(--demo-nav-height)); display: flex; align-items: center; position: sticky; top: var(--demo-nav-height); isolation: isolate; background: var(--bg); }
.hero-live > .wrap { width: min(100%, calc((100svh - var(--demo-nav-height) - 96px) * 1.6 + 48px)); position: relative; }
.hero-live .live { margin: 0; position: relative; }
.hero-live .live-stage { border: 1px solid rgba(83, 39, 18, 0.2); border-radius: var(--media-radius); box-shadow: none; }
@media (max-width: 46rem) { .cards { grid-template-columns: 1fr; } }
@media (max-width: 34rem) {
  nav.top .wrap { gap: 1rem; }
  a.card .card-body { padding: 1.5rem 1.25rem 0.5rem; }
  a.card--hero h3 { font-size: 1.5rem; }
  a.card .thumb { padding-inline: 1rem; }
}

/* ---------- share gate ----------
   A link-shared portfolio, not a locked one: the gate keeps the page out of
   a crawler's hands and out of a forwarded thread. Everything it hides is
   still in the source, so it is a courtesy, not a control. */
html[data-gated] body > *:not(.gate) { display: none; }
html[data-gated], html[data-gated] body { background: var(--bg); }
.gate {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center; padding: 1.5rem;
  background: var(--bg);
}
.gate-card {
  width: 100%; max-width: 22rem; display: flex; flex-direction: column;
}
/* The signature, not the typed name: this is the one screen a stranger sees
   before any of the work, so it is the one place the drawn mark earns its
   size. */
/* `align-self` matters more than the size here: the card is a column flex,
   whose default stretch had pinned the mark to the card's full width while
   the height stayed put — a 3:1 signature drawn at 8:1. Sized by width with
   height auto so the ratio comes from the file, not from this rule. */
.gate-sign {
  display: block; align-self: flex-start;
  width: 200px; height: auto; margin: 0 0 0.9rem;
}
.gate-note {
  margin: 0 0 1.25rem; color: var(--text-muted);
  font-size: 0.9375rem; line-height: 1.5;
}
.gate-input {
  font: inherit; font-size: 0.9375rem; color: var(--fg);
  padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); outline: none;
}
.gate-input:focus { border-color: var(--a4); }
.gate-go {
  margin-top: 0.6rem; font: inherit; font-size: 0.9375rem; font-weight: 500;
  padding: 0.5rem 0.75rem; border: 1px solid var(--fg); border-radius: 8px;
  background: var(--fg); color: var(--bg); cursor: pointer;
}
/* A link, not a second button: asking for the word is the way out of this
   screen, not the other half of a choice — so hover underlines it rather than
   growing a surface, which read as a second, paler button to press. */
.gate-ask {
  margin-top: 0.75rem; align-self: center;
  font-size: 0.875rem; color: var(--text-muted); text-decoration: none;
}
/* The two ways out sit closer to each other than to the button above them,
   so they read as a pair of exits rather than a third and fourth action. */
.gate-ask + .gate-ask { margin-top: 0.4rem; }
.gate-ask:hover, .gate-ask:focus-visible {
  color: var(--fg);
  text-decoration: underline; text-underline-offset: 0.2em;
}

/* The slot is always in the layout, empty or not — an error that appears by
   growing the form pushes everything under it and makes the page twitch at
   the exact moment the reader got something wrong. */
.gate-err {
  margin: 0.75rem 0 0; min-height: 1.25rem;
  font-size: 0.875rem; line-height: 1.25rem; color: var(--danger);
}
/* The message alone is easy to miss at this size, so the field says it too.
   `--danger` is the palette's own error tone — cinnabar, in the accent's
   family — rather than a signal red that would read as an alarm. */
.gate-card.is-wrong .gate-input { border-color: var(--danger); }

/* The footer's last line is about the site, not about the work — set below
   the contact row's weight so it reads as a postscript. */
.footer-note { margin-top: 1.25rem; color: var(--text-muted); font-size: 0.875rem; }

/* The solid backdrop reaches full color when the demo is centered in the viewport. */
.hero-live::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: #ed9563;
  pointer-events: none;
}
.hero-live .live::before {
  content: ""; position: absolute; inset: 0; border-radius: var(--media-radius);
  box-shadow: 0 28px 60px -16px rgba(94, 42, 16, 0.3), 0 4px 12px rgba(94, 42, 16, 0.08);
  pointer-events: none;
}
@keyframes demo-wash {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes demo-window {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes demo-shadow { from { opacity: 0.25; } to { opacity: 1; } }
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .demo-scroll-track { view-timeline-name: --demo-reveal; view-timeline-axis: block; view-timeline-inset: var(--demo-nav-height) 0; }
    .hero-live::before, .hero-live .live, .hero-live .live::before {
      animation-duration: 1ms;
      animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
      animation-fill-mode: both;
      animation-timeline: --demo-reveal;
    }
    .hero-live::before { animation-name: demo-wash; animation-range: contain 0% contain 35%; }
    .hero-live .live { animation-name: demo-window; animation-range: entry 0% entry 45%; }
    .hero-live .live::before { animation-name: demo-shadow; animation-range: contain 0% contain 35%; }
    .hero-live:has(:focus-visible) .live, .hero-live:has(:focus-visible) .live::before,
    .hero-live:has(:focus-visible)::before { animation: none; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .demo-scroll-track { height: auto; }
  .hero-live { position: relative; top: auto; }
}

#featured + .cards { margin-top: 1.5rem; }
/* ---------- the chapter mark ----------
   Lifted from xiaoyanghu.com: a handwritten number over a handwritten title in
   the project accent, then the prose in the interface face under it. It is the
   strongest identity signal on the main site, and it is what makes a Helio page
   read as one of the same set rather than a different site that happens to be
   linked. The number is a CSS counter, so adding or moving a section cannot
   leave the sequence wrong. */
article.detail > .wrap { counter-reset: chapter; }

article.detail > .wrap > h4 {
  counter-increment: chapter;
  font-family: var(--hand);
  font-weight: 400;
  font-size: 1.75rem;
  line-height: 1.25;
  letter-spacing: 0;
  color: var(--accent);
  margin: 4.5rem 0 1.5rem;
}
article.detail > .wrap > h4::before {
  content: counter(chapter, decimal-leading-zero);
  display: block;
  font-size: 1.375rem;
  line-height: 1.1;
  color: var(--accent-60);
  margin-bottom: 0.15rem;
}
article.detail > .wrap > h4:first-child { margin-top: 1rem; }

/* A subhead under a chapter stays in the interface face: the handwriting marks
   chapters only, or it stops marking anything. */
article.detail > .wrap > h5 {
  font-family: var(--sans);
  font-weight: 500; font-size: 1.375rem; line-height: 1.35;
  letter-spacing: -0.02em; color: var(--fg);
  margin: 2.5rem 0 0.75rem;
}

/* ---------- surfaces ----------
   White cards on the pale canvas, hairline border, no shadow. Elevation on the
   main site comes from alignment, not from lifting things off the page. */
.gallery figure,
.arc figure,
.cmp-cell,
figure.shot img,
.live-stage,
.compare img,
.gallery img,
.arc img { border-radius: 16px; }

figure.shot img,
.gallery img,
.arc img,
.cmp-cell img {
  background: var(--paper);
  border: 1px solid var(--hairline);
}

.live-stage {
  background: var(--paper);
  border: 1px solid var(--hairline);
  overflow: hidden;
}

/* ---------- the metric figure ----------
   Numbers are handwritten on the main site wherever they are the point of the
   sentence. */
.metric-n, .dec-n { font-family: var(--hand); font-weight: 400; }
/* ---------- detail page ---------- */
.back {
  display: block; width: fit-content; font-family: var(--mono); font-size: 0.875rem;
  color: var(--text-muted); text-decoration: none; margin: 2rem 0 0;
}
.back:hover { color: var(--fg); }

article.detail { padding: 1.5rem 0 0; }
article.detail > .wrap { max-width: var(--page); }

/* Prose retains its reading measure and shares the media’s left edge. */
article.detail > .wrap > h1,
article.detail > .wrap > h4,
article.detail > .wrap > h5,
article.detail > .wrap > p,
article.detail > .wrap > ul,
article.detail > .wrap > ol,
article.detail > .wrap > .role,
article.detail > .wrap > .pull { max-width: var(--read); margin-inline: 0; }
article.detail h1 {
  font-size: clamp(1.875rem, 5vw, 2.5rem); font-weight: 500; line-height: 1.1;
  letter-spacing: -0.04em; margin: 0 0 1.75rem;
}
/* One labelled row per fact. The label column sizes to the longest label
   so every value starts on the same edge. */
.role {
  display: grid; grid-template-columns: max-content minmax(0, 1fr);
  gap: 0.4rem 1.5rem;
  font-family: var(--mono); font-size: 0.8125rem; line-height: 1.6;
  color: var(--text-body);
  border-block: 1px solid var(--hairline);
  padding: 1.1rem 0; margin: 0 0 2.5rem;
}
.role dt { color: var(--fg); font-weight: 500; }
.role dd { margin: 0; }
.role b { color: var(--fg); font-weight: 500; }
@media (max-width: 34rem) {
  .role { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; }
  .role dd { margin-bottom: 0.5rem; }
  .role dd:last-child { margin-bottom: 0; }
}
article.detail p { margin: 0 0 1.15rem; }
article.detail > .wrap > p { margin-inline: 0; }
article.detail h4 {
  font-weight: 600; color: var(--fg); font-size: 1.0625rem;
  letter-spacing: -0.025em; margin: 2.25rem 0 0.6rem;
}
.pull {
  font-size: clamp(1.125rem, 2.6vw, 1.375rem); line-height: 1.35;
  letter-spacing: -0.03em; font-weight: 500; color: var(--fg);
  background: var(--accent-wash); border-radius: var(--media-radius); padding: 1.5rem;
  margin: 2rem 0; max-width: var(--read);
}
ul.plain { margin: 0 0 1.15rem; padding-left: 1.1rem; }
ul.plain li { margin: 0 0 0.4rem; }

/* ---------- media ---------- */
figure.shot { margin: 2rem 0; }
figure.shot img, figure.shot video {
  width: 100%; display: block; border: 1px solid var(--border);
  border-radius: 8px; background: var(--surface);
}
figure.shot.plain img { border: 0; background: none; }
figure.shot figcaption {
  font-family: var(--mono); font-size: 0.75rem; line-height: 1.6;
  color: var(--text-muted); margin-top: 0.75rem;
  display: flex; gap: 0.75rem; justify-content: space-between; flex-wrap: wrap;
}
figure.shot figcaption b { color: var(--a4); font-weight: 400; }
/* let wide media break the reading column */
.bleed { width: 100%; }

/* ---------- live preview embed ---------- */
.live { margin: 2rem 0; }
.live-stage {
  position: relative; width: 100%; aspect-ratio: 1280 / 800;
  border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
  background: var(--surface);
}
.live-stage iframe {
  position: absolute; top: 0; left: 0;
  width: 1280px; height: 800px; border: 0;
  transform-origin: top left;
}
.live-idle {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 0.75rem;
  font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted);
  background: var(--surface); cursor: pointer; border: 0; width: 100%;
}
.live-idle:hover { color: var(--fg); }
.live-idle .dot-run { font-size: 1.5rem; line-height: 1; }
.live-cap {
  font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted);
  margin-top: 0.75rem; display: flex; gap: 0.75rem; justify-content: space-between; flex-wrap: wrap;
}
.live-cap b { color: var(--a4); font-weight: 400; }

/* ---------- sticker grid ---------- */
.stickers { display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: 0.75rem; margin: 2rem 0; }
.stickers figure { margin: 0; }
.stickers img {
  width: 100%; aspect-ratio: 1; object-fit: contain; display: block;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.4rem;
}
.stickers figcaption {
  font-family: var(--mono); font-size: 0.625rem; color: var(--text-muted);
  margin-top: 0.35rem; text-align: center; line-height: 1.4;
}

/* ---------- craft list ---------- */
.craft { display: grid; gap: 2.5rem; margin-top: 1rem; }
.craft > article { border-top: 1px solid var(--border); padding-top: 1.25rem; }
.craft h4 { font-weight: 600; color: var(--fg); font-size: 1.0625rem; letter-spacing: -0.025em; margin: 0 0 0.5rem; }
.craft p { margin: 0; font-size: 0.9375rem; color: var(--text-muted); }


/* A sub-point inside a section, not a section of its own. */
article.detail h5 {
  font-weight: 600; color: var(--fg); font-size: 0.9375rem;
  letter-spacing: -0.015em; margin: 1.75rem 0 0.5rem; max-width: var(--read);
}

figure.shot img, figure.shot video, .live-stage { border-radius: var(--media-radius); }
/* ---------- slot (unrecorded) ---------- */
.slot {
  margin: 2rem 0; border: 1px dashed var(--a3); border-radius: 8px;
  background: var(--surface); aspect-ratio: var(--ar, 16 / 9);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.5rem; padding: 1.25rem; text-align: center;
}
.slot-tag { font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--fg); }
.slot-desc { font-family: var(--mono); font-size: 0.75rem; line-height: 1.6; color: var(--text-muted); margin: 0; }
.slot-route { font-family: var(--mono); font-size: 0.6875rem; color: var(--a4); }

/* ---------- footer ---------- */
footer { margin-top: 5rem; border-top: 1px solid var(--border); padding: 2.5rem 0 5rem; }
footer .wrap { max-width: var(--page); }
footer p { max-width: var(--read); }
footer p { margin: 0 0 0.75rem; }
.fineprint { font-family: var(--mono); font-size: 0.6875rem; line-height: 1.8; color: var(--a4); margin-top: 2.5rem; }

/* ---------- pager ---------- */
.pager { display: flex; justify-content: space-between; gap: 1rem; border-top: 1px solid var(--border); margin-top: 3.5rem; padding-top: 1.25rem; font-size: 0.9375rem; }
.pager a { text-decoration: none; color: var(--text-muted); }
.pager a:hover { color: var(--fg); }
.pager .nxt { text-align: left; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}

/* ---------- text thumbnail (cards with no image) ---------- */
.thumb-text {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: flex-start; justify-content: center; gap: 0.5rem;
  padding: 1.5rem 1.75rem;
  font-family: var(--mono); font-size: 0.8125rem; color: var(--text-muted);
  background: var(--surface);
}
.thumb-text span { display: block; }
.thumb-text span::before { content: "· "; color: var(--a3); }
/* the locale row reads as a row, not a stack */
.thumb-text:has(span:nth-child(6)) {
  flex-direction: row; flex-wrap: wrap; align-content: center; gap: 0.4rem 0.9rem;
  font-size: 0.9375rem;
}

/* ---------- live stages break out of the reading column ---------- */
.live {
  width: 100%;
}
figure.shot.wide { margin-left: 0; margin-right: 0; }
/* the sticker grid wants the full page too */
.stickers {
  width: 100%;
}


footer { border-color: var(--hairline); }
.pager { max-width: var(--read); margin-inline: 0; border-color: var(--hairline); gap: 2rem; }
.pager > a { flex: 1; }
@media (max-width: 34rem) { .pager { flex-direction: column; } .pager .nxt { margin-left: 0; text-align: left; } }
@media (prefers-reduced-motion: reduce) { a.card { transition: none; } a.card:hover { transform: none; } }
/* ---------- live idle poster ---------- */
.live-idle.has-poster {
  background-size: cover; background-position: top center; background-repeat: no-repeat;
  justify-content: flex-end; align-items: center; padding: 0 0 1.25rem;
}
.live-idle.has-poster::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to bottom, transparent 55%, color-mix(in srgb, var(--bg) 78%, transparent));
}
.live-play {
  position: relative; z-index: 1;
  font-family: var(--mono); font-size: 0.75rem; color: var(--fg);
  background: var(--bg); border: 1px solid var(--border); border-radius: 999px;
  padding: 0.4rem 0.9rem; box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
.live-idle:hover .live-play { border-color: var(--a3); }

/* ---------- short hero stage: clips the surface's empty lower half ---------- */
.live--short .live-stage { aspect-ratio: 1280 / 560; }

/* ---------- flow strip: walk a multi-step surface ---------- */
.live-steps {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 0.5rem; margin-top: 0.75rem;
}
@media (max-width: 46rem) {
  .live-steps { grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); }
}
.live-steps button {
  display: block; text-align: left; cursor: pointer;
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
}
.live-steps button img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface);
  transition: border-color 120ms ease;
}
.live-steps button:hover img { border-color: var(--a3); }
.live-steps button.is-active img { border-color: var(--fg); }
.live-steps button span {
  display: block; font-family: var(--mono); font-size: 0.6875rem; line-height: 1.4;
  color: var(--text-muted); margin-top: 0.4rem;
}
.live-steps button.is-active span { color: var(--fg); }
.live-steps button:focus-visible img { outline: 2px solid var(--fg); outline-offset: 2px; }

/* a live stage that has booted no longer needs the surface tint */
.live-stage.is-live { background: var(--bg); }

.live-hint {
  font-family: var(--mono); font-size: 0.6875rem; color: var(--a4);
  margin: 0.5rem 0 0;
}

/* still strips reuse the flow-strip look */
.still-steps { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.5rem; margin-top: 0.75rem; }
@media (max-width: 46rem) { .still-steps { grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); } }
.still-steps button { display: block; text-align: left; cursor: pointer; background: none; border: 0; padding: 0; font: inherit; color: inherit; }
.still-steps button img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface); transition: border-color 120ms ease;
}
.still-steps button:hover img { border-color: var(--a3); }
.still-steps button.is-active img { border-color: var(--fg); }
.still-steps button span { display: block; font-family: var(--mono); font-size: 0.6875rem; line-height: 1.4; color: var(--text-muted); margin-top: 0.4rem; }
.still-steps button.is-active span { color: var(--fg); }
.still-steps button:focus-visible img { outline: 2px solid var(--fg); outline-offset: 2px; }

/* strips need air before the prose resumes */
.live-steps, .still-steps { margin-bottom: 2rem; }
.live .live-steps { margin-bottom: 0.75rem; }


/* ---------- contact strip: a flow at a glance ---------- */
.arc {
  width: 100%;
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.5rem;
  margin-top: 1.25rem;
}
@media (max-width: 46rem) { .arc { grid-template-columns: repeat(3, 1fr); } }
.arc figure { margin: 0; }
.arc a { display: block; }
.arc img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface);
  transition: border-color 120ms ease;
}
.arc a:hover img { border-color: var(--a3); }
.arc a:focus-visible img { outline: 2px solid var(--fg); outline-offset: 2px; }
.arc figcaption {
  font-family: var(--mono); font-size: 0.6875rem; line-height: 1.4;
  color: var(--text-muted); margin-top: 0.4rem;
}
.arc + .live-hint { margin-bottom: 2rem; }

/* ---------- commit ladder ---------- */
.log { list-style: none; margin: 1.5rem 0 2rem; padding: 0; border-top: 1px solid var(--border); }
.log-row {
  display: grid; grid-template-columns: 5.5rem 1fr auto; gap: 0.75rem; align-items: baseline;
  padding: 0.55rem 0; border-bottom: 1px solid var(--border);
  font-family: var(--mono); font-size: 0.8125rem; line-height: 1.5;
}
@media (max-width: 34rem) { .log-row { grid-template-columns: 4.5rem 1fr; } }
.log-ver { color: var(--a4); font-variant-numeric: tabular-nums; }
.log-msg { color: var(--text-body); }
.log-kind {
  font-size: 0.625rem; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-muted); border: 1px solid var(--border); border-radius: 4px;
  padding: 0.05rem 0.35rem; white-space: nowrap;
}
.log-revert, .log-killed, .log-reversed { color: var(--fg); border-color: var(--a3); }


/* ---------- an intro that leads the frame ----------
   A reader meeting a surface before any prose needs to be told what it is
   before they look at it. The line sits above the frame and replaces the
   caption and hint that would otherwise trail it. */
.live-intro {
  margin: 0 0 0.85rem;
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--text-body);
  max-width: 46rem;
}
.live--intro .live-stage { margin-top: 0; }

/* A bare poster button: the intro above already says what a click does, so the
   affordance is the pointer and the hover lift, not a word printed on the art. */
.live-idle.is-bare { padding: 0; }
.live-idle.is-bare::before {
  background: color-mix(in srgb, var(--bg) 8%, transparent);
  transition: background 140ms ease;
}
.live-idle.is-bare:hover::before {
  background: color-mix(in srgb, var(--bg) 0%, transparent);
}
a.card .thumb.mobile-cover { padding: 1.5rem; }
.mobile-stack { position: relative; display: block; height: 100%; width: 100%; }
a.card .mobile-stack img { position: absolute; height: 88%; width: auto; max-width: none; object-fit: contain; border-radius: 10px; border: 1px solid var(--border); box-shadow: 0 10px 24px rgba(12,19,27,0.1); }
a.card .mobile-stack img:first-child { left: 46%; top: 0; transform: rotate(5deg); }
a.card .mobile-stack img:last-child { right: 43%; bottom: 0; transform: rotate(-5deg); }
.ios-screens { display: flex; align-items: flex-start; gap: clamp(16px, 4vw, 40px); }
figure.ios-pair img { width: calc((100% - clamp(16px, 4vw, 40px)) / 2); max-width: 390px; height: auto; }
figure.ios-screen img { width: 100%; max-width: 390px; height: auto; }

@media (max-width: 40rem) {
  .ios-screens { flex-direction: column; }
  figure.ios-pair img { width: 100%; }
}
/* ---------- trade-off table ---------- */
.tradeoff-wrap {
  width: 100%;
  overflow-x: auto; margin-block: 1.5rem 2.25rem;
}
table.tradeoff { width: 100%; border-collapse: collapse; font-size: 0.875rem; min-width: 44rem; }
table.tradeoff th {
  text-align: left; font-family: var(--mono); font-size: 0.6875rem; font-weight: 400;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted);
  border-bottom: 1px solid var(--fg); padding: 0 0.9rem 0.5rem 0;
}
table.tradeoff td { padding: 0.8rem 0.9rem 0.8rem 0; border-bottom: 1px solid var(--border); vertical-align: top; line-height: 1.5; }
table.tradeoff .td-q { color: var(--fg); width: 22%; }
table.tradeoff .td-chose { width: 22%; }
table.tradeoff .td-chose b { color: var(--fg); font-weight: 600; }
table.tradeoff .td-not { width: 22%; color: var(--a4); text-decoration: line-through; text-decoration-color: var(--a3); }
table.tradeoff .td-why { color: var(--text-muted); }

/* ---------- Claude Code usage widget ---------- */
.tk {
  margin: 2rem 0 2.5rem;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); padding: 1.75rem 1.75rem 1.5rem;
}
.tk-head { margin: 0 0 1.5rem; }
.tk-kicker {
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-muted); margin: 0 0 0.75rem;
}
.tk-big { margin: 0; display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.tk-big b {
  font-weight: 600; color: var(--fg); font-size: clamp(3rem, 11vw, 4.5rem);
  line-height: 0.9; letter-spacing: -0.055em; font-variant-numeric: tabular-nums;
}
.tk-big small { font-size: 0.5em; letter-spacing: -0.04em; color: var(--a4); }
.tk-big span {
  font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-muted);
}

/* the heatmap: week columns, Sunday first, same shape /stats draws */
.tk-map { --tk-cell: 11px; --tk-gap: 3px; overflow-x: auto; padding-bottom: 0.25rem; }
.tk-months, .tk-grid {
  display: grid;
  grid-template-columns: repeat(var(--tk-weeks), var(--tk-cell));
  gap: var(--tk-gap);
  min-width: min-content;
}
.tk-months {
  margin-bottom: 0.4rem;
  font-family: var(--mono); font-size: 0.625rem; color: var(--a4);
}
.tk-months span { grid-row: 1; white-space: nowrap; }
.tk-grid { grid-template-rows: repeat(7, var(--tk-cell)); grid-auto-flow: column; }
.tk-c { display: block; width: var(--tk-cell); height: var(--tk-cell); border-radius: 2px; }
.tk-off { background: none; }
/* mixed into --surface, not --bg, so the empty level stays visible on the dark panel */
.tk-l0 { background: color-mix(in srgb, var(--fg) 12%, var(--surface)); }
.tk-l1 { background: color-mix(in srgb, var(--fg) 30%, var(--surface)); }
.tk-l2 { background: color-mix(in srgb, var(--fg) 50%, var(--surface)); }
.tk-l3 { background: color-mix(in srgb, var(--fg) 72%, var(--surface)); }
.tk-l4 { background: var(--fg); }

.tk-legend {
  display: flex; align-items: center; gap: 0.3rem;
  margin: 0.85rem 0 0; font-family: var(--mono); font-size: 0.625rem; color: var(--a4);
}
.tk-legend span { margin: 0 0.15rem; }
.tk-legend .tk-c { width: 9px; height: 9px; }
.tk-legend em { margin-left: auto; font-style: normal; }

/* two readings of the same number */
.tk-tabs { display: flex; gap: 0.4rem; margin: 1.75rem 0 1.25rem; }
.tk-tabs button {
  font: inherit; font-size: 0.8125rem; color: var(--text-muted);
  background: var(--bg); border: 1px solid var(--border); border-radius: 999px;
  padding: 0.3rem 0.85rem; cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.tk-tabs button:hover { color: var(--fg); border-color: var(--a3); }
.tk-tabs button[aria-selected="true"] { color: var(--bg); background: var(--fg); border-color: var(--fg); }
.tk-tabs button:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

.tk-face .figs { margin-bottom: 1.25rem; }
.tk-face .fig b { font-size: 1.375rem; }
.tk-note { margin: 0 0 0.9rem; font-size: 0.875rem; color: var(--text-muted); line-height: 1.6; }
.tk-note:last-of-type { margin-bottom: 0; }
.tk-note strong { color: var(--fg); font-weight: 600; }
.tk-note b { color: var(--fg); font-weight: 500; font-variant-numeric: tabular-nums; }

.tk-bars { list-style: none; margin: 0 0 1.25rem; padding: 0; }
.tk-bar {
  display: grid; align-items: center; gap: 0 0.75rem;
  grid-template-columns: 6.5rem 1fr 4rem 3.5rem;
  padding: 0.4rem 0; border-bottom: 1px solid var(--border);
}
.tk-bar:last-child { border-bottom: none; }
.tk-bar-l { font-size: 0.8125rem; color: var(--fg); }
.tk-bar-t { display: block; height: 6px; background: color-mix(in srgb, var(--fg) 12%, var(--surface)); border-radius: 3px; }
.tk-bar-t i { display: block; height: 100%; background: var(--fg); border-radius: 3px; }
.tk-bar-v, .tk-bar-p {
  font-family: var(--mono); font-size: 0.75rem; text-align: right;
  font-variant-numeric: tabular-nums;
}
.tk-bar-v { color: var(--fg); }
.tk-bar-p { color: var(--text-muted); }
.tk-bar-n { grid-column: 2 / -1; font-size: 0.75rem; color: var(--a4); }

.tk-models { list-style: none; margin: 0 0 1.25rem; padding: 0; }
.tk-models li { display: grid; grid-template-columns: 5rem 1fr 3rem; align-items: center; gap: 0.75rem; padding: 0.2rem 0; }
.tk-models span { font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted); }
.tk-models i { display: block; height: 4px; border-radius: 2px; background: color-mix(in srgb, var(--fg) 50%, var(--surface)); }
.tk-models em {
  font-family: var(--mono); font-size: 0.75rem; font-style: normal;
  color: var(--a4); text-align: right; font-variant-numeric: tabular-nums;
}

.tk-src { margin: 1.75rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); font-family: var(--mono); font-size: 0.625rem; color: var(--a4); line-height: 1.7; }
.tk-src code { font-size: 1em; background: none; border: none; padding: 0; color: var(--text-muted); }

/* card thumbnail */
a.card .thumb.tk-thumb { display: flex; align-items: center; justify-content: center; padding: 1rem; }
.tk-thumb .tk-map { overflow: hidden; }
.tk-thumb-n {
  position: absolute; right: 0.9rem; bottom: 0.7rem;
  font-family: var(--mono); font-size: 0.6875rem; color: var(--a4);
}

@media (max-width: 34rem) {
  .tk { padding: 1.25rem 1.1rem 1.1rem; }
  .tk-bar { grid-template-columns: 5.5rem 1fr 3.5rem; }
  .tk-bar-p { display: none; }
}

/* ---------- before / after ---------- */
.compare {
  width: 100%;
  display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-block: 1.5rem 0.75rem;
}
@media (max-width: 40rem) { .compare { grid-template-columns: 1fr; } }
.compare figure { margin: 0; }
.compare img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
}
.compare .is-after img { border-color: var(--a3); }
.compare figcaption {
  font-family: var(--mono); font-size: 0.75rem; line-height: 1.5;
  color: var(--text-muted); margin-top: 0.6rem;
}
.compare figcaption b {
  display: block; color: var(--fg); font-weight: 500; margin-bottom: 0.15rem;
  text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.6875rem;
}

/* ---------- gallery ---------- */
.gallery {
  width: 100%;
  display: grid; grid-template-columns: repeat(var(--cols, 4), 1fr); gap: 0.75rem;
  margin-block: 1.5rem 0.75rem;
}
@media (max-width: 46rem) { .gallery { grid-template-columns: repeat(2, 1fr); } }
.gallery figure { margin: 0; }
.gallery img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface);
  transition: border-color 120ms ease;
}
.gallery a:hover img { border-color: var(--a3); }
.gallery .is-pick img { border-color: var(--fg); border-width: 2px; }
.gallery figcaption {
  font-family: var(--mono); font-size: 0.6875rem; line-height: 1.4;
  color: var(--text-muted); margin-top: 0.4rem;
}
.gallery .is-pick figcaption { color: var(--fg); }

/* ---------- decision blocks ---------- */
ol.decisions { list-style: none; margin: 1.5rem 0 2.25rem; padding: 0; }
.dec { border-top: 1px solid var(--border); padding: 1.1rem 0; }
.dec:last-child { border-bottom: 1px solid var(--border); }
.dec-q { margin: 0 0 0.5rem; color: var(--fg); font-weight: 600; letter-spacing: -0.02em; }
.dec-n { font-family: var(--mono); font-size: 0.75rem; font-weight: 400; color: var(--a4); margin-right: 0.6rem; }
.dec-pick { margin: 0 0 0.4rem; font-size: 0.9375rem; }
.dec-pick b { color: var(--fg); font-weight: 600; }
.dec-over { color: var(--a4); margin-left: 0.5rem; }
.dec-over::before { content: ""; }
.dec-why { margin: 0; font-size: 0.9375rem; color: var(--text-muted); }

/* ---------- before / after switch ---------- */
/* The toggle leads the frame: a reader has to know this is a comparison
   before they look at it, not after. Selectors are doubled on
   `.live-steps.cmp-steps` so they outrank the flow-strip base rules
   without reaching for !important. */
.cmp-head { margin: 1.75rem 0 0; }
.cmp-title {
  margin: 0 0 0.65rem; font-family: var(--mono); font-size: 0.75rem;
  line-height: 1.5; color: var(--text-muted);
}
.live-steps.cmp-steps {
  display: inline-flex; gap: 0; margin-top: 0; position: relative;
  border: 1px solid var(--border); border-radius: 999px; padding: 3px;
  /* reserves the caption slot, so switching never moves the frame */
  margin-bottom: 3.4rem;
}
.live-steps.cmp-steps button {
  display: block; padding: 0.32rem 1rem; border-radius: 999px;
  color: var(--text-muted); text-align: center;
  transition: background-color 120ms ease, color 120ms ease;
}
.live-steps.cmp-steps button b {
  display: block; font-family: var(--mono); font-weight: 500;
  font-size: 0.6875rem; letter-spacing: 0.06em; text-transform: uppercase;
}
.live-steps.cmp-steps button:hover { color: var(--fg); }
.live-steps.cmp-steps button.is-active { background: var(--fg); color: var(--bg); }
.live-steps.cmp-steps button .cmp-cap {
  position: absolute; left: 0; top: calc(100% + 0.6rem);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  width: max-content; max-width: min(var(--read), calc(100vw - 3rem));
  margin-top: 0; text-align: left;
  font-family: inherit; font-size: 0.8125rem; line-height: 1.5;
  color: var(--text-muted); opacity: 0; pointer-events: none;
  transition: opacity 120ms ease;
}
.live-steps.cmp-steps button.is-active .cmp-cap { opacity: 1; color: var(--text-muted); }
.live-steps.cmp-steps button:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

/* ---------- gallery ---------- */
.gallery {
  width: 100%;
  display: grid; grid-template-columns: repeat(var(--cols, 4), 1fr); gap: 0.75rem;
  margin-block: 1.5rem 0.75rem;
}
@media (max-width: 46rem) { .gallery { grid-template-columns: repeat(2, 1fr); } }
.gallery figure { margin: 0; }
.gallery img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface);
  transition: border-color 120ms ease;
}
.gallery a:hover img { border-color: var(--a3); }
.gallery .is-pick img { border-color: var(--fg); border-width: 2px; }
.gallery figcaption {
  font-family: var(--mono); font-size: 0.6875rem; line-height: 1.4;
  color: var(--text-muted); margin-top: 0.4rem;
}
.gallery .is-pick figcaption { color: var(--fg); }

/* ---------- decision blocks ---------- */
ol.decisions { list-style: none; margin: 1.5rem 0 2.25rem; padding: 0; }
.dec { border-top: 1px solid var(--border); padding: 1.1rem 0; }
.dec:last-child { border-bottom: 1px solid var(--border); }
.dec-q { margin: 0 0 0.5rem; color: var(--fg); font-weight: 600; letter-spacing: -0.02em; }
.dec-n { font-family: var(--mono); font-size: 0.75rem; font-weight: 400; color: var(--a4); margin-right: 0.6rem; }
.dec-pick { margin: 0 0 0.4rem; font-size: 0.9375rem; }
.dec-pick b { color: var(--fg); font-weight: 600; }
.dec-over { color: var(--a4); margin-left: 0.5rem; }
.dec-over::before { content: ""; }
.dec-why { margin: 0; font-size: 0.9375rem; color: var(--text-muted); }

/* ---------- before / after switch ---------- */
/* The toggle leads the frame: a reader has to know this is a comparison
   before they look at it, not after. */
.cmp-head { margin: 1.5rem 0 0.6rem; }
.cmp-title {
  margin: 0 0 0.6rem; font-family: var(--mono); font-size: 0.75rem;
  line-height: 1.5; color: var(--text-muted);
}
.cmp-steps {
  display: inline-flex !important; grid-template-columns: none !important;
  gap: 0 !important; position: relative;
  border: 1px solid var(--border); border-radius: 999px; padding: 3px;
  /* the active caption sits in this reserved slot, so switching never
     moves the frame below */
  margin-bottom: 2.1rem;
}
.cmp-btn {
  display: block; cursor: pointer; background: none; border: 0; font: inherit;
  padding: 0.3rem 0.95rem; border-radius: 999px; color: var(--text-muted);
  transition: background-color 120ms ease, color 120ms ease;
}
.cmp-btn b {
  display: block; font-weight: 500; font-size: 0.75rem;
  font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
}
.cmp-btn:hover { color: var(--fg); }
.cmp-btn.is-active { background: var(--fg); color: var(--bg); }
/* Both captions share one absolutely-positioned slot under the toggle;
   only the active one is visible. */
.cmp-cap {
  position: absolute; left: 0; top: calc(100% + 0.55rem); width: max-content;
  max-width: min(var(--read), 100%);
  font-size: 0.8125rem; line-height: 1.5; color: var(--text-muted);
  text-align: left; opacity: 0; pointer-events: none;
  transition: opacity 120ms ease;
}
.cmp-btn.is-active .cmp-cap { opacity: 1; }
.cmp-btn:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
@media (max-width: 40rem) {
  .cmp-steps { margin-bottom: 3rem; }
  .cmp-cap { max-width: calc(100vw - 3rem); }
}

/* ---------- context diagram ---------- */
.diagram { margin: 2rem 0; }
.diagram svg { width: 100%; height: auto; display: block; color: var(--a3); }
.dg-lane {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
  fill: var(--text-muted);
}
.dg-node rect { fill: var(--surface); stroke: var(--border); stroke-width: 1; }
.dg-node text {
  font-family: var(--sans); font-size: 15px; font-weight: 500;
  fill: var(--fg); text-anchor: middle;
}
.dg-end rect { stroke: var(--fg); }
.dg-ghost rect { fill: none; stroke-dasharray: 4 4; }
.dg-ghost text { fill: var(--text-muted); font-weight: 400; }
.dg-link { fill: none; stroke: currentColor; stroke-width: 1.5; }
.dg-split { stroke: var(--border); stroke-width: 1.5; stroke-dasharray: 4 4; }
.dg-note { font-family: var(--mono); font-size: 11px; fill: var(--text-muted); }
.diagram figcaption {
  font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted);
  margin-top: 0.75rem; display: flex; gap: 0.75rem; justify-content: space-between; flex-wrap: wrap;
}
.diagram figcaption b { color: var(--a4); font-weight: 400; }

/* a second line inside a diagram node */
.dg-sub { font-family: var(--mono); font-size: 11px; fill: var(--text-muted); text-anchor: middle; }

/* ---------- what the information follows ---------- */
.follows { list-style: none; margin: 1.5rem 0 2rem; padding: 0; border-top: 1px solid var(--border); }
.follows li {
  display: grid; grid-template-columns: 13rem 1fr; gap: 1rem; align-items: baseline;
  padding: 0.7rem 0; border-bottom: 1px solid var(--border);
}
@media (max-width: 36rem) { .follows li { grid-template-columns: 1fr; gap: 0.2rem; } }
.follows b {
  font-family: var(--mono); font-size: 0.75rem; font-weight: 500; color: var(--fg);
  letter-spacing: -0.005em;
}
.follows span { font-size: 0.9375rem; line-height: 1.55; color: var(--text-muted); }

/* ---------- version strip ---------- */
.vstrip {
  width: 100%;
  margin-inline: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 1rem;
  margin-block: 1.75rem 0.75rem;
}
@media (max-width: 52rem) { .vstrip { grid-template-columns: repeat(2, 1fr); } }
.vs-cell { margin: 0; }
.vs-cell img {
  width: 100%; display: block; aspect-ratio: 16 / 10; object-fit: cover; object-position: top left;
  border: 1px solid var(--border); border-radius: 6px; background: var(--surface);
  transition: border-color 120ms ease;
}
.vs-cell a:hover img { border-color: var(--a3); }
.vs-cell figcaption { margin-top: 0.6rem; }
.vs-cell b {
  display: block; font-family: var(--mono); font-size: 0.8125rem; color: var(--fg); font-weight: 500;
}
.vs-date {
  display: block; font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted); margin-top: 0.1rem;
}
.vs-what {
  display: block; font-size: 0.8125rem; line-height: 1.5; color: var(--text-muted); margin-top: 0.4rem;
}

/* ---------- version cell states ---------- */
.vs-shot img { cursor: default; }
.vs-states { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.5rem; }
.vs-state {
  font-family: var(--mono); font-size: 0.625rem; line-height: 1.4;
  color: var(--text-muted); background: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: 4px; padding: 0.12rem 0.4rem;
  transition: border-color 120ms ease, color 120ms ease;
}
.vs-state:hover { color: var(--fg); border-color: var(--a3); }
.vs-state.is-active { color: var(--fg); border-color: var(--fg); }

@media (max-width: 34rem) { .vstrip { grid-template-columns: 1fr; gap: 1.5rem; } }
/* ---------- skin overrides ----------
   Loaded last on purpose. The parts stylesheets concatenate in filename order,
   so anything here has to outrank 60-trade-off-table.css, which sets its own
   pill and caption colours from the old black-and-white skin.
*/

/* ---------- pills ----------
   The main site marks a selected state with the project accent, not with an
   inverted black chip. Same shape, same size, different signal. */
/* A segmented control is one container and two segments. The container already
   carries the capsule border and 3px of padding (60-trade-off-table.css), so a
   border on each segment stacks three outlines inside each other. The segments
   carry fill only, and the selected one is the only thing that fills.
   60-trade-off-table.css sets the selected fill from a four-class selector, so
   these have to match that weight to land at all. */
.live-steps.cmp-steps { border-color: var(--hairline); }

.live-steps.cmp-steps button,
.cmp-btn {
  background: none;
  border: 0;
  color: var(--text-body);
}
.live-steps.cmp-steps button.is-active,
.cmp-btn.is-active {
  background: rgba(249, 115, 22, 0.16);
  border: 0;
  color: var(--accent);
}
.live-steps.cmp-steps button b,
.cmp-btn b { color: var(--text-body); font-weight: 500; }
.live-steps.cmp-steps button.is-active b,
.cmp-btn.is-active b { color: var(--accent); }
.live-steps.cmp-steps button:hover:not(.is-active) { background: rgba(12, 19, 27, 0.04); }

.cmp-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.live-play {
  border-radius: 999px;
  border-color: var(--hairline);
  background: var(--paper);
}
.live-idle:hover .live-play { border-color: var(--accent-60); color: var(--accent); }

/* ---------- caption contrast ----------
   A caption that says what a frame is has to be readable; the tertiary tone is
   for things a reader may skip, not for the line that tells them what they are
   looking at. */
.cmp-title,
.live-cap,
.live-cap span,
.live-hint { color: var(--text-body); }
.live-cap b { color: var(--text-muted); }

/* A step strip reads as a row of pills too. */
.live-steps button { border-radius: 12px; }
.live-steps button.is-active { border-color: var(--accent-60); }

/* A figure caption and the role block are both saying what a reader is looking
   at. The tertiary tone is for things that can be skipped; neither of these
   can. */
figure figcaption,
figure figcaption span,
.role { color: var(--text-body); }
figure figcaption b,
.role b { color: var(--fg); }
.compare figcaption { color: var(--text-body); }
.gallery figcaption { color: var(--text-body); }

/* ---------- case, not shouting ----------
   Before and After are words, not labels. The old skin set every small label in
   uppercase with tracking, which the main site does not do. The source strings
   are already sentence case; only the transform was making them shout. */
.live-steps.cmp-steps button b,
.cmp-btn b,
.compare figcaption b {
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.8125rem;
}

/* The commit-ladder tags come out of the data lowercase and were being shouted
   by the transform. Capitalize rather than none, so they read as words with a
   capital and not as an unstyled string. */
.log-kind {
  text-transform: capitalize;
  letter-spacing: 0;
  font-size: 0.6875rem;
  border-color: var(--hairline);
}

/* Card numbers were cut as small caps; the strings are sentence case. */
.card-no,
a.card--hero .card-no,
a.card--hero-lead .card-no {
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.75rem;
}
