/* ============================================================
   About page V2 — section studies.
   Loaded after about.css, so it overrides v1 where they overlap.
   Only sections that have been reimagined appear here; everything else on
   index.html is still v1's markup and v1's styling.
   ============================================================ */

/* ---------------------------------------------------------------- W1 wild
 *
 * The section pins and its row pans sideways, driven by vertical scroll.
 *
 * Two numbers do the work, both set in v2.js from the laid-out track rather
 * than assumed:
 *   --pan        how far the row has to travel (trackWidth - viewport)
 *   --scroll-len how much page scroll that takes (--pan / --pan-ratio)
 * Deriving the section's height from the content means it is never longer
 * than the distance it actually needs.
 */

.v2-sec {
  position: relative;
  padding: 0;
}

.wild-scroller {
  --pan: 0px;
  --scroll-len: 0px;
  /* The HEIGHT is what the cards share; each one's width comes from its own
     picture. Capped against the viewport as well as the page, because the
     stage is one screen and has to hold the header and the rail too. */
  --card-h: min(420px, 46.5vw, 44svh);
  /* A nominal width, used only to pad the track's ends by half a card so the
     first and last can reach dead centre. The cards themselves are no longer
     this wide — they are as wide as their pictures. */
  --card-w: min(560px, 62vw);
  /* Above 1 the row moves faster than the finger, which keeps the section
     short — twelve cards at 1:1 would want about six screens of scrolling. */
  --pan-ratio: 1.6;

  position: relative;
  /* One viewport to hold the pinned stage, plus exactly the scroll needed. */
  height: calc(100vh + var(--scroll-len));
  /* Names the timeline the track animates against. */
  view-timeline-name: --wild;
  view-timeline-axis: block;
}

.wild-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(18px, 3vh, 40px);
  /* No ground of its own: the page's gradient runs behind it, so the pinned
     stage does not reintroduce a seam at its edges. */
}

/* Fixed within the stage, so the heading holds still while the row travels.
   Block, not flex: the 01 / 11 readout that sat opposite the title is gone, and
   a lone flex item is only as wide as its text — which would have made the band
   a label-sized box instead of a full-bleed band. The gutter padding here and
   the band's negative gutter margins cancel, so the band spans the stage while
   its text still lines up with the page. */
.wild-head {
  padding: 0 var(--gut);
  display: block;
}
/* No overrides. The title left-aligned and ran smaller because it used to
   share a flex row with the 01 / 11 readout; that readout is gone, and the
   leftovers were the only reason this header did not look like the other
   nine. It takes the shared band as-is. */
/* The 01 / 11 — NAME readout is gone; the rail below carries progress. */

.wild-track {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.4vw, 40px);
  padding: 0 max(var(--gut), calc(50vw - var(--card-w) / 2));
  width: max-content;
  will-change: transform;
}

.wild-card {
  flex: 0 0 auto;
  /* One height for every card, and a width computed from the picture's own
     aspect. NOT intrinsic sizing: `img, video { max-width: 100% }` applies to
     every picture on this page, so a picture cannot contribute its real width
     to a min-content calculation — it is capped by a width that does not exist
     yet — and the card collapsed to the widest WORD in its label, with the
     picture stretched to fill the result. `--ratio` comes from the tile (or
     from the media itself, for the ones the pipeline does not measure). */
  width: calc(var(--card-h) * var(--ratio, 1.3333));
  color: inherit;
  text-decoration: none;
  /* Off-centre cards sit back; the active one comes forward. Transform and
     filter only — nothing here moves the row's layout. */
  transform: scale(0.9);
  filter: brightness(0.45) saturate(0.75);
  transition: transform 0.55s var(--ease-out, ease),
              filter 0.55s var(--ease-out, ease);
}
.wild-card.is-active {
  transform: scale(1);
  filter: none;
}

/* One height, and the picture's own width. A 4:3 box wanted either a crop
   (`cover`) or black bars down the sides (`contain`); a row of mixed aspects at
   one height wants neither. */
.wild-frame {
  position: relative;
  height: var(--card-h);
  overflow: hidden;
  border: 1px solid var(--line);
}
.wild-frame img,
.wild-frame video {
  width: 100%;
  height: 100%;
  display: block;
  /* The frame is already at the picture's own aspect, so this crops nothing —
     it is only insurance against the ratio being rounded. */
  object-fit: cover;
  transform: scale(1.02);
  transition: transform 0.7s var(--ease-out, ease);
}
.wild-card.is-active .wild-frame img,
.wild-card.is-active .wild-frame video { transform: scale(1); }

/* The two-column grid existed only to park a number beside the name. With the
   number gone the name and venue simply stack. */
.wild-meta {
  margin-top: 16px;
}
.wild-name {
  margin: 0;
  color: var(--primary);
  font-size: clamp(15px, 1.9vw, 24px);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.15;
}
.wild-venue {
  display: block;
  color: var(--dim);
  font-size: 12px;
  letter-spacing: 0.1em;
}
/* Only the card in focus advertises that it leads somewhere. */
a.wild-card.is-active .wild-name::after {
  content: ' ↗';
  font-size: 0.75em;
  opacity: 0.8;
}
a.wild-card:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 6px;
}

/* Progress rail along the bottom of the stage. */
.wild-rail {
  position: absolute;
  left: var(--gut);
  right: var(--gut);
  bottom: clamp(18px, 4vh, 44px);
  height: 1px;
  background: rgba(51, 255, 51, 0.18);
}
.wild-rail span {
  display: block;
  height: 100%;
  background: var(--primary);
  transform-origin: 0 50%;
  transform: scaleX(0);
  transition: transform 0.5s var(--ease-out, ease);
}

/* ---- the pan itself ---- */
@keyframes wild-pan {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(var(--pan) * -1), 0, 0); }
}

@supports (animation-timeline: view()) {
  .wild-track {
    animation: wild-pan linear both;
    animation-timeline: --wild;
    /* `contain` is exactly the span where the scroller covers the scrollport,
       which is the same span the stage is stuck for. */
    animation-range: contain 0% contain 100%;
  }
}

/* ---- fallback and small screens ----
 *
 * Pinning is wrong on a phone: the section becomes a tall dead zone and the
 * cards end up too small to read. A native scroll-snap strip is better there
 * anyway — it is swipeable, it is one gesture, and it needs no JS. The same
 * fallback carries browsers without scroll-driven animations, and anyone who
 * has asked for reduced motion.
 */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .wild-scroller { height: auto; view-timeline-name: none; }
  .wild-stage {
    position: static;
    height: auto;
    padding: var(--sec-pad) 0;
    overflow: visible;
  }
  .wild-track {
    animation: none !important;
    transform: none !important;
    width: auto;
    padding: 4px var(--gut) 20px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .wild-track::-webkit-scrollbar { display: none; }
  .wild-scroller { --card-w: min(420px, 78vw); --card-h: min(300px, 58vw); }
  .wild-card {
    scroll-snap-align: center;
    /* Nothing is "active" without the pin, so every card reads fully. */
    transform: none;
    filter: none;
  }
  .wild-frame img, .wild-frame video { transform: none; }
  .wild-rail { display: none; }
  .wild-head { display: block; }
  /* The stage drops its timeline above, so the band animations have nothing to
     run against. Left alone they hold their from-state and the header never
     appears at all. */
  .wild-head .sec-title,
  .wild-head .sec-title .band-text {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .wild-head .sec-title::before, .wild-head .sec-title::after {
    animation: none !important;
    transform: scaleX(1) !important;
  }
}

@supports not (animation-timeline: view()) {
  /* Same reasoning: with no timeline the pin would just stick and never pan. */
  .wild-scroller { height: auto; }
  .wild-stage { position: static; height: auto; padding: var(--sec-pad) 0; overflow: visible; }
  .wild-track { width: auto; overflow-x: auto; scroll-snap-type: x mandatory; }
  .wild-card { transform: none; filter: none; scroll-snap-align: center; }
  .wild-rail { display: none; }
}

/* ---------------------------------------------------------------- W2 typing
 *
 * The three prose blocks type out as you scroll, Apple II style: green
 * phosphor, and a solid block cursor that inverts the cell it sits in rather
 * than being a thin bar.
 *
 * PINNED. The header rides up the screen, anchors at the top, and the text
 * prints as you keep scrolling; when it is finished the page carries on.
 *
 * Two earlier attempts at this were abandoned over a real problem: a pinned
 * stage needs a scroller taller than itself, and that surplus showed up as a
 * blank band between the paragraph and whatever came next. The fix is the
 * stage's height. Sized to its CONTENT it is shorter than the window, so the
 * surplus sits below it in plain view. Sized to the WINDOW it covers the
 * viewport for the whole hold, and at the frame the pin releases the stage's
 * bottom edge and the scroller's bottom edge are the same line — the surplus
 * has been spent and there is nothing left to see.
 *
 * Still no scroll hijacking, which is what made the original version worth
 * keeping: no preventDefault, no wheel handler, no synthetic scrolling. The
 * section is simply tall enough to need that much scrolling, with a sticky
 * stage inside. Momentum, trackpads, paging, find-in-page and the scrollbar
 * all keep working, and scrolling back up un-types.
 */
.type-scroller {
  position: relative;
  /* One screen of stage plus the hold the typing spends. The stage is exactly
     one window, so while it is pinned it covers the viewport completely and the
     hold is never visible as empty space — at the frame the pin releases, the
     stage's bottom edge and this box's bottom edge are the same line. */
  --type-hold: 72svh;
  height: calc(100svh + var(--type-hold));
}

/* Everything in the section is in here, so nothing moves while the line prints
   and the whole thing leaves together: the prompt line, the header, and the
   picture under them. */
.type-stage {
  position: sticky;
  top: 0;
  z-index: 45;
  height: 100svh;
  /* It is a `.plate` as well, which is where the column, the ground and the
     gapless stacking come from. The picture below the band is the only thing a
     stage has that the other plates do not. */
}
/* Only the picture gives. An explicit `min-height` on a flex item REPLACES
   the automatic content-based minimum, so without this the prompt line was free
   to be squeezed below the height of its own text — and the band underneath,
   opaque and later in the DOM, painted over what spilled out. */
/* Only the picture gives. An explicit `min-height` on a flex item REPLACES the
   automatic content-based minimum, so without this the prompt line was free to
   be squeezed below the height of its own text — and the band underneath,
   opaque and later in the DOM, painted over what spilled out. */
.plate > .quest-text,
.plate > .sec-title { flex: none; }
/* Room under the band before whatever follows it — the picture in a stage, the
   foam hat's bands, the team grid. It was flush against the lower rule. Inside
   the plate, so the gap is the plate's own ground and not a hole. */
.plate > .sec-title { margin-bottom: clamp(10px, 1.8vh, 26px); }

/* The prompt line, in the strip the caret occupies, above the band. The band's
   own top rule is the line between the two halves of the plate.

   A BLOCK, and that is load-bearing. As a flex container this silently deleted
   every space in the line: flexbox wraps each contiguous run of child text in
   an anonymous flex item and does not render one that is only white space, and
   the typewriter emits spaces as bare text nodes so that lines can break. Every
   space vanished and every character became its own flex item on a single
   unwrappable row. */
/* The prompt line, on every plate. */
.plate > .quest-text {
  display: block;
  /* Its own, because it reaches past the plate's. The plate is inside the
     section's gutter padding and this pulls back out of it, so the plate's
     background stops short of both ends of the strip. */
  background: var(--band-bg);
  margin: 0 calc(var(--gut) * -1);
  padding: clamp(10px, 1.5vh, 18px) var(--gut) clamp(10px, 1.5vh, 18px)
           clamp(54px, 5.2vw, 82px);
  min-height: var(--head-top);
  max-width: none;
  text-align: left;
  /* `pretty`, NOT `balance`. Balance evens the lines out, which is how two long
     lines became three short ones. Pretty leaves them long and only pulls the
     last one back when a single word would be stranded on it — which was the
     whole complaint. Two lines at full width, three if the window is narrow,
     and nothing moves as it types because the cursor's cell reserves its space
     from the start. */
  text-wrap: pretty;
  /* A hair smaller and a hair tighter, which is what keeps the default at two
     lines rather than two and an orphan. */
  font-size: clamp(12px, 1.04vw, 14.5px);
  letter-spacing: 0.008em;
}

/* The two short lines take the welcome line's size — they are a few words over
   a full-bleed band, and at prose size they read as a caption that got lost up
   there. The prose lines keep theirs: it is what holds them to two lines across
   a full-width window. */
.sec-hat .plate > .quest-text,
.team-head > .quest-text {
  font-size: clamp(13px, 2.1vw, 22px);
  letter-spacing: 0.06em;
}

/* The two plates that are not typing stages: the foam hat's header and the
   sign-off. Same object — a line above, the band under it, one ground across
   both — so they take the stage's own rules above and only need to be told
   they are a column with a background. */
.plate {
  display: flex;
  flex-direction: column;
  background: var(--band-bg);
  gap: 0;
  /* OUT of the section's gutter, and the gutter given back as padding. The band
     and the strip inside pull out of it with negative margins of their own,
     which now cancel THIS instead of the section's — so plate, band and strip
     all end on the same edges. Inset, the plate's ground stopped a gutter short
     and the page's gradient showed in the gap: `#28272a` against `#1d1c1f`,
     which is the lighter border down both sides. */
  margin-inline: calc(var(--gut) * -1);
  padding-inline: var(--gut);
}
/* These two sections strip their own gutters, so there is none to come out of —
   but the padding stays, because the band inside still cancels it. One
   exception on the plate replaces one on the band and one on the line. */
.sec-office .plate,
.sec-hat .plate { margin-inline: 0; }
/* Inside a plate the gutter is the plate's; saying it again here doubles it. */
.plate > .cta-row { padding-inline: 0; }

/* The picture takes the height the plate has not used.

   `min-height: 0` is not optional: a flex item will not shrink below its
   content's intrinsic height without it, so the picture would push the stage
   past one window and the hold would start showing as a blank band. */
.type-stage > .bleed,
.type-stage > .office-banner {
  flex: 1 1 auto;
  min-height: 0;
  /* Full bleed inside the padded plate, the same way the band is. `.bleed`
     carries this itself already; the office banner never had to, because its
     section used to have no gutter at all. */
  margin-inline: calc(var(--gut) * -1);
  margin-top: 0;
  /* The picture is positioned against this box rather than sized as a
     percentage of it: `height: 100%` needs a definite height to resolve
     against, and whether a flex item's counts as definite depends on how it was
     sized — so the same rule resolves in one section and quietly becomes `auto`
     in the next. */
  position: relative;
  overflow: hidden;
}
.type-stage > .bleed img,
.type-stage > .office-banner img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* THE TRADE. A full-width 16:9 picture is about 90% of a typical window's
     height on its own, so it and a header cannot both be whole on one screen —
     anchoring the picture costs either width or a crop, and full width was the
     ask. Biased downward because these scenes put their subject low: the office
     gives up ceiling, not desk. Swap to `contain` for the whole picture at
     roughly three quarters of the width. */
  object-fit: cover;
  object-position: center 62%;
  transform: none;
  animation: none;
}

/* The carousel is this section's picture. Sized by the height left over rather
   than by its own width — a 620px square under a header does not fit a short
   window — and in svh rather than per cent, because `.carousel-reel` is a flex
   item under `align-items: center`, never stretched, so a percentage height
   there would quietly mean `auto`. */
.type-stage > .carousel {
  /* Grows as well as shrinks. At `0 1 auto` the bottom of the stage went
     unused — and the stage is exactly one window, so that space was dead air
     between the link and the next section. Growing puts the caption and the
     link at the foot of the stage. */
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 0;
  /* And the row stretches rather than centring, which is what gives the reel a
     definite height for the frame below to be a percentage of. */
  align-items: stretch;
}
/* A chain of definite heights, rather than a guessed `svh` cap that has to be
   safe on the shortest window and is therefore too small on every other one.
   The row stretches, so the reel has a definite height; the frame takes that
   height and `aspect-ratio: 1` turns it back into a width. The result fills
   whatever the stage has left at any size. */
.type-stage .carousel-nav { align-self: center; }
.type-stage .carousel-reel {
  height: 100%;
  align-items: center;
}
.type-stage .carousel-stage {
  width: auto;
  height: 100%;
  /* Still bounded, or on a tall window it would outgrow the page's width. */
  max-width: min(760px, 52vw);
}
.type-stage .carousel-side { height: 56%; width: auto; }
.type-stage > .carousel-caption,
.type-stage > .cta-row { flex: 0 0 auto; }

@supports (animation-timeline: view()) {
  /* Everything in the plate that arrived on `view()` has to come off it: each
     would freeze part-way the instant the plate anchors (trap 2), and the
     plate's own arrival covers them. The ids are the carousel's two blurred
     flankers, and an id-beating selector is what it takes. */
  .plate .quest-text,
  .type-stage .carousel-caption,
  .type-stage .carousel-stage,
  .type-stage .office-banner,
  .type-stage .office-banner img,
  .type-stage #carousel-side-prev,
  .type-stage #carousel-side-next {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* No timelines, or no appetite for motion: the plate would stick and never
   release, so there is nothing to pin for. Everything goes back to flowing. */
@supports not (animation-timeline: view()) {
  .type-scroller { height: auto; }
  .type-stage { position: static; height: auto; }
}
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .type-scroller { height: auto; }
  .type-stage { position: static; height: auto; }
  /* Unpinned there is no caret to type after and no plate to be the top half
     of, so the line is a centred paragraph again. */
  .plate > .quest-text {
    /* `auto` both cancels the negative gutter and re-centres it. */
    margin-inline: auto;
    padding-left: var(--gut);
    min-height: 0;
    max-width: min(980px, 92vw);
    text-align: center;
  }
  /* And the picture is back to its own aspect rather than filling a height
     that no longer exists — nothing is cropped here. */
  .type-stage > .bleed,
  .type-stage > .office-banner { position: static; }
  .type-stage > .bleed img,
  .type-stage > .office-banner img {
    position: static;
    height: auto;
    object-fit: fill;
  }
  .type-stage .carousel-stage { width: min(620px, 50vw); height: auto; }
  .type-stage .carousel-side { width: min(250px, 20vw); height: auto; }
  .plate .sec-title:not(.visually-hidden),
  .plate .sec-title:not(.visually-hidden) .band-text {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* The cursor waits for the anchor. Built into the first cell from the start,
   it rode into view blinking long before the plate had landed and long before
   there was anything to type. It appears with the first character now — the
   same moment the caret in the corner turns back into a prompt — so the two
   arrive together. Once the line is finished the cursor stays, still blinking:
   the terminal is waiting for the next line, not done with you. */
.type-out.is-waiting .c.cur {
  /* `opacity` is the one that matters and the one I left out. An Apple II
     cursor INVERTS the cell it sits in rather than sitting between cells, so
     the cursor and the first character are the same box — taking away the
     background, the blink and the glow still left the letter visible, riding
     into view ahead of everything else. */
  opacity: 0;
  background: none;
  color: inherit;
  box-shadow: none;
  animation: none;
}
.type-out.is-waiting .c.end.cur::before { content: none; }

/* Every character is present from the start and only changes visibility, so
   the paragraph never reflows and the page cannot shift under the reader. */
.type-out { white-space: normal; }
.type-out .c {
  /* Deliberately NOT inline-block: an atomic inline is a break opportunity, so
     every character would become somewhere the line may wrap and words would
     split down the middle. Plain inline spans leave line breaking to the text,
     which is why spaces are emitted as text nodes rather than cells. */
  opacity: 0;
  /* No transition: a fade reads as a reveal. A character on a terminal is
     either drawn or it is not. */
}
.type-out .c.on { opacity: 1; }
/* The cursor's resting place past the last character. It holds that space from
   the start, not from when the cursor arrives in it. Reserving it late is a
   reflow of the whole paragraph on the LAST keystroke: the line that fitted all
   the way through the typing suddenly does not, and the text jumps from two
   lines to three. Every other character reserves its width up front; this was
   the one that did not. */
.type-out .c.end { opacity: 1; }
.type-out .c.end::before { content: '\00a0'; }
.type-out .c.cur {
  opacity: 1;
  background: var(--primary);
  color: var(--ground-deep);
  box-shadow: 0 0 12px rgba(51, 255, 51, 0.55);
  animation: apple-cursor 0.62s steps(1, end) infinite;
}
@keyframes apple-cursor {
  0%, 49%   { background: var(--primary); color: var(--ground-deep); }
  50%, 100% { background: transparent; color: var(--primary); }
}
/* The cursor stays put at the end of the text, still blinking — the terminal
   is waiting for the next line, not finished with you. */

/* Phosphor. The glow is on the block as a whole rather than per character —
   750 text-shadows would be paid on every repaint. */
.type-out {
  color: var(--primary);
  text-shadow: 0 0 18px rgba(51, 255, 51, 0.28);
  cursor: pointer;
}
.type-out .c { color: inherit; }

@media (prefers-reduced-motion: reduce) {
  .type-out .c { opacity: 1; }
  .type-out .c.cur { animation: none; background: transparent; color: inherit; box-shadow: none; }
  .type-out { cursor: auto; }
}

/* ---------------------------------------------------------------- W2 the pile
 *
 * A heap of memes on a table, swept off as the section passes.
 *
 * The section pins, and each card runs its own from/to transform against the
 * scroller's timeline — from its place in the heap to somewhere past the edge,
 * spinning. Every from/to pair is a custom property set once in v2.js from a
 * seeded PRNG, so the mess is the same mess every load.
 *
 * --lag staggers the departure by depth: the card on top leaves first and the
 * one at the bottom last, so the pile peels instead of exploding.
 */
.pile-scroller {
  position: relative;
  /* One screen holds the pinned stage, one more scatters the heap across it.
     Shorter than before because nothing has to travel off-screen now. */
  height: 190vh;
  view-timeline-name: --pile;
  view-timeline-axis: block;
}
.pile-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: grid;
  place-items: center;
  /* Same: the continuous page ground shows through. */
}
/* Qualified with the stage so this is two classes, the same weight as the band
   rule it has to overrule — at one class every margin below was discarded and
   the title kept the band's negative gutters. v2.css is linked after
   about.css, so an equal-weight rule here wins. */
.pile-stage .pile-title {
  position: absolute;
  /* Use the existing short-passage strip, not a separate viewport offset
     that puts this band through the persistent terminal prompt. */
  top: var(--meme-header-top, 132px);
  /* left/right already span the stage, so the band's negative gutter margins
     would make the box a gutter wider than the screen at each edge and leave
     the stage to clip the difference. */
  left: 0;
  right: 0;
  margin: 0;
  z-index: 60;          /* the heap flies past it, not over it */
  pointer-events: none;
}

/* The table: a square field the cards are dealt into, centred in the stage. */
.pile {
  position: relative;
  width: min(78vmin, 900px);
  aspect-ratio: 1;
}
.pile-card {
  position: absolute;
  inset: 0;
  margin: auto;
  /* Each card is its image's own shape, set in v2.js from the manifest ratio.
     A square frame cropped the wide and tall memes down to blank margin. */
  width: var(--cw, 27vmin);
  height: var(--ch, 27vmin);
  /* The resting position in the heap. The scatter animation overrides this
     transform wholesale, so both states live in the same property and nothing
     fights over it. */
  transform:
    translate3d(var(--x0, 0), var(--y0, 0), 0)
    rotate(var(--r0, 0deg))
    scale(var(--s, 1));
  will-change: transform;
}
.pile-card img {
  width: 100%;
  height: 100%;
  /* The frame already matches the image's ratio, so this only absorbs
     sub-pixel rounding; there is nothing left to crop. */
  /* `contain`, not `cover`. Cover fills the box by cutting the picture
     off — which is what was happening to the office banner. */
  object-fit: contain;
  display: block;
  /* Print edges: a white border and a dropped shadow are what make a stack of
     images read as a stack of physical things rather than a collage. */
  border: clamp(4px, 0.7vmin, 9px) solid #f2f2ee;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
}

/* A card with a tweet behind it.
   The affordance goes on the IMAGE, never on the card: the card's transform is
   the scatter animation, and a hover transform there would fight it for the
   same property and jump the card out of the pile. */
a.pile-card {
  cursor: pointer;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.pile-card img { transition: filter 0.18s ease, box-shadow 0.18s ease; }
a.pile-card:hover img,
a.pile-card:focus-visible img {
  filter: brightness(1.08);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.7);
}
/* Focus has to be visible on a rotated card lying under other cards, so it is
   an outline on the image's own edge rather than a ring around the box. */
a.pile-card:focus-visible {
  outline: none;
}
a.pile-card:focus-visible img {
  outline: 3px solid var(--primary, #33ff33);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .pile-card img { transition: none; }
}

/* Two stops: heap, then the scattered table. There is no exit — the cards do
   NOT fly off. They spread until the table is full and messy and then stay put,
   and the page carries on scrolling past a table that is still covered. Sending
   them off-screen left the reader looking at nothing. */
@keyframes pile-sweep {
  from {
    transform:
      translate3d(var(--x0, 0), var(--y0, 0), 0)
      rotate(var(--r0, 0deg))
      scale(var(--s, 1));
  }
  to {
    transform:
      translate3d(var(--x1, 0), var(--y1, 0), 0)
      rotate(var(--r1, 0deg))
      scale(var(--s, 1));
  }
}

/* The band on a pinned header, driven by the section rather than by itself.
   Same look and same motion as every other header; only the clock differs. */
@supports (animation-timeline: view()) {
  .pile-stage .pile-title,
  .wild-stage .wild-head .sec-title {
    animation: band-in linear both, band-out linear forwards;
  }
  .pile-stage .pile-title {
    animation-timeline: --pile, --pile;
    /* Out across the middle of the scatter, not after it. The heap finishes
       spreading at contain 78% (58% for the card on top, plus the 20% of lag
       carried by the one at the bottom), so at 85-99% the title was still
       sitting over a table that had already filled. */
    animation-range: entry 20% entry 80%, contain 34% contain 56%;
  }
  .wild-stage .wild-head .sec-title {
    animation-timeline: --wild, --wild;
    animation-range: entry 20% entry 80%, contain 85% contain 99%;
  }
  .pile-stage .pile-title::before,
  .pile-stage .pile-title::after {
    animation-timeline: --pile;
    animation-range: entry 34% entry 92%;
  }
  .wild-stage .wild-head .sec-title::before,
  .wild-stage .wild-head .sec-title::after {
    animation-timeline: --wild;
    animation-range: entry 34% entry 92%;
  }
  .pile-stage .pile-title .band-text {
    animation-timeline: --pile;
    animation-range: entry 28% entry 88%;
  }
  .wild-stage .wild-head .sec-title .band-text {
    animation-timeline: --wild;
    animation-range: entry 28% entry 88%;
  }
}

@supports (animation-timeline: view()) {
  .pile-card {
    animation: pile-sweep linear both;
    animation-timeline: --pile;
    /* Spread over the first two thirds of the pin, so the last third is spent
       looking at the full table before the page moves on. --lag staggers the
       starts by depth so the heap peels top-down instead of blooming at once. */
    animation-range:
      contain calc(3% + (var(--lag, 0) * 16%))
      contain calc(58% + (var(--lag, 0) * 20%));
  }
}

/* ---- unpinned: phones, no scroll timelines, reduced motion ----
 * A pile that needs 2.6 screens of scroll to clear is wrong on a phone, and a
 * heap that never scatters is just an unreadable stack. Fall back to the thing
 * that always works: the images, laid out, all visible.
 */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .pile-scroller { height: auto; view-timeline-name: none; }
  .pile-stage {
    position: static;
    height: auto;
    padding: var(--sec-pad) var(--gut);
    overflow: visible;
    display: block;
  }
  /* Two classes, to stay above the pinned rule — a media query adds no
     specificity of its own.

     RELATIVE, not static. The band's two rules are absolutely positioned
     `left: 0; right: 0`, and an absolute box measures its nearest POSITIONED
     ancestor — so a static title handed them the section instead, and the
     bottom rule went to the bottom of every meme on the page. `static` was
     copied down from unpinning the STAGE, which does need it; the title was
     never sticky. */
  .pile-stage .pile-title {
    position: relative;
    top: 0;
    margin: 0 calc(var(--gut) * -1) clamp(24px, 5vh, 48px);
  }
  /* The stage drops its timeline above, so the band animations have nothing to
     run against. Left alone they hold their from-state and the header never
     appears at all. */
  .pile-stage .pile-title,
  .pile-stage .pile-title .band-text {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .pile-stage .pile-title::before, .pile-stage .pile-title::after {
    animation: none !important;
    transform: scaleX(1) !important;
  }
  .pile {
    width: auto;
    aspect-ratio: auto;
    columns: 2;
    column-gap: 10px;
  }
  .pile-card {
    position: static;
    inset: auto;
    width: auto;
    height: auto;
    margin: 0 0 10px;
    transform: none !important;
    animation: none !important;
    break-inside: avoid;
  }
  .pile-card img { height: auto; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45); }
}
@supports not (animation-timeline: view()) {
  .pile-scroller { height: auto; }
  .pile-stage {
    position: static; height: auto; overflow: visible; display: block;
    padding: var(--sec-pad) var(--gut);
  }
  /* Two classes, to stay above the pinned rule — a media query adds no
     specificity of its own.

     RELATIVE, not static. The band's two rules are absolutely positioned
     `left: 0; right: 0`, and an absolute box measures its nearest POSITIONED
     ancestor — so a static title handed them the section instead, and the
     bottom rule went to the bottom of every meme on the page. `static` was
     copied down from unpinning the STAGE, which does need it; the title was
     never sticky. */
  .pile-stage .pile-title {
    position: relative;
    top: 0;
    margin: 0 calc(var(--gut) * -1) clamp(24px, 5vh, 48px);
  }
  .pile { width: auto; aspect-ratio: auto; columns: 3; column-gap: 12px; }
  .pile-card {
    position: static; inset: auto; width: auto; height: auto;
    margin: 0 0 12px; transform: none !important; break-inside: avoid;
  }
  .pile-card img { height: auto; }
}
