/* ============================================================
   NASTASI FOODS — scenes.css
   ------------------------------------------------------------
   The scroll-driven set pieces on the homepage: the rolling
   import seal, Our Story, How it works, the award ribbons,
   reviews, Our advantages and the supplier CTA.

   Loaded after pages.css. Nothing here is shared with the
   sub-pages, so it stays out of the critical sheet.
   ============================================================ */

/* THE ROLLING IMPORT SEAL — `.disc` now lives in shapes.css.
   It was here, and appointment.html + import-inquiry.html carry `.disc`
   markup but do not load this sheet: the seal fell back to a static block,
   took 355px of flow, and pushed both heroes down behind a band of empty
   paper. That was the "huge empty gap with only the pizza in it". A
   primitive used by more than one page belongs in the primitives sheet. */

/* ============================================================
   OUR STORY — rebuilt on the reference's `.section_intro`
   ------------------------------------------------------------
   The reference lays this out as one relative wrapper with an
   8em left inset, holding three absolutely-placed pieces: the
   ticket (44em, rotated −1.5deg, on top), a disc at the lower
   left, and a 60%-wide collage pinned to the right edge, with
   two rotated badges over the photographs.

   Ours is the same arrangement in our own proportions. The one
   deliberate departure: their band is plain colour, ours keeps
   the pasta film — but as ONE card inside the collage instead of
   a full-bleed background under a scrim. Behind everything it was
   what flattened the section; inside the collage it is the only
   thing on the page that moves by itself.
   ============================================================ */
.story {
  position: relative; overflow: hidden; isolation: isolate;
  padding-block: clamp(4.5rem, 3rem + 8vw, 10rem);
  color: var(--text);
  background:
    radial-gradient(66% 54% at 8% 6%,   color-mix(in srgb, var(--saffron) 26%, transparent), transparent 64%),
    radial-gradient(62% 58% at 96% 94%, color-mix(in srgb, var(--adriatic) 44%, transparent), transparent 70%),
    linear-gradient(178deg, var(--bone-pure) 0%, var(--bone) 64%, var(--bone-deep) 100%);
}
.story__video { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; }

/* The desk: the letter, and the photographs lying next to it. A grid rather
   than the old absolutely-positioned collage, so the two columns can never
   overlap and crush the reading measure — which is what forced the old
   version's badges to be nudged clear of the card by hand. */
.story__desk {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr);
  gap: clamp(1.5rem, 1rem + 3.4vw, 4.5rem);
  align-items: center;
}

/* ---- the letter ---- */
.story__letter {
  position: relative; z-index: 2;
  --vc-lift: 0 26px 60px rgba(22, 20, 15, .2);
}
/* the bottom padding reserves the strip the tickets straddle — see .story__foot */
.story__letter .vcard__body {
  padding: clamp(1.85rem, 1.2rem + 2.6vw, 3.6rem) clamp(1.6rem, 1rem + 2.4vw, 3.25rem)
           clamp(3.4rem, 2.2rem + 2.6vw, 5rem);
}
/* the dateline — a letter states where it was written from, and that is a real
   piece of information here, not an eyebrow for decoration */
.story__dateline {
  font-family: var(--font-mono); font-size: .68rem;
  letter-spacing: .2em; text-transform: uppercase; color: var(--stamp);
  padding-bottom: .9rem; margin-bottom: 1.1rem;
  border-bottom: 1px solid var(--line-strong);
}
.story__letter h2 {
  font-size: clamp(1.7rem, 1.1rem + 2.3vw, 2.95rem);
  line-height: 1.04; letter-spacing: -.018em;
  margin: 0 0 1.25rem; max-width: 24ch;
}
.story__letter h2 em { font-family: var(--font-accent); font-style: italic; font-weight: 400; color: var(--stamp); }
/* 60–70 characters is the reading measure; a letter is the one block on this
   page that is genuinely meant to be read straight through */
.story__prose { max-width: 64ch; }
.story__prose p { line-height: 1.68; margin-bottom: .95rem; }
.story__prose p:last-child { margin-bottom: 0; }
.story__sign {
  display: block; margin-top: 1.6rem; padding-top: 1.1rem;
  border-top: 1px solid var(--line);
}
.scribble--sign {
  font-size: clamp(1.7rem, 1.4rem + .9vw, 2.3rem); color: var(--ink);
  transform: rotate(-3deg); transform-origin: left center;
}

/* ============================================================
   THE CENTRED LETTER, WITH FOUR LIVE SNAPSHOTS AROUND IT
   ------------------------------------------------------------
   The two-column desk is gone: the letter takes the middle of the band
   on its own and the photographs are scattered into the margins the
   grid used to fill. Each snapshot is an <a>, so this band now has
   five routes out of it instead of one.

   TWO LAYERS, TWO OWNERS. The outer .story__plate is what
   nf-scenes.js writes `transform` on for the scroll drift; the inner
   .story__plate-in owns the hover. An inline transform beats a
   stylesheet :hover, so they cannot be the same element.
   ============================================================ */
.story--centred .story__desk { display: block; }
.story--centred .story__letter {
  width: min(100%, 660px); margin-inline: auto;
  transform: rotate(-.5deg);
}

.story__scatter { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.story--centred > .wrap { position: relative; z-index: 2; }

.story__plate {
  position: absolute; margin: 0; display: block;
  pointer-events: auto; text-decoration: none; color: inherit;
  will-change: transform;
}
.story__plate-in {
  display: block; position: relative;
  --perf: 6px; --gap: 17px;
  filter: drop-shadow(0 18px 34px rgba(22, 20, 15, .3));
  transition: transform 420ms var(--ease-out), filter 420ms var(--ease-out);
}
.story__plate img, .story__plate video {
  width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block;
}
/* the label is printed on the stamp's own bottom margin, so it needs no
   scrim over the photograph */
.story__plate-lbl {
  display: block; padding: .5rem .1rem .05rem; text-align: center;
  font-family: var(--font-mono); font-size: .58rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--stamp);
  opacity: 0; transform: translateY(4px);
  transition: opacity 280ms ease, transform 280ms var(--ease-out);
}
.story__plate-lbl i { font-style: normal; }

/* Gated on a real pointer: on touch, :hover latches after a tap and the
   snapshot stays stuck enlarged over its neighbours (plan 004's fault). */
@media (hover: hover) and (pointer: fine) {
  .story__plate:hover .story__plate-in,
  .story__plate:focus-visible .story__plate-in {
    transform: rotate(0deg) scale(1.06) translateY(-8px);
    filter: drop-shadow(0 28px 52px rgba(22, 20, 15, .42));
  }
  .story__plate:hover .story__plate-lbl,
  .story__plate:focus-visible .story__plate-lbl { opacity: 1; transform: none; }
}
/* no pointer to hover with — just show the labels */
@media (hover: none), (pointer: coarse) {
  .story__plate-lbl { opacity: 1; transform: none; }
}
.story__plate:focus-visible { outline: 3px solid var(--brass); outline-offset: 5px; }
.story__plate:hover { z-index: 9; }

.story__plate--1 { width: clamp(150px, 15vw, 250px); left: 1.5%;  top: 9%;    z-index: 3; }
.story__plate--2 { width: clamp(130px, 13vw, 215px); left: 4.5%;  bottom: 8%; z-index: 2; }
.story__plate--3 { width: clamp(140px, 14vw, 230px); right: 1.5%; top: 6%;    z-index: 3; }
.story__plate--4 { width: clamp(125px, 12.5vw, 205px); right: 5%; bottom: 11%; z-index: 2; }

/* Not top-centre: the band's top edge passes under the fixed header, and a
   scribble at 2.4% printed straight through the nav. It lives in the left
   margin instead, in the gap between the two snapshots on that side. */
.story__scribble {
  position: absolute; left: 5%; top: 43%; z-index: 4;
  transform: rotate(-6deg);
  font-size: clamp(1.2rem, 1.9vw, 1.8rem); color: var(--tomato);
  white-space: nowrap;
}

/* Below this the side channels are gone: the letter fills the wrap, so the
   snapshots come out of the margins and lie in a row underneath it. */
@media (max-width: 1180px) {
  .story__scatter {
    position: static; inset: auto; pointer-events: auto;
    display: flex; gap: clamp(.6rem, 2vw, 1.25rem); justify-content: center;
    flex-wrap: wrap; margin-top: clamp(1.75rem, 4vw, 3rem);
    order: 2;
    /* Sitio para el GIRO. Las placas 1 y 3 van a +-2.5deg y un giro no
       cuenta para la maquetacion: la fila cree que cabe justa y las
       esquinas giradas asoman ~4px por cada lado. Con la fila casi llena
       eso saca la primera placa fuera de la pantalla — medido x=-3 con
       cuatro placas a 512px de ancho, y x=-4 con dos a 250px.
       6px de margen interior valen para cualquier ancho: donde la fila va
       holgada no se nota, y donde va justa fuerza el salto de linea antes
       de que la tinta se salga. */
    padding-inline: 6px;
  }
  .story--centred { display: flex; flex-direction: column; }
  .story--centred > .wrap { order: 1; }
  .story__plate { position: static; width: clamp(120px, 21vw, 190px) !important; transform: none !important; }
  .story__plate--1 { transform: rotate(-2.5deg) !important; }
  .story__plate--3 { transform: rotate(2.5deg) !important; }
  .story__scribble { display: none; }
}
/* A 250-260px de ancho (un movil de 375 o 390 al 150%) las dos placas por
   fila ya no caben: 2 x 120px de suelo + 9.6 de hueco son 249.6 sobre 250, y
   el GIRO de +-2.5deg de las placas 1 y 3 ensancha su caja envolvente otros
   7px (120·cos2.5 + 170·sin2.5 = 127.3). Resultado medido: la placa
   "Tomatoes & Sauces" arrancaba en x=-4, cortada por el borde.
   Se baja el suelo del ancho en ese tramo — dos placas siguen entrando y no
   hay que apilarlas, que en una pantalla asi son 700px mas de scroll. */
@media (max-width: 290px) {
  .story__plate { width: clamp(104px, 40vw, 190px) !important; }
}

/* Two tickets torn off the bottom edge of the letter.
   z-index 3 is load-bearing, and it is the same trap .benefits__cta documents:
   .vcard is `isolation: isolate` with the drawn paper (.vcard__shape) at
   z-index 1 and the body at 2, so an absolutely-positioned child left at
   `z-index: auto` paints UNDER the paper and the buttons vanish. Anything laid
   over a .vcard has to clear 2. The letter buys the room back with the extra
   bottom padding on its .vcard__body below. */
.story__foot {
  position: absolute; left: 50%; bottom: 0; z-index: 3;
  transform: translate(-50%, 50%) rotate(.5deg);
  display: flex; gap: clamp(.7rem, 1.4vw, 1.15rem);
  flex-wrap: wrap; justify-content: center;
  width: max-content; max-width: 92%;
}
/* Too narrow for two tickets on one torn edge: back into the flow of the
   letter, where they stack instead of hanging half off a card barely wider
   than they are.

   THIS BLOCK HAS TO SIT AFTER THE RULE IT CANCELS. A media query adds no
   specificity, so `.story__foot` here and `.story__foot` above are both
   (0,1,0) and the later one simply wins — written first, the whole fallback
   was dead and the tickets stayed absolutely positioned on a 400px card. */
@media (max-width: 620px) {
  .story__foot {
    position: static; transform: none; width: auto; max-width: none;
    justify-content: flex-start; margin-top: -.6rem;
    /* it sits OUTSIDE .vcard__body, so in flow it has to bring the card's own
       inset with it or it prints flush against the drawn edge */
    padding: 0 clamp(1.6rem, 1rem + 2.4vw, 3.25rem) clamp(1.85rem, 1.2rem + 2.6vw, 3.6rem);
  }
  .story__letter .vcard__body { padding-bottom: 1.4rem; }
}

/* ============================================================
   HOW IT WORKS — cards on the rim of an enormous wheel
   ------------------------------------------------------------
   Each card's transform-origin sits --R below it, so rotating
   the card swings it along an arc of that radius: it travels
   across the stage, tilts as it goes, and settles into a fanned
   deck. All four share one angle (--a) and differ only by a
   fixed offset, so the deck moves as one piece.

   The origin trick matters: a literal 400vw wheel element would
   be a ~50-megapixel composited layer per card and Chrome drops
   the paint. This is the same geometry for four small nodes.
   ============================================================ */
.steps {
  --R: 190vw;        /* arc radius — larger radius, flatter arc */
  --spread: 6.6deg;  /* angular gap between adjacent cards */
  position: relative; color: var(--text);
  background:
    radial-gradient(70% 55% at 14% 4%,  color-mix(in srgb, var(--saffron) 30%, transparent), transparent 66%),
    radial-gradient(62% 55% at 92% 96%, color-mix(in srgb, var(--adriatic) 46%, transparent), transparent 70%),
    linear-gradient(178deg, var(--bone-pure) 0%, var(--bone) 62%, var(--bone-deep) 100%);
}
/* ScrollTrigger owns the pin now (see steps-gsap.js): it sets position:fixed
   on the stage and inserts its own spacer, so the hand-written 300vh pin and
   the -100vh overlap are gone. The overlap made the ribbons ride up over the
   deck, which was lifted from the reference — but it also meant the deck was
   still moving while being covered, and it is the piece that kept breaking at
   different zoom levels. The section now releases cleanly and the ribbons
   follow in normal flow, which is the behaviour Sergio asked for. */
/* A tail below the stage. Sergio: the band came out too short, and the
   ribbons had nothing to overlap but the tickets themselves. The stage
   stays exactly one viewport (every position inside it is a % of that);
   the extra length is padding underneath it, which is also where the
   buried half of the seal lives. */
/* ============================================================
   THE HOLD IS CSS NOW — position: sticky, not a JS pin.
   ------------------------------------------------------------
   THREE GSAP PIN CONFIGURATIONS WERE TRIED AND ALL THREE HOP, each in its
   own way (docs/HANDOFF.md round 8 §1 and §1b):

     fixed + anticipatePin  pins EARLY in proportion to scroll VELOCITY —
                            51px snap at a flick, 0 at rest, which is why
                            three rounds of static measurement called it clean
     fixed, no anticipate   the position: relative→fixed swap lands a frame or
                            two late on a busy main thread. THIS is the one
                            Sergio kept photographing
     transform              no swap, but a translate must be re-written every
                            frame; late frames judder — his "múltiples brincos"

   A sticky element is held BY THE COMPOSITOR, with no JS in the loop and no
   position swap, so there is no engagement frame that can land late: the
   failure mode does not exist rather than being tuned down. This project
   already proved the pattern on .supplier and .teamfan, and story.html runs
   its whole ribbon hand-off this way with no pin at all.

   THE GEOMETRY IS UNCHANGED, deliberately — only what holds it changed:

     .steps__pin height = 100svh (the reveal) + 120vh (the hold) + the tail
     sticky travel      = content box − stage = 120vh = (SETTLE + OVERLAP)vh
                          — exactly the spacer ScrollTrigger used to insert

   So the ribbons' -92vh below still claws back the same OVERLAP half and
   every number in steps-gsap.js still means what it meant. box-sizing is
   border-box (base.css), which is why the tail padding is ADDED to the height
   rather than eating into the hold.

   If SETTLE or OVERLAP ever change in steps-gsap.js, --steps-hold here and
   the -92vh below change in the same commit. Three numbers, one geometry.
   ============================================================ */
/* A tail below the stage. Sergio: the band came out too short, and the
   ribbons had nothing to overlap but the tickets themselves. The stage
   stays exactly one viewport (every position inside it is a % of that);
   the extra length is padding underneath it, which is also where the
   buried half of the seal lives. */
.steps__pin {
  position: relative;
  --steps-tail: clamp(96px, 15vh, 190px);
  --steps-hold: 120vh;              /* (SETTLE 0.28 + OVERLAP 0.92) × 100vh */
  height: calc(100svh + var(--steps-hold) + var(--steps-tail));
  padding-bottom: var(--steps-tail);
}
.steps__stage { position: sticky; top: 0; height: 100svh; overflow: hidden; }

/* The reveal curtain. Same box as the stage, so everything inside keeps the
   containing block it was already positioned against. steps-gsap.js scrubs
   `clip-path: inset(0 0 X% 0)` from 100% to 0 across the pin.

   The inline default is 0 (fully open) rather than closed: if the script
   never runs — no GSAP, reduced motion, a narrow viewport that falls back to
   the stacked layout — the section has to be simply THERE. A stylesheet that
   starts it closed and waits for JS to open it is one failed request away
   from a blank screen. */
.steps__curtain { position: absolute; inset: 0; z-index: 3; clip-path: inset(0 0 0 0); }
@media (prefers-reduced-motion: reduce) { .steps__curtain { clip-path: none; } }

/* --- live background: film, warm wash, drifting case photos --- */
.steps__bg { position: absolute; inset: 0; z-index: 0; }
.steps__video { width: 100%; height: 100%; object-fit: cover; display: block; }
.steps__base { position: absolute; inset: 0; background-size: cover; background-position: 62% 42%; }
/* The wash used to be a near-black scrim so a light headline could sit on the
   film. The headline is dark ink now, so the job reversed: bleach the
   photograph back until it reads as a sun-faded print pinned behind the
   tickets, and let the saffron and label-blue do the colouring. */
.steps__base { opacity: .3; filter: saturate(.72) contrast(.9); }
.steps__wash {
  position: absolute; inset: 0;
  background:
    linear-gradient(100deg,
      color-mix(in srgb, var(--bone-pure) 92%, transparent) 0%,
      color-mix(in srgb, var(--bone-pure) 84%, transparent) 30%,
      color-mix(in srgb, var(--bone) 62%, transparent) 52%,
      color-mix(in srgb, var(--bone) 72%, transparent) 100%),
    radial-gradient(58% 55% at 84% 24%, color-mix(in srgb, var(--saffron) 34%, transparent), transparent 70%),
    radial-gradient(55% 50% at 70% 92%, color-mix(in srgb, var(--adriatic) 40%, transparent), transparent 68%);
}
.steps__tiles { position: absolute; inset: 0; }
.steps__tiles i {
  position: absolute; left: var(--x); top: var(--y);
  width: var(--w); aspect-ratio: 4 / 5;
  background-size: cover; background-position: center;
  transform: rotate(var(--t)); opacity: .82;
  box-shadow: 0 16px 40px rgba(22, 20, 15, .22);
  border: 6px solid var(--bone-pure);
}
/* dark speckle now — the band underneath it is paper, not film */
.steps__grain {
  position: absolute; inset: -10%; opacity: .5;
  background-image:
    radial-gradient(rgba(22,20,15,.055) .6px, transparent .7px),
    radial-gradient(rgba(22,20,15,.04)  .5px, transparent .6px);
  background-size: 13px 13px, 29px 29px;
  background-position: 0 0, 7px 11px;
}

/* --- three horizontal bands that never share vertical space:
       head at the top, the deck through the middle, buttons at the foot --- */
.steps__head {
  position: absolute; z-index: 4;
  left: var(--gutter); right: var(--gutter);
  top: calc(var(--header-h) + clamp(.75rem, 3vh, 2.25rem));
  width: auto; max-width: 54ch; margin-inline: auto; text-align: center;
}
.steps__head .eyebrow { color: var(--stamp); }
.steps__head h2 {
  font-family: var(--font-display); font-weight: 600; line-height: .94;
  font-size: clamp(2.2rem, 1.2rem + 3.4vw, 4.6rem); margin: .3rem 0 0;
  /* tracking tightens as the type grows — one fixed value is wrong at an end */
  letter-spacing: clamp(-0.035em, -0.016em - 0.2vw, -0.016em);
}
.steps__head h2 em {
  font-family: var(--font-accent); font-style: italic; font-weight: 400; color: var(--tomato);
  display: inline-block; transform: rotate(-2.5deg);
}
.steps__sub {
  color: var(--text-muted); margin: .7rem auto 0; font-size: var(--fs-body);
  max-width: 52ch; text-wrap: pretty;
}
/* the buttons close the centred column, directly under the line of copy */
.steps__cta {
  display: flex; gap: 1.1rem; flex-wrap: wrap; justify-content: center;
  margin-top: clamp(1rem, 2.4vh, 1.75rem);
}

/* --- the four declaration tickets on their arc --- */
.step-card {
  position: absolute; z-index: 3; left: 50%; top: 58%;
  width: clamp(240px, 21vw, 340px); pointer-events: auto;
  padding: 1.6rem 1.5rem 1.75rem;
  filter: drop-shadow(0 22px 44px rgba(22, 20, 15, .26));
  /* The pivot used to sit --R (190vw) below the card so rotating it swung the
     card along that arc. It looked right but the geometry scaled with the
     viewport, so the deck landed somewhere different at every zoom level.
     steps-gsap.js now computes explicit offsets against the measured stage
     and pivots each card about its own centre. */
  transform-origin: 50% 50%;
}
.step-card.is-paper { --cut-bg: var(--bone-pure); --cut-edge: var(--ink); color: var(--ink); }
.step-card.is-sky   { --cut-bg: var(--adriatic); --cut-edge: var(--adriatic-ink); color: var(--adriatic-ink); }
.step-card__n {
  display: block; line-height: 1;
  font-family: var(--font-script); font-size: 2rem; font-weight: 700; color: var(--tomato);
}
.step-card.is-sky .step-card__n { color: var(--adriatic-ink); }
.step-card h3 {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(1.25rem, 1rem + .8vw, 1.75rem); line-height: 1.05; letter-spacing: -.015em;
  margin: .45rem 0 .7rem; color: inherit;
}
.step-card p { font-size: .95rem; line-height: 1.5; color: var(--text-muted); }
.step-card.is-sky p { color: color-mix(in srgb, var(--adriatic-ink) 82%, transparent); }

/* Centred at the foot of the frame with half of it below the edge, turning
   as it comes down — the reference does exactly this. Half-visible means
   bottom = −½ the diameter; keep the two in step across the tiers below.

   ABSOLUTE, inside .steps__stage, at z-index 2.

   It was `position: fixed` and a child of .steps, to escape the stage's
   overflow:hidden. That trade was a bad one: ScrollTrigger pins the stage by
   writing a transform on it, any transform creates a stacking context, so the
   pinned stage is atomic and a disc outside it at z-index 1 painted over the
   whole section — the pizza sat on top of the headline. Inside the stage,
   z-index 2 sits between .steps__bg (0) and the tickets (3) and the head (4),
   which is the "seal rolling behind the deck" the section was designed around.

   The clip that made `fixed` seem necessary is not a problem here: during the
   pin the stage is exactly the viewport, so overflow:hidden only ever removes
   the buried half that is off screen regardless, and the disc's entrance
   slides out from under the top edge instead of popping in.

   No opacity transition: the fade and the descent are scrubbed frames of the
   master timeline in steps-gsap.js, and a CSS transition on top of a scrubbed
   property fights the scroll instead of following it. */
.steps .disc--steps {
  position: absolute; width: 30vw; left: 50%; bottom: -15vw; z-index: 6;
  transform: translate(-50%, 0);
  opacity: 0;
}
/* No `visibility: hidden`, deliberately: it hid the wheel before its image
   had ever been fetched, and a lazy <img> inside a visibility:hidden subtree
   is never requested at all — so the artwork silently never loaded. Opacity
   alone hides it, and .disc is already pointer-events: none.

   Which also means elementFromPoint / elementsFromPoint can NEVER be used to
   work out whether this disc is painting over something: hit-testing skips
   pointer-events:none entirely, so it always reports the element underneath
   and the stacking bug above looked fine for as long as it was probed that
   way. Read the computed `transform` / `z-index` of the ancestors instead. */
/* the stacked fallback has no pin and no room for the arc */
.steps[data-static="true"] .disc--steps { display: none; }

/* Short laptops keep the arc; it just gets smaller. Three bands have to fit
   inside the stage — head, deck, buttons — so on a shorter viewport the card
   shrinks, the deck rides higher and the head tightens. The arc itself is
   untouched: same radius, same spread, same roll. */
@media (min-width: 900px) and (max-height: 1000px) {
  .steps__head { top: calc(var(--header-h) + .5rem); max-width: 48ch; }
  .steps__head h2 { font-size: clamp(2rem, 1rem + 2.6vw, 3.2rem); }
  .steps__sub { font-size: var(--fs-small); margin-top: .5rem; max-width: 48ch; }
  .steps__cta { margin-top: .9rem; gap: .8rem; }
  .steps__cta .btn--ticket { padding: .68rem 1.2rem; font-size: .88rem; }
  .step-card {
    top: 66%; width: clamp(210px, 17.5vw, 280px);
    padding: 1.15rem 1.15rem 1.3rem;
  }
  .step-card__n { font-size: 1.5rem; }
  .step-card h3 { font-size: clamp(1rem, .85rem + .45vw, 1.25rem); margin: .3rem 0 .45rem; }
  .step-card p { font-size: .82rem; line-height: 1.42; }
  .steps .disc--steps { width: 24vw; bottom: -12vw; }
}
@media (min-width: 900px) and (max-height: 820px) {
  .steps__head h2 { font-size: clamp(1.8rem, .9rem + 2.2vw, 2.6rem); }
  .steps__sub { font-size: .82rem; }
  .step-card { top: 64%; width: clamp(195px, 16vw, 250px); padding: 1rem 1rem 1.1rem; }
  .step-card p { font-size: .76rem; }
  .steps .disc--steps { width: 21vw; bottom: -10.5vw; }
}
/* One more compaction tier, for the client's 110% zoom (a 1164×575 viewport).
   Without it the head and the deck were only ~8px apart and the stacked
   fallback took over instead, so the section lost its arc at every zoom step
   above 100%. Same principle as the tiers above: shrink the bands, keep the
   arc's radius, spread and roll untouched.

   The cutoff is 620px, not 700px, on purpose: 632px is the client's viewport at
   100% zoom and that layout is already verified good (head 109–312, deck
   331–541). This tier must not reach it and shrink the cards on his default
   screen — it exists only for the zoom steps below it. */
@media (min-width: 900px) and (max-height: 620px) {
  .steps__head { top: calc(var(--header-h) + .25rem); max-width: 44ch; }
  .steps__head h2 { font-size: clamp(1.5rem, .8rem + 1.8vw, 2.1rem); }
  .steps__sub { font-size: .76rem; margin-top: .35rem; max-width: 44ch; }
  .steps__cta { margin-top: .6rem; gap: .6rem; }
  .steps__cta .btn--ticket { padding: .55rem 1rem; font-size: .82rem; }
  .step-card {
    /* 69%, not 67%: at 1100×542 — the shortest viewport that still gets the
       arc — 67% left only 18px between the head and the top of the deck. The
       deck has ~49px of slack under it there, so it can afford to sit lower. */
    top: 69%; width: clamp(180px, 15vw, 232px);
    padding: .85rem .9rem .95rem;
  }
  .step-card__n { font-size: 1.25rem; }
  .step-card h3 { font-size: clamp(.92rem, .8rem + .35vw, 1.1rem); margin: .25rem 0 .35rem; }
  .step-card p { font-size: .72rem; line-height: 1.38; }
  .steps .disc--steps { width: 18.5vw; bottom: -9.25vw; }
}

/* stacked fallback: phones and genuinely tiny windows only */
.steps[data-static="true"] .step-card { transform-origin: 50% 50%; }

/* Stacked fallback. The height query matters as much as the width one: a laptop
   at 1440×720 is wide enough for the arc but far too short for it, and the head,
   deck and buttons end up piled on each other.

   The height cutoff was 620px, which put the client's 110% zoom (575px tall) on
   the stacked layout — he lost the arc one zoom step above his default. The
   (max-height: 700px) tier above now compacts the arc enough to fit 575px, so
   the cutoff drops to 539px: 575px keeps the arc, 507px (125% zoom) and below
   are genuinely too short and still stack.
   steps-gsap.js's matchMedia must stay the exact complement of this query. */
@media (max-width: 899px), (max-height: 539px) {
  .steps__pin { height: auto; margin-bottom: 0; }
  .steps__stage {
    position: relative; height: auto; overflow: hidden;
    padding: calc(var(--header-h) + 2rem) var(--gutter) clamp(3rem, 8vw, 5rem);
  }
  .steps__head {
    position: relative; left: auto; right: auto; top: auto;
    width: auto; max-width: 46ch;
  }
  .steps .step-card,
  .steps[data-static="true"] .step-card {
    position: relative; left: auto; top: auto; width: auto; margin-top: 1rem;
    transform: none !important; transform-origin: 50% 50%;
  }
  .steps__cta {
    position: relative; left: auto; bottom: auto; transform: none;
    width: auto; max-width: none; margin-top: 2.25rem;
  }
  /* EL TELON VUELVE AL FLUJO, o la banda entera se recorta.

     `.steps__curtain` es `position: absolute; inset: 0` con
     `clip-path: inset(0)`: en modo apilado eso lo ata a la caja del stage, y
     el stage — `height: auto` con TODO su contenido fuera de flujo — se queda
     en la altura de su padding. Medido a 375x667: stage 150px de alto, telon
     150px, y dentro un `.steps__head` de 285px cuyo pie (los dos CTA "Start
     Shopping" y "Visit Our Warehouse") caia en y=74..130, ya por debajo del
     recorte. No estaban tapados: estaban RECORTADOS — `elementsFromPoint`
     sobre el boton devolvia [pin, section, main, body], sin boton.

     En flujo, el stage mide lo que miden la cabeza, el mazo y los botones, y
     no hay nada que recortar. El pin sigue siendo el que manda en escritorio;
     esto solo existe por debajo de la consulta del pin. */
  .steps__curtain { position: relative; inset: auto; clip-path: none; }

  /* Y la rueda se queda a la vista. `.steps[data-static="true"] .disc--steps`
     pone `display: none` mas arriba — pensado para el caso sin animacion —
     pero el modo apilado SI la quiere, y de hecho la coloca (46vw, centrada).
     Las dos reglas se contradecian y solo se notaba desde que data-static se
     escribe de verdad. Con el atributo en el selector para ganar en
     especificidad, no con !important. */
  /* `transform: none` SIN `!important` (ronda 18). El `!important` estaba para
     que la limpieza de GSAP (`clearProps: "all"` en rest()) no dejara puesto
     un transform del modo fijado — pero también le ganaba a cualquier
     transform en línea, y con él la rueda no podía moverse aquí NUNCA.

     Medido antes del cambio, muestreando siete puntos del bloque a 390x844:
     la pizza salía del borde superior con el scroll y quedaba fuera de
     pantalla en CINCO de los siete. O sea: en un teléfono, "How it works" no
     tenía pizza. Ahora rueda — la escena `stepsDiscStatic` de nf-scenes.js
     escribe el transform en línea y sólo cuando `data-static="true"`, así que
     en modo fijado sigue mandando GSAP y no hay dos dueños de la propiedad.

     `will-change` la sube a su propia capa: es una imagen grande y girarla
     sin promocionar repinta la banda entera en cada fotograma. */
  .steps .disc--steps,
  .steps[data-static="true"] .disc--steps {
    display: block;
    position: relative; width: min(46vw, 210px); left: auto; bottom: auto;
    margin: 2rem auto 0; transform: none;
    will-change: transform;
  }
  @media (prefers-reduced-motion: reduce) {
    .steps[data-static="true"] .disc--steps { transform: none !important; will-change: auto; }
  }
}

/* ============================================================
   AWARD RIBBONS — three angled bands that slide past each other
   ============================================================ */
.ribbons {
  position: relative; z-index: 3; overflow: hidden;
  background: var(--bone); padding-block: clamp(2.5rem, 2rem + 3vw, 4.5rem);
}
/* The ribbons ride up over the pinned deck, and drag Reviews with them.
   ScrollTrigger's spacer puts them politely after the pin, so a negative
   margin pulls them back into it. 70vh, not the old 100vh: the deck is still
   settling through the last stretch of the timeline, and a full viewport of
   overlap covered it while it was still moving — which was the original
   complaint about this hand-off.

   Reviews follows because it is a plain sibling in flow; what makes it come
   up opaque rather than letting the stage show through is its own
   `position: relative; z-index: 3` (see the note there). */
/* The ribbons follow straight on. It was -70vh to claw them back over
   ScrollTrigger's pin spacer; the pin is gone (see steps-gsap.js), so a
   full-viewport pull would now cover the deck itself. A short bite is all
   the overlap the reference has. */
/* -92vh, matching OVERLAP in steps-gsap.js. The hold pin carries a spacer of
   SETTLE + OVERLAP; this hands the OVERLAP half back, so the ribbons sit that
   far up inside the spacer and climb over the frozen stage — while the first
   SETTLE of the spacer is left clear for the wheel to keep turning in, which is
   what Sergio asked for. Change one of these two numbers and you have to change
   the other. Only in the pinned mode: the stacked fallback has no spacer to
   claw back and a negative margin there would just eat the ribbons. */
.steps:not([data-static="true"]) + .ribbons { margin-top: -92vh; }
/* ...Y EL MODO APILADO TIENE QUE ANULARLO DESDE EL CSS, no confiando en que
   el JS ponga data-static.

   Ese -92vh solo tiene sentido con el pin: devuelve el OVERLAP del espaciador.
   En el fallback apilado no hay espaciador que devolver y el tiron se come la
   banda de How it works. Medido a 375x667: .ribbons con margin-top -613.9px,
   asi que #testimonials (z-index: 3) arrancaba en y=5705 mientras los dos CTA
   de How it works vivian en y=5778 — "Start Shopping" daba
   document.elementFromPoint = DIV.s-head, o sea inalcanzable, en las 16
   combinaciones de movil. Tambien tapaba la cola de #about, que es por lo que
   las placas de la historia salian "tapadas" a varios tamanos.

   El guardian era `:not([data-static="true"])`, un atributo que escribe
   steps-gsap.js — y no lo escribia: gsap.matchMedia().add({pinned: ...}) NO
   llama al callback cuando la condicion no encaja, asi que rest() nunca corria
   en un movil. El comentario de arriba decia "the two can never disagree"; si
   podian. La consulta se repite aqui, que es la complementaria exacta de la
   del pin, y asi el CSS se sostiene solo aunque el JS no llegue a cargar.
   Misma especificidad y DESPUES de la regla que cancela: una media query no
   suma especificidad. */
@media (max-width: 899px), (max-height: 539px) {
  .steps:not([data-static="true"]) + .ribbons { margin-top: 0; }
}
.ribbon-wrap { position: relative; display: flex; justify-content: center; will-change: transform; }
/* they overlap, but never far enough to clip each other's type */
.ribbon-wrap--top { z-index: 1; transform: translateX(var(--slide, 0)) rotate(2deg); }
.ribbon-wrap--mid { z-index: 3; transform: translate(var(--slide, 0), -.5em) rotate(-1.2deg); }
.ribbon-wrap--bot { z-index: 2; transform: translate(var(--slide, 0), -1em) rotate(2deg); }
.ribbon {
  position: relative; width: 124vw; flex: none;
  display: flex; align-items: center; justify-content: center;
  gap: clamp(.75rem, 2vw, 2.25rem); padding-block: clamp(.9rem, .6rem + 1.2vw, 1.9rem);
}
.ribbon--brass { background: var(--brass); color: #2a1e08; }
.ribbon--stamp { background: var(--stamp); color: var(--bone-pure); }
.ribbon--olive { background: var(--green-800); color: var(--bone); }
.ribbon__title {
  margin: 0; white-space: nowrap;
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(1.25rem, .6rem + 3.2vw, 3.6rem); line-height: 1; letter-spacing: -.02em;
}
.ribbon__stars { display: flex; gap: .1em; }
.ribbon__stars svg { width: clamp(1rem, 2.4vw, 2.4rem); height: clamp(1rem, 2.4vw, 2.4rem); }
.ribbon__rule { position: absolute; left: 0; right: 0; top: .45em; height: 2px; background: currentColor; opacity: .55; }
.ribbon__rule--b { top: auto; bottom: .45em; }
@media (prefers-reduced-motion: reduce) {
  .ribbon-wrap--top, .ribbon-wrap--mid, .ribbon-wrap--bot { transform: none; }
  .ribbon { width: 100%; }
}

/* ============================================================
   REVIEWS — on paper, not in a dark green box
   ============================================================ */
/* position + z-index are load-bearing, not decoration.
   .steps__stage is `position: sticky`, and the steps pin carries
   margin-bottom:-100vh so this section rides up over it while the stage is
   still stuck. A positioned element paints above the background of a
   non-positioned block in the same stacking context (CSS 2.1 painting order,
   steps 3 vs 8) — so an opaque background alone was not enough, and the
   stage showed straight through this section. .ribbons already had the
   pairing; without it here, the hand-off dropped everything after the
   ribbons back behind the stage. */
.reviews { position: relative; z-index: 3; background: var(--bone-deep); }
/* ---- the testimonial marquee ----
   Two rows travelling in opposite directions with the ends faded out.

   The seamless loop depends on one detail: the track holds two identical
   groups and travels exactly -50%. That only lines up if the gap between
   the groups equals the gap inside them, so the spacing lives entirely on
   .rev-group (internal gap + a trailing padding) and .rev-track itself has
   none. Put the gap on the track instead and the loop slips by half a gap
   on every cycle. */
.rev-marquee {
  --rev-gap: clamp(1rem, .6rem + 1.4vw, 2rem);
  display: grid; gap: var(--rev-gap);
  margin-top: clamp(2rem, 1.5rem + 2vw, 3rem);
}
/* THE TWO DARK RECTANGLES AT THE ENDS OF THE ROW (ronda 7)
   ------------------------------------------------------------
   Sergio: "se ven dos rectángulos oscuros que incluso recortan las tarjetas
   abajo … no vayas a dañar el efecto de las reseñas."

   They were not rectangles anybody drew. The old mask was a LINEAR ramp over
   10% of the row — 115px at a 1152px row — and a review card is ~258px wide.
   So at each end there was always a whole card sitting at somewhere between
   20% and 60% opacity, and a flat coloured ticket at 30% over bone paper does
   not read as a fading card: it reads as a dull grey-green block, cut off
   square where the row's `overflow: hidden` ends.

   Measured at Sergio's 1904×880 before the fix: from y 290 to 415 the pixel at
   the row's left end was 213,212,184 against paper of 222,212,188 — a flat
   126px-tall slab, uniform, with a hard vertical edge. That is the rectangle.

   The fix is the SHAPE of the ramp, not the marquee. The fade is wider (18%
   instead of 10%) but concave: alpha stays under 12% for the first two thirds
   of it and only then climbs, so a card crossing the edge is already invisible
   while it is still inside the fade instead of loitering at half strength.
   Nothing here touches .rev-track, the cloning or --rev-shift — the motion is
   exactly as it was. */
.rev-row {
  overflow: hidden;
  mask-image: linear-gradient(90deg,
    transparent 0,
    rgba(0, 0, 0, 0.02) 5%,
    rgba(0, 0, 0, 0.10) 10%,
    rgba(0, 0, 0, 0.38) 14%,
    rgba(0, 0, 0, 0.78) 16.5%,
    #000 18%,
    #000 82%,
    rgba(0, 0, 0, 0.78) 83.5%,
    rgba(0, 0, 0, 0.38) 86%,
    rgba(0, 0, 0, 0.10) 90%,
    rgba(0, 0, 0, 0.02) 95%,
    transparent 100%);
}
.rev-track {
  display: flex; width: max-content; gap: 0;
  animation: rev-scroll var(--dur, 60s) linear infinite;
  will-change: transform;
}
.rev-group {
  display: flex; gap: var(--rev-gap);
  padding-right: var(--rev-gap);
}
/* The travel is ONE group's share of the track, not a fixed -50%.
   -50% only closes the loop when the track holds exactly two groups AND a
   group is at least as wide as the row — with three quotes at ~870px inside a
   1152px row it left 282px of blank paper on every cycle, which is the gap
   Sergio saw. nf-scenes.js now clones as many copies as the row needs and
   writes --rev-shift to match, so the seam always lands off-screen. */
@keyframes rev-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(var(--rev-shift, -50%), 0, 0); }
}
.rev-row--reverse .rev-track { animation-direction: reverse; }
/* The rows do NOT stop under the cursor. Merely crossing the strip with the
   mouse froze it, and the marquee IS the effect — losing it on an accidental
   pass reads as the page hanging. Reading a single quote is served by
   keyboard focus (below) and by prefers-reduced-motion, both of which are
   deliberate acts; hover is not. */
.rev-row:focus-within .rev-track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .rev-track { animation: none; }
  .rev-row { overflow-x: auto; mask-image: none; }
}
/* the ticket shape comes from .vcard; this only handles the contents and the
   slight stagger that stops three of them reading as a table */
.review-card {
  margin: 0;
  transition: transform var(--dur-mid) var(--ease-out);
}
/* Tighter than the site-wide .vcard padding: these are quote slips running
   past, not pages to be read at rest. The vertical figure never drops below
   24px, because that is the corner-bite clearance — see the corner trap in
   VINTAGE-SYSTEM.md. Horizontal can be tighter; a line ending mid-width is
   never near a corner. */
.review-card .vcard__body {
  display: flex; flex-direction: column; gap: .6rem;
  padding: clamp(1.5rem, 1.3rem + .5vw, 1.7rem) clamp(1.15rem, .95rem + .6vw, 1.4rem);
}
.review-card blockquote, .review-card figcaption b { color: inherit; }
.review-card figcaption { border-top-color: color-mix(in srgb, currentColor 26%, transparent); }
/* ------------------------------------------------------------
   §57  UNA OPACIDAD ES UN COLOR QUE NADIE MIDIO  (27 ago 2026)
   ------------------------------------------------------------
   Sergio: "en las tarjetas de resenas el texto gris es casi ilegible".

   Y lo era por una regla que NO ponia el color. Estas dos lineas daban
   `color: inherit` mas una opacidad, pensadas para cuando la tarjeta viaja
   sobre un fondo de otro tono; treinta lineas mas abajo llegan las reglas de
   verdad y ponen `--text-faint` y `--brass`. La `color` de abajo gana (misma
   especificidad, va despues) — pero LA OPACIDAD DE AQUI SE QUEDA, porque
   nadie la cancela. El resultado no es el color que se eligio: es ese color
   rebajado por un numero puesto para otra cosa.

   Medido sobre el papel de la ficha (#FBF8F0):
     · el pie (10,2px en versalitas) — #7A7363 al 66 %  =  2,45:1
       (AA pide 4,5:1 para texto normal; a 10px no cuenta como texto grande)
     · las estrellas               — #A87B3D al 80 %  =  2,65:1

   Se quitan las dos opacidades y se elige el color a la vista:
     · el pie pasa a `--text-muted` (#5B5547) = 6,98:1
     · las estrellas a `--brass-deep` (#8A6430) = 4,84:1 — el mismo laton, el
       tono oscuro que la app ya usaba por esta misma razon.
   La jerarquia no se pierde: la sigue marcando el TAMANO y la caja alta, que
   es como se marca en el resto del sistema.
   ------------------------------------------------------------ */
.review-card figcaption span { color: var(--text-muted); }
.review-card__stars { color: inherit; }

/* The cards ride the marquee now, so the tilt is no longer gated on the
   reveal class — [data-reveal] sits on the marquee wrapper, not on each
   ticket, so nothing here fights the entrance any more. */
.review-card { width: clamp(208px, 16.5vw, 258px); flex: none; }
.review-card:nth-child(1) { transform: rotate(-1.1deg); }
.review-card:nth-child(2) { transform: rotate(.5deg) translateY(10px); }
.review-card:nth-child(3) { transform: rotate(1.4deg); }
@media (hover: hover) and (pointer: fine) {
  .review-card:nth-child(1):hover { transform: rotate(-1.1deg) translateY(-6px); }
  .review-card:nth-child(2):hover { transform: rotate(.5deg) translateY(4px); }
  .review-card:nth-child(3):hover { transform: rotate(1.4deg) translateY(-6px); }
}
/* §57 — laton oscuro, no laton. Y medido contra el fondo QUE HAY DEBAJO, que
   no es el papel claro: la ficha de resena es `.vcard--paper` y su fondo sale
   transparente en `getComputedStyle` (lo dibuja la capa de la tarjeta), asi
   que lo que hay detras del texto es la banda `--bone-deep` (#DED4BC).
   Medido ahi: #A87B3D = 3,29:1 · #8A6430 = 3,61:1 · #7A4A12 = 5,06:1.
   Se elige el tercero, que ademas ya existe en el sistema (`.selbar .warn`). */
.review-card__stars { color: #7A4A12; letter-spacing: .1em; font-size: .88rem; }
.review-card blockquote {
  font-family: var(--font-accent); font-weight: 400; font-style: italic;
  font-size: .9rem; line-height: 1.42; color: var(--ink);
}
.review-card figcaption { margin-top: auto; padding-top: .65rem; border-top: 1px solid var(--line); font-size: .84rem; }
.review-card figcaption b { display: block; }
.review-card figcaption span {
  display: block; margin-top: 3px;
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .12em;
  /* §57 — `--text-faint` da 4,44:1 sobre este papel: por debajo de AA para un
     rotulo de 10,2px. `--text-muted` da 6,98:1 y sigue siendo el gris tibio de
     la casa. La jerarquia la marca el tamano, no la palidez. */
  text-transform: uppercase; color: var(--text-muted);
}
.trusted-by--light { border-top-color: var(--line); }
.trusted-by--light .trusted-by__label { color: var(--brass); }
.trusted-by--light .trusted-by__list li { color: var(--ink); }
@media (max-width: 860px) { .review-grid { grid-template-columns: 1fr; } }

/* ============================================================
   OUR ADVANTAGES — checklist card over a drift of photos
   ============================================================ */
.benefits { position: relative; background: var(--bone); overflow: hidden; }
/* ---- the Nutella film, washed into the paper ----
   Same recipe as .supplier__bg: the clip at low opacity and desaturated,
   a warm gradient over it that keeps the reading side pale, and the grain
   on top so the whole band still reads as printed paper. Everything in
   .wrap has to be lifted above it — a z-index on the film alone is not
   enough, because .wrap is unpositioned and would paint under a
   positioned sibling regardless of its background. */
.benefits__bg { position: absolute; inset: 0; z-index: 0; }
.benefits > .wrap { position: relative; z-index: 1; }
.benefits__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  opacity: .3; filter: saturate(.72) contrast(.92);
}
.benefits__wash {
  position: absolute; inset: 0;
  background:
    linear-gradient(92deg,
      color-mix(in srgb, var(--bone) 88%, transparent) 0%,
      color-mix(in srgb, var(--bone) 74%, transparent) 38%,
      color-mix(in srgb, var(--bone) 56%, transparent) 66%,
      color-mix(in srgb, var(--bone-deep) 72%, transparent) 100%),
    radial-gradient(58% 58% at 88% 22%, color-mix(in srgb, var(--saffron) 26%, transparent), transparent 70%),
    radial-gradient(52% 50% at 12% 92%, color-mix(in srgb, var(--adriatic) 26%, transparent), transparent 70%);
}
.benefits__grain {
  position: absolute; inset: -10%; opacity: .45;
  background-image:
    radial-gradient(rgba(22,20,15,.055) .6px, transparent .7px),
    radial-gradient(rgba(22,20,15,.04)  .5px, transparent .6px);
  background-size: 13px 13px, 29px 29px;
  background-position: 0 0, 7px 11px;
}
@media (prefers-reduced-motion: reduce) { .benefits__video { display: none; } }

.benefits__stage { position: relative; display: flex; justify-content: flex-end; padding-left: clamp(0rem, 8vw, 9rem); }
.benefits__photos { position: absolute; inset: 0 auto 0 0; width: 55%; z-index: 0; }
/* THREE PINNED PHOTOGRAPHS THAT DO NOT TOUCH.
   They used to be 330px laid 14–15rem apart, which left them overlapping AT
   REST — measured at 1600: 62×237px of photo 2 on photo 1, 134×157 of 3 on 2.
   Hovering then grew the one under the cursor by 7% straight into whichever
   neighbour was sitting on it, which is the "se montan una encima de la otra".
   Raising z-index (which was already here) cannot fix that: it decides WHICH
   one is on top, not whether they collide.

   So the scatter is re-spaced to clear at every live width, and the hover
   became elevation instead of growth — it straightens, lifts and casts a
   longer shadow, with only a hair of scale. A photograph that comes toward
   you does not need to get bigger than the gap it sits in. */
.benefits__photo {
  position: absolute; margin: 0; width: clamp(175px, 17.5vw, 275px);
  --perf: 6px; --gap: 17px;
  filter: drop-shadow(0 18px 34px rgba(22, 20, 15, .32));
  transition: transform 420ms var(--ease-out), filter 420ms var(--ease-out);
}
.benefits__photo img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; }
.benefits__photo--1 { top: -2.5rem; left: -3.5rem; z-index: 0; transform: rotate(-5deg); }
.benefits__photo--2 { top: 13rem;   left: 18rem;   z-index: 1; transform: rotate(3.5deg); }
.benefits__photo--3 { top: 28.5rem; left: -2.5rem; z-index: 2; transform: rotate(4.5deg); }
/* Gated on a real pointer: on touch, :hover latches after a tap and the
   stamp stays stuck raised over its neighbours. */
@media (hover: hover) and (pointer: fine) {
  .benefits__photo:hover {
    z-index: 9;
    filter: drop-shadow(0 34px 58px rgba(22, 20, 15, .46));
  }
  .benefits__photo--1:hover { transform: rotate(-1.5deg) scale(1.025) translateY(-14px); }
  .benefits__photo--2:hover { transform: rotate(1deg)    scale(1.025) translateY(-14px); }
  .benefits__photo--3:hover { transform: rotate(1.6deg)  scale(1.025) translateY(-14px); }
}
@media (prefers-reduced-motion: reduce) {
  .benefits__photo { transition: none; }
  .benefits__photo:hover { transform: none; }
}

/* the ticket shape comes from .vcard; the rotation stays here */
.benefits__card {
  position: relative; z-index: 10; width: min(620px, 100%);
  transform: rotate(1.2deg);
  --vc-lift: 0 26px 60px rgba(22, 20, 15, .2);
}
.benefits__card .vcard__body {
  padding: clamp(1.75rem, 1.2rem + 2.5vw, 3rem) clamp(1.5rem, 1rem + 2vw, 2.75rem) clamp(3.5rem, 5vw, 4.5rem);
}
.benefits__card h2 { font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.4rem); margin: .5rem 0 1.5rem; }
.benefits__list { display: grid; gap: 1.15rem; }
.benefits__list li { border-bottom: 1px solid var(--line); padding-bottom: 1.05rem; }
.benefits__list li:last-child { border-bottom: none; padding-bottom: 0; }
.benefits__li-head { display: flex; align-items: center; gap: .75rem; }
.benefits__li-head svg { width: 22px; height: 22px; color: var(--brass); flex: none; }
.benefits__li-head h3 { font-family: var(--font-body); font-weight: 650; font-size: 1.02rem; margin: 0; }
.benefits__list p { font-size: .86rem; line-height: 1.55; color: var(--text-muted); margin-top: .45rem; padding-left: calc(22px + .75rem); }
/* z-index 3 is load-bearing. This sits inside .vcard, which is
   `isolation: isolate`, and the card's drawn paper (.vcard__shape) is z-index 1
   with the body at 2. An absolutely-positioned child with `z-index: auto`
   therefore paints UNDER the paper — the buttons were behind the card, which is
   exactly what Sergio reported. Anything laid over a .vcard has to clear 2. */
.benefits__cta {
  position: absolute; left: 50%; bottom: 0; z-index: 3;
  transform: translate(-50%, 50%) rotate(-1.2deg);
  display: flex; gap: .7rem; flex-wrap: wrap; justify-content: center; width: max-content; max-width: 92%;
}
@media (max-width: 1080px) {
  .benefits__stage { justify-content: center; padding-left: 0; }
  .benefits__photos { display: none; }
  .benefits__card { transform: none; padding-bottom: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
  .benefits__cta { position: static; transform: none; margin-top: 2rem; width: auto; max-width: none; justify-content: flex-start; }
}

/* ============================================================
   SUPPLIER CTA — the seal rolls again, on ink
   ============================================================ */
/* THE BAND IS HELD ON SCREEN UNTIL THE SEAL HAS CROSSED IT.
   The wrapper is one viewport plus the seal's run; the band sticks to the top of
   the viewport for exactly that extra distance and then releases. Sticky rather
   than a ScrollTrigger pin on purpose: this section's seal is driven by
   nf-scenes.js's shared rAF loop, and putting a second pinning mechanism on an
   element already animated by hand is what made the How-it-works hand-off so
   expensive to get right.

   Gated on the same room the other pinned scenes require. On a phone, or in a
   window too short to hold the band, sticky would trap a section taller than
   the viewport and you could never read its bottom. */
.supplier-pin { position: relative; }
@media (min-width: 900px) and (min-height: 540px) {
  /* 0.85 is the seal's run: it travels -26vw → 48vw, and that reads as a
     complete crossing in a little under one viewport of scroll. */
  .supplier-pin { height: calc(100svh + 85vh); }
  .supplier-pin > .supplier {
    position: sticky; top: 0;
    min-height: 100svh;
    display: flex; flex-direction: column; justify-content: center;
  }
}
.supplier {
  position: relative; overflow: hidden; color: var(--text);
  padding-block: clamp(4rem, 3rem + 6vw, 8rem);
  background:
    radial-gradient(72% 58% at 10% 2%,  color-mix(in srgb, var(--saffron) 32%, transparent), transparent 66%),
    radial-gradient(64% 56% at 94% 98%, color-mix(in srgb, var(--adriatic) 48%, transparent), transparent 70%),
    linear-gradient(178deg, var(--bone-pure) 0%, var(--bone) 60%, var(--bone-deep) 100%);
}
.supplier__bg { position: absolute; inset: 0; z-index: 0; }
.supplier__base {
  position: absolute; inset: 0;
  background-size: cover; background-position: center 40%;
}
.supplier__collage { position: absolute; inset: 0; }
.supplier__pin {
  position: absolute; margin: 0; left: var(--x); top: var(--y);
  width: var(--w); --perf: 6px; --gap: 17px;
  transform: rotate(var(--t));
  filter: drop-shadow(0 20px 40px rgba(0, 0, 0, .5));
}
.supplier__pin img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; }
/* reading column on the left, colour and food on the right — now bleached
   back rather than blacked out, because the copy on top is ink, not bone */
.supplier__base { opacity: .28; filter: saturate(.74) contrast(.9); }
.supplier__wash {
  position: absolute; inset: 0;
  background:
    linear-gradient(95deg,
      color-mix(in srgb, var(--bone-pure) 93%, transparent) 0%,
      color-mix(in srgb, var(--bone-pure) 86%, transparent) 34%,
      color-mix(in srgb, var(--bone) 60%, transparent) 60%,
      color-mix(in srgb, var(--bone) 70%, transparent) 100%),
    radial-gradient(56% 60% at 82% 30%, color-mix(in srgb, var(--saffron) 30%, transparent), transparent 70%),
    radial-gradient(50% 50% at 60% 96%, color-mix(in srgb, var(--tomato) 20%, transparent), transparent 70%);
}
.supplier__grain {
  position: absolute; inset: -10%; opacity: .5;
  background-image:
    radial-gradient(rgba(22,20,15,.055) .6px, transparent .7px),
    radial-gradient(rgba(22,20,15,.04)  .5px, transparent .6px);
  background-size: 13px 13px, 29px 29px;
  background-position: 0 0, 7px 11px;
}
.supplier__stage { position: relative; z-index: 1; }
/* The 64ch cap was meant for the prose, but the two cards inherited it and
   ended up ~270px each once split into two columns — smaller than the stamps
   pinned beside them. The prose carries its own measure, so the cap moves off
   the container and onto the text. */
.supplier__body { max-width: none; }
/* EVERY MEASURE IN THIS COLUMN IS CAPPED AT HALF THE STAGE, not just in ch.
   The collage is placed in PERCENT — its leftmost pin sits at `--x: 53%` — and
   the copy was capped only in ch, which is a fixed rem. So the two scaled on
   different curves and at 80% zoom the lede's last words printed straight
   across the tortellini photograph: "el texto queda encima de las fotografías".
   `min(…ch, 50%)` can never reach 53% of the stage at any width or zoom, and
   below 900px the collage is gone and the cap stops mattering. Same rule the
   margin note already uses — see .scribble--sup. */
.supplier__body > .eyebrow { max-width: min(56ch, 50%); }
.supplier .eyebrow { color: var(--stamp); }
.supplier h2 { font-size: var(--fs-h2); margin-top: .5rem; max-width: min(18ch, 50%); }
.supplier__lede { color: var(--text-muted); font-size: var(--fs-lead); margin-top: 1.1rem; max-width: min(56ch, 50%); }
/* z-index 2 so the seal can roll behind the cards, not around them */
.supplier__cards {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1rem, 1.8vw, 1.9rem);
  margin-top: clamp(2rem, 3vw, 3rem);
  width: min(720px, 100%);
}
.supplier__card {
  position: relative; display: flex; flex-direction: column; gap: .5rem;
  text-decoration: none; color: var(--vc-ink);
  transition: background var(--dur-mid), border-color var(--dur-mid), transform var(--dur-mid) var(--ease-out);
}
.supplier__card .vcard__body { display: flex; flex-direction: column; gap: .5rem; height: 100%; }
@media (hover: hover) and (pointer: fine) {
  .supplier__card:hover { transform: translateY(-5px); }
  .supplier__card:hover .p-outer { stroke-width: 3.5; }
}
.supplier__card .p-outer { transition: stroke-width var(--dur-mid) var(--ease-out); }
.supplier__card-n { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .18em; opacity: .65; }
.supplier__card h3 { font-size: 1.3rem; color: inherit; }
.supplier__card p { color: inherit; opacity: .82; font-size: var(--fs-small); flex: 1; }
.supplier__card .link-arrow { color: inherit; }
/* In flow at the end of the lede — see the note in index.html for why it is no
   longer absolutely positioned over the collage. It is set to the right of the
   copy measure so it leans toward the photographs it is talking about, and the
   negative bottom margin spends the cards' own 48px top gap instead of adding
   height: this band is already taller than the viewport and its hold is
   measured in JS, so every row costs scroll. Measured at 1600: scribble
   ink 11648–11700, cards start 11736 — 36px of daylight, cards unmoved. */
/* ALIGNED TO THE LEDE'S MEASURE, NOT THE BODY'S. `.supplier__body` is
   `max-width: none` — the prose carries its own cap — so `margin-left: auto`
   here pushes to the right edge of the whole STAGE, which is the collage.

   The cap is `min(56ch, 50%)` and the 50% is the load-bearing half. 56ch alone
   is the lede's measure, but a ch is a fixed rem and the collage is placed in
   PERCENT (`--x: 53%` on the leftmost pin) — so at 1600 the note cleared and at
   1081 the same 56ch reached straight back into the photographs. Half the stage
   can never reach 53% of it at any width. Measured clear of all three pins at
   1081 / 1280 / 1600 / 1920. */
.scribble--sup {
  display: block; max-width: min(56ch, 50%); text-align: right;
  margin: .7rem 0 -.55em;
  font-size: clamp(1.3rem, 1.9vw, 2rem); transform: rotate(-2.6deg);
  transform-origin: right center; color: var(--stamp);
}
/* the seal crosses the whole band, passing behind the two cards */
.supplier .disc--supplier { width: 19vw; left: 50%; bottom: -5vw; z-index: 1; }
@media (max-width: 900px) {
  .supplier__cards { grid-template-columns: 1fr; }
  /* one column: nothing to lean toward, so it goes back to the left margin and
     stops borrowing the cards' gap */
  .supplier__body > .scribble--sup {
    text-align: left; margin: .9rem 0 .2rem;
    transform: rotate(-2deg); transform-origin: left center;
  }
  .supplier .disc--supplier {
    position: relative; width: min(46vw, 200px); right: auto; bottom: auto;
    margin: 2.5rem auto 0; transform: none !important;
  }
}

/* ---------- small shared bits ---------- */
.brands-foot {
  display: flex; flex-direction: column; align-items: center; gap: .75rem;
  text-align: center; margin-top: 1.75rem; color: var(--text-faint);
}
.dept-foot { display: flex; align-items: center; gap: 1.25rem 2rem; flex-wrap: wrap; margin-top: clamp(1.75rem, 3vw, 2.75rem); }
.biz-card__visit { width: 100%; justify-content: center; margin-bottom: 1rem; }
