/*
 * A panel along the bottom, not a wall across the screen.
 *
 * The mascot is the thing being looked at here; the question is the caption
 * under him. So the card is pinned low at every width -- this used to be a
 * phone-only arrangement, with desktop giving him a column beside a giant
 * BOOM XTRACTS lockup, and having two stagings meant gateDock() had to measure
 * which one was up before it could stand him anywhere.
 *
 * What is above the card is his, and mascot-stage.mjs measures exactly that:
 * `.age-card`'s top edge is the floor of his band. Anything added here that
 * eats into that space takes it off him.
 */
.age-gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: end center;
  padding: 16px 16px clamp(18px, 4vh, 40px);
  background:
    radial-gradient(circle at 50% 34%, rgba(135, 126, 188,.27), transparent 34%),
    #08070a;
}
.age-gate.hidden { display: none; }

/*
 * Entry used to be a hard cut into the hero. Dissolve the gate instead, timed
 * to the smoke burst site-interactions.js fires on unlock.
 *
 * That dissolve is now the fallback rather than the usual exit: when there is a
 * mascot he blows the gate away himself, with `.is-wall` / `.is-blown` over in
 * mascot-stage.css. This still runs whenever nobody claims the exit -- no
 * WebGL, reduced motion, a model that never arrived.
 */
.age-gate {
  transition: opacity 620ms ease, filter 620ms ease, transform 620ms ease;
}
.age-gate.leaving {
  opacity: 0;
  filter: blur(14px);
  transform: scale(1.06);
  pointer-events: none;
}

/* The wall he blows through lives in mascot-stage.css -- it is his, and it has
   to outrank the floor-stripping rule there to take the background back. */

.age-shell {
  width: min(92vw, 34rem);
}
.age-card {
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: clamp(20px, 3.2vw, 34px);
  background: rgba(18,16,22,.94);
  box-shadow: 0 30px 100px rgba(0,0,0,.35);
}
.age-card h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2rem, 5.6vw, 2.9rem);
  line-height: .95;
  text-transform: uppercase;
  font-weight: 400;
}
.age-intro { margin: .55rem 0 0; color: #aaa4ae; font-size: .92rem; line-height: 1.5; }
/* The date boxes used to hold the gap open under the intro copy. With one
   button standing where they were, the gap has to come from the button. */
#ageForm .btn { margin-top: 18px; }

/*
 * Held, not broken.
 *
 * mascot-stage.mjs holds this button for about a second while he walks on, so
 * that the one thing the page is built around is not missed by everyone who
 * answers immediately. It uses aria-disabled rather than the `disabled`
 * attribute -- the button carries `autofocus`, and disabling a focused element
 * throws focus to the body -- which means there is no :disabled for the browser
 * to style and the state has to be drawn here or the hold looks like a dead
 * button. The No link is never held; see mascot-stage.mjs.
 */
.btn[aria-disabled="true"] { opacity: .45; cursor: default; }
.btn[aria-disabled="true"]:hover { transform: none; background: var(--brand-deep); }
.form-error {
  min-height: 20px;
  margin: 10px 0;
  color: #e7a8b0;
  font-size: .72rem;
}
.leave-link {
  display: block;
  margin-top: 16px;
  text-align: center;
  color: #8d8791;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .12em;
}
.legal-small {
  margin: 16px 0 0;
  color: #8d8791;
  font-size: .66rem;
  line-height: 1.5;
}

.reveal {
  opacity: 0;
  transform: translateY(24px) perspective(1100px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  transition: opacity .75s ease, transform .75s ease;
}
.reveal.visible {
  opacity: 1;
  transform: translateY(0) perspective(1100px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
}
.tilt-target.visible { transition: opacity .75s ease, transform 160ms ease-out, border-color 180ms ease; }
.tilt-target.tilting { transition: opacity .75s ease, transform 60ms linear, border-color 180ms ease; }

@media (max-width: 1000px) {
  .site-header { grid-template-columns: 1fr auto auto; }
  /*
   * Below 1000px the word goes and the icon takes over. The box is held at
   * 44x44 because that is a fingertip: the wishlist control measured 35x37
   * here, which is under the floor on both axes and sits next to another
   * control of nearly the same size.
   */
  .header-cart {
    padding: 10px 12px;
    min-width: 44px;
    min-height: 44px;
    justify-content: center;
  }
  .header-cart-word { display: none; }
  .header-cart .cart-icon { display: block; }
  .menu-toggle { display: inline-flex; min-height: 44px; align-items: center; }
  .site-header nav {
    position: absolute;
    top: 78px;
    left: 0;
    right: 0;
    display: none;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 14px 20px 22px;
    background: #0d0c0f;
    border-bottom: 1px solid var(--line);
  }
  .site-header nav.open { display: flex; }
  .site-header nav a { width: 100%; padding: 15px 0; border-bottom: 1px solid var(--line); }
  .hero-grid { grid-template-columns: 1fr; padding-top: 80px; }
  .hero-art { min-height: 560px; }
  .hero-logo { max-height: 600px; }
  .section-heading,
  .find-panel,
  .faq { grid-template-columns: 1fr; }
  .section-copy { justify-self: start; }
  .sticker-story {
    min-height: 0;
    grid-template-columns: 1fr;
  }
  .sticker-story-copy {
    max-width: 680px;
    padding-bottom: 24px;
  }
  .sticker-stage {
    min-height: 680px;
    margin-top: -170px;
    margin-left: 24%;
  }
  .footer-mascot { width: min(48vw, 520px); }
  .faq { gap: 35px; }
}

/*
 * Below 560px every control in the header is an icon, and there are five of
 * them: mascot, Menu, search, wishlist, cart. Found on an iPhone 2026-09-05
 * with the cart chip hanging off the right edge -- the chips had been grown to
 * a 44px fingertip (8/22), search had been prepended into the same row
 * (8/24), and the 32px grid gap above never shrank, so the row needed 400px
 * in a 390px screen's 350px lane. The predeploy gate reads scrollWidth minus
 * clientWidth, and html clips horizontally, so it measured 0 while a control
 * was out of reach. test/header-fit.test.mjs now does the sum.
 *
 * The chip becomes exactly the fingertip and the count moves to its corner,
 * where a shopper expects it on a bag icon; side by side the two cost 74px
 * each. 58 + 14 + 62 + 14 + 44 + 8 + 44 + 8 + 44 = 296px, which fits a
 * 360px phone's 320px lane with room and a 390px one with 54px over.
 */
@media (max-width: 560px) {
  .site-header { gap: 14px; }
  .header-actions { gap: 8px; }
  .header-cart {
    position: relative;
    width: 44px;
    padding: 0;
    gap: 0;
  }
  .header-cart [data-cart-count],
  .header-cart [data-wishlist-count] {
    position: absolute;
    top: -7px;
    right: -7px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    font-size: .6rem;
    box-shadow: 0 0 0 2px rgba(8, 7, 10, .95);
  }
}
/* The 320px phones (first-generation SE) have a 280px lane at 20px padding,
   4px short of the row above; the cart's corner badge was the part that got
   cut. Padding and gaps give up the difference. */
@media (max-width: 360px) {
  .site-header { gap: 8px; padding: 0 14px; }
}

@media (max-width: 720px) {
  /* The headline now shares the screen with the logo, so it cannot run at 23vw. */
  .hero h1 { font-size: clamp(2.9rem, 16vw, 4.6rem); }
  .hero-lead { margin-top: 16px; font-size: 1rem; line-height: 1.55; }
  .hero-actions { margin-top: 22px; gap: 10px; flex-wrap: nowrap; }
  .hero-actions .btn {
    flex: 1 1 0;
    min-width: 0;
    min-height: 46px;
    padding: 0 8px;
    font-size: .64rem;
    letter-spacing: .06em;
    white-space: nowrap;
  }
  .hero-headline .kicker { margin-bottom: 12px; letter-spacing: .16em; }
  .hero-notes { margin-top: 20px; gap: 8px 18px; font-size: .62rem; }
  .hero-grid { padding-bottom: 90px; }
  .hero-art { min-height: 470px; }
  .hero-logo { width: 100%; max-height: 500px; }
  /* Was a flat 440px, set when the art was a short row and the wheel only ever
     showed as a band of rays. It has the full screen now, so scale it. */
  .spin-wheel { width: min(500px, 106vw); }
  .scroll-cue { display: none; }
  .section-heading { gap: 26px; }
  .product-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 7px;
  }
  .category-panel { min-height: 165px; }
  .category-panel.category-feature { min-height: 255px; }
  .category-panel.category-wide { min-height: 205px; }
  .category-copy { left: 12px; right: 10px; bottom: 12px; }
  .category-copy small { margin-bottom: 5px; font-size: .5rem; }
  .category-copy strong { font-size: clamp(1.8rem, 9.5vw, 3rem); }
  .category-feature .category-copy strong { font-size: clamp(3.4rem, 18vw, 5.2rem); }
  .category-concentrates .category-copy strong { font-size: clamp(1.65rem, 8.5vw, 2.7rem); }
  .category-arrow { top: 9px; right: 9px; width: 30px; height: 30px; font-size: .8rem; }
  /* The narrow-screen sizing for the seven mascot PNGs went with them. The one
     mascot who replaced them is sized from the grid's width in JS, not here. */
  .sticker-story-copy {
    padding: 76px 20px 20px;
  }
  .sticker-story-copy h2 { font-size: clamp(5rem, 24vw, 7rem); }
  .sticker-story-lede {
    max-width: 82%;
    margin-top: 28px;
    font-size: .98rem;
  }
  .sticker-stage {
    min-height: 620px;
    margin: -35px 0 0;
  }
  .sticker-stage > img {
    width: 79%;
    height: 100%;
    right: -5%;
    bottom: -18%;
  }
  .mascot-sticker {
    min-width: 56px;
    min-height: 56px;
    padding: 9px;
    border-width: 2px;
    box-shadow:
      0 0 0 3px #faf4e7,
      4px 5px 0 rgba(14,8,17,.38);
  }
  .mascot-sticker b { font-size: 1.15rem; }
  .mascot-sticker small { font-size: .42rem; }
  .sticker-eight { width: 68px; height: 68px; left: 3%; top: 29%; }
  .sticker-eight b { font-size: 2.5rem; }
  .sticker-fuse { right: 3%; top: 7%; }
  .sticker-boom { right: -3%; top: 31%; }
  .sticker-leaf { left: 4%; top: 50%; }
  .sticker-roll { left: 26%; top: 37%; }
  .sticker-torch { right: 11%; top: 46%; }
  .sticker-mass { left: 3%; bottom: 13%; }
  .sticker-rig { right: 2%; bottom: 18%; }
  .sticker-key { left: 28%; top: 8%; }
  .sticker-star {
    min-width: 44px;
    min-height: 44px;
    width: 44px;
    height: 44px;
    font-size: 1.45rem;
  }
  .sticker-star-one { left: 35%; bottom: 6%; }
  .sticker-star-two { right: 30%; top: 22%; }
  .sticker-xtracts { right: 24%; bottom: 3%; }
  .find-panel { gap: 38px; }
  .location-stack article { grid-template-columns: 1fr; }
  .location-stack strong { grid-row: auto; }
  .footer-sendoff { min-height: 690px; }
  .footer-sendoff-copy { padding: 74px 20px; }
  .footer-sendoff h2 { font-size: clamp(2.2rem, 9vw, 3.4rem); }
  .footer-mascot {
    width: min(78vw, 420px);
    height: 76%;
    right: -7%;
    bottom: -17%;
  }
  .footer-top-link { margin-top: 30px; }
  .footer-bottom { flex-direction: column; }
  .footer-utility nav { gap: 17px 25px; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
  .reveal { opacity: 1; transform: none; }
  /*
   * The blanket rule above crushes animation-duration but NOT animation-delay,
   * and site-interactions.js hands out `.visible` immediately when motion is
   * reduced -- so the sticker slam would start on schedule and every badge
   * would sit frozen at its 0% keyframe, invisible, for up to 1.25 seconds.
   * Asking for less motion has to mean the stickers are simply there.
   */
  .sticker-stage .mascot-sticker { animation: none !important; opacity: 1 !important; }
  .tilt-target { transform: none !important; will-change: auto; }
}

/*
 * Purple smoke blast layers.
 *
 * The clip carries a black background instead of an alpha channel, so `screen`
 * is what makes it composite: black drops out against the dark canvas and only
 * the purple survives. That keeps it to one h264 file rather than needing
 * VP9-alpha for Chrome plus HEVC-alpha for Safari.
 */
.smoke-fx {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  /* Kept off the compositor until it fires; a permanently promoted full-bleed
   * layer costs VRAM on mobile for something that plays once. */
  contain: paint;
}
.smoke-fx.smoke-fx-live {
  opacity: 1;
  will-change: opacity;
}
.smoke-fx video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/*
 * The scroll-back-to-top replay. Unlike the gate handoff this is atmosphere
 * rather than a transition, so it sits *under* the hero: z-index 1 puts it above
 * the section's gradient background but below .hero-grid (z-index 2) and the
 * scroll cue (z-index 9), leaving the headline and logo untouched.
 */
.smoke-fx-hero {
  z-index: 1;
  top: 0;
  left: 0;
  right: 0;
  /* Pinned to the first screenful. The hero runs far taller than the viewport
   * for its scroll-driven explosion, and stretching to that height would
   * over-zoom the 16:9 clip the same way the products layer did. */
  height: 100svh;
  bottom: auto;
}
.smoke-fx-hero.smoke-fx-live { opacity: .7; }

/*
 * Text protection behind the hero copy, and only while the blast is playing.
 *
 * The blast is a bright screen-blend layer, and the hero's smaller grey type
 * sits at a luminance the smoke sweeps straight through: measured, .hero-lead
 * goes from 11.7:1 at rest to 1.6:1 at the bloom peak, and .hero-notes to 2.0:1.
 * Lowering the layer opacity does not rescue it — the ratio bottoms out around
 * 1.1:1 as the background crosses the text's own luminance and only recovers
 * once the smoke is faint enough to be pointless. A pocket of canvas colour
 * behind this one column restores it while leaving the smoke fully visible
 * across the rest of the hero, which is where the bomb logo sits anyway.
 *
 * Horizontal gradient feathers toward the logo; the mask feathers top and
 * bottom so the pocket has no straight edges. Safe to combine here — unlike on
 * .smoke-fx, this element has no mix-blend-mode for the mask to collide with.
 */
/*
 * The pocket must sit between the canvas and the words. Without its own
 * stacking context the ::before's z-index -1 escapes behind the 3D canvas,
 * which is exactly where it is useless -- at 768 the copy overlaps the
 * starburst and the gate measured the kicker at 2.1:1 over the rays while
 * the scrim glowed behind the scenery. z-index 6 clears the mascot's canvas
 * (5); the pseudo then sizes to the copy box as the inset always meant to.
 */
.hero-copy {
  position: relative;
  z-index: 6;
}
.hero-copy::before {
  content: "";
  position: absolute;
  inset: -8% -20% -10% -22%;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to right,
    rgba(8, 7, 10, .92) 0%,
    rgba(8, 7, 10, .88) 56%,
    rgba(8, 7, 10, 0) 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 13%, #000 87%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 13%, #000 87%, transparent 100%);
  /*
   * Always on, faint, and stronger under smoke. It used to appear only with
   * the smoke pass, but the predeploy gate's time-axis check caught the
   * kicker and headline washing out against the starburst's bright rays at
   * their worst animation frame (2.5:1 where 3+ is owed) -- a single
   * screenshot never sees it, the gate samples the loop. The pocket under
   * the copy is the fix the check itself prescribes: scrim the text, not
   * the frame.
   */
  opacity: .8;
  transition: opacity 420ms ease;
}
.hero:has(.smoke-fx-live) .hero-copy::before { opacity: 1; }

/*
 * Below 1024 the copy sits over the starburst rather than beside it, and the
 * kicker runs the full width of the box -- so the horizontal fade that looks
 * right on desktop leaves its last letters on bare rays (2.3:1 at the gate's
 * worst frame, and not much better at its best). Hold the pocket almost to
 * the edge, do the fading in the 20% overhang past the text, and make it
 * near-solid: here the text can sit on the sign's WHITE letter faces and on
 * the mascot walking past, and 0.2 of a white face through the scrim still
 * only reads 2.4:1 against this lavender.
 */
@media (max-width: 1024px) {
  .hero-copy::before {
    background: linear-gradient(to right,
      rgba(8, 7, 10, .96) 0%,
      rgba(8, 7, 10, .94) 88%,
      rgba(8, 7, 10, 0) 100%);
  }
}

/*
 * Same failure, smaller victim: the scroll cue is .68rem grey sitting on
 * whatever the starburst is doing down there (1.8:1 at its worst frame). Its
 * own background makes its contrast a constant instead of a function of time
 * -- on the element, not a ::before, so nothing about stacking or how a
 * checker isolates the ground can lose it.
 */
.scroll-cue {
  padding: 0 14px;
  margin-left: -14px;
  border-radius: 999px;
  background: rgba(8, 7, 10, .82);
}
/* Left and right terminate on the viewport edges, so only the bottom needs
 * feathering — otherwise the layer ends on a straight line across the hero. */
.smoke-fx-hero video {
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 97%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 97%);
}

/* Anchored inside #products so it scrolls with the grid instead of sliding
 * against it. Sits behind the cards — the bloom washes over the section while
 * the cards come up through it. */
.smoke-fx-products {
  z-index: 0;
  /* The blast is horizontal; letting it bleed past the section edges stops the
   * wings terminating on a visible straight line. */
  left: -12%;
  /* Stops short of the .section-copy column on purpose. A bright screen-blend
   * layer behind that mid-grey body text collapses its contrast to ~1.1:1 at
   * the bloom peak, and no opacity fixes it — the text's own luminance sits
   * inside the range the smoke sweeps through. So the blast is kept to the
   * display-headline side, where huge white Anton type stays dominant. */
  right: 46%;
  top: 3%;
  /* Deliberately NOT stretched to the section's full height. The section runs
   * ~2300px tall, and `object-fit: cover` over a box that tall would scale a
   * 16:9 clip by ~4x — you'd be watching a zoomed corner of the blast rather
   * than the blast. Holding the source aspect keeps it near 1:1. */
  aspect-ratio: 16 / 9;
  height: auto;
  bottom: auto;
  opacity: 0;
}
.smoke-fx-products.smoke-fx-live { opacity: .8; }

/*
 * Feather, or the layer's own box clips the smoke into hard straight seams — a
 * vertical line down the right edge and a horizontal one across the top.
 *
 * Two things matter here. It goes on the video, not on .smoke-fx: stacking a
 * mask on top of `mix-blend-mode` + `contain: paint` on one element makes the
 * blend drop out entirely and the smoke vanishes. And the radii are sized to
 * reach transparent *inside* the box — 38% + 58% lands the right fade at 720 of
 * 750px, 55% - 54% lands the top fade at 4 of 422px. Radii that overshoot the
 * box never reach transparent, which is a silent no-op rather than a visible
 * error. Left stays hard (off-viewport at -12%) and so does bottom (covered by
 * the first product card).
 */
.smoke-fx-products video {
  -webkit-mask-image: radial-gradient(ellipse 58% 54% at 38% 55%, #000 45%, transparent 100%);
  mask-image: radial-gradient(ellipse 58% 54% at 38% 55%, #000 45%, transparent 100%);
}

/*
 * Below the two-column breakpoint the heading and its body copy stack, so there
 * is no longer a text-free side to put the blast on. The gate transition still
 * carries the effect; this one steps aside rather than sitting behind copy.
 */
@media (max-width: 900px) {
  .smoke-fx-products { display: none; }
}

.section.products { position: relative; }
.section.products > *:not(.smoke-fx) { position: relative; z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  .smoke-fx { display: none; }
}
