/* =========================================================================
   Alternate homepage (based on North Bureau). Loads AFTER site.css only to
   reuse its @font-face, :root tokens and reset — every rule here is scoped to
   .alt so it can't touch the real homepage.

   Black & white for now. The one colour moment we keep is the marker sweep
   from the original homepage, rendered in grey until accents land.
   ========================================================================= */

.alt {
  /* marker sweep, greyscale for now — swaps to the yellow accent later */
  --mk-a: rgb(23 22 28 / .10);
  --mk-b: rgb(23 22 28 / .20);
}
/* The page gutter: the distance from the viewport edge to the content's left
   edge — the centering leftover above the wrap's max plus its inline padding.
   The fixed wayfinding title mirrors it on the right, so both margins match. */
.alt-root {
  /* the two-column gap, shared so the intro can measure against it */
  --grid-gap: clamp(2rem, 5vw, 5rem);
  /* How much of the NEXT section stays on screen when the last image of a
     strip is resting on the focus line. The strip is a film strip — every
     slide peeks at the one after it — and the last slide was the only one that
     didn't, because its runway filled the rest of the viewport exactly. That
     read as the end of the site: half a screen of white with nothing under it.
     Now the run stops short by this much and the next section shows through. */
  --section-peek: 45svh;

  /* Empty run after the last image. Sized so the strip ends one peek ABOVE the
     viewport bottom rather than flush with it. Clamped at 0 so a short viewport
     simply shows more of the next section instead of going negative.
     --tile-h is measured by alt.js. */
  --runway: max(0px, calc(100svh - var(--focus-top) - var(--tile-h, 60svh) - var(--section-peek)));
}

/* Where the active image + description settle — the top edge of the wireframe's
   "slide area". Both the snap position (scroll-padding-top, on <html>) and the
   pinned description (top, under <body>) use this, so image-top and text-top
   land on the same line. Defined on .alt-root (<html>) so it inherits to both;
   defined on <body> it would never reach the html above it.
   Dropped low enough that the previous image's tail is visible above the band
   (peek = focus-top minus the strip gap, so these two values work as a pair). */
.alt-root {
  --focus-top: clamp(5rem, 22vh, 15rem);
  /* Distance between consecutive shots in the strip. NOT a spacing-scale step,
     and it must not be rounded onto one: the peek — how much of the previous
     image stays visible above the active one — is exactly
     (--focus-top − --strip-gap). It is an output of the choreography, so it
     lives here beside the line it is measured against rather than in the
     rhythm scale, where a later tidy-up would "correct" it and silently change
     what the strip shows. */
  --strip-gap: clamp(3rem, 12vh, 8rem);
}

/* Mandatory scroll-snap: every scroll lands on a snap point (each image, or the
   writing table). The shots are now evenly spaced, so there's no dead band; the
   root is the scroll container and scroll-padding-top drops each snap to the
   focus line. */
.alt-root {
  scroll-snap-type: y mandatory; scroll-padding-top: var(--focus-top);
  /* the full-bleed title uses 100vw, which includes the scrollbar on some
     platforms — clip the few px of horizontal spill instead of scrolling */
  overflow-x: clip;
}


/* ---- the highlight hover, retained from the original homepage --------------
   A highlighter stroke sweeps left-to-right behind the text on hover. Same
   mechanic as .art .go / the note links; grey now, accent later. */

/* --------------------------------- intro ---------------------------------
   Sits in column ONE of the same two-column grid the film strip uses, so it
   reads at exactly the measure a project description does on the right — just
   mirrored. Scrolls normally — nothing sticky.

   It carries the top-of-page snap point (the masthead can't: see masthead.css).
   With no scroll-margin its snap position resolves below zero and clamps to 0,
   so the resting state is the masthead plus the whole intro. */
.alt-hero {
  /* A slim band with the text centred in it, rather than text plus padding.
     The band is deliberately short of a full screen — the first project should
     already be peeking in below it. */
  min-height: 38svh;
  display: flex; align-items: center;
  padding-block: var(--space-fluid-s);
  scroll-snap-align: start;
}
/* Spans both columns but stops around the midpoint of the right-hand
   description. One column is (100% − gap)/2, so column + gap + half a column
   works out to 75% of the row plus a quarter of the gap. */
.alt-hero__body { grid-column: 1 / -1; }

/* The lede's measure — image column + gap + half the description column, which
   is 75% of the row plus a quarter of the gap. It lives on .alt-lede rather
   than on the hero, so the intro, Writing and the Bookshelf are the same width
   by construction instead of by three rules agreeing with each other.

   Percentages resolve against the containing block, which is the full content
   band in all three cases — the hero's body spans 1/-1, and the other two sit
   directly in .wrap. */
.alt-lede { max-width: calc(75% + var(--grid-gap) / 4); }

/* Air between a lede and whatever it introduces. Set on the receiving element
   rather than as `.alt-lede + *`: that adjacent rule lost to `.alt-w` at equal
   specificity, and on the Bookshelf the rows are not siblings of the lede at
   all — they sit outside .wrap so they can run edge to edge. */
/* Both sizes come from the Figma header (node 1602:11904): the two-line
   greeting at 16px on normal leading, the lead at 20/30.

   Kept as clamps rather than fixed px so they still degrade on a phone — the
   vw term is tuned so each hits its Figma value exactly at the design's own
   1512px frame width and is capped there. */
/* The section lede: a two-line small block over a larger paragraph. The
   intro, Writing and the Bookshelf all open this way, so it is named once
   rather than listed as three selectors that have to be kept in step. */
:root { --lede-gap: var(--space-fluid-m); }

/* Both halves of a lede are set at the project blurb's scale — see the shared
   declaration further down. They previously ran two steps smaller, which made
   the intro read as a caption above the work rather than as the same voice. */
.alt-lede__big { margin-top: var(--space-s); }

/* ------------------------------ headshot cursor --------------------------
   A JS-positioned element rather than a CSS `cursor: url()` — that can't be
   masked to a circle, is capped at small sizes, and won't animate.

   It sits BESIDE the pointer rather than under it: the real cursor stays
   visible and keeps doing its job (you can still see what you're about to
   click), and the headshot reads as something the pointer is carrying. */
.alt-cursor {
  position: fixed; top: 0; left: 0; z-index: 20;
  /* fixed, not viewport-scaled — a pointer should be the same size everywhere.
     120, up from 84: the rotation is eight different expressions and at 84 the
     face was the size of a favicon, so the thing that makes it worth rotating
     was the thing you could not read. */
  width: 120px; aspect-ratio: 1;
  border-radius: var(--radius-round); overflow: hidden;
  pointer-events: none;
  /* gap from the pointer to the headshot's near edge; negated by alt.js when
     there isn't room on the right */
  --cursor-gap: 50px;
  /* grows out of the pointer rather than out of its own middle */
  transform-origin: left center;
  opacity: 0; transform: translate(var(--cursor-gap), -50%) scale(.7);
  transition: opacity .2s ease, transform .2s cubic-bezier(.16,1,.3,1);
}
.alt-cursor.is-on { opacity: 1; transform: translate(var(--cursor-gap), -50%) scale(1); }
/* Flipped to the pointer's left near the right edge, where it would otherwise
   be cut off by the viewport. */
.alt-cursor.is-flipped { transform-origin: right center; }
.alt-cursor.is-flipped { transform: translate(calc(-100% - var(--cursor-gap)), -50%) scale(.7); }
.alt-cursor.is-flipped.is-on { transform: translate(calc(-100% - var(--cursor-gap)), -50%) scale(1); }
.alt-cursor img { width: 100%; height: 100%; object-fit: cover; }

/* ------------------------------ work stream ------------------------------
   A film strip of images scrolls up the left; the description on the right is
   pinned and cross-fades to the image currently settled, as does the giant
   name. Image and description share a top edge on the focus line (a little
   above centre). Everything here sits above the fixed giant name. */
.alt-work { position: relative; }
.alt-work__grid {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--grid-gap);
  /* stretch so the description column matches the tall image strip and its
     sticky child can travel the whole section — the top-alignment comes from
     the sticky `top` and the strip's top padding, not from grid alignment */
  align-items: stretch;
}

/* left: the scrolling image strip. The gap between shots is what lets the next
   one peek in below the active image, and sets the scroll distance per project. */
.alt-work__media {
  display: flex; flex-direction: column;
  /* smaller than --focus-top by design: the previous image peeks in above the
     band by exactly (focus-top − gap) */
  gap: var(--strip-gap);
  /* start the first image on the focus line (so its snap lands at scroll 0 and
     its top aligns with the pinned description); the end runway stops the strip
     one --section-peek short of the viewport bottom, so the last slide hands
     over to the next section the same way every other slide hands over to the
     one after it. --tile-h is measured by alt.js. */
  padding-top: var(--focus-top);
  padding-bottom: var(--runway);
}
.alt-shot {
  scroll-snap-align: start;
  /* The tile: each project's own gradient artboard as the ground, with the
     screenshot floating inside it. It is the same field the case study's hero
     and role pill sit on, so a tile and the page it opens are the same object.

     The two LogixBoard tiles take the standard and inverted artboards — same
     ramp, differently proportioned — so two projects from one company read as
     related without reading as duplicates.

     `background-size` is stated because the gradients are SVGs with a viewBox
     and no intrinsic size; left to `auto` a tile inherits whatever the
     browser infers rather than the box it actually occupies. */
  background: var(--tile, transparent);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  border-radius: var(--tile-radius);
  padding: var(--space-fluid-xs);
}
.alt-shot img {
  width: 100%; max-width: 580px; height: auto; display: block; margin-inline: auto;
  filter: drop-shadow(0 22px 42px rgb(23 22 28 / .18));
}
.alt-shot__cap { display: none; }   /* desktop uses the sticky panel instead */

/* -------------------------- the settled tile ----------------------------
   Everything but the tile the strip has settled on drops back, so the strip
   reads as one thing in focus rather than five competing for it. Gated on
   `.js-shots` — see alt.js; nothing chooses an active tile without it. */
.js-shots .alt-shot { opacity: .6; transition: opacity .45s ease; }
.js-shots .alt-shot.is-active { opacity: 1; }


/* right: the pinned description. The exit-sync spacer that used to live here
   is gone — it existed to keep the per-project title from colliding with this
   text on the way out, and the title is now fixed page-level wayfinding that
   never moves. */
.alt-work__panel {
  display: flex; flex-direction: column;
  /* Match the strip's own top offset so the description's natural position
     lines up with the FIRST image's top. Without it the text sat --focus-top
     higher than the image whenever the section was only partly scrolled into
     view — visible as a stagger now that the greeting no longer fills a
     screen. Sticky still clamps it to the focus line once scrolled. */
  padding-top: var(--focus-top);
  /* End the column where the LAST image ends rather than at the bottom of the
     strip's runway. The sticky description can only travel inside this box, so
     without this it stayed pinned at the focus line long after its image had
     scrolled away — text hanging alone over empty space. Now the two leave
     together. */
  margin-bottom: var(--runway);
}
.alt-desc {
  position: sticky; top: var(--focus-top);
  display: grid; align-items: start;
}
.alt-desc__item {
  grid-area: 1 / 1;                 /* stack every item in one cell */
  opacity: 0; pointer-events: none;
  transition: opacity .35s ease;
}
.alt-desc__item.is-active { opacity: 1; pointer-events: auto; }

.alt-proj__meta { display: flex; flex-wrap: wrap; gap: var(--space-3xs) var(--space-s); align-items: baseline; }
/* Explicit, for the same reason as .meta-row .who — it was inheriting. */
.alt-proj__co { font-size: 13px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ink); }
.alt-proj__yr { font-size: 13px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.alt-proj__tag { font-size: 13px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-faint); }
/* With the headline gone this is the project's whole voice, so it runs a step
   larger than a supporting blurb would. */
.alt-proj__blurb { margin-top: var(--space-s); max-width: 46ch; }

/* One scale for every block of running copy on the page: the two lines that
   open a section, the paragraph under them, and a project's description. They
   are the same voice, so the size is declared once here rather than as three
   scales that have to be kept in step by hand.

   Tracking is an absolute value, so it stays constant as these blocks scale
   with the viewport rather than growing with the type. */
.alt-lede__small, .alt-lede__big, .alt-proj__blurb {
  font-size: clamp(1.05rem, 1.25vw, 1.35rem);
  line-height: 1.5;
  letter-spacing: -.5px;
  color: var(--body-text);
}
.alt-proj__cta { display: inline-block; margin-top: var(--space-m); font-size: 15px; font-weight: 600; }


@media (prefers-reduced-motion: reduce) {
  .alt-cursor { transition: none; }
  /* The dim stays: a cross-fade between two resting states is not the kind of
     motion this preference is asking about, and it is the only thing saying
     which tile the strip has settled on. The mobile card's scroll-driven
     growth does go — see the end of the file. */
}
/* no pointer to replace on touch */
@media (hover: none) {
  .alt-hero .alt-lede__big { cursor: auto; }
  .alt-cursor { display: none; }
}

/* the contact links these used to style now live in the masthead */
/* .alt-icon lives in masthead.css — it is chrome, on every page. */

/* ----------------------------- writing table -----------------------------
   Sits above the giant name (z-index) on an opaque ground, so the name from
   the last project can't bleed over the table during the hand-off. Uses the
   same grid as work/about with an empty image column — no image here, so the
   wider left margin is what keeps the table on the page's grid. */
.alt-writing {
  position: relative; z-index: 3; background: var(--card);
  /* a final snap point so mandatory snap doesn't trap you on the last image;
     flush to the viewport top like about, so the about slide clears completely */
  scroll-snap-align: start;
  scroll-margin-top: calc(-1 * var(--focus-top));
  padding-top: var(--focus-top);
  padding-bottom: var(--space-fluid-m);
}
.alt-w { width: 100%; border-collapse: collapse; margin-top: var(--lede-gap); }
.alt-w tr { border-top: 1px solid var(--line); }
.alt-w tr:last-child { border-bottom: 1px solid var(--line); }
.alt-w td { padding-block: clamp(1rem, 2.5vw, 1.6rem); vertical-align: baseline; }
.alt-w__title a {
  font-size: clamp(1.2rem, 2.2vw, 1.9rem); font-weight: 600; letter-spacing: -.02em;
}
.alt-w__platform {
  text-align: right; white-space: nowrap; width: 1%;
  font-size: 13px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-faint);
}

/* --------------------------------- footer --------------------------------
   Left-aligned throughout: the wayfinding title and its dots are fixed over the
   bottom-right corner, so nothing here may run into that lane. */
/* .alt-footer lives in masthead.css — it is chrome, on every page. */




/* ================================ bookshelf ==============================
   Two rows of covers drifting in opposite directions. Edge to edge: the rows
   deliberately ignore the page gutter and run the full width, so covers leave
   and enter past the screen rather than stopping at a margin — that is what
   makes it read as a shelf continuing beyond the frame rather than a strip.

   The heading above keeps the grid; only the rows break it.
   ========================================================================= */
.alt-shelf {
  position: relative; z-index: 3; background: var(--card);
  /* the last section, so it inherits writing's old job of leaving room for the
     footer — the final snap shows the shelf AND the footer in one screen.
     --footer-h is measured by alt.js. */
  scroll-snap-align: start;
  scroll-margin-top: calc(-1 * var(--focus-top));
  padding-top: var(--focus-top);
  padding-bottom: var(--space-fluid-m);
  min-height: calc(100svh - var(--footer-h, 7rem));
  overflow: hidden;                 /* the rows run wider than the page */
}

.alt-shelf__rows { margin-top: var(--lede-gap); }
.alt-shelf__row + .alt-shelf__row { margin-top: var(--space-s); }
.alt-shelf__row {
  --shelf-gap: 16px;
  /* Wide enough to clear the section rail. Measured at 1903: the dots sit at
     38-78 and an open label pill reaches 277, so the covers must be well faded
     out by then or the rail reads against book art. */
  --shelf-fade: clamp(72px, 15vw, 280px);
  overflow: hidden;
  /* Fades both edges to the page ground, so covers arrive and leave rather
     than being cut off, and the rail always has quiet ground under it. */
  -webkit-mask-image: linear-gradient(to right, transparent 0,
    #000 var(--shelf-fade), #000 calc(100% - var(--shelf-fade)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0,
    #000 var(--shelf-fade), #000 calc(100% - var(--shelf-fade)), transparent 100%);
}
.alt-shelf__track {
  display: flex; gap: var(--shelf-gap);
  width: max-content; margin: 0; padding: 0; list-style: none;
  /* One run plus one gap. Half the track is one run plus HALF a gap, because
     2n covers have 2n-1 gaps — hence the correction, without which the loop
     jumps by 8px every cycle. */
  animation: alt-shelf-drift var(--shelf-duration, 200s) linear infinite;
}
.alt-shelf__row--rtl .alt-shelf__track { animation-direction: reverse; }

/* Both rows stop together while either is hovered — one frozen row beside one
   still drifting reads as a glitch rather than a pause.

   Scoped to .alt-shelf__rows, NOT the section. The section is a full screen
   tall (it reserves room for the footer), so hovering it would mean "the
   pointer is anywhere on screen" and the shelf would sit paused almost
   permanently. The rows are the thing you actually hover to look at. */
.alt-shelf__rows:hover .alt-shelf__track { animation-play-state: paused; }

@keyframes alt-shelf-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-50% - var(--shelf-gap) / 2)); }
}

.alt-shelf__track li { flex: none; }
.alt-shelf__track img {
  display: block;
  width: clamp(84px, 8vw, 120px); height: auto;
  aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: 4px; background: var(--line);
  box-shadow: var(--shadow-raised);
}

/* Motion this large is the first thing to go. Paused, the rows become an
   ordinary horizontal scroller so every cover is still reachable. */
@media (prefers-reduced-motion: reduce) {
  .alt-shelf__track { animation: none; }
  .alt-shelf__row { overflow-x: auto; scrollbar-width: thin; }
}

/* ============================ narrow screens =============================
   ONE mobile block, and it is LAST in the file on purpose.

   It used to sit in the middle, between Writing and the Bookshelf, and that
   quietly broke it: `.alt-shelf`'s own rules are declared below it at equal
   specificity, so every shelf override in here lost to the base rule it was
   meant to replace. The bookshelf kept its 22vh focus-line lead-in and its
   full-viewport min-height on a phone while every other section shed theirs.

   Anything that overrides a section must come after every section. Keep this
   block at the bottom, and keep it the only one.
   ========================================================================= */
@media (max-width: 900px) {
  /* The pinned column goes; the snapping stays. It used to go too, because a
     card was a content-sized block in a free-scrolling stack and snapping to
     one meant landing on an arbitrary strip of page. A card is a whole screen
     now, so a snap point and a card are the same thing and the strip reads as
     a deck.

     Checked before turning this back on: every section below the deck fits in
     one screen at 375×812 — hero 240, Writing 742, Bookshelf 453, footer 64.
     Mandatory snapping strands anything taller than the viewport that sits
     BETWEEN two snap points, and nothing here does. If a section ever grows
     past a screen, it needs its own snap point or this has to drop to
     `proximity`. */
  /* On the root, not on the strip: the card's height and the scroll position it
     snaps to are the same measurement, and one of them lives on <html>. */
  .alt-root { scroll-snap-type: y mandatory; --card-gutter: var(--space-m); }

  /* This is what makes it a deck rather than a slideshow. Without it one hard
     flick carries you through three or four cards and lands wherever the
     momentum ran out; with it the scroll is obliged to stop at the very next
     card, so a flick advances by one — the way a thumb moves one card off a
     stack however hard you push it. */
  .alt-shot { scroll-snap-stop: always; }

  .alt-work__grid { grid-template-columns: 1fr; }
  .alt-work__panel { display: none; }         /* no pinned column on mobile */
  .alt-work__media { gap: 0; }
  /* ---- one card, one screen
     A tile fills the screen under the bar, with the same air above and below
     it that the page already leaves at its sides — `--card-gutter` is the
     wrap's own inline padding, spent as a block margin, so the card sits in an
     even frame rather than a wider one on two sides.

     `svh`, not `vh`: on a phone `vh` is the tallest the viewport ever gets, so
     a `100vh` card is permanently taller than the screen while the browser
     chrome is showing. `svh` is the smallest, which is the one measurement
     that is never wrong. */
  /* The rail's floating "Index" pill is fixed 16px off the bottom and is a 44px
     touch target, so the bottom 60px of the screen is already spoken for. The
     card stops a gutter short of it rather than sliding underneath — otherwise
     the pill sits on the artboard and the last line of the blurb reads through
     it. */
  .alt-shot {
    --rail-pill-zone: 60px;
    height: calc(100svh - var(--masthead-h, 4rem) - 2 * var(--card-gutter)
                 - var(--rail-pill-zone));
    margin-block: var(--card-gutter);
    padding: var(--space-fluid-xs);
    display: flex; flex-direction: column; justify-content: center;
    gap: var(--space-m);
  }
  .alt-shot img { width: 100%; max-width: 100%; }
  .alt-shot__cap { display: block; margin-top: 0; }

  /* The year and the tags are `--ink-faint` — a mid grey that reads as
     secondary against paper and washes out entirely against an artboard. On
     mobile the caption is ON the artboard, so they take the body colour: still
     a step down from the near-black company name, but dark enough to survive
     the gradient.

     Not white. Every one of these ramps is a pale pastel — the blurb is legible
     on them at rgb(69,69,69), which is only true of a light ground — so white
     would take these two lines from hard to read to invisible. */
  .alt-shot__cap .alt-proj__yr,
  .alt-shot__cap .alt-proj__tag { color: var(--body-text); }

  /* No focus line here, so "active" means nothing: the strip is a stack and
     every tile is equally being read. Dimming four of five would just make
     their captions — which are visible at this width — harder to read. */
  .js-shots .alt-shot, .js-shots .alt-shot.is-active { opacity: 1; }

  /* ---- the focus line does not exist here, so nothing may reserve room for it
     Every section positioned itself against --focus-top: the strip started its
     first tile on that line, the pinned description matched it, and Writing and
     the Bookshelf cleared it so their snap landed flush. All of that is
     choreography for a snapping film strip, and the rule above switches the
     snapping off — leaving four 22vh lead-ins holding space for a line nothing
     lands on. Measured at 756: 222px each above the first tile, Writing and the
     Bookshelf.

     Zeroed at the source rather than per section, so a section added later
     cannot inherit the dead space. --runway goes with it: it is the tail of the
     same strip, and its formula subtracts --focus-top, so leaving it would make
     it GROW as the lead-in shrank. */
  .alt-root {
    --focus-top: 0px;
    --runway: 0px;
    /* --focus-top was also doubling as the anchor offset, and it is zero here,
       so this has to be stated rather than inherited — otherwise every snap and
       every rail jump lands underneath the sticky bar.

       The offset is the bar plus one gutter, which is what puts a snapped card
       in the same even frame it is built with: bar, 24, card, 24, screen edge.
       A round 1rem here would have made the gap above a snapped card smaller
       than the gap below it. */
    scroll-padding-top: calc(var(--masthead-h, 4rem) + var(--card-gutter));
  }

  /* One gap between sections, replacing the lead-ins removed above. */
  .alt-work__media,
  .alt-writing,
  .alt-shelf { padding-top: var(--space-fluid-m); }

  /* ---- and nothing may reserve a screen it does not fill
     .alt-hero centred its text in a 38svh band so the first project peeked in
     underneath; .alt-shelf reserved a viewport so the last snap showed the
     shelf and the footer together. Both are snap-strip devices. Without the
     strip they are 126px of slack around a 132px intro and 304px under the
     covers. */
  .alt-hero { min-height: 0; }
  .alt-shelf { min-height: 0; }

  /* ---- the lede takes the whole measure
     75% + a quarter-gap is the desktop two-column measure: image column, gap,
     half the description column. Below the breakpoint there is no second
     column for it to line up against, so it stops being a measure and becomes
     a 24% waste of an already narrow page. One rule, and the intro, Writing
     and the Bookshelf all widen together — which is why the cap lives on
     .alt-lede rather than on the three sections. */
  .alt-lede { max-width: none; }

  /* Writing keeps its snap point here, unlike on desktop. It is the landing the
     deck hands over to: without one, scrolling off the last card falls through
     742px of unsnapped page before the Bookshelf catches it, and mandatory
     snapping spends that whole stretch trying to pull you back to the card you
     just left. */
  .alt-writing { min-height: 0; }

  /* ---- every card grows in and shrinks out
     Scroll-driven, so the size tracks the finger instead of running on a clock
     of its own — scrub back up and it scrubs back down.

     ONE animation across the whole `cover` range, not an entry animation plus
     an exit animation. Two of those on the same element would both be animating
     `transform`, and the second would win outright: with `both` fill the exit
     animation holds its from-value through its entire before-phase, pinning the
     card at full size for the whole of the entry it was supposed to grow
     through. A single set of keyframes with a plateau in the middle is the only
     way to say grow-hold-shrink with one property.

     The plateau is where the card is parked. `cover` runs a viewport plus a
     card — 812 + 640 = 1452 here — and a snapped card sits with its top on the
     88px scroll-padding line, which is 724 of those, or 50%. The hold is 44–56%
     so a card is unambiguously at full size wherever it comes to rest, with
     room for the ratio to shift on other screens before that stops being true.

     A transform, not a height. A scroll-driven animation that changed layout
     would be changing the very scroll length that drives it, and that loop can
     oscillate. The box keeps one size; only its picture moves.

     The @supports gate is not decoration. An unsupported `animation-timeline`
     is dropped on its own, and the `animation` shorthand beside it would
     survive and run on the document timeline — so every card would play a
     one-shot grow on page load, on exactly the browsers that can't scrub it. */
  @supports (animation-timeline: view()) {
    .alt-shot {
      animation: card-scale linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    @keyframes card-scale {
      0%, 20%   { transform: scale(.86); }
      44%, 56%  { transform: scale(1); }
      80%, 100% { transform: scale(.86); }
    }

    /* Scrubbing a large element's size off the scroll position is squarely
       what this preference is about. Every card simply sits at full size. */
    @media (prefers-reduced-motion: reduce) {
      .alt-shot { animation: none; transform: none; }
    }
  }
}
