/* ===== Scroll-margin for hash links, Lenis html/body sizing, desktop custom cursor ===== */
    /* The header is position:fixed and 80px tall. Anything that lands on a
       section by hash (a shared /#pawby-menu or /#faq link, a browser
       back/forward, the menu tabs' scrollIntoView) otherwise puts the
       section's first 80px — its heading — underneath the header. The JS
       anchor handlers already subtract this offset themselves; this covers
       every native scroll-to-target. */
    body.home section[id], body.home footer[id] { scroll-margin-top: 80px; }

    html.lenis, html.lenis body { height: auto; }
    .lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
    .lenis [data-lenis-prevent] { overscroll-behavior: contain; }
    .lenis.lenis-smooth iframe { pointer-events: none; }

    /* Smooth custom cursor (src/scripts/smooth-cursor.js) — a small navy dot
       that trails the real cursor with inertia. Purely decorative: fixed,
       pointer-events:none, so it never intercepts clicks/drags/hover. The
       script itself only ever creates this element on desktop-with-mouse
       (hover:hover + pointer:fine), so the @media guard here is belt-and-
       braces, not load-bearing — but keeps the rule inert everywhere else
       even if the DOM node were ever present. */
    @media (hover: hover) and (pointer: fine) {
      .pk-cursor {
        position: fixed;
        top: 0;
        left: 0;
        width: 9px;
        height: 9px;
        border-radius: 50%;
        /* Exactly two colors — navy over the page's light sections, white
           over its dark ones (.hero / #benefits / .site-footer) — instead of
           mix-blend-mode:difference, whose inverted-pixel result produced
           off-brand tints (e.g. a warm tan over navy) rather than a clean
           navy/white swap. .pk-cursor--dark is toggled by smooth-cursor.js
           via delegated pointerover/pointerout against DARK_BG_SELECTOR. */
        background: var(--color-primary, #15243C);
        pointer-events: none;
        z-index: 10000; /* above the header/cart/menu overlays (max existing: 9999) */
        opacity: 0;
        transform: translate3d(-100px, -100px, 0) translate(-50%, -50%);
        transition: opacity 0.2s ease, width 0.2s ease, height 0.2s ease,
          border-radius 0.2s ease, padding 0.2s ease, background 0.2s ease;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .pk-cursor--visible { opacity: 1; }
      /* Over a dark section (.hero / #benefits / .site-footer) the dot
         flips to white instead of navy — see DARK_BG_SELECTOR in
         smooth-cursor.js. */
      .pk-cursor--dark { background: #fff; }
      /* Grow on hover over anything clickable (links, buttons, cards) —
         .pk-cursor--hover is toggled by smooth-cursor.js via delegated
         pointerover/pointerout. */
      .pk-cursor--hover { width: 22px; height: 22px; opacity: 0.55; }

      /* FAQ label text — hidden (0 size) except in --faq mode, so it never
         affects the plain dot/hover layout. */
      .pk-cursor-label {
        display: block;
        font-family: var(--font-heading, 'Nunito', sans-serif);
        font-size: 0;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: #fff;
        white-space: nowrap;
        line-height: 1;
      }
      /* FAQ rows: dot morphs into an "Open"/"Close" pill instead of growing —
         see the faqLabel()/pointerover handling in smooth-cursor.js. */
      .pk-cursor--faq {
        width: auto;
        height: auto;
        min-width: 0;
        border-radius: 999px;
        padding: 9px 16px;
        opacity: 1;
        /* The Open/Close pill stays a solid readable navy label — it only
           ever shows over the FAQ section's light background, so it opts
           out of the difference blend the plain dot uses elsewhere. */
        mix-blend-mode: normal;
        background: var(--color-primary, #15243C);
      }
      .pk-cursor--faq .pk-cursor-label { font-size: 0.7rem; }
    }
    @media (prefers-reduced-motion: reduce) {
      .pk-cursor { display: none; }
    }

/* ===== Paw-duo background prints + cursor paw-trail decorations ===== */
    .paw-duo-host { isolation: isolate; }

    .paw-duo {
      position: absolute;
      inset: 0;
      z-index: -1;               /* behind the host's content, above its background */
      pointer-events: none;
      overflow: hidden;          /* keep the big paws inside the section box */
      --paw-ink: rgba(72, 82, 99, 0.11);
    }
    .paw-duo__paw {
      position: absolute;
      width: var(--paw-size, 232px);
      height: var(--paw-size, 232px);
      color: var(--paw-ink);
      opacity: 0;
      transform: translate(-50%, -50%)
                 rotate(var(--paw-rot, 0deg))
                 scaleX(var(--paw-flip, 1))
                 scale(0.62);
      transform-origin: 50% 50%;
    }
    .paw-duo__paw svg { width: 100%; height: 100%; display: block; fill: currentColor; }

    @media (max-width: 768px) {
      .paw-duo__paw { width: var(--paw-size-m, 124px); height: var(--paw-size-m, 124px); }
    }

    .paw-duo__paw {
      transition: opacity .7s ease,
                  transform .9s cubic-bezier(0.22, 1, 0.36, 1);
    }
    .paw-duo--in .paw-duo__paw {
      opacity: 1;
      transform: translate(-50%, -50%)
                 rotate(var(--paw-rot, 0deg))
                 scaleX(var(--paw-flip, 1))
                 scale(1);
    }
    .paw-duo--in .paw-duo__paw:nth-child(2) { transition-delay: .12s; }

    /* ── Cursor paw trail — #about / #faq / #pawby-menu / .pawby-quiz-section
       Built by initPawTrail() in site-animations.js. One .paw-trail layer per
       section; initPawTrail() sets its z-index inline per host (About 1, FAQ 0,
       Menu/Quiz -1) so it always sits above that section's decorative layers
       (dot pattern, big .paw-duo) but below its interactive/text content —
       prints show in the whitespace and never hurt readability. Each
       .paw-trail__paw spawns at the cursor, plays paw-trail once, then removes
       itself. pointer-events:none throughout. */
    .paw-trail {
      position: absolute;
      inset: 0;
      pointer-events: none;
      overflow: hidden;          /* clip prints laid near the section edge */
    }
    .paw-trail__paw {
      position: absolute;
      width: 20px;
      height: 20px;
      margin: -10px 0 0 -10px;   /* centre the print on the cursor point */
      color: rgba(72, 82, 99, 0.22);   /* same slate ink as .paw-duo, a touch stronger */
      opacity: 0;
      transform: rotate(var(--rot, 0deg)) scale(0.65);
      will-change: transform, opacity;
      animation: paw-trail 1.05s cubic-bezier(0.33, 1, 0.68, 1) forwards;
    }
    .paw-trail__paw svg { width: 100%; height: 100%; display: block; fill: currentColor; }

    @keyframes paw-trail {
      0%   { opacity: 0;    transform: rotate(var(--rot, 0deg)) scale(0.65); }
      35%  { opacity: 0.85; transform: rotate(var(--rot, 0deg)) scale(0.94); }
      55%  { opacity: 1;    transform: rotate(var(--rot, 0deg)) scale(1); }
      100% { opacity: 0;    transform: rotate(var(--rot, 0deg)) scale(1.1); }
    }

    @media (prefers-reduced-motion: reduce) {
      .paw-duo__paw { transition: none; }
      .paw-trail { display: none; }   /* backup — initPawTrail() also bails */
    }

/* ===== Bloom-outline hover — shared CTA effect (About/FAQ/Benefits) ===== */
      /* ── Bloom-outline hover — shared across 3 CTAs ──────────────────────
         Converted from threeui's "bloom-outline-button" (@designcodeio/
         threeui), which is a React component with a pointermove-driven
         CSS custom property + a canvas-free radial "bloom" pseudo-element —
         reimplemented here as plain CSS + one small inline script instead
         of pulling in the npm package. Kept each button's own solid pill
         look (not the demo's transparent-outline default) so it stays
         visually consistent with the rest of the Tier-1 CTA family in
         style.css; `.btn-bloom` + `data-bloom-btn` opt a button INTO the
         squared-off corners + bloom/label-swap layers without touching the
         shared base classes (`.btn-mission-primary` / `.faq-cta-btn` /
         `.benefits-cta`), so any other user of those classes is untouched.
         Used on: the About "Our Menu" button, the FAQ Telegram CTA (both
         navy bg / white text, so `--white-fill` — bloom fills white, text
         flips to navy), and the Benefits Telegram CTA (the one Tier-1
         exception with a white bg / navy text, since that section sits on
         a dark photo — so it gets `--navy-fill` instead: a white bloom
         there would be invisible on the already-white base).

         Cursor-only (mouse/trackpad): the bloom position script and the
         hover reveal below are both gated on `(hover: hover) and
         (pointer: fine)`, same reasoning as the earlier label-swap — a
         touchscreen has no real hover state, so it would either get stuck
         mid-animation or double-fire as both hover and tap. Touch just
         gets each button's plain static look, unchanged. */
      .btn-bloom {
        border-radius: 10px !important;
        position: relative;
        overflow: hidden;
        isolation: isolate;
      }
      .btn-bloom > * {
        position: relative;
        z-index: 1;
      }
      .btn-bloom-fill {
        --bloom-x: 50%;
        --bloom-y: 50%;
        --bloom-d: 0px;
        position: absolute;
        z-index: 0;
        left: var(--bloom-x);
        top: var(--bloom-y);
        width: var(--bloom-d);
        height: var(--bloom-d);
        border-radius: 50%;
        transform: translate(-50%, -50%) scale(0);
        pointer-events: none;
      }
      .btn-bloom--white-fill .btn-bloom-fill {
        background: #fff;
      }
      .btn-bloom--navy-fill .btn-bloom-fill {
        background: var(--color-primary-dark);
      }
      .btn-label-swap {
        position: relative;
        display: inline-grid;
        overflow: hidden;
      }
      .btn-label-swap__text {
        grid-area: 1 / 1;
      }
      @media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
        .btn-bloom-fill {
          transition: transform 0.55s cubic-bezier(0.19, 1, 0.22, 1);
        }
        .btn-bloom:hover .btn-bloom-fill {
          transform: translate(-50%, -50%) scale(1);
        }
        .btn-bloom--white-fill:hover {
          color: var(--color-primary-dark) !important;
        }
        .btn-bloom--navy-fill:hover {
          color: #fff !important;
        }
        .btn-label-swap__text--out {
          transition: transform 0.4s ease-out, opacity 0.4s ease-out, filter 0.4s ease-out;
        }
        .btn-label-swap__text--in {
          transform: translateY(-100%);
          opacity: 0;
          filter: blur(4px);
          transition: transform 0.3s ease-in, opacity 0.3s ease-in, filter 0.3s ease-in;
        }
        .btn-bloom:hover .btn-label-swap__text--out {
          transform: translateY(100%);
          opacity: 0;
          filter: blur(4px);
        }
        .btn-bloom:hover .btn-label-swap__text--in {
          transform: translateY(0);
          opacity: 1;
          filter: blur(0);
        }
      }

/* ===== About section — sliding product cups decoration ===== */
      /* ── About sliding cups ───────────────────────────────────────────────
         4 product-cup webp (src/assets/cups/) sliding horizontally through
         the empty gutters of .mission-top as the section scrolls through
         the viewport, continuously and bidirectionally — not a one-time
         entrance. src/scripts/about-cups.js writes two non-negative
         progress fractions on .mission-cups every scroll tick (rAF-
         throttled, same getBoundingClientRect() pattern as --hero-progress
         for the Hero→Mission transition):
           --mission-cup-enter: 1 while the section is fully below the
             viewport (cups off-screen, entering side) → 0 once the section
             is vertically centered (cups at their placed rest spot — this
             is also the hard stop: the rest spot is calibrated to sit in
             the gutter, clear of the text column, and a cup never travels
             any further inward than that, so it can never reach the text)
           --mission-cup-exit:  0 at that same centered point → 1 once the
             section has scrolled fully past, above the viewport (cups
             off-screen again — but back out the SAME side they entered
             from, a retreat, not a continued crossing)
         Exactly one of the two is ever non-zero at a time (both are 0 at
         rest), so a cup's own --cup-enter-offset / --cup-exit-offset —
         signed px+vw distance, calibrated per cup from its own rest
         position out to its own entering edge — combine additively into
         one translateX with no CSS conditional needed:
         `enter*enterOffset + exit*exitOffset`. The two offsets are the
         SAME value per cup on purpose: exit just retraces the enter path
         in reverse, so the cup never has any reason to travel toward the
         section's centre/text column in either direction — it only ever
         moves between its rest spot (already clear of the text, see the
         placement comment below) and straight off-screen on its own side.
         (An earlier version sent the exit leg crossing to the *opposite*
         edge, which meant a slow crawl that visibly passed behind the
         section's own text/CTA — replaced by this same-side retreat.)
         Left-side cups (1 beef, 2 chicken) enter from and retreat back out
         the left; right-side cups (3 pork, 4 fish) enter from and retreat
         back out the right — each only ever moves toward its own natural
         side, which reads more naturally against their fixed left/right
         placement below, and guarantees it can never cross the text.
         transform:translateX() only (GPU, no layout, no opacity/rotation).
         Reduced motion: the JS never runs, both custom properties stay at
         their CSS default 0, and the rule that turns them into a
         translateX only lives inside the same
         prefers-reduced-motion:no-preference block as before, so cups sit
         at their static rest position with zero motion.
         Layered z-index:1 (above .mission-dots + .paw-duo, below the z-index:2
         .mission-main-content) and pointer-events:none, so they never cover
         the copy or the "Our Menu" button. Clipped by .mission-top's own
         overflow:hidden, same as .paw-duo — .mission-top is measured to be
         exactly viewport-width, which is what the vw terms below assume. */
      .mission-cups {
        position: absolute;
        inset: 0;
        z-index: 1;
        pointer-events: none;
        overflow: hidden;
        --mission-cup-enter: 0;
        --mission-cup-exit: 0;
      }
      .mission-cup { position: absolute; }
      .mission-cup img {
        display: block;
        width: 100%;
        height: auto;
        filter: drop-shadow(0 20px 30px rgba(15, 42, 61, 0.18));
      }

      /* placement + rest size — no motion, so it's outside the media query.
         2 left / 2 right, deliberately uneven sizes + offsets.
         Beef + Fish sit at mid-height, NOT in the top-left / bottom-right
         corners: initPawDuos() (site-animations.js) drops a 250px paw at
         15%/24% and a 224px paw at 85%/83%, and a corner cup would land
         right on top of it. Pushed to top:47% / bottom:38% they clear the
         paw bounding box by ~45-80px at every desktop width while staying
         in the gutter (left/right ~3% keeps a 230-250px cup left of the
         780px text column). Chicken (bottom-left) + Pork (top-right) keep
         their corners — no paw there. */
      /* --cup-enter-offset / --cup-exit-offset: same signed distance (at
         full enter=1 or exit=1) that clears this cup's TRAILING edge off
         its own entering side — e.g. cup 1's left edge sits at 3vw, so its
         right edge (left+width) must pass 0: -(3vw + 230px + 60px margin).
         Both properties get the identical value: enter slides the cup in
         from there, exit retreats it straight back out to the same spot —
         neither leg ever moves toward the opposite edge, so the cup stays
         in its own gutter the whole time and can't reach the text column.
         (For the right-anchored cups, 3 & 4, the offset instead clears
         their left edge past 100vw — same idea, mirrored.) */
      .mission-cup--1 {
        width: 230px; left: 3%; top: 47%;
        --cup-enter-offset: calc(-3vw - 290px);
        --cup-exit-offset: calc(-3vw - 290px);
      } /* beef    — mid-left,    enters left,  retreats back out left */
      .mission-cup--2 {
        width: 180px; left: 6%; bottom: 9%;
        --cup-enter-offset: calc(-6vw - 240px);
        --cup-exit-offset: calc(-6vw - 240px);
      } /* chicken — lower-left,  enters left,  retreats back out left */
      .mission-cup--3 {
        width: 210px; right: 4%; top: 15%;
        --cup-enter-offset: calc(4vw + 270px);
        --cup-exit-offset: calc(4vw + 270px);
      } /* pork    — upper-right, enters right, retreats back out right */
      .mission-cup--4 {
        width: 250px; right: 2%; bottom: 38%;
        --cup-enter-offset: calc(2vw + 310px);
        --cup-exit-offset: calc(2vw + 310px);
      } /* fish    — mid-right,   enters right, retreats back out right */

      @media (prefers-reduced-motion: no-preference) {
        .mission-cup {
          /* --cup-away: 0 at rest → 1 fully off-screen (whichever direction),
             used only for the scale/opacity settle below — the translateX
             itself still comes straight from the eased enter/exit fractions
             (about-cups.js), unchanged. A cup arriving at rest eases in
             very slightly larger-than-life (1.04) and settles to 1, and
             fades from 0.85 back to fully opaque, so the landing itself
             reads as a soft settle rather than the slide just stopping. */
          --cup-away: calc(var(--mission-cup-enter) + var(--mission-cup-exit));
          transform: translateX(calc(
            var(--mission-cup-enter) * var(--cup-enter-offset) +
            var(--mission-cup-exit) * var(--cup-exit-offset)
          )) scale(calc(1 + var(--cup-away) * 0.04));
          opacity: calc(1 - var(--cup-away) * 0.15);
          will-change: transform, opacity;
        }
      }

      /* Align to the codebase's existing .mission breakpoints (768 / 480). */
      @media (max-width: 768px) {
        .mission-cup--1, .mission-cup--2,
        .mission-cup--3, .mission-cup--4 { width: 110px; }
      }
      @media (max-width: 480px) {
        .mission-cup--1, .mission-cup--2,
        .mission-cup--3, .mission-cup--4 { width: 78px; }
      }

      /* ── Large screens: anchor cups to the TEXT COLUMN edge, not the
         viewport edge ───────────────────────────────────────────────────
         The base rules above place cups at a fixed % from the viewport's
         own left/right edge (left:3%, right:4%, etc.) — fine up to roughly
         desktop width, but on a wide screen that % keeps pushing the cup
         further from the text column, leaving a big empty gap and making
         the cup read as small relative to all that space.
         From 1100px up, position each cup relative to the text column's
         OWN rendered edge instead — via --text-edge-left/--text-edge-right,
         two px custom properties src/scripts/about-cups.js measures from
         .mission-main-content's real getBoundingClientRect() (it's a flex
         column with align-items:center, so its width is content-fit,
         capped at max-width:780px but usually narrower — not a fixed box a
         pure CSS calc() can derive from the viewport alone) — with a small
         fixed gap, so the cup sits right beside the text at any width
         above this breakpoint, and scale its size up with the viewport
         (clamp) so it doesn't look tiny on a big screen. Only left/right +
         width change here — top/bottom stay as set above (already tuned to
         clear the paw-duo decorations), and
         --cup-enter-offset/--cup-exit-offset are recalculated as a
         generous half-viewport-plus-own-width overshoot (comfortably
         off-screen regardless of the now-dynamic rest position) rather
         than the old fixed px+vw figures, keeping the same-side retreat
         behavior from the block above intact. */
      @media (min-width: 1100px) {
        .mission-cup--1 {
          --cup-w: clamp(240px, 15vw, 360px);
          width: var(--cup-w);
          left: calc(var(--text-edge-left, 50%) - 40px - var(--cup-w));
          right: auto;
          --cup-enter-offset: calc(-50vw - var(--cup-w) - 100px);
          --cup-exit-offset: calc(-50vw - var(--cup-w) - 100px);
        }
        .mission-cup--2 {
          --cup-w: clamp(190px, 12vw, 280px);
          width: var(--cup-w);
          left: calc(var(--text-edge-left, 50%) - 40px - var(--cup-w));
          right: auto;
          --cup-enter-offset: calc(-50vw - var(--cup-w) - 100px);
          --cup-exit-offset: calc(-50vw - var(--cup-w) - 100px);
        }
        .mission-cup--3 {
          --cup-w: clamp(220px, 13.5vw, 320px);
          width: var(--cup-w);
          left: calc(var(--text-edge-right, 50%) + 40px);
          right: auto;
          --cup-enter-offset: calc(50vw + var(--cup-w) + 100px);
          --cup-exit-offset: calc(50vw + var(--cup-w) + 100px);
        }
        .mission-cup--4 {
          --cup-w: clamp(250px, 16vw, 380px);
          width: var(--cup-w);
          left: calc(var(--text-edge-right, 50%) + 40px);
          right: auto;
          --cup-enter-offset: calc(50vw + var(--cup-w) + 100px);
          --cup-exit-offset: calc(50vw + var(--cup-w) + 100px);
        }
      }

      /* ── About handwritten reveal ───────────────────────────────────────
         src/scripts/about-handwriting.js, once #about's .mission-top passes
         threshold 0.35, splits each .hw-line into .hw-char / .hw-word spans
         (per its data-hw), adds `.hw-armed` to [data-hw-root] in the SAME
         frame (so the hide below applies with no flash), then `.hw-in` to
         run the staggered reveal. Per-unit `transition-delay` is written
         inline by the script as one continuous, overlapping timeline across
         badge → greeting → headline → paragraph → features. The badge oval
         scribble draws on via stroke-dashoffset (--hw-scribble-delay), and
         the greeting's paw emoji fades in after the greeting text
         (--hw-paw-delay) — both set inline by the script.

         Once the whole reveal has finished the script wires the "pawsome"
         hover-wave onto the .hw-char spans it already made (mhWave below).

         Base / no-JS / reduced motion: no `.hw-armed`, so every .hw-line is
         just plain styled text and the scribble sits fully drawn. The script
         bails entirely on prefers-reduced-motion — text important, not
         decorative, so it must never end up hidden. */
      .hw-char, .hw-word { display: inline-block; white-space: pre; }

      /* Compositing hint ONLY while the reveal is animating. ~150 spans each
         promoted to a layer permanently blows iOS Safari's layer budget
         (garbled / mis-placed glyphs), so about-handwriting.js adds `.hw-done`
         to [data-hw-root] once every unit has finished transitioning and we
         drop the hint again. `.hw-armed` is never removed, so the `.hw-done`
         rule (equal specificity, later in source) is what turns it back off. */
      .hw-armed .hw-char,
      .hw-armed .hw-word { will-change: transform; }
      .hw-done .hw-char,
      .hw-done .hw-word { will-change: auto; }

      .hw-armed .hw-char,
      .hw-armed .hw-word {
        opacity: 0;
        transform: translateY(0.3em) rotate(1.5deg);
      }
      .hw-armed .mission-greeting-paw {
        opacity: 0;
        transform: translateY(0.25em) scale(0.8);
      }
      .hw-armed .hw-scribble__path { stroke-dashoffset: 1; }

      .hw-in .hw-char,
      .hw-in .hw-word {
        opacity: 1;
        transform: none;
        transition: opacity 0.65s ease, transform 0.65s cubic-bezier(0.2, 0.7, 0.3, 1);
        transition-delay: var(--d, 0ms);
      }
      .hw-in .mission-greeting-paw {
        opacity: 1;
        transform: none;
        transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.34, 1.4, 0.64, 1);
        transition-delay: var(--hw-paw-delay, 0ms);
      }
      .hw-in .hw-scribble__path {
        stroke-dashoffset: 0;
        transition: stroke-dashoffset 0.95s ease;
        transition-delay: var(--hw-scribble-delay, 0ms);
      }

      /* "pawsome" hover-wave — reuses the .hw-char spans the reveal made
         (no re-split). about-handwriting.js wires the hover/tap once the
         reveal is done and writes each char's animation-delay for the
         left↔right ripple. Transform-only, so the heading never reflows. */
      @media (prefers-reduced-motion: no-preference) {
        .mission-highlight.mh-waving .hw-char {
          animation: mhWave 620ms both;
        }
        @keyframes mhWave {
          0%   { transform: translateY(0);       animation-timing-function: cubic-bezier(0.33, 0, 0.25, 1); }
          38%  { transform: translateY(-0.16em); animation-timing-function: cubic-bezier(0.34, 1.45, 0.64, 1); }
          100% { transform: translateY(0); }
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* belt-and-suspenders — the script won't run, but never hide text */
        .hw-armed .hw-char,
        .hw-armed .hw-word,
        .hw-armed .mission-greeting-paw { opacity: 1; transform: none; }
        .hw-armed .hw-scribble__path { stroke-dashoffset: 0; }
      }

      /* Mobile: the reveal is fully disabled in about-handwriting.js (no split,
         no `.hw-armed`). This is the belt-and-suspenders for a desktop→mobile
         resize AFTER the section already armed — force every unit to its final
         state with zero transition, so #about carries 0 animation on phones. */
      @media (max-width: 768px) {
        .hw-armed .hw-char,
        .hw-armed .hw-word,
        .hw-armed .mission-greeting-paw,
        .hw-in .hw-char,
        .hw-in .hw-word,
        .hw-in .mission-greeting-paw {
          opacity: 1 !important;
          transform: none !important;
          transition: none !important;
        }
        .hw-armed .hw-scribble__path,
        .hw-in .hw-scribble__path {
          stroke-dashoffset: 0 !important;
          transition: none !important;
        }
        .mission-highlight.mh-waving .hw-char { animation: none !important; }
      }

      /* ── Hero → Mission scroll-stack: REVIVED ──────────────────────────────
         Brought back for the new HeroShowcase hero, adapted rather than
         reusing the old .stack-hero-mission wrapper verbatim: HeroShowcase
         already renders its own two-part structure — .hs-track (160vh,
         `position: relative`) containing .hs-stage (100vh, `position:
         sticky; top:0`, the literal pinned box) — so THAT pair stands in
         directly for the old wrapper/pinned pair, no extra wrapper div
         needed. .hs-stage stays pinned for exactly .hs-track's own last
         60vh of scroll (160vh − 100vh) before naturally releasing — the
         same window HeroShowcase's own discrete-step JS uses to hand off
         to native scroll once the last product is reached. `.mission`'s
         negative margin-top (-60vh, matching that exact window) pulls it
         up to start overlapping .hs-stage's tail end, so it visually rises
         and covers Hero over precisely the scroll distance Hero is
         shrinking/fading through via --hero-progress
         (src/scripts/hero-mission-shrink.js's initShrinkStack, called
         with wrapperSel: '.hs-track', pinnedSel: '.hs-stage' — same
         function/math as Happy Tails → Benefits below, just pointed at
         different selectors). Real scroll position stays frozen at
         .hs-track's own top for the entire 4-step product interaction
         (HeroShowcase locks/prevents it), so --hero-progress — and the
         mission overlap — only ever starts advancing once that interaction
         is done and the user scrolls one gesture further, exactly as
         intended; nothing here touches HeroShowcase.astro or
         hero-showcase.js. */
      @media (prefers-reduced-motion: no-preference) {
        .hs-stage {
          --hero-progress: 0;
          transform: scale(calc(1 - var(--hero-progress) * 0.13));
          transform-origin: top center;
          opacity: calc(1 - var(--hero-progress));
          will-change: transform, opacity;
        }
        .hs-stage.hero-receded {
          pointer-events: none;
        }
        .mission {
          z-index: 2;
          margin-top: -60vh;
        }

        /* ── Happy Tails → Benefits scroll-stack ──────────────────────────
           Same mechanism, no shrink/fade layer needed: Happy Tails (teal,
           opaque) freezes at the top while Benefits (its own opaque dark
           bg + ::before overlay) scrolls up and covers it completely —
           the covering IS the transition, so both sections dropped the
           .depth scroll-fade entirely. */
        .stack-family-benefits { position: relative; }
        .stack-family-benefits > .pawby-family-section {
          position: sticky;
          top: 0;
          z-index: 1;
          /* Shrink + fade as Benefits covers it — same --progress/transform
             recipe as .stack-hero-mission > .hero, driven by the same
             hero-mission-shrink.js (see initShrinkStack there). */
          --family-progress: 0;
          transform: scale(calc(1 - var(--family-progress) * 0.13));
          opacity: calc(1 - var(--family-progress));
          will-change: transform, opacity;
        }
        .stack-family-benefits > .pawby-family-section.family-receded {
          pointer-events: none;
        }
        .stack-family-benefits > .benefits {
          position: relative;
          z-index: 2;
        }

        /* Desktop (>=1024px): the hero is 100dvh and .mission is padded to
           one viewport too (see the .mission-top block in style.css), so the
           wrapper is ~200dvh on its own — a sticky child's pin range equals
           (wrapper height − hero height), which now works out to a full
           viewport + the 8px sub-pixel buffer baked into .mission-top's
           min-height. No wrapper min-height needed; the range follows the
           real element sizes. 769–1023px / mobile keep the clamp() hero +
           the shorter Mission, where the pin range is already enough. */
      }

/* ===== Pawby Menu — prev/next arrow nav ===== */
      /* ── Pawby Menu — prev/next arrows on every scrollable card row ───────
         Signature / Meal Plans / Sweet Treats now use the same .pf-nav-wrap +
         .pf-arrow markup as Special Menu (wired by initMenuArrowNav() in
         site-animations.js). .pf-nav--static is toggled on by updateArrowState()
         when the cards already fit — hide the arrows rather than show them
         greyed-out. Arrows are already display:none below 768px (style.css),
         where touch + swipe-dots take over. */
      .pf-nav-wrap.pf-nav--static .pf-arrow { display: none; }
      /* left-align the meal-plan row so arrow scrolling starts at card 1
         (cards flex-grow to fill on desktop, so this is a no-op there) */
      .pf-nav-wrap > .mp-cards-wrap { justify-content: flex-start; }

      /* ── Pawby Menu — static cards, no reveal/crossfade ────────────────────
         Was: a staggered card-reveal animation (initPawbyMenuAnimations() in
         site-animations.js, keyframe pmCardIn) plus an opacity crossfade on
         tab switch (@supports allow-discrete block). Both removed on request
         — the menu cards render at rest (opacity: 1, no transform/animation)
         from first paint, and switching tabs swaps panels instantly with no
         fade. initPawbyMenuAnimations() is no longer called from
         site-animations.js, so nothing ever adds .pmenu-cards-armed /
         .pmenu-cards-lit — this block only needs to guarantee the cards'
         resting state now that those classes never arrive. Tab-switching
         logic itself (data-active, hash routing, panel .pmenu-panel--active
         toggle) is untouched — still owned by initPawbyMenuTabs() in
         pawby-app.js. */
      .pawby-menu .pmenu-cards > .product-card,
      .pawby-menu .mp-cards-wrap > .mp-card-v2 { opacity: 1; }

/* ===== Pawby Moments — intro reveal + card track ===== */
      /* Pawby Moments — intro text entrance (eyebrow / title / subtitle only).
         initPawbyMomentsIntro() in site-animations.js adds .pm-intro--armed on
         load then .pm-intro--in when the column scrolls into view (its own
         one-shot IntersectionObserver — the stage-timeline IO with
         rootMargin -45% 0 -45%, the sticky column and the rail progress are
         all untouched). JS off / reduced motion => no classes => text shows at
         its natural state, so nothing is ever stuck invisible. */
      @media (prefers-reduced-motion: no-preference) {
        #pawby-moments .pm-intro--armed .pm-eyebrow,
        #pawby-moments .pm-intro--armed .pm-title,
        #pawby-moments .pm-intro--armed .pm-subtitle {
          opacity: 0;
          transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
                      transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
        }
        #pawby-moments .pm-intro--armed .pm-eyebrow  { transform: translateY(14px); }
        #pawby-moments .pm-intro--armed .pm-title    { transform: translateY(14px); }
        #pawby-moments .pm-intro--armed .pm-subtitle { transform: translateY(14px); }

        /* staggered reveal: badge -> title -> subtitle. The subtitle's 0.85
           resting opacity is kept exactly as authored in style.css */
        #pawby-moments .pm-intro--armed.pm-intro--in .pm-eyebrow  { opacity: 1;    transform: translateY(0); }
        #pawby-moments .pm-intro--armed.pm-intro--in .pm-title    { opacity: 1;    transform: translateY(0); transition-delay: 120ms; }
        #pawby-moments .pm-intro--armed.pm-intro--in .pm-subtitle { opacity: 0.85; transform: translateY(0); transition-delay: 240ms; }
      }

      /* ============================================================
         DEPTH TRANSITION LAYER — antar-section, scroll-driven, 0 KB JS.
         Hanya transform/opacity/filter. .depth-shell = sibling header fixed,
         TIDAK membungkus .stack-hero-mission (perspective bikin containing
         block baru → merusak sticky + fixed).
         ============================================================ */
      .depth-shell { perspective: 1400px; perspective-origin: 50% 50%; }

      @media (prefers-reduced-motion: no-preference) {
        @supports (animation-timeline: view()) {
          .depth-shell > .depth {
            animation: pkDepth linear both;
            /* !important: cegah lightningcss (minifier build) melipat
               animation-timeline ke dalam shorthand `animation` — hasilnya
               `animation:linear both pkDepth view()` yang INVALID di Chrome
               terkini (timeline bukan bagian shorthand `animation` per spec
               CSS Animations L2, cuma di-reset) → seluruh deklarasi dibuang →
               animasi mati. Terpisah + !important tidak bisa dilipat. */
            animation-timeline: view() !important;
            animation-range: cover 0% cover 100%;
            /* tanpa will-change: scroll-driven animation promote on demand */
          }
        }
      }

      /* Desktop — dramatic. Easing per-keyframe (timeline view() linear).
         Plateau scale(1)/opacity(1) diperlebar (16%–84%, dulu 44%–62%) supaya
         section tetangga TIDAK tampil sebagai ghost buram/menyusut saat masih
         bersinggungan dengan #pawby-moments di titik seam — state buram kini
         terkompres ke 16% terluar tiap sisi. */
      @keyframes pkDepth {
        0%   { transform: translateZ(-260px) scale(0.89); opacity: 0.25; filter: blur(4px); animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
        16%  { transform: translateZ(-40px)  scale(0.98); opacity: 0.9;  filter: blur(0);   animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1); }
        38%  { transform: translateZ(0)      scale(1);    opacity: 1;    filter: blur(0);   animation-timing-function: linear; }
        64%  { transform: translateZ(0)      scale(1);    opacity: 1;    filter: blur(0);   animation-timing-function: cubic-bezier(0.32, 0, 0.67, 0); }
        84%  { transform: translateZ(-40px)  scale(0.98); opacity: 0.9;  filter: blur(0);   animation-timing-function: cubic-bezier(0.64, 0, 0.78, 0); }
        100% { transform: translateZ(-260px) scale(0.89); opacity: 0.25; filter: blur(4px); }
      }

      /* Mobile: efek sama, intensitas turun, blur OFF. */
      @media (max-width: 768px) {
        .depth-shell { perspective: 900px; }
        @keyframes pkDepth {
          0%   { transform: translateZ(-90px) scale(0.97); opacity: 0.5; }
          26%  { transform: translateZ(0) scale(1);        opacity: 1; }
          74%  { transform: translateZ(0) scale(1);        opacity: 1; }
          100% { transform: translateZ(-90px) scale(0.97); opacity: 0.5; }
        }

        /* #pawby-menu is the LAST section inside depth-shell 1, right before
           #pawby-moments — which plays its own one-shot fade-up intro the
           moment it scrolls into view. On mobile that intro was landing at
           almost the same scroll position where pkDepth's own EXIT phase
           (shrink to scale 0.97 / fade to opacity 0.5) was still playing on
           the outgoing Pawby Menu section, so two independent animations —
           one continuous/scroll-linked, one one-shot/triggered — overlapped
           right at the seam and read as a jolt. Turning the whole .depth
           effect off for Pawby Menu on mobile (both its entrance settle
           coming from #ingredients above, and its exit toward Moments) makes
           it render statically at rest the entire time it's on screen, so
           the handoff into Pawby Moments is driven by exactly one animation
           (Moments' own fade-up), not two colliding. */
        .pawby-menu.depth {
          animation: none;
          transform: none;
          opacity: 1;
        }
      }

      /* ── #ingredients gets its own, more cinematic version ────────────────
         Same shared mechanism as every other .depth section above (one
         animation-timeline: view() rule, 0 KB JS) — but non-uniform
         scaleX/scaleY instead of a plain uniform scale: it starts as a
         narrow vertical sliver (scaleX .18 — much smaller than scaleY .55)
         and WIDENS out to full, rather than growing evenly in both
         directions. Reads as the section "unfolding/opening up" rather
         than a generic zoom — fitting for a section whose own content is a
         horizontally-scrolling ingredient strip. Border-radius stays
         proportionally large while it's that narrow (a pill/capsule shape)
         and flattens to 0 as it reaches full width. Overriding only
         `animation-name` (via the #id selector's higher specificity) keeps
         everything else — timeline, range, reduced-motion / no-@supports
         fallback to the plain static section — inherited from the shared
         .depth-shell > .depth rule above; nothing duplicated. */
      @media (prefers-reduced-motion: no-preference) {
        @supports (animation-timeline: view()) {
          #ingredients.depth {
            animation-name: pkDepthIngredients;
            overflow: hidden;
          }
        }
      }
      @keyframes pkDepthIngredients {
        0%   { transform: translateZ(-260px) scale3d(0.18, 0.55, 1); opacity: 0.12; filter: blur(7px); border-radius: 60px; animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
        16%  { transform: translateZ(-40px)  scale3d(0.7, 0.94, 1);  opacity: 0.9;  filter: blur(0);   border-radius: 18px; animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1); }
        38%  { transform: translateZ(0)      scale3d(1, 1, 1);      opacity: 1;    filter: blur(0);   border-radius: 0;    animation-timing-function: linear; }
        64%  { transform: translateZ(0)      scale3d(1, 1, 1);      opacity: 1;    filter: blur(0);   border-radius: 0;    animation-timing-function: cubic-bezier(0.32, 0, 0.67, 0); }
        84%  { transform: translateZ(-40px)  scale3d(0.7, 0.94, 1);  opacity: 0.9;  filter: blur(0);   border-radius: 18px; animation-timing-function: cubic-bezier(0.64, 0, 0.78, 0); }
        100% { transform: translateZ(-260px) scale3d(0.18, 0.55, 1); opacity: 0.12; filter: blur(7px); border-radius: 60px; }
      }
      /* Mobile: same "unfold" idea, gentler (matches the shared pkDepth
         mobile override right above — lower intensity, no blur). Also drops
         the border-radius animation the desktop version has: unlike
         transform/opacity, border-radius isn't a compositor-only property —
         animating it forces a repaint every scroll-timeline frame, which
         adds up on a section whose own content is a horizontally-scrolling
         image strip. Mobile keeps the "unfold" scale/opacity motion, just
         without that extra repaint cost. */
      @media (max-width: 768px) {
        @keyframes pkDepthIngredients {
          0%   { transform: translateZ(-90px) scale3d(0.5, 0.8, 1); opacity: 0.35; }
          26%  { transform: translateZ(0) scale3d(1, 1, 1);          opacity: 1; }
          74%  { transform: translateZ(0) scale3d(1, 1, 1);          opacity: 1; }
          100% { transform: translateZ(-90px) scale3d(0.5, 0.8, 1); opacity: 0.35; }
        }
      }

      /* ============================================================
         PAWBY MOMENTS — 4 colour cards (full-bleed photo) scrolled sideways.
         Opsi A+C: a CSS scroll-timeline (--pmTrack) pins the 340vh section on
         a wide + tall viewport and slides the track left as you scroll down,
         so all 4 cards pass before the page reaches Pawby Tools. Everywhere
         the timeline isn't available (Safari today, <=900px, short viewport,
         reduced-motion) the SAME markup is a free horizontal swipe strip
         (overflow-x, no scroll-snap) with no pin — the baseline below; the
         pinned layer only overrides it.

         src/scripts/pawby-moments.js: sets --pm-shift (pinned keyframe),
         runs the left step <button>s (progress-driven .is-active +
         aria-current, click to jump), and expands the active card's
         description (.pm-card--current). One passive scroll listener.

         Colours reuse the menu-category palette (:root in style.css) — no
         new tokens; each card's photo backdrop is already that colour.
         Cursor paw trail: initPawTrail() in site-animations.js (host
         #pawby-moments, z:0 → behind .pm-inner z:1, so prints only show in
         the left gutter + section padding). ==================================== */
      #pawby-moments {
        background-color: var(--color-background);
        background-image: none;
        padding: 96px 0 108px;
      }

      #pawby-moments .pm-inner {
        position: relative;
        z-index: 1;
        max-width: 1240px;
        margin: 0 auto;
        padding: 0 24px;
        display: grid;
        grid-template-columns: 300px 1fr;
        gap: 56px;
        align-items: start;
      }

      /* ── Left column: intro text + vertical progress list ── */
      #pawby-moments .pm-aside { min-width: 0; }
      #pawby-moments .pm-intro { margin-bottom: 30px; }
      #pawby-moments .pm-title {
        font-family: var(--font-heading);
        color: var(--color-heading);
        font-size: clamp(28px, 3.4vw, 40px);
        font-weight: 900;
        line-height: 1.12;
        margin: 14px 0 14px;
      }
      #pawby-moments .pm-subtitle {
        color: var(--color-text-light);
        font-size: 15px;
        line-height: 1.6;
        margin: 0;
        max-width: 34ch;
      }
      #pawby-moments .pm-progress {
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
      /* <button> — clickable jump to a step (src/scripts/pawby-moments.js).
         .is-active + aria-current are set by the same progress-driven logic
         that runs on manual scroll, so a click and a scroll look identical. */
      #pawby-moments .pm-progress-item {
        align-self: flex-start;
        appearance: none;
        border: 0;
        background-color: transparent;
        cursor: pointer;
        text-align: left;
        font-family: var(--font-heading);
        font-size: 11px;
        font-weight: 800;
        letter-spacing: 1.4px;
        text-transform: uppercase;
        color: var(--color-text-light);
        opacity: 0.45;
        padding: 7px 12px;
        border-radius: 7px;
        transition: opacity 0.3s ease, color 0.3s ease, background-color 0.3s ease;
      }
      #pawby-moments .pm-progress-item:hover { opacity: 0.85; }
      #pawby-moments .pm-progress-item:focus-visible {
        outline: 2px solid var(--color-heading);
        outline-offset: 2px;
        opacity: 1;
      }
      #pawby-moments .pm-progress-item.is-active {
        opacity: 1;
        color: #fff;
        background-color: var(--pm-accent, var(--color-heading));
      }
      #pawby-moments .pm-progress-item[data-step="1"] { --pm-accent: var(--color-cat-mealplans); }
      #pawby-moments .pm-progress-item[data-step="2"] { --pm-accent: var(--color-cat-treats); }
      #pawby-moments .pm-progress-item[data-step="3"] { --pm-accent: var(--color-cat-signature); }
      #pawby-moments .pm-progress-item[data-step="4"] { --pm-accent: var(--color-cat-special); }

      /* ── Right column: the card track ──
         BASELINE = native horizontal swipe strip. The pinned layer further
         down turns off the overflow + snap and drives translateX instead. */
      #pawby-moments .pm-viewport { position: relative; min-width: 0; }
      #pawby-moments .pm-track {
        list-style: none;
        margin: 0;
        padding: 6px 2px;
        display: flex;
        gap: 28px;
        /* Plain horizontal scroll, same recipe as the site's other card
           rails (.mp-cards-wrap / .pf-scroll): overflow-x + momentum, and
           nothing else. NO `touch-action` (pan-x would forbid a vertical
           page scroll for any touch starting on a card) and NO
           `data-lenis-prevent` (its `overscroll-behavior: contain` trapped
           vertical swipes; and the step <button>s already cover desktop
           navigation, like .pf-arrow elsewhere). `overscroll-behavior-x`
           only, so a vertical swipe still chains to the page. No scroll-snap
           — cards glide past at any speed and rest wherever the swipe ends. */
        overflow-x: auto;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
      }
      #pawby-moments .pm-track::-webkit-scrollbar { display: none; }

      #pawby-moments .pm-card {
        position: relative;
        flex: 0 0 clamp(232px, 66vw, 292px);
        aspect-ratio: 4 / 5;
        border-radius: 22px;
        overflow: hidden;
        display: flex;
        flex-direction: column;
        color: #fff;
        box-shadow: 0 14px 32px rgba(21, 36, 60, 0.16);
      }
      #pawby-moments .pm-card--green  { background: var(--color-cat-mealplans); }
      #pawby-moments .pm-card--plum   { background: var(--color-cat-treats); }
      #pawby-moments .pm-card--blue   { background: var(--color-cat-signature); }
      #pawby-moments .pm-card--orange { background: var(--color-cat-special); }

      #pawby-moments .pm-card-num {
        position: absolute;
        top: 16px;
        left: 16px;
        z-index: 2;
        background: #fff;
        color: var(--color-heading);
        font-family: var(--font-heading);
        font-weight: 900;
        font-size: 15px;
        line-height: 1;
        padding: 6px 10px;
        border-radius: 9px;
      }
      /* ── Card photo — full-bleed (PM_STAGES[].img) ──
         Runs edge to edge behind the number badge (z-index:2) and the text
         band (z-index:2, its own scrim). The number badge, title + the
         expanding lead/desc all sit above it. The photo's backdrop is already
         the card colour, so any object-fit crop at the edges is invisible. */
      #pawby-moments .pm-card-art {
        position: absolute;
        inset: 0;
        pointer-events: none;
        overflow: hidden;
      }
      #pawby-moments .pm-card-art img { width: 100%; height: 100%; object-fit: cover; display: block; }

      /* Text band scrim — sits over the photo bottom. A top-to-bottom darken
         (never fully transparent) so white text clears contrast on any of the
         four photos, however tall the band expands. */
      #pawby-moments .pm-card-text {
        margin-top: auto;
        position: relative;
        z-index: 2;
        background: linear-gradient(to bottom, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.6));
        padding: 14px 16px 16px;
      }
      #pawby-moments .pm-card-title {
        font-family: var(--font-heading);
        font-size: 18px;
        font-weight: 800;
        line-height: 1.2;
        margin: 0;
      }
      /* Description block — collapsed to 0 height while JS is on (#pawby-moments
         gets .pm-js), expands to the exact content height on the active card
         via grid-template-rows 0fr→1fr. No JS / reduced motion: stays open, so
         every card shows its full text. Toggled by .pm-card--current, set by
         the same progress-driven code that lights the step buttons. */
      #pawby-moments .pm-card-expand { display: grid; grid-template-rows: 1fr; }
      #pawby-moments.pm-js .pm-card-expand {
        grid-template-rows: 0fr;
        transition: grid-template-rows 0.45s cubic-bezier(0.16, 1, 0.3, 1);
      }
      #pawby-moments.pm-js .pm-card--current .pm-card-expand { grid-template-rows: 1fr; }
      #pawby-moments .pm-card-expand-in {
        overflow: hidden;
        min-height: 0;
        padding-top: 7px;               /* title↔lead gap, clipped when collapsed */
      }
      #pawby-moments.pm-js .pm-card-expand-in { opacity: 0; transition: opacity 0.3s ease; }
      #pawby-moments.pm-js .pm-card--current .pm-card-expand-in {
        opacity: 1;
        transition: opacity 0.4s ease 0.1s;
      }
      #pawby-moments .pm-card-lead {
        font-weight: 700;
        font-size: 13px;
        margin: 0 0 3px;
      }
      #pawby-moments .pm-card-desc {
        font-size: 12.5px;
        line-height: 1.55;
        margin: 0;
        color: rgba(255, 255, 255, 0.86);
      }

      /* ── Mobile / small tablet: single column, progress list hidden ── */
      @media (max-width: 900px) {
        #pawby-moments .pm-inner { grid-template-columns: 1fr; gap: 26px; }
        #pawby-moments .pm-intro { text-align: center; margin-bottom: 4px; }
        #pawby-moments .pm-subtitle { max-width: none; margin-inline: auto; }
        #pawby-moments .pm-progress { display: none; }
        #pawby-moments .pm-track {
          gap: 24px;
          padding-left: 20px;
          padding-right: 20px;
        }
        #pawby-moments .pm-card { flex-basis: min(74vw, 274px); }
      }

      /* ── Desktop pinned horizontal scroll-timeline (Opsi A) ──
         Gated on a wide + tall viewport (a pinned box on a short screen would
         clip the card with no way out) and on animation-timeline support.
         Everything the baseline set for the swipe strip is reverted here. */
      @media (prefers-reduced-motion: no-preference) and (min-width: 901px) and (min-height: 700px) {
        @supports (animation-timeline: view()) {
          #pawby-moments {
            view-timeline-name: --pmTrack;
            view-timeline-axis: block;
            /* explicit height (not min-height) so the box is exactly this tall
               regardless of content — the pin range (this − 100vh) is the
               scroll distance for all 4 cards. 340vh (was 280) gives ~80vh
               per card: more room to read each one at a normal scroll pace. */
            height: 340vh;
            overflow: clip; /* clip, not hidden: no scroll container / BFC, so
                               the sticky children still pin to the viewport */
            padding-top: 0;
            padding-bottom: 0;
          }
          #pawby-moments .pm-inner {
            height: 340vh;
            grid-template-rows: 100%;
          }
          #pawby-moments .pm-aside {
            position: sticky;
            top: 80px;
            height: calc(100vh - 80px);
            display: flex;
            flex-direction: column;
            justify-content: center;
          }
          #pawby-moments .pm-viewport {
            position: sticky;
            top: 80px;
            height: calc(100vh - 80px);
            display: flex;
            align-items: center;
            overflow: clip;
          }
          #pawby-moments .pm-track {
            /* pinned: NOT a scroll container — page scroll + the timeline
               drive translateX. Wheel over the cards is handled by Lenis
               (page scroll → drives the pin), same as the rest of the page. */
            overflow: visible;
            width: max-content;
            gap: 40px;
            padding: 0;
            animation: pmTrackSlide linear both;
            /* !important on its own line: stop lightningcss folding
               animation-timeline into the `animation` shorthand (→ invalid
               `animation: … --pmTrack`, whole declaration dropped, track
               never slides — the same class of prod-only break noted for
               pkDepth / the old pmStageSwap). */
            animation-timeline: --pmTrack !important;
            animation-range: contain 0% contain 100%;
          }
          #pawby-moments .pm-card {
            flex: 0 0 min(30vw, 344px);
          }
          @keyframes pmTrackSlide {
            from { transform: translateX(0); }
            /* --pm-shift = -(trackWidth - viewportWidth), set exactly by
               pawby-moments.js on resize. The calc() is a rough fallback if
               that script fails to run. */
            to   { transform: translateX(var(--pm-shift, calc(56vw - 100%))); }
          }
        }
      }

/* ===== Cart list scroll hardening ===== */
    /* Cart list scroll hardening. The flex column in style.css is already
       correct (header/footer flex-shrink:0, body flex:1 overflow-y:auto);
       min-height:0 is the belt-and-suspenders companion so the body can
       shrink below its content in every engine, and overscroll-behavior
       keeps a bounce at the list ends from reaching the page. The real fix
       for "can't scroll" is data-lenis-prevent on .cart-sidebar above. */
    .cart-sidebar-body {
      min-height: 0;
      overscroll-behavior: contain;
    }

/* ===== Pawby AI CS Widget bubble (disabled) ===== */
    #pk-ph-bubble{position:fixed;bottom:0;right:max(24px,calc((100vw - 1400px)/2 + 24px));width:160px;cursor:pointer;z-index:99999;display:flex;flex-direction:column;align-items:center;animation:pkSitIn .8s cubic-bezier(.34,1.56,.64,1) .8s both;transition:opacity .3s ease,transform .3s cubic-bezier(.34,1.56,.64,1)}
    #pk-ph-bubble.pk-loading{opacity:.6;pointer-events:none}
    #pk-ph-bubble:hover #pk-ph-img{transform:translateY(-8px) scale(1.04)}
    #pk-ph-img{width:160px;height:auto;display:block;transition:transform .35s cubic-bezier(.34,1.56,.64,1);animation:pkFloat 4s ease-in-out 1.5s infinite;filter:drop-shadow(0 8px 20px rgba(21,36,60,.2))}
    #pk-ph-label{position:relative;background:#fff;color:var(--color-text);font-size:12px;font-weight:700;font-family:'Nunito','Inter',sans-serif;padding:7px 14px;border-radius:20px;border:1.5px solid rgba(45,155,138,.25);white-space:nowrap;box-shadow:0 4px 16px rgba(21,36,60,.12);margin-bottom:6px;letter-spacing:.2px;animation:pkSpeechPop .5s cubic-bezier(.34,1.56,.64,1) 1.5s both}
    #pk-ph-label::after{content:'';position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);border:8px solid transparent;border-top-color:#fff}
    #pk-ph-label::before{content:'';position:absolute;bottom:-10px;left:50%;transform:translateX(-50%);border:9px solid transparent;border-top-color:rgba(45,155,138,.25)}
    #pk-ph-badge{position:fixed;bottom:175px;right:max(80px,calc((100vw - 1400px)/2 + 80px));width:20px;height:20px;background:#ef4444;color:#fff;border-radius:50%;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;z-index:99999;font-family:sans-serif;animation:pkBadgePop .4s cubic-bezier(.34,1.56,.64,1) 3.5s both}
    @keyframes pkSitIn{from{transform:translateY(120%);opacity:0}to{transform:none;opacity:1}}
    @keyframes pkFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
    @keyframes pkSpeechPop{from{transform:scale(.7);opacity:0}to{transform:scale(1);opacity:1}}
    @keyframes pkBadgePop{from{transform:scale(0)}to{transform:scale(1)}}
    /* Widget disabled — remove this rule to re-enable the Toby chat bubble */
    #pk-ph-bubble, #pk-ph-badge { display: none !important; }
