/* /content — THE FEED AS EVIDENCE, NOT AS DECORATION.
   ==================================================================
   🔴 FACADE PATTERN. EVERY CARD IS A SELF-HOSTED WebP THAT LINKS OUT.
   There are no embeds on this page and there must not be. Instagram's oEmbed
   went tokenless again in Jun 2026 so an embed is *possible* — and it is still
   wrong here for four reasons, in order of how much they matter:
     1. A live feed is UNCURATED. Several of Juan's own reels state his personal
        income and a card balance. On a job-search page those would surface by
        themselves, and nobody would have chosen them.
     2. Each embed is 50–100KB of third-party JS, wants cookie consent, and
        sometimes needs a viewer login to render at all.
     3. It would break no-build / no-dependency / no-webfonts in one move.
     4. A hiring page should not change without Juan looking at it.
   Ten posters: nine at 420x746 plus the hero at 720x1280. Refreshing them is a
   manual step on purpose — see site-docs for the refresher.

   ⚠️ THE NUMBERS ON THE CARDS ARE PER-POST VIEW COUNTS read from the platform,
   not follower-relative rates. A profile-level average and a per-post average
   are different measurements and mixing them produced a wrong claim once
   already. If these are refreshed, refresh §01's stat block in the same pass —
   and re-derive the MEDIAN, not just the mean: one reel is 52x the median, so an
   average alone misrepresents the channel in the other direction.

   ⚠️ THE GRID IS GONE, Sep 1 2026 — `.feed__grid` and `.feed__card` with it.
   The nine cards used to sit in an auto-fill grid two-thirds of the way down the
   page; they are now panels in the horizontal rail at the top, because the media
   was below the fold on a page about making media. `.feed__shot`, `.feed__play`,
   `.feed__n` and `.feed__t` survived the move unchanged and are reused by the
   rail — the frame, the play mark and the two text styles were right already. */

/* ============================================================ THE HEADER ===
   Three lines, and it used to be five. The eyebrow and the lede went so the rail
   could start above the fold; the <h1> stays visible because this page is a
   destination people are sent to, not one they arrive at from elsewhere. */
.feed__head {
  /* 🔴 MORE AIR ABOVE, LESS BELOW — Juan, Sep 3 2026: "the main headline and
     icon is too close to the top of the page, can we add a gap or padding of
     some sort to bring it more towards the middle or closer to the actual
     media." Both halves of that are one change: push the group DOWN off the top
     edge, and close the gap to the rail it introduces.
     Measured at 1440x900 before: the mark sat 78.9px from the top and the h1 was
     already only 16.2px off the rail.
     🟢 AND THERE WAS ROOM, WHICH IS WHY THIS IS SAFE. The note above says the
     eyebrow and the lede were cut "so the rail could start above the fold", so
     the obvious worry is pushing it back below. Measured: #reels is 675.7px tall
     but its last real child, the shared caption, ends 73.2px before its own
     bottom edge — so there was 73px of slack inside the section that nothing was
     using. This spends about half of it.
     ⚠️ THE BASE RULE IS THE GENEROUS ONE BECAUSE THE GUARDS ALREADY EXIST: a
     `max-height: 800px` query and a `max-width: 640px` query both override this
     padding with tight values, so a short or narrow window never sees these
     numbers. Check both before widening this further. */
  padding: clamp(34px, 8vh, 96px) var(--gut-min) 0;
}
/* 🔴 THE HEADLINE IS CENTRED BETWEEN THE ICON AND THE MEDIA, AND IT IS
   STRUCTURAL RATHER THAN TUNED. Juan: "center the headline between the icon and
   the media itself." Measured at 1440x1125 before: 22px above the h1 against
   13.5px below it — a 63% difference, which is why it read as sitting off
   centre in its own slot even though the block as a whole was not too high.
   ⚠️ AND THERE IS NO ROOM TO MOVE THE BLOCK ITSELF DOWN, which is worth writing
   down so the next pass does not try. The rail grows with viewport height (a
   680px track at 1125), so at that height the caption already ends 28.2px off
   the bottom edge. The composition is full; only the DISTRIBUTION inside the
   header was available.
   🟢 So both gaps are now the SAME declaration — the h1's own symmetric margin
   — with the mark's bottom margin and the header's bottom padding zeroed out
   from under it. Equal by construction at every viewport instead of two clamps
   that happen to agree at one height and drift at the next.
   ⚠️ Scoped to this base rule on purpose: the max-width 640px branch keeps its
   own deliberately tighter values and is left alone. */
.feed__head .page-mark { margin-bottom: 0; }
.feed__head .back { margin-bottom: clamp(12px, 2vh, 20px); }
/* ⚠️ CENTRED, and only the heading — the back link stays hard left where the
   site's back link always is. Juan's call, Sep 1 2026. `margin-inline: auto`
   rather than `text-align: center` on the header, because the header also holds
   the back link and centring that would move a navigation affordance. */
.feed__head .doc__h1 {
  max-width: 26ch;
  text-wrap: balance;
  margin: clamp(15px, 1.9vh, 26px) auto;
  text-align: center;
  font-size: var(--t-4xl);
}

/* ============================================================= THE FRAME ===
   `aspect-ratio` reserves the box before the image decodes, so the rail does not
   reflow as ten files land — the same reason every <img> carries explicit
   width/height. */
.feed__shot {
  position: relative;
  display: block;
  aspect-ratio: 9 / 16;
  overflow: hidden;
  background: #050706;
  border: 1px solid rgba(220, 230, 221, .16);
  transition: border-color .18s ease;
}

.feed__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Sits under the tube's own treatment, so it is pulled back a touch to stop
     the photographs fighting the phosphor. */
  filter: saturate(.92) contrast(1.02);
  transition: transform .3s ease;
}

/* THE PLAY MARK IS THE HONEST PART OF THE FACADE: it says "this is a video and
   it lives somewhere else." Without it a still reads as a photograph, and the
   card would be pretending to be an embed. */
.feed__play {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 22px 10px 9px;
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg);
  background: linear-gradient(to top, rgba(5, 7, 6, .88), rgba(5, 7, 6, 0));
}

.feed__play::before {
  content: "";
  flex: none;
  width: 0;
  height: 0;
  border-left: 8px solid var(--amber);
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
}

.feed__n {
  font-family: var(--mono);
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
  color: var(--amber);
  letter-spacing: .02em;
}
.feed__n b { font-weight: 400; font-size: 15px; }

.feed__t {
  font-size: 13.2px;
  line-height: 1.45;
  color: var(--fg-dim);
}

/* ⚠️ svh, NOT vh, AND THIS IS THE ONE THAT ONLY A REAL DEVICE CATCHES.
   On iOS Safari `vh` is the LARGE viewport — the height the page would have if
   the toolbar were hidden — so a card sized in vh is always taller than the
   space actually on screen while the toolbar is showing. Every desktop
   measurement said this fitted; the simulator showed the indicator sitting on
   top of the HUD. `svh` is the small viewport, which is the conservative one and
   the only honest basis for "above the fold".
   ⚠️ FOR THE SAME REASON, THE BREAKPOINTS BELOW KEY ON max-width, NOT
   max-height: a `max-height` query also resolves against the large viewport, so
   an 874pt phone never matched the 800px rule that was meant to catch it. */

/* ========================================================== THE REEL RAIL ===
   One reel in focus in the middle, the rest dimmed either side, scroll or click
   to change which. Juan's shape; see content.html for why focus follows SCROLL
   POSITION rather than hover.

   ⚠️ THE SECTION OWNS ONE SCREEN WITH THE HEADER, and the rail takes what is
   left. Same structure /work's picker uses, and for the same reason: once the
   column is pinned to the viewport, the card can be sized off the space that is
   actually free instead of guessed at. */
/* 🔴 ONE SCREEN, AND THE RAIL IS CENTRED IN WHAT THE HEAD LEAVES OF IT.
   Juan, Sep 1 2026: "the carousel should be centred vertically, similar to how
   we're doing it with other pages."
   ⚠️ THE WRAPPER IS WHY THIS WORKS. `justify-content: center` needs a box that
   owns exactly one viewport; .doc__main also carries §01 and §02, so centring
   against IT would have stretched the whole document to fit the sections too.
   This is the same shape /work's picker uses — a 100svh flex column where the
   header takes what it needs and the rail takes all the slack. */
.feed__screen {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}
.reelrail {
  /* ⚠️ min-height: 0 IS LOAD-BEARING ON A FLEX CHILD. Without it the default
     `min-height: auto` refuses to shrink below the track's content height, so on
     a short window the rail pushes past the fold instead of the stage giving way
     — the same class of bug --stage solves on /work. */
  flex: 1 1 auto; min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(10px, 1.6vh, 18px);
  /* ⚠️ THE HUD RESERVE IS PART OF THIS PADDING, not decoration. The indicator is
     the last row above the fold and the sound bar is fixed over the bottom 52px;
     measured before this, the dots finished 44px inside it on a phone. */
  padding-bottom: calc(var(--hud-h) + clamp(6px, 1vh, 14px));
}

/* ⚠️ TRANSFORM, NOT WIDTH, IS WHAT SHRINKS THE NEIGHBOURS. Sizing the off-centre
   cards with a smaller height would relayout the whole track on every focus
   change — and because focus changes DURING a scroll, that is layout thrash on
   the main thread while the user's finger is still moving. A transform is
   composited and costs nothing. The panels therefore all reserve the hero's
   width, and the dimmed ones simply do not fill it. */
/* 🔴 THE RAIL IS HELD UNTIL IT HAS BEEN PLACED, BECAUSE IT OPENS 3,197px AWAY
   FROM WHERE IT BELONGS. content.js positions the rail by writing
   `track.scrollLeft = posOf(start)` from a DEFERRED script, and the focused reel
   is deliberately the middle one of thirty (ten cards × the three-copy loop). So
   the browser's own initial scroll position — 0, the far left of the first copy
   — is not merely a little off, it is fourteen cards away. Measured at 1440px:
   settled scrollLeft 3197 against a painted 0, with every card still at full
   brightness because mark() had not run either.
   That is Juan's report, Sep 3 2026: "a slight glitch or jump that happens when
   people first open it showing the content in, like, not the intended way."

   ⚠️ THE FALLBACK IS AN ANIMATION, NOT A noscript BLOCK, and that is deliberate.
   This rail is designed to work with no JS at all — the file's own header says
   so: thirty real links you can swipe. A guard class that only JS removes would
   turn a graceful degradation into a blank box, and it would do the same if
   content.js ever threw before reaching land(). `railsettle` reveals the rail on
   its own after 600ms whatever happens, so the worst case is a short wait rather
   than an empty page, and the normal case never sees it — JS clears the class a
   few milliseconds in, long before the keyframe would have. */
.reelrail__track.is-settling { animation: railsettle .6s step-end both; }
@keyframes railsettle { from { opacity: 0; } to { opacity: 1; } }
/* ⚠️ NO prefers-reduced-motion EXCEPTION, ON PURPOSE. `step-end` holds opacity 0
   and then snaps — nothing travels, so there is nothing for that query to spare
   anyone. Exempting it would do the opposite of what it is for: it would restore
   the 3,197px jump for exactly the readers who asked the page to hold still. */

.reelrail__track {
  display: grid;
  grid-auto-flow: column;
  justify-content: start;
  gap: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  /* ⚠️ THE PADDING IS WHAT LETS THE FIRST AND LAST CARD REACH THE CENTRE. Without
     it neither end can ever be the focused one, because the scroller runs out
     before the card gets to the middle. `50% - var(--rw)/2` is exactly the gap
     between the scrollport's centre and a card's leading edge. */
  padding-inline: calc(50% - (var(--pw) / 2));
  scroll-padding-inline: calc(50% - (var(--pw) / 2));
  /* 🔴 SELF-FITTING, NOT A FLAT FRACTION — Juan: "can it also be bigger, take up
     more space on the page." The card is now sized the way /work sizes --stage,
     and for the same reason: the chrome around it does NOT scale with the
     viewport. Measured at 1440x700 by collapsing --rh to 0 and reading the
     screen's intrinsic height, the head + gap + caption + HUD reserve come to
     368px FIXED, so a flat svh fraction either wastes room on a tall window or
     runs past the fold on a short one.
       min(66svh, 100svh - chrome)  -> the fraction wins tall, the subtraction wins short.

     🔴 AND THE CHROME IS NOT ONE NUMBER — IT CHANGES AT THE 800px BREAKPOINT.
     That is the bug this comment exists to stop repeating: the first version used
     the 368px measured at 1440x700 for every height, and at 1440x1000 the page
     overflowed by 57px. The `@media (max-height: 800px)` block below SHRINKS THE
     HEAD, so above 800 the head grows back and the chrome is 437px, not 368.
     Measured both ways, by collapsing --rh to 0 and reading the screen's
     intrinsic height:
        > 800 tall  ->  437px   (full head)      <- this rule
       <= 800 tall  ->  368px   (head shrunk)    <- the override below
     ⚠️ A constant derived at one viewport is a constant derived at one viewport.
     If the head or the caption changes, re-measure BOTH. */
  /* 🔴 AND A WIDTH BOUND, BECAUSE HEIGHT ALONE MAKES THE DRUM ILLEGIBLE.
     Sizing purely off the viewport HEIGHT let the card grow until the first
     neighbours ran off both edges of the screen — the rail read as three big
     panels rather than a wheel, which trades away the exact thing the cosine
     was built for. The card is 9:16, so its width is rh * .5625, and focus plus
     both neighbours (at cos30 = .866) need
         w + 2(.866w) + 2(tight)  =  2.732w + 40px  <=  100vw
     giving w <= (100vw - 40px) / 2.732, i.e. rh <= (100vw - 40px) * .651.
     ⚠️ Three constraints, and the SMALLEST wins: the fraction on a tall window,
     the chrome subtraction on a short one, and this on a narrow one. */
  /* ⚠️ .651 IS DERIVED, BUT WHICH NEIGHBOURS YOU WANT TO SEE IS TASTE, so the
     coefficient is a variable rather than a literal. At .651 focus and both
     ±1 neighbours sit flush to the viewport edges by construction, which is
     exactly what the algebra above asks for — and it means ±2 is off screen
     where it used to peek. Lowering --rfit shrinks the card and buys that peek
     back. It is on /content?tune, because it is Juan's eye that settles it and
     not mine.
     ⚠️ Do NOT put --rh itself on the tuner: it is a clamp(), and
     getPropertyValue returns the UNRESOLVED token, so parseFloat gives 0 and
     the slider starts dead. A plain number is what a slider can read. */
  --rfit: .651;
  --rh: clamp(240px, min(66svh, calc(100svh - 437px), calc((100vw - 40px) * var(--rfit))), 680px);
  --rw: calc(var(--rh) * 0.5625);   /* 9:16 — the card's width follows its height */
  /* ⚠️ ONE NUMBER CONTROLS THE SPACING, AND IT IS THE ONE A HUMAN CAN JUDGE.
     --reel-gap is the VISIBLE GAP between the focused card's edge and its
     dimmed neighbour's edge, in px. Everything else is derived from it.

     It has to be derived, because the thing you see is not any single property.
     A neighbour is scaled down, so it leaves empty space inside its own column;
     the apparent gap is the column width MINUS half the focused card MINUS half
     the neighbour's scaled width. Tuning the column or the CSS `gap` directly
     means moving a number that is two steps away from the result, which is how
     the first attempt overshot into an overlap.

       column = (card + card x scale) / 2 + gap

     ⚠️ EVERY CENTRING NUMBER KEYS OFF --pw, NOT --rw. scrollLeft = posOf(i)
     centres panel i only while padding-inline is (scrollport - COLUMN) / 2. Set
     one of these to --rw and the rail lands off-centre by half the difference —
     the same class of error as the raw/normalised mix in content.js.

     ⚠️ THIS SAID "Juan tunes this live at /content?tune" AND THERE IS NO TUNER —
     content.js has no `tune` branch at all; it went with a later rewrite and the
     comment outlived it. Corrected Sep 1 2026 rather than deleted, because the
     capability is worth rebuilding now that --fall and --odrop below are also
     taste numbers. The default here reproduces the ORIGINAL spread he asked to
     go back to. */
  --sc: .8;                         /* neighbour scale */
  /* ⚠️ TWO GAPS, BECAUSE THERE ARE GENUINELY TWO. A dimmed card is scaled to
     --sc and the focused one is not, so the same column width produces a
     DIFFERENT visible gap depending on which pair you look at — by exactly
     rw x (1 - sc) / 2, which is 27px here. The first version had one variable
     and it silently meant "the gap next to the focused card"; the strip between
     the dimmed ones was 33px while the variable said 6.
     These two say what they mean and everything else is derived from them. */
  --tight: 20px;                    /* dimmed card to dimmed card */
  /* ⚠️ EQUAL TO --tight ON PURPOSE, Sep 1 2026 — Juan: "the gaps between the
     highlighted clip and the ones next to it should be the same as the rest of
     the dim gaps now, because that looks better in this mode." It was 30 against
     a 20 tight gap, which was right when the rail was FLAT and the focused card
     needed room carved out for it. Now that the falloff carries the emphasis,
     the extra gap reads as a hole. Kept as its own variable rather than deleted:
     the two are genuinely different measurements and the tuner still exposes
     both, so the distinction is one slider away if the look changes again. */
  --focus-gap: 20px;                /* focused card to its neighbour */
  --pw: calc(var(--sc) * var(--rw) + var(--tight));
  /* how far each side of the strip slides outward to open --focus-gap up:
     focus - tight + rw(1 - sc)/2 */
  --push: calc(var(--focus-gap) - var(--tight) + var(--rw) * (1 - var(--sc)) / 2);
  grid-auto-columns: var(--pw);
}
.reelrail__track::-webkit-scrollbar { display: none; }

/* ⚠️ `position: relative` IS THE FIX FOR A HORIZONTAL SCROLLBAR ON THE WHOLE
   DOCUMENT, not a style choice. The visually-hidden .reel__meta blocks are
   `position: absolute`; with no positioned ancestor they resolved against the
   initial containing block, which also means the track's `overflow-x: auto`
   did NOT clip them — an element only clips absolutely-positioned descendants
   when it is their containing block. So ten hidden captions sat at their static
   x inside the track, the last at 2881px, and the DOCUMENT picked up 1,441px of
   horizontal scroll. Juan felt it as "able to scroll past the viewport on the
   right ... like I'm going out of bounds." Measured: docScrollWidth 2881 against
   a 1440 viewport. Making the panel the containing block puts them back inside
   the scroller, where the overflow rule can do its job. */
.reel {
  position: relative;
  scroll-snap-align: center;
  min-width: 0;
  display: grid;
  /* ⚠️ start, NOT center — see the margin on .reel__card below */
  justify-items: start;
  align-content: start;
}
/* the focused card is wider than its column, so it has to paint over the
   neighbours it now overlaps rather than under them */
.reel.is-at { z-index: 2; }

/* ⚠️ THE CARD IS CENTRED BY MARGIN, NOT BY justify-items, AND THAT IS A
   CORRECTNESS FIX RATHER THAN A PREFERENCE. Once the gap goes small the CARD is
   wider than its COLUMN, and a grid item that overflows its track does not stay
   centred — the browser falls back to safe/start alignment to avoid clipping the
   leading edge. Measured: at the tight preset the hero sat exactly +18px right
   of centre, which is precisely half the (268 - 232) overflow.

   `(--pw - --rw) / 2` is positive when the column is wider than the card and
   negative when it is narrower, so this centres it either way and behaves the
   same in every engine instead of depending on how each one resolves overflow
   alignment. Without it the thing Juan is trying to judge drifts off centre as
   he drags the slider, which would make the gap look wrong for a reason that has
   nothing to do with the gap. */
.reel__card {
  display: block;
  width: var(--rw);
  margin-inline: calc((var(--pw) - var(--rw)) / 2);
  height: var(--rh);
  text-decoration: none;
  color: inherit;
  /* the resting state IS the dimmed one — .is-at is what promotes a card.
     ⚠️ transform-origin CENTER, not `center top`. With a top origin the shrunk
     neighbours hang from the top of their row and the rail reads as a set of
     mismatched cards rather than as one focused strip — caught by looking at it,
     not by measuring, because every rect was correct either way. */
  transform: scale(var(--sc));
  transform-origin: center;
  /* ⚠️ .62, NOT LOWER. Most of these posters are dim footage to begin with — a
     night gym, a talking head in a dark room — so at .46 the neighbours did not
     read as dimmed, they read as EMPTY FRAMES, and the tube's own vignette takes
     another bite at the edges. The dimming has to be enough to make the focused
     card obviously the focused one and no more; judge it against the DARKEST
     poster in the set, never the brightest. */
  opacity: .62;
  filter: saturate(.7);
  transition: transform .42s cubic-bezier(.2, .7, .25, 1),
              opacity .42s ease, filter .42s ease;
}
.reel.is-at .reel__card { transform: none; opacity: 1; filter: none; }

/* ⚠️ THE GAP THAT MATTERS IS ONLY THE ONE EITHER SIDE OF THE FOCUSED CARD.
   Juan, Sep 1 2026: "for the tight version ... the problem is only the gap
   between the thing in focus and the one right next to it."

   That is the correct reading of what was wrong. A uniform gap trades the whole
   Rolodex away to fix one seam, which is how the first attempt ended back at the
   spread. So the strip stays tight at --tight and the focus is given room.

   ⚠️ EVERY CARD ON A SIDE MOVES, NOT JUST THE ADJACENT ONE. Pushing only the two
   neighbours opens the focus gap by stealing it from the seam immediately
   beyond: measured 32px around the hero and 7px just outside it, against 33
   everywhere else — a tight pinch in the middle of a loose strip, which is worse
   than either. Sliding the whole left side left and the whole right side right
   leaves every other gap untouched, because both cards in those pairs move
   together.

   ⚠️ translate, NOT margin — focus changes DURING a scroll, and margins would
   relayout the track under the user's finger and move every offsetLeft the
   centring maths depends on. A transform is composited and leaves posOf() true.
   ⚠️ translateX FIRST, THEN scale: transform functions apply right to left, so
   `scale() translateX()` would translate in the scaled coordinate system and the
   push would come out 0.8x short. */
/* ⚠️ THE :has() PAIR IS THE NO-JS FALLBACK ONLY, and scoping it to
   :not(.js) is deliberate. It cannot express the cylinder below — a sibling
   combinator knows "somewhere after", not "three after" — so with JS the rule
   underneath replaces it wholesale. Both were live at one point and they tie on
   specificity (0,4,0 each), which means the winner was SOURCE ORDER and a future
   reorder would have silently changed the page. Two mutually exclusive scopes
   cannot tie. */
.reelrail:not(.js) .reel:has(~ .reel.is-at) .reel__card {
  transform: translateX(calc(-1 * var(--push))) scale(var(--sc));
}
.reelrail:not(.js) .reel.is-at ~ .reel .reel__card {
  transform: translateX(var(--push)) scale(var(--sc));
}

/* =================================================== THE RAIL IS A CYLINDER ===
   Juan, Sep 1 2026: "the focused content how it is and the two right next to it
   how they are, but the rest that follow even smaller in scale, so it looks like
   they're rotating in a circular way rather than in a flat plane ... smaller and
   smaller and dimmer and dimmer."

   This is Cover Flow's falloff (Apple, 2006) minus the rotation: a card's SCALE
   and OPACITY carry the depth read on their own, and the rotateY that Cover Flow
   adds is what a 9:16 photographic poster can least afford — it skews faces and
   text at exactly the sizes where the poster is already hardest to read.

   ⚠️ THE FIRST NEIGHBOUR DOES NOT MOVE. `--fall` is the shrink PER STEP BEYOND
   the first, so |d|=1 still resolves to exactly --sc and |d|=0 is still --is-at.
   That is the literal ask ("the two right next to it, how they are") and it is
   also why the falloff cannot just be `scale(1 - d * k)`.

   🔴 SCALING ALONE OPENS THE GAPS, WHICH IS THE OPPOSITE OF A CYLINDER.
   The columns are a fixed pitch (--pw), so a card that shrinks leaves MORE space
   in its own column: the visible gap between neighbours j-1 and j grows to
   pw - rw(s(j-1)+s(j))/2. Cards would recede and drift apart at once, which
   reads as a strip coming unglued rather than as a wheel turning away.
   So each step is pulled back in by rw(s(j-1)+s(j))/2 - rw*sc, and summing that
   from the first neighbour outwards collapses to a closed form:

       cumulative pull = rw * fall * (d - 1)² / 2

   which is 0 at d=1 (nothing moves, as promised) and grows quadratically. The
   visible gap then stays at --tight the whole way out while the cards shrink,
   and THAT is the geometry that reads as circular.
   ⚠️ Derived, not eye-tuned. Tuning stops by eye when the SIZING is wrong is the
   mistake `resolve-the-sizing-before-tuning-stops` records; the sizing is solved
   here and --fall is the only taste number left.

   ⚠️ LAYOUT IS UNTOUCHED — this is all transform. offsetLeft still reports the
   uniform pitch, so posOf(), the centring maths and the wrap all keep working on
   numbers the visual falloff never perturbs. The same reason the push above is a
   transform and not a margin.
   ⚠️ translateX FIRST, THEN scale — transform functions apply right to left, so
   `scale() translateX()` would translate in the scaled system and come out short. */
.reelrail.js .reel {
  --ad: 1;    /* |distance from the focused card|, set by content.js */
  --tx: 0px;  /* how far it slides to keep the gaps equal — see content.js § shift */
}
/* ⚠️ THE FALLOFF NUMBERS LIVE ON THE RAIL, NOT ON THE CARD, AND THAT IS WHAT
   MAKES THEM TUNABLE. A custom property declared in the same rule that uses it
   cannot be overridden from an ancestor — the rule's own declaration wins over
   anything inherited — so with these on .reel__card the tuner could set them all
   day and nothing would move. On the container they inherit down, and an inline
   style on .reelrail beats them. See content.js § the tuner. */
.reelrail.js {
  /* how much smaller each step beyond the first neighbour gets, and how much
     dimmer. ⚠️ The opacity floor is NOT 0 — a card that fades to nothing reads
     as a loading failure at the edge of the rail, and these posters are dim to
     begin with (see the .62 note above, which was measured against the DARKEST
     poster in the set, not the brightest). */
  /* 🔴 FIVE CARDS, AND THE REST ARE BEHIND THEM — Juan, Sep 1 2026: "we only need
     to see three at a time or five max, because the farther away the clips are
     the deeper they'd be in that circular rotation ... the ones we wouldn't see
     would just mean they're behind the clips that are already there."

     That is the correct reading of the geometry, and it is what the first pass
     got wrong: a cylinder OCCLUDES. Cards that rotate past the edge go behind
     the ones in front, they do not carry on shrinking in the same plane forever.
     Nine visible cards reads as a receding strip; five reads as a wheel.

     FIVE, not three. Three shows the focus and one neighbour either side — and
     the neighbour is at --sc, exactly where it sat in the ORIGINAL flat rolodex,
     so there is only ONE step and no ramp to read. The depth needs at least two
     steps to be legible as depth, which is what --vis 2 gives.

     ⚠️ THE THREE NUMBERS ARE TIED TOGETHER, so do not tune one alone:
       ad:  0     1     2     3
       sc:  1    .80   .68   (never seen)
       op:  1    .62   .31    0
     --odrop is .31 precisely so that .62 - 2(.31) reaches ZERO at ad 3, which is
     the first hidden ring. That makes the fade a TRANSITION rather than a pop:
     a card promoted from 3 to 2 animates 0 -> .31 through the existing .42s ease,
     so it rotates INTO view instead of appearing. Change --odrop and that lands
     somewhere arbitrary, and the seam starts to blink. */
  /* 🔴 A COSINE PROJECTION, NOT A LINEAR RAMP — Juan, Sep 1 2026: "either the
     clips right next to the focus should be bigger in scale, or the ones at the
     end of the visible row should be smaller, or both, to make it seem more of a
     round rotation."

     Both, and they are the same fix: a linear ramp is the ONE thing a wheel
     never does. Project a card that has rotated by θ and its visible width goes
     as cos θ — nearly FLAT near the front (so the first neighbour barely
     shrinks) and steepening toward the edge (so the last one falls off a
     cliff). The old ramp took equal bites at every ring, which is exactly why it
     read as a flat strip sliding rather than a drum turning.

       ring     0      1      2      3
       θ        0°     30°    60°    90°   <- edge-on, and therefore invisible
       cos θ    1     .866    .5      0
       was      1     .80    .68      0    <- equal steps: no curve to read

     --step 30deg is what makes ring 3 exactly edge-on, so the disappearance is
     GEOMETRY rather than a cutoff: a card does not fade out, it turns away.
     ⚠️ Change --step and 1.9099 below must change with it — it is 1/(π/6), the
     radian measure of one step, and it converts the sine back into card widths. */
  --step: 30deg;
  --smin: 0;                        /* scale at edge-on. 0 = a real cylinder */
  --omin: 0;
}

/* ⚠️ @supports, BECAUSE A BAD cos() TAKES THE WHOLE DECLARATION WITH IT.
   An unsupported function makes the value invalid at computed-value time, and
   this file's own header records what that looks like here: not an error, a
   silent fallback to something plausible. The linear ramp above stays as the
   fallback body; this block replaces it only where the trig actually resolves. */
@supports (width: calc(cos(30deg) * 1px)) {
  .reelrail.js .reel__card {
    /* how far round the drum this card has turned */
    --th: calc(var(--ad) * var(--step));
    /* 1 at the front, 0 edge-on. max() clamps the back half of the drum. */
    --c: max(0, cos(var(--th)));
    --s: calc(var(--smin) + (1 - var(--smin)) * var(--c));

    /* 🔴 --tx IS COMPUTED IN JS, AND THAT IS A RETREAT FROM CSS ARITHMETIC ON
       PURPOSE. The offset needs an integral (see content.js § shift), and the
       CSS version of it was WRONG BY 40px in a way that took four probes to
       corner: `--centre` measured 192.965px and `var(--ad) * var(--pw)` measured
       164.9px on their own, but subtracting them inside one calc() produced
       68.07 instead of 28.07. --pw is itself a calc() wrapping a clamp(), and
       substituting it into a second calc() alongside a trig term stopped
       resolving the way both halves did in isolation.
       ⚠️ It did not throw. It produced a plausible number, and the tell was the
       measured GAPS — 64px beside the focus and -17px (overlapping!) one card
       further out, against the 20px they were supposed to be.
       ⇒ Scale and opacity STAY here, because they are single expressions that
       measure exactly right (.866 and .5 at rings 1 and 2, verified). Only the
       compound one moved. JS reads --sc / --step / --smin from this file and
       measures --rw and --pw off the DOM, so the numbers still live here. */
    transform: translateX(var(--tx, 0px)) scale(var(--s));
    /* c² keeps the focus clearly dominant while still reaching 0 exactly edge-on */
    opacity: calc(var(--omin) + (1 - var(--omin)) * var(--c) * var(--c));
  }
}

/* 🔴 NO TRANSITION WHILE JS DRIVES THIS, AND THAT IS THE WHOLE OF JUAN'S THIRD
   NOTE: "the appearance and disappearance shouldn't happen when they lock into
   the clip, it should happen as the scroll is happening ... it shouldn't happen
   at once, it should happen along with the scroll."

   A transition is precisely what made it happen "at once, on lock": --ad only
   ever changed at a settled integer, so every card animated together on a timer
   the scroll had no say in. --ad is now a FRACTION recomputed from scrollLeft on
   every frame (content.js § depthAt), so the card's own position IS the clock —
   drag halfway and the ring is halfway turned. A transition on top of that would
   only add lag between the finger and the drum.
   ⚠️ goTo() still animates, because glide() tweens scrollLeft itself and the
   falloff follows the scroll for free. Removing this transition did not remove
   the easing; it moved it to the one place that knows where the rail is. */
.reelrail.js .reel__card { transition: none; }

/* 🔴 THE `.is-at` OVERRIDE IS GONE, AND REMOVING IT IS THE FIX FOR THE LAST
   MID-SCROLL BUG. It read `transform: none; opacity: 1` and was necessary while
   focus was DISCRETE — the earlier linear formula returned a nonsense value at
   ring 0 (sc + fall, just ABOVE the first neighbour), so the focused card had to
   be exempted from its own ramp.

   The cosine formula needs no exemption: at --ad 0 it returns cos(0) = 1, so the
   scale is exactly 1, the opacity exactly 1 and --tx exactly 0 — the very values
   the override used to force. Keeping it did real damage the moment the rail
   moved continuously, because `is-at` stays on the OLD card until the NEAREST one
   changes: mid-scroll it PINNED that card at its layout position while every
   neighbour slid past it, and the two centre gaps measured 37.5px and 4px against
   the ~24 they should both have been. At rest it looked perfect, which is exactly
   why it survived two passes.
   ⇒ A special case for the focused element is a bug waiting for the day focus
   stops being discrete. The formula covers ring 0; let it.
   ⚠️ The no-JS rule at § the resting state keeps ITS own is-at override, and must
   — nothing computes --ad there. */
.reelrail.js .reel__card { filter: none; }

/* ⚠️ AND THE STACKING HAS TO BE REAL, or "behind" is only a word. Without it the
   cards paint in DOM order, so every card overlaps the one to its LEFT and the
   rail leans one way — the focused card was already carrying `z-index: 2` for
   exactly this reason, which fixed the hero and left the ramp lopsided.
   JS sets --z as (100 - distance), so the order falls away symmetrically from
   the middle and each card tucks behind its inner neighbour, which is what a
   cylinder does. ⚠️ z-index needs an integer, and --z is set by JS as one; do
   not wrap it in calc() with a unit. */
.reelrail.js .reel { z-index: var(--z, 1); }

/* Hover undims a neighbour a little, as an affordance. ⚠️ IT DOES NOT PROMOTE
   ONE — see the note in content.html. Kept well short of `.is-at` so there is
   never a moment where two cards look equally focused. */
.reel:not(.is-at) .reel__card:hover { opacity: .68; filter: saturate(.8); }

.reel__card .feed__shot { height: 100%; aspect-ratio: auto; }

/* ---------------------------------------------------------- the caption ---
   ⚠️ WITH JS THE PER-CARD BLOCK IS VISUALLY HIDDEN, NOT REMOVED. It is still the
   source the shared caption is written from, and still what a screen reader
   reads in place — so each reel keeps its own views and description associated
   with its own card, which one shared live region could not do. Same .vh
   treatment the site uses elsewhere; `display: none` here would take all ten
   descriptions out of the accessibility tree to save nothing. */
.reelrail.js .reel__meta {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* without JS every card simply captions itself, at card measure */
.reel__meta { display: grid; gap: 5px; margin-top: 12px; }

/* THE SHARED CAPTION. Its own measure, centred under the rail — the whole point
   is that it is NOT bound to the 9:16 card's width. min-height reserves the
   tallest of the ten so the indicator below it cannot move as focus changes;
   a jumping row of dots reads as the rail being unstable. */
.reelrail__cap {
  margin-inline: auto;
  max-width: min(52ch, 86vw);
  /* ⚠️ A FIXED GUTTER, NOT 7vw. `padding-inline: 7vw` sits INSIDE the max-width
     (border-box), so at 1440 it took 101px off each side of a 400px box and left
     the caption 198px of measure — narrower than the card it describes, which is
     the exact problem the shared region exists to solve. */
  padding-inline: 16px;
  display: grid;
  gap: 6px;
  /* a floor only — content.js measures the tallest caption and reserves it,
     because the real height varies by two lines between reels */
  min-height: 4.6em;
  text-align: center;
}
.reelrail__cap .feed__t { text-wrap: pretty; }

/* ------------------------------------------------------------ the indicator ---
   Answers "how many, and which one" — the single thing a swipeable set cannot go
   without. Every mark is a real button so a keyboard reaches any reel in one
   tab-and-arrow instead of ten swipes. */
.reelrail__dots {
  display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
  gap: 2px;
  padding-inline: var(--gut-min);
}
.reel__dot { padding: 9px 5px; border: none; background: none; cursor: pointer; line-height: 0; }
.reel__dot i {
  display: block; width: 7px; height: 7px; border-radius: 1px;
  background: rgba(220, 230, 221, .24);
  transition: background .3s ease, box-shadow .3s ease, transform .3s ease;
}
.reel__dot:hover i { background: rgba(220, 230, 221, .5); }
.reel__dot.is-on i {
  background: var(--amber); box-shadow: 0 0 8px rgba(240, 168, 60, .8); transform: scale(1.2);
}
/* a 7px dot with 5px of padding is a 17px target — under the WCAG 2.2 floor of
   24, and this is how you reach nine of ten reels without swiping */
@media (pointer: coarse) { .reel__dot { padding: 13px 8px; } }
.reel__count {
  margin-left: 12px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .18em;
  color: var(--fg-dim); opacity: .7;
}
.reel__count i { font-style: normal; opacity: .45; margin: 0 3px; }

/* ⚠️ A SHORT LAPTOP IS WHERE "ABOVE THE FOLD" BREAKS, and the heading is the
   part that is oversized for it — the card already scales with vh, the h1 does
   not. Same lever as /work's picker, same threshold. */
/* ⚠️ AND (min-width) IS LOAD-BEARING — WITHOUT IT THIS BLOCK HIJACKS PHONES.
   iOS Safari MATCHES `max-height: 800px` on a 402x874 phone, because the media
   query resolves against the CURRENT visual viewport (~730pt with the toolbar
   showing) — while `vh` INSIDE the block resolves against the LARGE one (874).
   So the block applied where it was never meant to, and, being later in the
   file, it beat the phone rule above and set the heading back to 46px. That is
   the two-viewport inconsistency in one rule: matched small, sized large. */
@media (max-height: 800px) and (min-width: 641px) {
  .feed__head { padding-top: clamp(16px, 2.6svh, 28px); }
  .feed__head .doc__h1 { font-size: clamp(24px, 4.4svh, 38px); }
  /* ⚠️ THE SAME SHAPE AS THE BASE RULE, WITH THIS BRANCH'S OWN CHROME. The two
     rules above shrink the head here, so there is 368px of chrome rather than
     437 — and the card is therefore allowed to be BIGGER on a short screen than
     the base rule would permit, not smaller. Dropping this override (which the
     first attempt did, calling it redundant) makes every screen under 800px tall
     lose ~69px of card for no reason. It is not redundant; it carries the other
     measurement. */
  .reelrail__track { --rh: clamp(200px, min(66svh, calc(100svh - 368px), calc((100vw - 40px) * var(--rfit))), 680px); }
}

/* On a phone the neighbours are mostly off-screen anyway, so they shrink less —
   a 0.66 scale next to a card that already fills 62% of the viewport leaves the
   rail looking gap-toothed rather than focused. */
@media (max-width: 640px) {
  /* ⚠️ THE HEADING IS WHAT PUSHES THE RAIL OFF A PHONE, NOT THE CARD. At 375x812
     the h1 hit its 28px floor and wrapped to three lines — 217px of header — and
     the indicator finished 11px BELOW the fold, measured. Dropping the heading to
     24px buys back three lines' worth and lets the card grow at the same time,
     which is the trade the page wants: on the device this link is opened on
     most, the reel should be the biggest thing on screen. */
  .feed__head { padding: clamp(14px, 2.2vh, 22px) var(--gut-min) clamp(6px, 1vh, 12px); }
  .feed__head .back { margin-bottom: 8px; }
  .feed__head .page-mark { margin-bottom: 8px; }
  .feed__head .doc__h1 { font-size: 24px; max-width: 20ch; }
  .reelrail__track { --rh: clamp(210px, 48svh, 430px); }
  .reelrail__track { --sc: .84; --tight: 14px; --focus-gap: 22px; }
  .feed__t { font-size: 12.6px; }
  .reelrail__cap { min-height: 5.6em; padding-inline: 20px; }
}

/* Hover is a lift, not a glow — the tube already glows and a second glow reads
   as a bug. Keyboard focus gets the same treatment plus a real ring. */
.reel__card:hover .feed__shot { border-color: rgba(240, 168, 60, .45); }
.reel__card:hover .feed__shot img { transform: scale(1.03); }
.reel__card:focus-visible { outline: none; }
.reel__card:focus-visible .feed__shot {
  border-color: var(--amber);
  outline: 2px solid var(--amber);
  outline-offset: 2px;
}

/* =================================================== §01 + §02, SIDE BY SIDE ===
   ⚠️ THESE RULES EXIST BECAUSE WRAPPING THE SECTIONS BROKE THE ONES THAT USED TO
   STYLE THEM. `#rail.flow > .item` and the 62ch cap `#rail > .item > *:not(...)`
   are CHILD selectors, and .feed__pair made both sections grandchildren — so
   padding, the hairline rule and the prose measure all silently stopped
   applying. Nothing throws when that happens; the copy just runs the full width
   of its column and the section loses its top border. Re-stated here rather
   than loosened to a descendant selector, because /kova and /sipful still want
   the child-only version. */
.feed__pair > .item {
  padding: clamp(42px, 6vh, 68px) var(--gut-min);
  border-top: 1px solid rgba(220, 230, 221, .09);
}
.feed__pair > .item > *:not(.item__lead),
.feed__pair > .item .item__lead { max-width: var(--measure); }

@media (min-width: 900px) {
  .feed__pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: clamp(34px, 5vw, 82px);
    padding-inline: var(--gut-min);
    border-top: 1px solid rgba(220, 230, 221, .09);
  }
  /* the wrapper owns the outer padding and the rule now, so the sections drop
     both — otherwise there are two hairlines and 7vw of gutter inside 7vw of
     gutter */
  .feed__pair > .item { padding-inline: 0; border-top: none; }
  /* a column is already near the ideal measure, so the cap only has to stop the
     stats row from stretching */
  .feed__pair > .item > *:not(.item__lead),
  .feed__pair > .item .item__lead { max-width: none; }
}

.feed__also {
  margin-top: 30px;
  padding-top: 18px;
  border-top: 1px solid rgba(220, 230, 221, .09);
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--fg-dim);
  line-height: 1.7;
}
.feed__also b { color: var(--fg); font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .feed__shot img, .feed__shot { transition: none; }
  .reel__card:hover .feed__shot img { transform: none; }
  .reel__card, .reel__meta, .reel__dot i { transition: none; }
}
