/* ==========================================================================
   v3.0 THE LANDING HERO — VARIANT B (landing-b/), pass B1.

   Derived wholesale from landing/hero.css (variant A, R5) — A is FROZEN and
   this file is its sibling, not its successor. Every rule below that is not
   marked B1 is A's, byte for byte. B1 changes exactly five things:
     · --stage-h 868 → 812 (the stage lands at --fit 1 at the reference)
     · the two toolbar bars take STADIUM ENDS
     · FOUR wire inks instead of three
     · the Iterate block's window loop returns (.img-loop)
     · the Text block's enhance bar moves INSIDE the card, under a divider

   FIDELITY LAW: every value below is COPIED from the app's src/index.css
   (source noted in the comments) — never imported, never approximated.
   The landing page ships standalone; when a token changes in the app it is
   changed here by hand, deliberately.

   Both themes ship. DARK is the default (the ground the studio is known by);
   the toolbar's switch is real and remembers.
   ========================================================================== */

/* THE FONT LAW (index.css §--font-primary): ONE family, Source Sans 3,
   400 + 700 + the 900 Black cut the wordmark wears. Self-hosted. */
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal; font-weight: 400; font-display: swap;
  src: url('./assets/fonts/source-sans-3-latin-400-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal; font-weight: 700; font-display: swap;
  src: url('./assets/fonts/source-sans-3-latin-700-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Source Sans 3';
  font-style: normal; font-weight: 900; font-display: swap;
  src: url('./assets/fonts/source-sans-3-latin-900-normal.woff2') format('woff2');
}

/* ---- LIGHT: index.css :root — THE MINIMAL DEFAULT (v2.94 G2), the
   value-inverted twin of Kevin's authored dark five --------------------- */
:root {
  --paper:        #EDEDED;
  --paper-line:   #D1D1D1;
  --ink:          #1F1F1F;
  --ink-soft:     #4F4F4F;
  --ink-faint:    #919191;

  --card:         #E4E4E4;
  --stage:        #E9E9E9;
  --card-hi:      #DDDDDD;
  --card-top:     #D7D7D7;
  --card-border:  #BBBBBB;
  --card-shadow:  0 1px 2px rgba(28,28,26,0.04), 0 6px 20px rgba(28,28,26,0.05);
  --card-shadow-lift: 0 2px 4px rgba(28,28,26,0.06), 0 14px 34px rgba(28,28,26,0.10);

  --blueprint:    #777777;
  --blueprint-ink:#5E5E5E;
  --select:       #0A8CFF;

  /* v2.32.2 C — ONE action blue, sampled from the wordmark's J */
  --action:       #95B1CE;
  /* v2.8 G0 — THE BRAND BLUE for the swept ink. --action is a MID-LIGHT STEEL
     (index.css v2.32.2 C: "the blue is mid-light, so the LABEL is dark ink") —
     right as a button fill under dark type, wrong as INK on the dark canvas,
     where it reads grey. The app's ONE bright blue is --select (index.css
     v2.71 S1: "deliberately ELECTRIC ... the dark theme brightens it to true
     neon"), so the sweep is pinned to it. */
  --brand-blue:   #0A8CFF;
  /* v2.10 G3 — the swept ink's BASE. Kevin's effect is white↔brand-blue, and
     white is what the dark ground wants. On the LIGHT ground white is nothing
     at all (the 2.9 light shot has an invisible headline), so the light theme
     bases the sweep on the house ink and the blue band travels through THAT.
     One mechanism, one clock; only the base changes with the ground. */
  --sweep-base:   #1F1F1F;
  /* v2.10 G4 — THE NEON ROSTER (Kevin's assignment). Four signage inks, each
     derived from the app family that owns the hue and pushed to a true neon:
       Architects    blue   --select     #0A8CFF  → used as-is (already neon)
       Designers     green  --geo-accent #2F9E6E  → #00A85C light / #2BFF9B dark
       Collaborators pink   --animate    #B5468A  → #D6008E light / #FF4FC3 dark
       You           yellow --txt-port   #B0752F  → #B8930A light / #FFE83D dark
     The light theme takes each a step darker so it holds on #EDEDED — a true
     neon yellow cannot, so light's "You" is the saturated gold beneath it. */
  --neon-blue:    #0A8CFF;
  --neon-green:   #00A85C;
  --neon-pink:    #D6008E;
  --neon-yellow:  #B8930A;
  --action-hover: #86A5C6;
  --action-edge:  #7E9CBB;
  --action-ink:   #14232F;

  --img-port:     #245BB5;
  --txt-port:     #B0752F;
  --render-port:  #6D4AB5;
  --mesh-accent:  #5B7083;
  --animate-accent:#B5468A;
  --note-accent:  #C79A2E;   /* the Collaborator's yellow */
  --sketch-accent:#D2604A;   /* the canvas's drawing red (Sketch) */
  --iterate-accent:#2C9C9C;  /* Iterate — teal   (the rotator's Designers)   */
  --geo-accent:   #2F9E6E;   /* GeoLock — green  (the rotator's Contractors) */

  --win-hi:       #FAF7F0;
  --hatch-line:   rgba(28, 28, 26, 0.10);

  /* the toolbar pill surface (index.css .toolbar) */
  --pill-bg:      rgba(255,255,255,0.86);

  --font-primary:   'Source Sans 3', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --font-secondary: 'Source Sans 3', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --mono:  var(--font-secondary);
  --sans:  var(--font-secondary);

  /* type scale — color codes TYPE, size codes IMPORTANCE (index.css §scale) */
  --t-display: 700 22px/1.2 var(--font-primary);
  --t-title:   700 13px/1.3 var(--font-primary);
  --t-ui:      700 12px/1.4 var(--font-primary);
  --t-body:    400 12.5px/1.5 var(--font-secondary);
  --t-meta:    400 10px/1.3 var(--font-secondary);

  --radius: 7px;
  --hdr-ctl: 28px;      /* index.css — ONE header control height */
  --hdr-center: 37px;   /* index.css — ONE header centre line */

  /* R2 G0 — THE BAR'S SEAT. --tb-gap is R1's seat, derived from the app's own
     header centre line exactly as it always was (37 − 14 − 9 = 14px). Kevin's
     move TRIPLES it: 14px → 42px. --tb-band is the whole strip the chrome now
     occupies (42 + the pill's own 28 + 18 = 88px) and it is what the centre
     stack centres BELOW — see .hero-copy. Authored, never measured: the copy
     reads a constant, the copy's headline is what the bars read, and nothing
     reads back. */
  --tb-gap:  calc(var(--hdr-center) - var(--hdr-ctl) / 2 - 9px);
  --tb-top:  calc(var(--tb-gap) * 3);
  --tb-band: calc(var(--tb-top) + var(--hdr-ctl) + 18px);

  /* the composition's design frame — main.js scales it to the viewport.
     R1 — the frame grew from 560 to 720: the four blocks stand at the
     corners of a loose pinwheel and the centre stack lives in the band
     between them, which is height the old frame never had.
     R2 G2 — 720 → 800: Kevin's sketch grows every block (1.15× to 1.35×) and
     the tripled toolbar gap takes 28px off the ceiling, so the pinwheel needs
     the room. 800 still fits 1440×900 at --fit 1 (820px of margin-boxed
     height available), so the reference viewport is unscaled as before.
     R5 G1 — 800 → 868, AND IT COSTS THE SCALE. The Animate block's sketched
     seat puts its floor at stage 884, which no scale of an 800-tall stage can
     land inside a 900px window: the drop has to be bought with --fit, and 868
     is what buys it (812 / 868 = 0.935 at the reference viewport, so the whole
     composition ships 6.5% smaller than R4's). The reference viewport is NO
     LONGER unscaled — that is the named cost of the seat, not a drift. */
  --stage-w: 1360px;
  /* B1 G0 — 812, and the reference viewport is UNSCALED again (--fit 1).
     812 = 900 − 88: the canvas below the chrome band, exactly. That is what
     makes the drawing readable in the source — stage (x + 40, y + 88) is
     Kevin's own viewport pixel, so a seat at 0.25H is a seat at stage 137 and
     nobody has to divide by a scale to check it. */
  --stage-h: 812px;
}

/* ---- DARK: index.css :root[data-theme="dark"] — STUDIO NIGHT ---------- */
:root[data-theme="dark"] {
  --paper:        #121212;
  --paper-line:   #2E2E2E;
  --ink:          #E0E0E0;
  --ink-soft:     #B0B0B0;
  --ink-faint:    #6E6E6E;

  --stage:        #161616;
  --card:         #1B1B1B;
  --card-hi:      #222222;
  --card-top:     #282828;
  --card-border:  #444444;
  --card-shadow:  0 1px 2px rgba(0,0,0,0.30), 0 6px 20px rgba(0,0,0,0.40);
  --card-shadow-lift: 0 2px 4px rgba(0,0,0,0.40), 0 14px 34px rgba(0,0,0,0.55);

  --blueprint:    #888888;
  --blueprint-ink:#6F6F6F;
  --select:       #2E9BFF;
  --brand-blue:   #2E9BFF;   /* index.css: the dark theme brightens it to neon */
  --sweep-base:   #FFFFFF;
  --neon-blue:    #2E9BFF;
  --neon-green:   #2BFF9B;
  --neon-pink:    #FF4FC3;
  --neon-yellow:  #FFE83D;

  --img-port:     #4A76B8;
  --txt-port:     #C9955C;
  --render-port:  #9C86C9;
  --mesh-accent:  #93A5B4;
  --animate-accent:#C77FA8;
  --note-accent:  #CBB05E;
  --sketch-accent:#D98D77;
  --iterate-accent:#64B0B0;
  --geo-accent:   #6FAE8F;

  --win-hi:       #2B2A25;
  --hatch-line:   rgba(233, 228, 218, 0.10);

  /* index.css :root[data-theme="dark"] .toolbar */
  --pill-bg:      rgba(36,35,32,0.86);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0;
  width: 100%;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  /* CLIP, not hidden: `overflow-x: hidden` makes html/body a scroll container
     and a scroll container kills `position: sticky` inside it — the hero
     would never pin and the whole score would scroll away. */
  overflow-x: clip;
}
html { scroll-behavior: auto; }

/* ==========================================================================
   THE TOOLBAR — fixed, ONE CENTRED PILL (index.css .toolbar geometry: the one
   control height, the one centre line, the 10px pill radius)
   ========================================================================== */
/* R2 G0 — THE BAR SPLITS, AND BOTH HALVES SIT ON THE HEADLINE.
   R1's single centred pill is repealed. There are TWO bars now and they do not
   centre: the LEFT bar's left edge sits on "Think"'s left edge and the RIGHT
   bar's right edge on "Seconds."'s right edge — the headline's own Range rect,
   written to --hl-l / --hl-w by main.js.

   THIS IS A SOLVER, and R1 deleted all of them, so it is worth being exact
   about why this one is allowed: it runs in ONE DIRECTION. The headline is
   authored (a fixed string at a fixed size on a centred column) and frozen in
   the manifest; the chrome reads it; nothing reads back. The vertical is not
   measured at all — the bar's seat and the strip it occupies are the authored
   --tb-top / --tb-band above. A cycle is what made 2.8–2.11 chase; a read of a
   frozen thing is just alignment. */
.lb-toolbar {
  position: fixed; z-index: 40;
  top: var(--tb-top);
  left: var(--tb-l, 14px);
  width: var(--tb-w, calc(100% - 28px));
  right: auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  pointer-events: none; /* the gaps between the bars are canvas, not chrome */
}
/* index.css .toolbar .rule — the app's own divider, verbatim */
.lb-rule {
  width: 1px; height: 20px; background: var(--card-border); margin: 0 2px;
  align-self: center; flex: none;
}
.lb-pill {
  pointer-events: auto;
  min-height: calc(var(--hdr-ctl) + 18px);
  display: inline-flex; align-items: center; gap: 10px;
  padding: 8px 16px;
  background: var(--pill-bg);
  backdrop-filter: blur(8px);
  border: 1px solid var(--card-border);
  /* B1 G0 — STADIUM ENDS, the toolbars' ONE difference from A. The radius is
     HALF THE BAR'S OWN HEIGHT, written as the same arithmetic that sets the
     height (min-height above), so the caps stay round if the bar ever changes:
     (28 + 18) / 2 = 23px, where A's was a flat 10px. */
  border-radius: calc((var(--hdr-ctl) + 18px) / 2);
  box-shadow: var(--card-shadow);
  text-decoration: none;
}
/* v2.7 G2 — one row, uniform text size, the app's rules between items.
   .lb-item is the ONE size — the wordmark and LOGIN wear it too, so both bars
   read as one bar cut in half. */
.lb-markline { display: inline-flex; align-items: center; }
.lb-bar-left, .lb-bar-right { flex: none; }
.lb-item {
  font: var(--t-ui); font-size: 13px; letter-spacing: 0.06em;
  color: var(--ink-soft); text-decoration: none;
  transition: color .12s ease;
}
.lb-item:hover { color: var(--ink); }
/* Kevin's leaning-figure J as an alpha mask, tinted by currentColor
   (index.css .jimin-mark-logo) */
.jimin-mark-logo {
  width: 22px; height: 22px; display: block;
  background: var(--brand-blue);   /* the mark and the word share one ink */
  -webkit-mask-image: url('./assets/favicon.png'); mask-image: url('./assets/favicon.png');
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
}
/* index.css .projects-wordmark — Source Sans 3 Black (900), action blue.
   v2.6 G1: the ink is the SHARED sweep (.gradient-ink below) — one keyframes,
   one duration, one start, so the wordmark and line 1 breathe together. */
/* the wordmark keeps the Black cut and the gradient, but joins .lb-item's
   size — one bar, one type size. R2 G0: the word is UPPERCASE now, set in the
   markup rather than by text-transform so the string the suite reads and the
   string the visitor sees are the same string. */
.lb-wordmark { font-weight: 900; }

/* v2.6 G1 + G5 — THE SHARED SWEEP. The gradient runs white → the Jimin blue
   (index.css --action, v2.32.2 C: the one action blue, sampled from the
   wordmark's J) and back, clipped to the glyphs. Both wearers use THIS rule —
   same @keyframes, same 16s, same phase (both start at page load) — so they
   are never two clocks pretending to agree. */
/* v2.8 G0 — WHERE THE DARK CAME FROM. Two causes, both named and both fixed:
   (1) the ramp's blue end was var(--action), a MID-LIGHT STEEL — grey as ink
       on the dark canvas. It is --brand-blue now.
   (2) `background-clip: text` was WASHING THE WHOLE LINE. Measured, not
       guessed: with the clip on, the brightest pixel in the headline was
       rgb(180,180,180); the same text painted plainly reached rgb(255,255,255),
       and the same gradient UNCLIPPED reached rgb(255,255,255) too. Chrome
       rasterises the text-as-mask at partial coverage here, so every glyph
       came out about 67% of its colour -- over #121212 that is a grey wash,
       and it is why the line read "weird dark" whichever blue went into the
       ramp. (2.7's pixel-period fix made it MOVE; this is the colour half of
       the same bug.)
   THE MECHANISM NOW: no clipping. The text paints WHITE at full fidelity and
   a second copy of it, in the brand blue, lies exactly on top, revealed
   through a slanted clip-path BAND that travels across the line. White
   dominates by construction; the blue is a band passing through; both inks
   are exact. ONE @keyframes, one duration, one start -- the wordmark and
   line 1 tick together, never on two clocks. */
/* v2.10 G3 — ONE EFFECT, ONE CLOCK, FOUR SURFACES. The wordmark, line 1,
   line 2 and line 3's static run all wear this. Two rules make that possible:
     · every host HUGS ITS OWN GLYPHS (inline-block, .sweep-run), so the band
       travels across the text and never across empty box. Line 2's paragraph
       is 700px wide and its sentence 530 — masking the paragraph would have
       parked the blue over nothing for half the cycle.
     · the tile is the HOST's own width (--sw-tile, written by main.js), so a
       70px wordmark and a 1370px headline each see exactly ONE band pass per
       cycle. Same @keyframes, same 9s, same start ⇒ same phase: at any instant
       all four are at the same point of their own sweep. */
.gradient-ink {
  position: relative;
  color: var(--sweep-base);
}
.sweep-run { display: inline-block; }
.ink-sweep {
  position: absolute; left: 0; top: 0; right: 0; bottom: 0;
  color: var(--brand-blue);
  pointer-events: none;
  white-space: inherit;
  text-shadow: none;   /* the halo belongs to the base copy, and only once */
  /* v2.9 G1 — THE BAND IS FEATHERED. A clip-path cut the glyphs at a cliff:
     half a letter white, half solid blue. An ALPHA MASK ramps instead, so the
     blue eases in and out of the white beneath it and no boundary is ever
     visible. This is a mask on the ELEMENT's output, not `background-clip:
     text` on its fill — the banned mechanism (v2.8 G0: Chrome rasterises a
     text-mask at ~67% coverage and washes the line grey). The white base is a
     separate element and is never masked, so its pixels stay a true 255.
     The tile is WIDER than the line and shifts by exactly one tile, so the
     loop is seamless and a single band crosses per cycle — and the offsets
     are PIXELS, never percentages (v2.7 G0: a percentage background/mask
     offset resolves against (box − image) and flies the wrong way). */
  --sw-tile: 2400px;   /* main.js re-writes this per surface, every layout */
  -webkit-mask-image: linear-gradient(100deg,
    transparent 0px, transparent calc(var(--sw-tile) * 0.3167),
    rgba(0, 0, 0, 0.55) calc(var(--sw-tile) * 0.4083),
    #000 calc(var(--sw-tile) * 0.4833), #000 calc(var(--sw-tile) * 0.5167),
    rgba(0, 0, 0, 0.55) calc(var(--sw-tile) * 0.5917),
    transparent calc(var(--sw-tile) * 0.6833), transparent var(--sw-tile));
  mask-image: linear-gradient(100deg,
    transparent 0px, transparent calc(var(--sw-tile) * 0.3167),
    rgba(0, 0, 0, 0.55) calc(var(--sw-tile) * 0.4083),
    #000 calc(var(--sw-tile) * 0.4833), #000 calc(var(--sw-tile) * 0.5167),
    rgba(0, 0, 0, 0.55) calc(var(--sw-tile) * 0.5917),
    transparent calc(var(--sw-tile) * 0.6833), transparent var(--sw-tile));
  -webkit-mask-size: var(--sw-tile) 100%; mask-size: var(--sw-tile) 100%;
  -webkit-mask-repeat: repeat; mask-repeat: repeat;
  -webkit-mask-position: 0 0; mask-position: 0 0;
  animation: line-sweep 9s linear infinite;
}
@keyframes line-sweep {
  to { -webkit-mask-position: var(--sw-tile) 0; mask-position: var(--sw-tile) 0; }
}


/* index.css .theme-switch — the pill switch, verbatim */
.theme-switch {
  position: relative; flex: none;
  width: 38px; height: 20px;
  border-radius: 11px;
  border: 1px solid var(--card-border);
  background: var(--paper-line);
  cursor: pointer; padding: 0;
  transition: background .16s ease, border-color .16s ease;
}
.theme-switch.on { background: var(--action); border-color: var(--action-edge); }
.theme-switch-knob {
  position: absolute; top: 1px; left: 1px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--card);
  box-shadow: 0 1px 2px rgba(0,0,0,0.25);
  transition: transform .16s ease;
}
.theme-switch.on .theme-switch-knob { transform: translateX(18px); background: var(--action-ink); }
.theme-switch-glyph {
  position: absolute; top: 50%; transform: translateY(-50%);
  font-size: 9px; line-height: 1; pointer-events: none;
}
.theme-switch-glyph.sun { left: 4px; color: #B8860B; opacity: 1; }
.theme-switch-glyph.moon { right: 4px; color: #EDEBE4; opacity: 0; }
.theme-switch.on .theme-switch-glyph.sun { opacity: 0; }
.theme-switch.on .theme-switch-glyph.moon { opacity: 1; }

/* ==========================================================================
   THE SCORE TRACK — the hero pins over a tall track; scroll drives the beats
   ========================================================================== */
.scroll-track { position: relative; height: 620vh; }
.hero {
  position: sticky; top: 0;
  width: 100%; height: 100vh; min-height: 620px;
  overflow: hidden;
}
/* SEAM: where Cindy's sections attach — the hero releases into normal scroll */
.after-hero { min-height: 40vh; background: var(--paper); }

/* THE DOT GRID — the studio's own ground. The canvas draws it with
   <Background variant={Dots} gap={22} size={1.6} color="var(--paper-line)" />
   (App.jsx:5186); an SVG dot pattern of that gap/size is exactly this tile. */
.hero-ground {
  position: absolute; inset: 0;
  background-color: var(--paper);
  background-image: radial-gradient(circle at 1px 1px, var(--paper-line) 0.8px, transparent 0.9px);
  background-size: 22px 22px;
}

/* the composition, in design coordinates; scaled as a whole (the canvas's
   own "fit" gesture — blocks, wires and type scale together)

   R5 G1 — THE STAGE CENTRES ON THE CANVAS, NOT THE WINDOW. R3 flagged this as
   latent and did not fix it: the stage is SIZED against the canvas (main.js
   fits it into innerHeight − --tb-band) but was CENTRED on the whole window,
   so the moment --fit fell below 1 the stage's top edge pinned at 44px — 44px
   INSIDE the 88px chrome band — and the top of the composition slid under the
   toolbar. Measured on R4's shipped build: at 1440×880 (--fit 0.99) the Render
   caption already sat at 84.7px, under a band whose lower edge is 88.
   G1 is what forced the fix. Growing the stage is a --fit change by
   construction, and every --fit below 1 spent the ceiling: the Render caption
   would have gone 4.6px under the bar to buy Animate its floor. Centred on the
   CANVAS the two edges stop trading against each other — stage y 0 lands on
   the band's lower edge at every scale — which is what lets the 3D block reach
   its sketched seat AND Animate reach its floor in the same pass. */
.hero-stage {
  position: absolute;
  left: 50%; top: calc(50% + var(--tb-band) / 2);
  width: var(--stage-w); height: var(--stage-h);
  margin-left: calc(var(--stage-w) / -2);
  margin-top: calc(var(--stage-h) / -2);
  transform: scale(var(--fit, 1));
  transform-origin: 50% 50%;
}

/* ---- wires (index.css .react-flow__edge-path) -------------------------- */
.wires {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible; pointer-events: none;
  z-index: 1;
}
.wire { fill: none; stroke: var(--ink-soft); stroke-width: 1.5; }
/* B1 G2 — FOUR WIRES, FOUR INKS, by A's own payload rule: a leg carrying
   GEOMETRY wears the accent of THE ROOM IT ARRIVES IN, a leg carrying WORDS
   keeps its own ink the whole way.

     Site   → Render:IMG    --render-port     geometry, arriving violet
     Render → Iterate:IMG   --iterate-accent  geometry, arriving teal
     Render → Animate:IMG   --animate-accent  geometry, arriving magenta
     Text   → Animate:TXT   --sketch-accent   words, the warm family's red step

   B's graph is what finally makes the arriving-room rule pay: THREE geometry
   legs arrive in three different rooms and take three different inks from one
   sentence of law. The words leg keeps A's --sketch-accent — the TXT family's
   red step, one warm move toward the magenta room it lands in. All four
   resolve distinct in both themes and the suite proves it. The ink is declared
   on the path (data-ink) so a wire and its stroke can never drift apart, and
   every token moves with the theme. */
.wire[data-ink="render"]   { stroke: var(--render-port); }
.wire[data-ink="iterate"]  { stroke: var(--iterate-accent); }
.wire[data-ink="animate"]  { stroke: var(--animate-accent); }
.wire[data-ink="txt-anim"] { stroke: var(--sketch-accent); }
/* v2.69 GROUP C — THE RUNNING GRAMMAR, verbatim: a quiet slow dash drift
   toward the target (negative offset = forward along source→target) */
.wire.marching {
  stroke-dasharray: 7 5;
  animation: edge-flow 1.4s linear infinite;
}
@keyframes edge-flow { to { stroke-dashoffset: -24; } }

/* ---- the block card (index.css .node / .xblock) ------------------------ */
.node {
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  font-family: var(--sans);
  overflow: hidden;
}
.xblock { display: flex; flex-direction: column; width: 100%; }
/* the RF node wrapper is the containing block for the floating furniture —
   the card itself clips, so the title and ports must escape it. On the canvas
   that is the wrapper; here it is this element, card painted by ::before. */
/* v2.6 G6 — THE BLOCKS ARE FROZEN. The drag machinery (the --dx/--dy
   transform, the grab cursors, the lifted-card state) is deleted, not
   disabled: a block sits where it was sketched. The 3D viewport keeps its own
   gesture — orbiting a model is content, not a block move. */
.lb-node {
  position: absolute; z-index: 2;
  overflow: visible; background: none; border: none; box-shadow: none;
}
.lb-node::before {
  content: '';
  position: absolute; inset: 0;
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
}
.lb-node > .block-window { border-radius: var(--radius); }
.lb-node > * { position: relative; z-index: 1; }
/* the 3D viewport owns its own pointer: inside it, the model orbits. R2 G5 —
   the grab cursor gives way to the visitor's DRESS like every other face; the
   dress is pointer-events: none, so pressing here still starts the orbit
   exactly as it did. */
#mount-3d .mesh-viewport { cursor: none; }

/* ---- the window (index.css .block-window) ------------------------------ */
.block-window {
  position: relative; width: 100%;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(155deg, var(--paper) 0%, var(--win-hi) 100%);
  overflow: hidden;
  border: none; padding: 0;
}
.block-window > img, .block-window > video {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* B1 G1 — THE WINDOW LOOP RETURNS, on the Iterate block. This is the pre-R1
   Image block's mechanism verbatim (landing/hero.css at 8ca224a, .img-loop /
   .loop-layer): two stacked layers, one slow calm crossfade at 1.1s inside a
   3400ms cadence, so the face never cuts. What changed is which block wears it
   and which frames it cycles — Kevin's render variations. */
.img-loop .loop-layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; transition: opacity 1.1s ease;
}
.img-loop .loop-layer.is-on { opacity: 1; }
/* v2.6 G7 — the Animate block's two cuts stack the same way: 01 runs, 02
   crossfades in at its end, and round again */
.vid-seq .vid-layer {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; transition: opacity .7s ease; background: #000;
}
.vid-seq .vid-layer.is-on { opacity: 1; }

/* ---- the floating identity (index.css .xfloat-title / .xfloat-bare) ----
   On the canvas the title is a hover/selection reveal. A PORTRAIT is always
   named — these sit in the revealed state, the paint the app shows the
   instant a pointer touches a block. */
.xfloat-title {
  position: absolute; bottom: 100%; left: 0; margin-bottom: 6px;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 3px 9px;
  background: var(--card); border: 1px solid var(--card-border);
  border-radius: 6px; box-shadow: var(--card-shadow);
  font: var(--t-title); color: var(--ink);
  white-space: nowrap; z-index: 3;
}
.xfloat-title.xfloat-bare {
  left: 0; right: 0;
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  background: transparent; border: none; box-shadow: none;
  padding: 0 1px; margin-bottom: 4px;
}
.xfloat-bare .xfloat-id { display: inline-flex; align-items: center; gap: 7px; min-width: 0; overflow: hidden; }
.xfloat-bare .xfloat-id > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xfloat-bare .xfloat-res {
  font: var(--t-meta); color: var(--ink-faint);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.xfloat-title .swatch { width: 8px; height: 8px; border-radius: 2px; display: inline-block; flex: none; }
.node-title { font: var(--t-title); color: var(--ink); display: inline-block; }

/* ---- ports (index.css .rf-handle + xyflow's handle placement) ---------- */
.rf-handle {
  width: 11px; height: 11px;
  border-radius: 2px;
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 1px var(--card-border);
}
.rf-handle.img  { background: var(--img-port); }
.rf-handle.txt  { background: var(--txt-port); }
.rf-handle.out  { background: var(--render-port); }
/* xyflow centres a handle with translate(0,-50%). Here the offset is a
   NEGATIVE MARGIN instead: same paint, but offsetTop tells the truth, so
   main.js can measure a port centre without unpicking a transform. */
.lb-port {
  position: absolute; top: 50%;
  margin-top: -5.5px;
  z-index: 4;
}
.lb-port.left  { left: -4px; }
.lb-port.right { right: -4px; }

.port-label {
  position: absolute;
  font-family: var(--mono);
  font-size: 8.5px; letter-spacing: 0.06em;
  color: var(--ink-faint);
  pointer-events: none; z-index: 4;
}
.lb-label { position: absolute; }
.left-tick { left: -8px; top: calc(50% - 6px); }

/* ---- the 3D face (index.css .mesh-frame / .mesh-viewport) -------------- */
/* index.css .mesh-frame — with ONE deliberate departure (v2.6 G0): the app's
   8px margin-bottom reserves room for furniture that lives under the face on
   the canvas. There is no furniture here, so the margin left a dead band at
   the block's foot. The viewport fills the face exactly. */
.mesh-frame {
  position: relative;
  flex: 1; min-height: 0;
  border: 1px solid var(--card-border);
  border-radius: var(--radius);
  background: var(--paper);
  overflow: hidden;
  margin-bottom: 0;
}
.mesh-frame .mesh-still {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: opacity .5s ease;
}
.mesh-frame.live .mesh-still { opacity: 0; }
.mesh-viewport { position: absolute; inset: 0; }
/* R4 G0 — THE CANVAS FILLS ITS BOX, AND THIS IS THE WHOLE DEFECT KEVIN SAW.
   The renderer is handed the frame in CSS pixels and multiplies by the device
   pixel ratio to size the drawing buffer: at dpr 1 the canvas ATTRIBUTE size
   (310x221) happens to equal its CSS size, so the face fitted and every sitting
   — and every headless shot, all of them taken at deviceScaleFactor 1 — showed
   a correctly framed model. On a 2x display the same call makes the canvas
   620x442 ATTRIBUTE pixels, and an unstyled canvas takes its attributes as its
   CSS size: 620x442 CSS px pinned to the top-left of a 312x223 frame that
   clips. The visitor sees the TOP-LEFT QUARTER of a centred render, which puts
   the rock low, right, and cut off by two edges — the report, exactly, and it
   was never the camera. Measured: overflow 1.987x at dpr 2 and dpr 3.
   The box is declared HERE rather than by letting the renderer write inline
   styles, because this is the one place that knows what the face is: the frame
   owns the CSS box, the renderer owns the buffer. */
.mesh-viewport canvas { display: block; width: 100%; height: 100%; }
.mesh3d-node .block-window.mesh-frame { min-height: 0; }

/* ---- the text face (index.css .txt-window / .txt-preview) -------------- */
.txt-window { flex: 1 1 auto; min-height: 90px; }
/* every text node in the app carries an EXPLICIT height (index.css §.txt-window,
   v2.72 — creation default 200). Here it also keeps the card from growing
   under the typing, which would drag its ports and wires around mid-beat. */
/* B1 G0 — THE DRAWING'S SQUARE, and the arithmetic that makes it one: the
   card is 187 wide, the enhance bar's seat inside it costs 39 (a 28px control,
   5px of air above and below, and the 1px divider), so the prompt window is
   148 and the card is 187 — square, to the pixel. main.js holds the same 148
   as TXT_TALL, which is the FLOOR the enhanced state may not go under; the
   payload asks for more than the floor at this width and gets it. */
#blk-txt .txt-window { height: 148px; border-radius: var(--radius) var(--radius) 0 0; }
.txt-preview {
  width: 100%; height: 100%; align-self: stretch;
  padding: 10px 12px;
  font: var(--t-body); color: var(--ink);
  white-space: pre-wrap; overflow-y: auto; text-align: left;
}
/* ---- R3 G4 · THE STRUCTURED PAYLOAD -------------------------------------
   The enhanced prompt is not a paragraph any more. Six LABEL : VALUE rows on
   one grid each, sharing ONE RESERVED label column (--aspect-col, measured by
   main.js at the widest label), so every value starts on the same vertical and
   the eye can scan the labels alone. Six independent `auto` columns would
   zigzag — the same defect the rotator's reserved span cures on the sub-line,
   and the same cure.

   EACH LABEL WEARS ITS OWN INK — one of the app's accent tokens, written on
   the element by main.js (colour codes the FIELD) — and every value is the
   house ink (colour never codes the sentence). The suite resolves all six and
   requires them distinct, and requires each to be an accent the page itself
   declares, in both themes.

   THE CASCADE is opacity + a 6px lift, and deliberately nothing else: neither
   property changes layout height, so the box can be measured with all six rows
   in place BEFORE the first one has appeared. That is what keeps the grow law
   ("no scrollbar, ever") true at the new content height. */
.aspect-row {
  display: grid;
  grid-template-columns: var(--aspect-col, auto) 1fr;
  gap: 0 10px;
  margin-bottom: 6px;
  opacity: 0; transform: translateY(6px);
  transition: opacity .34s ease, transform .34s cubic-bezier(0.22, 0.9, 0.3, 1);
}
.aspect-row:last-child { margin-bottom: 0; }
.aspect-row.in { opacity: 1; transform: none; }
.aspect-label { font: var(--t-ui); white-space: nowrap; }
.aspect-value { font: var(--t-body); color: var(--ink); }
/* G4 — the caret lives only while the prompt types itself */
.txt-preview.typing::after {
  content: ''; display: inline-block;
  width: 1px; height: 1em; margin-left: 1px;
  background: var(--ink); vertical-align: -0.15em;
  animation: caret-blink 1s steps(1) infinite;
}
@keyframes caret-blink { 50% { opacity: 0; } }

/* ==========================================================================
   THE SCORE — every visual state is a class the score sets from progress
   ========================================================================== */
/* blocks do not exist until Jimin draws them */
.lb-node { opacity: 0; visibility: hidden; }
.lb-node.materialized {
  opacity: 1; visibility: visible;
  transition: opacity .45s ease;
}
/* the marquee Jimin drags out — a thin outline in the selection neon
   (index.css --select: the one place the canvas may glow) */
.marquees { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.marquee {
  position: absolute;
  border: 1.5px solid var(--select);
  border-radius: 3px;
  background: color-mix(in srgb, var(--select) 7%, transparent);
  opacity: 0;
}
.marquee.on { opacity: 1; }
.marquee.settling { transition: opacity .35s ease; }
.marquee-label {
  position: absolute; left: 0; bottom: 100%; margin-bottom: 4px;
  font: var(--t-meta); letter-spacing: 0.06em;
  color: var(--select); white-space: nowrap;
}

/* ==========================================================================
   R1 G2 — THE CENTRE STACK.

   The copy is a CENTRED COLUMN in the middle of the canvas: two headline
   lines, the sub-line with its ticker, and the chat bar. It is in front of
   the graph (z 7, over the wires at z 1 and the cards at z 2) and behind
   nothing.

   EVERY COPY SOLVER IS DELETED. 2.11 seated each line against a block edge
   (--l1-l, --cr-r, --l2-t, --l3-l/-t, and the two fitRun() measures that
   scaled the type to a span); a centred column has nothing to chase, so the
   whole chase is gone rather than disabled. The stack's homes are recorded in
   layout-manifest.json exactly as the blocks' are, and the suite asserts them
   entry by entry — the freeze law is unchanged, it simply now guards a
   composition that cannot drift in the first place.
   ========================================================================== */
/* R2 — THE STACK CENTRES IN THE CANVAS, NOT IN THE WINDOW. The toolbar's
   tripled gap (G0) turned the top 88px into chrome; centring in the raw
   viewport left only 240px above the stack for an upper block that Kevin's
   sketch grows to 244 — the arithmetic simply did not close. Centring below
   the band buys exactly the room the grown blocks need. --tb-band is
   AUTHORED, not measured off the toolbar, so this stays one-direction:
   constant → copy → the bars.

   R2 SCALED THIS OFFSET BY --fit, AND R5 TAKES THE SCALING BACK OFF — the same
   goal, the opposite arithmetic, because the thing it was correcting for is
   gone. R2's stage was centred on the WINDOW while the stack was centred on the
   CANVAS: the two centres differed by half the band, and a CONSTANT difference
   between two halves of one scaling composition walked the stack onto the Text
   block as the viewport narrowed (clearance 12.5px → 0 at --fit 0.78, −15px at
   768). Scaling the padding cancelled that. R5 G1 centres the STAGE on the
   canvas too (see .hero-stage), so the two centres are now the SAME POINT and
   there is nothing left to cancel — while the scaled padding, which had become
   a difference of its own, drifted the stack UP relative to the blocks instead
   (measured: 21px of overlap onto the Render block at 768). Plain --tb-band
   puts both halves on one centre at every --fit, which is what makes the whole
   composition ONE similarity and every clearance simply proportional. */
.hero-copy {
  position: absolute; inset: 0; z-index: 7;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: var(--tb-band) 4vw 0;
  pointer-events: none;
}
/* every member of the stack arrives on beat 2, one after another */
.stack-item {
  opacity: 0; transform: translateY(18px);
  transition: opacity .6s ease, transform .6s cubic-bezier(0.22, 0.9, 0.3, 1);
}
.stack-item.shown { opacity: 1; transform: none; }
.hero-line {
  margin: 0; padding: 0;
  text-align: center;
  font-family: var(--font-primary);
  /* the lines sit OVER the canvas (Kevin's sketch) — a paper-coloured halo
     keeps them legible above a render without tinting the ink */
  text-shadow: 0 0 18px var(--paper), 0 0 42px var(--paper);
}
/* R1 G2 — THE STACK RIDES THE FIT.
   Every measurement below is AUTHORED at the reference viewport and then
   multiplied by --fit, the same factor the stage is scaled by. That is not a
   nicety: the four blocks live on the fit-scaled stage, so at 768 they close
   in on the centre and the band between them shrinks by --fit — while a
   stack sized in vw shrank by a different law entirely and landed on top of
   the Site and Text blocks. Both halves scale by ONE number now, so the
   clearance the reference viewport has is the clearance EVERY viewport has,
   by construction rather than by a breakpoint. (A viewport-unit clamp was
   what 2.11's lines used; those lines were seated against block edges, so a
   solver corrected the mismatch every frame. There is no solver now, so the
   arithmetic has to be right instead.)

   The stack is NOT inside the scaled stage, deliberately: the sweep's mask
   tile is written in the host's own local pixels (--sw-tile) from a measured
   screen width, and a transformed ancestor would put those two in different
   spaces — one band per cycle would quietly become two. */
.hero-line-1, .hero-line-2 {
  font-size: calc(50px * var(--fit, 1)); font-weight: 900;
  line-height: 1.08; letter-spacing: -0.018em;
  white-space: nowrap;
}
/* R4 G3 — THE STACK'S RHYTHM. The two headline lines sat 21px apart
   (baseline of line 1 to cap of line 2) while line 2 to line 3 was 40px: one
   paragraph and then a stranger. 19px of margin opens the first gap to the
   second's 40, and BOTH are then equal by arithmetic rather than by eye.
   MEASURED BASELINE-TO-CAP, not rect-to-rect, and the difference matters: a
   Range rect carries the font's own leading, which is 18px above the cap at
   50px and 9px at 23px, so equal RECT gaps would leave line 1 -> 2 sitting
   seven pixels further apart optically than line 2 -> 3. The rect gaps land at
   +2 and +11 and that is the correct answer, not a miss.
   It rides --fit like every other measurement in the stack (R1 G2), so the
   rhythm is the same rhythm at every viewport; the column re-centres in its
   band as it grows, which is the same named move. */
.hero-line-1 { margin-bottom: calc(19px * var(--fit, 1)); }
.hero-line-2 { margin-bottom: calc(20px * var(--fit, 1)); }
/* the sub-line: the house ink, a step down, with the ticker inside it */
.hero-line-3 {
  font-size: calc(23px * var(--fit, 1)); font-weight: 400;
  line-height: 1.35; color: var(--ink);
  white-space: nowrap;
}
/* v2.7 G4 — THE ROTATOR, corrected. The old box was an inline-block with
   overflow:hidden, and such a box contributes its BOTTOM MARGIN EDGE as its
   baseline — which is exactly why every word floated above the line. The
   current word stays IN FLOW (so the box's baseline is the word's own) and the
   outgoing word is an absolutely-positioned ghost that rolls out over it.
   R1 G2 — THE SPAN IS RESERVED. The sub-line is CENTRED now, so a box that
   changed width with the audience would shove "Spatial Intelligence For"
   sideways on every roll — the exact defect 2.11 fixed by anchoring the line
   left, re-appearing in a centred composition. main.js measures the box at all
   four words and writes the widest into --rot-w; the box is that wide always,
   the line's total width is therefore constant, and the static run cannot
   move. The period lives INSIDE the box, immediately after the word, so it
   still hugs whichever audience is on screen — the slack sits at the box's
   right end where no glyph is. */
.rotator {
  position: relative; display: inline-block;
  width: var(--rot-w, auto); text-align: left;
  white-space: nowrap; vertical-align: baseline;
}
.rot-dot { margin-left: -0.02em; }
/* v2.8 G5 — THE TICKER. The swap is an odometer roll: the old word rides UP
   and out while the new one rides up into its seat, with a crisp settle at the
   end (the station-clock snap). inline-block keeps the baseline -- only
   overflow:hidden would break it, the trap 2.7 paid for -- so the roll is a
   transform on an element that still carries the line's baseline, and at rest
   the transform is none: 0px drift when settled. Both partials are on screen
   mid-roll, so the sentence never blinks. */
.rot-word {
  display: inline-block;
  transform: translateY(0);
  transition: transform .38s cubic-bezier(0.16, 0.9, 0.2, 1.06), opacity .26s ease;
}
.rot-ghost {
  /* v2.11 G5 — anchored LEFT now: the line is left-aligned, so an outgoing
     word must grow RIGHTWARD into the reserved space. (2.9 pinned it right for
     exactly the opposite reason, when the line hung off the right edge.) */
  position: absolute; left: 0; right: auto; top: 0;
  pointer-events: none;
  transform: translateY(0);
  transition: transform .38s cubic-bezier(0.16, 0.9, 0.2, 1.06), opacity .26s ease;
}
.rot-ghost.rot-out { transform: translateY(-0.92em); opacity: 0; }
.rot-word.rot-in { transform: translateY(0.92em); opacity: 0; transition: none; }
/* v2.10 G4 — THE NEON ROSTER, Kevin's ruling. Clients is REMOVED and the four
   inks go neon by his assignment (the tokens and their derivation live at the
   top of this file). 2.9's chroma floor of 120 was a "vivid" floor; these are
   signage, so the floor steps up — every ink below clears 165 in both themes. */
.rot-1 { color: var(--neon-blue); }         /* Architects    */
.rot-2 { color: var(--neon-green); }        /* Designers     */
.rot-3 { color: var(--neon-pink); }         /* Collaborators */
.rot-4 { color: var(--neon-yellow); }       /* You           */

/* ==========================================================================
   R1 G4 — THE CHAT BAR. A REAL text input, not a picture of one: it focuses,
   it carries a caret, it accepts typing. Both buttons are placeholder links
   (Cindy wires them at merge) and Enter blurs rather than submitting — there
   is nothing on this page to submit to, and a hero that swallows the return
   key is worse than one that does nothing with it.

   The whole hero-copy column is pointer-events: none so the canvas beneath it
   stays reachable; the bar takes its own pointer events back.
   ========================================================================== */
/* R2 G1 — THE ROW SPLITS. The input and its primary button stay one bar;
   the Join pill moves OUT into a pill of its own (R3 G1 relabelled it
   "Join Us"; the pill hugs its own text, so only its LEFT edge moved). The row spans the
   headline's own run (--hl-w, the same measurement the two toolbar bars take),
   so the bar's LEFT edge lands on "Think" and the pill's RIGHT edge on
   "Seconds." — the chrome above and the chrome below sit on the same two
   verticals. The bar takes the remainder; the gap between them is authored. */
.chat-row {
  margin-top: calc(26px * var(--fit, 1));
  display: flex; align-items: stretch;
  gap: calc(14px * var(--fit, 1));
  width: var(--hl-w, min(calc(640px * var(--fit, 1)), 92%));
  max-width: 100%;
  pointer-events: none;   /* the gap is canvas; each bar takes its own back */
}
.chat-bar {
  flex: 1 1 auto; min-width: 0;
  pointer-events: auto;
  display: flex; align-items: center; gap: calc(8px * var(--fit, 1));
  padding: calc(7px * var(--fit, 1));
  padding-left: calc(16px * var(--fit, 1));
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: calc(12px * var(--fit, 1));
  box-shadow: var(--card-shadow);
}
.join-pill {
  flex: none;
  pointer-events: auto;
  display: flex; align-items: center;
  padding: calc(7px * var(--fit, 1));
  background: var(--card);
  border: 1px solid var(--card-border);
  border-radius: calc(12px * var(--fit, 1));
  box-shadow: var(--card-shadow);
}
.chat-input {
  flex: 1 1 auto; min-width: 0;
  background: none; border: none; outline: none;
  font: var(--t-body); font-size: calc(14px * var(--fit, 1));
  color: var(--ink);
  padding: calc(6px * var(--fit, 1)) 0;
}
.chat-input::placeholder { color: var(--ink-faint); }
.chat-input:focus-visible { outline: none; }
.chat-btn {
  flex: none;
  height: calc(var(--hdr-ctl) * var(--fit, 1));
  display: inline-flex; align-items: center;
  padding: 0 calc(14px * var(--fit, 1));
  border-radius: calc(8px * var(--fit, 1));
  font: var(--t-ui); font-size: calc(12px * var(--fit, 1));
  letter-spacing: 0.02em;
  white-space: nowrap;
  text-decoration: none;
  transition: background .14s ease, border-color .14s ease, color .14s ease;
}
/* the brand-blue pill — index.css .btn-action: the action fill under DARK ink
   (v2.32.2 C: "the blue is mid-light, so the LABEL is dark ink") */
.chat-primary {
  background: var(--action); border: 1px solid var(--action-edge);
  color: var(--action-ink);
}
.chat-primary:hover { background: var(--action-hover); }
/* the quiet pill — the app's secondary control */
.chat-quiet {
  background: var(--card-hi); border: 1px solid var(--card-border);
  color: var(--ink-soft);
}
.chat-quiet:hover { color: var(--ink); border-color: var(--blueprint); }

/* ==========================================================================
   THE CURSORS (index.css .presence-cursor — the v2.49 J1 grammar, verbatim)

   R2 G5 — THE CAST: Jimin (--action), ARCHITECT (--animate-accent, the magenta
   slot the retired Client vacated) and Collaborator (--note-accent). Plus THE
   VISITOR'S DRESS: over open canvas the visitor keeps the browser's own
   cursor; the moment the pointer enters a block FACE the native cursor is
   hidden and our presence dress takes its place, tagged "Be Inspired" in a
   fourth ink (--iterate-accent) that none of the three actors wears. The dress
   lives in the same pointer-events: none layer as the actors, so it can never
   stand between the visitor and the thing they are pointing at — the 3D
   viewport still orbits and every control still takes its own clicks.
   ========================================================================== */
.cursor-layer { position: absolute; inset: 0; pointer-events: none; z-index: 8; }
/* the native cursor steps aside INSIDE a block face only. The floating title
   hangs outside the card's own box and is not a face: it keeps its cursor, and
   the dress's hit test (main.js, on the node's border box) agrees with this
   rule rather than restating it. */
.lb-node.materialized { cursor: none; }
.lb-node .xfloat-title { cursor: default; }
.presence-cursor {
  position: absolute; left: 0; top: 0;
  transition: transform 0.45s cubic-bezier(0.22, 0.9, 0.3, 1), opacity 0.35s ease;
  will-change: transform;
}
/* Jimin's hand is exact while she draws — the score sets her travel */
.presence-cursor.instant { transition: opacity 0.35s ease; }
.presence-cursor.hidden { opacity: 0; }
.presence-body { display: flex; align-items: flex-start; gap: 2px; }
.presence-arrow { display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.25)); }
.presence-tag {
  margin-top: 13px; margin-left: -3px;
  font: var(--t-meta); letter-spacing: 0.02em;
  color: #FFFFFF; background: var(--presence);
  padding: 2px 7px; border-radius: 3px 8px 8px 8px;
  white-space: nowrap;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
/* the drawing hand is a CROSSHAIR (index.css .rf-wrap.annotating: crosshair) */
.presence-cursor .presence-cross { display: none; }
.presence-cursor.drawing .presence-arrow { display: none; }
.presence-cursor.drawing .presence-cross { display: block; }
.presence-cursor.drawing .presence-tag { margin-top: 9px; margin-left: 3px; }

/* ==========================================================================
   v2.6 G3 — THE PAGE-TURN (index.css v2.93 GROUP 4, verbatim)
   The theme change is not a token flip: the app runs it through the View
   Transitions API and sweeps the NEW look over a static snapshot of the old
   one, right to left. These four rules kill the API's default crossfade so
   the sweep is the only motion; themeStore's own clip-path animation (600ms,
   cubic-bezier(0.22, 1, 0.36, 1)) is reproduced in main.js.
   ========================================================================== */
::view-transition { pointer-events: none; }
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-old(root) { z-index: 0; }
::view-transition-new(root) { z-index: 1; }

/* ==========================================================================
   v2.10 G1 — THE SKETCH SCENE DIED. The handwriting engine, the ink layer and
   the comment grammar before it are all gone from this sheet; beat 5 is three
   drifting cursors and nothing else, and those live in the .cursor-layer rules
   above. Nothing here paints on the canvas any more.
   ========================================================================== */

/* ==========================================================================
   v2.6 G9 — THE ENHANCE SEQUENCE (index.css v2.24 A2 / v2.26 D2 / v2.87 S4)
   The app's control, copied: the reserved constant-footprint slot below the
   block, the full-width secondary button, the star, the circling light
   segment, and the standard left-to-right progress fill under a label that
   never changes.
   ========================================================================== */
/* B1 G0 — THE ENHANCE BAR TAKES THE DRAWING'S INTERNAL DIVIDER. A hung this
   control in a reserved slot 8px BELOW the card; B seats it INSIDE, under a
   hairline, which is what the divider in Kevin's square is. The constant-
   footprint law is unchanged and is now structural rather than reserved: the
   row occupies its band in the card's flow at every moment of the performance,
   at opacity 0 until the prompt has finished typing, so nothing the enhance
   does can change the card's resting height. */
.enhance-inside {
  flex: none; z-index: 3;
  padding: 5px; border-top: 1px solid var(--card-border);
  opacity: 0; transition: opacity .4s ease;
}
.enhance-inside.on { opacity: 1; }
.enhance-row { display: flex; gap: 6px; width: 100%; }
.enhance-btn {
  font: var(--t-meta); color: var(--ink-soft);
  background: var(--card); border: 1px solid var(--card-border);
  border-radius: 4px; padding: 2px 8px;
  position: relative; overflow: hidden;
}
.enhance-primary {
  flex: 1; height: 28px; text-align: center; padding: 0 8px; font: var(--t-ui);
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
}
.enhance-btn .run-lbl { position: relative; z-index: 1; }
/* index.css .run-prog-fill — the ONE progress grammar */
.run-prog-fill {
  position: absolute; left: 0; top: 0; bottom: 0; z-index: 0;
  width: 0;
  background: var(--action);
  opacity: .38;
  transition: width .22s linear;
  pointer-events: none;
}
/* index.css B3 — the enhance button POPS: a circling light segment and a
   twinkling star */
@property --enh-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
.enhance-primary::before {
  content: ''; position: absolute; inset: -2px; border-radius: 7px; padding: 1.5px;
  background: conic-gradient(from var(--enh-angle),
    transparent 0deg 290deg, var(--blueprint) 325deg, transparent 360deg);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  animation: enh-sweep 3.2s linear infinite;
  pointer-events: none;
}
@keyframes enh-sweep { to { --enh-angle: 360deg; } }
.enh-star { display: inline-block; margin-right: 2px; animation: enh-twinkle 2.6s ease-in-out infinite; }
@keyframes enh-twinkle { 0%, 100% { opacity: 0.65; transform: scale(1); } 50% { opacity: 1; transform: scale(1.22); } }
/* index.css @keyframes enhance-invite — the one invitation pulse */
@keyframes enhance-invite {
  0%, 100% { box-shadow: 0 0 0 0 rgba(74, 111, 150, 0); }
  50% { box-shadow: 0 0 0 3px rgba(74, 111, 150, 0.25); border-color: var(--blueprint); }
}
.enhance-pulse { animation: enhance-invite 0.65s ease 2; }
/* the running neon while the fill sweeps (index.css .run-neon) */
.run-neon { animation: neon-edge 2.2s ease-in-out infinite; }
@keyframes neon-edge {
  0%, 100% { box-shadow: 0 0 0 1px var(--select), 0 0 6px 0 color-mix(in srgb, var(--select) 30%, transparent); }
  50% { box-shadow: 0 0 0 1.5px var(--select), 0 0 14px 1px color-mix(in srgb, var(--select) 55%, transparent); }
}
/* the box grows to hold the enhanced prompt */
#blk-txt .txt-window { transition: height .6s cubic-bezier(0.22, 0.9, 0.3, 1); }
/* v2.8 G1 — the Text block's WIDTH is the Massing block's width and stays
   there; only its HEIGHT grows for the prompt (v2.7 G6's no-scrollbar law
   holds at the narrower width -- the box simply gets taller). */
#blk-txt .txt-preview { overflow: hidden; }

/* ==========================================================================
   RESPONSIVE — the composition scales down to 768; below that it stacks
   ========================================================================== */
@media (max-width: 767px) {
  .scroll-track { height: auto; }
  .hero {
    position: static; height: auto; min-height: 0;
    padding: 96px 16px 40px;
  }
  .hero-stage {
    position: static; width: 100%; height: auto;
    margin: 0; transform: none;
    display: flex; flex-direction: column; gap: 46px;
  }
  .wires, .lb-port, .lb-label, .marquees, .cursor-layer { display: none; }
  .lb-node {
    position: static; width: 100% !important; left: auto !important; top: auto !important;
    transform: none; opacity: 1; visibility: visible;
  }
  .xfloat-title { position: static; margin: 0 0 6px; }
  .xfloat-title.xfloat-bare { margin-bottom: 4px; }
  .mesh-frame { min-height: 200px; }
  /* the stack leads the page, then the four blocks stack under it */
  .hero-copy {
    position: static; padding: 0;
    align-items: stretch; margin-bottom: 40px;
  }
  .stack-item { opacity: 1; transform: none; }
  .hero-line { text-align: left; margin: 0 0 12px; white-space: normal; }
  /* the stack leads a stacked page — it is no longer squeezed between blocks,
     so it takes a size of its own rather than the (absent) fit's */
  .hero-line-1, .hero-line-2 { font-size: clamp(28px, 7.6vw, 44px); white-space: normal; }
  .hero-line-3 { font-size: clamp(16px, 4.4vw, 22px); white-space: normal; }
  .chat-btn { height: 30px; font-size: 12px; padding: 0 14px; border-radius: 8px; }
  .chat-input { font-size: 15px; padding: 8px 0; }
  .rotator { width: auto; }
  .chat-row { margin-top: 14px; width: 100%; flex-wrap: wrap; }
  .chat-bar { flex: 1 0 100%; flex-wrap: wrap; }
  .join-pill { flex: none; }
  .chat-input { flex: 1 0 100%; }
  /* the two bars fall back to the window's own inset — there is no headline
     run to sit on once the page stacks */
  .lb-toolbar { flex-wrap: wrap; left: 14px !important; width: auto !important; right: 14px !important; }
  .lb-pill { flex-wrap: wrap; row-gap: 4px; }
  .lb-pill .lb-item:not(.lb-wordmark):not(.lb-login) { display: none; }
  .lb-pill .lb-rule { display: none; }
}

/* ==========================================================================
   REDUCED MOTION — no score. The finished still life, all three lines, static.
   (index.css §prefers-reduced-motion: the app's own rule — state without
   movement, never state withheld.)
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .scroll-track { height: 100vh; }
  .lb-node { opacity: 1; visibility: visible; transition: none; }
  .stack-item { opacity: 1; transform: none; transition: none; }
  .wire.marching { animation: none; stroke-dasharray: none; }
  /* R3 G2 — there is no turntable to still here any more: the model rests
     until it is touched, in every motion preference. R2 G5 — the visitor
     keeps their own cursor over every face, since a dress that appears on
     hover is motion the visitor did not ask for */
  .lb-node.materialized, #mount-3d .mesh-viewport { cursor: default; }
  .marquees, .cursor-layer { display: none; }
  .presence-cursor { transition: none; }
  .txt-preview.typing::after { animation: none; }
  /* B1 G1 — the loop does not run under reduced motion (main.js returns before
     it arms the interval); this stills the crossfade of the frame that stands */
  .img-loop .loop-layer { transition: none; }
  /* R3 G4 — reduced motion: the FINISHED structured state, no cascade */
  .aspect-row { opacity: 1; transform: none; transition: none; }
  /* the gradient STAYS — only its sweep stops (state without movement) */
  .ink-sweep {
    animation: none;
    -webkit-mask-position: calc(var(--sw-tile) * 0.5) 0;
    mask-position: calc(var(--sw-tile) * 0.5) 0;
  }
  .rot-word, .rot-ghost { transition: none; transform: none; }
  .enhance-primary::before { animation: none; background: conic-gradient(var(--blueprint) 0deg 360deg); opacity: 0.35; }
  .enh-star, .run-neon { animation: none; }
  #blk-txt .txt-window, #blk-txt { transition: none; }
}
