/* Meet Wabi — the Wabi 2.0 storyboard told as a page.
 *
 * The STRUCTURE is sota.bot/import: a full-screen hero on one idea, then a
 * short run of beats, then one closing CTA. The ELEMENT SCALE is butter.video
 * and trysoar.com: nothing on this page is set large except the hero. A beat is
 * a numbered hairline row, a narrow copy column and one small visual — so the
 * page reads as a sequence rather than as seven posters in a stack.
 *
 * The headlines are type-motion's: real DOM text, one inline-block per
 * character, so a swap REFLOWS the sentence. Everything under "the headline"
 * below is ported from type-motion/index.html and must stay in step with
 * js/engine.js — re-port rather than nudge.
 */

/* ── faces ─────────────────────────────────────────────────────────────── */
@font-face { font-family:'Selecta'; src:url('../fonts/Selecta-Regular.otf') format('opentype'); font-weight:400; font-display:swap; }
@font-face { font-family:'Selecta'; src:url('../fonts/Selecta-Medium.otf') format('opentype'); font-weight:500; font-display:swap; }

/* ── tokens ────────────────────────────────────────────────────────────── */
:root {
  /* Wabi's own light ground: #f0f0f0 paper, #191919 ink. Both come off the
     storyboard frames, not from taste. */
  --paper:#f0f0f0;
  --card:#fafafa;
  --ink:#191919;
  --ink-2:#5b5b5b;
  --ink-3:#8d8d8d;
  --hair:rgba(25,25,25,.12);
  --hair-soft:rgba(25,25,25,.07);
  --dark:#191919;
  --dark-2:#0f0f0f;
  --sheet:0 1px 2px rgba(25,25,25,.04), 0 12px 28px -12px rgba(25,25,25,.14);

  --face:'Selecta', system-ui, -apple-system, sans-serif;
  /* the facepile's white ring, as a fraction of the avatar it surrounds. ⚠ ONE number
     for both places it appears — the headline's 0.84em face and the subhead's 0.988em
     one — so "10% thinner" is this line and nothing else. Was .08. */
  --pile-ring:.072;

  /* one stack, declared once — two of these ranges are written by JS, and left
     to source order the dock ends up under the beats it is meant to drive */
  --z-page:0;
  --z-fly:20;
  --z-nav:40;
  --z-dock:60;

  /* the headline effect's own tokens (type-motion) */
  --tile:.78em;
  --tw:.30s;
  --tp:.46s;
  --ease:cubic-bezier(.22,.8,.28,1);
  --pop:cubic-bezier(.16,1.42,.4,1);
  /* the creator-program site's own reveal easing, verbatim — see .copy-in below */
  --ease-golden:cubic-bezier(.25, 1, .5, 1);
  --sh:.8;
  --sh-y:.25em;
  --sh-blur:.28em;
  --sh-a:.22;
  --tdy:0px;

  --gut:clamp(20px, 4vw, 44px);
  --wide:1180px;
}

*, *::before, *::after { box-sizing:border-box; }

html { -webkit-text-size-adjust:100%; }

body {
  margin:0;
  /* the frozen headlines are free to grow wider than their column; a page that
     scrolls sideways because of it is the one thing that must not happen */
  overflow-x:hidden;
  background:var(--paper);
  color:var(--ink);
  font-family:var(--face);
  font-weight:400;
  font-size:15px;
  line-height:1.5;
  letter-spacing:-.011em;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

::selection { background:rgba(25,25,25,.14); }

a { color:inherit; text-decoration:none; }

/* ⚠ An <img width= height=> maps both attributes to presentational CSS. A rule
   that sets only `width` therefore leaves the ATTRIBUTE height in force — the
   phone frame rendered 216x1562 and every orb was a 46x360 black streak, with
   `aspect-ratio` powerless because the height was no longer auto. Rules below
   that want a fixed height are more specific and still win. */
img { height:auto; }

.wrap { width:100%; max-width:var(--wide); margin-inline:auto; padding-inline:var(--gut); }

/* ── the small parts ───────────────────────────────────────────────────── */

.lead { font-size:16px; line-height:1.5; color:var(--ink-2); margin:0; max-width:44ch; }
/* The subhead under a headline is the second thing anybody reads, so it is set
   like copy meant to be read rather than like a caption. */
.body { font-size:clamp(17px, 1.35vw, 22px); line-height:1.5; color:var(--ink-2); margin:0; max-width:40ch; }
.tiny { font-size:16px; line-height:1.5; color:var(--ink-3); letter-spacing:-.008em; }

/* the production Wabi glass button, ported from era/css/wabi.css (itself lifted
   from sotabot/website-v1) — re-port it, never approximate the values */
.btn {
  --btn-bg:rgba(25,25,25,.92);
  display:inline-flex; align-items:center; gap:8px;
  height:38px; padding:0 17px;
  border:0; border-radius:999px;
  background:var(--btn-bg);
  color:#f0f0f0;
  font-family:var(--face); font-size:13.5px; font-weight:500; letter-spacing:-.01em;
  cursor:pointer;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14), 0 1px 2px rgba(25,25,25,.14), 0 8px 20px -10px rgba(25,25,25,.5);
  transition:transform .18s var(--ease), box-shadow .18s var(--ease), background .18s var(--ease);
}
.btn:hover { transform:translateY(-1px); box-shadow:inset 0 1px 0 rgba(255,255,255,.18), 0 2px 4px rgba(25,25,25,.16), 0 14px 26px -12px rgba(25,25,25,.55); }
.btn:active { transform:translateY(0); }
.btn.ghost {
  background:transparent; color:var(--ink);
  box-shadow:inset 0 0 0 1px var(--hair);
}
.btn.ghost:hover { background:rgba(25,25,25,.04); box-shadow:inset 0 0 0 1px rgba(25,25,25,.2); }
.btn .arr { font-size:15px; line-height:0; transform:translateY(-.5px); }

/* a word with a rule under it — everything that is not a primary is this */
.word {
  display:inline-flex; align-items:center; gap:6px;
  font-size:15px; font-weight:500; color:var(--ink);
  padding-bottom:2px; border-bottom:1px solid var(--hair);
  cursor:pointer; background:none; border-top:0; border-inline:0;
  font-family:var(--face); letter-spacing:-.01em;
  transition:border-color .18s var(--ease), color .18s var(--ease);
}
.word:hover { border-color:var(--ink); }

/* ── nav ───────────────────────────────────────────────────────────────── */
.nav {
  position:fixed; inset:0 0 auto; z-index:var(--z-nav);
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  height:58px; padding-inline:var(--gut);
  background:linear-gradient(to bottom, var(--paper) 42%, rgba(240,240,240,0));
  pointer-events:none;
}
.nav > * { pointer-events:auto; }
.mark { display:flex; align-items:center; gap:9px; font-size:14.5px; font-weight:500; letter-spacing:-.02em; }

.nav .right { display:flex; align-items:center; gap:16px; }
.nav .word { font-size:12.5px; }

/* ── hero ──────────────────────────────────────────────────────────────── */
/* The hero owns the first screen outright, so whatever follows starts below the
   fold and the page opens on one idea. Straight out of /import. */
/* the dock owns the bottom centre, so the hero stops just short of the fold
   rather than parking a cue underneath it */
/* The hero stands on the same ground as the showcase chapter under it — #f9f9f9,
   which is the Figma frames' own fill and therefore what is baked into the edges
   of every plate. The page's own paper picks up again after Discover, and that
   join is hidden by the dark band. */
/* ⚠ THE WHITE IS THE PAGE'S, NOT THE HERO'S. It used to be a full-bleed layer
   behind the hero, which put a hard edge across the page wherever that layer
   stopped — white above, paper below, in one pixel. There is no way to soften an
   edge like that from the element that owns it, because the element has to end
   somewhere. So the surface itself carries one long gradient: solid white down
   past the fold, then a 85vh fade into the paper. Nothing has an edge, and the
   hero needs no layer of its own. */
.hero { position:relative; }.hero {
  min-height:100svh;
  display:flex; flex-direction:column; align-items:center;
  /* `safe center` and not plain `center`: if the content is ever taller than the
     box, plain centring splits the overflow BOTH WAYS and the top goes off the
     screen where it cannot be scrolled to. `safe` falls back to start instead.
     That is exactly how era's `.hero { height:100vh }` collision presented. */
  justify-content:safe center;
  text-align:center;
  /* ⚠ block axis ONLY — a `padding` shorthand here resets .wrap's own
     padding-inline to 0 and the hero loses its side gutter entirely */
  padding-block:96px 84px;
}
.hero .lead { margin:calc(43px * var(--gap-scale, 1)) auto 0; max-width:40ch; text-align:center; }
/* 30% more air above the button than under the subhead's own gap, always — it
   is the one thing in the hero you are meant to reach for. */
.hero .cta { display:flex; align-items:center; justify-content:center; gap:18px; margin-top:calc(56px * var(--gap-scale, 1)); flex-wrap:wrap; }
html[data-cta="off"] .hero .cta { display:none; }
/* The two middle sections are off until they are wanted: the page is the hero,
   the wall and the end, and those three carry it on their own. */
html[data-body="off"] #does, html[data-body="off"] #remix { display:none; }

/* the tile drift under the hero: small, so it reads as "a lot of apps" rather
   than as a gallery of four */
.drift { width:100%; margin-top:44px; overflow:hidden; mask-image:linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); -webkit-mask-image:linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }
.drift-row { display:flex; gap:10px; width:max-content; animation:slide 46s linear infinite; }
.drift-row.b { animation-duration:64s; animation-direction:reverse; margin-top:10px; }
@keyframes slide { to { transform:translateX(-50%); } }
.drift img {
  width:56px; height:56px; border-radius:13px; display:block; object-fit:cover;
  box-shadow:0 1px 2px rgba(25,25,25,.08), 0 6px 14px -8px rgba(25,25,25,.3);
}


/* ── a beat ────────────────────────────────────────────────────────────── */
/* soar's shape: a hairline, an index, a narrow copy column and one small
   visual. Alternating sides, which is butter's. */
.beat { padding-block:clamp(56px, 8vw, 104px); border-top:1px solid var(--hair-soft);
        scroll-margin-top:96px; }
/* the two beats that are not two-column rows */
.wrap.mid { max-width:760px; }
.beat.centred { text-align:center; }
.beat.centred .fxwrap { display:flex; justify-content:center; }
.beat.centred .body { margin:20px auto 0; }
.beat.centred .stack { margin-inline:auto; display:table; }
.beat.bleed { padding-inline:0; max-width:none; }
.beat.bleed .wrap { margin-inline:auto; }
.beat:first-of-type { border-top:0; }
.beat .grid {
  display:grid; gap:clamp(28px, 4vw, 56px);
  grid-template-columns:minmax(0,1fr);
  align-items:center;
}
.beat .copy { max-width:460px; }
.beat .copy .body { margin-top:18px; }
.beat .copy .foot-row { margin-top:22px; display:flex; align-items:center; gap:16px; flex-wrap:wrap; }
.beat .vis { min-width:0; }

@media (min-width:860px) {
  .beat .grid { grid-template-columns:minmax(0,5fr) minmax(0,7fr); }
  .beat.flip .copy { order:2; }
  .beat.flip .vis { order:1; }
}

/* ── the visual cards ──────────────────────────────────────────────────── */
.card {
  position:relative;
  border-radius:20px;
  background:radial-gradient(120% 130% at 50% 0%, #fdfdfc 0%, var(--card) 62%, #f1f0ed 100%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.9), inset 0 0 0 1px var(--hair-soft), var(--sheet);
  padding:22px;
  overflow:hidden;
}
.card.dark {
  background:radial-gradient(130% 130% at 50% 0%, #232323 0%, var(--dark) 58%, var(--dark-2) 100%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.07), var(--sheet);
  color:#f0f0f0;
}
.card.dark .tiny { color:rgba(240,240,240,.5); }

/* the chat input, off the Remix frame — Selecta, one hairline, a live cursor */
.field {
  display:flex; align-items:center; gap:9px;
  height:50px; padding:0 18px;
  border-radius:999px;
  background:rgba(255,255,255,.72);
  box-shadow:inset 0 0 0 1px rgba(25,25,25,.09), 0 1px 1px rgba(25,25,25,.03);
  font-size:16px; color:var(--ink);
}
.field .car { width:1.5px; height:17px; background:var(--ink); animation:blink 1.1s steps(1) infinite; flex:none; }
@keyframes blink { 50% { opacity:0; } }
.field .ph { color:var(--ink-3); }
.field .typed { white-space:nowrap; overflow:hidden; }

/* a message bubble, off the same frame */
.bubble {
  align-self:flex-end;
  max-width:78%;
  padding:13px 18px;
  border-radius:20px;
  background:rgba(9,9,9,.97);
  color:#f2f2f2;
  font-size:15.5px; line-height:1.36;
}
.bubble.you { align-self:flex-start; background:#fff; color:var(--ink); box-shadow:inset 0 0 0 1px var(--hair-soft); }
.thread { display:flex; flex-direction:column; gap:9px; }

/* the phone: the storyboard's own frame, with a real app screen inside it */
.phone { position:relative; width:var(--pw,226px); margin-inline:auto; }
.phone > img.frame { width:100%; display:block; position:relative; z-index:2; pointer-events:none; }
.phone .screen {
  position:absolute; z-index:1; overflow:hidden;
  /* ⚠ The screen is deliberately a shade LARGER than the glass it shows through
     and its corners a shade TIGHTER. Measured exactly onto the hole (inset
     1.677/4.206/1.762/4.147, radius 16.06%/7.42%) the two roundings never agree
     to the pixel, and the mismatch shows as pale wedges in the corners. It sits
     BEHIND the frame, so overfilling costs nothing: the opaque bezel trims it. */
  inset:0.9% 3.3% 1.0% 3.25%;
  border-radius:13.5% / 6.4%;
  background:#000;
}
.phone .screen > img { width:100%; height:100%; display:block; object-fit:cover; object-position:top center; }

/* the tile grid — Discover. Small tiles on purpose: the point is how many. */
 /* A tile is a BOX with an image inside it, never a sized image: aspect-ratio
    on a replaced grid item is the shakiest way to get a square, because the
    row's auto sizing takes the image's own intrinsic block contribution. */
.t {
  position:relative; aspect-ratio:1; border-radius:14px; overflow:hidden; display:block; flex:none;
  box-shadow:0 1px 2px rgba(25,25,25,.07), 0 5px 12px -7px rgba(25,25,25,.28);
}
.t img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; }

.tiles { display:grid; grid-template-columns:repeat(auto-fill, minmax(62px,1fr)); gap:8px; align-content:start; }
.tiles .t { transition:transform .22s var(--ease); }
.tiles .t:hover { transform:translateY(-3px) scale(1.04); z-index:1; }

/* what the sentence turned into — the payoff row at the foot of a card */
.made { display:flex; align-items:center; gap:12px; margin-top:16px; position:relative; }
.made .t { width:50px; border-radius:13px; }
.made b { display:block; font-size:17px; font-weight:500; letter-spacing:-.015em; }
.made .tiny { display:block; margin-top:2px; }
.made .chip { pointer-events:none; }

/* the rotating word column, off the Discover frame's stacked list */
.roll { display:inline-grid; vertical-align:bottom; overflow:hidden; height:1.06em; }
.roll span { grid-area:1/1; opacity:0; transform:translateY(.5em); transition:opacity .4s var(--ease), transform .5s var(--ease); }
.roll span.on { opacity:1; transform:none; }

/* faces on an invite pill */
.pill {
  display:inline-flex; align-items:center; gap:11px;
  height:46px; padding:0 7px 0 18px; border-radius:999px;
  background:rgba(255,255,255,.8);
  box-shadow:inset 0 0 0 1px rgba(25,25,25,.08), 0 1px 2px rgba(25,25,25,.04);
  font-size:15px; color:var(--ink-2);
}
.faces { display:flex; }
.faces img, .faces i {
  width:32px; height:32px; border-radius:50%; display:block; margin-left:-9px;
  box-shadow:0 0 0 2px var(--card);
  background:var(--paper);
  font-style:normal; font-size:14px; font-weight:500; color:var(--ink-2);
  display:grid; place-items:center;
}
.faces > :first-child { margin-left:0; }

/* private ⇄ public, a real switch */
.swap { display:flex; align-items:center; gap:12px; font-size:15.5px; }
.sw { position:relative; width:44px; height:26px; border-radius:999px; border:0; cursor:pointer; padding:0; background:rgba(25,25,25,.16); transition:background .22s var(--ease); flex:none; }
.sw::after { content:''; position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#fff; box-shadow:0 1px 2px rgba(0,0,0,.2); transition:transform .22s var(--pop); }
.sw[aria-pressed="true"] { background:#2fbf5b; }
.sw[aria-pressed="true"]::after { transform:translateX(18px); }
.swap .lab { color:var(--ink-3); transition:color .2s var(--ease); }
.swap .lab.on { color:var(--ink); }

/* the save beat's own little sequence */
.save-row { display:flex; align-items:center; gap:10px; }
.chip {
  display:inline-flex; align-items:center; gap:7px;
  height:38px; padding:0 16px; border-radius:999px;
  background:#fff; box-shadow:inset 0 0 0 1px var(--hair-soft), 0 1px 2px rgba(25,25,25,.05);
  font-size:14.5px; font-weight:500; color:var(--ink);
  border:0; font-family:var(--face); cursor:pointer;
  transition:transform .2s var(--pop), background .2s var(--ease), color .2s var(--ease);
}
.chip[data-saved="true"] { background:var(--ink); color:#f0f0f0; box-shadow:none; }
.chip .tick { display:inline-block; width:13px; }

/* platforms: two words and a count */
.plat { display:flex; flex-wrap:wrap; gap:14px 40px; align-items:baseline; }
.beat.centred .plat { justify-content:center; margin-top:34px; }
.plat .k { font-size:14px; letter-spacing:.04em; color:var(--ink-3); display:block; margin-bottom:6px; }
.plat .v { font-size:21px; font-weight:500; letter-spacing:-.02em; }

/* the Wabi app icon — 1:1 off the storyboard's own: a 128 box at radius 29.913
   (23.4%), white → #eaeaea from 9.167%, a 1.708px inset white ring, five orbs
   of 31.208 (24.4% of the box) */
.icon {
  position:relative; width:var(--i, 96px); aspect-ratio:1;
  border-radius:23.4%;
  background:linear-gradient(to bottom, #fff 9.167%, #eaeaea 100%);
  box-shadow:inset 0 0 calc(var(--i, 96px) * .0067) calc(var(--i, 96px) * .0133) rgba(255,255,255,.6),
             0 1px 2px rgba(25,25,25,.08), 0 10px 22px -10px rgba(25,25,25,.32);
  overflow:hidden;
  flex:none;
}
.pit { position:absolute; inset:0; }
.pit img { position:absolute; filter:drop-shadow(0 2px 4px rgba(20,18,14,.28)); }
/* the same object at nav size, with no card under it */
.mark .icon { --i:20px; box-shadow:inset 0 0 0 .5px rgba(25,25,25,.06), 0 1px 2px rgba(25,25,25,.1); }

.iconrow { display:flex; align-items:center; gap:16px; justify-content:center; padding:14px 0 6px; }
.iconrow .cap { font-size:12.5px; color:var(--ink-2); }

/* ── closing ───────────────────────────────────────────────────────────── */
/* ⚠ 100px more air at each end than the section had. And written as two calc()s
   round the clamps rather than by editing the clamp's own numbers — a scripted
   substitution split inside `clamp(84px, 12vw, 150px)` on its spaces and produced
   `calc(clamp(84px, + 100px)`, which is invalid, so the whole declaration was
   dropped and the section lost its padding entirely. */
.closing {
  padding-block:calc(clamp(84px, 12vw, 150px) + 100px) calc(clamp(64px, 9vw, 110px) + 100px);
  text-align:center;
  border-top:1px solid var(--hair-soft);
}
.closing .lead { margin:22px auto 0; text-align:center; max-width:34ch;
                 font-size:clamp(19px, 1.8vw, 30px); }
.closing .cta { margin-top:26px; display:flex; justify-content:center; gap:16px; align-items:center; flex-wrap:wrap; }
/* the download block, laid open on the page rather than in a popup */
.closing .ado { max-width:620px; margin-inline:auto; }

footer.foot {
  border-top:1px solid var(--hair-soft);
  padding-block:22px 30px;
  display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap;
}
footer.foot .tiny a { border-bottom:1px solid var(--hair); }

/* ══ the headline ═══════════════════════════════════════════════════════ */
/* Ported from type-motion/index.html. The engine writes --w / --wOn / --k /
   --ang / --r / --rx / --rise per character; nothing here may be nudged
   independently of js/engine.js. */

.fxwrap { position:relative; }
/* The frozen line is `width:max-content`, so a centred context has to centre it
   as a flex ITEM — `text-align:center` cannot centre a max-content block.
   ⚠ `align-self:stretch` is load-bearing, not tidiness: .hero is a flex column
   with `align-items:center`, so without it the wrapper SHRINK-WRAPS to the line
   it contains — and fitBox() measuring that is circular, which fitted the
   headline to whatever it already was. */
.hero .fxwrap, .closing .fxwrap, .band .fxwrap {
  display:flex; justify-content:center; align-self:stretch; width:100%;
}
.fxwrap .fly {
  position:absolute; inset:0; z-index:var(--z-fly);
  pointer-events:none; overflow:visible;
}

.line {
  margin:0;
  font-family:var(--face);
  font-weight:400;
  letter-spacing:0;              /* tracking is added to the BOX, see measure() */
  color:var(--ink);
  position:relative; z-index:1;
}
/* Bold by SIZE, not by weight. Selecta ships Bold here, and the house rule is
   Medium and Regular only — the storyboard's own big type is Regular too (the
   `re m i x` title and `Meet Wabi.` are both Selecta Regular), so the headlines
   are Regular and simply large. Everything else on the page stays small, which
   is where the contrast comes from. */
h1.line { font-size:clamp(40px, 6.6vw, 88px); line-height:.99; letter-spacing:-.035em; }
h2.line { font-size:clamp(32px, 4.4vw, 58px); line-height:1.02; letter-spacing:-.032em; }
/* ⚠ 30% up on the 38/5.4vw/74 it was — every term, so the whole clamp scales rather
   than only its ceiling: a clamp whose middle term is untouched stops growing at the
   width where the vw term was already binding. */
/* ⚠ 20% DOWN ON 49/7vw/96 — every term, so the whole clamp scales rather than only
   its ceiling: leaving the middle term alone keeps the old size at every width where
   the vw term was already binding, which is most of them. */
.closing h2.line { font-size:clamp(39px, 5.6vw, 77px); letter-spacing:-.035em; }

/* the fit measures a line several times in one task, and a width TRANSITION is
   still sitting on its start value at that point — so every read would report
   the pre-freeze line */
.line.nofx .ch, .line.nofx .g, .line.nofx .o { transition:none !important; }

/* one frozen line. `nowrap` is what makes the break points permanent; the line
   is free to grow WIDER than its column, because a word jumping to the next row
   under the pointer is far louder than a line that overhangs. */
.ln { display:block; white-space:nowrap; text-align:inherit; }
.w { display:inline-block; white-space:nowrap; }

.ch {
  display:inline-block; position:relative; width:var(--w); text-align:center;
  /* the vanishing point belongs to the CHARACTER, not the line: one on the line
     is measured from the middle of the block, so a tile at the end of a row
     would tilt about a point half a headline away and shear instead of turn */
  perspective:520px; transform-style:preserve-3d;
  transition:width var(--tw) var(--ease);
}

.g {
  display:inline-block; font-style:normal; transform-origin:50% 62%;
  transition:opacity .15s linear, transform .24s var(--ease);
}
.ch.on .g { opacity:0; transform:scale(.34); }

.o {
  position:absolute; left:50%; top:50%;
  width:var(--tile); height:var(--tile);
  margin-left:calc(var(--tile) / -2); margin-top:calc(var(--tile) / -2 + var(--tdy));
  background-size:contain; background-position:center; background-repeat:no-repeat;
  font-style:normal; display:flex; align-items:center; justify-content:center;
  opacity:0; will-change:transform, opacity, scale;
  /* the pop rides `transform`; the swell rides the INDEPENDENT `scale`
     property, so a per-frame growth is not dragged through the pop's own
     460ms transition (they compose — scale applies before transform) */
  scale:var(--k);
  /* and the lean rides `rotate`, axis-angle, so the axis can be perpendicular
     to the pointer and the tile TURNS rather than slides */
  rotate:var(--ax, 1) var(--ay, 0) 0 var(--ang, 0deg);
  transform:translateY(var(--rise)) rotateX(var(--rx)) rotate(var(--r0)) scale(.14);
  transition:transform var(--tp) var(--pop), opacity .13s linear, scale .1s linear,
             rotate .18s cubic-bezier(.3,.8,.4,1);
  /* --adj is the per-set look, written on the tile when it is filled. It comes
     FIRST so the shadow is cast by the adjusted artwork rather than the
     adjustment being applied to the shadow. */
  filter:var(--adj, brightness(1))
         drop-shadow(0 calc(var(--sh) * var(--sh-y)) calc(var(--sh) * var(--sh-blur))
                     rgba(20,18,14,calc(var(--sh) * var(--sh-a))));
}
.ch.on .o { opacity:1; transform:translateY(0) rotateX(0deg) rotate(var(--r)) scale(1); }

/* the overshoot belongs to the ARRIVAL only — run it backwards on the way out
   and the tile bulges on its way to nothing, which reads as a glitch */
.ch:not(.on) .o { transition-timing-function:cubic-bezier(.45,0,.75,.35), linear, linear; }

/* a tile mid-swell owns its own transform outright — the CSS transition would
   be a second curve fighting the one keyframed animation */
.o.fly { transition:none; scale:1; }

.o.sym, .fx.sym {
  font-family:var(--face); font-weight:500; font-size:.92em; line-height:1;
  /* the glyph is a window onto the ramp: the gradient is the background and the
     letterform is the clip, so `color` has to be transparent */
  background-size:100% 100%; background-position:center; background-repeat:no-repeat;
  -webkit-background-clip:text; background-clip:text;
}
/* background-clip:text clips to the element's BOX, so a mark wider than the
   square tile simply is not painted — a mark's box is its own measured advance */
.o.sym {
  width:var(--symw, 1em); height:1.34em;
  margin-left:calc(var(--symw, 1em) / -2);
  margin-top:calc(-0.67em + var(--tdy));
}

/* things thrown off a letter (burst) */
.fx {
  position:absolute; display:flex; align-items:center; justify-content:center;
  background-size:contain; background-position:center; background-repeat:no-repeat;
  font-style:normal; line-height:1; will-change:transform, opacity;
  filter:var(--adj, brightness(1))
         drop-shadow(0 var(--sh-y) var(--sh-blur) rgba(20,18,14,var(--sh-a)));
}

/* ── the toolbars ─────────────────────────────────────────────────────────
   One bar per axis, stacked in the corner. Four groups on a single rail read as
   one long control whose sections you have to find; four bars read as four
   questions. They live bottom-LEFT so the orb stage's own nav keeps the centre
   of the screen it was ported with. */
#dock {
  position:fixed; z-index:var(--z-dock);
  left:clamp(12px, 1.6vw, 22px); bottom:clamp(12px, 1.6vw, 22px);
  display:flex; flex-direction:column; align-items:flex-start; gap:7px;
  max-width:calc(100vw - 24px);
  transition:opacity .38s var(--ease), transform .38s var(--ease);
}
#dock .bar {
  display:flex; align-items:center; gap:3px;
  max-width:100%;
  padding:4px 4px 4px 11px;
  border-radius:999px;
  background:rgba(250,250,250,.72);
  -webkit-backdrop-filter:blur(20px) saturate(1.4);
  backdrop-filter:blur(20px) saturate(1.4);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.7), inset 0 0 0 1px rgba(25,25,25,.08),
             0 2px 6px rgba(25,25,25,.06), 0 14px 30px -14px rgba(25,25,25,.3);
  overflow-x:auto;
  scrollbar-width:none;
  scroll-snap-type:x proximity;
  overscroll-behavior-x:contain;
  /* a scroller with a hard edge reads as a bar that has been cut off rather
     than one you can push */
  mask-image:linear-gradient(90deg, #000 calc(100% - 20px), transparent);
  -webkit-mask-image:linear-gradient(90deg, #000 calc(100% - 20px), transparent);
}
#dock .bar::-webkit-scrollbar { display:none; }
@media (min-width:900px) {
  #dock .bar { mask-image:none; -webkit-mask-image:none; }
}
/* ── a slider ──────────────────────────────────────────────────────────────
   Three bare range inputs in one row are three identical grey lines: you cannot
   tell which is which and you cannot tell what you have set. Each has its own
   labelled row, a track that shows how far along it is, and a live readout. */
#dock input[type="range"] {
  -webkit-appearance:none; appearance:none;
  width:104px; height:16px; flex:none;
  margin:0 2px; padding:0;
  background:transparent; cursor:pointer;
}
#dock input[type="range"]::-webkit-slider-runnable-track {
  height:4px; border-radius:999px;
  /* the filled part is written as a percentage by readout(), so the track shows
     the value as well as carrying the thumb */
  background:
    linear-gradient(90deg, var(--ink) 0 var(--fill, 50%), rgba(25, 25, 25, .13) var(--fill, 50%) 100%);
}
#dock input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance:none; appearance:none;
  width:13px; height:13px; margin-top:-4.5px;
  border-radius:50%; background:#fff;
  box-shadow:0 0 0 1px rgba(25, 25, 25, .16), 0 1px 3px rgba(25, 25, 25, .28);
  transition:transform .16s var(--pop);
}
#dock input[type="range"]:hover::-webkit-slider-thumb { transform:scale(1.14); }
#dock input[type="range"]:active::-webkit-slider-thumb { transform:scale(1.24); }
#dock input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow:0 0 0 1px rgba(25, 25, 25, .16), 0 0 0 3.5px rgba(47, 107, 255, .35);
}
/* the value, so the control says what it is set to */
#dock .val {
  font-style:normal;
  font-variant-numeric:tabular-nums;
  /* ⚠ 14px, not the 11.5 this started at — the page's own "no tiny text" rule is
     checked and it covers the dock too. Tabular figures are what let a readout
     this size sit in a fixed slot without the row twitching as it counts. */
  font-size:14px; font-weight:500; letter-spacing:-.01em;
  color:var(--ink-3);
  min-width:44px; text-align:right;
  padding-right:7px;
}

/* ── the panel's own switch ────────────────────────────────────────────────
   Collapsed, the dock is this button and nothing else. Same material as a bar,
   so it reads as the panel folded rather than as a new piece of furniture. */
.dock-switch {
  display:inline-flex; align-items:center; gap:8px;
  padding:7px 14px 7px 11px;
  border:0; border-radius:999px; cursor:pointer;
  font-family:var(--face); font-size:13px; font-weight:500; letter-spacing:-.01em;
  color:var(--ink-2);
  background:rgba(250,250,250,.72);
  -webkit-backdrop-filter:blur(20px) saturate(1.4);
  backdrop-filter:blur(20px) saturate(1.4);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.7), inset 0 0 0 1px rgba(25,25,25,.08),
             0 2px 6px rgba(25,25,25,.06), 0 14px 30px -14px rgba(25,25,25,.3);
}
/* The chevron turns, so the button says which way it is about to go — and ⚠ the
   dock opens UPWARD, because it is anchored to the bottom of the window. Closed
   it points up, open it points down. Drawn the other way round it read as a
   button that would push the panel further off the screen. */
.dock-switch i {
  width:9px; height:9px; flex:none;
  border-right:1.6px solid currentColor; border-bottom:1.6px solid currentColor;
  transform:rotate(225deg) translate(-1px, -1px);
  transition:transform .3s var(--pop);
}
html[data-dock="on"] .dock-switch i { transform:rotate(45deg) translate(-1px, -1px); }
/* ⚠ ORDER, not position: the switch is the FIRST child of the dock and the dock
   is a column, so it would sit above the rows — and the dock is anchored to the
   BOTTOM of the window, which is where a thumb and a cursor already are. */
/* ⚠ DIRECT CHILDREN. `#dock .lab` also matches the label INSIDE every row — the
   rows are flex and `.lab` is their first child — so an `order` written that way
   sorted every label to the END of its own row and the whole panel came out
   reading control-then-label, right to left. Same shape as a state class sharing
   a name with a child's class: the selector has to say which one it means. */
.dock-switch { order:2; }
/* ⚠ EVERY ROW GETS order:1, the group BODY included. Without it `.grp-body` keeps
   the default 0 and an opened section jumps to the top of the panel, above the
   rows it belongs under — which reads as the sliders appearing somewhere else
   entirely. With them all equal, DOM order decides, which is what lets the dock
   be ordered by subject in js/app.js. */
#dock > .bar, #dock > .lab, #dock > .grp, #dock > .grp-body,
#dock > .dock-cap { order:1; }
#dock > .dock-cap {
  margin:10px 0 2px; padding:0 2px;
  font-size:10.5px; font-weight:500; letter-spacing:.09em; text-transform:uppercase;
  color:rgba(25,25,25,.42);
}
#dock > .dock-cap:first-of-type { margin-top:0; }
html[data-dock="off"] #dock > .dock-cap { display:none; }
html[data-dock="off"] #dock > .bar,
html[data-dock="off"] #dock > .grp,
html[data-dock="off"] #dock > .grp-body,
html[data-dock="off"] #dock > .lab { display:none; }

/* ⚠ THE PANEL MUST NOT REACH THE NAV. Open, it is twenty-odd rows tall, and on a
   short window it ran from the bottom of the screen up past the wordmark — the
   suite's own reachability check found `DIV.bar` sitting on top of the mark. It is
   dev chrome and the page's own header outranks it, so it stops short of the top and
   scrolls inside itself. */
#dock {
  max-height:calc(100vh - 150px);
  overflow-y:auto; overscroll-behavior:contain;
  scrollbar-width:thin;
}
#dock::-webkit-scrollbar { width:6px; }
#dock::-webkit-scrollbar-thumb { background:rgba(25,25,25,.18); border-radius:3px; }

#dock .lab {
  font-size:14px; letter-spacing:.01em; color:var(--ink-3);
  flex:none; padding-right:6px;
}
#dock select {
  flex:none; cursor:pointer;
  height:30px; padding:0 26px 0 11px; border-radius:999px;
  border:0; box-shadow:inset 0 0 0 1px rgba(25,25,25,.12);
  background-color:rgba(255,255,255,.6);
  /* the chevron is drawn rather than left to the platform, so the control is the
     same object on every one */
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'><path d='M1 1.5 5 5.5 9 1.5' fill='none' stroke='%23191919' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat:no-repeat;
  background-position:right 10px center;
  appearance:none; -webkit-appearance:none;
  font-family:var(--face); font-size:14px; font-weight:500; letter-spacing:-.01em;
  color:var(--ink);
  transition:box-shadow .16s var(--ease), background-color .16s var(--ease);
}
#dock select:hover { box-shadow:inset 0 0 0 1px rgba(25,25,25,.28); }
#dock select:focus-visible { outline:2px solid rgba(25,25,25,.5); outline-offset:2px; }

#dock button {
  flex:none; border:0; cursor:pointer;
  height:30px; padding:0 12px; border-radius:999px;
  background:transparent; color:var(--ink-2);
  font-family:var(--face); font-size:14px; font-weight:500; letter-spacing:-.01em;
  scroll-snap-align:center;
  transition:background .16s var(--ease), color .16s var(--ease);
}
#dock button:hover { background:rgba(25,25,25,.05); color:var(--ink); }
#dock button.on { background:var(--ink); color:#f0f0f0; }
#dock button[data-replay] { box-shadow:inset 0 0 0 1px rgba(25,25,25,.12); }
#dock .bar { padding:4px 5px 4px 11px; }

#hint {
  position:fixed; z-index:var(--z-dock);
  left:clamp(12px, 1.6vw, 22px); bottom:calc(clamp(12px, 1.6vw, 22px) + 172px);
  font-size:14px; color:var(--ink-3); letter-spacing:0;
  opacity:0; transition:opacity .3s var(--ease); pointer-events:none;
  max-width:calc(100vw - 32px);
}
#hint.show { opacity:1; }

/* ── the way in ────────────────────────────────────────────────────────── */
.rise { opacity:0; transform:translateY(10px); }
.in .rise, .rise.in {
  opacity:1; transform:none;
  transition:opacity .8s var(--ease), transform .8s var(--ease);
}
.rise.d1 { transition-delay:.08s; }
.rise.d2 { transition-delay:.16s; }
.rise.d3 { transition-delay:.24s; }

@media (prefers-reduced-motion:reduce) {
  .rise { opacity:1; transform:none; }
  .drift-row { animation:none; }
  .field .car { animation:none; }
  * { transition-duration:.001s !important; }
}

/* ── the dark band ─────────────────────────────────────────────────────── */
/* Not a beat — one line, full bleed, and then straight on. butter's device for
   a claim that does not need a picture beside it. */
.band {
  background:var(--dark);
  color:#f0f0f0;
  padding-block:clamp(44px, 6vw, 74px);
  text-align:center;
}
.band .line { color:#f0f0f0; font-size:clamp(30px, 4vw, 52px); }
.band .fxwrap { display:flex; justify-content:center; }
.bandsub { font-size:14.5px; line-height:1.5; color:rgba(240,240,240,.52); margin:16px auto 0; max-width:44ch; }
.bandrow { display:flex; align-items:center; justify-content:center; gap:14px; flex-wrap:wrap; margin-top:26px; }
/* a link is the whole distribution, so the link is the artefact */
.band .link {
  font-size:14px; color:#f0f0f0;
  padding:9px 16px; border-radius:999px;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
  background:rgba(255,255,255,.05);
}
.band .link .dim { color:rgba(240,240,240,.45); }
.bandnote { font-size:12.5px; color:rgba(240,240,240,.42); }
/* On a dark ground the tile's own drop shadow does nothing, and the artwork
   needs a touch more separation from the ink behind it. */
.band .o, .band .fx { --sh-a:.5; }

/* ── the six words ─────────────────────────────────────────────────────── */
/* Stacked exactly as the Discover frame stacks them, at the frame's own 0.92
   leading — the list IS the argument, so it is set like a headline, not a list. */
.stack {
  list-style:none; margin:18px 0 0; padding:0;
  font-size:clamp(19px, 2vw, 25px); font-weight:500;
  letter-spacing:-.028em; line-height:.94;
  color:var(--ink-3);
}
/* Revealed by the SECTION's own `in` class, the same one every other rise on
   this page waits for — a second observer with its own timers was one more way
   for six words to stay invisible and leave a hole in the column. */
.stack li { opacity:0; transform:translateY(.28em); transition:opacity .5s var(--ease), transform .6s var(--ease), color .5s var(--ease); }
.beat.in .stack li { opacity:1; transform:none; }
.beat.in .stack li:first-child { color:var(--ink); }
.stack li:nth-child(1) { transition-delay:.04s; }
.stack li:nth-child(2) { transition-delay:.14s; }
.stack li:nth-child(3) { transition-delay:.24s; }
.stack li:nth-child(4) { transition-delay:.34s; }
.stack li:nth-child(5) { transition-delay:.44s; }
.stack li:nth-child(6) { transition-delay:.54s; }

@media (prefers-reduced-motion:reduce) {
  .stack li { opacity:1; transform:none; }
}

/* ══ Discover: the Wall, in its drift view ══════════════════════════════════
   Ported from `wabi mini apps/wall` — the same feed card (170.134 × 322, ratio
   0.5284, radius 14, 2px edge) and the same four-lane marquee. Each track holds
   its sequence TWICE and travels exactly -50%, which is what makes the loop
   seamless; anything else shows a seam every lap. */
.view-drift {
  display:flex; flex-direction:column; gap:var(--wgap);
  overflow-x:hidden;
  overflow-y:visible;
  /* ⚠ NOT CROPPED, and the air is the GAP. Every row shows whole, and the space above
     the first and below the last is exactly `--wgap` — the same gutter that sits
     between the wabis — so the rhythm is even from the section's edge through to the
     other side. An earlier pass cropped 30% into the outer rows; a later one left a
     dark band above them, which is the worst of both.
     ⚠ overflow-x only. The rows do not need vertical clipping now, and clipping there
     is what sheared the hover lift and its shadow. */
  padding-block:var(--wgap);
  margin-top:clamp(40px, 5vw, 68px);
  /* the field runs out of the page rather than stopping at an edge */
  mask-image:linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  -webkit-mask-image:linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
/* ⚠ NOT `overflow:hidden`. The lane does not need to clip — the track is wider
   than the drift and the DRIFT clips it — and clipping here is what sheared the
   hover lift and its shadow. A lane that clips cannot contain a card that rises
   out of it. */
.lane { min-width:0; flex:0 0 auto; }
.track {
  display:flex; gap:var(--wgap); width:max-content;
  animation:wslide calc(var(--dur, 50s) * var(--driftk, 1)) linear infinite;
  animation-direction:var(--dir, normal);
  will-change:transform;
}
@keyframes wslide { from { transform:none; } to { transform:translateX(-50%); } }
/* stop the lane you are reading */
.lane:hover .track { animation-play-state:paused; }
@media (prefers-reduced-motion:reduce) { .track { animation:none; } }

.wcard {
  --wgap:18px;
  flex:none; width:var(--wtile);
  display:block; text-decoration:none; color:inherit;
}
/* ⚠ A FIFTH LARGER than they were. At 132/164/180 the field fitted almost exactly
   three rows into the section and the outer two were sliced thin at the edges, which
   reads as a crop rather than as a wall running out of the frame. Bigger tiles mean
   the top and bottom rows are cut well inside themselves, which is a bleed. */
.view-drift { --wgap:15px; --wtile:158px; --wradius:13px; }
@media (min-width:860px) { .view-drift { --wgap:20px; --wtile:198px; --wradius:16px; } }
@media (min-width:1400px) { .view-drift { --wtile:218px; } }

.wshot {
  position:relative;
  aspect-ratio:0.5284;                 /* the Figma card's own ratio */
  border-radius:var(--wradius);
  border:1px solid var(--hair);
  overflow:hidden;
  background:#e6e6e4;
  box-shadow:0 8px 24px rgba(25,25,25,.13);
  transition:transform .5s cubic-bezier(.2,.7,.2,1), box-shadow .5s cubic-bezier(.2,.7,.2,1);
}
.wshot img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; }
@media (hover:hover) {
  .wcard:hover .wshot { transform:translateY(-6px) scale(1.014); box-shadow:0 16px 36px rgba(25,25,25,.2); }
}
.wmeta {
  display:grid; grid-template-columns:30px 1fr; column-gap:10px;
  align-items:start; margin-top:12px;
}
.wmeta .worb { grid-row:1 / span 2; width:30px; height:30px; display:block;
               filter:drop-shadow(0 4px 2px rgba(0,0,0,.1)); }
.wmeta .wname { font-size:14.5px; line-height:1.08; color:var(--ink);
                white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.wmeta .wby { font-size:14.5px; line-height:1.08; color:var(--ink-3); margin-top:3px;
              white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* ══ the home screen inside the phone ══════════════════════════════════════
   Save is the one claim on this page that can be shown rather than described,
   so the button does it: the app screen cross-fades to a home screen and the
   thing you saved lands in it. */
.phone .screen > .app, .phone .screen > .home {
  position:absolute; inset:0;
  transition:opacity .5s var(--ease), transform .6s var(--ease);
}
.phone .screen > .app { width:100%; height:100%; object-fit:cover; object-position:top center; }
.phone .screen .home {
  opacity:0; transform:scale(1.04);
  padding:13% 7% 7%;
  display:grid; grid-template-columns:repeat(4, 1fr); gap:9% 7%;
  align-content:start;
  background:linear-gradient(168deg, #f6d9c0 0%, #e7c3d4 38%, #cbd3ee 72%, #dfe7f2 100%);
}
.phone[data-saved="true"] .screen .app { opacity:0; transform:scale(.96); }
.phone[data-saved="true"] .screen .home { opacity:1; transform:none; }
.phone .screen .home .hi { position:relative; aspect-ratio:1; border-radius:24%; overflow:hidden;
                           box-shadow:0 1px 3px rgba(0,0,0,.18); }
.phone .screen .home .hi img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
/* the one that just arrived */
.phone .screen .home .hi.new { transform:scale(.2); opacity:0; }
.phone[data-saved="true"] .screen .home .hi.new {
  transform:none; opacity:1;
  transition:transform .7s var(--pop) .28s, opacity .3s linear .28s;
}
.phone .screen .home .hi.new .icon { --i:100%; border-radius:24%; }

/* ══ the stage the page ends on ════════════════════════════════════════════
   /import's OrbFooter, ported: the page surface is opaque and rounded at the
   foot, the stage is fixed behind it, and scrolling to the end lifts one off
   the other. The scenes in js/scenes/ are the import page's own, type-stripped
   with esbuild — re-strip rather than hand-edit. */
.page {
  position:relative; z-index:1;
  /* the paper is the base, and the gradient rides over it: white to the fold, then
     a long fade. `no-repeat` and a background-color under it, so the paper carries
     on past the gradient's own height for a page of any length. */
  background:var(--paper) linear-gradient(180deg,
    #fff 0, #fff 92vh, var(--paper) 177vh) no-repeat;
  border-bottom-left-radius:40px;
  border-bottom-right-radius:40px;
}
/* ⚠ ONLY BLOCKS IS GREY. The grey ground exists for the blocks layout, whose plates
   are #f9f9f9 to their own corners and need something to read against; in Sides and
   Stacks the artwork sits on the page directly and the grey just makes the whole
   chapter look dimmed. So the fade to paper is the blocks layout's, and the other
   two are white all the way down — one declaration rather than a background per
   section, because the sections do not paint their own ground and a per-section
   white would show every seam between them. */
html[data-layout="sides"] .page,
html[data-layout="stacks"] .page { background:#fff; }
/* the closing chapter is white in EVERY layout — it is the page's call to action,
   and the footer under it takes the same white so the two do not part company with
   a grey band between them.
   ⚠ THE WHITE CANNOT GO ON `.closing` ITSELF. That element is also a `.wrap`, i.e.
   `max-width:var(--wide); margin-inline:auto` — so a background on it painted a
   1230px white column down the middle of the page with the grey showing either
   side, which looked like the section had been cropped. The ground is a full-bleed
   pseudo behind the content instead, sized off `--vw` (the documentElement's own
   client width) rather than 100vw, because 100vw includes the scrollbar and would
   push a horizontal one onto the page. */
.closing, footer.foot { position:relative; background:transparent; }
.closing::before, footer.foot::before {
  content:''; position:absolute; z-index:-1; top:0; bottom:0;
  left:50%; transform:translateX(-50%);
  width:var(--vw, 100vw); background:#fff;
}
/* ══ the load ═══════════════════════════════════════════════════════════ */
/* ⚠ HIDDEN, NOT TRANSPARENT — `visibility:hidden` still lays out and still
   measures, and the whole point of the boot class is that the fit runs while
   these are invisible. `opacity:0` would do as well; `display:none` would not,
   it would measure every line at zero. See the inline script in index.html for
   why this exists at all. */
html.boot .fxwrap[data-line],
html.boot #dock,
html.boot .presets-wrap { visibility:hidden; }
/* ⚠ THE PHONES ARE NOT HIDDEN. Their fan-out is a deliberate entrance that starts
   at boot, and hiding them until the reveal would clip the front of it — the thing
   the fit is being hidden for is a JUMP, and the deck does not jump. */
/* and the first paint after the reveal is a fade, so the words arrive rather
   than appearing — 160ms, which is under the threshold where somebody would
   call it an animation */
/* ⚠ NO `both`, AND THAT IS NOT A DETAIL. An animation with a forwards fill wins
   against ordinary declarations for as long as it is filling — i.e. for ever — so
   with `both` this rule pinned #dock to opacity 1 and `body.stage-open #dock
   { opacity:0 }` could never take effect: the dock sat on top of the orb stage's
   own nav at the foot of the page. Same trap as an author `display` beating the
   `hidden` attribute; the suite caught it, no screenshot would have. */
html:not(.boot) .fxwrap[data-line],
html:not(.boot) #dock,
html:not(.boot) .presets-wrap { animation:boot-in 160ms var(--ease-wabi, cubic-bezier(.4,0,.2,1)); }
@keyframes boot-in { from { opacity:0; } to { opacity:1; } }
@media (prefers-reduced-motion:reduce) {
  html:not(.boot) .fxwrap[data-line],
  html:not(.boot) #dock,
  html:not(.boot) .presets-wrap { animation:none; }
}
.trigger { height:1px; }
.spacer { height:40vh; min-height:300px; }
@media (min-width:768px) { .spacer { height:62vh; min-height:370px; } }

/* 100dvh prevents a gap below the stage when the iOS toolbar moves */
.panel {
  position:fixed; left:0; right:0; bottom:0;
  height:100dvh;
  overflow:hidden; z-index:0;
  background-image:radial-gradient(120% 90% at 50% 0%, #232322 0%, #191919 100%);
}
@media (min-width:768px) { .panel { height:calc(62vh + 120px); min-height:490px; } }
.panel canvas {
  position:absolute; inset:0; width:100%; height:100%; display:block;
  opacity:1; transition:opacity 180ms ease;
}
.panel-nav {
  position:absolute; left:50%; bottom:calc(22px + env(safe-area-inset-bottom));
  transform:translate(-50%, 14px);
  display:flex; align-items:center; gap:2px; padding:5px;
  border-radius:999px;
  background:rgba(28,28,27,.6);
  -webkit-backdrop-filter:blur(18px) saturate(1.3);
  backdrop-filter:blur(18px) saturate(1.3);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.12), 0 8px 30px rgba(0,0,0,.35);
  opacity:0; pointer-events:none;
  transition:opacity 420ms cubic-bezier(.22,1,.36,1), transform 420ms cubic-bezier(.22,1,.36,1);
  max-width:calc(100% - 32px);
  /* ⚠ IT SCROLLS, because five scenes plus a reset is wider than a phone. The
     labels are `nowrap` and the row is a flex with no shrink, so without this the
     buttons simply spill out of the pill they are supposed to be inside — the
     rounded ground stops and the last label floats on the artwork. A pill you can
     push sideways keeps every control reachable at every width. */
  overflow-x:auto; overflow-y:hidden;
  scrollbar-width:none; -ms-overflow-style:none;
  overscroll-behavior-x:contain;
}
.panel-nav::-webkit-scrollbar { display:none; }
/* the reset is not one of the scenes, so it is not a choice — it never takes the
   pressed state, and a hairline says it belongs to whichever scene is running */
.panel-nav .sc-reset {
  margin-left:4px; color:rgba(240,240,240,.82);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.20);
  display:grid; place-items:center;
  /* square, and the same 44 target the labelled buttons make with their padding */
  width:44px; height:44px; padding:0; flex:0 0 auto;
}
@media (min-width:768px) { .panel-nav .sc-reset { width:38px; height:38px; } }
.panel-nav .sc-reset svg { display:block; }
.panel-nav .sc-reset:hover { color:#f0f0f0; background:rgba(255,255,255,.12); }
.panel-nav .sc-reset[hidden] { display:none; }
@media (min-width:768px) { .panel-nav { bottom:30px; } }
.panel-nav.open { opacity:1; transform:translate(-50%, 0); pointer-events:auto; }
.panel-nav button {
  appearance:none; border:0; cursor:pointer;
  /* the live panel sets 12.5/13.5px — raised, because this page carries no tiny
     text and these four are the only controls over the stage */
  font-family:var(--face); font-size:14px; font-weight:400; letter-spacing:-.01em;
  line-height:1; white-space:nowrap;
  color:rgba(240,240,240,.66); background:transparent;
  /* 16px of padding on a 12.5px line is a 44px target — this is a bar you
     reach for with a thumb, and 31px was not one */
  padding:16px 13px; border-radius:999px;
  transition:background-color 160ms, color 160ms;
}
@media (min-width:768px) { .panel-nav button { font-size:15px; padding:11px 16px; } }
/* six controls on a phone: the target height stays 44, the width comes in */
@media (max-width:767.98px) { .panel-nav button { padding:16px 11px; } }
.panel-nav button:hover { color:rgba(240,240,240,.92); background:rgba(255,255,255,.08); }
.panel-nav button[aria-pressed="true"] { color:#191919; background:#f0f0f0; }

/* The stage's nav and the effect dock are both fixed to the bottom centre, and
   past the foot of the page there is no headline left for the dock to affect. */
body.stage-open #dock { opacity:0; transform:translateX(-14px); pointer-events:none; }
/* ⚠ AND THE PRESETS GIVE WAY TOO, on a narrow screen. Three bars cannot share the
   foot of a 390pt window: the stage's own scene nav is centred there, and the preset
   pill sat on top of it with "Lens" disappearing under the 3. On a desktop they do
   not meet — the nav is centred, the presets are in the corner — so this is the
   narrow case only, and it is the same answer the dock already gives. */
@media (max-width:520px) {
  body.stage-open .presets-wrap { opacity:0; transform:translateY(10px); pointer-events:none; }
  /* ⚠ AND THE CHILDREN, because `.presets-wrap > *` sets `pointer-events:auto` — a
     child that turns them back on is not stopped by a parent that turned them off.
     Faded out to nothing, the preset pill was still swallowing every tap in the
     bottom-right of the stage, i.e. the right-hand end of the scene nav: on a phone
     the last scene in the pill could not be selected AT ALL, and the pill looked
     perfect. Same shape as the invisible thumb-pad overlay in flail. */
  body.stage-open .presets-wrap > * { pointer-events:none; }
}
.presets-wrap { transition:opacity .3s var(--ease), transform .3s var(--ease); }
body.stage-open #hint { opacity:0; }

/* ══ an app is always an orb ════════════════════════════════════════════════
   Never a rounded square. `img/apporb/*` are the covers clipped to a circle
   with the import page's own orb-overlay composited on top — baked, so nothing
   here depends on mix-blend-mode surviving an ancestor's opacity. */
.ao { width:50px; height:50px; display:block; flex:none;
      filter:drop-shadow(0 4px 9px rgba(20,18,14,.2)); }

/* the home screen: orbs, because that is what a saved wabi looks like */
.phone .screen .home { gap:11% 8%; }
.phone .screen .home .hi { width:100%; aspect-ratio:1; display:block;
                           filter:drop-shadow(0 2px 4px rgba(0,0,0,.22)); }

/* the hero is one idea now, so it can breathe */
.hero { padding-block:clamp(96px, 12vh, 150px) clamp(64px, 9vh, 110px); }

/* ── the air above the headline ────────────────────────────────────────────
   `centre`, `huge` and `top` stack the copy OVER the phone, so the top padding
   is the only thing holding the headline off the header — and with the phone on,
   the hero is taller than the window, `justify-content:safe center` falls back
   to start, and this number is exactly where the reader's eye lands first. One
   custom property so the phones-on and phones-off cases cannot drift.
   `left` is deliberately not in here: there the copy is centred in its own
   column beside the phone and the padding does nothing for it. */
html:not([data-hero="left"]) {
  /* the header's own measured height (js/app.js writes it) plus the air, less 50px:
     the hero opens higher than it did, which is what the fold has room for now that
     the ground no longer changes colour at the bottom of it. */
  --hero-top:calc(var(--nav-h, 124px) + clamp(76px, 9.5vh, 132px) - 50px);
}
html:not([data-hero="left"]) .hero { padding-block-start:var(--hero-top); }

/* ── the dock's two groups ──────────────────────────────────────────────── */

/* ══ the hero, in three sizes ═══════════════════════════════════════════════
   The size is a CEILING, not a promise: fitLine() searches down from it until
   the widest line fits its column, so `huge` can ask for 30vw and simply get
   the largest that fits. */
html[data-hero="huge"] .hero.wrap {
  max-width:none;
  padding-inline:clamp(12px, 1.6vw, 26px);
}
html[data-hero="huge"] h1.line { font-size:30vw; line-height:.94; letter-spacing:-.042em; }
/* at that size the words are the hero, so everything under them steps back */
html[data-hero="huge"] .hero .lead { font-size:calc(14.5px * var(--sub-scale, 1)); max-width:52ch; margin-top:calc(52px * var(--gap-scale, 1)); }
html[data-hero="huge"] .hero .cta { margin-top:calc(36px * var(--gap-scale, 1)); }

html[data-hero="play"] h1.line { font-size:clamp(46px, 9vw, 132px); line-height:.96; }

/* the subhead that carries the pile is the lockup's second line, so it is set
   larger than an ordinary lead and never wraps mid-phrase if it can help it */
.hero .lead.with-pile {
  font-size:calc(clamp(19px, 2.4vw, 34px) * var(--sub-scale, 1));
  line-height:1.25; letter-spacing:-.022em;
  color:var(--ink); max-width:24ch;
}


/* ══ the hero deck ══════════════════════════════════════════════════════════
   Three phones fanned as a stack, the way wabi.ai's hero fans them — but each
   one is a different VIEW of the app rather than another home screen, and the
   feed is the one in front. */
.deck-wrap { display:none; }
html[data-phones="on"] .deck-wrap { display:block; }

.deck {
  position:relative;
  width:100%;
  /* the cards are absolutely positioned about the centre, so the wrap needs a
     height of its own — the phone's aspect (760:1562) at the card width */
  height:calc(var(--dw, 240px) * 2.055 + 30px);
  transform-style:preserve-3d;
  touch-action:pan-y;                 /* a vertical drag is the page's, not the deck's */
}
.dcard {
  position:absolute; left:50%; top:50%;
  width:var(--dw, 240px);
  will-change:transform, opacity;
  cursor:grab;
}
.dcard.is-front { cursor:default; }
.dcard .phone { --pw:100%; width:100%; }
.dcard .screen { background:#f4f3f1; }

/* the two arrangements */
html[data-hero="left"] .hero { text-align:left; align-items:stretch; }
html[data-hero="left"] .hero .fxwrap { justify-content:flex-start; }
html[data-hero="left"] .hero .lead { margin-inline:0; text-align:left; }
html[data-hero="left"] .hero .cta { justify-content:flex-start; }
html[data-hero="left"] .hero .deck-wrap { margin:0; }
@media (min-width:900px) {
  html[data-hero="left"] .hero {
    display:grid;
    grid-template-columns:minmax(0,1.04fr) minmax(0,.96fr);
    /* ⚠ TWO 1fr SPACERS, so the copy is centred AS A BLOCK. With three auto rows and
       the deck spanning them, the copy was distributed down the deck's height — the
       headline near the top, the button near the foot — rather than sitting as one
       lockup on the vertical middle, which is how wabi.ai's left hero reads. The
       spacers take the slack and the three copy rows keep their own rhythm. */
    grid-template-rows:1fr auto auto auto 1fr;
    column-gap:clamp(28px, 4vw, 64px);
    row-gap:0;
    align-items:center;
    justify-content:center;
  }
  /* the copy is ONE cell holding a column, so the three parts keep their own
     rhythm instead of being spread over three stretched grid rows */
  html[data-hero="left"] .hero > .fxwrap,
  html[data-hero="left"] .hero > .lead,
  html[data-hero="left"] .hero > .cta { grid-column:1; }
  html[data-hero="left"] .hero > .fxwrap { grid-row:2; align-self:end; }
  html[data-hero="left"] .hero > .lead { grid-row:3; align-self:start; }
  html[data-hero="left"] .hero > .cta { grid-row:4; align-self:start; }
  html[data-hero="left"] .hero > .deck-wrap { grid-column:2; grid-row:1 / span 5; align-self:center; }
  html[data-hero="left"] .hero { align-content:center; }
  /* half the window is a narrower column, so the type is smaller in it — the
     fit's ceiling, which it then searches down from */
  html[data-hero="left"] h1.line { font-size:clamp(32px, 4.2vw, 64px); }
}
html[data-hero="left"] .hero .lead.with-pile { margin-inline:0; max-width:none; }
@media (min-width:900px) {
  html[data-hero="left"] .hero > .lead { margin-top:calc(26px * var(--gap-scale, 1)); }
  html[data-hero="left"] .hero > .cta { margin-top:calc(64px * var(--gap-scale, 1)); }
}
html[data-hero="top"] h1.line { font-size:clamp(34px, 4.4vw, 66px); }
/* Every arrangement but `left` puts the deck UNDER the copy — and lets the fold
   CUT it, so the phone peeks up from below rather than sitting whole in a box.
   The wrap takes whatever height is left and clips; because the hero's own
   bottom padding goes to zero, the clip and the fold are the same line. */
/* Every arrangement but `left` puts the deck UNDER the copy, WHOLE — the hero
   grows to hold it and the page carries on below. Nothing is clipped: an earlier
   pass cropped it at the fold and that also cut the subhead, because one
   `overflow:hidden` cannot tell the two axes apart. */
html[data-phones="on"]:not([data-hero="left"]) .hero {
  /* ⚠ The bottom padding is the gap between the FOOT OF THE PHONE and whatever
     comes next, and with the phone on that is the only thing keeping the dark
     Discover band off it — 100px more, because a dark full-bleed section
     arriving right under a phone reads as the phone standing on it. */
  padding-block:var(--hero-top) calc(clamp(28px, 4vh, 60px) + 100px);
}
html[data-phones="on"]:not([data-hero="left"]) .hero .deck-wrap {
  width:100%;
  /* the Gaps slider reaches this one too: the air under the headline block is
     every gap in the stack, and the button-to-phone one is the last of them */
  margin-top:calc(clamp(30px, 4vh, 55px) * var(--gap-scale, 1));
}
html[data-phones="on"]:not([data-hero="left"]) .hero .lead { margin-top:calc(26px * var(--gap-scale, 1)); }
html[data-phones="on"]:not([data-hero="left"]) .hero .cta { margin-top:calc(39px * var(--gap-scale, 1)); }
/* ⚠ With the button hidden the thing under the subhead is the PHONE, so the
   deck's own top margin is what "below the subheadline" means — scaled by the
   same 1.3. Only in that case: with the button shown this gap is the
   button-to-phone one, which has its own number. */
html[data-cta="off"][data-phones="on"]:not([data-hero="left"]) .hero .deck-wrap {
  margin-top:calc(clamp(39px, 5.2vh, 72px) * var(--gap-scale, 1));
}
/* the deck is smaller when it is UNDER the copy rather than beside it — it has
   the height left over rather than the height of the whole hero */
html[data-phones="on"]:not([data-hero="left"]) .deck {
  --dw:calc(min(450px, 88vw) * var(--deck-scale, 1));
}
@media (min-width:900px) {
  html[data-phones="on"]:not([data-hero="left"]) .deck {
    --dw:calc(min(540px, 48vw) * var(--deck-scale, 1));
  }
}

/* The card's own width. `left` falls through to these — the phones-on rules
   above are more specific and only apply when the deck is UNDER the copy.
   ⚠ The fan reaches 0.30 of a card past each edge, so the deck's real footprint
   is 1.60 cards and NOT one. Under the copy that overhang has the whole page to
   sit in; beside it, in `left`, it has one grid column, and widening the fan put
   10px of it outside the document. The card does NOT pay for that — js/deck.js
   clamps the REACH to the room its container actually has, which is the thing
   that varies. Shrinking the phone here was the first fix and it was the wrong
   one: the phone is what the reader is looking at, the fan is a flourish. */
.deck { --dw:calc(min(387px, 80vw) * var(--deck-scale, 1)); }
@media (min-width:900px) { .deck { --dw:calc(min(405px, 30vw) * var(--deck-scale, 1)); } }

/* ── the feed view: a film of the real feed ───────────────────────────────
   ⚠ There used to be ~90 lines of redrawn feed chrome here — a card at 90% of
   the width, an actions row, a byline, a tab bar, every length a share of the
   screen. All of it is gone: the phone now plays a recording of
   feed.joonas.wtf, so the only thing this needs to say is how a film sits in a
   screen. The film's frame is 19.5:9 like the glass, and `cover` takes up the
   hundredth of a per-cent the bezel trims. */
.vfilm {
  position:absolute; inset:0; width:100%; height:100%;
  display:block; object-fit:cover; object-position:top center;
  background:#000;
}
.dcard .screen { container-type:inline-size; }

/* ── the home view ───────────────────────────────────────────────────────── */
/* The pile's ring in the SUBHEAD, 30% thicker than the port's 3px. css/wabi.css
   stays verbatim; this is the override. */
/* the same 8% of the avatar — the port's own 0.988em face, so the ring is 0.079em.
   ⚠ THE RING IS WHITE, not `--paper`. It tracked the page's own ground, which is a
   grey — so on the grey the faces read as cut out of the page rather than as prints
   lying on it, and the ring disappeared entirely wherever the ground matched it. */
.hero .lead .facepile .av { box-shadow:0 0 0 calc(0.988em * var(--pile-ring, .08)) #fff; }

/* ── the two phones behind the feed ───────────────────────────────────────
   ⚠ There used to be ~45 lines here for a hand-drawn home screen and a
   hand-drawn compose thread. Both are gone: those phones now carry real app
   screens exported from the same Figma file as the showcase sections, and a
   drawing of a screen standing next to a FILM of one is the mismatch you notice
   before anything else. */
.phone .screen > img.vscr {
  position:absolute; inset:0;
  width:100%; height:100%;
  display:block;
  object-fit:cover; object-position:top center;
  background:#fff;
}


/* ══ the subhead, wabi.ai's own ═════════════════════════════════════════════
   Three lines from personal-subhead.tsx, at app.tsx's own type: weight 300,
   clamp(18px, 1.82vw, 29px), line-height 1.4, tracking -0.01em, dlig on, and
   marginTop clamp(25px, 2.8vh, 42px). */
html[data-subhead="off"] .hero .lead { display:none; }
/* the appear setting decides whether the blur runs at all */
/* ⚠ NO GATE ANY MORE — the blur is a constant. It used to be switched off for the
   appear values that did not want it, and with the axis reduced to the 3D
   (scatter/sweep/none) that same rule silently killed the blur on two of the three,
   including `none`, which is meant to mean "no 3D entrance" and not "nothing at
   all". Reduced-motion still turns it off, further down. */
.hero .lead {
  font-family:var(--face);
  /* the live site sets clamp(18px, 1.82vw, 29px) — larger here, because against
     a fitted headline that can reach 200px the live ratio reads as a caption */
  font-size:calc(clamp(21px, 2.4vw, 40px) * var(--sub-scale, 1));
  font-weight:300;
  line-height:1.4;
  letter-spacing:-0.01em;
  font-feature-settings:"dlig" 1;
  color:rgba(91,91,91,.9);
  max-width:none;
  margin-top:calc(clamp(40px, 4.55vh, 69px) * var(--gap-scale, 1));
}
@media (max-width:899px) { .hero .lead { font-size:calc(clamp(22px, 5.6vw, 34px) * var(--sub-scale, 1)); } }
.ps-line { display:block; }
/* Centred, the three-line subhead is a tall stack under a headline that is
   already several lines — so the last two join into one. Left-aligned it keeps
   wabi.ai's own three, where the ragged edge is the point. The space between
   them is a real text node (see personalSubhead), so it appears when they go
   inline and collapses when they are blocks. */
html:not([data-hero="left"]) .hero .ps-line-2,
html:not([data-hero="left"]) .hero .ps-line-3 { display:inline; }
/* ⚠ EXCEPT WHILE THE WORD IS CYCLING, when all three stay blocks. Joined, the last two
   are one wrapping line whose break depends on how wide the cycling word made the
   FIRST line — so every swap ("inside jokes" → "personal software" → "small games")
   re-broke it and the subhead jumped between two and three lines under the reader. The
   semantic break is fixed, so it cannot move; the paragraph-balanced version below is
   for the word held still, where nothing changes width. */
html[data-cycle="on"]:not([data-hero="left"]) .hero .ps-line-2,
html[data-cycle="on"]:not([data-hero="left"]) .hero .ps-line-3 { display:block; }
/* With the word held still the whole subhead is one static sentence, so it can
   be one flowing paragraph and the browser can balance it — which is the natural
   break. While the word is CYCLING it keeps the semantic break instead: a
   balanced paragraph re-breaks every time the word changes width, and the line
   jumps under the reader. */
html[data-cycle="off"]:not([data-hero="left"]) .hero .lead .ps-line { display:inline; }
html[data-cycle="off"]:not([data-hero="left"]) .hero .lead {
  text-wrap:balance;
  max-width:26ch;
  margin-inline:auto;
}
/* and 20% smaller centred, where it is a paragraph under a tall headline rather
   than the lockup's own second line */
html:not([data-hero="left"]) .hero .lead { font-size:calc(clamp(16px, 1.54vw, 26px) * var(--sub-scale, 1)); }
@media (max-width:899px) {
  html:not([data-hero="left"]) .hero .lead { font-size:calc(clamp(16px, 3.6vw, 22px) * var(--sub-scale, 1)); }
}
/* ⚠ The pile sits after "friends" on the third line, and its alignment is DERIVED
   the same way the headline's is: for an inline-block of height H, `vertical-align:X`
   puts its middle at X + H/2 above the baseline, and centring it on the line's
   optical middle (cap/2) gives X = (cap − H)/2. Selecta's cap measures 0.66em and the
   subhead avatar is the port's 0.988em, so X = −0.164em. The port's own −0.24em (and
   the −0.32em this had) are both a few px low, which is exactly how it looked. */
.ps-line-3 .facepile { vertical-align:calc((0.66em - 0.988em) / 2); }
[data-pileslot] { display:inline; }

/* ── the pile when it is IN the headline ───────────────────────────────────
   ⚠ Scoped to the headline, not applied to `.facepile` itself. The pile is sized
   in `em`, so one rule serves both places — but the two places want different
   numbers: in the subhead it sits in a line of 26px copy, in the headline it is
   a word in a lockup that can reach 200px. css/wabi.css is a verbatim port of
   the live site's own facepile and is not nudged; this is the override. */
/* ⚠ THE ALIGNMENT IS DERIVED, not nudged. Two passes at eyeballing it left the pile
   sitting low; the engine already centres a swapped tile on the line's OPTICAL
   MIDDLE — `capMid = baseline - cap/2`, cap being the font's measured ascent — so the
   pile uses the same arithmetic and agrees with the tiles by construction.
   For an inline-block of height H, `vertical-align: X` puts its bottom X above the
   text baseline, so its middle lands at X + H/2. Setting that equal to cap/2 gives
   X = (cap − H)/2. Selecta's cap MEASURES 0.66em (read off the running font, not the
   0.693 I first assumed) and the avatar is 0.84em, so X = −0.09em — where eyeballing
   had reached −0.205em, about 10px too low at 88px. Verified at 0px of residue. */
h1.line .facepile { vertical-align:calc((0.66em - 0.84em) / 2); }
h1.line .facepile .av {
  /* 15% off the 0.988em the port carries */
  width:0.84em; height:0.84em;
  /* ⚠ AND THE RING IS PROPORTIONAL — 8% of the avatar, not a px value. At a fixed
     5.07px the ring is right on a 74px face in the headline and far too heavy on the
     19px one in the subhead, which is the same rule seen at a quarter of the size.
     One ratio, two places, and it cannot drift. */
  box-shadow:0 0 0 calc(0.84em * var(--pile-ring, .08)) #fff;
}

/* the cycling word, from cycling-word.tsx */
.cw-wrap {
  position:relative; display:inline-block; vertical-align:bottom;
  overflow:hidden; height:1.4em;
  transition:width .45s cubic-bezier(0.25, 1, 0.5, 1);
  white-space:nowrap;
}
.cw-measure { visibility:hidden; white-space:nowrap; }
.cw-current, .cw-prev { position:absolute; left:0; top:0; white-space:nowrap; }
.cw-letter, .cw-letter-out { display:inline-block; white-space:pre; }
.cw-letter { animation:cw-in .5s cubic-bezier(0.25, 1, 0.5, 1) both; }
.cw-letter-out { animation:cw-out .4s cubic-bezier(0.5, 0, 0.75, 0) both; }
@keyframes cw-in  { from { opacity:0; transform:translateY(.72em); } to { opacity:1; transform:none; } }
@keyframes cw-out { from { opacity:1; transform:none; } to { opacity:0; transform:translateY(-.72em); } }
@media (prefers-reduced-motion:reduce) {
  .cw-letter, .cw-letter-out { animation:none; }
  .cw-letter-out { display:none; }
}

/* ── the headline's typeface ────────────────────────────────────────────────
   Kalice is the live site's --font-title, at clamp(29px, 4.61vw, 71px), and
   `Make it personal` is the live site's own headline. ⚠ These metrics belong to
   the FACE and not to the words: the block used to be keyed on
   [data-head="make"], which meant Kalice could only ever appear on that one
   headline and putting it on another would have given it a grotesk's size, line
   height and tracking. The Face control writes data-typeface. */
html[data-typeface="kalice"] h1.line {
  font-family:'Kalice', ui-serif, Georgia, serif;
  font-size:clamp(29px, 4.61vw, 71px);
  line-height:.98;
  letter-spacing:-.01em;
}
@media (max-width:899px) {
  html[data-typeface="kalice"] h1.line { font-size:clamp(37px, 11.98vw, 59px); }
}
/* huge is the one arrangement that overrides the copy's own ceiling, because
   its whole point is the biggest that fits */
html[data-hero="huge"][data-typeface="kalice"] h1.line { font-size:25.5vw; --fit-h:0.42; }
html[data-hero="huge"] h1.line {
  /* 25% off 30vw — huge was overshooting the room it had */
  font-size:22.5vw;
  /* three lines need more of the window than two, and being big is the whole
     point of this arrangement — the CTA still has to stay on the fold, which
     tools/heroes.mjs checks */
  --fit-h:0.46;
}

/* ══ Discover: the wall, on a dark ground, and nothing else ═════════════════
   No headline, no copy. Three lanes of the wall on #191919 — the cards are the
   only thing lit, which is the argument the copy was making in words. */
.beat.dark {
  background:var(--dark);
  border-top:0;
  /* ⚠ NO PADDING OF ITS OWN. The drift carries exactly one gutter above and below its
     rows, and a section padding on top of that is what produced the dark band. */
  padding-block:0;
}
.beat.dark .view-drift {
  margin-top:0;
  /* the mask fades the lanes into the dark rather than into paper */
  mask-image:linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  -webkit-mask-image:linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
/* ── the line over the wall ───────────────────────────────────────────────
   Centred in the lanes, not above them. ⚠ It needs a scrim: the backdrop is
   twenty-four app screens and no weight of type survives that on its own — a
   radial wash of the section's own ink, wide and soft, so the middle reads and
   the lanes still run behind it. */
#discover { position:relative; }
.wall-over {
  position:absolute; inset:0; z-index:2;
  display:flex; align-items:center; justify-content:center;
  padding-inline:var(--gut);
  pointer-events:none;
}
/* ⚠ TWO LAYERS, AND THE FIRST IS EVEN. A radial wash reads as a spotlight on the
   copy — it leaves the corners bright and the middle black, so the wall looks lit
   rather than dimmed. The dim is UNIFORM across the whole section, and the copy
   gets a SECOND, stronger patch behind it, which is what keeps it readable over a
   screenshot that might be pale or dark in any given place. */
/* ⚠ THE EVEN SCRIM IS GONE, and it had to be. It sat ABOVE the cards, so it dimmed
   the hovered one along with everything else and no card could ever read at full
   strength. The dim is on the CARDS now — opacity, not an overlay — so the one under
   the pointer can be exactly 100% while the rest fall back. The copy keeps its own
   patch below, which is what makes it readable over whichever screenshot is behind
   it. */
/* ⚠ AND THE PATCH IS GONE TOO. A radial darkening behind the copy is a spotlight
   whichever way round you put it: soft-edged or not, it makes a visible oval of
   deeper black in the middle of the wall, and the eye reads it as a smudge on the
   artwork rather than as a scrim. The dim is the CARDS' own opacity, which is even
   across the whole section by construction — so there is nothing here at all, and
   the headline reads against a wall that is uniformly held back. */
.wall-over .fxwrap {
  position:relative;
  /* ⚠ 80% WIDE, and that is what makes this headline 20% smaller. Its size does not
     come from the font-size below — it carries `data-fit`, so `fitLine` searches up
     until the line fills the ROOM, and it was landing at 97px against a clamp whose
     ceiling is 75. Changing the clamp did nothing; narrowing the box is the lever. */
  display:flex; justify-content:center; align-self:stretch;
  width:80%; margin-inline:auto;
  align-items:center;
}
.wall-over h2.line {
  text-align:center;
  color:#f9f9f9;
  /* ⚠ 20% up on 34/5.2vw/78, every term — a clamp whose middle term is left alone
     stops growing at the width where the vw term was already binding. */
  /* ⚠ 20% down again, on every term. This went UP 20% a moment ago and is now back
     within a hair of where it started (34/5.2/78) — worth noting so the next pass
     does not read the arithmetic as a mistake. */
  font-size:clamp(33px, 4.99vw, 75px);
  letter-spacing:-.035em;
  line-height:1.02;
  /* the swap lands on the same layer, so a thing standing in this line is lit
     against the scrim rather than against a screenshot */
  text-shadow:0 2px 24px rgba(0, 0, 0, .45);
}
/* the effect's own tiles need to sit above the scrim too */
.wall-over .fly { z-index:3; }

/* No hairline on the dark: at this size the border reads as a white outline
   drawn round every app rather than as the card's own edge.
   ⚠ `border-color:transparent` DOES NOT REMOVE IT. A background paints to the
   BORDER box, not the padding box, so a transparent border just shows the
   card's own pale `background:#e6e6e4` through it — which is a 1px light ring
   round every app, exactly the thing being removed, and it survived a pass that
   thought it had gone. The border has to have no WIDTH. */
.beat.dark .wshot { border:0; background:#141414; box-shadow:0 10px 30px rgba(0,0,0,.45); }
.beat.dark .wmeta .wname { color:#f0f0f0; }
.beat.dark .wmeta .wby { color:rgba(240,240,240,.5); }
/* the section above it ends on paper, so the joins are square rather than a
   hairline that would read as a border on the dark */
.beat.dark + .beat { border-top:0; }

/* ══ the cursor ═════════════════════════════════════════════════════════════
   A real ELEMENT, not `cursor: url()`, because an image cursor cannot be rotated
   and the lean is the point. Two layers: the arrow, and a halo centred on the
   TIP — the tip is what the arrow points with, so it is what should look lit.
   The tip is measured off the artwork (21.9%, 6.2%) and is both where the thing
   sits and what it turns about, so the point it indicates never moves.
   77px = the artwork's own 64 plus a fifth. */
#cursor {
  position:fixed; left:0; top:0; z-index:9999;
  /* ⚠ A PERCENTAGE MARGIN RESOLVES AGAINST THE CONTAINING BLOCK'S WIDTH, not the
     element's own — and this is position:fixed, so `margin-left:-21.9%` was 219px
     of the VIEWPORT rather than 17px of the cursor. The arrow was drawn a fifth
     of the window to the left of the pointer it was meant to be. transform-origin
     percentages ARE relative to the element's own box, which is why the tip
     pivoted correctly while the whole thing sat in the wrong place. The offsets
     are therefore taken off the cursor's own size, and the tip is a unitless
     FRACTION so it can be used in both. */
  --cur-s:77px;
  --tip-fx:0.219;
  --tip-fy:0.062;
  width:var(--cur-s); height:var(--cur-s);
  margin-left:calc(var(--cur-s) * var(--tip-fx) * -1);
  margin-top:calc(var(--cur-s) * var(--tip-fy) * -1);
  transform-origin:calc(var(--cur-s) * var(--tip-fx)) calc(var(--cur-s) * var(--tip-fy));
  pointer-events:none;
  opacity:0;
  transition:opacity .18s linear;
  will-change:transform;
}
#cursor.live { opacity:1; }
#cursor .arw {
  position:absolute; inset:0; width:100%; height:100%;
  image-rendering:pixelated;
  filter:drop-shadow(0 3px 6px rgba(20,18,14,.22));
  transform-origin:calc(var(--cur-s) * var(--tip-fx)) calc(var(--cur-s) * var(--tip-fy));
  transition:transform .22s var(--pop), filter .22s var(--ease);
}
/* The ring belonged to the previous second state, where the arrow stayed and a
   halo opened under its tip. The second state is now the pixel HAND, which says
   "this does something" on its own — a halo behind a hand cursor is the page
   explaining a cursor everybody has used since 1993. Kept, unused, at zero. */
#cursor .ring {
  position:absolute;
  /* a percentage offset on an absolutely positioned child DOES resolve against
     its parent's box, so these are correct as they are */
  left:calc(var(--tip-fx) * 100%); top:calc(var(--tip-fy) * 100%);
  width:44px; height:44px; margin:-22px 0 0 -22px;
  border-radius:50%;
  background:radial-gradient(circle at 50% 50%,
             rgba(25,25,25,.10) 0%, rgba(25,25,25,.055) 52%, rgba(25,25,25,0) 72%);
  box-shadow:inset 0 0 0 1.5px rgba(25,25,25,.16);
  -webkit-backdrop-filter:blur(2px) saturate(1.15);
  backdrop-filter:blur(2px) saturate(1.15);
  opacity:0; transform:scale(.35);
  transition:opacity .2s var(--ease), transform .3s var(--pop);
}
/* ⚠ The hand's size and hotspot are written as inline custom properties by
   js/app.js, in the same call that swaps the artwork — NOT here. Two places
   holding the geometry of one cursor is two chances for the drawn tip and the
   real point to disagree, which is a bug that looks like the page ignoring
   clicks. This rule only says the hand is a flat printed thing: no scale, and a
   shorter shadow, because a system cursor does not hover over the page. */
#cursor.hot .arw {
  transform:none;
  filter:drop-shadow(0 1px 2px rgba(20,18,14,.30));
}
html[data-cursor="arrow"] #cursor { display:none; }
/* the native pointer only goes away when ours is up, and never on a touch
   device, where there is no pointer to replace */
@media (hover: hover) and (pointer: fine) {
  html[data-cursor="wabi"] body, html[data-cursor="wabi"] body * { cursor:none; }
}
@media (hover: none) { #cursor { display:none; } }
@media (prefers-reduced-motion: reduce) {
  #cursor, #cursor .arw, #cursor .ring { transition:none; }
}

/* ══ what it does: three cards ══════════════════════════════════════════════
   butter.video's feature row: a tall rounded picture, then the name, then two
   lines. The picture is a real piece of the interface rather than a stock shot,
   which is the only reason a row like this says anything. */
.cards {
  display:flex; gap:clamp(16px, 1.8vw, 26px);
  margin-top:clamp(30px, 4vw, 54px);
  padding-inline:var(--gut);
  overflow-x:auto;
  scroll-snap-type:x proximity;
  overscroll-behavior-x:contain;
  scrollbar-width:none;
  /* the last card runs off the right edge, which is what says there is more */
  mask-image:linear-gradient(90deg, #000 calc(100% - 60px), transparent);
  -webkit-mask-image:linear-gradient(90deg, #000 calc(100% - 60px), transparent);
  cursor:grab;
}
.cards::-webkit-scrollbar { display:none; }
.cards:active { cursor:grabbing; }
.card-item {
  flex:none; scroll-snap-align:start;
  width:clamp(258px, 31vw, 430px);
}
.card-item h3 {
  margin:22px 0 0; font-size:clamp(23px, 2.1vw, 32px); font-weight:500;
  letter-spacing:-.025em; line-height:1.1;
}
.card-item p { margin:8px 0 0; font-size:clamp(17px, 1.3vw, 21px); line-height:1.45; color:var(--ink-2); }
.ca-art {
  position:relative; aspect-ratio:0.86; overflow:hidden;
  border-radius:clamp(20px, 2vw, 30px);
  background:radial-gradient(120% 130% at 50% 0%, #fdfdfc 0%, var(--card) 62%, #f1f0ed 100%);
  box-shadow:inset 0 0 0 1px var(--hair-soft), var(--sheet);
  display:flex; align-items:center; justify-content:center;
  padding:9%;
}
.card-item.is-dark .ca-art {
  background:radial-gradient(130% 130% at 50% 0%, #232323 0%, var(--dark) 58%, var(--dark-2) 100%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.07), var(--sheet);
  color:#f0f0f0;
}

/* Create */
.ca-make { display:flex; flex-direction:column; gap:9px; width:100%; align-self:flex-end; }
.ca-b {
  align-self:flex-start; max-width:88%; padding:11px 15px; border-radius:17px;
  background:rgba(255,255,255,.1); font-size:clamp(14px, 1.1vw, 17px); line-height:1.35;
}
.ca-b.you { align-self:flex-end; background:#fff; color:var(--ink); }
.ca-field {
  display:flex; align-items:center; gap:8px; margin-top:6px;
  padding:13px 16px; border-radius:999px;
  background:rgba(255,255,255,.08); color:rgba(240,240,240,.55);
  font-size:clamp(14px, 1.05vw, 16px);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.12);
}
.ca-field i { width:1.5px; height:1.1em; background:currentColor; flex:none; }

/* Remix */
.ca-remix { display:flex; flex-direction:column; gap:10px; width:100%; }
.ca-row {
  display:flex; align-items:center; gap:11px;
  padding:11px 13px; border-radius:16px;
  background:#fff; box-shadow:inset 0 0 0 1px var(--hair-soft), 0 3px 10px rgba(25,25,25,.05);
  font-size:clamp(14px, 1.1vw, 17px);
}
.ca-row img { width:34px; height:34px; flex:none; }
.ca-row b { font-weight:500; flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ca-row u { text-decoration:none; font-size:max(14px, .82em); color:var(--ink-3); flex:none; }
.ca-arrow { align-self:center; color:var(--ink-3); font-size:1.3em; line-height:1; }

/* Discover */
.ca-orbs {
  display:grid; grid-template-columns:repeat(4, 1fr); gap:9%;
  width:100%; align-content:center;
}
.ca-orbs img { width:100%; aspect-ratio:1; }

/* ══ the apps, and the prompt that remixes them ═════════════════════════════
   The app on the left, the list on the right, and the sentence that turns the
   one you picked into yours — set at reading size, because copy is the only
   thing anybody can take away from a page. */
.picker {
  display:grid; gap:clamp(24px, 3.4vw, 56px);
  grid-template-columns:minmax(0,1fr);
  margin-top:clamp(28px, 3.6vw, 48px);
  align-items:center;
}
@media (min-width:860px) { .picker { grid-template-columns:minmax(0,.85fr) minmax(0,1fr); } }
.pk-art {
  position:relative; aspect-ratio:0.66; overflow:hidden;
  border-radius:clamp(20px, 2.2vw, 32px);
  background:#eceae6;
  box-shadow:inset 0 0 0 1px var(--hair-soft), var(--sheet);
}
.pk-art img {
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:top center;
  transition:opacity .3s var(--ease);
}
.pk-list { list-style:none; margin:0; padding:0; }
.pk-list button {
  display:flex; align-items:baseline; gap:.4em;
  width:100%; text-align:left; background:none; border:0; cursor:pointer;
  padding:5px 10px; margin-left:-10px; border-radius:10px;
  font-family:var(--face);
  font-size:clamp(22px, 2.5vw, 38px); font-weight:400; letter-spacing:-.03em; line-height:1.16;
  color:var(--ink);
  box-shadow:inset 0 0 0 1px transparent;
  transition:box-shadow .18s var(--ease), color .18s var(--ease);
}
.pk-list span { color:var(--ink-3); }
.pk-list button:hover { color:var(--ink); }
.pk-list li:not(.on) button { color:var(--ink-3); }
.pk-list li:not(.on) span { color:rgba(25,25,25,.24); }
/* the picked one is BOXED, which is the reference's own way of saying which */
.pk-list li.on button { box-shadow:inset 0 0 0 1.5px var(--ink); color:var(--ink); }

.pk-prompt {
  margin:clamp(20px, 2.4vw, 34px) 0 0;
  font-size:clamp(18px, 1.5vw, 24px); line-height:1.45; color:var(--ink-2);
  max-width:34ch;
}
.pk-prompt span::before { content:'“'; }
.pk-prompt span::after { content:'”'; }
.picker .chip { margin-top:18px; }

/* ── the wall's captions ───────────────────────────────────────────────────
   Off by default. Discover is deliberately without copy: the argument is three
   lanes of real apps, and a name and a one-liner under every one turns a wall
   into a directory listing. The orb goes with the name because it belongs to the
   caption, not to the card. `display:none` and not `visibility` — the lane's
   height should come back too. */
html[data-labels="off"] .wmeta { display:none; }

/* ── an expandable section in the dock ───────────────────────────────────
 * The pop's seven numbers, behind their own summary row. ⚠ `.grp` is a BUTTON and
 * the dock's own rules target `#dock > .bar`, so the head takes the row's styling
 * and the body's rows are inside it rather than direct children of the dock. */
/* ⚠ ORDER 1, like every other row. The dock sorts its children — `#dock > .bar`
   and `#dock > .lab` are order 1 so the switch can sit at the foot — and a new
   child with no order defaults to 0, which puts it BEFORE all of them. The pop's
   seven rows rendered in a stack at the top of the window, detached from their own
   summary row halfway down the dock. */
#dock > .grp, #dock > .grp-body { order:1; }
#dock > .grp {
  align-items:center;
  gap:8px;
  cursor:pointer;
  border:0;
  font:inherit;
  color:inherit;
  text-align:left;
}
#dock > .grp .caret {
  width:8px; height:8px;
  border-right:1.6px solid #9a9a9a;
  border-bottom:1.6px solid #9a9a9a;
  transform:rotate(-45deg);
  transition:transform .18s ease;
}
#dock > .grp.is-open .caret { transform:rotate(45deg); }
#dock > .grp-body { display:flex; flex-direction:column; gap:6px; }
#dock > .grp-body[hidden] { display:none; }
/* the group's own rows sit a little in from its head, so the nesting is visible */
#dock > .grp-body > .bar { margin-left:14px; }

/* ── the maker line, on hover ────────────────────────────────────────────
 * The app's own like count and its maker's total, in a gradient at the foot of the
 * shot. ⚠ Inside `.wshot`, which is the element that clips and rounds, so the
 * gradient follows the card's corners instead of squaring them off. */
.wover {
  position:absolute;
  left:0; right:0; bottom:0;
  z-index:2;
  display:flex; align-items:flex-end; justify-content:space-between; gap:8px;
  padding:calc(var(--wtile) * .16) calc(var(--wtile) * .055) calc(var(--wtile) * .045);
  /* the picture going dark at the foot — a caption on a photograph, not a bar
     stuck over it */
  background:linear-gradient(to top,
    rgba(0,0,0,.78) 0%, rgba(0,0,0,.62) 38%, rgba(0,0,0,.28) 70%, rgba(0,0,0,0) 100%);
  color:#fff;
  /* ⚠ scaled off the TILE, not fixed: 11px is right on a 158px card and small on a
     218px one, and the tile size changes at two breakpoints */
  /* ⚠ 14px is this page's own floor for text and it applies here too — a maker line
     is read, not decoration. At .066 of the tile that is 14 at 198 and 14.4 at 218. */
  font-size:max(14px, calc(var(--wtile) * .066));
  line-height:1.1;
  letter-spacing:-.005em;
  opacity:0;
  transform:translateY(6px);
  transition:opacity .18s ease, transform .22s cubic-bezier(.22,.61,.36,1);
  pointer-events:none;
}
.wcard:hover .wover, .wcard:focus-visible .wover { opacity:1; transform:none; }
.wlikes { display:inline-flex; align-items:center; gap:3.5px; font-weight:500; }
.wlikes svg { width:1em; height:1em; fill:currentColor; }
/* the maker's own total is the quieter of the two — it is context, not the count */
.wmaker { opacity:.72; font-weight:500; }
/* ⚠ At the small tile the two lines will not sit side by side, so the maker's
   total gives way rather than wrapping into two rows inside a 12px band. */
@media (max-width:859px) { .wmaker { display:none; } }
@media (prefers-reduced-motion:reduce) { .wover { transition:none; } }

/* ── the wall dims, and the one you are on does not ─────────────────────────
 * ⚠ OPACITY, NOT AN OVERLAY. A scrim over the lanes cannot exempt the card under the
 * pointer — it is one layer over all of them — so each card carries its own dim and
 * hover takes it off. `.lane:hover` alone is not enough either: that would light the
 * whole lane, and what is wanted is the one card.
 * ⚠ The transition is on OPACITY only. `.wshot` already animates transform and
 * box-shadow for the lift on the same hover, and putting `all` here would restart
 * those from the wrong values. */
#discover .wcard {
  opacity:.34;
  transition:opacity .24s cubic-bezier(.22,.61,.36,1);
}
#discover .wcard:hover,
#discover .wcard:focus-visible { opacity:1; }
/* the whole wall comes up a little while a lane is paused under the pointer, so the
   neighbours of the card you are on are readable rather than black */
#discover .lane:hover .wcard { opacity:.5; }
#discover .lane:hover .wcard:hover { opacity:1; }
@media (prefers-reduced-motion:reduce) { #discover .wcard { transition:none; } }

/* ── the hero's QR ───────────────────────────────────────────────────────
 * ⚠ SIDE LAYOUTS ONLY, which is the live site's own rule: it renders the card when
 * `layout !== 'center'` and not on mobile, because a centred lockup has no room
 * beside it. Hidden everywhere else rather than moved. */
.hero-qr { display:none; }
html[data-hero="left"] .hero-qr { display:block; }
@media (max-width:899px) { html[data-hero="left"] .hero-qr { display:none; } }
@media (min-width:900px) {
  /* it belongs to the copy column, under the subhead — its own grid row */
  html[data-hero="left"] .hero > .hero-qr {
    grid-column:1; grid-row:4;
    align-self:start;
    margin-top:clamp(20px, 2.5vh, 36px);
  }
  /* the button follows it */
  html[data-hero="left"] .hero > .cta { grid-row:5; }
  html[data-hero="left"] .hero > .deck-wrap { grid-row:1 / span 6; }
  html[data-hero="left"] .hero { grid-template-rows:1fr auto auto auto auto 1fr; }
}

/* ── the three approved versions ─────────────────────────────────────────
 * ⚠ BOTTOM RIGHT: bottom left is the settings dock and bottom centre is the orb
 * stage's nav, so this is the corner left. Same glass as everything else, one digit
 * per button, and the one that is showing is lit. */
/* the caption sits on the page and the pill under it — see js/app.js */
.presets-wrap {
  position:fixed;
  right:clamp(14px, 2vw, 28px);
  bottom:clamp(14px, 2vw, 28px);
  z-index:41;
  display:flex; flex-direction:column; align-items:flex-end; gap:5px;
  pointer-events:none;
}
.presets-wrap > * { pointer-events:auto; }
.presets {
  display:flex;
  gap:6px;
  padding:6px;
  border-radius:999px;
  background:rgba(255,255,255,.74);
  -webkit-backdrop-filter:blur(22px) saturate(160%);
  backdrop-filter:blur(22px) saturate(160%);
  box-shadow:0 1px 2px rgba(0,0,0,.05), 0 10px 26px rgba(0,0,0,.09),
    inset 0 0 0 1px rgba(255,255,255,.5);
}
.presets button {
  width:32px; height:32px;
  border:0; border-radius:999px;
  background:transparent;
  font-family:var(--face);
  font-size:14px; font-weight:500;
  letter-spacing:0;
  color:#4a4a4a;
  cursor:pointer;
  transition:background-color .16s ease, color .16s ease;
}
.presets button:hover { background:rgba(25,25,25,.06); }
/* the one that is showing — a preset is only "on" while every value it names still
   matches, so nudging any slider turns the light off */
.presets button[aria-pressed="true"] { background:#191919; color:#fff; }
/* ⚠ THE PRESETS SHOW ON A PHONE TOO. They used to be hidden under 520px, which is
   exactly where somebody checking the three approved versions on a phone cannot
   reach them. They only have to share the foot with the Settings pill on the left,
   so on a narrow screen the buttons come down to 28px and the caption goes — the
   numbers are the control and the word is a label for a mouse. */
@media (max-width:520px) {
  .presets-wrap { right:10px; bottom:10px; gap:0; }
  .presets-cap { display:none; }
  .presets { padding:5px; gap:4px; }
  /* 40, not 28: a thumb is the pointer here, and three of these plus the Settings
     pill still leave 140px of clear space at 390 */
  .presets button { width:40px; height:40px; font-size:14px; }
}

/* ── the presets' label ──────────────────────────────────────────────────
 * ⚠ ABOVE the pill, not inside it: the row is three 32px circles and a word among
 * them would either shrink them or stretch the pill into a bar. It sits over the
 * top-right corner of the group, in the same 14px the rest of the chrome uses. */
.presets { flex-wrap:wrap; justify-content:flex-end; }
.presets-cap {
  /* ⚠ no background of its own — it is a word on the page, not a chip. On the
     white footer and on the dark stage both, the page's own ink at 55% reads and
     a second glass surface would not. */
  padding:0 12px 0 0;
  font-family:var(--face);
  font-size:12.5px;
  font-weight:500;
  letter-spacing:-.01em;
  color:#8a8a8a;
  text-align:center;
  pointer-events:none;
}


/* ══ the Discover headline hover ════════════════════════════════════════════
   js/mat.js drives the glyphs; this is the one thing it needs from CSS.
   ⚠ The canvas that used to be here went with the colour: the effect is motion
   now — letters rising toward the pointer — and motion needs no ink of its own.
   ⚠ The glyph is what moves, so it needs to be a box that can take a transform
   and one that does not clip its own descender when it does. `.g` is an `<i>`
   from the Line engine, i.e. inline by default. */
html[data-layout="blocks"] .show .sec h2.line .g,
.show .sec h2.line .g {
  display:inline-block;
  padding-bottom:.2em; margin-bottom:-.2em;
  will-change:transform;
}
.show .sec .fxwrap { position:relative; z-index:1; }

/* ══ the copy fade-in ═══════════════════════════════════════════════════════
   ⚠ PORTED, not approximated: `.reveal` from `Projects/creator program/index.html`
   — opacity, a 24px lift and a 6px BLUR, all three on 0.9s of --ease-golden. The
   blur is the part that makes it read as copy arriving rather than a box sliding;
   drop it and it is a generic fade. Re-port from there rather than nudging these.
   ⚠ On its own element, never on the headline itself: the captions' glyphs are
   driven by js/mat.js, which writes its own transform per glyph, and two owners
   of one transform is a fight. This sits on the wrapper. */
.copy-in { opacity:0; transform:translateY(24px); filter:blur(6px);
  transition:opacity .9s var(--ease-golden), transform .9s var(--ease-golden),
             filter .9s var(--ease-golden); }
.in .copy-in, .copy-in.in { opacity:1; transform:none; filter:blur(0); }
@media (prefers-reduced-motion:reduce) {
  .copy-in { opacity:1; transform:none; filter:none; transition:none; }
}
