/* ============================================================
   SHOWCASE  —  sticky stacking cards

   Three cards pin one after another and pile up, each landing a
   step lower than the last so a band of every earlier card stays
   visible. JS eases the buried ones back in scale and tilt, which
   is what makes the pile read as depth rather than as overlap.

   They are SIBLINGS in one container on purpose: a sticky element
   is released by its own containing block, so a card-per-slot
   layout let each card go the moment its slot scrolled past and no
   pile ever formed. See the note in showcase.js.

   `overflow` must not be set to `hidden` on any ancestor of these
   cards: that would create a scroll container and silently kill
   `position: sticky`. The reset uses `overflow-x: clip` on
   html/body for exactly this reason.
   ============================================================ */

.showcase {
  position: relative;
  z-index: var(--z-raised);
  background-color: var(--c-bg);
  padding-block: var(--sp-section);

  border-start-start-radius: var(--r-section);
  border-start-end-radius: var(--r-section);
  /* pulled up over the white panel above */
  margin-block-start: calc(var(--r-section) * -0.6);
}

.showcase__title {
  text-align: center;
  margin-block-end: var(--sp-heading-gap);
}

/* ---- the pile --------------------------------------------------

   .showcase__list is the shared containing block. Every card is a
   sticky child of it, so none of them is released until the LIST
   runs out — that is what lets them accumulate instead of each
   being let go by a slot of its own.
   ---------------------------------------------------------------- */

.showcase {
  /* How far below the previous card each new one lands, and so how
     much of every earlier card stays visible in the finished pile.

     28px was effectively a full cover — a 484px card showing a 28px
     sliver reads as one card replacing another, not as a stack. The
     upper end shows roughly an eighth of each card behind. It
     shrinks with the viewport because on a short window the total
     offset comes straight out of the height the top card has to
     live in. */
  --stack-step: clamp(32px, 6vh, 60px);

  /* Scroll distance between one card landing and the next, and —
     since there is a spacer after the LAST card too — the window
     the finished pile is held on screen for. Roughly a viewport, so
     each card gets its own moment. */
  --card-gap: 55vh;
}

.showcase__card {
  position: sticky;

  /* Each card sits one step lower than the one before it. --i is
     written into the markup by showcase.js; the JS reads this same
     computed `top` back when it measures, so the two can never
     disagree about where a card lands. */
  top: calc(var(--stick-top) + var(--i, 0) * var(--stack-step));

  /* No margin here — see .showcase__gap below. */
}

/* The scroll runway. A plain empty box, and it has to be a box.

   TWO WAYS THIS WAS TRIED AND MEASURED WRONG, both worth keeping
   written down because both look correct:

   1. `padding-block-end` on .showcase__list. A sticky box is
      constrained to its containing block's CONTENT box, and
      padding is outside it — so this added no range at all. The
      last card unpinned 12px after landing.

   2. `margin-block-end` on the cards. It is the card's own MARGIN
      box that has to stay inside the containing block, so a margin
      is subtracted from the very range it is trying to create.
      Worse than useless: card 2 ran out and unpinned 36px BEFORE
      card 3 landed, so the three were never up together.

   A sibling's height counts towards the containing block and
   belongs to no card's margin box, so every card keeps its full
   range. Structural only — no content, nothing to announce. */
.showcase__gap {
  block-size: var(--card-gap);
}

.showcase__card {
  display: flex;
  flex-direction: column;
  gap: clamp(1rem, 2.5vw, 2rem);

  /* The deepest card in the pile has to fit on screen BELOW its own
     sticky offset, or it is cut off by the bottom of the viewport
     and the effect looks broken rather than deep.

     This replaces a cap derived from the old slot height. The
     constraint was never really the slot — it is the viewport, less
     the space the pile itself occupies. */
  max-block-size: calc(
      100svh
      - var(--stick-top)
      - (var(--card-count, 3) - 1) * var(--stack-step)
      - 2rem);

  padding: clamp(1rem, 2vw, 2rem);

  /* Surface, edge and weight all come from tokens so the light
     theme can give the panel a fill and a heavier rule without
     touching the dark treatment, where a bright 2px line on the
     page colour is the whole look. */
  background-color: var(--c-panel);
  border: var(--bw-panel) solid var(--c-panel-border);
  border-radius: var(--r-section);

  /* Lifts the pile off the page. On dark this barely registers,
     which is correct — the border does that job there. */
  box-shadow: var(--shadow-panel);

  /* --sc and --rot are written by showcase.js each frame, and they
     meet in ONE transform declaration — two rules each naming
     `transform` would mean the later one silently wins and the
     other effect would never appear.

     `top center` origin so a card shrinks and tilts around the edge
     that stays visible above the card landing on it, rather than
     pivoting about its middle and sliding that edge out of view. */
  /* --lift is the arrival: the card rises the last half-viewport
     into place, so landing finishes a movement instead of stopping
     a scroll. It joins the same single declaration for the reason
     above — a second rule naming `transform` would silently win. */
  transform: translateY(var(--lift, 0px)) scale(var(--sc, 1)) rotate(var(--rot, 0deg));
  transform-origin: top center;
  will-change: transform;
}

/* THE SHADE

   A card that has been covered should sit in the shadow of the one
   above it. Scale and tilt alone read as "smaller and turned"; only
   light reads as "further away". --veil is written by showcase.js
   and deepens with how far down the pile the card has fallen.

   A scrim rather than `filter: brightness()`, because filter would
   make this sticky element a containing block and a stacking
   context — the class of change that breaks pinning without
   throwing anything. */
.showcase__card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background-color: rgb(0 0 0 / var(--veil, 0));
  pointer-events: none;
}

.showcase__card { position: sticky; }   /* re-stated: ::after needs a positioned parent */

.showcase {
  --stick-top: 6rem;
}

@media (min-width: 768px) {
  .showcase { --stick-top: 8rem; }
}

/* ---- card head ------------------------------------------------ */

.showcase__head {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 2rem);
  flex-wrap: wrap;

  /* Moves AGAINST the images: --drift gives it a slow counter-drift
     while the card is pinned, --vel the fast one while the reader is
     moving. Same direction as the images and the two would read as
     one block sliding; opposed, the card has depth inside it. */
  transform: translateY(calc(
      var(--drift, 0) * -5px + var(--vel, 0) * -0.22px));
}

.showcase__num {
  flex: none;
}

.showcase__meta {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
  /* takes the slack so the button is pushed to the far end */
  margin-inline-end: auto;
}

/* ---- card head typography -------------------------------------
   This is the section that presents three hand-picked projects,
   but its text was sized like a list item. Measured at 1280px:
   the numeral rendered at 38px while the project's own name sat at
   19px and its category at 14px — the decoration was twice the
   size of the thing it was decorating, and the category read as
   fine print.

   Raised so the name leads the card. The numeral is unchanged, so
   the gap between them closes from 2.0x to 1.6x.
   ---------------------------------------------------------------- */

.showcase__cat {
  font-size: var(--fs-lead);
  font-weight: var(--fw-light);
  color: var(--c-text-dim);
}

.showcase__name {
  font-size: var(--fs-subhead);
  font-weight: var(--fw-medium);
  line-height: var(--lh-snug);
}

/* Scoped to this card rather than raised on .btn globally — the
   hero and footer CTAs are sized for where they sit and were not
   what looked undersized. */
.showcase__head .btn {
  font-size: var(--fs-lead);
}

/* ---- card images ---------------------------------------------- */

/* Budgeted in vh, not vw. Sizing the images by width (as the
   reference does, at 39vw) makes card height independent of the
   viewport, so on a short or wide window the card grows past the
   space the pile has to fit in and the deepest card is cut off by
   the bottom of the screen.

   This is the card's PREFERRED height; the max-block-size above is
   the hard limit, and min-block-size: 0 below lets this give way
   to it. */
/* THE LAG

   The image block trails the card in proportion to scroll speed and
   slides back when the scroll stops. Together with the head moving
   the other way (below) it gives the card internal layers: pinned,
   it is one flat object; in motion, three depths.

   0.5px per px/frame of velocity, capped by the clamp in
   showcase.js at 90 -- so at most 45px, and the card's own padding
   is clamp(1rem, 2vw, 2rem). Deliberately inside that, so a hard
   fling never pushes the images past the card's rounded edge.

   THE DEFOCUS

   Buried cards blur with their shade. Applied here rather than on
   the card, because a filter on the card would make that sticky
   element a containing block and a stacking context -- the change
   that breaks pinning without throwing anything. .showcase__shots
   is an ordinary descendant and safe. */
.showcase__shots {
  display: grid;
  grid-template-columns: 40fr 60fr;
  gap: var(--shot-gap);
  height: clamp(220px, 42vh, 520px);
  transform: translateY(calc(var(--vel, 0) * 0.5px));
  filter: blur(calc(var(--veil, 0) * 5px));

  /* The images are the part that gives when the cap above bites —
     the head is text and must not be squeezed. `min-block-size: 0`
     is what makes that possible: a flex item defaults to
     `min-height: auto` and refuses to shrink past its content, so
     without this the cap would simply overflow the card. */
  min-block-size: 0;
}

.showcase {
  --shot-gap: clamp(0.5rem, 1.5vw, 1rem);
}

.showcase__col {
  display: flex;
  flex-direction: column;
  gap: var(--shot-gap);
  min-height: 0;
}

/* THE PAN

   Each screenshot is cropped by object-fit: cover, so there is real
   image above and below the visible box to travel through.
   Shifting object-position pans within that overflow — no wrapper,
   no clipping, no extra element, and it repaints without touching
   layout.

   This is the only motion that continues while a card is PINNED.
   For the length of its moment the card is motionless against the
   viewport, so without this the section stops moving exactly when
   it has the reader's attention.

   The three shots pan at different rates and one of them backwards,
   because moving in lockstep reads as the whole card sliding rather
   than as three separate images with depth between them. */
.showcase__shot {
  --pan: 1;
  width: 100%;
  object-fit: cover;
  object-position: center calc(50% + var(--drift, 0) * var(--pan) * 22%);
  border-radius: var(--r-card);
  background-color: var(--c-bg-elevated);
}

/* Same 38/62 split the reference gets from its two fixed heights. */
.showcase__shot--a { height: 38%; --pan: 1; }
.showcase__shot--b { height: calc(62% - var(--shot-gap)); --pan: -0.7; }
.showcase__shot--c { height: 100%; --pan: 0.55; }

/* Below this the cards are a plain vertical list: no pinning, no
   pile, so none of the scroll machinery applies. */
@media (max-width: 639px) {
  /* Scroll distance for an effect that is not running here would
     just be half a screen of empty space between each card. */
  .showcase__gap { display: none; }

  .showcase__card {
    position: static;
    max-block-size: none;
    margin-block-end: var(--sp-lg);
    /* Both, not just --sc: showcase.js bails out below 640px, so
       whatever value each card was left holding when the window
       was narrowed would otherwise stay baked into the transform. */
    /* These are the DEFAULTS the cards fall back to, not a reset.

       They cannot override anything: showcase.js writes all five as
       inline styles and no stylesheet rule beats an inline one. The
       earlier version of this block claimed to undo whatever a card
       was left holding when the window was narrowed, and never did
       -- it only looked right when the resize happened to catch the
       values at neutral. showcase.js now REMOVES the properties
       when it drops below this breakpoint, which is what actually
       hands control back to these lines. */
    --sc: 1;
    --rot: 0deg;
    --lift: 0px;
    --drift: 0;
    --veil: 0;
    --vel: 0;
  }

  .showcase__shots {
    grid-template-columns: 1fr;
    height: auto;
  }

  .showcase__shot--a,
  .showcase__shot--b,
  .showcase__shot--c { height: clamp(160px, 42vw, 260px); }
}

/* Reduced motion keeps the pile — it is layout, not decoration —
   but stops everything that moves inside it. */
@media (prefers-reduced-motion: reduce) {
  .showcase__card { --lift: 0px; --drift: 0; }
  .showcase__list { --vel: 0; }
  .showcase__shots { filter: none; }
}
