@font-face{font-family:'DOUG';src:url('/DOUGTEXT01.otf') format('opentype');font-display:swap}

/* ════════════════════════════════════════════════════════════════════
   TOKENS
   The ten brand tokens below are mirrored from public/vb.css and checked
   for drift by marketing-pages.test.js — change a value here and you must
   change it there too.
   ════════════════════════════════════════════════════════════════════ */
:root{
  color-scheme:dark;
  --black:#0a0908;
  --ink:#f4f0e8;
  --cream:#f3ece0;
  --gold:#f5c742;
  --rec:#d8231d;
  --s1:#efc953; --s2:#d59448; --s3:#c34664; --s4:#9d54e1; --s5:#2050f2;

  --panel:#141210;
  --panel-2:#1c1917;
  --ink-soft:#c6bdae;
  --muted:#8a8073;
  --line:#2c2723;
  --line-2:#3b342d;
  --cream-ink:#1a1613;
  --cream-soft:#4f463c;
  --cream-line:#ded3c1;
  --green:#5a9e3f;

  --display:'DOUG','Avenir Next',-apple-system,sans-serif;
  --body:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,monospace;

  --gut:clamp(20px,5vw,48px);
  --max:1180px;

  /* One easing curve does all the meaningful motion on this page. It pulls
     back before it moves and overshoots before it settles, which is what
     makes a mechanism feel mechanical rather than eased. */
  --lock:cubic-bezier(.86,-.04,.32,1.12);
  /* Radii are deliberately either near-square (tape plastic) or fully round
     (buttons). Nothing sits at the timid 8px default. */
  --r-tape:3px;
  --r-deck:14px;
}

*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
body{background:var(--black);color:var(--ink);font-family:var(--body);
  font-size:17px;line-height:1.6;letter-spacing:-.011em;overflow-x:hidden;-webkit-font-smoothing:antialiased}
::selection{background:var(--gold);color:#000}
img,video{max-width:100%;height:auto;display:block}
a{color:inherit}
:focus-visible{outline:2px solid var(--gold);outline-offset:3px;border-radius:2px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.skip{position:absolute;left:-999px;top:0;z-index:200;background:var(--ink);color:#000;padding:.6rem 1rem;font-size:.85rem}
.skip:focus{left:0;top:0}
/* Scoped to <main> on purpose: the shared nav/CTA/footer partials (outside
   <main>) must keep vb.css's .wrap, or this later rule clobbers .nav-in's
   vertical padding and the menu bar sits higher and wider than on every
   other page. */
main .wrap{max-width:var(--max);margin:0 auto;padding:0 var(--gut)}

/* The technical voice. Every machine-ish string on the page — timecodes,
   counts, section eyebrows — is set in this, so they read as camcorder OSD
   rather than as decoration. */
.lab{font-family:var(--mono);font-size:.66rem;font-weight:600;letter-spacing:.2em;
  text-transform:uppercase;color:var(--muted)}

h1,h2,.display{font-family:var(--display);font-weight:400;letter-spacing:-.014em;line-height:.96}

/* ——— VHS stripe rule: the app's signature ——— */
.stripes{display:flex;height:8px;width:100%;flex:none}
.stripes i{flex:1}
.stripes i:nth-child(1){background:var(--s1)}
.stripes i:nth-child(2){background:var(--s2)}
.stripes i:nth-child(3){background:var(--s3)}
.stripes i:nth-child(4){background:var(--s4)}
.stripes i:nth-child(5){background:var(--s5)}
.topbar{height:6px;position:relative;z-index:60}

/* ——— arrival: the page's one reveal rule ———
   The `rv` class is added by vb-home.js, never written into a shell: the
   eleven language shells must stay element-for-element identical, so motion
   a translator cannot see must not cost them a diff. Because the class only
   ever exists once the script has run, a visitor with no JavaScript sees the
   whole page exactly as before — there is no hidden state to get stuck in.
   Only blocks BELOW the fold at load are ever marked, so nothing the visitor
   is already looking at fades out and back in. */
@media (prefers-reduced-motion:no-preference){
  .rv{opacity:0;transform:translate3d(0,20px,0);
    transition:opacity .6s ease var(--rv-d,0ms),transform .68s var(--lock) var(--rv-d,0ms)}
  .rv.rv-in{opacity:1;transform:none}
}

/* No grain plate and no scanline overlay OVER the page. Both used to cover the
   whole page, which meant they covered every clip too — and the app does NOT
   put a VHS filter on your footage, so showing it that way here sold something
   we don't ship. The VHS language lives in the chrome instead: the stripe
   rules, the deck, the tape spines, the timecode. Footage renders exactly as
   recorded. The site-wide CRT ground (vb.css .vb-crt, 2026-09-14) keeps that
   rule: it sits BEHIND the page at z-index -1, so it shows only through the
   open black and never touches a clip. Do not reintroduce a full-viewport
   overlay above the content. */

/* ════════════════════════════════════════════════════════════════════
   THE OSD — a camcorder viewfinder overlay pinned to the window. #tc shows
   the visitor's own local time, ticking once a second (vb-home.js) — a real
   clock, not a decoration, which is why it keeps running under reduced
   motion.
   ════════════════════════════════════════════════════════════════════ */
.osd{position:fixed;left:0;right:0;bottom:0;height:30px;pointer-events:none;z-index:55;
  /* One child now that the tape-speed/battery cluster on the right is gone —
     left-aligned rather than centred or spread, so REC still reads as
     pinned to the strip's own edge like a real viewfinder readout, not
     stranded in the middle of an otherwise empty bar. */
  display:flex;align-items:center;justify-content:flex-start;gap:1rem;padding:0 var(--gut);
  font-family:var(--mono);font-size:clamp(.56rem,1.4vw,.68rem);letter-spacing:.16em;
  color:rgba(244,240,232,.82);text-shadow:0 1px 3px rgba(0,0,0,.9)}
/* The strip's backdrop fades out upward through a mask rather than ending on a
   hard edge, so it stays legible over both the black page and the cream band
   without ever reading as a bar bolted to the bottom of the window. */
.osd::before{content:"";position:absolute;inset:-30px 0 0 0;z-index:-1;
  background:rgba(10,9,8,.8);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  -webkit-mask-image:linear-gradient(to top,#000 0,#000 46%,rgba(0,0,0,.5) 74%,transparent 100%);
  mask-image:linear-gradient(to top,#000 0,#000 46%,rgba(0,0,0,.5) 74%,transparent 100%)}
.osd b{font-weight:600}
/* #tc: the visitor's own clock (vb-home.js), ticking every second — not the
   page's old scroll-driven timecode. A real clock has to hold its own
   width, or every tick, and every 9-to-10 hour rollover, reads as a glitch.
   tabular-nums (plus font-feature-settings as the belt-and-suspenders
   version some engines still want) is defensive — the font stack is already
   monospace — but the min-width is load-bearing: 12ch covers "10:05:03 AM"
   (11 characters, the longest shape a 12-hour locale produces — some
   engines render a single-digit hour even when 2-digit is requested, which
   is correct for that visitor) plus one character of headroom, since the
   script deliberately does not compose the AM/PM marker itself and some
   engines separate it with a narrow no-break space rather than a plain one,
   which this margin absorbs too. inline-block because min-width does
   nothing on a plain inline element. */
#tc{display:inline-block;min-width:12ch;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
.osd-l{display:flex;align-items:center;gap:.6em}
.osd-lamp{display:inline-block;width:.6em;height:.6em;border-radius:50%;background:var(--rec);box-shadow:0 0 8px rgba(216,35,29,.9)}
@media (prefers-reduced-motion:no-preference){
  .osd-lamp{animation:lamp 2.4s step-end infinite}
  @keyframes lamp{0%,100%{opacity:1}50%{opacity:.2}}
}
/* The clamp's floor is .56rem, and on a phone 1.4vw never reaches it, so the
   timecode rendered at 8.96px — smaller than any text on the page and below
   what anyone can read at arm's length. The floor is the only part that is
   wrong; the desktop end of the clamp is untouched. */
@media (max-width:719px){.osd{font-size:11px}}

/* ——— small-screen hardening ——— */
@media (max-width:479px){
  /* At phone width the REC button takes the full line rather than sitting
     stranded beside a wrapped note. */
  .hero .btn-rec{width:100%}
}
/* Nav, footer and the CTA band are the shared partials styled by vb.css.
   Everything below is homepage-only. Where a homepage class shares a name
   with a vb.css component (.shelf, .band, .btn-rec) the rules here are later
   in the cascade and win; the handful of
   vb.css properties they don't restate are neutralised in the overrides
   block at the end of this sheet. */

/* ════════════════════════════════════════════════════════════════════
   OPENER — the full-screen video open (2026-09-16)
   Real footage, no app in sight, one line, one button. It sits above the
   TapeFlow hero and reuses the reel engine (vb-home.js job 1) rather than a
   new player, so the same ten clips open the page, run the strip and play in
   the deck.

   The section is pulled up by its own negative top margin, sized to the nav's
   MEASURED height (--nav-h, set in the script), so it runs from the very top
   of the document, BEHIND the bar, rather than starting under it. Measured
   and not guessed, because the bar is a different height at every wordmark
   breakpoint (44/40/34/28px). The nav goes transparent for exactly as long as
   this section is on screen; everywhere else on this page, and on every other
   page, it is untouched.
   ════════════════════════════════════════════════════════════════════ */
.opener{position:relative;isolation:isolate;overflow:hidden;color:var(--ink);
  min-height:100vh;
  /* svh, because iOS Safari's 100vh includes chrome it then hides content
     under, and the three lines at the bottom of this section are exactly what
     would go missing on the one device this product is for. */
  /* Minus whatever sits above it: the rainbow topbar always, plus the
     cross-language suggestion bar on a first-time visit from one of the ten
     languages. Without this the section is exactly that many pixels taller
     than the screen, and the three lines along its foot are what falls off
     the bottom. The script measures it; 6px is the topbar alone, which is the
     right answer before the script runs. */
  min-height:calc(100svh - var(--opener-offset,6px));
  margin-top:calc(-1 * var(--nav-h,66px));
  display:flex;flex-direction:column;justify-content:center}
.opener-screen{position:absolute;inset:0;z-index:0}
/* ONE authored reel now, not two stacked <video class="seq"> cross-fading a
   chopped snippet every two seconds (founder: "let's just use heroreel"). A
   crafted 30-second cut does not want to be chopped again, so the whole
   snippet engine went with it and this rule went from a cross-fade between
   two clips to a single element.

   The fade that is left is a fade from STILL to MOTION, not between two
   pictures: heroreel.jpg is the reel's own frame zero, so the poster the
   browser paints before `preload="none"` has fetched a byte is already the
   right image, and the animation below simply brings the whole screen up out
   of the page's black as the opener arrives. Whether the poster or the first
   decoded frame is on screen when it finishes is invisible, which is the
   point.

   It runs on an ANIMATION rather than a class the script toggles, for the
   same reason .opener-body's does: the script's job here is arming and
   pausing the video, and a picture that only appears once JavaScript says so
   is one more way for the first screen to be black. Under reduced motion,
   and with no script at all, the reel is simply there at full opacity. */
.opener-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* The scrim is layered, not a flat tint, because a flat tint dark enough for
   the type flattens the footage into wallpaper and the whole point of this
   screen is that the footage is real. Each layer has one job:
     1. a vignette, darkening the edges without touching the middle;
     2. a vertical gradient, heaviest at the foot where the three facts sit.

   ⚠️ THE BAND BEHIND THE NAV IS NOT HERE ANY MORE — it moved onto the nav
   itself (see `nav.site-nav::before` near the foot of this file). It used to
   be the first layer of this gradient, and that was wrong the moment you
   scrolled: this element is the OPENER's backdrop, pinned to the top of a
   100vh section, while the nav is `position:sticky` and travels down over the
   reel. So the band stayed at the top and the menu ended up over bare bright
   footage about 200px in. A band that has to stay under a sticky element has
   to belong to that element.

   ⚠️ THERE IS NO LONGER A CENTRE WASH, AND THAT IS A FOUNDER DECISION
   (2026-09-17: "remove the darkening around the main letters in the middle.
   i can adjust the video if it's not working"). A pool of shadow behind the
   headline reads as a halo on a bright reel, and they would rather grade the
   FOOTAGE than carry one. So the middle of this screen is now the reel,
   untouched, and **the legibility of the headline and the dek is a property
   of the reel rather than of this stylesheet.**

   That is a real trade and it is theirs to make; the numbers are below so it
   can be checked rather than argued about. Do NOT quietly re-add a centre
   wash because a recut reel measures badly — say so and let them decide,
   because the fix they asked for is on the other side.

   ⚠️ THE MIDDLE IS DELIBERATELY UNCOVERED, and that only works because the
   REEL was graded for it. Both halves of that were founder calls on the same
   day (2026-09-17): first "remove the darkening around the main letters in
   the middle. i can adjust the video if it's not working", then a regraded
   reel, much darker, delivered for exactly that reason.

   The first cut of that reel was bright — sampling the mean luma of the box
   the type sits in (70% x 55%, centred) across its 30 seconds gave
   222 / 200 / 221 / 207 / 146 / 190 / 220 / 191 out of 255. With no centre
   wash the headline sat on 180 luma at 1.82:1 and the dek on 130 at 2.07:1,
   both failing. The reel that replaced it measures 56 / 40 / 64 / 46 / 12 /
   36 / 59 / 35 at the same points, and that is the whole fix.

   Measured on the shipped reel at its BRIGHTEST moment (t=8s), sampled off a
   render with the type hidden so these are the BACKGROUND and not the glyphs:

     headline band   59 luma   9.86:1  (--ink)       wants 3:1, large text
     dek band        30 luma   8.96:1  (--ink-soft)  wants 4.5:1, small text
     three facts      2 luma  18.25:1  (--ink)       layers 2 and 3 cover these

   All three clear comfortably, at the worst frame, with nothing over the
   middle of the picture at all.

   Re-measured on heroreel-v3, the founder's third cut (2026-09-17, 30s),
   every half second across its whole length: worst headline 57 luma at 6.5s
   and worst dek 76 at 7.5s at 1440x900; 79 and 79 at 390x844. Both inside
   the two numbers below, the phone dek only just. Table (every cut) and
   method in docs/LANDING_REEL.md.

   ⚠️ SO THE REEL IS LOAD-BEARING. If it is ever recut brighter, the type goes
   with it, and the two numbers to judge a replacement on are: **about 137
   luma or darker behind the headline for 3:1, and about 80 or darker behind
   the dek for 4.5:1.** Re-measure rather than re-reason — render the page,
   hide the type, sample the band under each piece, and check --ink-soft
   against it, never white. The dek is always the binding case: smallest type,
   lightest colour.

   And if a future reel does measure badly, SAY SO rather than quietly putting
   a centre wash back. A pool of shadow behind the headline reads as a halo,
   which is what the founder asked us to remove; the fix they chose is on the
   footage side, and that choice is theirs to re-make. */
.opener-screen::after{content:"";position:absolute;inset:0;z-index:1;
  background:
    radial-gradient(125% 95% at 50% 40%,rgba(10,9,8,0) 42%,rgba(10,9,8,.52) 100%),
    linear-gradient(180deg,rgba(10,9,8,.22) 0%,rgba(10,9,8,.1) 30%,rgba(10,9,8,.4) 62%,rgba(10,9,8,.92) 100%)}

/* Centred (founder, 2026-09-17), which is also why the scrim's wash moved to
   the middle. */
.opener-body{position:relative;z-index:2;max-width:880px;margin:0 auto;
  padding:0 var(--gut);text-align:center;
  padding-top:calc(var(--nav-h,66px) + clamp(1.2rem,3.4vw,2rem))}
/* Still the largest type on the site, and a step larger again this round
   (founder: "a little larger"). The ceiling is not taste, it is the point
   where "Life is meant to be remembered." stops being two lines, so it was
   measured rather than guessed: rendered in the brand font at each width,
   the largest size that still breaks in two is 56.5px at 480px, 91px at
   768px and 103px from 1024px up. The clamp below sits about a fifth under
   that everywhere (46.1 / 73.7 / 91.2), which leaves room for the fact that
   one engine's shaping is not every engine's. It was 8.4vw with a 5rem cap;
   both ends move up, and 1440px goes from 80px to 91.2px. */
.opener-line{font-family:var(--display);font-weight:400;margin:0 auto;
  font-size:clamp(2.6rem,9.6vw,5.7rem);line-height:.98;letter-spacing:-.01em;max-width:17ch;
  text-shadow:0 2px 24px rgba(0,0,0,.45)}
/* The line under it is four words now ("Capture, Collaborate, Store and
   Share"), and it sets as ONE line at every width the site supports, 320px
   included — so the 33ch measure below no longer decides anything and is
   kept only as the guard it was. It earned that number against a much longer
   line ("Capture, collaborate, store and share your moments. With VideoBro"),
   which at 36ch broke 477/132 and left "With VideoBro" alone on a stub of a
   second line; if a longer line ever comes back, that is why 33 and not 36. */
.opener-dek{margin:clamp(.9rem,2.2vw,1.4rem) auto 0;
  font-size:clamp(1.05rem,2vw,1.35rem);color:var(--ink-soft);max-width:33ch;
  text-shadow:0 1px 10px rgba(0,0,0,.5)}
.opener-body .cta-row{margin-top:clamp(1.4rem,3vw,2rem)}
/* The button and its QR code sit on one centred row (founder: "make sure
   the button the QR code create a centered feel... space remains between
   the two of them if I make the browser narrow"). A flex row rather than a
   grid — there is no third thing to line up against here — with a gap
   that has a real floor rather than collapsing to nothing as the row
   narrows. `.qr-get` removes itself under 720px (vb.css; not duplicated or
   fought here), so below that the button is simply the row's one child and
   still reads as deliberately centred rather than stranded. Takes the
   .cta-row's own top margin so the bare rule above stays exactly as it was
   for the prelaunch branch, which never has an .opener-get wrapper. */
.opener-get{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;
  gap:clamp(1rem,3.2vw,2.2rem);margin-top:clamp(1.4rem,3vw,2rem)}
.opener-get .cta-row,.opener-get .qr-get{margin:0}

/* Three facts along the foot, in thirds: left, centre, right (founder). A grid
   rather than a flex row with separators, so each one owns a third of the
   width and the middle one is genuinely centred on the page rather than
   wherever the first two happened to end.

   Padding is widened on a curve rather than left at the page's ordinary
   --gut (founder: "a little space to the left of collaborate with friends
   and to the right of private by default") — the two outer facts are the
   ones that would otherwise sit flush with the edge the way nothing else on
   this row-of-three does.

   Clear of the window-bottom OSD strip, which is fixed, 30px tall and painted
   over everything: anything closer reads as two overlapping footers. */
.opener-trust{position:absolute;left:0;right:0;z-index:2;
  /* Clear of the fixed 30px OSD strip alone now — the VHS switch that used
     to sit between the two is gone, so this is back to the pre-round-2
     offset rather than the taller stack that switch needed. */
  bottom:clamp(3.4rem,6vw,4.4rem);padding:0 clamp(1.4rem,7vw,5.5rem);
  display:grid;grid-template-columns:repeat(3,1fr);gap:.4rem 1rem;list-style:none}
.opener-trust li{font-family:var(--mono);font-size:.7rem;font-weight:600;
  letter-spacing:.16em;text-transform:uppercase;color:rgba(244,240,232,.9);
  text-shadow:0 1px 5px rgba(0,0,0,.8)}
.opener-trust li:nth-child(2){text-align:center}
.opener-trust li:nth-child(3){text-align:right}

/* The bar sits on footage here, not on the page's own black, so the links are
   a size up and full strength rather than the site's ordinary --ink-soft.
   Homepage only: vb-home.css loads nowhere else, and the shared partial is
   rendered on 27 other pages where the ordinary weight is correct. */
.site-nav .nav-links a,.site-nav .nav-dd>summary{font-size:1.02rem;color:var(--ink)}
html.opener-in .site-nav .nav-links a,
html.opener-in .site-nav .nav-dd>summary{text-shadow:0 1px 6px rgba(0,0,0,.6)}

@media (prefers-reduced-motion:no-preference){
  /* Plays once, on load. This is the first thing on the page, so it is not
     tied to scroll. Without the rule, and under reduced motion, the type and
     the button are simply there from the first frame. */
  @keyframes opener-rise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
  .js .opener-body{animation:opener-rise .9s var(--lock) .15s both}
  .js .opener-trust{animation:opener-rise .9s var(--lock) .4s both}
  /* The reel itself only fades: a full-bleed picture that also slides would
     show a strip of bare page along one edge for the length of the move. */
  @keyframes opener-fade{from{opacity:0}to{opacity:1}}
  .js .opener-vid{animation:opener-fade 1s ease .05s both}
}
@media (max-width:479px){
  /* "remembered." is the word that decides this line: at the floor it is
     tight against a 320px column, so the floor steps down rather than
     trusting the vw term alone at the smallest phones.

     That is why this override grows mostly at its TOP end. Measured in the
     brand font, a 320px phone has 280px of column and the largest size that
     still fits "remembered." on one of two lines is 36.5px — today's 10.5vw
     already spends 33.6 of it, so the vw term moves by a hair (10.9vw, 34.9px
     at 320) and the CAP carries the founder's "a little larger": 2.4rem to
     2.85rem, which is 38.4px to 45.6px from about 420px up, and lands within
     a pixel of what the base clamp gives at 480 so nothing jumps across the
     breakpoint. */
  .opener-line{font-size:clamp(2rem,10.9vw,2.85rem)}
  /* Three thirds do not survive a 320px column: at that width each is about
     93px, which is narrower than "COLLABORATE". One centred column instead. */
  .opener-trust{grid-template-columns:1fr;gap:.35rem;text-align:center}
  .opener-trust li:nth-child(3){text-align:center}
}

/* ════════════════════════════════════════════════════════════════════
   HERO
   One idea, set large, with a viewfinder locking onto signal beside it.
   ════════════════════════════════════════════════════════════════════ */
/* (2026-09-14, round 5) Tightened top and bottom so the clip strip under the
   hero shows in the first screen again ("shift everything up… remove a
   little of the space above 'free on iphone'"). */
/* Roomier than it was (founder: "I want this section to be a bit taller so
   it takes up more space"). The figure below grew at the same time, and the
   two do different halves of that job: the figure raises the floor on how
   tall the row can be, this gives the row air above and below it. */
.hero{padding:clamp(2rem,4vw,3.2rem) 0 clamp(2rem,4vw,3.4rem)}
/* ⚠️ minmax(0,…), NOT a bare `fr`. An `fr` track's implicit minimum is
   min-content, so the widest thing inside a track can OVERRULE its share —
   and the widest thing in the right-hand track is the tape, which grows a
   cell at a time and then rewinds. Measured before this: the columns went
   531/481 with an empty tape and 452/560 with a full one, so the headline,
   the three bullets and the button row reflowed every eleven seconds, and at
   452 the QR code landed 16px on top of the DOWNLOAD button. A proportional
   split has to actually be proportional. */
/* The figure's column is the WIDER of the two now (founder: "make this a
   little larger"). Its size is bounded by this split rather than by its own
   max-width, which is why raising that alone changed nothing: the figure is
   width:100% of whatever this gives it. minmax(0,...) on both, always — an
   `fr` track's implicit minimum is min-content, and that is how the tape
   got a vote on the headline's measure once already. */
.hero-grid{display:grid;grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr);gap:clamp(2rem,5vw,4.5rem);align-items:center}
@media (max-width:940px){.hero-grid{grid-template-columns:1fr;gap:2.6rem}}
.hero-kick{margin-bottom:clamp(.8rem,1.8vw,1.1rem);color:var(--gold)}
/* Three lines by construction: the width admits "EVERYONE'S PHONE." but
   not "ONE TAPE. EVERYONE'S", and the accent line is its own block. */
/* Sized so the three sentences land one per line: "Invite your friends."
   is 7.7em wide in the brand font, and the 1.05fr column at 1000-1380px is
   only ~7.4em of the old 4.5rem cap, which pushed "Invite" onto its own line. */
/* (2026-09-14) The headline is the founder's full sentence now, so it runs a
   size smaller and wider than the three-beat line it replaced: three lines
   at desktop width, not five, and the buttons stay in the first screen.
   (Round 5) The accent line "HOME MOVIE WORTH KEEPING." is ~9.9em in the
   brand font and the text column is ~520-530px from 1100px up (the .wrap
   cap, not the viewport, sets it), so the cap is 2.9rem: one accent line,
   three lines total, where 3.35rem broke it into four. */
/* Sized for the sentence it actually carries. The round-5 headline is about
   a quarter longer than the one it replaced ("VideoBro automatically
   collects and edits your videos into a home movie worth keeping") and its
   column got narrower in the same change, so at the old 2.9rem it came out
   as five lines of display type, which is a wall rather than a headline.
   `balance` rather than `normal` for the same reason: it is worth evening
   the lines when there are four of them. */
h1{font-size:clamp(2rem,3.7vw,2.5rem);max-width:13.5em;text-wrap:balance}
h1 em{font-style:normal;color:var(--gold);display:block}
.dek{margin-top:clamp(1.3rem,3vw,1.9rem);font-size:clamp(1.04rem,1.5vw,1.2rem);
  color:var(--ink-soft);max-width:46ch}
.dek strong{color:var(--ink);font-weight:650}

/* ════════════════════════════════════════════════════════════════════
   TAPEFLOW — the signature visual (v5, 2026-09-17: SIX phones in two rows,
   one per person, colour-coded, feeding a tape that reads as film stock and
   a film that plays inside the site's own TV)

   One scene, looping, wordless (founder: "I want this to just be explained
   visually"). Six landscape phones share the stage — three in a FRONT row
   and three smaller and dimmer BEHIND them — each playing a real clip held
   sideways, the way anyone actually films. Three was read as "some phones";
   six people each holding one is read as a crew, which is the founder's
   "clearer still". A phone's clip slides out of its own screen and shrinks
   into its slot on the tape below, and twice per lap it lands BETWEEN clips
   already there, which is the one thing this picture has to say without a
   word: nobody sorts anything. Each phone owns exactly ONE clip now, so
   nothing reloads mid-scene.

   EACH PHONE IS A PERSON, and its colour says so: every phone carries one of
   the site's five stripe colours (plus --green for the sixth) as a lit edge,
   and the cell that phone sends carries the same colour as a hairline round
   the frame. That is what makes the picture legible rather than busy — you
   can see whose clip is whose, which is what a shared tape actually is. The
   app colours members this way, so this is the product's own language rather
   than an invention. The colour lives on the position class (--tf-who) and
   the script copies it onto the cell, so the palette has one home.

   Once all six have landed, the tape's own clips merge upward into one
   finished film that grows to fill the stage — inside the vintage TV, the
   same object the web player wraps a finished film in (see .tf-film below) —
   then the whole thing rewinds and repeats. The tape itself reads as analog
   film stock, not a plain panel — see the sprocket holes on .tf-track.

   The earlier cut of this scene had a second act — the same phones held
   upright, showing the app, cut to with a real screen recording of the
   walkthrough. That act, its phones and the recording are gone entirely
   (founder: cut the vertical phones; the walkthrough left with them and is
   not relocated).

   The tape is `width:max-content` on purpose: the track is exactly as wide as
   the cells it holds, so its own housing extends as clips arrive. A fixed
   rail with cells filling it would have said "this tape is this long and is
   filling up"; this says "the tape is getting longer", which is the ask.

   Cell width is derived from --cell-h so the CSS and the clip list cannot
   disagree about how wide six 16:9 cells are.
   ════════════════════════════════════════════════════════════════════ */
.tapeflow{--cell-h:clamp(30px,5.2vw,64px);
  /* How many times bigger a FRONT-ROW phone is than a tape cell (the back
     row applies a further multiplier of its own, see .tf-ph2/4/6 below), the
     tilt a landed clip's flight falls back to, and the figure's row gap read
     again by .tf-film below — declared as custom properties, not literals,
     because .tf-hand, .tf-cell and .tf-film each have to agree with the
     others on exactly these numbers.

     4.2 was the number when three phones shared the stage. Six of them have
     to fit three across, so it is re-derived rather than nudged: three front
     bodies at 19%/50%/81% of the stage, each --cell-h × 16/9 × S + two
     bezels wide, sit side by side without eating each other's screens only
     while S is about 1.7 — at 560px (the figure's cap) that is a 171px body
     against 174px of spacing, so they very nearly touch and never cross.
     1.8 was tried and measured first: an 18px overlap at every shoulder,
     which reads as a fanned pile rather than the arranged group asked for. Changing it costs nothing elsewhere: the script
     MEASURES each phone's rendered scale and tilt off its own computed
     transform at drop time, so the flight follows whatever is written here.
     The two constants below survive only as the reduced-motion fallback on
     .tf-cell. */
  --tf-scale:1.7;
  --tf-tilt:-2deg;
  --tf-gap:clamp(.6rem,1.3vw,.95rem);
  /* Anything the RAIL adds between the stage and the tape beyond --tf-gap.
     Zero here; the phone-width block at the foot of this sheet sets it,
     because .tf-film's resting position is measured all the way down to the
     tape's own cells and would otherwise land a rail's margin too high. */
  --tf-rail-extra:0px;
  /* The screen a phone shows and the cell that flies out of it are the same
     16:9 rectangle, so both are sized off these two rather than three copies
     of the same formula (.tf-hand's body and .tf-screen itself read them). */
  --tf-screen-w:calc(var(--cell-h) * 16 / 9 * var(--tf-scale));
  --tf-screen-h:calc(var(--cell-h) * var(--tf-scale));
  /* The FILM has its own scale, and it is bigger than a phone's on purpose.
     Six small phones feed it, so sizing the finished film to one phone's
     screen made the payoff of the whole scene the smallest thing on the
     stage — backwards. It is written as a MULTIPLE of --tf-scale rather than
     as its own number (1.53 × 1.7 = 2.6) so the stage stays a one-knob
     system: the narrow-width rule at the foot of this block steps --tf-scale
     down, and a film scale that did not follow would have overflowed a stage
     that had just shrunk with it.

     1.75 (founder, 2026-09-18: "make the finished version graphic a little
     larger"; it was 1.53, so the TV is 14% wider and taller, 31% more
     picture). The TV's body is its screen plus roughly 29% of that screen's
     height in chrome plus 6px of borders, so it fits the stage while
     1.29 × --cell-h × S × 1.75 + 6 ≤ --cell-h × S × 2.5, i.e. while
     0.2425 × --cell-h × S ≥ 6. The tightest point in the range is the
     --cell-h floor under the narrow-width rule (30 × 1.42 = 42.6), where it
     holds by a factor of 1.7 and leaves about 2px above and below the TV;
     at the cap it is about 10px. About 1.83 is where the TV would touch the
     stage at that floor, so there is not much more to give here without
     growing the stage — which would move the tape and grow the hero.
     Width is never the binding side. */
  --tf-film-scale:calc(var(--tf-scale) * 1.75);
  --tf-film-w:calc(var(--cell-h) * 16 / 9 * var(--tf-film-scale));
  --tf-film-h:calc(var(--cell-h) * var(--tf-film-scale));
  /* Tall enough to hold two rows of three phones AND the finished film's TV
     without any of them reaching the stage's own top or bottom, and built
     from --cell-h alone — never the viewport, never which state the loop is
     in — which is what keeps the figure's box from changing size while it
     animates (see .tf-stage below, and the founder's hard requirement it
     exists to satisfy). The 2.5 is solved, not chosen; the arithmetic is in
     the comment on the position classes below. */
  --tf-stage-h:calc(var(--cell-h) * var(--tf-scale) * 2.5);
  position:relative;width:100%;display:flex;flex-direction:column;align-items:center;
  /* Bigger, and toward the RIGHT of its column (founder: "make this a
     little larger and slide it to the right a bit if we need to make room
     for it"). justify-self:end rather than a margin, so the slide is the
     grid placing it and nothing has to be unwound at the breakpoint where
     the columns stack. */
  gap:var(--tf-gap);justify-self:end;max-width:680px;
  /* Two jobs. It makes the figure a container so the cells can be sized from
     the width the figure was GIVEN rather than from the viewport — six cells
     at the old viewport-derived height wanted 572px inside a 481px column —
     and inline-size containment means the figure's own width no longer
     depends on its contents, which is the second, independent guard against
     the tape voting on the hero's column split (see .hero-grid). */
  container-type:inline-size;
  /* A cell that has not flown yet sits at the phone's scale (--fs, 1.7x) and
     its transform-origin is its own left edge while it is 0 wide, so it
     reaches about two cell-widths to the RIGHT of its slot. It is invisible
     (opacity:0) but it is still in the layout, and measured it was adding up
     to 70px to the document's scroll width at most viewports — a horizontal
     scrollbar on the homepage, from something nobody can see.

     `clip` rather than `hidden`: hidden makes this a scroll container, and a
     figure that can be scrolled sideways by a trackpad is its own bug. The
     flight itself is never clipped, because it runs from the phone (centred
     in this figure) to a slot (inside this figure) and so never leaves the
     box. ⚠️ Do NOT move this to .tf-cell or .tf-track — see the warning on
     .tf-cell.tf-in about overflow not being an animatable property. */
  overflow:clip}
/* The tape is a fixed number of 16:9 cells in a row, so the height each cell
   can be is a consequence of how wide the figure is, not a free choice:
   8px of housing (3px padding and a 1px border on each side), then --tf-n
   cells of (16/9 × height + 3px). Solved for the height, that is the calc
   below. --tf-n is set by the script from the clip list itself, so the CSS
   and the list cannot disagree about how many cells have to fit; the 6 is
   only what a page whose script has not run assumes. 30px is the floor; the
   cap is 64px and why it is that number is on the rule itself, below. */
@supports (width:1cqw){
  /* ⚠️ The 64px ceiling is load-bearing, not a round number. Widening the
     figure alone does nothing once this clamp is pinned: at the old 52px
     cap a 560px figure already solved to 50px, so every pixel of extra
     width past about 578px was thrown away and the scene stayed exactly
     the size it was. The cap and the width have to move together. */
  .tapeflow{--cell-h:clamp(30px, calc(((100cqw - 8px) / var(--tf-n,6) - 3px) * 9 / 16), 64px)}
}

/* The phones and the film sit on top of each other in a stage whose height
   is a pure function of --cell-h — never the viewport, never how many cells
   have landed, never whether the film is showing — so the figure's box
   cannot change size at any point in the loop: empty tape, full tape, film
   shown, rewinding all measure identical at a given viewport width
   (founder: "the graphic is changing size and compressing the stuff to the
   right"). */
.tf-stage{position:relative;width:100%;height:var(--tf-stage-h)}

/* .tf-hands (PLURAL) is the container the script mounts the six phones
   into; .tf-hand (singular, below) is one phone's own body. inset:0 makes
   it exactly the stage's own box, so .tf-ph1…6's percentages (further down)
   resolve against .tf-stage as written, rather than against whichever
   positioned ancestor they happened to land inside.
   ⚠️ Never give this element anything from .tf-hand's own rule. .tf-hand is
   opacity:0 until .tf-on and overflow:hidden; if the container picked up
   either by mistake, all six phones go invisible and clipped with
   nothing obviously broken to find — the failure looks like "the scene
   never arrives," not like a chrome bug. */
.tf-hands{position:absolute;inset:0}

/* ——— the phones: six, held landscape, mid-shot ———
   Each carries the identical .tf-hand body below — a dark shell with a real
   bezel, a speaker slit and a home-indicator bar, like an actual phone held
   sideways — and is placed, tilted, coloured and (for the three back-row
   phones) sized by the position classes further down. A back-row phone is
   shrunk with a plain CSS scale() on the whole body rather than a smaller
   box: scaling the finished shape takes the bezel and the radius down with
   the screen, so a smaller phone still looks like a phone rather than a
   screen in the wrong frame, and it means one real measurement covers both
   jobs — .tf-screen's own bounding box (getBoundingClientRect) already
   reflects a back-row phone's scale-down, so the script reads a single rect
   for wherever a clip should fly FROM rather than hardcoding two sizes.
   ⚠️ That measurement still has to be taken and SET, though — nothing here
   derives a cell's own --fs from --tf-pos-scale automatically. A fixed
   `.tf-cell{--fs:var(--tf-scale)}` is exactly the bug this shipped with
   once already: a clip leaving a .78-scaled back-row phone started its
   flight at a front-row phone's full size, because --fs never varied per
   phone. See the comment on .tf-cell below for where that fallback lives.

   THE BEZEL AND THE RADII ARE PROPORTIONAL NOW, not the fixed 8px and 22px
   the three-phone cut used. Those were drawn against a body 388px wide; the
   same numbers on a 173px body are a slab of plastic with a slot in it. Both
   are written as a share of --tf-screen-h so a phone looks like a phone at
   the --cell-h floor and at its cap, and the bezel keeps a 3px floor so the
   speaker slit and the lamp (which are sized off IT) never run out of room
   to sit in.

   A tenth of the screen's height is deliberately MORE bezel than a real 2026
   phone has. Rendered at 90px tall the honest proportion left a two-pixel
   dark line between the picture and the coloured edge, and six of those read
   as six tinted picture frames rather than six phones. The dark shell has to
   be visibly there for the object to be a phone at all, and at this size
   being recognisable beats being to scale. */
.tf-hand{position:absolute;z-index:2;box-sizing:border-box;
  --tf-bezel:clamp(3px,calc(var(--tf-screen-h) * .1),10px);
  /* The coloured edge is a real border, so it is inside the border-box and
     has to be added to the declared size or it would eat 3px out of the
     screen. --tf-screen-w/h must stay the TRUE size of the picture: the
     script measures the screen, but the cell that flies out of it is sized
     from these. */
  --tf-edge:1.5px;
  width:calc(var(--tf-screen-w) + (var(--tf-bezel) + var(--tf-edge)) * 2);
  height:calc(var(--tf-screen-h) + (var(--tf-bezel) + var(--tf-edge)) * 2);
  padding:var(--tf-bezel);
  /* The home-indicator bar lives in this background (see the chrome comment
     below) instead of as a third pseudo-element — a plain gradient layer
     under the shell's own fill, painted well inside the bezel so .tf-screen
     always covers the rest of it. Four-value position (`right <len> top
     50%`): an edge offset cannot ride in a two-value shorthand slot, and an
     invalid shorthand silently drops the WHOLE declaration, base fill and
     all — the same trap the sprocket holes on .tf-track document. */
  background:
    linear-gradient(var(--ink-soft),var(--ink-soft)) no-repeat
      right calc(var(--tf-bezel) * .28) top 50%/calc(var(--tf-bezel) * .44) 26%,
    #0c0b0a;
  /* WHOSE PHONE THIS IS. The lit edge plus the faint glow of the same colour
     is the whole legibility device: at this size a tinted lamp or a stripe
     inside the bezel would be three pixels of colour, where an edge round
     the entire body reads instantly and still lets the shell stay a dark
     phone rather than becoming a coloured rectangle. --tf-who is declared on
     the position classes below; the fallback is the old plain black edge, so
     a seventh phone with no colour is plain rather than broken. */
  border:var(--tf-edge) solid var(--tf-who,#000);
  border-radius:calc(var(--tf-screen-h) * .15);
  box-shadow:0 0 calc(var(--tf-screen-h) * .13) -3px var(--tf-who,transparent),
    0 18px 40px -16px rgba(0,0,0,.95);
  opacity:0;
  transform:translate(-50%,-50%) rotate(var(--tf-pos-tilt,var(--tf-tilt)))
    scale(calc(var(--tf-pos-scale,1) * .96));
  transition:opacity .5s ease,transform .7s var(--lock)}
.tf-hand.tf-on{opacity:1;
  transform:translate(-50%,-50%) rotate(var(--tf-pos-tilt,var(--tf-tilt)))
    scale(var(--tf-pos-scale,1))}
/* The screen: exactly --tf-screen-w by --tf-screen-h because it is 100% of
   a padding-box already sized to hold precisely that (.tf-hand's own
   width/height, above) — no separate aspect-ratio or calc needed, and
   nothing for the two to disagree about. This, not .tf-hand, is what the
   script measures at drop time: it is the exact picture that flies out. */
.tf-screen{position:relative;width:100%;height:100%;overflow:hidden;
  border-radius:calc(var(--tf-screen-h) * .075);background:#000}
.tf-vid{width:100%;height:100%;object-fit:cover;display:block}
/* The speaker slit sits on one short edge (::before) and the recording lamp
   on the phone's own top bezel (::after — centred, clear of the corner
   radius), still the shared `blink` keyframe from vb.css: the phone is
   mid-shot, which is what makes the clip leaving it read as a clip and not
   as a decoration. The home-indicator bar (the OTHER short edge) is the
   background layer on .tf-hand above rather than a third pseudo-element —
   a real element only ever gets two.

   Both are sized and placed off --tf-bezel rather than in fixed pixels, so
   neither can ever be wider than the bezel it is supposed to sit inside —
   which at the old fixed 3px offsets they would have been, now that a phone
   at the --cell-h floor has a bezel under 4px. The lamp stays RED on every
   phone: it means "this one is filming", which is not the same statement as
   the coloured edge's "this one is Kate's". */
.tf-hand::before{content:"";position:absolute;left:calc(var(--tf-bezel) * .28);top:50%;
  width:calc(var(--tf-bezel) * .44);height:22%;border-radius:2px;
  background:#000;box-shadow:inset 0 0 0 1px rgba(255,255,255,.05);
  transform:translateY(-50%)}
.tf-hand::after{content:"";position:absolute;top:calc(var(--tf-bezel) * .22);left:50%;
  width:calc(var(--tf-bezel) * .56);height:calc(var(--tf-bezel) * .56);border-radius:50%;
  background:var(--rec);box-shadow:0 0 7px rgba(216,35,29,.95);
  transform:translateX(-50%)}
@media (prefers-reduced-motion:no-preference){.tf-hand::after{animation:blink 2.4s step-end infinite}}

/* ——— where the six phones sit ———
   TWO TIDY ROWS OF THREE: a front row across the lower half at 19/50/81% of
   the stage, and behind it a back row at 25/50/75%, smaller (.78), dimmer
   and a touch more tilted, so the group has depth and reads as an arranged
   crew rather than a scatter (founder: "more phones … organized a bit
   more"). Nothing is directly hidden: the rows are separated vertically, so
   a back-row phone shows about four fifths of itself above the front row's
   top edge and the front row only clips its foot, which is the occlusion
   that makes the depth read at all.

   The ROW ORDER ALTERNATES on purpose — ph1 front-left, ph2 back-left, ph3
   front-centre, ph4 back-centre, ph5 front-right, ph6 back-right — because
   the script lights the phones in class order, one every 1.1s. Assigning
   the front row 1-2-3 would fill one row completely and only then start the
   other; alternating means both rows are always populated and the arrivals
   spread across the stage.

   Each class sets FOUR things and nothing else: left/top, --tf-pos-tilt,
   --tf-pos-scale and --tf-who. The shape, chrome and animation mechanics all
   stay on the shared .tf-hand/.tf-hand.tf-on rules above. --tf-pos-scale
   only drives the VISUAL scale(); it is not where a flying cell's own --fs
   comes from (see the warning above and the comment on .tf-cell).

   THE STAGE'S 2.5 MULTIPLIER (declared on .tapeflow, above) IS SOLVED, and
   this is the arithmetic. The binding case is the front row's bottom edge,
   because it is the biggest body sitting nearest the floor. Write S for
   --tf-scale (1.7), b for the bezel-plus-edge, H for the stage height:

     body w = --cell-h × 16/9 × S + 2b        body h = --cell-h × S + 2b
     a body tilted θ occupies  w·sinθ + h·cosθ  of vertical space
     front row fits  ⟺  0.70·H + half of that ≤ H  ⟺  half ≤ 0.30·H

   At the --cell-h FLOOR (30px): b = 5.1+1.5 = 6.6, body 104×64, tilt 2.6°
   → 4.7 + 64.1 = 68.8 tall, half 34.4. H = 30 × 1.7 × 2.5 = 127.5, and
   0.30 × 127.5 = 38.3 ≥ 34.4 ✓ (10% spare). The back row's top edge is the
   mirror case and clears by more, being .78 the size: 38.3 ≥ 27.6 ✓.

   At the --cell-h CAP (52px): b = 8.84+1.5 = 10.3, body 178×109, tilt 2.6°
   → 8.1 + 109 = 117.1 tall, half 58.6. H = 52 × 1.7 × 2.5 = 221, and
   0.30 × 221 = 66.3 ≥ 58.6 ✓ (12% spare — the margin is near enough the
   same at both ends because every term except the bezel's own clamp scales
   with --cell-h, which is the property that makes ONE multiplier correct
   across the whole range). The back row: 66.3 ≥ 47.5 ✓.

   The TV is the other tenant, alone on the stage by the time it shows, and
   since it grew to 1.75 (2026-09-18) it is the TIGHTER of the two: centred,
   its body is --tf-film-h plus its own chrome, 121px at the floor against a
   127.5px stage and 206px at the 52px mark against 221px (see .tf-film and
   the note on --tf-film-scale). Every one of these was then measured in a
   browser rather than left as arithmetic.

   HORIZONTALLY the outer front phones are the tightest, and they are the one
   thing here that wants a guard rather than a number. --cell-h has a 30px
   FLOOR, so below about a 345px figure the phones stop shrinking while the
   stage carries on: measured at 19% of a 280px stage (a 320px phone, the
   narrowest real case) the outer bodies hung a pixel over the edge.
   max()/min() below pin each outer phone's CENTRE no closer to the wall than
   its own half width — computed from the same --tf-screen-w and --tf-bezel
   the body is built from, scaled by --tf-pos-scale, plus what a tilt throws
   out at the corner — so the percentage governs everywhere it is safe and
   the phone's own size takes over exactly where it is not. */
.tf-hand{--tf-half:calc((var(--tf-screen-w) + (var(--tf-bezel) + var(--tf-edge)) * 2)
  * var(--tf-pos-scale,1) / 2 + var(--tf-screen-h) * .06 + 2px)}
.tf-ph1{left:max(19%,var(--tf-half));top:70%;--tf-pos-tilt:-2.6deg;--tf-who:var(--s1)}
.tf-ph2{left:max(25%,var(--tf-half));top:30%;--tf-pos-tilt:-4.4deg;--tf-pos-scale:.78;--tf-who:var(--s3)}
.tf-ph3{left:50%;top:70%;--tf-pos-tilt:-.8deg;--tf-who:var(--s5)}
.tf-ph4{left:50%;top:30%;--tf-pos-tilt:2deg;--tf-pos-scale:.78;--tf-who:var(--s2)}
.tf-ph5{left:min(81%,calc(100% - var(--tf-half)));top:70%;--tf-pos-tilt:2.4deg;--tf-who:var(--s4)}
.tf-ph6{left:min(75%,calc(100% - var(--tf-half)));top:30%;--tf-pos-tilt:4deg;--tf-pos-scale:.78;--tf-who:var(--green)}
/* The back row reads as further away, so it is dimmer as well as smaller —
   a filter rather than opacity, because .tf-hand already animates opacity
   from 0 to 1 on arrival and a second opinion about that property would
   have to fight it. It is scoped to the three back-row classes rather than
   living on .tf-hand with a brightness(1) default, so the front row never
   pays for a compositing layer it does not use. */
.tf-ph2,.tf-ph4,.tf-ph6{filter:brightness(.82) saturate(.94)}

/* ——— the film: the tape's clips, merged, inside the site's own TV ———
   The finished film plays in the VINTAGE TV (founder: "can we have it
   showing inside the graphic that looks like the TV that we have on the
   website?"). That is the object server/public/viewer.html wraps a real
   developed film in, so the picture ends on the same thing a visitor meets
   when somebody sends them a watch link — the grey brushed body, the big
   soft outer radius, a darker inner panel, and the screen sunk into it
   behind an inset shadow.

   It is REDRAWN here, not imported: viewer.html is a standalone player page
   with its own stylesheet, its sizes are absolute (a 900px frame, 20px and
   15px of padding, 30/20/12px radii) and this TV is a 200px object inside a
   hero figure. Every number below is therefore the viewer's PROPORTION
   re-expressed as a share of --tf-screen-h, which is what lets the same
   rules hold at the --cell-h floor and at its cap. Taking a dependency on
   that file instead would couple a marketing page to the player's layout
   for the sake of eight declarations.

   DOM it expects, built by the script:
       #tf-film > .tf-tv-screen > img.tf-frame × 2
   The two frames moved one level deeper this round so the screen could be a
   real inset rectangle rather than the whole object. Nothing else belongs
   in there: a third child would sit on top of the picture.

   SIZE. The screen inside is the same 16:9 rectangle everything else in this
   figure is, at --tf-film-scale (bigger than a phone — see the note on
   .tapeflow). The body is that plus its chrome, and its HEIGHT is computed
   rather than declared: the element's height is auto and the screen's own
   aspect-ratio decides it, so the body can never disagree with the picture
   it holds. The width is written out because an absolutely positioned box
   has no width to inherit.

   REST. It still grows out of the tape: at rest it is scaled by 1 over its
   own --tf-film-scale, which makes its SCREEN exactly one cell wide, and
   parked on the rail. The trip down is half the stage, the figure's
   row gap (plus anything the rail adds, see --tf-rail-extra), half a cell,
   and the track's own 7px vertical padding + 1px border. That last term was
   4px and wrong — it dated from when the track's padding was 3px all round,
   before the sprocket holes needed room. Because the chrome is symmetrical,
   the body's centre IS the screen's centre, so the trip does not have to
   know how thick the chrome is. */
.tf-film{position:absolute;left:50%;top:50%;z-index:1;
  /* The viewer's chrome, measured against its own screen: on a 900px frame
     that screen is 820 x 461, so its 20px outer padding is 4.3% of the
     screen's height, its 15px inner padding 3.3%, and its 30px radius 6.5%.
     The three below are roughly DOUBLE those, on purpose and for the same
     reason the phone bezel above is: at 130px tall, 4.3% is five and a half
     pixels of chrome, and five and a half pixels of grey around a picture is
     a mount, not a television. The RATIOS between the three are what carry
     the resemblance, and those are kept. */
  --tv-pad:calc(var(--tf-film-h) * .085);
  --tv-pad2:calc(var(--tf-film-h) * .06);
  --tv-r:calc(var(--tf-film-h) * .075);
  width:calc(var(--tf-film-w) + (2px + var(--tv-pad) + var(--tv-pad2) + 1px) * 2);
  padding:calc(var(--tv-pad) + var(--tv-pad2) + 1px);
  background:linear-gradient(145deg,#6b6b6b 0%,#4a4a4a 20%,#3d3d3d 50%,#4a4a4a 80%,#5a5a5a 100%);
  border:2px solid #5a5a5a;border-radius:var(--tv-r);
  box-shadow:0 26px 54px -20px rgba(0,0,0,.95),
    inset 0 1px 2px rgba(255,255,255,.12),inset 0 -1px 2px rgba(0,0,0,.35);
  opacity:0;
  transform:translate(-50%,calc(-50% + var(--tf-stage-h)/2 + var(--tf-gap) + var(--tf-rail-extra) + var(--cell-h)/2 + 8px)) scale(calc(1 / var(--tf-film-scale)));
  transition:opacity .6s ease,transform .9s var(--lock)}
.tapeflow.tf-done .tf-film{opacity:1;transform:translate(-50%,-50%) scale(1)}
/* The inner panel (viewer.html's .vintage-tv-inner), drawn as a pseudo
   element rather than a third DOM node the script would have to build. It is
   inset from the PADDING box, which is inside the 2px outer border, and the
   screen's own padding leaves --tv-pad2 of it showing all the way round. */
.tf-film::before{content:"";position:absolute;inset:var(--tv-pad);
  background:linear-gradient(145deg,#3a3a3a,#2a2a2a);
  border:1px solid #333;border-radius:calc(var(--tv-r) * .66)}
/* The one thing this TV has that the player's does not: the five stripe
   colours as a hairline across the lower chrome, where the player puts its
   wordmark. Same rainbow rule as .stripes everywhere else on the page, drawn
   with one gradient rather than five extra elements. It paints after
   ::before, so it sits on the panel rather than under it. */
.tf-film::after{content:"";position:absolute;
  left:calc(var(--tv-pad) + var(--tv-pad2));right:calc(var(--tv-pad) + var(--tv-pad2));
  bottom:calc(var(--tv-pad) * .42);height:2px;border-radius:1px;
  background:linear-gradient(90deg,var(--s1) 0 20%,var(--s2) 20% 40%,var(--s3) 40% 60%,var(--s4) 60% 80%,var(--s5) 80% 100%)}
/* The screen. width:100% of a content box already sized to the picture, and
   its own aspect-ratio rather than a height, so the ratio is stated once and
   the body's height follows from it. z-index lifts it over both pseudo
   elements without needing a stacking context on the body. */
.tf-tv-screen{position:relative;z-index:1;width:100%;aspect-ratio:16/9;
  overflow:hidden;background:#000;border-radius:calc(var(--tv-r) * .45);
  box-shadow:inset 0 0 calc(var(--tf-film-h) * .07) rgba(0,0,0,.6),
    inset 0 0 calc(var(--tf-film-h) * .14) rgba(0,0,0,.25)}
.tf-frame{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .5s ease,filter .6s ease}
.tf-frame.on{opacity:1}
/* THE END CARD — "2 GB free", on the TV's own glass (founder, 2026-09-18:
   "after it's done, have 2 GB Free animate in before the animation
   restarts"). The script builds it inside .tf-tv-screen from the shell's
   data-label-free and turns it on with .tf-offer on the figure, three
   seconds after the film appears; rewind() takes it off with everything
   else.

   It lives ON the screen rather than beside the TV because the screen is
   the one place in the figure guaranteed to be big enough at every width:
   beside a 1.75× TV at a 320px phone there is about 60px of stage left, and
   a badge squeezed into that would set "FREE" at eight pixels. On the glass
   the type is a fixed share of the picture's height (.26), so it scales with
   everything else in the scene. Every locale's phrase is the glossary's
   short two_gb_free term, and text-wrap:balance is there so that a longer
   one would fall to two even lines rather than overflow the glass.

   The film keeps cutting underneath, dimmed, so the words sit on the film
   rather than replacing it. Entrance is a focus pull — soft and small to
   sharp and full size on the house overshoot easing — the camera's own
   gesture rather than a generic fade. */
.tf-free{position:absolute;inset:0;z-index:2;margin:0;max-width:none;
  display:grid;place-items:center;padding:0 7%;
  font-family:var(--display);font-weight:400;font-size:calc(var(--tf-film-h) * .26);
  line-height:1.02;letter-spacing:.04em;text-transform:uppercase;text-align:center;text-wrap:balance;
  color:var(--gold);
  text-shadow:0 0 .45em rgba(245,199,66,.45),0 .06em 0 rgba(0,0,0,.6);
  opacity:0;transform:scale(.7);filter:blur(6px);
  transition:opacity .3s ease,transform .3s ease,filter .3s ease}
.tapeflow.tf-offer .tf-free{opacity:1;transform:none;filter:none;
  transition:opacity .45s ease,transform .7s var(--lock),filter .6s ease}
.tapeflow.tf-offer .tf-frame{filter:brightness(.24) saturate(.6)}
/* The film is clearly the subject once it has grown: the tape's own clips
   dim rather than disappear, so the tape still reads as where it came from. */
.tapeflow.tf-done .tf-cell img{filter:brightness(.55) saturate(.85);transition:filter .6s ease}

/* ——— the tape ——— */
.tf-rail{position:relative;width:100%;display:flex;justify-content:center}
.tf-track{display:flex;align-items:stretch;height:var(--cell-h);
  /* ⚠️ The HORIZONTAL padding must stay 3px: --cell-h above is solved from
     `100cqw - 8px`, and that 8 is this padding plus the 1px border on each
     side. The VERTICAL padding is free, and it is deliberately much larger,
     because it is the film's edge — at 3px there was no room for a
     perforation to read as anything but a hairline, which is how the first
     cut of this looked (measured: a 2px strip inside a 43px tape). */
  /* content-box, against the sheet's own `*{box-sizing:border-box}` reset:
     `height` here has to mean the FRAME's height, not the frame plus its
     film edge. Under border-box the 7px margins ate into --cell-h instead of
     adding to it, so raising the padding silently squashed every frame out
     of 16:9 while the tape stayed exactly as tall as before. */
  box-sizing:content-box;
  width:max-content;max-width:100%;padding:7px 3px;
  /* Sprocket holes, not a plain panel: two rows of perforations painted
     along the top and bottom margins as a repeating gradient rather than as
     elements, so the tape can lengthen forever without the pattern needing
     to know how wide it has become. The pitch is 16px against a cell about
     78px wide, so a frame carries roughly five holes, which is about what
     35mm does. The base is --black rather than --panel-2, a step darker,
     because a hole only reads as punched if the stock around it is darker
     than the light coming through.

     ⚠️ The positions below are the FOUR-value form (`left 0 top 3px`) and
     have to be. An edge offset like `top 3px` is not a legal two-value
     background-position — the horizontal slot cannot hold a vertical
     keyword — so the shorthand was invalid, and an invalid shorthand drops
     the WHOLE declaration, base colour included. The first cut of this
     shipped that way: no perforations, no darker stock, and nothing in the
     console to say so. Computed `background-size` reading `auto` rather
     than `100% 3px` is how to catch it again. */
  background:
    repeating-linear-gradient(90deg,rgba(244,240,232,.3) 0 6px,transparent 6px 16px) left 0 top 3px/100% 3px no-repeat,
    repeating-linear-gradient(90deg,rgba(244,240,232,.3) 0 6px,transparent 6px 16px) left 0 bottom 3px/100% 3px no-repeat,
    var(--black);
  border:1px solid var(--line-2);border-radius:5px;
  box-shadow:0 10px 26px -14px rgba(0,0,0,.9)}
/* Width, not margin, carries the arrival: a cell growing from zero pushes its
   neighbours aside, so room is visibly MADE mid-tape for a clip that belongs
   there.

   The cell starts at the PHONE'S position and the PHONE'S size — --fx/--fy
   (position) and --fr/--fs (tilt/scale) are all measured by the script at
   drop time, now that six phones can each hand a cell off at a different
   size and angle. The values declared here are only the fallback for a
   front-row phone; a clip flying from a smaller back-row phone gets its own
   --fr/--fs set inline, the same way --fx/--fy already are. It shrinks into
   its slot, so it reads as the clip sliding out of whichever phone it came
   from. It used to start small and
   grow, which read as a thumbnail fading in from nowhere and threw away the
   one connection the picture exists to make. `overflow:visible` while it
   travels, or the oversized frame would be clipped to a slot it has not
   reached yet. */
.tf-cell{flex:none;width:0;opacity:0;overflow:visible;z-index:2;
  --fr:var(--tf-tilt);--fs:var(--tf-scale);
  transform:translate(var(--fx,0px),var(--fy,0px)) rotate(var(--fr)) scale(var(--fs));
  transition:width .6s var(--lock),opacity .32s ease,transform .6s var(--lock)}
/* WHOSE CLIP THIS IS — the same colour as the phone that sent it, as a
   hairline round the frame. The script copies --tf-who off the sending
   phone, so the palette stays declared in one place (the position classes
   above) and a cell can never disagree with its phone.

   An OUTLINE with a negative offset, deliberately, and neither of the two
   obvious alternatives: a border would sit inside the border-box and shave
   the frame out of 16:9, and an inset box-shadow paints under the <img>
   that fills the cell, so it would be invisible. An outline paints over the
   content, follows the radius, and costs the layout nothing. It survives
   .tf-done's dimming too (that filter is on the img, not here), so the six
   colours still say who shot what once the film exists. */
.tf-cell>span{display:block;position:relative;height:100%;
  width:calc(var(--cell-h) * 16 / 9);margin:0 1.5px;overflow:hidden;
  border-radius:2px;background:#000;
  outline:max(1px,calc(var(--cell-h) * .028)) solid var(--tf-who,transparent);
  outline-offset:calc(-1 * max(1px,calc(var(--cell-h) * .028)))}
.tf-cell img{width:100%;height:100%;object-fit:cover;display:block}
/* Settled: its own size, in its own slot.

   ⚠️ NO `overflow:hidden` here, and no transition trying to delay one.
   `overflow` is not an animatable property, so a value set on this rule
   applies the instant the class lands — which is the START of the flight, not
   the end. That clipped the oversized travelling frame to the cell's own
   near-zero animating width and turned the whole thing into a sliver. The
   cell stays `overflow:visible`; once settled its span fits exactly, so
   there is nothing to clip anyway. */
.tf-cell.tf-in{width:calc(var(--cell-h) * 16 / 9 + 3px);opacity:1;transform:none;z-index:auto}

/* Below about a 340px window the tape's own --cell-h floor has taken over:
   the cells (and therefore the phones, which are sized off them) stop
   shrinking while the stage carries on narrowing, so three front-row bodies
   start fighting over less spacing than they need. Measured at a 320px
   phone that is 104px of phone into 87px of room, a 23px pile at every
   shoulder. One step down in scale here puts them back to touching, and
   because everything in the figure — the stage's height, the flight's
   fallback, the film's own size — is derived from --tf-scale, this one
   declaration carries all of it.

   It is a deliberately LATE breakpoint. At 375px and up the phones still
   land within a pixel or two of each other at full size, so only the oldest
   and smallest phones ever see the smaller arrangement.

   The line that actually matters, and the one to re-measure if any of these
   numbers move, is that no phone's SCREEN is ever covered. Bodies may graze:
   a body carries a bezel and a coloured edge on each side, so the screens
   are still two lots of that apart when the bodies just touch. Measured, the
   smallest screen-to-screen gap anywhere in the range is 6.7px at 360px, and
   at 320px WITHOUT this rule it would be minus 9.5 — a picture with a corner
   of somebody else's phone parked on it. */
@media (max-width:359px){
  .tapeflow{--tf-scale:1.42}
}

@media (prefers-reduced-motion:no-preference){
  /* The scene arrives once, on load, and is not tied to scroll. */
  .tapeflow{opacity:0;transform:translateY(16px);animation:tf-arrive 1s .15s var(--lock) forwards}
  @keyframes tf-arrive{to{opacity:1;transform:none}}
}
@media (prefers-reduced-motion:reduce){
  .tf-hand,.tf-cell,.tf-film,.tf-frame,.tf-free,.tapeflow.tf-offer .tf-free{transition:none}
}

/* ——— waitlist form ——— */
.cta-row{margin-top:clamp(1.5rem,3vw,2.1rem)}
/* Used as an <a> here and as a <button> on /download, so everything a link
   doesn't inherit from a form control is spelled out. */
.btn-rec{display:inline-block;cursor:pointer;text-decoration:none;text-align:center;
  border:2px solid rgba(255,255,255,.9);background:var(--rec);color:#fff !important;
  font-family:var(--display);font-size:clamp(1.02rem,1.5vw,1.15rem);letter-spacing:.06em;
  padding:.85rem 1.7rem;border-radius:6px;box-shadow:0 3px 0 rgba(0,0,0,.5);
  transition:transform .12s ease,filter .18s ease}
.btn-rec:hover{filter:brightness(1.12)}
.btn-rec:active{transform:translateY(2px);box-shadow:0 1px 0 rgba(0,0,0,.5)}
.btn-rec[disabled]{opacity:.55;cursor:default}
.wl-note{margin-top:.9rem;font-size:.84rem;color:var(--muted)}
@media (prefers-reduced-motion:reduce){.btn-rec:active{transform:none}}

/* ════════════════════════════════════════════════════════════════════
   THE STRIP — ten real clips running edge to edge like tape past a head.
   The premise of the product stated as an image: separate phones, one
   continuous strip. Each tile only plays while it is on screen (see the
   observer at the foot of the page) — ten simultaneous H.264 decodes would
   cook a laptop fan for no benefit.
   ════════════════════════════════════════════════════════════════════ */
.strip-sec{padding:1rem 0 clamp(3.5rem,8vw,6rem)}
.strip-head{display:flex;align-items:center;gap:.9rem;margin-bottom:1rem}
.strip-head h2{font-size:clamp(1.4rem,3vw,2rem)}
.chip{width:46px;height:46px;flex:none;background:none;border:0;padding:0;box-shadow:none}
.chip img{width:100%;height:100%;object-fit:contain}

/* Full-bleed: break the content column so the tape genuinely runs off both
   edges. Anything less reads as a carousel widget. */
/* Full-bleed without 100vw: these are direct children of a full-width
   section, so 100% already is the viewport. 100vw would include the classic
   scrollbar's width and push the page into horizontal overflow. */
.bleed{width:100%}
.strip{position:relative;overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent)}
.strip-track{display:flex;gap:14px;width:max-content;padding:2px 0}
/* The track is built by the script. With no script there is nothing to show,
   so drop the band rather than leave a full-bleed empty gap — the section's
   heading and copy still stand on their own. */
html:not(.js) .strip{display:none}
@media (prefers-reduced-motion:no-preference){
  .strip-track{animation:roll 74s linear infinite}
  .strip:hover .strip-track{animation-play-state:paused}
  @keyframes roll{from{transform:translate3d(0,0,0)}to{transform:translate3d(calc(-50% - 7px),0,0)}}
}
/* Reduced motion: a plain swipeable row rather than a frozen marquee that
   would hide half its contents off-screen. */
@media (prefers-reduced-motion:reduce){
  .strip{overflow-x:auto;-webkit-mask-image:none;mask-image:none}
  .strip-track{animation:none}
  /* The duplicate pass only exists to make the marquee seamless. Stopped, it
     is ten repeated cells — and once they carry `controls` they are ten
     focusable controls inside aria-hidden wrappers. display:none removes them
     from the tab order and from the swipe. */
  .strip-track>.cell[aria-hidden="true"]{display:none}
  /* Movement on hover is the part reduced motion is actually about; colour
     and shadow changes can stay. */
  .spine:hover,.spine:focus-visible{transform:none}
  .pill:active{transform:none}
}
.cell{position:relative;flex:none;width:clamp(210px,26vw,320px);
  background:#000;border:2px solid var(--line-2);border-radius:var(--r-tape);overflow:hidden}
.cell video,.cell img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}
/* Clip credit, styled after the app's own clip rows: who shot it, how long. */
.cell-tag{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:space-between;gap:.6rem;
  padding:.42rem .55rem;background:linear-gradient(transparent,rgba(0,0,0,.85) 55%);
  font-family:var(--mono);font-size:.6rem;letter-spacing:.16em;text-transform:uppercase;color:rgba(244,240,232,.9)}
.cell-tag b{font-weight:600}

/* ════════════════════════════════════════════════════════════════════
   VHS MODE — the payoff section built around the real screen recording of
   the switch. A change of material rather than a second cream band: full-
   bleed panel tone between two black sections, no new choreography.
   ════════════════════════════════════════════════════════════════════ */
.vhs-sec{padding:clamp(4rem,9vw,7rem) 0;border-top:1px solid var(--line);
  background:var(--panel)}
/* Text left, graphic right (founder, 2026-09-17). .vhs-head and .vhs-shot
   are already siblings inside .wrap in that DOM order, so a plain two-column
   grid is enough — nothing here reorders them. minmax(0,1fr) on both
   tracks, never a bare `fr`: an `fr` track's implicit minimum is min-content,
   and the widest thing in the right-hand track is a video at its own natural
   width, exactly the bug .hero-grid's own comment (above) already fixed
   once elsewhere on this page. */
.vhs-sec .wrap{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(2rem,5vw,4rem);align-items:center}
.vhs-head{text-align:left;max-width:44ch;margin:0}
.vhs-head h2{font-size:clamp(1.9rem,6vw,3.6rem);max-width:16ch;margin:.55rem 0 0}
.vhs-head p{margin:1.1rem 0 0;color:var(--ink-soft);font-size:1.03rem}
.vhs-head .flow-more{margin-top:1rem}

.vhs-shot{width:100%;margin:0}
.vhs-shot video{width:100%;display:block;border-radius:var(--r-tape);
  border:1px solid var(--line-2);background:#000;
  box-shadow:0 30px 60px -22px rgba(0,0,0,.7)}
.vhs-shot figcaption{margin-top:.8rem;text-align:center;font-size:.9rem;color:var(--muted)}
/* Back to one column under about 860px — the two-column split gets tight
   before that. The heading stays flush LEFT down here, like every other
   section heading on the page (founder, 2026-09-17: "'flip on the nostalgic
   charm' should be more pinned to the left side like the rest of the
   headers"); this block used to re-centre it, which on a phone made it the
   one heading on the page that wandered off the left edge the others share.
   Only the width changes from the desktop rule: 52ch instead of 44ch.

   minmax(0,1fr), never a bare 1fr, for the reason given above the desktop
   rule — and here it was a live bug, not a precaution. The shot used to be
   min(640px,92vw) wide, and 92vw is wider than the text column on a phone
   (359px against 350 at 390), so the 1fr track grew to fit it and pushed the
   whole section 9px past the page's right gutter. The shot now takes the
   column's width, capped where it always was, so its edges line up with the
   heading's. */
@media (max-width:860px){
  .vhs-sec .wrap{grid-template-columns:minmax(0,1fr)}
  .vhs-head{max-width:52ch}
  .vhs-shot{width:min(640px,100%);margin:clamp(2.4rem,5vw,3.4rem) auto 0}
}

/* ════════════════════════════════════════════════════════════════════
   HOW IT WORKS — eight steps as a native horizontal scroller (grew from
   five, 2026-09-17, moved up the page to sit right after the hero — the
   founder's fuller list: make a tape, ARM, invite friends, film with your
   own camera, +, trim, develop, cabinet). A flex row, overflow-x:auto,
   scroll-snap-type:x mandatory; no JS carousel. The bottom icon row is real
   buttons that scrollIntoView their step and pick up aria-current from an
   IntersectionObserver (vb-home.js); at eight dots it now wraps onto a
   second row on a phone (see .how-nav below). With no JS at all this is
   still a swipeable/dragable row — the buttons still work, they just jump
   instead of gliding.
   ════════════════════════════════════════════════════════════════════ */
.how{padding:clamp(4rem,8.6vw,7.6rem) 0;border-top:1px solid var(--line)}
.how-head{max-width:640px}
.how h2{font-size:clamp(1.9rem,4.4vw,2.9rem);margin-top:.55rem}
.how-head p{margin-top:.9rem;color:var(--ink-soft);max-width:46ch}

.how-track{
  display:flex;
  gap:clamp(1.4rem,3.5vw,2.6rem);
  /* ⚠️ The vh cap is not a second opinion about the same number — it is the
     only term that knows a laptop is SHORT before it is narrow. The founder's
     round-5 ask was that the heading, the step and the five icons read at
     once "on my laptop", and what binds is viewport HEIGHT: measured, the
     section spanned 691px of the 693 a 760px viewport leaves under the sticky
     nav, so it fitted by two pixels and missed by nine the moment the framing
     was off. Everything reclaimed here and on .how-nav is GAP. The picture
     keeps its 50vh, because shrinking the one thing the section exists to
     show would be answering a different question.
     The cap is written so it only ever bites a screen that needs it: 5.5vh
     passes 3.2rem at 931px of viewport, so every taller window renders byte
     for byte what it did before, and there is no breakpoint to cross. */
  margin-top:min(clamp(2.2rem,5vw,3.2rem),5.5vh);
  padding:.4rem 0 min(1.6rem,2.8vh);
  /* Centred, with a REAL gutter either side rather than bled to the
     viewport edge — the founder's "the user should be able to put the
     cursor to the right or left side to scroll past it" only works if
     there is a strip of the section's own background out there for the
     cursor to land on. vb-home.js only captures wheel input over the track
     itself, so this gutter is the escape hatch, not decoration: never let
     the track widen back out to the wrap's edges. */
  max-width:min(980px,calc(100% - 2 * clamp(24px,6vw,80px)));
  margin-inline:auto;
  overflow-x:auto;
  overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  /* A restrained, always-visible scrollbar rather than hiding it: this is
     the ONLY hint on a mouse/trackpad device (no swipe affordance) that
     there's more to the right, and hiding it would leave the edge-fade as
     the sole clue. Thin and low-contrast so it doesn't compete with the
     dots row directly below, which is the real navigation. */
  scrollbar-width:thin;
  scrollbar-color:var(--line-2) transparent;
}
.how-track::-webkit-scrollbar{height:8px}
.how-track::-webkit-scrollbar-track{background:transparent}
.how-track::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:99px}
.how-track::-webkit-scrollbar-thumb:hover{background:var(--muted)}
/* The track is a REAL keyboard control as of round 4 — left and right move a
   step, Home and End jump to the ends — so its focus ring has to say so
   rather than being the polite default this had while it was only a
   scroller. The inset outline is what shows against the step's own picture;
   the soft ring outside it is what makes the whole strip read as the thing
   currently taking key presses, and it has room to show because the track
   keeps a real gutter either side (see the max-width above). */
.how-track:focus-visible{outline:2px solid var(--gold);outline-offset:-2px;
  border-radius:var(--r-tape);box-shadow:0 0 0 5px rgba(245,199,66,.15)}

.how-step{
  /* The site gives every <figure> a 2.6rem block margin, and inside this
     track that is 83px of nothing between the heading and the icon row —
     the track already provides its own padding. It is the single biggest
     reason the section did not fit a laptop screen, and it is invisible:
     every step measured the right height while the row around them was
     83px taller than the tallest of them. */
  margin-block:0;
  /* One step fills the whole track, so exactly one is on screen at a time
     (founder: "let's show only 1 at a time so we can make each blurb and
     screenshot bigger") — the track's own gutters (above) are what keep
     this from ever touching the viewport edge. */
  flex:0 0 100%;
  scroll-snap-align:center;
  display:grid;
  /* Blurb first, screenshot to the RIGHT of it (founder, 2026-09-17). The
     grid areas carry the order, not the DOM, so the markup can keep the
     screenshot first where it belongs for a reader with no stylesheet: the
     picture is what the step is about, and it should not arrive after two
     paragraphs describing it. */
  /* The text column is a fixed SHARE of the step and the pair is centred,
     rather than two intrinsic columns that shrink to their content.

     This changed in round 4 and the reason is worth keeping. `auto auto` let
     each column hug its own words, which was fine while every step carried a
     <p> capped at 44ch: measured at 1440px, seven of the eight text columns
     came out at exactly 496px and the screenshot sat in the same place in
     all of them. Steps 1, 2, 4 and 6 lost their <p> this round, and with only
     a heading to size it the column collapsed to whatever that heading
     happened to measure — 135px for "Tap ARM.", 409px for step 6 — which
     moved the SCREENSHOT 183px sideways between consecutive steps. In a
     one-at-a-time snap carousel that is not variety, it is the picture
     jumping every time you press an arrow key. A share the step owns is
     stable whether the card has one part or two.

     53% is what the 44ch measure was already producing (496 of 924), so the
     seven steps that looked right keep looking exactly as they did. The <p>'s
     own 44ch cap stays: it is a real measure, it simply no longer decides
     the layout. */
  grid-template-columns:minmax(0,53%) auto;
  justify-content:center;
  /* Four rows, two of them empty 1fr spacers, so the blurb is centred against
     the screenshot as a BLOCK. The obvious `align-items:center` does not do
     this: it centres each item inside its own row, and with rows of `auto 1fr`
     the title's row is still pinned to the top. The shot spans all four, so
     its height is what the spacers split. */
  grid-template-rows:1fr auto auto 1fr;
  grid-template-areas:". shot" "title shot" "body shot" ". shot";
  column-gap:clamp(1.6rem,4vw,3rem);
  /* Centred, not top-aligned. A phone screenshot is 760x1650, so at the size
     one-up buys it the picture is ~650px tall against about 120px of words —
     top-aligning left 500px of void under the blurb and read as a layout bug
     rather than as breathing room. */
  align-items:center;
  /* The shot is sized by its HEIGHT, and capped against the viewport, because
     the tall thing here is what decides whether the step and the icon row
     below it can be on screen together. The first term is the old width ramp
     (190px to 300px) expressed as a height, so nothing got smaller on a
     roomy screen; the second is what keeps a short laptop from having to
     scroll to find the navigation. */
  /* ⚠️ The vh term is what makes the whole section fit a laptop at once
     (founder: "make the steps section a little smaller so that the how it
     works text and the 5 scrollable icons at the bottom can all appear at
     once"). The sum that has to clear the viewport is heading 139 + its
     gap 51 + this + the track's own 32 of padding + gap 35 + the icon row
     96, so about 353 plus this number. At 64vh that was 929 against a
     900px window and the icons fell off the bottom; at 50vh it is 803,
     and 783 on a 14-inch MacBook. Raise it and you put them back off.
     (Those three gap figures are the UNCAPPED ones, so the sum is the
     worst case: below about 930px of viewport the vh caps on .how-track
     and .how-nav take roughly 20px more out of it. Measured after both
     changes: 671 of 693 available at 760, 695 of 733 at 800, 730 of 793
     at 860 — counting from the heading down.) */
  --shot-h:min(clamp(360px,44vw,560px),50vh);
}
.how-step picture,.how-step img,.how-step video{
  grid-area:shot;
  height:var(--shot-h);
  width:auto;
  /* The five step recordings are 720x1560; the ratio is declared rather
     than inferred so a card reserves the right space before the video
     has loaded a single byte (they are all preload="none"). */
  aspect-ratio:720/1560;
  object-fit:cover;
  border:3px solid #2f2a26;
  border-radius:16px;
  background:#121212;
  box-shadow:0 14px 32px -18px rgba(0,0,0,.95);
}
/* margin-top:0 is load-bearing, not tidying. vb.css gives every h3 on the
   site `margin-top:2rem`, which inside this grid is 32px of empty track
   INSIDE the title row — so the four-row `1fr auto auto 1fr` centring, which
   is otherwise exact, put the whole text block 16px below the middle of the
   screenshot on all eight steps. It was invisible while every card had the
   same shape and would have stopped being invisible now that four of them are
   heading-only. With the margin gone the title row is exactly the heading's
   own height, the two 1fr spacers split what is left evenly, and a card with
   a heading alone centres the same way a card with a heading and a paragraph
   does — which is the whole ask.

   `align-self:end` went with it. It existed to push the heading to the
   bottom of a row that was taller than the heading; now that the row is the
   heading's height, end and centre are the same place, and a declaration
   that only matters when something else is wrong is worse than none. */
.how-step h3{grid-area:title;font-size:clamp(1.4rem,3vw,2rem);font-weight:650;
  letter-spacing:-.01em;margin-top:0}
.how-step p{grid-area:body;margin-top:.6rem;color:var(--ink-soft);
  font-size:clamp(1.02rem,1.4vw,1.12rem);max-width:44ch}

/* At card width the desktop side-by-side reads cramped and the screenshot
   is the whole point — stack it full-width on top instead, text below. */
@media (max-width:640px){
  .how-step{
    grid-template-columns:1fr;
    grid-template-rows:auto auto auto;
    grid-template-areas:"shot" "title" "body";
    column-gap:0;
    /* Packed to the TOP, and this is the bulk of the fix for "a lot of space
       between the video, the text and the 5 page icons" (founder,
       2026-09-17). Every step in a flex row is as tall as the tallest one —
       step 5, with the longest caption — and a grid given more height than
       its rows need spreads the surplus across the rows, while the desktop
       rule's align-items:center then floats each piece in the middle of its
       swollen row. Measured at 390x664 before this: 66px between step 1's
       video and its heading, a gap above the video too, and the whole
       section 997px tall on a screen with 599px to show it. Packed, the
       video and its words sit together in the same place on every step, and
       whatever a short step has left over falls below its text. */
    align-content:start;
    align-items:start;
  }
  .how-step picture,.how-step img,.how-step video{
    /* Sized by HEIGHT against the screen here, as the desktop rule is, and
       for the same reason: the tall thing decides whether the step and the
       icon row under it fit on screen together. The old rule sized it by
       width (280px), which made it 607px tall, taller on its own than the
       599px a 390x664 phone shows under the nav, so the icons could never
       share a screen with it. At 50svh it is 332px on that phone, and the
       video, its caption and the icons fit in about 575px. The floor keeps
       the app UI in the recording legible on a very short screen; the
       ceiling is roughly the old size, reached only by a phone tall enough to
       spare it.
       svh, not vh or dvh: dvh changes as Safari's toolbar collapses, which
       would resize the video mid-scroll, and svh is the size with the
       toolbar showing, the one the page is actually read at. The vh line is
       the fallback for a browser without the unit. */
    height:clamp(260px,48vh,440px);
    height:clamp(260px,48svh,440px);
    width:auto;
    max-width:100%;
    justify-self:center;
    margin:0;
  }
  /* Tighter steps between the picture, the heading and the caption. */
  .how-step h3{margin-top:.75rem;text-align:center}
  .how-step p{margin-top:.35rem;text-align:center;max-width:44ch;margin-inline:auto;
    font-size:.98rem;line-height:1.5}
  /* The gaps above and below the track come down with it. They are capped
     against viewport height upstairs, but at phone width that cap is not what
     binds: the clamps' own rem floors are.

     The track also gives up its extra side gutter here and takes the full
     text column. That gutter is there for a MOUSE (see the note on the
     desktop rule: the wheel is captured over the track, so the cursor needs
     somewhere else to land to scroll the page), and a phone has no wheel —
     a vertical swipe over the track scrolls the page anyway. The page's own
     .wrap gutter still frames it, so a narrow desktop window keeps a strip
     to scroll from. The 48px it hands back is width for the captions: step
     5's goes from five lines to four, and the tallest caption is what sets
     every step's height and so where the icon row sits. */
  .how-track{margin-top:1rem;padding-bottom:.5rem;max-width:none}
  .how-nav{margin-top:.75rem}
}

.how-nav{
  display:flex;
  justify-content:center;
  /* Eight dots don't fit one row at the floor: at 320px the .wrap gutter
     leaves 280px, and 8 dots at the --dot floor (48px, below) plus 7 gaps
     (8px each at this width) would need 440px. Wrapping is the fix, not a
     smaller floor — 44px+ is the tap-target rule and 48px already clears
     it. Five of the eight fit a 280px row (5 × 48 + 4 × 8 = 272px); the
     other three wrap to a second row, centred the same way flex already
     centres the first. */
  flex-wrap:wrap;
  /* --dot is declared HERE, on the row, and inherits down to the dots. It
     describes a dot's size and .how-dot below is its main reader, but the row
     has to do arithmetic with it too (the cap below), and a custom property
     declared on the child would be undefined up here — which silently voids
     the whole calc() rather than failing loudly. One declaration, one source
     of truth, readable by both. */
  --dot:clamp(48px,9vw,96px);
  gap:var(--dot-gap);
  --dot-gap:clamp(.5rem,2vw,1rem);
  /* Capped against viewport height for the same reason as .how-track's, and
     with the same property: 3.8vh passes 2.2rem at 926px, so a tall window
     is untouched. */
  margin-top:min(clamp(1.6rem,3.5vw,2.2rem),3.8vh);
}
/* Six dots (the Tape Cabinet came back as step 6) fit one row down to a
   380px viewport at the ordinary gaps: the .wrap gutter leaves 340px there,
   and six at the --dot floor plus five 8px gaps need 328px. Below that the
   gaps tighten to 6px, which still fits a 360px phone (318 of the 320px it
   leaves). Narrower than that no single row of six clears the 44px tap
   target, so the row is capped at THREE and wraps three over three — never
   five and a lone sixth dot on a line of its own, the same reason round 4
   capped its eight at four. `flex-wrap` above stays the safety net. */
@media (max-width:379.98px){ .how-nav{--dot-gap:6px} }
@media (max-width:359.98px){
  .how-nav{max-width:calc(3 * var(--dot) + 2 * var(--dot-gap));margin-inline:auto}
}
.how-dot{
  /* At least twice today's 44px (founder: "make the icons at the bottom at
     least twice as big"), with a floor that is a deliberate exception, not
     an oversight: at 320px the .wrap gutter leaves 280px for this row, and
     five dots at the "doubled" 88px plus four gaps would need well over
     400px. 48px is the largest floor five of them still fit inside at
     320px with room to spare (5 × 48 + 4 × 8 = 272px of 280 available);
     the clamp reaches the full doubling-and-past-it 96px by the time the
     row has a desktop's width to spend it in. --dot exists so the padding
     and the ARM badge below can stay exactly proportional to it rather
     than guessing a second clamp into rough agreement with this one. */
  flex:none;
  width:var(--dot);height:var(--dot);
  display:flex;align-items:center;justify-content:center;
  background:var(--panel-2);
  border:1px solid var(--line-2);
  border-radius:50%;
  padding:calc(var(--dot) * .17);
  cursor:pointer;
  transition:transform .3s var(--lock),border-color .2s ease,background .2s ease;
}
.how-dot img{width:100%;height:100%;object-fit:contain}
.how-dot:hover{transform:scale(1.08)}
.how-dot[aria-current="true"]{
  border-color:var(--gold);
  background:var(--panel);
  box-shadow:0 0 0 3px rgba(245,199,66,.22);
}
/* The dots drawn in CSS rather than shipped as an image — the ARM badge,
   the + glyph, the camera and the trim blade below — share this
   fill-the-button sizing with the app's own icon dots (.how-dot img,
   above), so a future one only has to carry .how-dot-ic to match. */
.how-dot-ic{display:flex;align-items:center;justify-content:center;width:100%;height:100%}
/* Step 2's dot (tap ARM): the same red-dot/ARM badge used elsewhere for the
   literal button name. */
.how-dot-arm{gap:.25em;font-family:var(--mono);font-weight:700;
  font-size:clamp(.7rem,1.6vw,.95rem);letter-spacing:.05em;color:var(--ink)}
.how-dot-arm i{display:block;width:calc(var(--dot) * .15);height:calc(var(--dot) * .15);
  border-radius:50%;background:var(--rec);box-shadow:0 0 0 2px rgba(224,60,44,.28)}
/* Step 5's dot (missed one? tap +): drawn inline rather than shipped as an
   asset — the same reasoning as the ARM badge, and in currentColor so it
   never needs its own dark/light asset. */
.how-dot-plus{color:var(--ink)}
.how-dot-plus svg{width:100%;height:100%;display:block}

@media (prefers-reduced-motion:no-preference){
  .how-track{scroll-behavior:smooth}
}
/* Reduced motion: scroll-snap's own jump is instant already (it isn't an
   animation), so nothing here needs disabling beyond smooth-scroll above —
   this rule exists to say so explicitly rather than leave it implicit. */
@media (prefers-reduced-motion:reduce){
  .how-dot{transition:none}
  .how-dot:hover{transform:none}
}

/* ════════════════════════════════════════════════════════════════════
   ONE PHONE OR MANY (2026-09-16)
   Two equal cards, wired into a single point, because the page read as a
   group app and a person filming their own kid's season was bouncing.

   The join is drawn inside the two glyphs (see .solo-ic below). Nothing here
   is a CSS `content` string with words in it — every line a translator needs
   to read stays in the eleven HTML shells.
   ════════════════════════════════════════════════════════════════════ */
.solo{padding:clamp(3.4rem,7vw,6rem) 0;border-top:1px solid var(--line)}
.solo-head{max-width:540px;margin:0 auto;text-align:center}
/* No top margin any more: the .55rem existed to separate this heading from
   the "Two ways in" eyebrow above it, and that line is gone (founder). The
   section's own padding is now the only thing setting the space above it,
   which is what every other section here already does. */
.solo-head h2{font-size:clamp(1.9rem,4.4vw,2.9rem);margin-top:0}
.solo-head p{margin-top:.9rem;color:var(--ink-soft)}

.solo-fork{position:relative;display:grid;grid-template-columns:1fr 1fr;
  gap:clamp(1.2rem,3vw,2rem);max-width:760px;margin:clamp(2.2rem,4.5vw,3rem) auto 0;
  padding-bottom:clamp(1rem,2.2vw,1.4rem)}
.solo-path{background:var(--panel);border:1px solid var(--line);border-radius:14px;
  padding:clamp(1.5rem,3vw,2rem) clamp(1.2rem,3vw,1.7rem);text-align:center}
.solo-path h3{font-size:1.05rem;margin-bottom:.4rem}
.solo-path h3 a{color:inherit;text-decoration:none}
.solo-path h3 a:hover{color:var(--gold)}
.solo-path p{font-size:.92rem;color:var(--ink-soft);max-width:34ch;margin:0 auto}

/* The glyphs are inline SVG (2026-09-16). CSS pseudo-elements cannot draw
   eight lines converging on one point, which is the entire point of the
   right-hand card. Both SVGs share one viewBox and end their lines at the
   same point (120, 100 of 240 × 104), so scaling them to the same container
   width lands both hand-offs at the same y with no fudge factor.

   What they hand off TO is a real photograph now, not a drawn rectangle:
   create-tape-dialog.png, the orange NEW TAPE cassette the app itself puts
   on screen (founder). The bottom margin came down from 1.15rem with it,
   because the PNG carries about an eighth of its own height as transparency
   BELOW the cassette and that skirt is now doing most of the job the margin
   used to. What is left here is the part that is still a design decision. */
.solo-ic{display:block;width:100%;max-width:264px;margin:0 auto .5rem}
/* Same trap as .how-step h3 (see the note there): vb.css gives every h3 on
   the site `margin-top:2rem`, and 32px of it was silently standing between
   this glyph and its heading. Leaving it would have meant the real spacing
   was the sum of a margin declared here and one declared in another
   stylesheet for another reason — so the margin above is the whole answer
   now, and it is the only number to change if this wants more air. */
.solo-path h3{margin-top:0}
.solo-svg{display:block;width:100%;height:auto}
/* The cassette the lines flow into.

   SIZE: 53% of the glyph's width, capped at 140px, which is what makes the
   VISIBLE cassette the same size the drawn one was. The artwork fills about
   86% of its square canvas, and the drawn cassette was 110 units of a
   240-unit viewBox (45.8%), so 45.8 / 0.86 ≈ 53. Sizing it at the drawn
   shape's own 46% would have shipped a noticeably smaller tape.

   POSITION: the negative top margin closes the join, and it is two terms
   because there are two gaps to take out. The artwork leaves 8.5% of its
   height clear above the cassette's lid, which is --st-w × .085 once the
   image is square; and the SVG's own box runs 4 units past where the lines
   stop, which is 1.6% of the container's width. Percentage margins resolve
   against the containing block's WIDTH even vertically, so both terms scale
   with the card and the line meets the lid at every size.

   ⚠️ The 1.6% is read off the shell's `viewBox="0 0 240 104"` with its lines
   ending at y=100. Change that viewBox and this number is wrong — the
   symptom is a visible gap between the last line and the cassette, or the
   line disappearing behind it. */
.solo-tape{--st-w:min(140px,53%);
  display:block;width:var(--st-w);height:auto;
  margin:calc(var(--st-w) * -.085 - 1.6%) auto 0}

@media (prefers-reduced-motion:no-preference){
  /* The lines draw themselves in when the card arrives. stroke-dasharray and
     stroke-dashoffset are inherited SVG properties, so setting them on the
     single path and on the <g> of eight is enough for every path inside to
     run its own.

     ⚠️ The hidden state is scoped to `.rv`, the class the reveal system puts
     on a card it has decided to animate — NOT to `.js`. The reveal
     deliberately skips anything already on screen at load, so a card that is
     above the fold never receives `.rv-in`; keying the hidden state on `.js`
     alone left those cards with permanently invisible lines, which is most of
     the picture. A card that is never marked keeps its finished lines, which
     is also what a no-JS or reduced-motion visitor gets. 200 is longer than
     any path here, so offset 200 hides completely and 0 is the whole line. */
  .js .solo-path.rv .solo-line,
  .js .solo-path.rv .solo-lines{stroke-dasharray:200;stroke-dashoffset:200;
    transition:stroke-dashoffset 1.1s ease-out}
  .js .solo-path.rv.rv-in .solo-line,
  .js .solo-path.rv.rv-in .solo-lines{stroke-dashoffset:0}
}

/* Stacked, the elbow bracket is meaningless: two columns are what it assumes.
   One straight trunk through the row gap says the same thing the way a single
   column actually reads. It runs behind the cards, so the row gap is the only
   place it shows.

   `content-box` (2026-09-18) keeps it OUT of the fork's padding-bottom. Sized
   against the padding box, as it was, the trunk ran on through that padding
   and hung a short stray line off the bottom of "Make one together", pointing
   at nothing (founder: "it shouldn't be there"). The padding itself stays, so
   the spacing below the cards is unchanged. */
@media (max-width:600px){
  .solo-fork{grid-template-columns:1fr;max-width:360px;
    background:linear-gradient(var(--line-2),var(--line-2)) no-repeat 50% 0/2px 100% content-box}
}

/* ════════════════════════════════════════════════════════════════════
   NO WORK (2026-09-16, reimagined again 2026-09-17) — the founder's words:
   "the idea is there's no effort. no scrapbooking, no random clips all over
   the place." The corkboard cut said the opposite of what it meant: nine
   crooked, taped-down scraps read as effort no matter how calm the card
   beside them was. This cut says the same thing with four quiet lines,
   struck through once and left alone, beside the one card that answers
   them — the price of the old way, named and crossed out, not restaged as
   a pile to sort through.

   Not a checklist: a checklist compares features side by side, and what is
   being compared here is WEIGHT. A few muted, struck lines on one side, the
   cream card's full colour and size on the other, so the eye lands on the
   answer rather than on the list of what it replaced.

   The cream card is the page's second "step into paper" move (see the CREAM
   BAND banner below). Here it is one lit rectangle inside a dark section
   rather than a full-width band, so the relief reads as a break in the room
   rather than a change of page.
   ════════════════════════════════════════════════════════════════════ */
.nowork{padding:clamp(4rem,8.6vw,7.6rem) 0;border-top:1px solid var(--line)}
.nowork-head{text-align:center;margin:0 auto clamp(2.4rem,5vw,3.6rem);max-width:32ch}
.nowork-head h2{font-size:clamp(2.1rem,5.6vw,3.6rem);margin:.55rem auto 0}

.nowork-scene{position:relative;display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);
  gap:0 clamp(2rem,5vw,3.5rem);align-items:center;max-width:860px;margin:0 auto}
.nowork-scene ul{list-style:none;margin:0;padding:0}
.nowork-tag{position:relative;z-index:2;font-family:var(--mono);font-size:.62rem;
  font-weight:600;letter-spacing:.18em;text-transform:uppercase;margin-bottom:.9rem}

/* ——— the skip list: four lines, quietly struck through. The strike-through
   IS the idea, made once — no board, no tape, no pile of scraps standing in
   for the work, just the four things you don't do, named and crossed out,
   sized to read as a list beside the card rather than a block competing
   with it. ——— */
.nowork-skip{display:flex;flex-direction:column;gap:.7rem}
.nowork-skip li{font-size:clamp(.95rem,1.6vw,1.05rem);line-height:1.5;
  color:var(--muted);text-decoration:line-through;
  text-decoration-color:var(--line-2);text-decoration-thickness:1px}

/* ——— VideoBro: two words and all the air in the world ——— */
.nowork-new{background:var(--cream);color:var(--cream-ink);border-radius:var(--r-deck);
  padding:clamp(2.2rem,5vw,3rem) clamp(1.6rem,4vw,2.2rem);text-align:center;
  box-shadow:0 30px 60px -22px rgba(0,0,0,.55)}
.nowork-new .nowork-tag{color:var(--cream-soft)}
.nowork-new ul{display:flex;flex-direction:column;align-items:center;gap:.25rem}
.nowork-new li{font-family:var(--display);font-weight:400;letter-spacing:-.01em;line-height:1.1}
.nowork-new li:nth-child(1){font-size:clamp(1.25rem,2.8vw,1.6rem);color:var(--cream-soft)}
.nowork-new li:nth-child(2){font-size:clamp(2.6rem,7vw,3.8rem)}

/* Stacked, the story still reads top to bottom: the skip list first (the
   price you are not paying), the card below it (the answer) — plain DOM
   order, the same way the two-column version read left to right. */
@media (max-width:719px){
  .nowork-scene{grid-template-columns:1fr;gap:1.75rem 0;max-width:420px}
  .nowork-skip{text-align:center}
}

/* prefers-reduced-motion needs no rule here: nothing in this block moves on
   its own. The skip list rides along with its one reveal (vb-home.js job 8,
   `.nowork-skip,.nowork-new`) rather than animating independently. */

/* ════════════════════════════════════════════════════════════════════
   CREAM BAND — a hard cut to paper, the one place the page stops being a
   screen. It tucks under the section above by a rounded lip rather than
   butting against it.
   ════════════════════════════════════════════════════════════════════ */
.band{background:var(--cream);color:var(--cream-ink);position:relative;z-index:52;
  margin-top:-26px;border-radius:26px 26px 0 0}
.band .wrap{padding-top:clamp(4rem,9vw,7rem);padding-bottom:clamp(4rem,9vw,7rem)}
.band h2{font-size:clamp(2.3rem,7vw,4.6rem);max-width:13ch;margin-top:.55rem}
.band-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(2rem,5vw,4.5rem);
  margin-top:clamp(2rem,4vw,3rem);align-items:start}
@media (max-width:820px){.band-grid{grid-template-columns:1fr}}
.band-copy p{max-width:44ch;color:var(--cream-ink);font-size:1.04rem}
.band-copy p+p{margin-top:1rem;color:var(--cream-soft)}
.band-points{list-style:none}
.band-points li{border-top:1px solid var(--cream-line);padding:1.05rem 0;font-size:.96rem;color:var(--cream-soft)}
.band-points li:last-child{border-bottom:1px solid var(--cream-line)}
.band-points strong{color:var(--cream-ink);font-weight:650}
.band .lab{color:var(--cream-soft)}
.band :focus-visible{outline-color:var(--cream-ink)}

/* ════════════════════════════════════════════════════════════════════
   THE SHELF — occasions as labelled tape spines rather than feature cards.
   Same information, in the object the product is about; hovering pulls one
   out of the cabinet.
   ════════════════════════════════════════════════════════════════════ */
.shelf-sec{padding:clamp(4rem,8.6vw,7.6rem) 0}
/* Two columns: the spines (each one a link to its occasion page) on the
   left, the Cabinet as it looks on the web on the right. */
.shelf-sec .wrap{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:clamp(1.2rem,3vw,2rem) clamp(1.6rem,4vw,3.4rem);align-items:start}
.shelf-sec .shelf-head{grid-column:1 / -1;margin-bottom:0}
.shelf-sec .shelf{grid-column:1;margin:0}
.shelf-shot{grid-column:2;grid-row:2}
.shelf-shot img{border-radius:12px}
.shelf-sec .shelf-note{grid-column:1 / -1;margin-top:0}
/* minmax(0,1fr), never a bare `1fr`: an `fr` track's implicit minimum is
   min-content, and a tape spine's min-content is about 347px (two fixed
   flex caps plus the label's padding). Measured at a 320px viewport that
   made this section's single column 347px wide inside a 280px content box,
   which is a 47px horizontal scrollbar on the whole homepage. Same trap,
   same fix as .hero-grid above. */
@media (max-width:900px){.shelf-sec .wrap{grid-template-columns:minmax(0,1fr)}.shelf-shot{grid-column:1;grid-row:auto}}
.shelf-head{display:flex;align-items:flex-end;justify-content:space-between;gap:1.6rem;
  flex-wrap:wrap;margin-bottom:clamp(1.6rem,4vw,2.4rem)}
.shelf-title{display:flex;align-items:center;gap:.9rem}
.shelf-head h2{font-size:clamp(1.9rem,4.4vw,2.9rem)}
.shelf-head>p{color:var(--ink-soft);max-width:42ch}
.shelf{list-style:none;border:2px solid var(--line-2);border-radius:8px;background:#100e0d;
  padding:12px;display:flex;flex-direction:column;gap:9px}
/* One spine per kind of tape, drawn the way the app draws an Active Tape:
   the rainbow end, a white label in the handwriting face, a coloured hub end. */
.spine{position:relative;text-decoration:none;display:flex;align-items:stretch;
  height:clamp(64px,8.5vw,84px);background:linear-gradient(180deg,#2c2926,#1b1918);
  border:2px solid var(--black);border-radius:6px;overflow:hidden;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 2px 0 rgba(0,0,0,.65);
  transition:transform .26s var(--lock),box-shadow .26s ease}
.spine:hover,.spine:focus-visible{transform:translateX(clamp(10px,2.5vw,26px));
  box-shadow:-9px 0 24px -8px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.07)}
.spine-cap{flex:0 0 clamp(40px,6vw,64px);margin:6px 0 6px 6px;border-radius:3px;
  background:linear-gradient(90deg,var(--s5) 0 20%,var(--s2) 20% 40%,var(--s3) 40% 60%,var(--s4) 60% 80%,var(--green) 80% 100%)}
.spine-label{flex:1;min-width:0;margin:6px 8px;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:.1rem;
  padding:0 clamp(.8rem,2vw,1.2rem);background:#f6f3ea;border-radius:3px;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}
.spine-label span{max-width:100%;font-family:var(--display);font-size:clamp(1.05rem,2.6vw,1.45rem);color:#171310;
  letter-spacing:.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.05}
.spine-label small{max-width:100%;font-family:var(--mono);font-size:.58rem;
  letter-spacing:.12em;text-transform:uppercase;color:#6d6659;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
@media (max-width:640px){.spine-label small{display:none}}
.spine-block{flex:0 0 clamp(52px,7vw,80px);position:relative;margin:6px 6px 6px 0;border-radius:3px;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.28)}
.spine-block::after{content:"";position:absolute;top:50%;left:50%;width:14px;height:14px;
  transform:translate(-50%,-50%);border-radius:50%;background:#fff;box-shadow:0 0 0 3px rgba(0,0,0,.25)}
.shelf li:nth-child(1) .spine-block{background:var(--s5)}
.shelf li:nth-child(2) .spine-block{background:var(--s2)}
.shelf li:nth-child(3) .spine-block{background:var(--s3)}
.shelf li:nth-child(4) .spine-block{background:var(--s1)}
.shelf li:nth-child(5) .spine-block{background:var(--s4)}
.shelf li:nth-child(6) .spine-block{background:var(--green)}
.shelf-note{margin-top:1.1rem;font-size:.86rem;color:var(--muted)}

/* ════════════════════════════════════════════════════════════════════
   THE DETAILS — price posture and the FAQ, one section rather than two.
   ════════════════════════════════════════════════════════════════════ */
.details{padding:clamp(4rem,8.6vw,7.6rem) 0;border-top:1px solid var(--line)}
.det-grid{display:grid;grid-template-columns:.85fr 1.15fr;gap:clamp(2.2rem,6vw,5rem);align-items:start}
@media (max-width:940px){.det-grid{grid-template-columns:1fr;gap:2.4rem}}
.det-side h2{font-size:clamp(1.8rem,4vw,2.6rem);margin-top:.55rem}
.cost-points{list-style:none;margin-top:1.4rem}
.cost-points li{border-top:1px solid var(--line);padding:.95rem 0;font-size:.95rem;color:var(--ink-soft)}
.cost-points li:last-child{border-bottom:1px solid var(--line)}
.cost-points strong{color:var(--ink);font-weight:650}
.faq-item{border-bottom:1px solid var(--line)}
.faq-item:first-of-type{border-top:1px solid var(--line)}
.faq-item summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;
  align-items:baseline;gap:1rem;padding:1rem 0;font-size:1rem;font-weight:600;letter-spacing:-.008em}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary::after{content:"+";flex:none;color:var(--gold);font-size:1.15rem;line-height:1;
  transition:transform .22s ease}
.faq-item[open] summary::after{transform:rotate(45deg)}
.faq-item p{padding:0 0 1.15rem;font-size:.93rem;color:var(--ink-soft);max-width:60ch}

/* ════════════════════════════════════════════════════════════════════
   PRODUCT SECTIONS — the substantive middle of the site, on the homepage in
   brief: the chronology diagram, the four feature pages, watching without
   the app. Components come from vb.css; only the framing is set here.
   ════════════════════════════════════════════════════════════════════ */
.prod{padding:clamp(4rem,8.6vw,7.6rem) 0;border-top:1px solid var(--line)}
.prod-grid{display:grid;grid-template-columns:1fr 1.65fr;gap:clamp(2rem,6vw,5rem);align-items:start}
@media (max-width:900px){.prod-grid{grid-template-columns:1fr;gap:2rem}}
.prod h2{font-size:clamp(1.9rem,4.4vw,2.9rem);margin-top:.55rem;max-width:14ch}
.prod-head p{margin-top:.9rem;color:var(--ink-soft);max-width:34ch}
.prod-head .flow-more{margin-top:1rem}
.prod .index{list-style:none;counter-reset:pf;margin:0;
  display:grid;grid-template-columns:repeat(2,1fr);grid-auto-flow:column;
  grid-template-rows:repeat(5,auto);column-gap:clamp(2rem,4vw,3rem)}
.prod .index li{counter-increment:pf;position:relative;
  padding:1.05rem 0 1.05rem 1.15rem;border-top:1px solid var(--line)}
.prod .index li:nth-child(1),.prod .index li:nth-child(6){border-top:none;padding-top:0}
.prod .index li::before{content:"";position:absolute;left:0;top:1.4rem;
  width:8px;height:8px;border-radius:2px;background:var(--s1)}
.prod .index li:nth-child(5n+2)::before{background:var(--s2)}
.prod .index li:nth-child(5n+3)::before{background:var(--s3)}
.prod .index li:nth-child(5n+4)::before{background:var(--s4)}
.prod .index li:nth-child(5n+5)::before{background:var(--s5)}
.prod .index h3{font-size:1rem;font-weight:650;letter-spacing:-.008em;margin-top:0}
.prod .index h3::before{counter-increment:none;content:counter(pf,decimal-leading-zero);
  font-family:var(--mono);font-size:.7rem;letter-spacing:.16em;color:var(--gold);
  font-weight:600;margin-right:.6em}
.prod .index h3 a{color:var(--ink);text-decoration:none}
.prod .index h3 a:hover{color:var(--gold)}
.prod .index p{margin-top:.35rem;font-size:.9rem;color:var(--ink-soft);max-width:34ch}
@media (prefers-reduced-motion:no-preference){
  .prod .index li::before{transition:transform .25s var(--lock)}
  .prod .index li:hover::before{transform:scale(1.4)}
}
@media (max-width:700px){
  .prod .index{grid-template-columns:1fr;grid-template-rows:none;grid-auto-flow:row}
  .prod .index li:nth-child(6){border-top:1px solid var(--line);padding-top:1.05rem}
}
/* Head on the left, the graphic on the right, in the same 1fr / 1.65fr
   split the chronology diagram used to occupy (founder call, 2026-09-01).
   The head keeps the ordinary .prod-head measure; the image keeps its own
   aspect ratio at the column's width. */
.watch{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(2rem,5vw,4.5rem);align-items:center}
@media (max-width:900px){.watch{grid-template-columns:1fr}}
.watch p{color:var(--ink-soft);max-width:48ch;margin-top:.9rem}
.watch p strong{color:var(--ink)}
.watch .flow-more{margin-top:1rem}

/* ════════════════════════════════════════════════════════════════════
   DOWNLOAD BUTTON SHEEN (2026-09-16) — founder: "add a shimmer over the
   download buttons (include the one in the top right)."

   `.pill` is the nav's store button and lives in the SHARED partial rendered
   on 27 other pages; `.btn-rec` is the shared design-system CTA. This file
   loads only on the homepage, after vb.css, so writing the rule HERE is what
   scopes the sheen to this page alone, with no extra selector. Same logic as
   the collision overrides below.

   The sweep is one ::after given `z-index:-1` inside an `isolation:isolate`
   context, which puts it ABOVE the button's own fill and BELOW its label BY
   CONSTRUCTION rather than by picking an opacity the text can survive. That
   distinction matters: a sheen tuned to be faint enough not to hurt the
   label is one type-size change away from hurting it. `overflow:hidden`
   clips the band to the rounded corners and does not touch box-shadow, which
   paints outside the element's box, so the :active press is unaffected.

   ~1.1s of sweep inside a 7s loop: long enough to read as the button
   catching the light once in a while, not as an animation asking for
   attention. All of it, including the geometry, lives inside the
   no-preference query, so reduced motion leaves a completely plain button
   with no leftover pseudo-element.
   ════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion:no-preference){
  .btn-rec,.pill{position:relative;isolation:isolate;overflow:hidden}
  .btn-rec::after,.pill::after{content:"";position:absolute;top:0;left:-70%;
    width:40%;height:100%;z-index:-1;pointer-events:none;transform:skewX(-18deg);
    background:linear-gradient(115deg,transparent 0%,rgba(255,255,255,.05) 40%,
      rgba(255,255,255,.22) 50%,rgba(255,255,255,.05) 60%,transparent 100%);
    animation:btn-sheen 7s ease-in-out infinite}
  /* Nothing shimmers on a button nobody can press. */
  .btn-rec[disabled]::after{animation-play-state:paused;opacity:0}
  @keyframes btn-sheen{0%{left:-70%}16%{left:170%}100%{left:170%}}
}

/* ——— vb.css collision overrides (see the note above the hero) ——— */
/* The shared chrome (nav, CTA, footer) must render exactly as on every other
   page, so it does not inherit this page's tighter body letter-spacing. */
.site-nav,.cta-free,.site-foot{letter-spacing:normal;line-height:1.65}
/* The opener plays full-bleed BEHIND the nav (founder: "remove the black bar
   under the top menu so we just have the logo and the buttons"). The script
   toggles .opener-in on <html> for exactly as long as the opener is on screen;
   every other moment on this page, and every other page, keeps the ordinary
   solid bar. */
html.opener-in nav.site-nav{background:transparent;border-bottom-color:transparent}
/* The band that makes the menu readable TRAVELS WITH THE MENU (founder,
   2026-09-17: "bring the darkened gradient bar downward with the menu so the
   menu titles read clear... until it switches to a full dark bar when we
   scroll down far enough").

   It is the nav's own pseudo-element rather than a layer of the opener's
   backdrop, which is the whole fix: the nav is `position:sticky`, so anything
   painted on the section behind it is left at the top the moment you scroll,
   and the menu spends the rest of the opener over bare footage.

   It reaches BELOW the bar (height:220%) and fades out, so it reads as haze
   the bar sits in rather than as a second, taller bar with an edge of its own.
   `z-index:-1` keeps it behind the nav's own content; the nav already makes a
   stacking context (z-index:60), so that cannot escape behind the page.

   Opacity rather than a rule that only exists while .opener-in does, so the
   hand-off to the solid bar can be a cross-fade instead of a snap — the two
   transitions are matched on purpose, and both are on the nav's own
   properties so the bar and its haze always change together. */
nav.site-nav{transition:background-color .28s ease,border-color .28s ease}
nav.site-nav::before{content:"";position:absolute;left:0;right:0;top:0;height:220%;
  z-index:-1;pointer-events:none;opacity:0;transition:opacity .28s ease;
  background:linear-gradient(180deg,rgba(20,18,16,.92) 0%,rgba(20,18,16,.66) 42%,rgba(20,18,16,.28) 72%,rgba(20,18,16,0) 100%)}
html.opener-in nav.site-nav::before{opacity:1}
@media (prefers-reduced-motion:reduce){
  nav.site-nav,nav.site-nav::before{transition:none}
}
h2{max-width:none}
.shelf img{border-radius:0;box-shadow:none;transition:none}
.shelf img:hover{transform:none}
.band{border-top:0}
/* The OSD strip sits over the bottom 30px of the window; give the shared
   footer's last row room to clear it. */
.site-foot .foot-bottom{padding-bottom:calc(2.2rem + 34px)}

/* ── Founder notes, 2026-09-14 ──────────────────────────────────────────────
   The hero states the claim in one line, three bullets carry the mechanism,
   TapeFlow plays beside it, and the same web sign-in login.html offers sits
   below the bullets — the download button and QR code that used to live
   here moved up into the opener (round 2, 2026-09-17): "let's remove the
   download to iphone and QR code here since they're above." */
.hero-points{list-style:none;margin:clamp(.8rem,1.8vw,1.1rem) 0 0;padding:0;display:grid;gap:.5rem;max-width:52ch;
  font-size:clamp(1rem,1.4vw,1.12rem);line-height:1.5;color:var(--ink-soft)}
.hero-points li{position:relative;padding-left:1.35rem}
.hero-points li::before{content:"";position:absolute;left:0;top:.6em;width:.55rem;height:.55rem;border-radius:50%;background:var(--gold)}
.hero-auth{margin-top:.9rem}
/* The two sign-in buttons (.auth-row, .btn-auth, .auth-msg) are shared with
   /login.html since 2026-09-14 and live in vb.css; the hero only places them
   — except for the stack below, which is this page's own departure from
   that shared two-column grid. */
.auth-note{margin:.55rem 0 0;font-size:.85rem;color:var(--muted);max-width:none}
/* Narrower and stacked, Apple on top (founder, round 2: "make the continue
   with google and continue with apple buttons not as wide... put the apple
   button on top too" — the DOM order is the html agent's job; this only
   restacks and narrows what vb.css already draws for /login.html, which
   keeps its own two-column grid untouched). */
.auth-row{display:flex;flex-direction:column;align-items:stretch;gap:.55rem;max-width:320px}
.auth-row .btn-auth{width:100%}

/* ——— the scroll nudge (founder, 2026-09-14; then "bigger and centered, and
   make sure it's on brand") ———
   A card centred in the window above the OSD, which slides in once the page's
   first store button has scrolled off the top. That button is the OPENER's
   since 2026-09-17, not the hero's, which is why vb-home.js resolves the
   anchor as `.opener .btn-rec` with the hero one only as a fallback. The brand is doing the talking: the
   app's own five-colour stripe rule along the top edge (the same signature as
   the page's topbar), the deck radius, the line in the display face in gold,
   and the REC-red store button. vb-home.js owns when it shows; `hidden` is its
   resting state, so without JS there is simply no card. It sits above the OSD
   (55) and below the nav (60).
   ⚠️ Every transform here must keep translateX(-50%) — it IS the centring, so
   a `transform:none` in any state (the reduced-motion block was the trap)
   throws the card into the right half of the window. */
.nudge{position:fixed;left:50%;bottom:46px;z-index:58;
  width:min(620px,calc(100vw - 2 * var(--gut)));
  border-radius:var(--r-deck);overflow:hidden;
  background:var(--panel-2);border:1px solid var(--line-2);
  box-shadow:0 0 0 1px #000,0 26px 54px -18px rgba(0,0,0,.95);
  opacity:0;transform:translateX(-50%) translateY(16px);
  transition:opacity .25s ease,transform .32s var(--lock)}
.nudge[hidden]{display:none}
.nudge.on{opacity:1;transform:translateX(-50%)}
.nudge .stripes{height:6px}
/* The row centres and wraps: a long line in German or French takes two rows
   rather than squeezing the button. */
.nudge-in{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:.85rem 1.3rem;padding:1.15rem 3.2rem 1.25rem}
.nudge-line{font-family:var(--display);font-size:clamp(1.25rem,2.4vw,1.6rem);line-height:1.05;
  letter-spacing:.01em;color:var(--gold);text-align:center}
.nudge .btn-rec{font-size:1rem;padding:.72rem 1.35rem}
.nudge-x{position:absolute;top:calc(6px + .5rem);right:.5rem;width:34px;height:34px;border-radius:50%;
  border:1px solid var(--line-2);background:transparent;color:var(--ink-soft);
  font:400 1.3rem/1 var(--body);cursor:pointer;transition:border-color .18s ease,color .18s ease}
.nudge-x:hover{color:#fff;border-color:var(--gold)}
/* On a phone this card is not a detail, it is a share of the screen. Measured
   at 390x844: nav 65 + card 119 + its 40px offset + the OSD 30 = 254px, about
   30% of the window permanently spoken for, and nearer 40% once Safari's own
   chrome is counted. The line still wraps above the button (German and French
   need that), so the saving comes from padding and type rather than from
   forcing one row, and the offset now clears the home indicator instead of
   assuming a flat 40px. */
@media (max-width:600px){
  .nudge{bottom:calc(34px + env(safe-area-inset-bottom,0px))}
  .nudge-in{gap:.5rem;padding:.75rem 2.8rem .8rem}
  .nudge-line{font-size:1.05rem}
  /* Padding, not type, carries the saving here: the button has to stay a
     44px target, which is the whole reason the card is on screen. */
  .nudge .btn-rec{font-size:.85rem;padding:.7rem 1.05rem}
}
@media (prefers-reduced-motion:reduce){.nudge{transition:none;transform:translateX(-50%)}}

/* ════════════════════════════════════════════════════════════════════
   THE PHONE HERO (2026-09-15) — phones only; desktop is untouched.

   Measured on a 390x734 viewport (an iPhone 15 in Safari, once its own
   chrome is taken off the 844): the walkthrough started at 708px, so the
   entire first screen was words plus three buttons and nobody saw the
   product before being asked to decide. The headline took four lines,
   and two of the three buttons sign you in to the WEB cabinet directly
   under a caption saying filming takes the app.

   This is CSS only, so it lands in all eleven languages at once and no
   page shell changes. Nothing here is shared with desktop, and that was
   verified rather than assumed: the 1280px layout measures identically
   with this block present and absent (page 11594px, hero 630px, both).
   ════════════════════════════════════════════════════════════════════ */
@media (max-width:719px){
  /* .hero-grid is already one column below 940px, but it has only TWO
     children — a wrapper <div> holding everything textual, and the
     figure — so the video can never come anywhere but last. display:
     contents dissolves the two wrappers (plain <div>s carrying no role,
     so nothing leaves the accessibility tree) and the five real pieces
     become grid items that `order` can place. Under prelaunch .hero-auth
     does not exist and this rule simply matches nothing. */
  .hero-grid>div{display:contents}
  .hero-kick{order:1}
  .hero-grid h1{order:2}
  .hero-grid .tapeflow{order:3}
  .hero-points{order:4}
  .hero-auth{order:5}
  /* The gap was tuned for two big blocks; five small ones want less. */
  .hero-grid{gap:1.05rem}

  /* Three lines instead of four. */
  .hero-grid h1{font-size:clamp(1.9rem,7.9vw,2.1rem)}

  /* The frame was sized to fill a desktop column. On a phone the scene only
     has to read as "clips land on one tape", and every pixel it gives back is
     one the three facts and the store button gain above the fold. .tf-stage
     already scales with the viewport, so all that is left here is the gap
     between the phones and the tape they feed.

     .tf-film's resting position is measured all the way down to a cell on
     that tape, so the same number has to be declared where the film can read
     it — otherwise the film would appear to grow from a point 18px above the
     tape rather than out of it. One value, two readers. */
  .tapeflow{--tf-rail-extra:1.15rem}
  .tf-rail{margin-top:var(--tf-rail-extra)}

  /* Sign-in is the returning-visitor door, not the main one. It keeps its
     full 48px tap target and its explanation, just one step quieter. */
  .hero-auth{margin-top:.35rem}
  .hero-auth .btn-auth{font-size:.95rem}
  /* Centred on a phone (founder, 2026-09-17: "center the continue with apple
     and continue with google buttons"). The stack keeps its 320px cap, so on
     a phone it is a column narrower than the text above it, and pinned left
     that read as a layout that had not finished. The note under it centres
     with it: a centred pair of buttons over a left-flushed caption looks like
     two components that disagree. The status line (.auth-msg) is the same
     caption's twin and follows it; it carries vb.css's 48ch cap, so its box
     is centred as well as its text. */
  .hero-auth .auth-row{margin-inline:auto}
  .hero-auth .auth-note,.hero-auth .auth-msg{text-align:center}
  .hero-auth .auth-msg{margin-inline:auto}

}
