/* yeaboi.ai landing page — extracted from index.html so it can be
   universally loaded (needed for client-side nav to/from the landing page). */
  /* Landing-page layout. Colors route through site.css tokens. */

  /* ---------- hero — the headline owns the first viewport; product below the fold ---------- */
  /* overflow:clip, NOT hidden — hidden would make .hero the scrollport that
     position:sticky resolves against (it never scrolls, so the pinned
     terminal in .scrolly-sticky would never stick); clip just clips. */
  /* overflow-clip-margin: the terminal's shadow reaches ~100px past the frame
     (0 40px 90px -30px), and a bare clip cut it off against the hero's own box
     — a straight edge through a soft shadow, which reads as a rendering fault.
     The margin lets the paint bleed that far before the clip applies. */
  .hero{padding:0 0 110px; position:relative; overflow:clip; overflow-clip-margin:140px}
  .hero > .wrap{
    /* ONE source for the hero's width: the pull-back clamp below has to use the
       same number as max-width. They were separate literals, so widening the
       hero left the clamp on the old value — it over-pulled and shoved the
       terminal off the left edge of the window. */
    --hero-max:1320px;
    width:100%; max-width:var(--hero-max);
    /* tighter than the site-wide .wrap gutter — this row is a terminal and a
       narrow caption column, and 40px a side was 80px of terminal */
    padding-left:24px; padding-right:24px;
    /* body is inset by the fixed rail, so "centered" lands right of the true
       viewport centre — pull back by up to rail/2, clamped so the content
       never slides under the rail on narrower windows */
    transform:translateX(calc(-1 * min(var(--rail) / 2, max(0px, (100vw - var(--rail) - var(--hero-max)) / 2))));
  }
  .hero-copy{
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    text-align:center; min-height:100vh; position:relative; padding:40px 0;
  }
  .scroll-hint{
    /* centered via inset+margin, not transform — the entrance animation's
       end state (transform:none) would cancel a translateX(-50%) */
    position:absolute; bottom:30px; left:0; right:0; margin:0 auto; width:max-content;
    font-family:var(--mono); font-size:.74rem; letter-spacing:.14em; text-transform:uppercase;
    color:var(--text-dim); display:flex; flex-direction:column; align-items:center; gap:6px;
  }
  .scroll-hint::after{content:"↓"; font-size:1rem; animation:hint-bob 2s ease-in-out infinite}
  @keyframes hint-bob{50%{transform:translateY(5px)}}
  @media (prefers-reduced-motion:reduce){.scroll-hint::after{animation:none}}
  .tagline{
    margin:26px 0 0; font-family:var(--display); font-weight:640;
    font-size:clamp(2.7rem,5.8vw,4.4rem); letter-spacing:-.032em; line-height:1.04;
  }
  .tagline em{font-style:normal; color:var(--accent-bright); font-weight:640}
  .lede{margin:24px 0 36px; max-width:56ch; color:var(--text-muted); font-size:1.14rem; line-height:1.7}
  .lede strong{color:var(--text); font-weight:600}
  .hero-cta{display:flex; align-items:center; gap:14px; flex-wrap:wrap; justify-content:center}
  .hero-cta .codeblock{min-width:360px}
  .subnote{margin:16px 0 0; font-family:var(--mono); font-size:.8rem; color:var(--text-dim)}
  .subnote a{color:var(--text-muted)} .subnote a:hover{color:var(--accent)}

  /* the Mac app in the hero row. It sits beside a filled primary and an install
     command, so it carries the mark and the tag rather than a third ghost
     outline; the ground is the app icon's own plate colour. */
  .btn-mac{
    background:linear-gradient(180deg, rgba(24,58,44,.62), rgba(17,19,23,.92));
    border-color:rgba(42,170,105,.32); gap:8px; padding-right:14px;
  }
  .btn-mac:hover{
    border-color:rgba(42,170,105,.7); color:var(--text); background:linear-gradient(180deg, rgba(30,72,54,.8), rgba(19,23,27,.95));
  }
  .btn-mac .dl-apple{width:13px; height:16px}
  .btn-new{
    font-family:var(--mono); font-size:.62rem; letter-spacing:.12em; text-transform:uppercase;
    line-height:1; padding:3px 7px; border-radius:999px;
    color:var(--success); border:1px solid rgba(89,192,126,.34); background:rgba(42,170,105,.09);
  }

  /* staggered hero entrance */
  @keyframes rise{from{opacity:0; transform:translateY(22px)} to{opacity:1; transform:none}}
  .hero-copy > *{animation:rise .8s cubic-bezier(.2,.6,.2,1) both}
  .hero-copy > *:nth-child(1){animation-delay:.05s}
  .hero-copy > *:nth-child(2){animation-delay:.14s}
  .hero-copy > *:nth-child(3){animation-delay:.23s}
  .hero-copy > *:nth-child(4){animation-delay:.32s}
  .hero-copy > *:nth-child(5){animation-delay:.4s}
  .hero-demo{animation:rise 1s cubic-bezier(.2,.6,.2,1) .35s both}
  @media (prefers-reduced-motion:reduce){.hero-copy > *,.hero-demo{animation:none}}

  /* the product, front and centre, floating on a glow — below the fold. */
  .hero-demo{position:relative; margin-top:8vh; padding-bottom:60px}

  /* ---- scrollytelling: pinned terminal + a step rail at the side.
     The terminal column is sticky (centred in the viewport) while the steps
     scroll past; site.js watches each .scrolly-step cross the viewport's
     middle and drives the TUI's real selection — the same show() the
     arrow-key auto-cycle used. Pure CSS sticky + IntersectionObserver,
     no scroll library. ---- */
  .scrolly{
    /* Every pixel not spent on the rail or the gutters is terminal. At a 1440
       viewport the hero column is only 1176 (the nav rail owns the rest), so
       the frame's width is whatever this row does not take: a narrower step
       column and tighter gutters are the only places to find it. */
    display:grid; grid-template-columns:minmax(0,1fr) 232px;
    gap:36px; align-items:start;
  }
  .scrolly-sticky{
    position:sticky; top:max(28px, calc(50vh - 340px));
    z-index:1;
  }
  .scrolly-sticky::before{
    /* the frame's glow, moved here so it travels with the pinned terminal.
       No filter:blur() — a radial gradient is already a smooth falloff, and
       blurring one on a near-black page buys nothing while costing an
       offscreen layer and quantising the low-alpha tail into visible blotches
       with the blur box's own edges showing through them. The falloff does the
       softening instead: both stops now reach full transparency well inside
       the element, so nothing is clipped at its bounds. */
    content:""; position:absolute; inset:-16% -12%; z-index:0; pointer-events:none;
    /* Centred on the frame, not high on it. The frame is opaque and sits on top,
       so the only part of this that is ever seen is what escapes around the
       edges: put the bright core anywhere other than the middle and it stops
       being a halo and becomes a lit band with the frame's own straight edge
       cutting it off. Centred, and weak, it reads as the glow it is. */
    background:
      radial-gradient(62% 58% at 50% 50%, var(--accent-glow), transparent 72%),
      radial-gradient(42% 38% at 26% 74%, rgba(80,150,210,.10), transparent 70%);
    opacity:.45;
  }
  .scrolly-steps{display:flex; flex-direction:column; padding:4vh 0 8vh}
  .scrolly-step{
    min-height:62vh; display:flex; flex-direction:column; justify-content:center;
    gap:14px; opacity:.25; transition:opacity .45s ease;
  }
  .scrolly-step.is-active{opacity:1}
  /* Scroll-driven fade (site.js adds .is-driven once it is actually running, so
     the rules above stay the no-JS and reduced-motion baseline). A step's
     opacity comes from how near its middle is to the viewport's: full through
     the centre band, gone by a third of a viewport out. The point is that a step
     never reaches the top or bottom edge — it has finished arriving, and starts
     leaving, well inside the page. */
  .scrolly-steps.is-driven .scrolly-step{
    opacity:var(--step-o,0); transition:none;
    transform:translateY(var(--step-y,0));
  }
  /* while a line is still churning through glyphs it wears the step's own
     accent, then eases back to its resting colour as the text resolves */
  .scrolly-step .decoding{color:var(--sh)}
  .step-k{
    margin:0; font-family:var(--mono); font-size:.78rem; font-weight:600;
    letter-spacing:.14em; text-transform:uppercase; color:var(--sh);
  }
  .scrolly-step h3{
    margin:0; font-family:var(--display); font-weight:620; font-size:1.35rem;
    letter-spacing:-.02em; line-height:1.25; color:var(--text);
    transition:color .5s ease;
  }
  .scrolly-step > p:not(.step-k){
    margin:0; color:var(--text-muted); font-size:.98rem; line-height:1.65;
  }
  @media (max-width:900px){
    /* mobile: the step rail goes away entirely — as a flat list it just
       duplicated the modes grid below and doubled the page length. The
       terminal stands alone and auto-cycles through the modes itself
       (site.js re-enables the auto-advance when the rail is hidden). */
    .scrolly{grid-template-columns:1fr}
    .scrolly-sticky{position:static}
    .scrolly-steps{display:none}
  }
  .app-frame{
    /* A real terminal WINDOW, not a floating panel: chrome bar, light border,
       heavy soft shadow. No ground of its own — the page shows through, and
       what reads as the terminal is the border and the chrome. Sized for the
       app's real 80-column frame at a 14px cell. */
    position:relative; z-index:1; width:100%; max-width:940px; margin:0 auto;
    background:transparent; border:1px solid var(--border-strong);
    border-radius:var(--radius-lg); overflow:hidden;
    box-shadow:var(--shadow-term), 0 0 0 1px rgba(0,0,0,.4);
  }
  .tui-bar{
    display:flex; align-items:center; gap:7px; padding:12px 16px;
    /* a tint rather than a fill, so the bar still separates from the screen
       below it without putting the ground back */
    background:rgba(255,255,255,.03);
    border-bottom:1px solid var(--border);
  }
  .tui-bar .dot{width:11px; height:11px; border-radius:50%}
  .tui-bar .dot:nth-child(1){background:#ec6a5e}
  .tui-bar .dot:nth-child(2){background:#f4bf4f}
  .tui-bar .dot:nth-child(3){background:#61c554}
  .tui-bar-title{
    margin:0 auto; transform:translateX(-20px);
    font-family:var(--mono); font-size:.74rem; color:var(--text-dim); white-space:nowrap;
  }

  /* HTML recreation of the real app STARTUP — src/yeaboi/ui/splash.py's
     wordmark fade/shine/fade, then the actual mode-select screen
     (src/yeaboi/ui/mode_select/screens/_screens.py:_build_mode_screen).
     One terminal cell size everywhere (a real terminal has exactly one
     grid); the app's own rounded white Panel border drawn inside. */
  .tui{
    --t-text:#e4e6e8;          /* rich "white" */
    --t-muted:#787890;         /* theme.muted rgb(120,120,140) */
    --t-dim:#62676f;           /* theme.dim */
    color:var(--t-text); font-family:var(--tui-mono);
    /* integer font-size: at 13.5px the cell width lands on a fraction and the
       block glyphs get sub-pixel gaps between columns. No ligatures, and no
       kerning pass, so a row of ▀▄█ tiles exactly like a terminal grid. */
    font-size:14px; line-height:1.5; padding:12px 14px 16px;
    font-variant-ligatures:none; font-kerning:none; text-rendering:optimizeSpeed;
  }
  /* <pre> carries its own UA font-family, which beats inheritance — so the
     splash wordmark kept falling back to the webfont, which has no box-drawing
     coverage, even after .tui switched stacks. (The mode titles and the duck no
     longer go through a font at all; see .tui-mode-title and .tui-duck.) */
  .tui pre{font-family:inherit; font-size:inherit}
  /* the app's Panel(box=ROUNDED, border_style="white", padding=(1,2)) */
  .tui-panel{
    position:relative;
    /* The border is transparent HERE and redrawn on ::before, which is the only
       way to interrupt it under the music alcove now that the frame has no
       ground of its own. It used to be masked by painting the pocket a solid
       #0a0b0d; with the page showing through, that solid is a dark rectangle
       against the glow — measured 10-20 units of blue off its surroundings.
       The width stays for layout, so nothing shifts. */
    border:1px solid transparent;
    border-radius:10px;
    /* the app's padding=(1,2) — two CELLS, not a round pixel count, or every
       row inside sits a third of a character further in than the app's does */
    padding:20px 2ch 14px;
  }
  .tui-panel::before{
    content:""; position:absolute; inset:-1px; pointer-events:none;
    border:1px solid rgba(228,230,232,.45); border-radius:10px;
    /* Two mask layers, unioned: everything above the bottom band, plus the
       bottom band minus the alcove's span. --gap-l/--gap-r are set by site.js
       from the pocket's measured box (its width is text-driven, so CSS cannot
       know it). The band is 14px, short enough that the bottom corners — which
       live in the last 10px at either end — are outside the gap and survive. */
    -webkit-mask-image:linear-gradient(#000,#000),
      linear-gradient(90deg,#000 0 var(--gap-l,100%),transparent var(--gap-l,100%) var(--gap-r,100%),#000 var(--gap-r,100%));
            mask-image:linear-gradient(#000,#000),
      linear-gradient(90deg,#000 0 var(--gap-l,100%),transparent var(--gap-l,100%) var(--gap-r,100%),#000 var(--gap-r,100%));
    -webkit-mask-size:100% calc(100% - 14px),100% 14px;
            mask-size:100% calc(100% - 14px),100% 14px;
    -webkit-mask-position:top left,bottom left;
            mask-position:top left,bottom left;
    -webkit-mask-repeat:no-repeat;
            mask-repeat:no-repeat;
  }

  .tui-splash{
    /* min-height = the drawn menu's exact height, measured, so the Panel does
       not resize by a single pixel when the splash hands over to the mode
       screen. Re-measure it after anything that changes a row's height (the
       menu gap, the reserved description block). */
    display:flex; align-items:center; justify-content:center; min-height:663px;
    animation:tui-splash-fade 2.4s ease forwards;
  }
  .tui-splash-word{
    margin:0; font-size:13px; line-height:1; letter-spacing:0; text-align:center;
    color:#4664b4; text-shadow:0 0 16px var(--accent-glow);
  }
  @keyframes tui-splash-fade{
    0%{opacity:0} 18%{opacity:1} 72%{opacity:1} 100%{opacity:0}
  }
  @media (prefers-reduced-motion:reduce){
    .tui-splash{animation:none; opacity:0}
  }

  /* gap:13 — tighter than it was, because the selected card now opens a
     description block underneath itself the way the real list does, and the
     frame has a fixed height to live inside. The app's own rows sit closer
     together than 20px anyway; the breathing room came from the description
     being missing.

     min-height holds the frame at the size the splash was drawn to, and the two
     auto margins (below, and on .tui-foot) split whatever is left over equally
     — so the list sits centred in the space above the foot instead of hugging
     the top of the panel. */
  .tui-menu{
    display:flex; flex-direction:column; gap:13px; min-height:663px;
  }
  /* The terminal's three screens (split, Humans menu, Agents menu) deliberately
     have NO entry animation. There used to be an `animation:tui-fade` here, from
     when the menu appeared exactly once after the splash. Once the screens
     started swapping as the rail scrolls, that animation stopped advancing —
     the element sat pinned a frame in at ~2% opacity, which rendered as an
     empty terminal. The splash carries the only reveal that needs animating,
     and it does its own fade. */
  .tui-menu > .tui-mode:first-of-type{margin-top:auto}
  /* the hidden attribute must actually hide: this class's display:flex is an
     author rule, so it silently overrides the UA's [hidden]{display:none} —
     without this, the "hidden" menu renders UNDER the splash during startup,
     stacking the two and inflating the terminal until the splash is removed */
  .tui-menu[hidden]{display:none}

  .tui-mode{
    -webkit-appearance:none; appearance:none; background:none; border:none; padding:0;
    display:block; width:100%; text-align:left; cursor:pointer;
    padding-left:2ch; /* pulled back in off the panel's left edge */
    font-size:inherit; font-family:inherit;
  }
  /* The titles are the app's own two-row block typeface (_ascii_font.py), but
     they are NOT set in a font here — they are a mask.

     A browser draws U+2580–259F (▀▄█░) with whatever the font supplies, and no
     monospace font fills the cell the way a terminal does: the ▀ row and the ▄
     row miss each other by a sub-pixel, the columns antialias apart, and ░ is a
     real 25% stipple rather than the negative space this font intends it as. The
     result is mottled letterforms. iTerm2, Kitty and Alacritty all sidestep this
     the same way — they ignore the font for that range and fill the cell
     geometrically. So do we: each glyph becomes rects in an SVG mask (--mask,
     generated from render_ascii_text), one cell = 1 unit wide × 2 tall, and ░
     contributes nothing. The paint is then just a background, which is why the
     shimmer below still works. */
  .tui-mode-title{
    display:inline-block; vertical-align:middle;
    width:calc(var(--cols) * 1ch); height:4ch;   /* a cell is one advance wide, two tall */
    -webkit-mask:var(--mask) no-repeat center/100% 100%;
            mask:var(--mask) no-repeat center/100% 100%;
    /* unselected: the app's exact dim — each channel halved, floored at 40
       (see _build_mode_row's _unsel_style), baked into --mcd per mode */
    background-color:rgb(var(--mcd)); transition:background-color .35s;
  }
  .tui-mode.is-active .tui-mode-title{
    /* selected: full accent with the app's traveling white shimmer hotspot
       (shimmer_style: 0.6 sweeps/sec) — a narrow white band crossing the
       block letters. The mask clips it to the letterforms. */
    background-color:transparent;
    background-image:linear-gradient(90deg,
      rgb(var(--mc)) 0%, rgb(var(--mc)) 42%,
      rgb(255,255,255) 50%,
      rgb(var(--mc)) 58%, rgb(var(--mc)) 100%);
    background-size:300% 100%;
    animation:tui-shimmer 1.67s linear infinite;
  }
  @keyframes tui-shimmer{from{background-position:100% 0} to{background-position:0% 0}}
  @media (prefers-reduced-motion:reduce){
    .tui-mode.is-active .tui-mode-title{
      background-color:rgb(var(--mc)); background-image:none; animation:none;
    }
  }
  .tui-mode-desc{
    /* only the selected item's description takes up space, same as the
       real arrow-key list — one blank row then the typewritten desc in
       white (matches _build_mode_row's style="white"). The open slot is a
       FIXED two-row height (the real builder reserves desc space in body_h
       and the Panel is terminal-fixed) so the frame never wobbles as
       different-length descriptions cycle. */
    margin:0; height:0; overflow:hidden; line-height:1.5; color:var(--t-text);
    max-width:60ch; opacity:0;
    transition:opacity .2s, height .25s ease, margin .25s ease;
  }
  .tui-mode.is-active .tui-mode-desc{
    /* one blank row, then two reserved rows — exactly what _build_mode_row lays
       out. Reserved rather than fitted: the height is the same for a one-line
       and a two-line description, so arrowing down the list never moves the
       rows below it. Exactly one card is active at any time, so the menu's
       total height is constant too. */
    height:3em; margin-top:1.5em; opacity:1;
  }
  @media (hover:hover){
    .tui-mode:focus-visible{outline:1px solid var(--accent-dim); outline-offset:4px; border-radius:2px}
  }

  /* bottom furniture — the app's tip block + version row (_build_tip_rows /
     _build_version_row), pinned under the centred mode list */
  /* The foot is now two lanes: the version row holds the left, the companion
     (tip bubble → tail → duck) stacks up the right, exactly as
     _build_companion lays it out. */
  .tui-foot{
    /* The two lanes do NOT share a flex row: the duck is wide and the version
       row is long, so side by side they overflowed the panel. In the app they
       never compete — the companion sits on the right where the left is empty,
       and only the narrow duck is ever level with the version text. Absolute
       placement inside a sized foot reproduces that.

       margin-top:auto pins the foot to the bottom; the matching auto above the
       first mode row splits the slack evenly, centring the list. */
    position:relative; margin-top:auto; padding-top:22px; min-height:130px;
  }
  .tui-companion{
    /* bottom:30px — down on the alcove's roof, clearing it by a few pixels
       rather than a whole row. The sprite carries _SHADES_TOP_PAD's two blank
       cell rows on top so the box never resizes mid-gag, and that padding grows
       upward, so this anchor is what sets where his feet land. */
    position:absolute; right:0; bottom:30px;
    display:flex; flex-direction:column; align-items:center; gap:0;
  }
  /* (The amber update box that used to sit above the bubble is gone: it is a
     state the app is only sometimes in, and a screenshot of the product should
     not be advertising a release that is newer than the one on the page.) */
  /* BETA chip beside the Performance title, as the real list shows it */
  .tui-beta{
    display:inline-block; vertical-align:middle; margin-left:10px;
    font-size:.78em; letter-spacing:.06em; color:rgb(226,186,96);
    border:1px solid rgb(120,95,45); border-radius:4px; padding:0 5px;
  }
  /* (No tip bubble. The mode's own description now carries the copy, under the
     title where the list puts it, so a second block of prose in the corner was
     saying nothing the screen did not already say. The duck stays.) */
  /* mirrored: the app's companion faces LEFT, toward the menu it sits beside
     (see _build_companion) — the source sprite faces right */
  /* No glyphs here either, and for a second reason on top of the mask story
     above: _pack_cells packs TWO source pixels into one cell — a ▀ carries the
     top colour as foreground and the bottom colour as background. Markup that
     keeps only `color` throws half the sprite away, which is why the duck came
     out as disconnected specks. Each cell is a box instead, and a two-colour
     cell is a hard-stop gradient at 50%. Nothing is measured against a font, so
     the rows meet exactly and the aspect is the terminal's own 1:2. */
  .tui-duck{margin:0; line-height:0; font-size:inherit; cursor:pointer}
  .tui-duck:focus-visible{outline:1px solid var(--accent-dim); outline-offset:4px}
  .tui-duck .r{display:block; height:2ch}
  .tui-duck i{display:inline-block; vertical-align:top; width:1ch; height:2ch}
  /* the robo companion on the Agents menu has no shades gag to click */
  .tui-duck-robo{cursor:default}

  /* ---- the Humans/Agents landing split (_screens_category.py) --------------
     The app's Phase 0: two rounded world-cards side by side, the question on
     the OUTER frame's border title rather than floating above them, and the key
     hints pinned at the bottom. Selection is carried by the card itself — accent
     border, tinted interior, bright title, and the mascot flapping — because
     that is all the real screen has: no cursor, no radio, no extra chrome. */
  .tui-split{
    display:flex; flex-direction:column; min-height:663px;
    /* the cards' own border titles sit at -8px, so without a band of clear air
       under the frame's title the two rows of eyebrows collide */
    padding-top:14px;
  }
  .tui-split[hidden]{display:none}
  /* The question rides the panel's top border, the way a Rich border title
     does: lifted onto the rule, with an opaque cut-out behind it to break the
     line. The panel has no ground of its own (the page glow shows through), so
     that cut-out paints --bg — the same colour the music alcove uses for the
     same job. `top` is measured from the padding box's outer edge, so the
     panel's own padding does not enter into it: the rule ::before draws sits
     2px above that edge (1px transparent border + the 1px outset), and a 14px
     label centres on it at -9px. */
  .tui-split-q{
    position:absolute; top:-9px; left:50%; transform:translateX(-50%);
    height:14px; line-height:14px;
    margin:0; padding:0 10px; background:var(--bg);
    color:rgb(150,150,168); font-size:12px; letter-spacing:.02em; white-space:nowrap;
  }
  .tui-worlds{display:grid; grid-template-columns:1fr 1fr; gap:2ch; flex:1; margin:auto 0}
  .tui-world{
    /* font:inherit is load-bearing, not tidiness. This is a <button>, and a
       button does NOT inherit the font — the UA gives it 13.33px Arial. Both
       children are sized in ch (the sprite's cells are width:1ch, the block
       title is calc(--cols * 1ch)), so without this the mascot is measured in
       Arial's ch instead of the terminal's, and the mobile font-size that
       shrinks the two worlds to fit a phone column never reaches them. */
    font:inherit;
    -webkit-appearance:none; appearance:none; cursor:pointer;
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0;
    padding:18px 12px 20px; text-align:center;
    background:transparent; border:1px solid rgb(52,54,62); border-radius:10px;
    position:relative; transition:border-color .18s ease, background-color .18s ease;
  }
  /* the card's border title — the lowercase category eyebrow, on the rule */
  /* the card draws its own 1px border rather than an outset ::before, so its
     rule is 1px above the padding box and a 14px label centres at -8px */
  .tui-world-eyebrow{
    position:absolute; top:-8px; left:14px; padding:0 6px;
    height:14px; line-height:14px; background:var(--bg);
    font-size:11px; letter-spacing:.08em; color:rgb(90,90,104);
  }
  .tui-world.is-active{border-color:rgb(var(--wc)); background:rgb(var(--wct))}
  .tui-world.is-active .tui-world-eyebrow{color:rgb(var(--wc))}
  .tui-world:focus-visible{outline:1px solid var(--accent-dim); outline-offset:4px}

  /* the 18-row full-body mascot, painted by site.js (WORLD_SPRITES) */
  .tui-sprite{display:block; line-height:0; margin:6px 0 14px; min-height:36ch}
  .tui-sprite .r{display:block; height:2ch}
  .tui-sprite i{display:inline-block; vertical-align:top; width:1ch; height:2ch}

  /* the block title reuses the mode-title mask machinery, in the card's accent */
  .tui-world-title{background-color:rgb(var(--wcd))}
  .tui-world.is-active .tui-world-title{background-color:rgb(var(--wc))}
  .tui-world-verb{
    display:block; margin-top:12px; font-size:13px; font-weight:700;
    color:rgb(var(--wcd));
  }
  .tui-world.is-active .tui-world-verb{color:rgb(210,214,220)}
  .tui-world-caps{display:block; margin-top:6px; font-size:12px; color:rgb(88,90,102)}
  .tui-world.is-active .tui-world-caps{color:rgb(140,144,156)}
  .tui-world-hint{
    margin:14px 0 0; text-align:center; font-size:12px; color:rgb(70,70,82);
  }
  .tui-world-hint b{font-weight:700; color:rgb(210,210,220)}
  /* left:4ch, not 0 — _build_version_row leads with _PAD precisely so its left
     edge lines up with the mode titles above rather than sitting flush to the
     panel padding, and staggering the two is what made the titles read as
     over-indented */
  /* bottom:-6px drops it into the panel's bottom padding on purpose. The music
     pocket's TEXT row is the last interior row of the screen, and
     _build_version_row is that same row — "bottom-left, opposite the music bar".
     They belong on one line; the foot's own box bottom is two rows higher than
     that, so the row has to reach past it. */
  .tui-version{position:absolute; left:2ch; bottom:-6px; margin:0; color:rgb(70,70,82); white-space:nowrap}
  .tui-version .v{color:rgb(120,120,140)}
  .tui-version b{font-weight:700; color:rgb(210,210,220)}
  .tui-version .sep,.tui-version .lbl{color:rgb(70,70,82)}
  /* The music status is a BOXED pocket now, not a flat subtitle: draw_music_pocket
     gives it its own rounded frame and the page Panel's bottom border curves up
     around it. The opaque background masks the border where it passes behind. */
  .tui-music{
    position:absolute; right:24px; bottom:-1px;
    /* two rows tall, because that is what the alcove is: draw_music_pocket
       spends one row on the roof (╭──╮) and the next on the music itself,
       before the panel's border row underneath. A single-row box was the roof
       sitting straight on the text. */
    background:transparent; padding:11px 10px 10px; white-space:pre;
    font-size:.92em; color:var(--t-muted);
    /* no bottom border, and bottom:-1px so the element covers the panel's own
       bottom border across its width — the alcove is open underneath, exactly
       as the app draws it (╰──╯ … gap … ╰──╯) */
    border:1px solid rgba(228,230,232,.45); border-bottom:none;
    border-radius:8px 8px 0 0;
  }
  /* The wall does not meet the border at a right angle: draw_music_pocket's
     border row is ╰───╯ · gap · ╰───╯, so the panel's bottom border RISES into
     each alcove wall through a rounded join. Two 8px quarter-arcs sit just
     outside the pocket, ╯ on the left and ╰ on the right.

     The arc is a radial-gradient RING rather than a rounded border, because the
     8px it spans is 8px of the panel's own bottom border that has to be covered
     — and a border-radius would clip exactly the corner where the cover is
     needed. A gradient is not clipped, so the box can be opaque underneath and
     still draw the curve on top. (An earlier pass masked with two offset copies
     of the pocket's silhouette instead; those ran the full height of the bar and
     showed as vertical seams either side of it.) */
  .tui-music::before,.tui-music::after{
    content:""; position:absolute; bottom:0; width:8px; height:8px;
    background-repeat:no-repeat;
  }
  .tui-music::before{
    right:100%;
    background-image:radial-gradient(circle 8px at 0 0,
      transparent 0 7px, rgba(228,230,232,.45) 7px 8px, transparent 8px);
  }
  .tui-music::after{
    left:100%;
    background-image:radial-gradient(circle 8px at 100% 0,
      transparent 0 7px, rgba(228,230,232,.45) 7px 8px, transparent 8px);
  }
  .tui-music .hint{color:var(--t-dim)}

  /* ---------- shared section furniture ---------- */
  section{padding:84px 0; position:relative}
  .eyebrow{
    font-family:var(--mono); font-size:.76rem; color:var(--accent); margin:0 0 14px;
    letter-spacing:.16em; text-transform:uppercase; font-weight:500;
  }
  h2{
    margin:0 0 12px; font-family:var(--display); font-weight:620;
    font-size:clamp(1.7rem,3.4vw,2.35rem); letter-spacing:-.025em; line-height:1.15;
  }
  h2 em{font-style:normal; color:var(--accent-bright); font-weight:620}
  .section-lede{margin:0 0 44px; color:var(--text-muted); max-width:58ch; font-size:1.05rem}
  .rule{border:none; border-top:1px solid var(--border); margin:0}

  /* ---------- stack / integrations (plain left-aligned text, on-theme) —
     two labelled groups: board/data sources and LLM providers ---------- */
  .stack{padding:40px 0}
  .stack .eyebrow{margin-bottom:22px}
  .stack-group{
    display:grid; grid-template-columns:170px 1fr; align-items:baseline; gap:8px 28px;
    max-width:760px;
  }
  .stack-group + .stack-group{margin-top:18px; padding-top:18px; border-top:1px solid var(--border)}
  .stack-cat{
    font-family:var(--mono); font-size:.74rem; letter-spacing:.14em; text-transform:uppercase;
    color:var(--text-dim); font-weight:500;
  }
  .stack-list{
    display:flex; flex-wrap:wrap; gap:12px 26px;
    font-family:var(--mono); font-size:.92rem; font-weight:500;
  }
  .stack-list span{color:var(--text-muted); transition:color .2s}
  .stack-list span:hover{color:var(--text)}
  @media (max-width:600px){
    .stack-group{grid-template-columns:1fr; gap:10px}
  }

  /* The two worlds' headers in the modes section — the same lowercase eyebrow
     the landing split stamps on each card's border, so the page and the app
     name the two families the same way. */
  .world-label{
    display:flex; align-items:center; gap:12px;
    margin:0 0 18px; font-family:var(--mono); font-size:.85rem; color:var(--text-dim);
  }
  .world-label span{
    font-weight:700; letter-spacing:.08em; text-transform:lowercase;
    color:var(--m-analysis);
  }
  .world-label::after{content:""; flex:1; height:1px; background:var(--border)}
  .world-label-agents{margin-top:56px}
  .world-label-agents span{color:var(--m-agent-usage)}
  /* the agents world runs one row of three, and its lede sits under the label */
  .world-label-agents + .section-lede{margin-bottom:18px}

  /* ---------- modes — one connected panel, hairline-divided, each cell
     carrying its own TUI accent (--mh, set inline in the HTML from the
     per-mode tokens). Grid rows share track height so cells always align;
     copy is never clamped — the cell grows to fit and the mono tag line
     pins to the bottom so there's no dead space, only structure. ---------- */
  .modes{
    display:grid; grid-template-columns:repeat(3,1fr); gap:1px; background:var(--border);
    border:1px solid var(--border); border-radius:var(--radius-lg); overflow:hidden;
    /* Scopes each card's backdrop-filter to blur only what's inside .modes —
       without this, the cards' glassy blur reaches straight through to the
       page's fixed, scroll-linked .scroll-aura glow behind everything and
       shows it as a smeared colored haze once you're scrolled far enough
       down for its opacity to have built up. */
    isolation:isolate;
  }
  /* An odd card count leaves the last row short: 7 modes in a 3-wide grid put
     Reporting alone beside two empty cells. The final card spans the row — but
     spanning alone just trades two empty cells for one empty card, so it also
     switches to a horizontal layout: heading, copy and tag sit side by side and
     fill the width with content. Reverts to the normal stacked card at the
     single-column breakpoint, where nothing is spanning. */
  .modes > .mode-card:last-child:nth-child(3n + 1){
    grid-column:1 / -1; min-height:0;
    flex-direction:row; align-items:center; gap:32px;
  }
  .modes > .mode-card:last-child:nth-child(3n + 1) .mode-head{flex:0 0 auto; min-width:180px}
  .modes > .mode-card:last-child:nth-child(3n + 1) > p{max-width:60ch}
  .modes > .mode-card:last-child:nth-child(3n + 1) .mode-tag{flex:0 0 auto; margin-left:auto}
  .mode-card{
    position:relative; min-height:225px;
    background:rgba(255,255,255,.022); isolation:isolate;
    -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
    border:none; border-radius:0;
    padding:28px 26px 24px; display:flex; flex-direction:column; gap:12px;
    transition:background .25s;
  }
  /* mode-colored hairline along the top edge, lit on hover — the cell's
     accent identity without painting the whole card */
  .mode-card::before{
    content:""; position:absolute; top:0; left:0; right:0; height:2px;
    background:linear-gradient(90deg, var(--mh), transparent 75%);
    opacity:.28; transition:opacity .25s;
  }
  .mode-card:hover::before{opacity:.9}
  .mode-card:hover{background:color-mix(in srgb, var(--mh) 4.5%, rgba(255,255,255,.015))}
  .mode-head{display:flex; align-items:baseline; gap:12px}
  .mode-head .num{font-family:var(--mono); font-size:.76rem; color:var(--mh); font-weight:600; letter-spacing:.04em}
  .mode-head h3{margin:0; font-size:1.08rem; color:var(--text); font-weight:600; letter-spacing:-.01em}
  .mode-card > p{margin:0; color:var(--text-muted); font-size:.93rem; line-height:1.62; max-width:44ch; flex:1}
  .mode-tag{
    font-family:var(--mono); font-size:.72rem; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-dim);
  }
  .mode-tag::before{content:"→ "; color:var(--mh)}

  /* ---------- pipeline — a real flow diagram: numbered rings sitting on a
     connecting line, each stage a step off it. Mirrors the hero demo above,
     which cycles through these exact same five stages. ---------- */
  .pipeline{
    display:grid; grid-template-columns:repeat(5,1fr); gap:0; counter-reset:stage;
    border:1px solid var(--border); border-radius:var(--radius-lg); overflow:hidden;
    background:linear-gradient(180deg, var(--bg-raised-2), var(--bg-raised)); box-shadow:var(--shadow-card);
    position:relative;
  }
  .pipeline::before{
    content:""; position:absolute; left:44px; right:44px; top:47px; height:1px;
    background:linear-gradient(90deg, transparent, var(--border-strong) 6%, var(--border-strong) 94%, transparent);
    z-index:0;
  }
  .stage{
    padding:26px 22px 24px; border-left:1px solid var(--border); position:relative;
    transition:background .2s;
  }
  .stage:first-child{border-left:none}
  .stage:hover{background:rgba(255,255,255,.02)}
  .stage::before{
    counter-increment:stage; content:counter(stage,decimal-leading-zero);
    position:relative; z-index:1; display:flex; align-items:center; justify-content:center;
    width:30px; height:30px; border-radius:50%;
    border:1px solid var(--border-strong); background:var(--bg-raised);
    font-family:var(--mono); font-size:.7rem; color:var(--accent); letter-spacing:.02em;
    transition:border-color .2s, color .2s, box-shadow .2s;
  }
  .stage:hover::before{border-color:var(--accent); color:var(--accent-bright); box-shadow:0 0 0 3px var(--accent-dim)}
  .stage h3{margin:14px 0 8px; font-size:1rem; font-weight:600; letter-spacing:-.01em}
  .stage p{margin:0; color:var(--text-muted); font-size:.85rem; line-height:1.6}
  .pipe-note{
    margin:26px 0 0; display:flex; gap:14px; align-items:baseline; max-width:64ch;
    color:var(--text-muted); font-size:.95rem;
  }
  .pipe-dots{display:none}  /* phone-only carousel indicator (see 600px block) */
  .pipe-note .k{font-family:var(--mono); font-size:.8rem; color:var(--accent); white-space:nowrap}
  .pipe-note code{color:var(--text); font-size:.85em; background:var(--bg-raised);
    border:1px solid var(--border); border-radius:5px; padding:1px 6px}

  /* ---------- get started ---------- */
  .start-grid{display:grid; grid-template-columns:1fr 1fr; gap:64px; align-items:start}
  /* one compact ROW per step — label · command chip · note — so the cards
     stay short and fill their width instead of stranding empty space. Each
     part owns its own grid column, so nothing wraps mid-line. Collapses to a
     clean vertical stack on narrow phones (see the 600px block). */
  .steps{display:grid; gap:10px}
  .step{
    display:grid; grid-template-columns:108px auto minmax(0,1fr);
    align-items:center; gap:16px; padding:12px 20px; transition:border-color .2s;
  }
  .step:hover{border-color:var(--border-strong)}
  .step-k{
    margin:0; font-family:var(--mono); font-size:.74rem; letter-spacing:.14em;
    text-transform:uppercase; color:var(--text-dim); font-weight:500; white-space:nowrap;
  }
  .step-k .n{color:var(--accent); font-weight:700; margin-right:8px}
  .step-cmd{
    font-family:var(--mono); font-size:.92rem; color:var(--text);
    background:var(--bg-sunken); border:1px solid var(--border); border-radius:6px;
    padding:6px 12px; max-width:100%; overflow-x:auto; white-space:nowrap; justify-self:start;
  }
  .step-cmd::before{content:"$ "; color:var(--accent)}
  .step-note{margin:0; color:var(--text-muted); font-size:.86rem}
  .step-note code{color:var(--text); background:var(--bg-sunken); border:1px solid var(--border);
    border-radius:5px; padding:2px 7px; font-size:.88em}
  /* step 1's alternative is wide (a whole command) — drop it to a second row
     under the command chip instead of cramming the note column */
  .step-2row .step-note{grid-column:2 / -1; grid-row:2}
  .dim{color:var(--text-dim)}
  .note{margin-top:18px; color:var(--text-muted); font-size:.92rem}
  .note code{color:var(--text); font-size:.86em}

  .cta-panel h2{margin-bottom:16px}
  .cta-panel .lede2{color:var(--text-muted); margin:0 0 30px; max-width:40ch; font-size:1.05rem}
  .cta-panel .codeblock{max-width:430px}
  .cta-links{margin-top:22px; display:flex; gap:12px; flex-wrap:wrap}

  /* ---------- download — the macOS app ----------
     One control in two states. Both targets are the same .dl-btn;
     .is-recommended styles the action and everything else styles the alternate,
     which drops its own label and shows only the architecture it is for.
     initDesktopDownload() moves that class on an Intel Mac, so both states have
     to read correctly whichever anchor carries it. */
  :root{
    /* Apple's mark as a mask, so it takes the button's colour in either state.
       A path rather than U+F8FF, which is private-use and renders only on Apple
       platforms. */
    --apple-mark:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z'/%3E%3C/svg%3E");
  }
  .dl-apple{
    flex:none; width:16px; height:19px; background:currentColor;
    -webkit-mask:var(--apple-mark) no-repeat center/contain;
    mask:var(--apple-mark) no-repeat center/contain;
  }
  .dl-row{display:flex; align-items:stretch; gap:12px; flex-wrap:wrap; margin:30px 0 0}
  .dl-btn{
    display:flex; align-items:center; gap:14px; padding:15px 22px;
    border:1px solid var(--border-strong); border-radius:var(--radius);
    background:var(--bg-raised); color:var(--text-muted);
    transition:border-color .18s, color .18s, background .18s, transform .18s, box-shadow .18s;
  }
  .dl-btn:hover{border-color:var(--accent); color:var(--text); transform:translateY(-1px)}
  .dl-btn:focus-visible{outline:2px solid var(--accent-bright); outline-offset:3px}
  .dl-btn .dl-t{display:flex; flex-direction:column; gap:2px; min-width:0}
  .dl-btn .dl-k{font-weight:640; font-size:1.02rem; letter-spacing:-.012em; color:var(--text)}
  .dl-btn .dl-sub{font-family:var(--mono); font-size:.73rem; letter-spacing:.05em; color:var(--text-dim)}

  /* the action */
  .dl-btn.is-recommended{
    order:-1;
    padding:17px 28px; border-color:transparent;
    background:var(--text); color:var(--bg);
    box-shadow:0 16px 36px -20px rgba(140,170,255,.8);
  }
  .dl-btn.is-recommended .dl-k{color:var(--bg)}
  .dl-btn.is-recommended .dl-sub{color:color-mix(in srgb, var(--bg) 62%, var(--text))}
  .dl-btn.is-recommended:hover{background:var(--accent-bright); color:var(--bg); transform:translateY(-2px)}

  /* the alternate */
  .dl-btn:not(.is-recommended) .dl-k{display:none}
  .dl-btn:not(.is-recommended){padding:14px 16px; gap:10px}
  .dl-btn:not(.is-recommended) .dl-sub{font-size:.77rem; letter-spacing:.03em; color:var(--text-muted)}
  .dl-btn:not(.is-recommended) .dl-apple{width:13px; height:15px}

  /* what the download weighs and what it needs */
  .dl-meta{display:flex; flex-wrap:wrap; gap:8px 9px; margin:20px 0 0; padding:0; list-style:none}
  .dl-meta li{
    font-family:var(--mono); font-size:.71rem; letter-spacing:.1em; text-transform:uppercase;
    color:var(--text-muted); border:1px solid var(--border); border-radius:999px; padding:4px 11px;
  }
  .dl-meta .is-good{color:var(--success); border-color:rgba(89,192,126,.3)}
  .dl-hint{
    margin:14px 0 0; max-width:60ch;
    font-family:var(--mono); font-size:.76rem; line-height:1.7; color:var(--text-dim);
  }
  .dl-hint b{color:var(--text-muted); font-weight:600}
  .dl-hint a{white-space:nowrap}

  /* ---------- the Dock ----------
     A macOS Dock holding the app's icon, built from the same three duck layers
     the mascot walks on. The plate mirrors the desktop repo's icon generator:
     rgb(18,43,34)→rgb(8,16,14) at a 208/1024 corner radius. */
  .mac-split{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,392px); gap:40px; align-items:center}
  .mac-split .tagline{font-size:clamp(2.4rem,4.4vw,3.4rem)}
  /* the top padding is the magnified icon's overhang plus room for its label,
     so neither can ever ride up into the copy above when the split stacks */
  .dock{--dock-icon:92px; --dock-h:52px; display:flex; justify-content:center; padding:94px 0 6px}
  .dock-tray{
    position:relative; display:flex; align-items:flex-end; gap:10px; padding:9px 12px 10px;
    border:1px solid rgba(255,255,255,.1); border-radius:20px;
    background:rgba(255,255,255,.055);
    -webkit-backdrop-filter:blur(22px); backdrop-filter:blur(22px);
    box-shadow:0 32px 62px -30px rgba(0,0,0,.9), 0 1px 0 rgba(255,255,255,.06) inset;
  }
  .dock-slot{
    flex:none; width:var(--dock-h); height:var(--dock-h); border-radius:12px;
    background:linear-gradient(180deg, rgba(255,255,255,.075), rgba(255,255,255,.028));
    border:1px solid rgba(255,255,255,.05);
  }
  /* ours sits lifted and named, the way the Dock magnifies the icon under the
     pointer. The box keeps the tray's own height (--dock-h) and the icon
     overhangs it, so the tray is never stretched by the magnification. */
  .dock-app{
    position:relative; flex:none; margin:0 6px;
    width:var(--dock-icon); height:var(--dock-h);
  }
  .dock-app .app-icon{position:absolute; bottom:0; left:0; width:100%}
  .dock-run{
    position:absolute; bottom:-7px; left:50%; margin-left:-2px;
    width:4px; height:4px; border-radius:50%; background:var(--text-muted);
  }
  .dock-label{
    position:absolute; left:50%; transform:translateX(-50%); white-space:nowrap;
    bottom:calc(var(--dock-icon) + 22px);
    font-family:var(--mono); font-size:.72rem; color:var(--text);
    background:rgba(28,30,34,.94); border:1px solid rgba(255,255,255,.1);
    border-radius:6px; padding:4px 10px;
    -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  }
  .app-icon{
    position:relative; aspect-ratio:1; border-radius:20.3%;
    background:linear-gradient(180deg, rgb(18,43,34), rgb(8,16,14));
    box-shadow:
      0 0 0 1.5px rgba(146,214,178,.4),
      0 1px 0 rgba(255,255,255,.1) inset,
      0 22px 40px -14px rgba(0,0,0,.9);
  }
  .app-icon .ai-rig{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:72%}
  .app-icon img{
    display:block; width:100%; height:auto; image-rendering:pixelated;
    user-select:none; -webkit-user-drag:none;
  }
  .app-icon .ai-wing,.app-icon .ai-glasses{position:absolute; top:0; left:0}
  /* the one motion moment on these pages: a single Dock bounce as the strip
     arrives, never a loop */
  [data-reveal].is-visible .app-icon{animation:dock-bounce 1.2s cubic-bezier(.28,.9,.34,1) .35s 1 both}
  @keyframes dock-bounce{
    0%{transform:translateY(0)}
    22%{transform:translateY(-14px)}
    44%{transform:translateY(0)}
    58%{transform:translateY(-5px)}
    76%{transform:translateY(0)}
    88%{transform:translateY(-2px)}
    100%{transform:translateY(0)}
  }
  @media (prefers-reduced-motion:reduce){
    [data-reveal].is-visible .app-icon{animation:none}
  }

  /* the launch band — the one section on the landing page with a ground of its
     own, tinted from the icon plate so arriving in it reads as news */
  .launch{position:relative; isolation:isolate}
  /* the ground runs under the nav rail too — body carries padding-left:var(--rail),
     so a background on the section itself would stop at the rail's inner edge and
     read as a seam. isolation: keeps the -1 above the page's own ground. */
  .launch::before{
    content:""; position:absolute; z-index:-1;
    top:0; bottom:0; right:0; left:calc(-1 * var(--rail));
    border-top:1px solid var(--border); border-bottom:1px solid var(--border);
    background:
      radial-gradient(90% 120% at 74% 16%, rgba(42,170,105,.1), transparent 62%),
      linear-gradient(180deg, rgba(18,43,34,.36), rgba(11,12,14,0) 78%);
  }
  @media (max-width:900px){ .launch::before{left:0} }

  /* what it isn't — a term and its qualification, at a readable measure */
  .facts{margin:36px 0 0; padding:0}
  .fact{
    display:grid; grid-template-columns:236px minmax(0,1fr); gap:6px 36px;
    padding:20px 0; border-top:1px solid var(--border);
  }
  .fact:last-child{border-bottom:1px solid var(--border)}
  .fact dt{
    font-family:var(--mono); font-size:.74rem; letter-spacing:.1em; text-transform:uppercase;
    color:var(--text); font-weight:500;
  }
  .fact dd{margin:0; color:var(--text-muted); font-size:.97rem; line-height:1.66; max-width:62ch}

  /* body carries padding-left:var(--rail) until 900px, so the copy column is the
     viewport less the rail, the wrap's padding, the gap and the dock's track.
     Two columns only below this point once that leaves room for the download
     row's two buttons side by side. */
  @media (max-width:1360px){
    .mac-split{grid-template-columns:1fr; gap:24px}
    .dock{justify-content:flex-start}
  }
  @media (max-width:760px){
    .fact{grid-template-columns:1fr; gap:8px; padding:18px 0}
  }
  @media (max-width:600px){
    .dl-row{gap:10px}
    .dl-btn{min-width:0; width:100%}
    .dock{--dock-icon:70px; --dock-h:42px; padding-top:82px}
    .dock-tray{gap:8px; padding:8px 10px 9px}
  }

  /* ---------- footer (left-aligned, on-theme) ---------- */
  /* divider fades in from the left (a border-image gradient rather than a flat
     hairline) so it reads as emerging into the footer rather than a hard rule */
  footer{
    border-top:1px solid transparent;
    border-image:linear-gradient(90deg, transparent 0%, var(--border) 22%, var(--border) 100%) 1;
    padding:48px 0 80px; position:relative; z-index:1;
  }
  .foot{display:flex; flex-direction:column; align-items:flex-start; gap:24px}

  /* the mascot: walks the bottom edge of the viewport, x driven from scroll
     progress (site.js). Transform layering, outermost → innermost:
       .duck-walker  — JS translateX + direction flip
       .duck-rig     — waddle while scrolling
       .d-wing       — continuous gentle flap (own timeline)
       .d-glasses    — slow sunglasses bob (own timeline)
     Separate elements per transform so none of them ever fight. */
  .duck-walker{
    position:fixed; top:0; left:0; z-index:40; pointer-events:none;
    will-change:transform;
    transition:opacity .15s ease;
  }
  /* the sprite itself IS tappable (the wrapper stays click-through so it never
     blocks links behind it) — a tap makes it jump, same startle as the footer
     cursor-dodge. */
  .duck-rig{pointer-events:auto; cursor:pointer; touch-action:manipulation}
  /* between areas the duck TELEPORTS — fades out here, pops in there
     (site.js swaps the position while this class holds opacity at 0) */
  .duck-walker.teleporting{opacity:0}
  /* page entrance: hidden until the hero's staggered rise finishes, then a
     gentle fade-in (backwards fill only — a forwards fill would pin opacity
     and block the teleport transitions afterwards) */
  .duck-walker.unloaded{opacity:0}
  .duck-walker.hatch{animation:duck-hatch .9s ease backwards}
  @keyframes duck-hatch{from{opacity:0} to{opacity:1}}
  .duck-rig{position:relative; width:64px}
  .duck-rig img{
    display:block; width:100%; height:auto;
    image-rendering:pixelated; user-select:none; -webkit-user-drag:none;
  }
  .duck-rig .d-base{filter:drop-shadow(0 5px 4px rgba(0,0,0,.45))}
  .duck-rig .d-wing,.duck-rig .d-glasses,.duck-rig .d-glasses2{position:absolute; top:0; left:0}
  /* the SECOND pair of shades — hidden until a startle jump reveals it at
     eye level while the first pair is up the forehead (DOM-ordered under
     .d-glasses so the returning pair lands on top of it) */
  .duck-rig .d-glasses2{opacity:0}
  .duck-walker.startled .d-glasses2{animation:glasses2-reveal 1.35s linear both}
  @keyframes glasses2-reveal{
    0%,6%{opacity:0}
    12%{opacity:1}
    56%{opacity:1}
    63%,100%{opacity:0}
  }
  .d-wing{transform-origin:42% 55%; animation:wing-flap 1.4s ease-in-out infinite}
  @keyframes wing-flap{
    0%,100%{transform:translateY(0) rotate(0deg)}
    50%{transform:translateY(-2.2%) rotate(-2.5deg)}
  }
  .d-glasses{animation:glasses-bob 2.6s ease-in-out infinite}
  @keyframes glasses-bob{
    0%,100%{transform:translateY(0)}
    50%{transform:translateY(1.5%)}
  }
  /* startled (cursor touch → jump): the shades fly high up the forehead,
     hang there through the whole flight, and drop back with an overshoot */
  .duck-walker.startled .d-glasses{animation:glasses-pop 1.35s cubic-bezier(.3,1.3,.4,1) both}
  @keyframes glasses-pop{
    0%{transform:translateY(0) rotate(0deg)}
    10%{transform:translateY(-10%) rotate(-7deg)}
    52%{transform:translateY(-8.5%) rotate(-5deg)}
    64%{transform:translateY(1.2%) rotate(0deg)}
    72%{transform:translateY(-1%) rotate(0deg)}
    80%,100%{transform:translateY(0) rotate(0deg)}
  }
  /* airborne: the wings work hard — much faster, deeper flap while the
     physics floats it back down to the ground */
  .duck-walker.airborne .d-wing{animation:wing-flap-hard .3s ease-in-out infinite}
  @keyframes wing-flap-hard{
    0%,100%{transform:translateY(0) rotate(0deg)}
    50%{transform:translateY(-4.5%) rotate(-6deg)}
  }
  .duck-walker.walking .duck-rig{animation:duck-waddle .38s ease-in-out infinite}
  @keyframes duck-waddle{
    0%,100%{transform:rotate(0deg) translateY(0)}
    25%{transform:rotate(-4deg) translateY(-2px)}
    75%{transform:rotate(4deg) translateY(-2px)}
  }
  /* tap-jump: a self-contained hop played when the mascot is clicked/tapped
     away from the footer (the footer perch uses the physics leap instead). The
     .hop class wins over .walking since it's applied last / one-shot. */
  .duck-walker .duck-rig.hop{animation:duck-hop .62s cubic-bezier(.3,1.1,.4,1) 1}
  @keyframes duck-hop{
    0%{transform:translateY(0) rotate(0deg)}
    28%{transform:translateY(-34px) rotate(-4deg)}
    46%{transform:translateY(-38px) rotate(0deg)}
    70%{transform:translateY(0) rotate(0deg)}
    82%{transform:translateY(-5px) rotate(0deg)}
    100%{transform:translateY(0) rotate(0deg)}
  }
  /* its whole purpose is motion — hide under reduced motion */
  @media (prefers-reduced-motion:reduce){.duck-walker{display:none}}

  /* the duck's rotating "catch me" speech bubble — fades in once the page is
     scrolled to the very bottom, sits IN LINE beside the duck (level with its
     head), tail pointing sideways back at it. The line cycles on a timer and a
     jump interrupts it with a reaction (landing only). */
  .duck-bubble{
    position:fixed; top:0; left:0; z-index:41; pointer-events:none;
    transform:translateX(-6px); transform-origin:left center;
    background:var(--bg-raised); color:var(--text);
    border:1px solid var(--border-strong); border-radius:12px;
    padding:8px 13px; font-family:var(--mono); font-size:.82rem; white-space:nowrap;
    box-shadow:0 8px 22px rgba(0,0,0,.5);
    opacity:0; visibility:hidden;
    transition:opacity .28s ease, transform .32s cubic-bezier(.2,1.3,.4,1);
  }
  .duck-bubble.show{opacity:1; visibility:visible; transform:translateX(0)}
  /* the cycling text swaps with a quick pop (retriggered from JS on each line) */
  .duck-bubble.say{animation:duck-say .34s ease}
  @keyframes duck-say{0%{opacity:.25; transform:translateX(0) scale(.94)} 100%{opacity:1; transform:translateX(0) scale(1)}}
  /* tail on the LEFT side, pointing back toward the duck (bubble is to its right) */
  .duck-bubble::after{
    content:""; position:absolute; top:50%; left:-6px; margin-top:-5px;
    width:11px; height:11px; background:var(--bg-raised);
    border-left:1px solid var(--border-strong); border-bottom:1px solid var(--border-strong);
    transform:rotate(45deg);
  }
  /* flipped: bubble sits to the LEFT of the duck → tail on the right */
  .duck-bubble.flip{transform-origin:right center; transform:translateX(6px)}
  .duck-bubble.flip.show{transform:translateX(0)}
  .duck-bubble.flip::after{
    left:auto; right:-6px;
    border-left:none; border-bottom:none;
    border-right:1px solid var(--border-strong); border-top:1px solid var(--border-strong);
  }
  .foot .brandline{font-family:var(--mono); font-weight:800; font-size:1.3rem;
    letter-spacing:.08em; text-transform:uppercase; color:var(--text)}
  .foot .brandline .sub{display:block; margin-top:8px; font-weight:400; font-size:.8rem;
    letter-spacing:0; text-transform:none; color:var(--text-dim)}
  .foot .links{display:flex; flex-wrap:wrap; gap:10px 28px; font-family:var(--mono); font-size:.88rem}
  .foot .links a{color:var(--text-muted); transition:color .2s} .foot .links a:hover{color:var(--accent)}
  .foot .meta{font-family:var(--mono); font-size:.78rem; color:var(--text-dim); line-height:1.9; text-align:left}
  .foot .meta code{color:var(--text-muted)}

  @media (max-width:980px){
    .modes{grid-template-columns:1fr 1fr}
    .pipeline{grid-template-columns:1fr 1fr}
    .pipeline::before{display:none}
    .stage{border-top:1px solid var(--border)}
    .stage:nth-child(-n+2){border-top:none}
    .stage:nth-child(odd){border-left:none}
    .start-grid{grid-template-columns:1fr; gap:48px}
    /* drop the full-height hero centering as soon as we're off the wide
       layout — the 100vh min + justify-center strands the copy mid-screen with
       a big dead band above it once the demo stacks below rather than beside.
       Top-align it and sit it just under the (sticky) nav. */
    .hero-copy{min-height:0; justify-content:flex-start; padding:20px 0 8px}
    .scroll-hint{display:none}
  }
  @media (max-width:860px){
    .hero{padding:0 0 72px}
    .hero-copy{min-height:0; justify-content:flex-start; padding:16px 0 8px}
    /* the "AI agent…" pill wraps to two lines and crowds the hero on phones */
    .hero-copy .badge{display:none}
    /* the hint exists to point below the fold on the full-height desktop
       hero; on mobile the terminal is directly beneath the CTA */
    .scroll-hint{display:none}
    .hero-cta .codeblock{min-width:0; width:100%}
    .tui{font-size:9.5px; padding:8px 8px 12px}
    .tui-panel{padding:14px 14px 10px}
    .tui-splash{min-height:380px}
    .tui-splash-word{font-size:8px}
  }
  @media (max-width:600px){
    section{padding:60px 0}
    /* steps go back to a clean left-aligned stack on phones, where a row
       would wrap */
    .step{grid-template-columns:1fr; justify-items:start; gap:9px; padding:14px 18px}
    /* phone: the full 8-row menu is a 7px unreadable wall — show a
       single-mode showcase instead: only the active mode renders, at a
       legible size, cycling with its typed description. Tip block, version
       row, and music bar are noise at this scale and are dropped. */
    .tui{font-size:11px}
    .tui-panel{padding:16px 16px 14px}
    .tui-splash{min-height:170px}
    .tui-splash-word{font-size:7px}
    .tui-menu{gap:0}
    .tui-mode{display:none; padding-left:0}
    /* centred composition: big title filling the width (site.js scales each
       title from its column count), description centred beneath */
    .tui-mode.is-active{
      display:flex; flex-direction:column; align-items:center; justify-content:center;
      min-height:170px; text-align:center;
    }
    .tui-mode-desc{max-width:36ch; text-align:left}
    .tui-mode.is-active .tui-mode-desc{height:54px; margin:14px 0 0}
    .tui-foot,.tui-music{display:none}
    /* The split is the one screen that must stay two-up on a phone — stacked,
       the two worlds stop reading as a choice between them.

       Both the mascot (34 cells at width:1ch) and the block title
       (width:calc(--cols * 1ch), 25ch) are sized in ch, and a phone column is
       ~150px. So the lever is font-size, NOT transform: a transform scales the
       painted result and leaves the layout box at full width, which wraps the
       sprite's inline-block cells and scrambles the mascot. Shrinking the ch
       unit shrinks the boxes themselves, and both children follow it. */
    .tui-split{min-height:0; font-size:6px}
    .tui-worlds{gap:1ch}
    .tui-world{padding:14px 6px 16px}
    .tui-sprite{margin-bottom:0}
    /* belt and braces: a sprite row must never wrap, whatever the column does */
    .tui-sprite .r{white-space:nowrap}
    /* these are px-sized, so they do not ride the ch shrink above */
    .tui-world-eyebrow{font-size:9px}
    .tui-world-verb{font-size:11px; margin-top:6px}
    .tui-world-caps{font-size:9px}
    .tui-split-q{font-size:10px}
    .tui-world-hint{display:none}
    /* the compact frame doesn't need the desktop demo's breathing room */
    .hero{padding-bottom:36px}
    .hero-demo{margin-top:20px; padding-bottom:0}
    .modes{grid-template-columns:1fr}
    .mode-card{min-height:0; padding:22px 20px 20px}
    /* nothing spans at one column, so the last card goes back to the stacked
       layout every other card uses */
    .modes > .mode-card:last-child:nth-child(3n + 1){flex-direction:column; align-items:stretch; gap:12px}
    .modes > .mode-card:last-child:nth-child(3n + 1) .mode-tag{margin-left:0}
    /* pipeline: a horizontal snap carousel instead of five stacked cards —
       one swipe-height instead of ~5 screens; the 76% column leaves the next
       stage peeking in as the scroll affordance */
    .pipeline{
      grid-template-columns:none; grid-auto-flow:column; grid-auto-columns:76%;
      overflow-x:auto; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch;
      scrollbar-width:none;
    }
    .pipeline::-webkit-scrollbar{display:none}
    .pipeline::before{display:none}
    .stage{scroll-snap-align:start; border-top:none; border-left:1px solid var(--border)}
    /* re-assert over the 980px two-column rules' higher-specificity selectors */
    .stage:nth-child(-n+2){border-top:none}
    .stage:nth-child(odd){border-left:1px solid var(--border)}
    .stage:first-child{border-left:none}
    /* label stacks above the text — the two-column note is too cramped here */
    .pipe-note{flex-direction:column; gap:8px}
    /* carousel affordances: content fades out at the right edge while there's
       more to scroll (site.js drops .at-end's mask when you reach the end),
       plus position dots + a swipe hint in the TUI's ● ○ language */
    .pipeline{
      -webkit-mask-image:linear-gradient(90deg, #000 calc(100% - 56px), rgba(0,0,0,.15));
              mask-image:linear-gradient(90deg, #000 calc(100% - 56px), rgba(0,0,0,.15));
    }
    .pipeline.at-end{-webkit-mask-image:none; mask-image:none}
    .pipe-dots{
      position:relative; display:flex; align-items:center; justify-content:center; gap:9px;
      margin-top:16px; font-family:var(--mono); font-size:.72rem; color:rgb(70,70,82);
    }
    .pipe-dots .on{color:var(--accent)}
    /* absolute so the hint never shifts the dot cluster off true centre —
       as a flex sibling it kept occupying space even at opacity 0 */
    .pipe-dots .pipe-hint{
      position:absolute; left:50%; margin-left:56px; top:50%; transform:translateY(-50%);
      font-size:.66rem; letter-spacing:.14em; text-transform:uppercase;
      color:var(--text-dim); transition:opacity .5s;
    }
    .pipe-dots.swiped .pipe-hint{opacity:0}
  }
