/* =============================================================
   MOBILE REFINEMENT PASS
   Loaded last. Everything here corrects phone-only geometry that
   the earlier layers left behind; desktop is untouched unless a
   rule says so explicitly.
   ============================================================= */

/* The theme control belongs in the bottom-right corner at every size,
   sitting just clear of the phone browser's own navigation bar. */
.theme-toggle,
html .theme-toggle {
  top: auto;
  right: max(16px, calc(env(safe-area-inset-right) + 12px));
  bottom: max(20px, calc(env(safe-area-inset-bottom) + 16px));
}

@media (max-width: 860px) {
  html .theme-toggle {
    right: max(14px, calc(env(safe-area-inset-right) + 10px));
    bottom: max(24px, calc(env(safe-area-inset-bottom) + 20px));
  }

  /* The hero was locked to a fixed 100svh box, so on short viewports its
     CTA overflowed underneath the client ribbon — which read as an empty
     band where the button should have been. Let the section grow. */
  html .hero.has-sky,
  html .hero.has-sky-fallback {
    height: auto;
    min-height: 100svh;
    padding-bottom: clamp(12px, 3.5vw, 22px);
  }

  html .hero .hero-grid { min-height: 0; }

  /* Two lines instead of three, so the media can start higher. */
  .hero .hero-intro {
    max-width: min(100%, 46ch);
    margin-top: 10px;
    font-size: clamp(.76rem, 3.2vw, .92rem);
    line-height: 1.5;
    letter-spacing: -.005em;
  }

  /* Lift the product stage; the space it gives back goes to the CTA. */
  .hero .hero-showcase-wrap { margin-top: 0; }
  .hero .hero-scene { padding-top: 26px; }

  /* Column labels read as headings for their column, so centre them. */
  .hero .hero-group {
    padding-inline: 4px;
    text-align: center;
  }

  /* The readout was pulled up against the mode grid. */
  .hero .hero-actions .hero-readout {
    top: 0 !important;
    margin-top: 12px;
  }

  /* One line for the readout wherever the copy allows it: at .74rem about 58
     characters fit the 343px column, which covers four of the six modes. The
     two longest still wrap, so the reservation stays at two lines to keep the
     media from twitching as the mode changes. */
  .hero .hero-readout-desc {
    font-size: .84rem;
    line-height: 1.4;
  }

  /* 3px was all that stood between the fifth readout string and one line. */
  .hero .hero-actions .hero-readout { width: min(46ch, 97%); }

  /* Reclaim the last of the vertical budget so the CTA finishes above the
     fold on a phone that is showing its browser chrome. 44px keeps every
     mode button a comfortable touch target. */
  .hero .hero-mode {
    min-height: 38px;
    padding-block: 4px;
    font-size: .55rem;
  }

  .hero .hero-mode svg { width: 22px; height: 22px; }
  .hero .hero-mode > svg + span { letter-spacing: .04em; }

  .hero .hero-group { padding-top: 6px; }
  .hero .hero-actions { margin-top: 4px; }

  .ribbon-stack { margin-top: clamp(20px, 5vw, 32px); }
}

/* -------------------------------------------------------------
   PACKS — header alignment
   The eyebrow is inline-flex, so the earlier `text-align: right`
   never moved it while the headline and the LED copy did move.
   The head now reads on one edge, like Process and Work above it.
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  #packs .packs-head-copy .eyebrow,
  #packs .packs-head-copy h2 {
    justify-self: start;
    text-align: left;
  }

}

/* -------------------------------------------------------------
   PACKS — an expanded card carries the same facts as on desktop
   What is included and how long it takes were both hidden on
   mobile, so an opened card showed sliders and a CTA and nothing
   to price the decision on.
   ------------------------------------------------------------- */
@media (max-width: 680px) {
  #packs .pack-rail-card.is-expanded .pack-rail-card-stats {
    display: grid !important;
    gap: 6px;
    font-size: .72rem;
  }

  #packs .pack-rail-card.is-expanded .pack-rail-card-stats li { gap: 8px; }
  #packs .pack-rail-card.is-expanded .pack-rail-card-stats svg { width: 16px; height: 16px; }

  #packs.packs-outcomes[data-packs-rail] .pack-rail-card.is-expanded .pack-rail-card-note {
    display: block !important;
    font-size: .68rem;
    line-height: 1.45;
  }

  /* The summary row already carries the price, so the detail line keeps
     only the part it does not repeat: the quote guide and the timeline. */
  #packs .pack-rail-card.is-expanded .pack-rail-card-price {
    display: block !important;
    margin: 0;
    font-size: 0;
    line-height: 0;
  }

  #packs .pack-rail-card.is-expanded .pack-rail-card-price em,
  #packs .pack-rail-card.is-expanded .pack-rail-card-price small { font-size: .68rem; }
  #packs .pack-rail-card.is-expanded .pack-rail-card-price em { display: none; }

  #packs .pack-rail-card.is-expanded .pack-rail-card-price small {
    display: block;
    margin: 0;
    line-height: 1.4;
    letter-spacing: .04em;
  }
}

/* -------------------------------------------------------------
   PROCESS DIAL — label breathing room
   Seven labels ring a small arc on a phone, so they were touching
   the track and each other. Smaller type plus a wider radial
   offset gives each one its own air without shrinking the dial.
   ------------------------------------------------------------- */
@media (max-width: 760px) {
  #process-radial .pr-copy {
    width: 90px !important;
    min-width: 90px !important;
  }

  #process-radial .pr-num {
    font-size: .5rem !important;
    letter-spacing: .16em !important;
  }

  #process-radial .pr-title {
    margin-top: 2px !important;
    font-size: .62rem !important;
    line-height: 1.14 !important;
  }

  #process-radial .pr-step[data-place="bl"] .pr-copy { left: -45px !important; top: 32px !important; }
  /* 02 and 06 sit between two pucks, so they lean outward rather than
     centring on their own and clipping the neighbour above. */
  #process-radial .pr-step[data-place="l"]  .pr-copy { width: 76px !important; min-width: 76px !important; left: -56px !important; bottom: 38px !important; }
  #process-radial .pr-step[data-place="tl"] .pr-copy { right: -25px !important; bottom: 37px !important; }
  #process-radial .pr-step[data-place="t"]  .pr-copy { left: -45px !important; bottom: 40px !important; }
  #process-radial .pr-step[data-place="tr"] .pr-copy { left: -25px !important; bottom: 37px !important; }
  #process-radial .pr-step[data-place="r"]  .pr-copy { width: 76px !important; min-width: 76px !important; left: -20px !important; bottom: 38px !important; }
  #process-radial .pr-step[data-place="br"] .pr-copy { right: -45px !important; top: 32px !important; }
}

/* -------------------------------------------------------------
   TASTE STAGE — close the gap under the headline
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  #taste-stage .taste-stage-intro { margin-bottom: 4px; }
  #taste-stage .taste-stage-arena { padding-top: 0; }
}

/* -------------------------------------------------------------
   HERO — the text block owns the top, the media starts below it
   The headline's two spans are display:block, so it always broke
   to two lines and the product frame crowded the second one. One
   line at 10.2vw fits every phone width with room to spare
   (measured: the full string is 399px at the old 47.2px, so it
   needs 42.2px at 393px wide).
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  /* One line, so the height it used to cost goes to the media instead.
     The two spans were display:block, which did two things: it forced the
     second line, and it pinned .hero-copy -- a flex item in a column flex
     container -- to the width of the longer half, 174px of a 339px column.
     Inline spans let the copy grow to the whole string, and then the only
     question is whether the string fits. Measured: it sets 8.34x its own
     font-size, so at 9.6vw it needs 80vw of a (100vw - 36px) column and
     clears it at every phone width down to 320px. nowrap holds that line
     even if the font falls back to a wider face. */
  .hero h1 {
    font-size: clamp(1.45rem, 9.6vw, 2.4rem);
    line-height: 1.12;
    white-space: nowrap;
    text-wrap: nowrap;
    max-width: none;
  }

  .hero h1 span { display: inline; }

  /* The subtitle repeats what the mode readout says a moment later, and on a
     phone it cost three lines at the top of the fold. */
  .hero .hero-intro { display: none; }

  /* The freed height goes to the media, which no longer starts
     under the headline's descenders. */
  .hero .hero-showcase-wrap { margin-top: 4px; }

  /* The lime tip marker belongs to the desktop rail diagram; on the
     stacked mobile list it reads as a stray arrow beside the icon. */
  .hero .hero-mode::before,
  .hero .hero-mode::after { display: none !important; }

  /* The readout reserves two lines so the layout cannot shift when a
     description wraps. A one-line description was sitting at the top of
     that box, which is the gap above the CTA — centre it in the space
     it already owns instead of removing the reservation. */
  /* Equal air above and below the readout, whether it runs to one line or two:
     the flex gap is zeroed and the spacing comes from a symmetric margin, so
     the block always sits centred between the mode grid and the CTA. */
  .hero .hero-actions { gap: 0; }

  .hero .hero-actions .hero-readout {
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-block: 14px;
  }

  .hero .hero-readout-desc {
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

/* -------------------------------------------------------------
   HERO SKY — stars belong to the sky, not to the cloud banks
   ------------------------------------------------------------- */
html .hero-sky .hero-stars,
html[data-skin="nocturne"] .hero-sky .hero-stars { z-index: 0; }
html .hero-sky .hero-sky-layer--far { z-index: 1; }

/* -------------------------------------------------------------
   PROCESS — the dial gets the room the lede was using
   ------------------------------------------------------------- */
@media (max-width: 760px) {
  #process-radial .pr-lede { display: none; }
  #process-radial .pr-heading { margin-bottom: 0 !important; }
  #process-radial .pr-board { margin-top: -6px !important; }
  #process-radial .pr-shell { padding-bottom: 16px; }
}

/* -------------------------------------------------------------
   PACKS — the outcome note sits on the opposite edge to the title
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  #packs .packs-head-led {
    grid-template-columns: minmax(0, 1fr) 18px;
    max-width: 100%;
    margin-left: auto;
  }

  #packs .packs-head-led .taste-stage-led-mark { grid-column: 2; grid-row: 1; }

  #packs .packs-head-led .taste-stage-led-kicker {
    grid-column: 1;
    grid-row: 1;
    text-align: right !important;
  }

  #packs .packs-head-led .taste-stage-led-copy {
    grid-column: 1 / -1;
    grid-row: 2;
    text-align: right !important;
  }
}

/* -------------------------------------------------------------
   HERO (mobile) — fill the viewport, spend the slack on the media
   min-height:100svh with a content-sized grid left the stack top
   aligned, so a tall phone showed ~218px of dead sky under the CTA.
   The grid now fills the frame the way it does on desktop: the
   showcase grows, the CTA sits just above a small bottom pad, and
   the product stage takes the room that opens up.

   site.css:4213 pins `html .hero.has-sky .hero-grid` to display:block
   at (0,4,1), so every rule here carries the same .has-sky weight.
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  html .hero.has-sky,
  html .hero.has-sky-fallback {
    padding-bottom: clamp(16px, 2.6vh, 28px);
  }

  html .hero.has-sky .hero-grid,
  html .hero.has-sky-fallback .hero-grid {
    display: flex;
    flex-direction: column;
    min-height: calc(100svh - var(--header, 64px) - 14px - clamp(16px, 2.6vh, 28px));
  }

  html .hero.has-sky .hero-copy,
  html .hero.has-sky-fallback .hero-copy { flex: 0 0 auto; }

  html .hero.has-sky .hero-showcase-wrap,
  html .hero.has-sky-fallback .hero-showcase-wrap {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }

  /* display:flex, not block — the scene cannot resolve a percentage height
     against a block parent whose own height comes from flex. */
  html .hero.has-sky .hero-showcase,
  html .hero.has-sky-fallback .hero-showcase {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }

  html .hero.has-sky .hero-scene,
  html .hero.has-sky-fallback .hero-scene {
    flex: 1 1 auto;
    min-height: 0;
    grid-template-rows: minmax(0, 1fr) auto;
  }

  html .hero.has-sky .hero-product-stage,
  html .hero.has-sky-fallback .hero-product-stage {
    place-self: center;
    /* Everything except the stage costs ~447px of column at this width, so the
       stage may claim whatever is left — capped at the container so it never
       overflows horizontally on a very tall, narrow viewport. The aspect ratio
       turns that width into the height, so the cap below is only a backstop;
       site.css pins it to min(24vh, 196px), which is what held the media small. */
    width: min(100%, calc((100svh - 447px) * 1.34));
    max-height: min(38vh, 300px);
  }

  html .hero.has-sky .hero-actions,
  html .hero.has-sky-fallback .hero-actions { flex: 0 0 auto; }
}

/* -------------------------------------------------------------
   TASTE STAGE (mobile) — base, headline reveal, softer shuffle
   ------------------------------------------------------------- */
@media (max-width: 860px) and (min-height: 620px) {
  /* The plinth was exactly as wide as the front card, and the card's bottom
     overlaps it by 8px — so the card's 18px corners curved away and showed the
     base through the gap, reading as floating rather than resting. 10px of
     proud edge on each side puts the card on top of something. */
  #taste-stage.is-mobile-story .taste-plinth { margin-inline: -10px; }

  /* The headline arrives with the section instead of waiting on the runway. */
  #taste-stage.is-mobile-story .taste-stage-intro {
    opacity: 0 !important;
    transform: translateY(10px) scale(.94) !important;
    transform-origin: 50% 100%;
    transition: opacity 560ms cubic-bezier(.22, 1, .36, 1),
                transform 560ms cubic-bezier(.22, 1, .36, 1);
    will-change: opacity, transform;
  }

  #taste-stage.is-mobile-story.is-title-in .taste-stage-intro {
    opacity: 1 !important;
    transform: none !important;
  }
}

@media (max-width: 860px) and (min-height: 620px) and (prefers-reduced-motion: reduce) {
  #taste-stage.is-mobile-story .taste-stage-intro {
    opacity: 1 !important;
    transform: none !important;
    transition: none;
  }
}

/* -------------------------------------------------------------
   PACKS / TASTE / FOOTER — final mobile pass
   ------------------------------------------------------------- */
@media (max-width: 680px) {
  /* The stack is one list: only its two ends are round, and an open card keeps
     the shape of its slot. The expanded card used to become a detached 16px
     pill with 6px margins, which is wrong in the middle of a list — and worse,
     easing that shape back on collapse left an empty rounded pill on screen for
     260ms, because `display` is not transitionable so the content vanished at
     once while the shell lingered. No shape change, nothing to ease. */
  #packs.packs-outcomes .pack-rail-card,
  #packs.packs-outcomes .pack-rail-card.is-expanded,
  #packs.packs-outcomes .pack-rail-card:focus-within {
    border-radius: 0 !important;
    margin-block: 0 !important;
    transition: background-color 180ms ease !important;
  }

  #packs.packs-outcomes .pack-rail-card:first-child,
  #packs.packs-outcomes .pack-rail-card:first-child.is-expanded,
  #packs.packs-outcomes .pack-rail-card:first-child:focus-within {
    border-radius: 14px 14px 0 0 !important;
  }

  #packs.packs-outcomes .pack-rail-card:last-child,
  #packs.packs-outcomes .pack-rail-card:last-child.is-expanded,
  #packs.packs-outcomes .pack-rail-card:last-child:focus-within {
    border-radius: 0 0 14px 14px !important;
  }

  /* Focus alone must not dress a collapsed card as an open one. Tapping a row
     leaves focus on it, so a card you had just closed still looked open. */
  #packs.packs-outcomes .pack-rail-card:focus-within:not(.is-expanded) {
    box-shadow: inset 0 -1px 0 #0b173418 !important;
    z-index: auto;
  }

  #packs.packs-outcomes .pack-rail-card.is-expanded {
    box-shadow: inset 0 1px 0 #ffffffcc, inset 0 -1px 0 #0b173418 !important;
    z-index: 2;
  }

  #packs.packs-outcomes .pack-rail-card:last-child,
  #packs.packs-outcomes .pack-rail-card:last-child:focus-within:not(.is-expanded) {
    box-shadow: none !important;
  }

  #packs.packs-outcomes .pack-rail-card:last-child.is-expanded {
    box-shadow: inset 0 1px 0 #ffffffcc !important;
  }
}

@media (max-width: 860px) {
  /* Centre the rule in the gap rather than sitting it on the footer's edge:
     it was 40px under the card and 70px above the wordmark. */
  .site-footer .footer-main::before { top: 15px; }
}

/* -------------------------------------------------------------
   PACKS — the group separator belongs between rows, not under the
   last one, where the 14px bottom radius clips it into two stubs
   in the corners. That is the odd edge on the collapsed stack.
   ------------------------------------------------------------- */
@media (max-width: 680px) {
  #packs.packs-outcomes .pack-rail-card:last-child { box-shadow: none !important; }
}

/* -------------------------------------------------------------
   TASTE STAGE (mobile) — smaller category pills, staggered swap
   ------------------------------------------------------------- */
@media (max-width: 860px) and (min-height: 620px) {
  #taste-stage.is-mobile-story .taste-stage-tabs { padding: 3px; }

  /* compare-mobile-story.css scopes these through .taste-lane--pick, which
     outranks a plainer selector — match that path. */
  #taste-stage.is-mobile-story .taste-lane--pick .taste-stage-tabs button {
    min-height: 26px;
    padding: 2px 10px;
    font-size: .52rem;
  }

}

/* -------------------------------------------------------------
   HERO READOUT (mobile) — equal air above and below
   The service copy sits between the mode grid and the CTA and should
   read as centred in that gap whether it runs to one line or two.
   Several layers set the flex gap and the readout's margins through
   .has-sky selectors, so this matches that weight rather than
   fighting it from a plainer one.
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  html .hero.has-sky .hero-actions,
  html .hero.has-sky-fallback .hero-actions {
    gap: 0 !important;
    /* site.css adds 6px here, which would land on the readout's top side only. */
    margin-top: 0 !important;
  }

  html .hero.has-sky .hero-actions .hero-readout,
  html .hero.has-sky-fallback .hero-actions .hero-readout {
    margin: 14px 0 !important;
    top: 0 !important;
  }
}

/* -------------------------------------------------------------
   HERO MODE PILLS (mobile) — an active marker that is not clipped
   The selected row was marked with `box-shadow: inset 3px 0 0` on a
   box with an 8px radius, so the bar's two ends were shaved off by
   the rounded corners and it read as a cut-off shadow sitting under
   the icon's own offset shadow. A short centred bar owns its shape
   instead, and clears the icon.
   ------------------------------------------------------------- */
@media (max-width: 860px) {
  .hero .hero-mode[aria-checked="true"] {
    /* site.css:1441 sets the clipped bar with !important. */
    box-shadow: none !important;
    position: relative;
  }

  .hero .hero-mode[aria-checked="true"]::after {
    content: "";
    display: block !important;
    position: absolute;
    left: 5px;
    top: 50%;
    width: 3px;
    height: 20px;
    margin: 0;
    border-radius: 2px;
    background: var(--cta-hover, #3357d7);
    transform: translateY(-50%);
    opacity: 1;
    pointer-events: none;
  }
}

/* -------------------------------------------------------------
   TASTE STAGE (mobile) — the stacked runway

   The deck is a pure function of scroll progress now, so there are
   no transitions on the cards: scroll is the clock. The driver
   writes four numbers per lane — --d (stack depth 0..2), --dim (how
   far it falls back in the colour), and --bow/--fade for the card
   currently sliding under the deck — and the single transform below
   turns those into the whole choreography. Nothing ever leaves to
   the side; the cards stay one stack throughout.

   The runway lives in .taste-stage-hold rather than the section's
   own height, so sticky travel equals the spacer exactly with no
   arithmetic against padding. Both page-polish.css and
   compare-scroll-story.css pin that spacer to 0 with !important at
   (1,2,1), hence the weight here.
   ------------------------------------------------------------- */
@media (max-width: 860px) and (min-height: 620px) {
  html #taste-stage.taste-stage.is-mobile-story {
    height: auto !important;
    min-height: 0 !important;
    padding-block: 0 !important;
    overflow: visible;
    --peek: 30px;
    --plinth-stack: 46px;
  }

  html #taste-stage.taste-stage.is-mobile-story .taste-stage-hold {
    /* No pin, so no runway. The hold existed only to give a sticky shell
       something to hold against; with the shell in normal flow it is dead
       scroll the reader has to drag through. */
    display: none !important;
    height: 0 !important;
    pointer-events: none;
  }

  /* Not sticky on a phone. The deck opens on Interaktive and does not step
     through beats any more, so pinning bought nothing and cost a stretch of
     scroll that felt stuck. The shell keeps a definite height because the
     lanes and cards inside it are absolutely positioned -- with height:auto
     the arena collapses to nothing and the deck disappears -- but it now
     scrolls past with the rest of the page like any other section. */
  #taste-stage.is-mobile-story .taste-stage-shell {
    position: static !important;
    top: auto !important;
    height: calc(100svh - 160px);
    padding-top: 18px;
  }

  #taste-stage.is-mobile-story .taste-card-motion {
    top: calc(var(--peek) * 2) !important;
    /* Pulled in a little. The deck ran the full width of the arena, which made
       the card wider than the square shot inside it and left a band of plate
       down each side. */
    left: 11px !important;
    right: 11px !important;
    /* A stack, not a fan: depth is the only axis. --bow and --fade belong to the
       card currently sliding under the deck; everything else leaves them at rest. */
    --d: 0;
    --drop: 0px;
    --bow: 0px;
    --fade: 1;
    --dim: 0;
    transform-origin: 50% 0;
    will-change: transform, opacity, filter;
    backface-visibility: hidden;
    /* The entrance rides inside these declarations rather than in a keyframe.
       Both transform and opacity are !important here, and an !important
       declaration beats an animation -- the same trap that left the hero
       reveal animating opacity while its scale silently did nothing. Composing
       --enter-x / --enter-y / --enter-fade into the values that already win
       means the stack maths and the entrance cannot fight each other.

       The transition was none because the old scroll-linked painter rewrote
       --d every frame and any easing read as lag. Nothing drives --d per frame
       now; it changes when a card is tapped to the front, where easing is what
       you want anyway. */
    /* None. These are scrubbed against scroll position; easing them would put
       the deck behind the reader's thumb. */
    transition: none !important;
    transform:
      translate3d(
        var(--enter-x, 0px),
        calc(var(--d) * var(--peek) * -1 + var(--drop) + var(--bow) + var(--enter-y, 0px)),
        0)
      scale(calc(1 - var(--d) * 0.09)) !important;
    /* compare-mobile-story.css still carries opacity:1!important from the
       CSS-only version of this section, so this has to answer in kind. */
    opacity: calc(var(--fade) * var(--enter-fade, 1)) !important;
    filter: saturate(calc(1 - var(--dim) * .55)) brightness(calc(1 - var(--dim) * .18));
  }

  /* The podium and its category pills stay above the cards, so the front card
     reads as seated on the plinth rather than floating over it. The cards run
     in a 14-30 band; anything here above that is clear of them. */
  /* ---- Entrance -------------------------------------------------------
     DIY and Traditional come in from the sides they already sit on, and
     Interaktive drops onto the stack from above -- the one that matters
     arrives last and lands on top. Offsets are deliberately short (78% of the
     card, not a full screen) and paired with a fade that runs at roughly half
     the distance's duration, so a card is barely visible while it is outside
     the stage. That avoids clipping the arena, which would have cut the card
     shadows off with it.

     The stagger is what makes it read as a sequence rather than three things
     moving at once: sides first, hero card a beat behind.

     These states hang off .is-deck-armed, not off the absence of .is-deck-in,
     and the difference matters. The pre-entry state is opacity 0. If it were
     the default, then any path where the script does not run -- an error
     earlier in the file, a blocked bundle, a browser that never fires the
     trigger -- would leave this section permanently blank, and it is the
     argument the page is making. Armed is added by the script at the moment it
     commits to firing the entrance, so the cards can only be hidden while
     something is alive and intending to show them. With no script at all the
     deck simply renders, which is the right thing to fail to. */
  /* Scrubbed, not transitioned. Each lane reads its own slice of the scroll
     progress, so the offset is a function of where the reader is rather than
     of how long ago a class was added -- which is what lets it run backwards
     when they scroll up. A transition here would fight the scroll and lag
     behind it, exactly as it does on desktop, which is why desktop sets none.

     The 1 fallbacks matter: with no script the deck is simply finished. */
  #taste-stage.is-mobile-story .taste-lane--diy .taste-card-motion {
    --enter-x: calc((1 - var(--deck-diy, 1)) * -78%);
    --enter-fade: var(--deck-diy, 1);
  }
  #taste-stage.is-mobile-story .taste-lane--agency .taste-card-motion {
    --enter-x: calc((1 - var(--deck-agency, 1)) * 78%);
    --enter-fade: var(--deck-agency, 1);
  }
  /* Travel and fade are driven separately here: the drop is long, the fade is
     short, so the card is opaque for all of the fall you actually see. */
  #taste-stage.is-mobile-story .taste-lane--pick .taste-card-motion {
    /* 17vh, not 46. At 46 the card began about 329px above the stage, so it
       spent most of its drop over the client ribbon and the industries strip
       -- a card sliding across two other sections on its way in. 17vh starts
       it just above the deck it is joining: still unmistakably a fall, but one
       that happens inside its own section. */
    --enter-y: calc((1 - var(--deck-pick, 1)) * -17vh);
    --enter-fade: var(--deck-pick-fade, 1);
  }

  #taste-stage.is-mobile-story .taste-lane--diy .taste-card-motion {
    transition-delay: 0ms, 0ms !important;
  }
  #taste-stage.is-mobile-story .taste-lane--agency .taste-card-motion {
    transition-delay: 90ms, 90ms !important;
  }
  #taste-stage.is-mobile-story .taste-lane--pick .taste-card-motion {
    transition-delay: 280ms, 280ms !important;
  }

  /* ---- The rest of the section arrives too ---------------------------
     Desktop resolves the title, the spotlight, the podium and the category
     pills by scrubbing --title-reveal / --spotlight-reveal / --podium-reveal /
     --category-reveal as the section passes. Every consumer of those is behind
     min-width: 861px and .is-desktop-story, so on a phone the variables were
     written and read by nothing: the chrome was always fully present and the
     cards were the only moving part, which is what read as flat.

     These are the mobile consumers of the same four variables, so mobile and
     desktop now express one idea rather than two. compare-mobile-story.js
     drives them from the entrance instead of from scroll, because this section
     no longer pins.

     Each falls back to 1. If the script never runs, the chrome is simply
     there -- the same way the cards default to visible.

     The !important is load-bearing: compare-mobile-story.css pins the intro to
     opacity:1 / transform:none with !important, left over from the CSS-only
     version of this section. */
  /* Delayed to meet the Interaktive card as it lands. The heading and the card
     it belongs to should read as one arrival, not as a caption that turned up
     first and then waited. */
  #taste-stage.is-mobile-story .taste-stage-intro > * {
    opacity: var(--title-reveal, 1) !important;
    transform: translate3d(0, calc((1 - var(--title-reveal, 1)) * 16px), 0) !important;
    transition: none !important;
  }

  #taste-stage.is-mobile-story .taste-plinth {
    opacity: var(--podium-reveal, 1) !important;
    transform: translate3d(0, calc((1 - var(--podium-reveal, 1)) * 26px), 0) !important;
    transition: none !important;
  }

  /* The mobile spotlight rests at .55, not 1, so it scales rather than snaps. */
  #taste-stage.is-mobile-story .taste-stage-shell::after {
    opacity: calc(var(--spotlight-reveal, 1) * .55) !important;
    transition: none !important;
  }

  /* compare-refinement.css pins .taste-lane--pick .taste-stage-tabs to
     opacity:1!important, which is one class more specific than the plain tabs
     selector -- so the pills were the one piece of chrome that ignored the
     reveal and sat there fully formed while everything else arrived. Matching
     its shape is enough; no extra weight needed beyond the lane. */
  /* Opacity only. The pills are position:absolute with left:50%, centred by a
     translate(-50%, -50%) that compare-refinement.css also uses to carry their
     scale reveal. Overriding transform here dropped the centring and threw the
     row half its own width to the right, off the screen edge -- and it was
     redundant besides, because scale(var(--category-reveal)) already brings
     them in. Leave the transform to the rule that owns the positioning. */
  #taste-stage.is-mobile-story .taste-stage-tabs,
  #taste-stage.is-mobile-story .taste-lane--pick .taste-stage-tabs {
    opacity: var(--category-reveal, 1) !important;
    transition: none !important;
  }

  /* Nothing travels for anyone who asked for less motion; the deck simply is
     where it belongs. */
  @media (prefers-reduced-motion: reduce) {
    #taste-stage.is-mobile-story .taste-card-motion,
    #taste-stage.is-mobile-story.is-deck-armed:not(.is-deck-in) .taste-lane--diy .taste-card-motion,
    #taste-stage.is-mobile-story.is-deck-armed:not(.is-deck-in) .taste-lane--agency .taste-card-motion,
    #taste-stage.is-mobile-story.is-deck-armed:not(.is-deck-in) .taste-lane--pick .taste-card-motion {
      --enter-x: 0px;
      --enter-y: 0px;
      --enter-fade: 1;
      transition: none !important;
      transition-delay: 0ms !important;
    }
    #taste-stage.is-mobile-story .taste-stage-intro > *,
    #taste-stage.is-mobile-story .taste-plinth,
    #taste-stage.is-mobile-story .taste-stage-tabs {
      opacity: 1 !important;
      transform: none !important;
      transition: none !important;
    }
    #taste-stage.is-mobile-story .taste-stage-shell::after {
      opacity: .55 !important;
      transition: none !important;
    }
  }

  /* The comparison stills are square masters (~721x724) and the desktop frame
     is square to match. On a phone the frame was flex-sized to whatever height
     the card had left over -- 327 x 310, an aspect of 1.053 -- and with
     object-fit: contain the image fitted to height and left ~17px of plate
     showing down either side. That is the "looks wide": the picture was square
     all along, the box around it was not.

     Squaring the frame means one square export serves both layouts and nothing
     letterboxes. */
  /* 16:9, matching the stills. This was 1/1 when the masters were square;
     mobile-refine loads after compare-scroll-story, so leaving it would have
     kept the frame square and letterboxed the new wide art. */
  /* 16:9 is what the shot wants, not what it must have. The card's height is
     fixed by the deck layout, and on a short phone the label, a rigid 16:9
     picture and three lines of copy come to more than there is -- measured at
     393x660: 291px of content in a 244px card, so the third line was clipped
     and the list spilled 36px past the card onto the plinth.

     flex-shrink lets the picture give way instead. It keeps its ratio whenever
     there is room and loses height only when the alternative is losing words,
     and because it is object-fit: cover, losing height crops the frame rather
     than squashing what is in it. The copy never shrinks: it is the argument.  */
  #taste-stage.is-mobile-story .taste-card-shot {
    aspect-ratio: 16 / 9 !important;
    flex: 0 1 auto !important;
    width: 100% !important;
    height: auto !important;
    min-height: 0 !important;
  }

  #taste-stage.is-mobile-story .taste-card-points {
    flex: 0 0 auto !important;
  }

  #taste-stage.is-mobile-story .taste-plinth { z-index: 40; }
  #taste-stage.is-mobile-story .taste-stage-tabs { z-index: 41; }
}

/* ============================================================
   Taste deck — one scroll, one card
   ------------------------------------------------------------
   The runway carries three zero-size snap markers (written by
   compare-mobile-story.js). scroll-snap-stop:always means the
   page may not pass over one, so a flick of any length lands on
   the next card and stops there. Mandatory snapping is armed by
   that script only while the section owns the frame — the rest
   of the page scrolls normally.
   ============================================================ */
/* Proximity, not mandatory. Mandatory on the document scroller drags every
   small movement back to the current snap position, so a slow scroll moved
   nothing until the finger lifted. Proximity leaves ordinary scrolling alone
   and lets scroll-snap-stop:always do the arresting on a flick. */
/* No longer applied: compare-mobile-story.js stopped arming this class once
   the deck came down to one beat, because a single scroll-snap-stop marker on
   the document scroller only ever arrested the page instead of landing it.
   Kept as a definition so the marker rules below still read as a pair. */
html.is-taste-snap { scroll-snap-type: y proximity; }

#taste-stage.taste-stage.is-mobile-story { position: relative; }

#taste-stage.is-mobile-story .taste-snap {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  background: none;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* Stood down while an in-page anchor is animating through the section. */
#taste-stage.is-mobile-story.is-taste-anchoring .taste-snap { scroll-snap-align: none; }

/* ============================================================
   MASTHEAD — hamburger on mobile. Discuss lives in the sheet,
   not on the top bar (it is already in the footer).
   ============================================================ */
@media (max-width: 860px) {
  .site-header .menu-button { display: inline-flex !important; }
  .site-header .site-nav {
    display: none !important;
  }
  .site-header .site-nav.is-open {
    display: flex !important;
  }
  .site-header .header-inner { gap: 12px; }
}
