/* ==========================================================================
   bto-reels-panel.css
   SHIPPED COPY — lifted 2026-09-30 from _lab/reels-panel/reels-panel.css

   The LAB folder is the DESIGN source and the founder review page (_lab/index.html).
   THIS file is what the homepage serves. They are two copies on purpose: the lab
   page must stay editable without changing a live page.

   If you change the design, change it in the lab, review it there, then re-lift
   with tools/lift-lab-section.sh. Do NOT diverge this file by hand.
   ==========================================================================*/
/* ==========================================================================
   reels-panel.css — homepage slot 2. Copy left, a wall of work right, both
   scroll-driven. Pairs with reels-panel.js and data.js.
   No build step, no framework, no CDN, no webfont fetch.

   THREE RIBBONS AT THREE ASPECT RATIOS — 16:9, 9:16, 4:5 — rebuilt
   2026-09-30 from all-vertical, on founder instruction. The ratios, the rail
   order, the travel directions and the matched-pixels-per-second law are
   taken from the work panel already live on the homepage
   (assets/css/bto-workpanel.css, lines 288-290 for the ratios). Every place
   this file borrows from that one says so on the spot, with the line.

   WHAT IS REUSED vs WHAT IS WRITTEN FRESH is stated in each block below.
   Broadly: the ratios, directions, the px/s law, the alpha-mask edge and the
   phone's "keep all three ratios" policy are the live panel's. The widths,
   the split, the scroll-driven engine hooks and the hand-off are this file's,
   because .bto-wp is a fixed-height CSS marquee inside a two-up grid and this
   is a pinned, scroll-scrubbed, full-viewport stage — the two cannot share a
   rail implementation even where they agree on the numbers.

   PINNING IS NATIVE `position: sticky`. That is load-bearing:
   never introduce Lenis, ScrollSmoother or any smooth-scroll library that
   scrolls by transforming a wrapper. A transformed ancestor becomes the
   containing block and kills every descendant sticky — documented in
   assets/js/bto-motion.js, where it already cost this site a re-authored pin.
   `.btorp` uses `overflow-x: clip`, NOT `hidden`: clip does not establish a
   scroll container, so the sticky stage still sticks to the viewport. The
   same choice, for the same reason, as blob-chapters/chapters.css.

   HOT PATH. Per frame the engine writes only `transform` and `opacity`, and
   only where the value changed. Nothing here animates top/left/width/height/
   border-radius/filter. The radius is a static 16px, not a licence to tween it.

   THEMING: every knob is a custom property in the :root block below.
   Values tagged [SKIN] come from _lab/SKIN.md and must move with it.
   ========================================================================== */

:root {
  /* ─── PALETTE ─────────────────────────────────── [SKIN §0, §4] ─────── */
  --rp-bg:          #030303;              /* --primary. ONE ground for all
                                             four sections — this is the value
                                             that makes the page read as one  */
  --rp-ink:         #ffffff;              /* --on-dark                        */
  --rp-dim:         rgba(255, 255, 255, .78);  /* 12.27:1 — --wr-copy-dim     */
  --rp-line:        rgba(255, 255, 255, .14);  /* --wr-hairline               */
  --rp-plate:       rgba(255, 255, 255, .06);  /* a tile before its poster
                                             paints. Never a hole.            */
  --rp-cta-fill:    #ffffff;              /* the site pill, inverted for dark */
  --rp-cta-label:   #030303;              /* 20.59:1                          */
  /* Caption scrim. 14px is SMALL text and wants 4.5:1, not the 3:1 the card
     scrim in SKIN §2 was measured for — so this is opaque at the baseline
     rather than a 0.46 wash. White on #030303 @ .90 over film computes past
     16:1. Measured, not eyeballed. */
  --rp-cap-scrim:   linear-gradient(to top,
                      rgba(3, 3, 3, .90) 0%,
                      rgba(3, 3, 3, .68) 46%,
                      rgba(3, 3, 3, 0) 100%);

  /* Accent: NONE. By The Owl is monochrome — founder ruling 2026-09-23. */

  /* ─── RADIUS ──────────────────────────────────── [SKIN §1] ─────────── */
  --rp-radius:      16px;                 /* --bto-radius-l. The card step.   */
  --rp-btn-radius:  9999px;               /* --type-btn-radius                */

  /* ─── TYPE ────────────────────────────────────── [SKIN §3] ─────────── */
  /* Two faces exist on disk and Anton is spent on the wordmark section.
     This is Inter, which is --font-family-1 AND --font-family-2. No third. */
  --rp-font:        "Inter", system-ui, -apple-system, "Segoe UI", Roboto,
                    Helvetica, Arial, sans-serif;

  --rp-title-size:    clamp(30px, 3.5vw, 49px);  /* ceiling = --type-display  */
  --rp-title-lh:      1.00;                      /* --type-lh-display         */
  --rp-title-weight:  400;                       /* --type-weight-display     */
  --rp-title-track:   -0.028em;                  /* --type-track-display      */

  --rp-eyebrow-size:  14px;               /* --type-small, sentence case      */
  --rp-eyebrow-lh:    1.43;               /* --type-lh-small                  */

  --rp-body-size:     16px;               /* --type-body                      */
  --rp-body-lh:       1.50;               /* --type-lh-body                   */

  /* The closing line. --type-subtitle 20px at the wrapping leading, because
     a 20px line set at 1.00 collides with itself. */
  --rp-statement-size: clamp(17px, 1.35vw, 20px);
  --rp-statement-lh:   1.40;              /* --type-lh-statement              */

  --rp-cap-size:      14px;               /* --type-small                     */
  --rp-btn-size:      14px;               /* --type-small                     */
  --rp-btn-weight:    600;
  --rp-btn-h:         40px;               /* --type-btn-h, border-boxed       */
  --rp-btn-pad:       24px;

  /* ─── LAYOUT ──────────────────────────────────────────────────────────  */
  --rp-gutter:        clamp(20px, 4.2vw, 72px);
  --rp-split-gap:     clamp(28px, 4vw, 72px);  /* copy column <-> wall        */
  --rp-measure:       34rem;              /* copy column max width            */
  --rp-tile-gap:      clamp(10px, .9vw, 16px);
  --rp-stack-gap:     16px;               /* --type-stack-gap                 */

  /* ─── THE WALL'S SILHOUETTE ───────────────────────────────────────────
     THREE RIBBONS OF THREE DIFFERENT TILE HEIGHTS DO NOT STACK INTO A TIDY
     RECTANGLE, and pretending otherwise is what makes a collage look broken.
     So the wall's OUTLINE is made a rectangle explicitly and every interior
     edge is left to disagree:

       · --rp-wall-bleed pulls the wall out past the grid's block padding so
         the ribbons run to the stage edges instead of stopping short and
         reading as a card floating in black;
       · --rp-wall-fade is an ALPHA mask at that top and bottom edge, the same
         device and the same reason as bto-workpanel.css's --wp-fade: "so an
         infinite column does not read as content that got guillotined by the
         section edge". It matters MORE with three pitches than with one,
         because the cut lands mid-tile at three different places at once.

     The mask has to sit on .btorp__wall and nowhere else: the COLUMNS are
     translated every frame, and a mask on a transformed element travels with
     it, so the fade would slide up and down the ribbon instead of holding the
     wall's edge. .bto-wp gets away with masking its columns because there the
     animated element is the track INSIDE the column. */
  --rp-wall-bleed:    clamp(32px, 6vh, 72px);   /* = the pinned padding-block */
  --rp-wall-fade:     clamp(28px, 5vh, 56px);

  /* ─── COLUMN WIDTHS — and this is where the three ratios are paid for ──
     Flex weights, and they are UNEQUAL because the three ribbons carry three
     different aspect ratios and an equal split serves none of them.

       --rp-col-1   rail a   16:9   the widest, because a 16:9 frame needs
                                    WIDTH to be a frame. The live .bto-wp
                                    panel runs its 16:9 column at ~205px and
                                    its own CSS says 147px is "where a 16:9
                                    frame stops being a frame".
       --rp-col-2   rail b   9:16   the narrowest. It is tall anyway; width
                                    spent here buys height it does not need.
       --rp-col-3   rail c   4:5    in between, on both axes, on purpose —
                                    it is the mediator, not a third voice.

     THESE ARE PAIRED WITH THE ORDER OF `rails` IN data.js. Reorder the rails
     and you must reorder these, or the 16:9 rail lands in the narrow column.
     Both files say so.

     MEASURED AT 1440 (wall 713px, two 13px gaps):
         a  293 x 165      b  188 x 334      c  206 x 258
     No two tiles share a width and no two share a height — which is the
     whole point, and it is what the previous all-9:16 version could not do.
     At 1024 (wall 498px): a 205 x 115, b 131 x 233, c 144 x 180 — the 16:9
     lands on the same ~205px the live panel runs it at. */
  --rp-col-1:         1.28;
  --rp-col-2:         0.82;
  --rp-col-3:         0.90;

  /* ─── PHONE / SMALL TABLET — the same judgement, one axis over ─────────
     Below 900 the wall becomes ONE HORIZONTAL rail. Three ratios cannot share
     a height on a 390px screen — a 16:9 tile as tall as a readable 9:16 one
     would be 715px wide — so the rail keeps three WIDTHS and lets the heights
     differ, exactly as the columns do.

     Only ONE number is authored: --rp-rail-base, the 9:16 tile's width. The
     other two are derived from the SAME --rp-col-* weights above, so the
     desktop wall and the phone rail cannot disagree about how much width a
     16:9 frame deserves. At 390: a 280x157, b 179x319, c 197x246.

     WHY 46vw AND NOT THE 58vw THE ALL-VERTICAL VERSION USED. There is no
     value at which all three ratios are BOTH readable AND on a 390px screen
     together: the weights sum to 3.66 units, so three tiles inside 390px
     minus gaps puts the 9:16 at 100px, which is a thumbnail. Measured, not
     assumed. So the phone gets readable tiles and the ratio change arrives
     over the scroll instead of all at once — about 1.5 tiles in frame at 390
     and 2.4 at 768. 46vw is the balance: the 9:16 keeps 179px (the old
     all-vertical rail gave it 226px) and the extra width buys the other two
     ratios more time on screen, which is the thing the founder asked for. */
  --rp-rail-base:     46vw;
  --rp-rail-w-a:      calc(var(--rp-rail-base) * var(--rp-col-1) / var(--rp-col-2));
  --rp-rail-w-c:      calc(var(--rp-rail-base) * var(--rp-col-3) / var(--rp-col-2));

  /* ─── MOTION — "smoothness and slowness" as numbers [SKIN §5] ───────── */

  /* 5.1 · Scroll-follow lag, tau. The loop runs
         cur += (raw - cur) * (1 - e^(-dt/tau))
     At 60fps tau=0.16 catches 9.9% of the gap per frame and lands in ~0.48s.
     Identical constant and identical formula to --stack-smoothing, so this
     section and the card stack arrive with the same weight. */
  --rp-smoothing:     0.16s;

  /* 5.3 · Scroll budget, in extra viewport-heights on top of the pinned one.
     Base 1.8 (the wall travels ~1.6 screens of film past you, which is the
     least that reads as a wall rather than a slideshow) x the 1.33 the whole
     set was slowed by = 2.4. Same multiplier as --chapters-step 100->132svh
     and --wr-span 2.4->3.2. One multiplier across four sections, or they read
     as four different speeds on one page. */
  --rp-span:          2.4;

  /* How far a column travels over the whole span, in stage-heights.          */
  --rp-drift:         1.15;

  /* Residual speed skew. Speeds are 1-skew, 1, 1+skew.
     IT DROPPED FROM 0.34 TO 0.08 WHEN THE THREE RATIOS ARRIVED, and that is
     not a tuning tweak, it is the site's own law applied.

     bto-workpanel.css matches its three columns on PIXELS PER SECOND, not on
     tiles per second: "Equal durations would send the 9:16 column past the
     eye at three times the pixel speed of the 16:9 one, which reads as a
     fault rather than as rhythm." Its three columns are equal width and all
     three run at 0.199 W/s — identical. The rhythm there comes entirely from
     the tile HEIGHTS differing.

     That is now true here too. The pitches are 178 / 347 / 271px at 1440, so
     at one shared speed the 16:9 ribbon crosses 1.95 tiles for every one the
     9:16 ribbon crosses. THE RATIOS SUPPLY THE RHYTHM THE SKEW USED TO FAKE.
     A 0.34 skew on top of that is two rhythm systems fighting.

     It is not 0, for one reason: rails a and c both travel DOWN, so at a
     perfectly shared speed their tiles would be frozen relative to each other
     and the two would read as one rigid sheet with two grids painted on it.
     0.08 spreads them by 2 x 0.08 x 1.15 x stageH = ~166px over the whole
     span — about one 16:9 tile of relative drift, and px/s stays within +/-8%
     of matched. This is the ONE place this panel departs from the live
     panel's numbers, and that is the reason. */
  --rp-col-skew:      0.08;

  /* ─── THE HAND-OFF INTO SLOT 3 ────────────────────────────────────────
     Slot 3 (blob-chapters) opens with CENTRED copy on the same #030303 and
     then puts a circle in the middle of the screen. So the last fifth of this
     section spends itself getting to that centre: the three column speeds
     converge and decelerate to rest, the wall closes on the page's centre
     line, the copy column releases upward, and the tiles fade from the
     OUTSIDE IN so the last thing alive is a single tile at screen centre.
     Raise --rp-handoff for a longer hand-over, drop it to 0 to switch the
     whole gesture off. */
  --rp-handoff:       0.22;   /* fraction of the span spent handing over      */
  /* TWO convergences, and they are not the same number on purpose.
     --rp-centre moves the WHOLE wall onto the page's centre line, and it is 1
     because the point of the gesture is to arrive exactly where slot 3 puts
     its blob — 0.62 there leaves the survivor 128px off centre at 1440, which
     is enough to read as "nearly" and kill the effect.
     --rp-converge closes each ribbon on the wall's own centre, and it is SMALL.
     RE-MEASURED 2026-09-30 for the three-ratio geometry, because the column
     widths changed: at 1440 the ribbon centres now sit 210 / 44 / 254px from
     the wall's centre and the gaps between them are 13px, so the value that
     exactly closes the first gap is 13 / (210 + 44) = 0.051. Anything past
     that overlaps — which was true of the old geometry too (~0.06 there).
     Dropped 0.22 -> 0.14: the 16:9 ribbon is now 293px wide instead of 162,
     so the same fraction throws it 46px into its neighbour instead of 25.
     0.14 leaves ~22px of overlap at e=1, by which point those tiles are under
     30% opacity. The gathering is carried by --rp-centre and --rp-exit-scale,
     not by this. */
  --rp-centre:        1;
  --rp-converge:      0.14;
  --rp-exit-scale:    0.86;   /* the wall's scale at the seam                 */
  --rp-copy-lift:     72px;   /* how far the copy column rises as it goes     */
  --rp-copy-drift:    34px;   /* slow parallax against the ribbons, +/- this   */

  /* The headline lights up word by word as you scroll in — the live panel's
     own move, rewritten as opacity. --rp-title-dim is where a word starts;
     --rp-title-scrub is the fraction of the span the whole wave takes. Set
     --rp-title-dim to 1 and the headline is simply always lit. */
  --rp-title-dim:     0.32;
  --rp-title-scrub:   0.46;

  /* ─── UI TRANSITIONS (not the scroll path) ─────────────── [SKIN §5.2] */
  --rp-ease:          cubic-bezier(.65, 0, .34, 1);   /* = --bto-ease         */
  --rp-dur:           300ms;                          /* = --bto-dur-ui       */
  --rp-reveal-dur:    760ms;
  --rp-reveal-ease:   cubic-bezier(.22, 1, .36, 1);
}

/* ==========================================================================
   SECTION
   ========================================================================== */
.btorp {
  position: relative;
  background: var(--rp-bg);
  color: var(--rp-ink);
  font-family: var(--rp-font);
  -webkit-font-smoothing: antialiased;
  /* clip, NOT hidden — see the file header. */
  overflow-x: clip;
}

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

.btorp__sr {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The tall element. Its height is the scroll budget and is applied ONLY when
   the pinned mode is actually running — never on the static fallback, never
   before the engine has measured. */
.btorp__track { position: relative; }
.btorp.is-pinned .btorp__track {
  height: calc(100svh + var(--rp-span) * 100svh);
}

/* The pin. `overflow: hidden` HERE is safe and deliberate: it clips the
   columns to the viewport without putting an overflow ancestor above the
   sticky element itself. */
.btorp__stage { position: relative; }
.btorp.is-pinned .btorp__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.btorp__grid {
  height: 100%;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--rp-split-gap);
  align-items: center;
  padding: clamp(48px, 8vh, 96px) var(--rp-gutter);
}
.btorp.is-pinned .btorp__grid { padding-block: clamp(32px, 6vh, 72px); }

/* ==========================================================================
   LEFT — the copy
   ========================================================================== */
.btorp__copy {
  position: relative;
  z-index: 2;
  max-width: var(--rp-measure);
  display: flex;
  flex-direction: column;
  gap: var(--rp-stack-gap);
}

.btorp__rule {
  height: 1px;
  background: var(--rp-line);
  border: 0;
  margin: 0;
}

.btorp__eyebrow {
  margin: 0;
  font-size: var(--rp-eyebrow-size);
  line-height: var(--rp-eyebrow-lh);
  font-weight: 400;
  color: var(--rp-dim);
  letter-spacing: normal;
  text-transform: none;   /* sentence case — bto-type.css's ONE correction */
}

.btorp__title {
  margin: 0;
  font-size: var(--rp-title-size);
  line-height: var(--rp-title-lh);
  font-weight: var(--rp-title-weight);
  letter-spacing: var(--rp-title-track);
  text-wrap: balance;
}
/* The live site sets this word apart with an <em> inside a wiped slab. Here
   it is the same word, set as its own block so it lands on its own line —
   monochrome, no slab, no colour. */
.btorp__title em {
  display: block;
  font-style: italic;
}
/* One span per word. `inline` (not inline-block) so the headline still wraps
   and reflows exactly as plain text does, and so selection runs through it. */
.btorp__w { display: inline; }
/* Static fallback: no scroll, so no scrub — the headline is simply lit. */
.btorp.is-static .btorp__w { opacity: 1 !important; }

.btorp__lead {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--rp-stack-gap);
  max-width: 46ch;
}
.btorp__lead p {
  margin: 0;
  font-size: var(--rp-body-size);
  line-height: var(--rp-body-lh);
  letter-spacing: var(--rp-body-track, normal);
  color: var(--rp-dim);
}
.btorp__lead p.is-statement {
  font-size: var(--rp-statement-size);
  line-height: var(--rp-statement-lh);
  color: var(--rp-ink);
}

.btorp__ctas {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 6px;
}

.btorp__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--rp-btn-h);
  padding: 9px var(--rp-btn-pad);
  border: 1px solid transparent;
  border-radius: var(--rp-btn-radius);
  font-size: var(--rp-btn-size);
  font-weight: var(--rp-btn-weight);
  line-height: 1.2;
  letter-spacing: normal;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--rp-dur) var(--rp-ease),
              color var(--rp-dur) var(--rp-ease),
              border-color var(--rp-dur) var(--rp-ease);
}
.btorp__cta--primary {
  background: var(--rp-cta-fill);
  color: var(--rp-cta-label);
}
.btorp__cta--primary:hover { background: var(--rp-dim); }
.btorp__cta--ghost {
  background: transparent;
  color: var(--rp-ink);
  border-color: var(--rp-line);
}
.btorp__cta--ghost:hover { border-color: var(--rp-ink); }
.btorp__cta:focus-visible {
  outline: 2px solid var(--rp-ink);
  outline-offset: 3px;
}

.btorp__meta { display: flex; flex-direction: column; gap: 2px; }
.btorp__meta-label { margin: 0; font-size: var(--rp-eyebrow-size);
                     line-height: var(--rp-eyebrow-lh); color: var(--rp-dim); }
.btorp__meta-value { margin: 0; font-size: var(--rp-eyebrow-size);
                     line-height: var(--rp-eyebrow-lh); color: var(--rp-ink); }

/* ==========================================================================
   RIGHT — the wall
   ========================================================================== */
.btorp__wall {
  position: relative;
  min-width: 0;
  height: 100%;
}

/* The silhouette. See --rp-wall-bleed / --rp-wall-fade in :root for why this
   is here and why it cannot be on the columns. Pinned only: in the unpinned
   rail branch the wall is not full-height and there is nothing to bleed into. */
.btorp.is-pinned .btorp__wall {
  margin-block: calc(-1 * var(--rp-wall-bleed));
  height: calc(100% + 2 * var(--rp-wall-bleed));
  -webkit-mask-image: linear-gradient(to bottom,
      transparent 0, #000 var(--rp-wall-fade),
      #000 calc(100% - var(--rp-wall-fade)), transparent 100%);
          mask-image: linear-gradient(to bottom,
      transparent 0, #000 var(--rp-wall-fade),
      #000 calc(100% - var(--rp-wall-fade)), transparent 100%);
}

/* Everything that converges on the page's centre line during the hand-off
   lives on THIS element. One transform, once per frame. */
.btorp__wall-inner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--rp-tile-gap);
  transform-origin: 50% 50%;
}

.btorp__col {
  flex: 1 1 0;
  min-width: 0;
  /* the engine writes translate3d() here, every frame */
}
.btorp__col--1 { flex-grow: var(--rp-col-1); }
.btorp__col--2 { flex-grow: var(--rp-col-2); }
.btorp__col--3 { flex-grow: var(--rp-col-3); }

/* ---- a tile ---------------------------------------------------------- */
/* THE RATIO COMES FROM data.js AND EXISTS IN EXACTLY ONE PLACE. The engine
   writes --rp-ar on each tile from its rail's `ratio`, once, at creation —
   never in the hot path. Hard-coding 16/9, 9/16 and 4/5 in this file as well
   would be a second copy of the geometry, right on the day it was written and
   wrong the first time anyone edits the data. The fallback below is for a
   tile built before a ratio resolved, not a default anyone should rely on.

   NOTHING IS CROPPED TO FIT, and it does not need to be: every clip in a rail
   is NATIVE to that rail's ratio (1280x720, 540x960, 540x676/680) because
   that is the only rule build-workpanel.mjs uses to assign them. object-fit:
   cover below is a guard; the worst case in the set is 540x680 in a 4:5 box,
   which loses 0.74% of the height. */
.btorp__tile {
  position: relative;
  display: block;
  margin: 0 0 var(--rp-tile-gap);
  aspect-ratio: var(--rp-ar, 9 / 16);
  border-radius: var(--rp-radius);
  overflow: hidden;
  background: var(--rp-plate);
  /* opacity is written per frame during the hand-off; keep it cheap */
  isolation: isolate;
}

.btorp__tile img,
.btorp__tile video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border: 0;
}

/* The video sits OVER its poster and fades in when it can actually play, so
   a tile never flashes empty. Until the engine assigns a src this element has
   no source at all — readyState 0, zero bytes fetched. */
.btorp__tile video {
  opacity: 0;
  transition: opacity 420ms var(--rp-ease);
}
.btorp__tile.is-playing video { opacity: 1; }

.btorp__cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  margin: 0;
  padding: 34px 12px 10px;
  background: var(--rp-cap-scrim);
  font-size: var(--rp-cap-size);
  line-height: var(--rp-eyebrow-lh);
  color: var(--rp-ink);
  opacity: 0;
  transition: opacity var(--rp-dur) var(--rp-ease);
  pointer-events: none;
}
.btorp__cap b { font-weight: 600; display: block; }
.btorp__cap span { display: block; color: var(--rp-dim); }

/* Shown on the tile that is playing, and on hover/focus. NEVER
   display:none / visibility:hidden — an opacity-0 caption is still in the
   accessibility tree, which is where the tile's only label lives. */
.btorp__tile.is-playing .btorp__cap,
.btorp__tile:hover .btorp__cap,
.btorp__tile:focus-within .btorp__cap { opacity: 1; }

/* ==========================================================================
   THE ENTRANCE
   Idempotent, not one-shot: the class goes on when the section first
   intersects and is never taken off. A one-shot observer plus a coarse
   scroll sweep is a documented capture trap in _wireframe/shot_at.py — an
   element can be stepped straight past and never intersect, which looks like
   a broken reveal and is not one.
   ========================================================================== */
.btorp__copy > *,
.btorp__col,
.btorp__rail-inner {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition: opacity var(--rp-reveal-dur) var(--rp-reveal-ease),
              transform var(--rp-reveal-dur) var(--rp-reveal-ease);
}
.btorp.is-revealed .btorp__copy > *,
.btorp.is-revealed .btorp__col,
.btorp.is-revealed .btorp__rail-inner {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}
/* Stagger. The copy reads top-down, then the wall arrives behind it. */
.btorp__copy > *:nth-child(1) { transition-delay:  40ms; }
.btorp__copy > *:nth-child(2) { transition-delay:  90ms; }
.btorp__copy > *:nth-child(3) { transition-delay: 140ms; }
.btorp__copy > *:nth-child(4) { transition-delay: 200ms; }
.btorp__copy > *:nth-child(5) { transition-delay: 260ms; }
.btorp__copy > *:nth-child(6) { transition-delay: 310ms; }
.btorp__copy > *:nth-child(7) { transition-delay: 360ms; }
.btorp__col--1 { transition-delay: 120ms; }
.btorp__col--2 { transition-delay: 210ms; }
.btorp__col--3 { transition-delay: 300ms; }

/* Once the section is live, the engine owns `transform` on the columns, so
   the reveal transition has to get out of the way or it fights the rAF. */
.btorp.is-running .btorp__col,
.btorp.is-running .btorp__rail-inner { transition: opacity var(--rp-reveal-dur) var(--rp-reveal-ease); }

/* will-change is a promise to the compositor and expensive to leave
   standing. Only while the section is on screen. */
.btorp.is-visible .btorp__col,
.btorp.is-visible .btorp__wall-inner,
.btorp.is-visible .btorp__rail-inner,
.btorp.is-visible .btorp__copy { will-change: transform; }

/* ==========================================================================
   BREAKPOINTS
   ========================================================================== */

/* >= 900: the split. Copy left, wall right, pinned.
   THE WALL TOOK WIDTH OFF THE COPY WHEN THE RATIOS ARRIVED — 0.94 -> 1.25 and
   1.06 -> 1.30. Three equal-width 9:16 ribbons were happy in the old share;
   three ribbons where one has to hold a 16:9 frame are not. bto-workpanel.css
   makes the same move for the same reason at its own laptop breakpoint ("the
   copy column has slack the media column does not").

   It costs the copy nothing measurable: --rp-measure caps that column at
   34rem = 544px, and at 1440 the 1.30 split still leaves it 548px. The copy
   is at its measure either way; everything above it was slack. */
@media (min-width: 900px) {
  .btorp__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  }
}
@media (min-width: 1200px) {
  .btorp__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.30fr);
  }
}

/* < 900: NOT a shrunk version of the split. Three vertical ribbons across a
   390px screen would be ~110px each, which is not a reel, it is a thumbnail.
   So the wall becomes ONE horizontal rail, the copy sits above it at full
   measure, and nothing pins. The collage survives at a size you can see; the
   pin does not.

   ── THREE RATIOS ON A PHONE, AND WHY THIS IS NOT JUST A SHRINK ───────────
   A horizontal rail wants a constant tile HEIGHT, because that is what gives
   the band a straight top and bottom edge. It cannot have one here: at a 9:16
   tile height readable on a 390px screen (~340px) the 16:9 tile would be
   604px wide, and at a 16:9 width that fits the screen the 9:16 tile would be
   99px wide. There is no shared height that serves both. Cropping one to fit
   is not available either — the whole point of these rails is that every clip
   is native to its ratio.

   So the rail keeps THREE WIDTHS (derived from the same --rp-col-* weights as
   the desktop columns — one judgement, not two) and lets the heights differ.
   The band's height is simply its tallest tile; no token sets it, so it can
   never disagree with the content.

   AND THE TILES ARE CENTRED IN IT, which is the whole alignment answer:
   `align-items: center` puts every tile's centre on the band's centre line.
   That is the SAME datum the pinned wall uses — three playing tiles of three
   different heights, centres on one horizontal line, which is also the line
   the hand-off converges on. The silhouette is a rectangle; the interior
   edges disagree; nothing is cropped. One idea, both breakpoints. */
.btorp__rail {
  position: relative;
  width: 100%;
  max-width: 100%;
  overflow: hidden;            /* the rail clips itself — never the page */
  padding-block: 4px;
}
.btorp__rail-inner {
  display: flex;
  gap: var(--rp-tile-gap);
  align-items: center;         /* THE DATUM. See above — not a tidy-up. */
  width: max-content;
}
/* The bare rule is the FALLBACK, for a rail whose id has no mapped width
   (add a fourth rail to data.js and it lands here rather than at zero). */
.btorp__rail .btorp__tile {
  flex: 0 0 auto;
  width: var(--rp-rail-base);
  margin-bottom: 0;
}
.btorp__rail .btorp__tile--a { width: var(--rp-rail-w-a); }
.btorp__rail .btorp__tile--b { width: var(--rp-rail-base); }
.btorp__rail .btorp__tile--c { width: var(--rp-rail-w-c); }

/* Small tablet. At 768 this gives a 312x175, a 200x355 and a 219x274 — about
   2.4 tiles on screen, so a ratio CHANGE is always in frame, which is the
   reason for keeping three ratios on a small screen at all. */
@media (min-width: 561px) and (max-width: 899px) {
  :root { --rp-rail-base: 26vw; }
}

@media (max-width: 899px) {
  .btorp__grid {
    align-items: start;
    gap: clamp(28px, 6vw, 48px);
    padding-inline: 0;
  }
  .btorp__copy {
    max-width: none;
    padding-inline: var(--rp-gutter);
  }
  .btorp__lead { max-width: 58ch; }
  .btorp__wall { height: auto; }
}

/* ==========================================================================
   STATIC FALLBACK — prefers-reduced-motion, no JS, or a browser without
   IntersectionObserver. A plain tall section: full copy, a still grid of
   posters, no pin, no rAF, no video, no transform, nothing that moves.
   ========================================================================== */
.btorp.is-static .btorp__track { height: auto; }
.btorp.is-static .btorp__stage { position: relative; height: auto; overflow: visible; }
/* One column, always. The split exists to put the wall beside copy that is
   HELD STILL by a pin; with no pin there is nothing to hold it, and the wall
   would be squeezed into half the width for no reason. */
.btorp.is-static .btorp__grid {
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  gap: clamp(32px, 5vw, 56px);
}
.btorp.is-static .btorp__wall { height: auto; }
.btorp.is-static .btorp__copy > *,
.btorp.is-static .btorp__grid-static {
  opacity: 1;
  transform: none;
  transition: none;
}
/* Three ratios in a poster grid: `align-items: start` so each tile keeps its
   OWN height instead of being stretched to its row's tallest member, which
   would silently letterbox the 16:9 tiles. The engine deals the three rails
   into this grid in a,b,c rotation, so at the 3-column width every row is one
   of each — wide-short, narrow-tall, mediator. Rows are ragged at the bottom
   by construction; that is the composition, not a gap to close. */
.btorp__grid-static {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--rp-tile-gap);
  align-items: start;
}
@media (min-width: 700px)  { .btorp__grid-static { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1200px) { .btorp__grid-static { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.btorp__grid-static .btorp__tile { margin-bottom: 0; }
/* In the static branch the label is permanent — there is no "playing" tile
   for it to follow. */
.btorp.is-static .btorp__cap { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .btorp *,
  .btorp *::before,
  .btorp *::after {
    animation: none !important;
    transition: none !important;
  }
  .btorp__copy > *,
  .btorp__col,
  .btorp__rail-inner { opacity: 1; transform: none; }
}
