/* The front door.
   The owner's design, 3 October (proposal A, "the device"): one handset pinned beside five beats
   of text, and the scroll is the finger that walks it through a whole grant — asked, allowed,
   open, watched, ended. Under that, the bands that make the story believable, ending with the
   one that lists what Vortex cannot do yet.

   House rules kept from the page this replaces:
   · one signal hue, used only where the page needs you
   · Doto for digits only
   · hairlines, never shadows, to separate things that are equal
   · one committed light look — there is no dark palette here, and no `prefers-color-scheme`
     block, because every token below is chosen against the same paper ground the app uses

   The fonts are the relay's own copies under `public/font/`, reached through `/vortex` like every
   other asset (serveStatic rewrites it): the page must not fetch a byte from anywhere else, and
   `font-src 'self'` in the HTML's CSP would refuse it if it tried. */
@font-face {
  font-family: 'Instrument Sans';
  font-style: normal; font-weight: 400 700; font-stretch: 100%; font-display: swap;
  src: url('/vortex/font/instrument-sans-latin.woff2') format('woff2');   /* OFL */
}
@font-face {
  font-family: 'Doto';
  font-style: normal; font-weight: 400 900; font-display: swap;
  src: url('/vortex/font/doto-latin.woff2') format('woff2');              /* OFL */
}

:root {
  --ink: #141614; --ink-2: #5C5F5B; --ink-3: #8A8D88;
  --line: #D8D8D3; --hairline: #ECECE8; --bg: #F6F6F4; --surface: #FFFFFF;
  --control: #E9E9E5;
  --signal: #2E5FCF; --signal-text: #234AA8; --signal-tint: #E8EEFB;
  --font-text: 'Instrument Sans', 'Helvetica Neue', Arial, sans-serif;
  --font-digits: 'Doto', 'Courier New', monospace;
  --r-tile: 8px; --r-card: 12px; --r-pill: 999px;
  --ls-label: .08em;
  --gutter: clamp(18px, 4vw, 48px);
  --maxw: 1180px;
  --dock-h: 0px;
  --top-h: 68px;        /* the sticky header's own height; the hero fills what is left */
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font-family: var(--font-text); font-size: 15px; line-height: 1.5;
  -webkit-font-smoothing: antialiased; overflow-x: hidden;
  padding-bottom: var(--dock-h);
}
[hidden] { display: none !important; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; border-radius: 6px; }
img, svg { max-width: 100%; }

.dot { font-family: var(--font-digits); font-weight: 800; font-variant-numeric: tabular-nums; }
.ic { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
.lbl { font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--ink-3); }
.wrap { max-width: var(--maxw); margin: 0 auto; padding-inline: var(--gutter); }

/* ── chrome ────────────────────────────────────────────────────────────────────────────── */
/* The wordmark stays where the page starts and scrolls away with it. It used to be sticky, and
   the owner read the result correctly on 4 October: the sheet behind it had to brighten as the
   ground faded out underneath, so the one patch of the page that never moved was also the only
   patch that changed colour. There is nothing to brighten over now. What follows the reader is
   the dock, which is the two things a reader might actually want on the way down. */
.top {
  position: static; z-index: 60;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  /* Full bleed, with the content held on the same column as `.wrap` by padding rather than by
     a max-width. The old version had `max-width` plus a `position: fixed` pseudo-element for its
     sheet — and because `.top` carries the arrival animation, it animates `translate`, which
     makes it a containing block for fixed children. So the "full width" sheet was only ever as
     wide as the content column. That is the bar.
     What replaces it is not a sheet at all. A sheet has an edge, and an edge over a continuous
     ground is a bar however you colour it. This is a gradient that gives out before it ends, so
     there is nothing to see the end of. */
  padding: 15px max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  background: none;
}
/* Lower case throughout, and the o is the thing itself: the orb is a crop of the same fluid that
   is turning behind the headline, at 3.4x zoom, so the corner is literally the eye of the ground.
   It reads as "vortex" or as "v0rtex" depending on what you are looking for, which is the point.

   The flat mark underneath it is not decoration — it is what a browser without WebGL2 gets, and
   what sits there for the half second before the first frame. The canvas fades over it. */
/* The wordmark is the logo. Not a mark beside a word — the o IS the vortex, the fluid is wider
   than the letter it stands in for, and its arms run behind the v and the r. That is why the
   canvas is transparent (vortex-gl.js writes coverage, not paper) and why it is absolutely
   positioned at 250% of its slot: it overflows the o in every direction and the letters, raised
   one layer, cut across it.

   The word is set large because the orb has a floor. Below about 26px the fluid stops being a
   vortex and becomes a smudge, and an `o` in a 17px logotype has 9px of x-height. So the word
   grows to meet the mark rather than the mark shrinking to meet the word. */
.mark { display: inline-flex; align-items: center; text-decoration: none; }
.wm {
  position: relative; display: inline-flex; align-items: center;
  font-weight: 700; font-size: 38px; letter-spacing: -.035em; line-height: 1;
}
.wm b { position: relative; z-index: 2; font-weight: inherit; }
.glyph { position: relative; z-index: 1; width: .5em; height: 1em; flex: 0 0 auto; }
.glyph svg {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  width: .58em; height: .58em; stroke-width: 2;
}
/* Square, and four times the width of the slot it stands in: the fluid is a disc that fades out
   on its own before it reaches the canvas edge, so there is nothing to cut off, and what spills
   past the o runs behind the v and the r. */
.glyph .orb {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  width: 1.68em; height: 1.68em; pointer-events: none; opacity: 0;
}
.glyph.live .orb { opacity: 1; }
.glyph.live svg { display: none; }
@media (max-width: 620px) { .wm { font-size: 29px; } }
.quiet { font-size: 13.5px; font-weight: 600; color: var(--ink-2); text-decoration: none; white-space: nowrap; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ── the device ─────────────────────────────────────────────────────────────────────────
   The product is the illustration. One phone, pinned, and the scroll is the finger that
   walks it through a whole grant: asked, allowed, open, watched, ended.                  */

/* hero */
/* The first screen is the hero and nothing else. Before this the handset band started wherever
   the hero happened to end, so on a tall window you got a phone cut in half at the fold and its
   scroll-driven story began before you had scrolled anywhere. `svh` and not `vh`: on a phone the
   browser's own toolbars are counted in, so the Android button lands above them rather than
   under them. The content still wins if it is taller than the screen — this is a floor, not a
   cage, and a landscape phone simply scrolls. */
/* The hero no longer carries the ground, so it needs no stacking context of its own — but it
   keeps `position: relative` because the dock and the arrival sequence measure against it. */
.hero {
  position: relative;
  min-height: calc(100vh - var(--top-h));
  min-height: calc(100svh - var(--top-h));
  display: grid; align-content: center;
  padding: clamp(14px, 2vw, 26px) 0 clamp(20px, 3vw, 38px);
}
/* The ground is the page's ground, not the hero's. While it lived inside the hero it stopped
   where the hero stopped, and the fold showed as a band of slightly different paper against
   everything below it — the thing the owner kept pointing at. Fixed behind the whole document
   there is one continuous surface and no edge to see. It sits outside `#shell` on purpose: the
   shell is faded and hidden during a transition, and the ground must survive both. */
#ground {
  position: fixed; inset: 0; z-index: -1; display: block; width: 100%; height: 100%;
  opacity: 0; transition: opacity 1s ease; pointer-events: none;
}
#ground.on { opacity: 1; }
.hero .wrap { position: relative; z-index: 2; }
.hero .wrap { display: grid; gap: clamp(16px, 2vw, 26px); justify-items: center; text-align: center; }
.hero h1 { margin: 0; max-width: 30ch; font-size: clamp(34px, 5.2vw, 62px); line-height: 1.02; letter-spacing: -.034em; font-weight: 700; color: var(--ink); }
.hero .tagline { margin: 0; max-width: 56ch; font-size: clamp(16.5px, 1.6vw, 22px); line-height: 1.4; letter-spacing: -.012em; font-weight: 500; color: var(--ink); }
/* The three things the line is selling, in the page's one hue. Not a genuinely light blue: on
   this paper ground that drops under 4.5:1 and stops being readable at 22px. `nowrap` because a
   highlighted phrase broken across two lines stops reading as one thing — "agentic / control"
   split the term in half at 1280px. */
.hero .tagline b { font-weight: 650; color: var(--signal); white-space: nowrap; }

/* The last word of the headline loses its grip for a fifth of a second every nine, the way a
   tape that has been rewound too many times does: two ghosts of the same word sit exactly on top
   of it, and a band of each is shoved a couple of pixels sideways. The word never stops being
   readable and nothing around it moves — the ghosts are absolutely positioned, so there is no
   reflow to see. Both ghosts are palette colours; a red/cyan split would be a second hue on a
   page that has one. It is the only motion here that is not answering something the reader did,
   which is the argument for keeping it this small and this rare. */
/* `steps(1, end)` on all three: a tape does not ease. Every value holds until the next frame
   replaces it, which is why a band appears and vanishes rather than swelling into view. Eight
   frames of about 50ms inside a 400ms tear, and the tear comes round every five seconds; the
   negative delay puts the first one at roughly 1.5s, just after the hero has finished arriving,
   so nobody has to wait to find out the headline does this.

   The word keeps its own colour and weight throughout. An early pass dimmed it and that did not
   read as a dropped frame, it read as a font failing to load — the displacement is what sells
   it, not the fading. Both ghosts are palette colours: a red/cyan split would put a second hue
   on a page that has exactly one. */
.warp { position: relative; display: inline-block; animation: warp-word 5s steps(1, end) -2.9s infinite; }
.warp::before, .warp::after {
  content: attr(data-w); position: absolute; inset: 0; pointer-events: none;
  opacity: 0; clip-path: inset(100% 0 0 0);
}
.warp::before { color: var(--signal); animation: warp-a 5s steps(1, end) -2.9s infinite; }
.warp::after  { color: var(--ink);    animation: warp-b 5s steps(1, end) -2.9s infinite; }

@keyframes warp-word {
  0%    { translate: 0 0; scale: 1 1; }
  87%   { translate: -3px 0; }
  87.8% { translate: 0 0; }
  88.6% { translate: 2.5px -1px; scale: 1.03 1; }
  89.4% { translate: -1.5px 0; scale: 1 1; }
  90.2% { translate: 0 0; }
  91.6% { translate: 3px 0; }
  92.4% { translate: 0 1px; }
  93.2% { translate: -2px 0; }
  94%   { translate: 0 0; }
}
@keyframes warp-a {
  0%    { opacity: 0; translate: -5px 0; clip-path: inset(8% 0 62% 0); }
  87%   { opacity: .6; translate: -5px 0; clip-path: inset(8% 0 62% 0); }
  87.8% { opacity: 0; }
  88.6% { opacity: .5; translate: -8px 0; clip-path: inset(44% 0 30% 0); }
  89.4% { opacity: 0; }
  91.6% { opacity: .55; translate: -4px 0; clip-path: inset(66% 0 10% 0); }
  92.4% { opacity: 0; }
  93.2% { opacity: .4; translate: -6px 0; clip-path: inset(24% 0 52% 0); }
  94%   { opacity: 0; }
}
@keyframes warp-b {
  0%    { opacity: 0; translate: 5px 0; clip-path: inset(30% 0 46% 0); }
  87.4% { opacity: .7; translate: 5px 0; clip-path: inset(30% 0 46% 0); }
  88.2% { opacity: 0; }
  89%   { opacity: .8; translate: 8px 0; clip-path: inset(58% 0 18% 0); }
  89.8% { opacity: 0; }
  92%   { opacity: .65; translate: 4px 0; clip-path: inset(12% 0 64% 0); }
  92.8% { opacity: 0; }
  93.6% { opacity: .5; translate: 7px 0; clip-path: inset(48% 0 28% 0); }
  94.2% { opacity: 0; }
}

/* the three windows — no rail, no baton: the window itself is the progress */
.trip { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: clamp(10px, 1.4vw, 20px); width: 100%; }
/* The window holds its shape by a floor, not by an aspect ratio. A ratio looked right at 1180px
   and collapsed between there and the phone breakpoint: the caption wraps to two lines somewhere
   around 820px and the art had nowhere to go but underneath it. Flow layout and a min-height let
   the card grow those few pixels instead. */
.tcard {
  position: relative; overflow: hidden; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); min-height: clamp(150px, 15vw, 182px);
  display: flex; flex-direction: column; gap: 10px; padding: 32px 14px 12px;
  transition: border-color .5s ease, box-shadow .5s ease, background .5s ease;
}
.tcard::before {           /* the highlight that fades 1 → 2 → 3 → back */
  content: ""; position: absolute; inset: 0; opacity: 0; transition: opacity .55s ease;
  background: radial-gradient(118% 92% at 50% 108%, var(--signal-tint) 0%, transparent 64%);
}
.tcard.lit { border-color: color-mix(in srgb, var(--signal) 46%, var(--line)); box-shadow: 0 0 0 3px var(--signal-tint); }
.tcard.lit::before { opacity: 1; }
.tnum { position: absolute; left: 14px; top: 10px; font-family: var(--font-digits); font-weight: 800; font-size: 16px; color: var(--ink-3); transition: color .4s ease; }
.tcard.lit .tnum { color: var(--signal-text); }
.tlbl { font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; line-height: 1.4; color: var(--ink-3); transition: color .4s ease; }
.tcard.lit .tlbl { color: var(--ink); }
.cast { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; gap: 14px; }
.sq {
  width: var(--n, 38px); height: var(--n, 38px); border-radius: calc(var(--n, 38px) / 4);
  display: grid; place-items: center; border: 1.4px solid var(--ink); color: var(--ink); background: var(--surface);
}
.sq svg { width: 58%; height: 58%; stroke-width: 1.5; }
.sq.fill { background: var(--ink); color: #fff; border-color: var(--ink); }
.sq.sig { background: var(--signal); border-color: var(--signal); color: #fff; }
.sq.ghost { border-style: dashed; border-color: var(--line); color: var(--ink-3); background: transparent; }
.tcard.lit .sq.sig { animation: pulse 1.15s ease-in-out infinite; }
@keyframes pulse { 50% { box-shadow: 0 0 0 7px var(--signal-tint); } }
.tcount { font-family: var(--font-digits); font-weight: 800; font-size: clamp(19px, 2.3vw, 27px); letter-spacing: .02em; }
/* Window 2 has to show that the thing doing the allowing is a vault: a password and a code that
   is made here and not fetched. Window 3 has to show that a grant has two endings, so the word
   between the clock and the stop is load-bearing, not decoration. */
.vchip { display: grid; gap: 5px; padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-tile); background: var(--surface); }
.vchip > span { display: flex; align-items: center; gap: 7px; color: var(--ink-2); }
.vchip .msk { font-size: 13px; letter-spacing: .07em; color: var(--ink); }
.vchip .dot { font-size: 14px; letter-spacing: .04em; color: var(--ink); }
.tor { font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--ink-3); }
/* On a phone the window stops being a picture with a caption under it and becomes a row: art on
   the left, the two lines of text on the right. Absolute positioning cannot do that — window 2's
   vault chip is wide enough to run straight under the caption — so here both halves go back into
   normal flow and the card is simply a flex row that grows to whatever it holds. */
@media (max-width: 620px) {
  .trip { grid-template-columns: 1fr; gap: 10px; }
  .tcard { min-height: 84px; flex-direction: row; align-items: center; gap: 12px; padding: 12px 14px 12px 38px; }
  .tnum { top: 50%; translate: 0 -50%; left: 13px; }
  .cast { flex: 0 0 auto; justify-content: flex-start; gap: 9px; }
  .cast .sq { --n: 36px !important; }
  .vchip { padding: 5px 7px; gap: 3px; }
  .vchip > span { gap: 6px; }
  .vchip .msk { font-size: 11.5px; } .vchip .dot { font-size: 12px; }
  .tcount { font-size: 18px; }
  .tor { font-size: 10px; }
  .tlbl { flex: 1 1 auto; min-width: 0; text-align: right; }
}

/* A short phone gets the same first screen, only tighter. A 375x667 handset was pushing the
   Android button about 40px past the fold, which is the one button on this page that must not
   need a scroll to be believed. */
@media (max-width: 620px) and (max-height: 720px) {
  .hero { padding: 10px 0 14px; }
  .hero .wrap { gap: 13px; }
  .hero h1 { font-size: clamp(27px, 8vw, 36px); }
  .hero .tagline { font-size: 14.5px; line-height: 1.35; }
  .trip { gap: 8px; }
  .tcard { min-height: 68px; padding: 10px 12px 10px 34px; }
  .cast .sq { --n: 32px !important; }
  .tcount { font-size: 16px; }
  .cta { gap: 8px; }
  .btn { height: 44px; }
}

/* ── arrival and reveal ────────────────────────────────────────────────────────────────────
   Dropped by the same commit as the buttons (32969647, "drop the transition between pages"),
   which took the entrances with the page transition. The markup still carries `enter` with its
   `--d` delays on every line of the hero, `front.js` still toggles `rise.in`, and the result was
   a page whose content simply appeared — and, for the half second before the first WebGL frame,
   appeared with the flat mark showing under the orb, because the fade that used to cover that
   half second was part of what went. Restored from 40c48599 unchanged. */
@keyframes arrive { from { opacity: 0; translate: 0 12px; } to { opacity: 1; translate: 0 0; } }
.enter { animation: arrive .4s cubic-bezier(.2,.7,.3,1) both; animation-delay: var(--d, 0s); }

.rise { transition: translate .55s cubic-bezier(.2,.7,.3,1), opacity .4s linear; }
/* Only where script can take them back out again: without JS a hidden `.rise` never returns. */
@media (scripting: enabled) {
  .rise { translate: 0 18px; opacity: 0; }
  .rise.in { translate: 0 0; opacity: 1; }
}
.rise.d1 { transition-delay: .06s; } .rise.d2 { transition-delay: .12s; } .rise.d3 { transition-delay: .18s; }

@media (prefers-reduced-motion: reduce) {
  .rise, .enter { translate: 0 0; opacity: 1; transition: none; animation: none; }
}

/* The cookie line. It is a sentence in the footer rather than a bar over the page, because the
   one cookie this site sets is the workspace's own name — functional, no consent to collect — and
   a banner asking permission for something that needs none teaches people to click through the
   ones that matter. */
/* And the footer itself, which `32969647` took with the rest and nobody noticed because an
   unstyled footer still reads — it just sits on one cramped line with no rule above it and the
   dock over the top of it. Restored from 40c48599; the bottom padding is what keeps the dock off
   the last line. */
/* The bottom padding clears the dock, which is fixed 20px off the bottom and is always on by the
   time anyone reaches the footer — 44px left its last line underneath the pill. On a phone the
   dock is a full-width bar and the body already carries `--dock-h` for it, so this is the desktop
   clearance only. */
.foot { border-top: 1px solid var(--hairline); padding: 28px 0 112px; }
@media (max-width: 720px) { .foot { padding-bottom: 44px; } }
.foot .wrap { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px 16px; font-size: 13px; color: var(--ink-3); }
.foot a { color: var(--ink-2); text-decoration: none; font-weight: 600; }
.foot a:hover { color: var(--ink); }
.foot .crumb { flex-basis: 100%; max-width: 62ch; text-align: center; margin: 2px auto 0; color: var(--ink-3); font-size: 12px; line-height: 1.45; }

/* ── the buttons, and the dock that carries them ───────────────────────────────────────────
   Removed by accident on 4 October together with the page transition (32969647): the markup and
   `front.js` both kept referring to `.btn`, `.cta` and `.dock`, so every call to action on the
   front door rendered as a bare inline link and the dock never appeared at all. The media queries
   below still set `.btn { height }`, which is what an override without a base rule looks like.
   Restored from 40c48599 unchanged. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  height: 50px; padding: 0 24px; border-radius: 10px; border: 1px solid var(--line);
  background: var(--surface); color: var(--ink); font: inherit; font-size: 15px; font-weight: 600;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: transform .14s ease, border-color .14s ease, background .14s ease;
}
.btn:hover { border-color: var(--ink-3); }
.btn:active { transform: translateY(1px); }
.btn.pri { background: var(--ink); color: #fff; border-color: var(--ink); }
.btn.pri:hover { background: #000; }
.btn.sm { height: 40px; font-size: 13.5px; padding: 0 16px; border-radius: 8px; }
.btn .v { font-family: var(--font-digits); font-weight: 800; font-size: 12.5px; opacity: .62; letter-spacing: .02em; }

/* the hero pair: one primary line, one secondary line, both centred on their own row */
.cta { display: grid; justify-items: center; gap: 12px; text-align: center; }
.cta .sub { font-size: 12.5px; color: var(--ink-3); }

/* The dock is the only thing that follows the reader down the page. The header no longer does
   (see `.top`), so these two buttons are what stays within reach. */
.dock {
  position: fixed; z-index: 70; left: 50%; bottom: 20px; translate: -50% 0;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px 9px 18px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(14px) saturate(1.3);
  border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 18px 40px -24px rgba(0,0,0,.45);
  opacity: 0; pointer-events: none; translate: -50% 14px;
  transition: opacity .3s ease, translate .3s ease;
}
.dock.on { opacity: 1; pointer-events: auto; translate: -50% 0; }
.dock .what { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.dock .btn { height: 40px; font-size: 13.5px; padding: 0 16px; border-radius: var(--r-pill); }
@media (max-width: 720px) {
  .dock {
    left: 0; right: 0; bottom: 0; translate: 0 100%; width: 100%; border-radius: 0;
    border-left: 0; border-right: 0; border-bottom: 0; padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
    gap: 8px;
  }
  .dock.on { translate: 0 0; }
  .dock .what { display: none; }
  .dock .btn { flex: 1 1 0; min-width: 0; padding: 0 8px; }
}

/* And one notch tighter again for the oldest small screens — a 320x568 was still 30px over. */
@media (max-width: 620px) and (max-height: 600px) {
  .hero { padding: 8px 0 10px; }
  .hero .wrap { gap: 10px; }
  .hero h1 { font-size: clamp(23px, 7.2vw, 29px); }
  .hero .tagline { font-size: 13.5px; }
  .trip { gap: 7px; }
  .tcard { min-height: 58px; padding: 8px 10px 8px 30px; }
  .cast .sq { --n: 28px !important; }
  .tcount { font-size: 14px; }
  .btn { height: 42px; }
  .cta { gap: 7px; }
  .cta .sub { font-size: 11.5px; }
}

/* ── the stage: a phone that the page scrolls ─────────────────────────────────────────── */
.story { position: relative; }
.story .wrap { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 420px); gap: clamp(24px, 4vw, 72px); align-items: start; }
.stage { position: sticky; top: 76px; order: 2; justify-self: center; }
.beats { order: 1; display: grid; }

.beat { min-height: 62vh; display: grid; align-content: center; gap: 12px; max-width: 42ch; }
.beat:last-child { min-height: 54vh; }
.beat-body { display: grid; gap: 12px; }
.beat .n { font-family: var(--font-digits); font-weight: 800; font-size: 13px; color: var(--ink-3); letter-spacing: .1em; }
.beat h3 { margin: 0; font-size: clamp(25px, 2.8vw, 34px); line-height: 1.12; letter-spacing: -.024em; font-weight: 700; color: var(--ink); transition: color .45s ease; }
.beat p { margin: 0; font-size: 16px; line-height: 1.6; color: var(--ink-2); transition: color .45s ease; max-width: 38ch; }
/* Beats are only dimmed while something is able to undim them again, which is why the dimming
   hangs off a class front.js adds. Written the other way round — grey by default, dark when
   active — a reader with no scripting gets all five beats at --ink-3 on --bg, which is under
   3:1 against the page and is simply the section greyed out. */
.story.live .beat h3, .story.live .beat p { color: var(--ink-3); }
.story.live .beat.on h3 { color: var(--ink); }
.story.live .beat.on p { color: var(--ink-2); }
.beat .edge { height: 2px; width: 46px; border-radius: 2px; background: var(--line); transition: background .45s ease, width .45s ease; }
.beat.on .edge { background: var(--signal); width: 86px; }

/* the handset */
.phone {
  --w: clamp(258px, 25vw, 320px);
  width: var(--w); aspect-ratio: 390 / 844; position: relative;
  border-radius: calc(var(--w) * .13);
  background: linear-gradient(152deg, #D9D9D4 0%, #B9B9B3 16%, #F2F2EE 34%, #C9C9C3 58%, #A7A7A1 78%, #E4E4DF 100%);
  padding: calc(var(--w) * .0295);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.55),
    0 1px 1px rgba(0,0,0,.18),
    0 2px 5px rgba(0,0,0,.10),
    0 40px 60px -34px rgba(20,22,20,.55),
    0 90px 90px -70px rgba(20,22,20,.5);
}
.phone::before {           /* the rail seam around the frame */
  content: ""; position: absolute; inset: calc(var(--w) * .013); border-radius: calc(var(--w) * .118);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.22); pointer-events: none; z-index: 6;
}
.phone .btn-side, .phone .btn-vol {
  position: absolute; right: calc(var(--w) * -.009); width: calc(var(--w) * .0105); border-radius: 0 3px 3px 0;
  background: linear-gradient(90deg, #9B9B95, #CFCFC9); box-shadow: 0 1px 1px rgba(0,0,0,.2);
}
.phone .btn-side { top: 23%; height: 9.6%; }
.phone .btn-vol { right: auto; left: calc(var(--w) * -.009); top: 17%; height: 13.5%; border-radius: 3px 0 0 3px; background: linear-gradient(270deg, #9B9B95, #CFCFC9); }
.screen {
  position: relative; width: 100%; height: 100%; overflow: hidden;
  border-radius: calc(var(--w) * .103); background: var(--bg);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.35);
}
.screen::after {           /* one specular sweep, the thing that makes glass read as glass */
  content: ""; position: absolute; inset: 0; z-index: 9; pointer-events: none;
  background: linear-gradient(118deg, rgba(255,255,255,.42) 0 14%, rgba(255,255,255,.07) 26%, transparent 44%);
}
.pill {                    /* camera island */
  position: absolute; z-index: 8; top: calc(var(--w) * .028); left: 50%; translate: -50% 0;
  width: 26%; height: calc(var(--w) * .058); border-radius: 999px; background: #0B0C0B;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
}
.pill::after { content: ""; position: absolute; right: 16%; top: 50%; translate: 0 -50%; width: calc(var(--w) * .026); height: calc(var(--w) * .026); border-radius: 50%; background: radial-gradient(circle at 34% 30%, #2B3440 0 38%, #0A0D11 70%); box-shadow: inset 0 0 0 1px rgba(255,255,255,.12); }

/* the screens themselves */
.scr {
  position: absolute; inset: 0; display: flex; flex-direction: column; gap: 9px;
  padding: calc(var(--w) * .145) calc(var(--w) * .042) calc(var(--w) * .035);
  font-size: calc(var(--w) * .0365); line-height: 1.38;
  opacity: 0; translate: 0 10px; transition: opacity .45s ease, translate .45s ease; pointer-events: none;
}
.scr.on { opacity: 1; translate: 0 0; }
.sbar { display: flex; justify-content: space-between; align-items: baseline; padding: 0 2px 1px; }
.sbar b { font-size: 1.72em; font-weight: 700; letter-spacing: -.01em; }
.sbar .dot { font-size: 1.28em; color: var(--signal-text); }
.pc { background: var(--surface); border: 1px solid var(--line); border-radius: calc(var(--w) * .038); padding: calc(var(--w) * .042); display: grid; gap: calc(var(--w) * .03); }
.pc.ask { border-color: var(--signal); box-shadow: 0 0 0 3px var(--signal-tint); }
.who { display: flex; align-items: center; gap: .8em; }
.who .nm { font-size: 1.28em; font-weight: 700; letter-spacing: -.01em; }
.who .mt { font-size: .98em; color: var(--ink-3); }
.facts { display: grid; grid-template-columns: 5.4em 1fr; gap: .46em .8em; padding-top: calc(var(--w) * .03); border-top: 1px solid var(--hairline); }
.facts span { color: var(--ink-3); }
.facts b { font-weight: 600; }
.quote { border-left: 2px solid var(--line); padding: .3em .8em; color: var(--ink-2); font-style: italic; }
.quote small { display: block; font-style: normal; font-size: .8em; font-weight: 700; letter-spacing: var(--ls-label); color: var(--ink-3); margin-bottom: .15em; }
.sfoot { margin-top: auto; display: grid; justify-items: center; gap: calc(var(--w) * .042); padding-bottom: calc(var(--w) * .035); }
.touch {
  width: calc(var(--w) * .225); height: calc(var(--w) * .225); border-radius: 50%; background: var(--ink);
  display: grid; place-items: center; color: #fff; position: relative; box-shadow: 0 0 0 0 var(--signal-tint);
  transition: box-shadow .35s ease, transform .2s ease;
}
.touch svg { width: 52%; height: 52%; stroke-width: 1.5; }
.scr.on .touch.live { animation: halo 1.7s ease-out infinite; }
@keyframes halo { 0% { box-shadow: 0 0 0 0 var(--signal-tint); } 70% { box-shadow: 0 0 0 calc(var(--w) * .04) rgba(232,238,251,0); } 100% { box-shadow: 0 0 0 0 rgba(232,238,251,0); } }
.touch.held { transform: scale(.96); box-shadow: 0 0 0 calc(var(--w) * .036) var(--signal-tint); }
.ring { position: absolute; inset: calc(var(--w) * -.018); rotate: -90deg; }
.ring circle { fill: none; stroke-width: 2.5; }
.pbtn { width: 100%; height: calc(var(--w) * .13); border-radius: calc(var(--w) * .03); border: 1px solid var(--line); background: var(--surface); display: grid; place-items: center; font-weight: 600; font-size: 1.04em; }
.pbtn.p { background: var(--ink); color: #fff; border-color: var(--ink); }
.big { font-family: var(--font-digits); font-weight: 800; font-size: calc(var(--w) * .18); line-height: 1; letter-spacing: .01em; }
.evt { display: grid; gap: .55em; }
.evt > div { display: flex; gap: .7em; align-items: baseline; opacity: 0; translate: 0 6px; animation: pop .4s ease forwards; }
.evt .t { color: var(--ink-3); font-family: var(--font-digits); font-weight: 800; font-size: .92em; min-width: 3.4em; }
@keyframes pop { to { opacity: 1; translate: 0 0; } }
.verd { display: inline-flex; align-items: center; gap: .4em; font-size: .86em; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; }
.verd::before { content: ""; width: .52em; height: .52em; border-radius: 50%; background: currentColor; }
.verd.ok { color: var(--ink); } .verd.no { color: #9A4B2F; }
.rcpt { display: grid; gap: .7em; }
.rcpt > div { display: grid; gap: .22em; padding-bottom: .7em; border-bottom: 1px solid var(--hairline); }
.rcpt > div:last-child { border-bottom: 0; padding-bottom: 0; }

@media (max-width: 900px) {
  .story .wrap { grid-template-columns: 1fr; gap: 0; }
  /* The mask is opaque for its whole height and feathers out in the 16px below it, which is
     exactly the gap before the caption slot starts. As a gradient inside the stage it faded
     while still over the handset, so the last caption — which has to leave the slot before the
     handset leaves the screen — showed through it at half strength on the way out. */
  .stage { order: 1; position: relative; z-index: 5; padding: 8px 0 14px; margin-inline: calc(var(--gutter) * -1); background: var(--bg); width: 100vw; display: grid; justify-items: center; }
  /* Pinned only while something is choosing what to pin. Left sticky without that, the
     handset sits over captions that are still scrolling freely past it — which is the
     defect this whole block exists to answer, reintroduced for anyone without scripting. */
  .story.live .stage { position: sticky; top: 58px; }
  .stage::after { content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 16px; background: linear-gradient(180deg, var(--bg), transparent); }
  .phone { --w: min(214px, 46vw); }
  .beats { order: 2; }
  .beat { min-height: 56vh; max-width: none; align-content: start; }
  /* The last one gets extra slack below it so its caption is still pinned when the handset
     finally scrolls away, instead of sliding up behind it for want of anything underneath. */
  .beat:last-child { min-height: calc(56vh + 200px); }

  /* On a phone the handset is pinned across the top half, so words that merely scroll past end up
     behind it: measured on a 400 × 860 screen, the beat the handset was showing was the one you
     could not read at two thirds of the scroll positions. Shrinking the handset or moving the
     active band does not fix it — text that scrolls freely under a fixed object will always pass
     behind it — so the caption is pinned the way the handset is, and the scroll swaps both at
     once. The offset is the stage: 58px of sticky top, the handset's own height (its 390 × 844
     screen, so 2.165 × its width), its 22px of padding, and a gap.

     The pinning hangs off the same class as the dimming: one caption in a slot only works while
     something is choosing which one, so with no scripting the captions stay in the flow and the
     section reads as a handset above a column of five — rather than five transparent captions
     stacked on top of each other in one slot. */
  .story.live .beat-body {
    position: sticky; top: calc(96px + min(214px, 46vw) * 2.165);
    opacity: 0; transition: opacity .3s ease;
  }
  .story.live .beat.on .beat-body { opacity: 1; }
}

/* ── the spine bands, drawn as the product draws them ─────────────────────────────────── */
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); gap: clamp(22px, 3.4vw, 54px); align-items: center; }
@media (max-width: 900px) { .split { grid-template-columns: 1fr; } }

/* blind path: a switch that changes what the server can read */
.seg { display: inline-flex; background: var(--control); border-radius: 9px; padding: 3px; gap: 3px; }
.seg button { height: 32px; padding: 0 15px; border-radius: 7px; border: 0; background: transparent; font: inherit; font-size: 13px; font-weight: 600; color: var(--ink-2); cursor: pointer; }
.seg button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(0,0,0,.12); }
.route { padding: 20px; display: grid; gap: 14px; }
.route .wire { position: relative; display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); align-items: center; gap: 0; padding: 18px 0 6px; }
.route .node { display: grid; justify-items: center; gap: 7px; position: relative; z-index: 2; }
.route .node .lbl { text-align: center; }
.route .line { position: absolute; left: 12.5%; right: 12.5%; top: 18px; height: 1.5px; background: var(--line); z-index: 0; }
.route .pk { position: absolute; top: 18px; width: 22px; height: 1.5px; border-radius: 2px; background: var(--ink); translate: 0 -50%; z-index: 1; animation: flow 3.4s linear infinite; }
@keyframes flow { 0% { left: 12.5%; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { left: calc(87.5% - 22px); opacity: 0; } }
.sees { display: grid; gap: 6px; padding: 12px 14px; border-radius: var(--r-tile); background: var(--bg); border: 1px solid var(--hairline); font-size: 13px; }
.sees code { font-family: var(--font-digits); font-weight: 800; font-size: 13.5px; letter-spacing: .06em; }
.sees .k { color: var(--ink-3); font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; }

/* the receipt band */
.ticket { padding: 22px 24px; display: grid; gap: 16px; }
.ticket .line2 { display: grid; gap: 4px; padding: 13px 0; border-top: 1px solid var(--hairline); }
.ticket .line2:first-of-type { border-top: 0; padding-top: 0; }
.ticket .line2 b { font-size: 15px; font-weight: 600; }
.ticket .line2 span { font-size: 13px; color: var(--ink-3); }

/* small picture cards used by the remaining bands */
.mini { padding: 18px; display: grid; gap: 12px; align-content: start; }
.mini h4 { margin: 0; font-size: 15.5px; font-weight: 700; letter-spacing: -.01em; }
.mini p { margin: 0; font-size: 13.5px; color: var(--ink-2); line-height: 1.5; }
.art { border-radius: var(--r-tile); background: var(--bg); border: 1px solid var(--hairline); padding: 14px; display: grid; gap: 9px; }
.fieldrow { display: flex; align-items: center; gap: 9px; height: 34px; padding: 0 10px; background: var(--surface); border: 1px solid var(--line); border-radius: 7px; font-size: 12.5px; color: var(--ink-3); }
.fieldrow .fill { margin-left: auto; font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); color: var(--signal-text); }
.term { font-family: var(--font-digits); font-weight: 800; font-size: 12.5px; letter-spacing: .04em; color: var(--ink-2); display: grid; gap: 5px; }
.term b { color: var(--ink); }
.swap { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 9px; font-size: 12.5px; }
.swap .box { border: 1px solid var(--line); border-radius: 7px; background: var(--surface); padding: 8px 10px; display: grid; gap: 3px; }
.swap .box .k { font-size: 10.5px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--ink-3); }
.swap .box.keep { border-color: color-mix(in srgb, var(--signal) 40%, var(--line)); background: var(--signal-tint); }

/* the band that makes the other bands believable */
.open { display: grid; }
.open > div { display: grid; grid-template-columns: 148px minmax(0,1fr); gap: 10px 24px; padding: 18px 0; border-top: 1px solid var(--hairline); align-items: start; }
.open > div:first-child { border-top: 0; }
.open h4 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -.01em; }
.open p { margin: 0; font-size: 14.5px; line-height: 1.55; color: var(--ink-2); max-width: 62ch; }
.open .tag { font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; color: var(--ink-3); padding-top: 3px; }
@media (max-width: 720px) { .open > div { grid-template-columns: 1fr; gap: 6px; } }
.ja4 { margin-top: 12px; border: 1px solid var(--line); border-radius: var(--r-tile); overflow: hidden; background: var(--surface); font-size: 12.5px; }
.ja4 > div { display: grid; grid-template-columns: 112px minmax(0,1fr) 54px; gap: 12px; align-items: center; padding: 9px 13px; border-top: 1px solid var(--hairline); }
.ja4 > div:first-child { border-top: 0; background: var(--bg); color: var(--ink-3); font-size: 11px; font-weight: 700; letter-spacing: var(--ls-label); text-transform: uppercase; }
.ja4 code { font-family: var(--font-digits); font-weight: 800; font-size: 12px; letter-spacing: .03em; overflow-wrap: anywhere; }
.ja4 .me { color: #9A4B2F; }
.ja4 b { font-weight: 600; }

.close { text-align: center; display: grid; justify-items: center; gap: 18px; padding: clamp(50px, 7vw, 96px) 0 clamp(20px, 3vw, 36px); }
.close h2 { margin: 0; font-size: clamp(28px, 4vw, 46px); line-height: 1.05; letter-spacing: -.03em; max-width: 18ch; }

/* A button with nothing behind it: still readable, no longer pretending to lead somewhere. */
.btn.off { color: var(--ink-3); border-style: dashed; cursor: default; }
.btn.off:hover { border-color: var(--line); }
