/* ============================================================================
 * PhotoPool public site — layer 1: the home canvas.
 *
 * Rebuilt from scratch 2026-07-26 after the previous marketing layer was removed.
 * Deliberately small: this file currently styles exactly one screen. Resist the
 * urge to grow a component library here before there are pages that need it.
 *
 * The composition: a dotted studio canvas, photographs floating behind and under
 * a single white card. Colour comes ONLY from the photography — the canvas, the
 * card, the type and the button are all neutral. That is what keeps a scatter
 * layout from reading as noise.
 *
 * Served via the DESIGN_FILE_RE allowlist in src/handlers/app.js — a new
 * stylesheet 404s silently until it is named there.
 * ========================================================================== */

@font-face {
  font-family: "Source Sans 3";
  src: url("/design/fonts/source-sans-3.woff2") format("woff2");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

/* This layout loads ONLY tokens.css + this file — no components.css — so the border-box
   reset has to live here. Without it .hm's min-height:100dvh sits on top of its own
   padding, making the canvas 1091px tall in a 963px viewport and scrolling a page that
   is meant to be exactly one screen. */
*, *::before, *::after { box-sizing: border-box; }

/* Dark studio. The canvas is near-black with a blue cast rather than neutral grey —
   pure #000 flattens photographs and makes every shadow invisible. Everything in the
   chrome is neutral so the photography is the only source of colour on the page. */
:root {
  --hm-canvas: #0a0b0f;
  --hm-card: #14171f;
  --hm-edge: rgba(255, 255, 255, .07);
  --hm-ink: #f3f3f7;
  --hm-muted: #9aa0b0;   /* 6.9:1 on the card — clears AA for body text */
  --hm-radius: 14px;
  --st-bar-h: 64px;
  /* The tap sequence's single clock. The pill, the reticle, the ripples, the shockwave and
     the page burst all run on THIS number and are staged by percentage inside it, so the
     choreography cannot drift apart. Changing it re-times the whole sequence at once. */
  --pb-seq: 1900ms;
}

/* .skip-link is defined in components.css, which this layout deliberately doesn't load.
   Without a local rule it renders as visible body text in the top-left of every page. */
.skip-link {
  position: fixed;
  left: var(--sp-4);
  top: var(--sp-4);
  z-index: 200;
  padding: var(--sp-2) var(--sp-4);
  background: #fff;
  color: var(--ink-950);
  border-radius: var(--r-sm);
  text-decoration: none;
  transform: translateY(-300%);
}
.skip-link:focus { transform: none; }

/* ---- top bar --------------------------------------------------------------
   The tapsnap.net bar: fixed, black, full-bleed, white lockup. Two items only —
   see templates/site-nav.html for why that constraint is load-bearing. */
.st-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  height: var(--st-bar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: 0 var(--sp-5);
}

/* Two backgrounds, cross-faded — NOT one background being recoloured.
   A gradient cannot be transitioned to a flat colour (they aren't interpolable), and
   animating `background` would repaint the bar every frame anyway. So the scrim and the
   solid bar are separate stacked pseudo-elements and only their OPACITY changes, which
   the compositor handles for free. Both are pointer-events:none so neither ever eats a
   click meant for the brand or the login link. */
.st-bar::before,
.st-bar::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  transition: opacity var(--t-room) var(--ease-standard);
}
/* over the hero: a soft fade so the chrome sits ON the photography, not in a band */
.st-bar::before {
  background: linear-gradient(to bottom,
              rgba(0, 0, 0, .78) 0%,
              rgba(0, 0, 0, .45) 45%,
              rgba(0, 0, 0, 0) 100%);
  opacity: 1;
}
/* past the hero: a real bar */
.st-bar::after {
  background: var(--hm-canvas);
  border-bottom: 1px solid var(--hm-edge);
  opacity: 0;
}
.st-bar.is-solid::before { opacity: 0; }
.st-bar.is-solid::after  { opacity: 1; }

/* The children have to out-stack both pseudo-elements. */
.st-bar-brand,
.st-bar-nav,
.st-bar-login { position: relative; z-index: 1; }
.st-bar-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  /* Explicit, even though the children set their own: without it the anchor resolves to
     the UA default #0000EE, so any bare text added here later renders as a raw link. */
  color: #fff;
  text-decoration: none;
}
.st-bar-brand img { height: 26px; width: auto; display: block; }
.st-bar-product {
  font-weight: 600;
  font-size: 15px;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  border-left: 1px solid rgba(255, 255, 255, .28);
  padding-left: var(--sp-3);
}
/* ---- the middle links ------------------------------------------------------
   The bar was deliberately two items — name left, one action right — precisely so
   there would be no drawer to build. Adding a front door in front of the narrative
   made that untenable: /photopool and the plans both need a way back to them from
   every page, and a page nothing links to is invisible to visitors AND to the site
   crawler's gate.

   The responsive plan, in place of a drawer: these two links are secondary
   navigation and they simply LEAVE below 640px. The brand (which is the way home)
   and Log in (the action) are always present, so no destination becomes
   unreachable on a phone — /photopool and #pricing are both linked from the body
   of the home page, which is where a phone visitor is coming from anyway. If a
   THIRD link ever needs to live here, that is the point at which this needs a real
   menu rather than another thing to hide. */
.st-bar-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;   /* pins the pair against Log in rather than centring them */
}
.st-bar-nav a,
.st-bar-login {
  color: #fff;
  text-decoration: none;
  font-size: 16px;
  white-space: nowrap;
  padding: var(--sp-2) var(--sp-2);
  transition: opacity var(--t-fast) var(--ease-standard);
}
.st-bar-nav a { opacity: .74; }
.st-bar-nav a:hover { opacity: 1; }
.st-bar-login:hover { opacity: .72; }
/* White ring, not the brand blue — a #2467e5 outline all but vanishes on near-black. */
.st-bar :focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

@media (max-width: 640px) {
  :root { --st-bar-h: 56px; }
  .st-bar { padding: 0 var(--sp-4); }
  .st-bar-brand img { height: 20px; }
  .st-bar-product { font-size: 13px; letter-spacing: .1em; padding-left: var(--sp-2); }
  .st-bar-login { font-size: 15px; }
  /* See .st-bar-nav: the secondary links leave rather than collapsing into a menu. */
  .st-bar-nav { display: none; }
}

/* ---- the canvas -----------------------------------------------------------
   One full-viewport stage. `overflow: hidden` is load-bearing: several photos are
   positioned past the edges so the scatter reads as a crop of something larger,
   and without it they would extend the page and produce a horizontal scrollbar. */
.hm {
  position: relative;
  /* Exactly one screen, so the next section begins precisely at the fold. */
  height: 100dvh;
  display: grid;
  place-items: center;
  /* Symmetric padding, NOT offset for the bar. The bar is a gradient scrim over this
     section rather than a solid band above it, so the hero is full-bleed and the wordmark
     centres in the viewport — reserving space for the chrome would push it visibly low. */
  padding: var(--sp-6) var(--sp-5);
  overflow: hidden;
  background-color: var(--hm-canvas);
}

/* ---- the faucet -----------------------------------------------------------
   Decorative: the field is aria-hidden in the markup, so none of this reaches the
   accessibility tree.

   Every photo runs the same four-beat lifecycle from a single point at the bottom
   centre of the canvas — the tap:

     BEAD     it rises just past the lip, small and squat
     TENSION  it swells and stretches upward, barely moving (this beat is the slowest
              in the cycle; the stillness is what makes the release land)
     RELEASE  it snaps free at full size and overshoots
     DRIFT    it coasts away along its own exit vector and fades

   The beats are shaped by per-keyframe `animation-timing-function`, which sets the
   easing for the segment that FOLLOWS each keyframe. That is the whole trick: one
   animation can accelerate, hold, snap and coast without any JS.

   Per-photo inputs: --w/--ar (size + crop), --sx (nozzle jitter), --ex/--ey/--exr
   (exit vector and spin), --dur/--delay (cycle length and phase). */
/* Stacking order across the whole page. .hm and .sec are position:relative with z-index
   auto, so they do NOT create stacking contexts — every one of these competes directly in
   the root context, including the fixed traveller. Hence one shared scale:
     0  photo field / section ambient glow
     1  the section's photo
     2  the travelling photo  (over the field, UNDER the wordmark it flies past)
     3  the hero copy
   100  the top bar */
.hm-field {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.hm-pic {
  --wv: calc(var(--w) * var(--hm-scale, 1));
  position: absolute;
  left: 50%;
  bottom: 0;
  width: var(--wv);
  margin: 0 0 0 calc(var(--wv) / -2);
  /* Bottom-centre origin is what makes the swell read as a drop forming AT the lip:
     scaling grows the photo upward out of the edge instead of outward from its middle,
     and the scaleY stretch necks it upward rather than pulling it through the floor.
     It also makes the keyframes legible — translateY is a straight % of the element's
     own height from the lip, so `40%` means "sixty percent of me is still below". */
  transform-origin: 50% 100%;
  animation: hm-tap var(--dur, 18s) var(--delay, 0s) linear infinite;
  will-change: transform;
}

/* The visual card is a separate element from the animated one so the shadow and the
   rounded crop aren't themselves being scaled and stretched during the tension beat —
   scaling a box-shadow smears it. */
.hm-pic-in {
  display: block;
  aspect-ratio: var(--ar, 4 / 3);
  border-radius: var(--hm-radius);
  overflow: hidden;
  background: var(--ink-800);
  /* On a near-black canvas a shadow alone can't define an edge, so lift comes from a
     hairline highlight (light from above) plus a deep shadow for separation. */
  box-shadow:
    inset 0 0 0 1px var(--hm-edge),
    0 2px 4px rgba(0, 0, 0, .5),
    0 18px 40px -10px rgba(0, 0, 0, .65);
}
.hm-pic img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* One continuous accelerating rise. Two rules make it smooth:

   1. EVERY SEGMENT IS LINEAR (the animation's own timing function; there are no
      per-keyframe overrides). An easing function applies to each keyframe-to-keyframe
      segment individually, NOT to the animation as a whole — so an eased multi-keyframe
      animation decelerates into every waypoint and accelerates out of it again. That
      repeated slow-fast-slow is read as stopping and starting. Acceleration is encoded
      in the SPACING of the waypoints instead: the keyframes are evenly spaced in time
      but progressively further apart in distance, so speed only ever increases.

      That spacing is also what controls how FULL the screen looks, and it is easy to get
      wrong. Dwell time in any band of the screen is inversely proportional to speed, so a
      strongly accelerating path crowds every photo into the slow zone near the lip and
      leaves the top of the screen empty. The steps below ramp up only through the
      emergence (24 -> 32 -> 42) and then hold roughly constant (52 -> 58 -> 60 -> 67),
      which spreads the photos evenly over the full height instead of pooling them.

   2. IDENTICAL TRANSFORM FUNCTION LISTS in every keyframe (translate3d, scale, scaleY,
      rotate — same order, always present). Matching lists interpolate component-by-
      component; mismatched ones silently fall back to matrix decomposition, which takes
      a different path between the same two points and can visibly wobble.

   The droplet character is carried entirely by SHAPE — the swell and the scaleY neck as
   it clears the lip — rather than by a pause. Nothing ever holds still.

   There is no opacity anywhere: photos are clipped by the canvas's overflow on the way
   in and are fully outside it on the way out, so they enter and leave by moving, never
   by fading. That is also why the exit vectors have to be big enough to genuinely clear
   the viewport — if one stops short it will visibly vanish when the loop resets. */
@keyframes hm-tap {
  /* EMERGENCE (0-26%) is measured in %, i.e. relative to the photo's OWN height, because
     "how much of me is still below the lip" is the thing being described. */
  0% {
    transform: translate3d(var(--sx, 0vw), 100%, 0)
               scale(.35) scaleY(.95) rotate(0deg);
  }
  10% {
    transform: translate3d(var(--sx, 0vw), 66%, 0)
               scale(.52) scaleY(1.08) rotate(calc(var(--exr, 8deg) * .01));
  }
  18% {
    transform: translate3d(var(--sx, 0vw), 30%, 0)
               scale(.68) scaleY(1.16) rotate(calc(var(--exr, 8deg) * .03));
  }
  /* FLIGHT (26-100%) switches to vh, i.e. relative to the VIEWPORT. This is the fix for a
     thin top of screen: in element-relative units a 190px photo and a 330px photo travel
     completely different real distances for the same %, and the small ones exited sideways
     before they ever climbed. In vh every photo crosses the same screen at the same rate,
     so the field stays evenly distributed top to bottom whatever its size. */
  26% {
    transform: translate3d(calc(var(--sx, 0vw) + var(--ex, 0vw) * .04), calc(-2% - 4vh), 0)
               scale(.84) scaleY(1.05) rotate(calc(var(--exr, 8deg) * .08));
  }
  40% {
    transform: translate3d(calc(var(--sx, 0vw) + var(--ex, 0vw) * .14), calc(-2% - 25vh + var(--ey, 0vh) * .10), 0)
               scale(.96) scaleY(1) rotate(calc(var(--exr, 8deg) * .18));
  }
  54% {
    transform: translate3d(calc(var(--sx, 0vw) + var(--ex, 0vw) * .28), calc(-2% - 47vh + var(--ey, 0vh) * .26), 0)
               scale(1) scaleY(1) rotate(calc(var(--exr, 8deg) * .32));
  }
  68% {
    transform: translate3d(calc(var(--sx, 0vw) + var(--ex, 0vw) * .45), calc(-2% - 69vh + var(--ey, 0vh) * .44), 0)
               scale(1) scaleY(1) rotate(calc(var(--exr, 8deg) * .48));
  }
  84% {
    transform: translate3d(calc(var(--sx, 0vw) + var(--ex, 0vw) * .68), calc(-2% - 94vh + var(--ey, 0vh) * .68), 0)
               scale(1) scaleY(1) rotate(calc(var(--exr, 8deg) * .70));
  }
  100% {
    transform: translate3d(calc(var(--sx, 0vw) + var(--ex, 0vw)), calc(-2% - 122vh + var(--ey, 0vh)), 0)
               scale(1) scaleY(1) rotate(var(--exr, 8deg));
  }
}

/* ---- the hero -------------------------------------------------------------
   No card. The wordmark sits directly on the stage at display scale, and the photo
   stream passes behind it. */
.hm-hero {
  position: relative;
  z-index: 3;
  text-align: center;
  pointer-events: none;   /* only the CTA takes clicks; see below */
}

/* Legibility scrim. White type over a stream of bright party photographs is the one
   real risk in this composition, and a card would defeat the point of removing it. This
   is a soft radial darkening centred on the type — invisible as a shape, but it keeps
   contrast when a lit dance-floor shot passes directly behind the wordmark. */
.hm-hero::before {
  content: "";
  position: absolute;
  /* Much larger than the type it protects, so the falloff has room to finish. */
  inset: -150% -70%;
  z-index: -1;
  pointer-events: none;
  /* THE LAST STOP MUST LAND WELL INSIDE THE BOX. A default `ellipse farthest-corner`
     gradient sizes its 100% to the box's CORNER, which means the left and right edges sit
     at only ~71% along the ramp (1/sqrt2). Any colour still opaque at 71% therefore gets
     sliced off flat by the element's own boundary — a vertical seam that is invisible on
     a still background and glaringly obvious the moment a photo slides across it.
     Reaching full transparency by 58% guarantees the scrim has already vanished before
     the box ends, in every direction. */
  background: radial-gradient(ellipse at center,
              rgba(10, 11, 15, .94) 0%,
              rgba(10, 11, 15, .82) 22%,
              rgba(10, 11, 15, .52) 38%,
              rgba(10, 11, 15, .20) 49%,
              rgba(10, 11, 15, 0) 58%);
}

.hm-mark {
  margin: 0;
  font-family: "Source Sans 3", system-ui, sans-serif;
  font-weight: 700;
  /* Display scale: the wordmark is the composition, not a label on it. */
  font-size: clamp(46px, 13vw, 200px);
  line-height: .9;
  /* The bigger the type the tighter the tracking has to be — at 200px the default
     spacing reads as gaps between letters rather than as a word. */
  letter-spacing: -.045em;
  color: var(--hm-ink);
}

.hm-tag {
  margin: var(--sp-4) 0 0;
  font-family: "Source Sans 3", system-ui, sans-serif;
  font-size: clamp(17px, 1.7vw, 22px);
  line-height: var(--lh-snug);
  letter-spacing: .01em;
  color: var(--hm-muted);
}

.hm-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The hero block is pointer-events:none so the photo stream behind it stays visually
     unobstructed and nothing invisible intercepts the cursor; the one interactive
     element opts back in. */
  pointer-events: auto;
  margin-top: clamp(28px, 3.5vw, 40px);
  padding: 15px var(--sp-6);
  /* Inverted for dark: the reference's near-black button would disappear into the card.
     White keeps the single-loud-action hierarchy and stays neutral, so the photographs
     remain the only colour on the page. */
  background: var(--hm-ink);
  color: var(--ink-950);
  text-decoration: none;
  border-radius: 10px;
  font-family: "Source Sans 3", system-ui, sans-serif;
  font-weight: 600;
  font-size: 17px;
  line-height: 1;
  transition: background-color var(--t-fast) var(--ease-standard),
              transform var(--t-fast) var(--ease-standard);
}
.hm-cta:hover { background: #ffffff; }
.hm-cta:active { transform: scale(.98); }
.hm-cta:focus-visible {
  outline: 2px solid var(--hm-ink);
  outline-offset: 3px;
}

/* Triggers the top bar's scrim -> solid swap. Positioned partway down the hero rather
   than at its foot so the bar finishes hardening before the next section arrives. */
.hm-sentinel {
  position: absolute;
  top: 60%;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

/* ---- section two: theater mode --------------------------------------------
   No panel. The "glass" is ambient light: a blown-up, heavily blurred copy of the
   photograph sitting directly behind it, so colour spills out past its edges and
   washes the dark page — the same trick as YouTube's ambient/theater mode.

   The two properties that make it read as light rather than as a second photo:
   the blur has to be large relative to the image (so no shape survives), and the
   glow must be ANCHORED TO THE PHOTO, not to the section. A section-wide bloom
   glows from the middle of the page regardless of where the image sits; this one
   is a child of the image's own box, so it always radiates from its edges. */
.sec {
  position: relative;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--sp-9) var(--sp-5);
  background: var(--hm-canvas);
  overflow: hidden;
}

.sec-inner {
  position: relative;
  z-index: 1;
  width: min(1140px, 100%);
  display: grid;
  grid-template-columns: 1.05fr .95fr;
  align-items: center;
  gap: clamp(32px, 5vw, 72px);
}

/* Holds the photo and its glow together, and carries the aspect so the glow scales
   with the image at every viewport. */
.sec-media {
  position: relative;
  aspect-ratio: 4 / 3;
}

.sec-ambient {
  position: absolute;
  /* Overscan. This is the glow's reach past the photo edges — and it must be generous,
     because `blur()` fades toward its own boundary, so a tight box produces a visible
     rectangular falloff instead of an open spill. */
  inset: -26%;
  z-index: 0;
  background-image: var(--sec-photo-src);
  background-size: cover;
  background-position: center;
  /* Tuned for a DARK, saturated source. brightness() above 1 is doing real work here:
     a night photograph blurred flat is mostly near-black, and without lifting it the glow
     never clears the page behind it. On a light, desaturated source the same settings
     produce a grey haze instead of colour — which is why the image choice matters more
     than the filter does. */
  filter: blur(64px) saturate(210%) brightness(1.35);
  opacity: .85;
  pointer-events: none;
  /* Fades the glow to nothing before the overscan box ends, so the spill dissolves into
     the page instead of stopping at an edge (the same failure the hero scrim had). */
  -webkit-mask-image: radial-gradient(ellipse at center, #000 34%, transparent 68%);
          mask-image: radial-gradient(ellipse at center, #000 34%, transparent 68%);
}

.sec-title {
  margin: 0;
  font-size: clamp(30px, 4.2vw, 52px);
  line-height: 1.04;
  letter-spacing: -.03em;
  color: var(--hm-ink);
}
.sec-body {
  margin: var(--sp-5) 0 0;
  font-size: clamp(16px, 1.4vw, 19px);
  line-height: var(--lh-body);
  color: var(--hm-muted);
  max-width: 46ch;
}

/* The resting image. This is what the section shows before anything has been snapped in,
   and it is also the whole no-JS / reduced-motion story: the section is complete without
   the stack ever running. Once the stack has a card it steps aside. */
.sec-photo {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: var(--hm-radius);
  overflow: hidden;
  box-shadow:
    inset 0 0 0 1px var(--hm-edge),
    0 18px 44px -12px rgba(0, 0, 0, .7);
}
.sec-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sec-media.has-stack .sec-photo { opacity: 0; }

/* Sits above the waveform and is centred on it — the ring reads as a halo around the
   type rather than as a panel behind it. The measure is held narrow so the copy stays
   inside the quiet middle of the ring instead of colliding with the bars. */
/* Pinned to the viewport with the wave, and revealed by the same curtain.
   The z-index has to be NEGATIVE — and that is the whole trick. Anything at 0 or above
   paints over the in-flow backgrounds of the hero and section two, so the copy would be
   legible straight through them from the top of the page. Negative values still stack
   among themselves, so -1 sits above the wave at -2 while both stay behind every
   section's background. */
.sec-wave-copy {
  position: fixed;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  z-index: -1;
  margin: 0 auto;
  max-width: 420px;
  text-align: center;
}
/* Quiets the mesh behind the type. The heading survives on its own — it is bold white —
   but --hm-muted body copy with lit strands crossing it is genuinely hard to read.
   Last stop at 60%, inside the ~70.7% where a farthest-corner gradient meets the box
   edge: the same seam that would otherwise be sliced flat, and with the layers sliding
   underneath it a hard edge would be unmissable. */
.sec-wave-copy::before {
  content: "";
  position: absolute;
  inset: -120% -85%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at center,
              rgba(10, 11, 15, .93) 0%,
              rgba(10, 11, 15, .80) 26%,
              rgba(10, 11, 15, .42) 44%,
              rgba(10, 11, 15, 0) 60%);
}
.sec-wave-copy .sec-body { margin-left: auto; margin-right: auto; }

/* ---- the reel -------------------------------------------------------------
   A slot-machine window: one message visible, the next rolling up into place as you
   scroll. Every item is exactly --slot-h tall so the reel offset is a plain multiple of
   the item height — no per-item measurement, and nothing drifts as copy lengths change. */
.slot {
  --slot-h: clamp(230px, 34vh, 310px);
  height: var(--slot-h);
  overflow: hidden;
  /* Softens the top and bottom edges so messages roll out of view rather than being
     guillotined by the window. */
  /* The clear band has to be wider than the TALLEST message, or the fade eats the last
     line of the longest one. At 290px this leaves ~209px clear against ~185px of content. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 14%, #000 86%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 14%, #000 86%, transparent 100%);
}
.slot-reel {
  will-change: transform;
}
.slot-item {
  height: var(--slot-h);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
}
.slot-item .sec-body { margin: 0; }

.sec-legal {
  margin: var(--sp-8) 0 0;
  font-size: var(--fs-200);
  color: var(--ink-500);
}
.sec-legal a { color: var(--hm-muted); text-decoration: none; }
.sec-legal a:hover { color: var(--hm-ink); text-decoration: underline; }
.sec-legal a:focus-visible { outline: 2px solid var(--hm-ink); outline-offset: 3px; }

@media (max-width: 860px) {
  /* Single column, photo on top — it is the thing that just flew in, and the glow needs
     room to spill sideways rather than being pinched into half a phone width. */
  .sec-inner { grid-template-columns: 1fr; gap: clamp(28px, 7vw, 44px); }
  .sec-ambient { inset: -20%; filter: blur(48px) saturate(210%) brightness(1.35); }
}

/* ---- section three: the flowing waveform ----------------------------------
   Three ribbon layers, each a seamlessly TILING svg scrolled sideways at its own
   speed. The whole effect rests on one property of the artwork: each tile is
   exactly periodic across its width, so `repeat-x` butts copies together with no
   seam and translating by exactly one tile width returns to an identical frame.
   That is what makes an infinite loop possible with a single `transform`.

   Why not animate the waveform itself: the svg is loaded through <img>, so nothing
   outside it can reach in — and CSS cannot tween polyline points in any case.
   Scrolling periodic artwork gives real travelling peaks for the price of a
   compositor-only transform, at any strand count.

   The three layers differ in speed, which is what stops it reading as one flat
   image panning: nearer/larger ribbons move faster, so the mesh appears to shear
   and recombine as they slide over each other. */
.wf {
  position: fixed;
  left: 0;
  right: 0;
  top: 50%;
  /* Sized for the LOUDEST state, not the resting one. The wave grows ~2.6x as the reel
     advances, and overflow:hidden here would shear the peaks off flat at full intensity. */
  height: min(820px, 92vh);
  transform: translateY(-50%);
  /* Behind every section but in front of the page background. The opaque sections above
     it act as the curtain: section three is transparent, so scrolling it into place
     uncovers the wave rather than moving it. -2 leaves room for the pinned copy at -1. */
  z-index: -2;
  pointer-events: none;
  overflow: hidden;
}

/* Transparent — this is what lets the fixed wave show through. Everything else on the
   page keeps an opaque background so the wave stays hidden until this section arrives.
   It is also over-tall on purpose: the pinned panel needs scroll distance to drive the
   reel through four messages, and this section IS that runway. Its own content is
   position:fixed, so the height is doing nothing but providing scroll. */
/* Transparent — this is what lets the fixed wave show through. Everything else on the
   page keeps an opaque background so the wave stays hidden until these sections arrive.
   They are also over-tall on purpose: each pins its content and uses the height purely as
   scroll runway.

   ORDER MATTERS HERE. This rule carries the reel's 340vh, and the three below override it
   with their own. When .sec--wave came LAST it won on source order and silently stretched
   every one of them to 340vh — which is why the page had grown to 15.6 viewports. */
.sec--wave {
  background: transparent;
  display: block;
  padding: 0;
  min-height: 340vh;
}
/* The zoom runway. At 190vh this was 90vh of scroll for the entire pull-back — a ~100x
   change in radius inside one screen of scrolling, which arrives far faster than you can
   read it. The reveal is the centrepiece of the sequence and it needs room to be watched.
   The extra height is pacing, not content. */
.sec--morph { min-height: 380vh; }
.sec--pulse { min-height: 210vh; }
/* ---- the notch ------------------------------------------------------------
   ONE snap point on the whole page, at the assembled bar. `proximity`, never `mandatory`:
   the sequence above it is scrubbed, and a mandatory container would fight every scroll
   through the reel and the pull-back. Proximity only engages when you come to rest near
   the point, which is exactly the behaviour wanted — it catches you at the payoff and is
   inert everywhere else.

   Snapping is dropped entirely under reduced motion: a viewport that moves on its own
   after you stop scrolling is precisely what that setting is asking not to happen. */
html { scroll-snap-type: y proximity; }
/* Off during the post-tap autoscroll — proximity snapping would otherwise drag the page
   back into the notch mid-flight. */
html.is-advancing { scroll-snap-type: none; }
.sec-notch {
  position: absolute;
  top: 120vh;                  /* 60% of this section's 200vh of scroll */
  left: 0;
  width: 1px;
  height: 1px;
  scroll-snap-align: start;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-snap-type: none; }
}

/* Longer than the others on purpose. At 180vh the runway between the bar seating and the
   layer releasing was ~60px of scroll — less than one wheel notch — so the tap fired and
   was wiped off the screen before its 940ms had played. The extra height is dwell time,
   not content. */
.sec--dock  { min-height: 300vh; }

/* ---- Party Pulse ----------------------------------------------------------
   The ring is the mark, so this layer only names it. Full-viewport and fixed like the
   wave, with the wordmark absolutely centred INSIDE the ring's opening and the proof
   points pinned near the bottom edge — a normally-centred block would push the wordmark
   off the ring's centre as soon as the facts were added below it.
   z-index -1 for the same reason as the reel copy: anything at 0 or above would be
   legible straight through the opaque sections higher up the page. */
.pulse-copy {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;              /* scroll drives this */
}
.pulse-centre {
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  text-align: center;
  padding: 0 var(--sp-5);
}
.pulse-mark {
  margin: 0;
  font-size: clamp(32px, 5vw, 68px);
  line-height: 1;
  letter-spacing: -.035em;
  color: var(--hm-ink);
}
.pulse-line {
  margin: var(--sp-3) 0 0;
  font-size: clamp(15px, 1.5vw, 19px);
  color: var(--hm-muted);
}
.pulse-facts {
  position: absolute;
  left: 0; right: 0;
  bottom: clamp(56px, 11vh, 104px);
  display: flex;
  justify-content: center;
  gap: clamp(20px, 4vw, 56px);
  flex-wrap: wrap;
  padding: 0 var(--sp-5);
}
/* The ring's lower arc runs straight through this row — measured at 94 lit pixels along
   the line just above it — and the middle column was unreadable. Quietens the strands
   behind the text without putting a visible panel on the page.
   Last stop at 62%, inside the ~70.7% where a farthest-corner gradient reaches the box
   edge; past that it would be sliced flat and the seam would crawl as the ring turns. */
.pulse-facts::before {
  content: "";
  position: absolute;
  inset: -34px -7vw;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at center,
              rgba(10, 11, 15, .96) 0%,
              rgba(10, 11, 15, .89) 30%,
              rgba(10, 11, 15, .48) 48%,
              rgba(10, 11, 15, 0) 62%);
}
.pulse-fact { position: relative; max-width: 200px; text-align: center; }
.pulse-fact b {
  display: block;
  font-size: clamp(16px, 1.6vw, 20px);
  color: var(--hm-ink);
}
/* --ink-500 is 3.0:1 on #0a0b0f — under AA for small text before the strands behind it
   are even considered. --hm-muted is 7.5:1 on the same background. */
.pulse-fact span {
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--fs-200);
  line-height: 1.45;
  color: var(--hm-muted);
}
.pulse-note {
  position: absolute;
  left: 0; right: 0;
  bottom: clamp(20px, 4vh, 36px);
  margin: 0;
  text-align: center;
  font-size: var(--fs-100);
  color: var(--hm-muted);
  opacity: .75;
}

@media (max-width: 700px) {
  /* Three columns become unreadable slivers on a phone; one wins. */
  .pulse-facts { flex-direction: column; gap: var(--sp-4); bottom: clamp(52px, 12vh, 96px); }
  .pulse-fact { max-width: 300px; }
  .pulse-fact span { display: none; }
}

/* ---- the Party Pulse bar --------------------------------------------------
   Designed around the RING rather than the ring being dropped into the shipped bar.
   The guest app's version is a filled #111521 card with a rainbow 1px border, a 850-
   weight tracked label and a hard grey collar around its circle — all of which fight a
   graphic made of 1px strands and gradient light. This keeps the component's structure
   (circle, eyebrow, title, meta, activity) and rebuilds its surface in the ring's terms:

     - no filled panel; a barely-there surface over the black, defined by a hairline and
       a top light-edge rather than a border colour
     - a pill, echoing the ring's roundness, instead of a 22px rounded rectangle
     - NO collar on the circle. The ring is the element; a 5px #222a39 band around it was
       heavier than the whole graphic inside it
     - type from the site's own scale in sentence case, one soft accent, no shouting
     - the only saturated colour is the ring's own gradient, spread behind the contents
       as drifting spotlights rather than applied to any piece of chrome
     - NO second animated indicator. A seven-bar equalizer next to the ring reported the
       same pulse the ring already reports, twice, in two visual languages. The space it
       held is now the message window, which says something the ring cannot.

   STACKING: this sits at -3, BEHIND the canvas at -2, which is what lets the ring draw
   into the circle. At -1 the bar's own surface would paint over the ring and the socket
   would read as empty. The pinned copy stays at -1, in front of both. */
.pb {
  --pb-s: 1.15;
  /* One number the ring socket, the message window and the padding all derive from, so
     the pill can only ever be as tall as its tallest part. */
  --pb-cell: calc(66px * var(--pb-s));
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);   /* dead centre of the viewport */
  z-index: -3;
  opacity: 0;                  /* scroll drives this */
  pointer-events: none;
  /* Fixed, not content-sized. The message window rolls between lines of very different
     lengths; a shrink-to-fit pill would resize under every one of them. */
  width: 620px;
  max-width: calc(100vw - 48px);
  display: flex;
  align-items: center;
  gap: calc(18px * var(--pb-s));
  padding: calc(12px * var(--pb-s));
  padding-right: calc(28px * var(--pb-s));
  border-radius: 999px;
  overflow: hidden;               /* the spotlights are clipped by the pill itself */
  /* DARK glass, not white-tinted glass. A .028 white film is invisible over black — which
     was fine while the page was black — but once the burst lights the page behind it, a
     white film reads as a bright panel and the ring and the two smaller type sizes lose
     their contrast to it. A dark tint behaves the same in both worlds: over black the
     hairline and the top light-edge still define the pill, and over colour it stays a
     dark pill with the light showing through rather than a washed-out one. */
  background: rgba(9, 10, 14, .74);
  border: 1px solid rgba(255, 255, 255, .10);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .09),
    0 30px 70px -30px rgba(0, 0, 0, .9);
  backdrop-filter: blur(20px) saturate(115%);
  -webkit-backdrop-filter: blur(20px) saturate(115%);
}

/* ---- the spotlights -------------------------------------------------------
   Coloured washes sweeping right behind the contents, in the ring's own three stops so
   the pill is lit by the same light the graphic is made of. Pure radial-gradients rather
   than blurred solids: the falloff is in the paint, so there is no `filter` on a
   permanently-animating element. Each ends fully transparent at 70% of its
   farthest-corner radius, which for a square box is ~99% of its own edge — so they are
   soft blobs, not discs with a rim.

   THE WRAP. A continuous one-way sweep has to loop without a seam, so the track is two
   IDENTICAL copies of the three lights laid end to end, 200% wide and offset left:-100%
   so copy two starts on screen and copy one is queued off the left edge. Translating the
   track right by 50% of itself (= exactly one pill width) puts copy one precisely where
   copy two began, so the animation restarts on a frame identical to the one it ended on.
   Any other travel distance would visibly jump.

   The two copies stay in lockstep because their per-light bob is keyed by matching
   nth-child pairs (1&4, 2&5, 3&6) with no delay between them — so the halves are always
   the same picture, whatever phase the bob is in, and the bob's period need not divide
   the sweep's. Only `transform` animates. */
.pb-lights {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
}
.pb-track {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -100%;
  width: 200%;
  will-change: transform;
  animation: pb-sweep 18s linear infinite;
}
@keyframes pb-sweep {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(50%, 0, 0); }
}
.pb-lights i {
  position: absolute;
  top: -90%;                    /* (280% - 100%) / 2 — vertically centred */
  height: 280%;
  aspect-ratio: 1;
  border-radius: 50%;
  will-change: transform;
}
/* Hot on purpose, and kept that way by request. I dimmed these to ~.18 on the argument
   that three overlapping washes at this strength stop reading as separate lights and
   become one purple-to-magenta gradient — the call was to put the heat back, so the
   lights are the pill's colour rather than a hint of it. If they ever need reining in,
   lower these three alphas together; the positions and periods are already spread.

   Positions are stated against the 200% track, so each is HALF its pill-relative figure,
   and copy two is the same three plus 50%. Pill-relative: 2%, 34%, 62%.
   The bob is vertical and scale only — the horizontal travel is the track's job, and a
   light that also drifted sideways would beat against the sweep. */
.pb-lights i:nth-child(1), .pb-lights i:nth-child(4) {
  background: radial-gradient(circle, rgba(47, 107, 255, .50), rgba(47, 107, 255, 0) 70%);
  animation: pb-bob-a 11s ease-in-out infinite alternate;
}
.pb-lights i:nth-child(2), .pb-lights i:nth-child(5) {
  background: radial-gradient(circle, rgba(214, 57, 95, .40), rgba(214, 57, 95, 0) 70%);
  animation: pb-bob-b 14s ease-in-out infinite alternate;
}
.pb-lights i:nth-child(3), .pb-lights i:nth-child(6) {
  background: radial-gradient(circle, rgba(122, 75, 216, .44), rgba(122, 75, 216, 0) 70%);
  animation: pb-bob-c 17s ease-in-out infinite alternate;
}
.pb-lights i:nth-child(1) { left: 1%; }
.pb-lights i:nth-child(2) { left: 17%; }
.pb-lights i:nth-child(3) { left: 31%; }
.pb-lights i:nth-child(4) { left: 51%; }
.pb-lights i:nth-child(5) { left: 67%; }
.pb-lights i:nth-child(6) { left: 81%; }
@keyframes pb-bob-a {
  from { transform: translate3d(0, -15%, 0) scale(1); }
  to   { transform: translate3d(0, 13%, 0) scale(1.22); }
}
@keyframes pb-bob-b {
  from { transform: translate3d(0, 14%, 0) scale(1.16); }
  to   { transform: translate3d(0, -12%, 0) scale(.9); }
}
@keyframes pb-bob-c {
  from { transform: translate3d(0, 17%, 0) scale(.95); }
  to   { transform: translate3d(0, -16%, 0) scale(1.3); }
}

/* The socket. Deliberately empty and unbordered — the canvas ring IS this element. */
.pb-ring {
  position: relative;
  z-index: 1;
  width: var(--pb-cell);
  height: var(--pb-cell);
  flex: none;
  border-radius: 50%;
  /* A faint bloom so the ring sits in light rather than on a flat surface — the same
     idea as the section-two ambient glow, at component scale. */
  background: radial-gradient(circle at center,
              rgba(122, 75, 216, .20) 0%,
              rgba(47, 107, 255, .10) 46%,
              transparent 68%);
}

/* ---- the message window ---------------------------------------------------
   The same slot machine as section three, at component scale: every card is exactly one
   --pb-cell tall, so the reel offset is a plain multiple of the cell and no card has to
   be measured. What rolls is the WHOLE card — label, headline and detail together — so a
   notification arrives as one thing rather than three fields changing out of step. */
.pb-slot {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 0;
  height: var(--pb-cell);
  overflow: hidden;
  /* A short fade at each end so a card rolls out of view instead of being guillotined.
     Kept to 9% (~7px) so it lands in the card's own clear band and never touches type. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 9%, #000 91%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 9%, #000 91%, transparent 100%);
}
.pb-reel {
  will-change: transform;
  transition: transform .58s cubic-bezier(.2, .9, .2, 1);
}
.pb-card {
  height: var(--pb-cell);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: calc(3px * var(--pb-s));
  min-width: 0;
}
.pb-eyebrow, .pb-title, .pb-meta {
  /* Cards carry lines of wildly different lengths. One-line-and-truncate keeps every
     card exactly one cell tall, which is the whole premise of the reel. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pb-eyebrow {
  font-size: calc(10.5px * var(--pb-s));
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: #9db4ff;
}
.pb-title {
  font-size: calc(17px * var(--pb-s));
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--hm-ink);
}
.pb-meta {
  font-size: calc(13px * var(--pb-s));
  color: var(--hm-muted);
}

/* ---- the tension layer ----------------------------------------------------
   The bottom of the fixed stack, under the burst at -4, so everything else paints on top
   of it. One value (--ten, written by scroll) drives both halves.

   Tension here is a NARROWING, not a brightening. The vignette's clear centre shrinks as
   --ten climbs, so the frame closes in on the ring and the visible world gets smaller —
   which is what reads as pressure. Doing it with light instead would spend the contrast
   the burst needs a moment later, and the payoff would land on an already-bright page.

   Both halves animate only opacity, scale and a gradient stop, and neither is on a timer:
   the whole layer is inert until scroll moves it. */
.ten {
  position: fixed;
  inset: 0;
  z-index: -5;
  pointer-events: none;
  --ten: 0;
}
.ten i { position: absolute; inset: 0; display: block; }

/* The swell behind the ring. Kept dim on purpose — it is the room heating up, not a light
   being switched on. */
.ten-glow {
  background: radial-gradient(circle at 50% 50%,
              rgba(122, 75, 216, .30) 0%,
              rgba(47, 107, 255, .13) 38%,
              transparent 66%);
  opacity: calc(var(--ten) * 0.9);
  scale: calc(0.72 + var(--ten) * 0.46);
}

/* The frame closing in. The clear centre goes from 62% of the box down to 30%, so by the
   time the bar is waiting to be pressed you are looking down a much tighter tunnel than
   you were during the reel. */
.ten-vig {
  background: radial-gradient(ellipse at 50% 50%,
              transparent calc(62% - var(--ten) * 32%),
              rgba(0, 0, 0, .58) calc(88% - var(--ten) * 24%),
              rgba(0, 0, 0, .94) 100%);
  opacity: var(--ten);
}

@media (prefers-reduced-motion: reduce) {
  /* Scroll-linked, so it is not "motion" in the animation sense — but a frame that closes
     in around you is exactly the kind of effect this setting is asking to be spared. */
  .ten { display: none; }
}

/* ---- the burst ------------------------------------------------------------
   The payoff. The ripple does not stop at the pill's edge — it keeps going and floods the
   page. Three colour lobes and a leading shock ring fire from the exact point the reticle
   pressed (JS writes --bx/--by in px from the pill's own box), expand past the far corner
   of the viewport, and then SETTLE rather than clearing: the lobes hold at ~.34 and drift,
   so the section below arrives lit instead of on flat black. The colour is the reward for
   the tap, not a strobe.

   `mix-blend-mode: screen` is doing real work here — it means the layer can only ADD
   light. Laid over the closing section it tints without ever darkening, so nothing it
   covers can lose contrast, whatever colour lands on it.

   Sized in vmax so the lobes clear the viewport's far corner in any aspect ratio, and
   centred by a negative margin so `scale`/`translate` stay free for the animation. */
.pb-burst {
  position: fixed;
  inset: 0;
  /* The BOTTOM of the fixed stack: below the pill (-3), the wave (-2) and the pinned copy
     (-1), and above the page's own background. Over the top of them it screened the pill
     and the ring — the two things it is exploding out of — to near-white. Underneath, it
     is the colour those things are lit BY.
     It must also stay a sibling of .wf and never a child: .wf is transform-positioned
     (so it would become this element's containing block and `inset: 0` would stop meaning
     the viewport) and is faded to 0 on release, which would take the colour with it. That
     nesting is exactly the bug this comment exists to prevent a repeat of. */
  z-index: -4;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
}
/* On instantly — the lobes' own keyframes are the entrance. Off on a transition, because
   scrolling back up past the tap should DRAIN the colour rather than cut it: the class is
   only removed once the fade has finished (see armTap). */
.pb-burst.is-burst { opacity: 1; transition: none; }
.pb-burst.is-out   { opacity: 0; transition: opacity 900ms var(--ease-standard); }
.pb-burst i, .pb-burst b {
  position: absolute;
  left: var(--bx, 50%);
  top: var(--by, 50%);
  border-radius: 50%;
  scale: 0;
  opacity: 0;
}
/* Three different sizes, not three of the same. Fired from one point at one size they
   stacked into a single flat purple field; graded — a tight red core inside a violet mid
   inside a wide blue halo — the resting wash is a composition that travels from warm at
   the tap point to cool at the edges of the screen. */
.pb-burst i {
  width: var(--r);
  height: var(--r);
  margin: calc(var(--r) / -2) 0 0 calc(var(--r) / -2);
}
.pb-burst.is-burst i {
  /* The SAME 1900ms clock the press runs on, so the colour can only ever leave the pill
     at the exact frame the pill releases. Two animations, deliberately on DIFFERENT
     properties: the burst owns `scale`, the drift owns `translate`, so the drift can
     start without stamping on the burst's resting value. */
  animation: pb-lobe var(--pb-seq, 1900ms) linear both,
             pb-lobe-drift 26s 1900ms ease-in-out infinite alternate;
}
.pb-burst i:nth-child(1) {
  --r: 260vmax;
  background: radial-gradient(circle, rgba(47, 107, 255, .85) 0%, rgba(47, 107, 255, .30) 30%, transparent 58%);
}
.pb-burst i:nth-child(2) {
  --r: 165vmax;
  background: radial-gradient(circle, rgba(122, 75, 216, .80) 0%, rgba(122, 75, 216, .28) 30%, transparent 58%);
  animation-delay: 60ms, 1960ms;
  animation-direction: normal, alternate-reverse;
}
.pb-burst i:nth-child(3) {
  --r: 100vmax;
  background: radial-gradient(circle, rgba(214, 57, 95, .74) 0%, rgba(214, 57, 95, .26) 28%, transparent 56%);
  animation-delay: 120ms, 2020ms;
}
/* Idle until 26% — the press-release frame — then out hard, overshoot, and SNAP back to
   a resting size rather than easing into it. The overshoot-and-return is what reads as
   the colour arriving with a stop instead of drifting to a halt. */
@keyframes pb-lobe {
  0%, 26% { scale: 0;    opacity: 0; animation-timing-function: cubic-bezier(.12, .85, .2, 1); }
  32%     { opacity: 1; }
  58%     { scale: 1.07; opacity: .98; animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
  67%     { scale: .965; animation-timing-function: cubic-bezier(.16, 1, .3, 1); }
  74%     { scale: 1;    opacity: .78; }
  100%    { scale: 1;    opacity: .68; }   /* it STAYS — this is the page's colour now */
}
@keyframes pb-lobe-drift {
  from { translate: -4vw 2vh; }
  to   { translate: 5vw -3vh; }
}

/* The leading edge — a hard white-blue shock that outruns the colour and vanishes. */
.pb-burst b {
  --r: 250vmax;
  width: var(--r);
  height: var(--r);
  margin: calc(var(--r) / -2) 0 0 calc(var(--r) / -2);
  border: 3px solid rgba(214, 228, 255, .95);
  box-shadow: 0 0 60px rgba(120, 165, 255, .55), inset 0 0 60px rgba(120, 165, 255, .4);
}
.pb-burst.is-burst b { animation: pb-shock var(--pb-seq, 1900ms) cubic-bezier(.10, .80, .20, 1) both; }
@keyframes pb-shock {
  0%, 26% { scale: 0; opacity: 0; }
  29%     { opacity: 1; }
  60%     { scale: 1; opacity: 0; }
  100%    { scale: 1; opacity: 0; }
}

/* ============================================================================
   THE SECTIONS AFTER THE ACT

   Everything from here down scrolls normally. The pinned sequence above is opaque-then-
   transparent by design: the burst is a fixed layer at z-index -4 that persists for the
   rest of the document, `.sec` is opaque, and `.sec--end` overrides to transparent — so the
   colour shows through the close and the first opaque section below ENDS it. That seam is
   the hand-off from payoff to practical and it costs nothing.
   ========================================================================== */

.sec-eyebrow {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-100);
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: #9db4ff;
}
.sec-body--soft { color: var(--ink-500); }
.sec-body--wide { max-width: 58ch; }
/* Photo-right instead of photo-left, so consecutive sections don't march. */
.sec-inner--flip > .sec-copy { order: -1; }

/* ---- 2. the scan ----------------------------------------------------------
   The code is a real /qr.svg. It sits on a white plate because a QR needs a light quiet
   zone to scan — inverting it for the dark page would make it decorative, which is the one
   thing this section must not be. */
/* A ROUND quiet zone, without touching a single module.
   The SVG carries its own white plate and a 4-module quiet zone, and both are load-bearing:
   scanners need that margin, and the plate is what keeps the code readable on a dark
   surface. So none of it changes. Instead the code sits centred on a larger white DISC —
   and a white square inside a white circle has no visible edge, so the eye only ever sees a
   round white field with a code floating in it. The illusion is free and the scan is
   untouched.
   Sizing is the one thing that has to be right: the square must fit inside the circle
   corner to corner, which needs a diameter of at least side x sqrt(2) ~ 1.41. 1.52 leaves
   optical breathing room past that minimum rather than sitting on it. */
.sec--scan .scan-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
}
.scan-plate {
  --qr: clamp(168px, 24vw, 226px);
  width: calc(var(--qr) * 1.52);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 34px 80px -34px rgba(0, 0, 0, .95);
}
.scan-qr { display: block; width: var(--qr); height: auto; }
/* Outside the disc now — a caption inside it would break the circle it exists to sell. */
.scan-cap {
  margin: 0;
  font-size: var(--fs-100);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-500);
}
.sec-media--qr { display: grid; place-items: center; }

/* ---- 3. the cast ----------------------------------------------------------
   Frames are stacked and cross-faded. f0 stays opaque underneath so there is never a hole
   if a later frame is slow to decode — swapping src instead would show a blank on the
   first cycle, which is the flicker the 420ms blink timing exists to avoid. */
.sec--cast { background: var(--hm-canvas); }
 /* 1120, not 980: seven faces at their 118px ceiling plus six gaps came to 982px against
    a 980px container, so the last one wrapped to a row of its own. */
.sec-cast-in { max-width: 1120px; text-align: center; }
/* The heading stays centred; the paragraph does not. Six centred lines is well past the
   two-line limit where centring stops helping and starts costing legibility. */
.sec--cast .sec-body {
  max-width: 60ch;
  margin-inline: auto;
  text-align: left;
}
/* FULL-BLEED. The section's copy stays in the content column but the bar runs the whole
   viewport, because a marquee that stops short of the edges reads as a widget rather than
   as something passing through. 100vw off a centred container needs the negative-margin
   trick; the layout already sets overflow-x hidden on the root, so it cannot bounce the
   horizontal scroll. Braces are not safe inside a comment here — css-oracle counts them
   and reports the next selector as having absorbed a stray one. */
.cast-rail {
  position: relative;
  width: 100vw;
  margin: var(--sp-7) calc(50% - 50vw) var(--sp-6);
  overflow: hidden;
  /* Fade both ends so faces arrive and leave rather than popping at a hard edge. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 9%, #000 91%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0%, #000 9%, #000 91%, transparent 100%);
}
/* Two identical copies end to end; translating by exactly half the track is one copy-width,
   so the loop restarts on a frame identical to the one it ended on. Any other distance
   visibly jumps. transform only — this runs for the whole time the section is on screen. */
.cast-track {
  display: flex;
  width: max-content;
  gap: clamp(14px, 2.2vw, 30px);
  animation: cast-march 64s linear infinite;
}
@keyframes cast-march {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
.cast-rail.is-paused .cast-track { animation-play-state: paused; }
/* Let someone stop it to look at a face. */
.cast-rail:hover .cast-track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .cast-track { animation: none; }
}
.cast-face {
  position: relative;
  flex: none;
  width: clamp(76px, 8vw, 112px);
  aspect-ratio: 1;
  margin: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 40%, rgba(122, 75, 216, .20), transparent 70%);
}
.cast-face img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}
/* Every frame after the base is absolutely stacked and hidden until its turn. */
.cast-face .cast-frame {
  position: absolute;
  inset: 0;
  opacity: 0;
}
.cast-face .cast-frame.is-on { opacity: 1; }
.sec-cast-count {
  margin: 0;
  font-size: var(--fs-300);
  color: var(--hm-muted);
}
.sec-cast-count b { color: var(--hm-ink); font-weight: 600; }

/* ---- 4. what you keep -----------------------------------------------------
   A zip, drawn as a grid of plates rather than photographed — the section is about the
   FILE, and using real party photos here would just restate section two. */
.sec-media--keep { display: grid; place-items: center; gap: var(--sp-4); }
.keep-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  padding: var(--sp-4);
  width: min(340px, 74vw);
  border-radius: var(--r-card);
  background: rgba(255, 255, 255, .04);
  border: 1px solid rgba(255, 255, 255, .08);
}
.keep-tile {
  aspect-ratio: 3 / 4;
  border-radius: 4px;
  background: linear-gradient(150deg, rgba(47, 107, 255, .55), rgba(214, 57, 95, .45));
}
.keep-tile--b { background: linear-gradient(150deg, rgba(122, 75, 216, .55), rgba(47, 107, 255, .40)); }
.keep-tile--c { background: linear-gradient(150deg, rgba(214, 57, 95, .50), rgba(122, 75, 216, .45)); }
.keep-tile--d { background: linear-gradient(150deg, rgba(47, 107, 255, .40), rgba(122, 75, 216, .55)); }
.keep-tile--e { background: linear-gradient(150deg, rgba(214, 57, 95, .42), rgba(47, 107, 255, .50)); }
.keep-tile--f { background: linear-gradient(150deg, rgba(122, 75, 216, .48), rgba(214, 57, 95, .40)); }
.keep-cap {
  margin: 0;
  font-size: var(--fs-100);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-500);
}

/* ---- 5. the host's side ---------------------------------------------------
   Three plain cards. No border on each — the separation is spacing and a background
   shade, which is quieter than three outlined boxes in a row. */
.sec-host-in { max-width: 1080px; text-align: center; }
.host-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--sp-5);
  margin-top: var(--sp-7);
  text-align: left;
}
.host-card {
  padding: var(--sp-5);
  border-radius: var(--r-card);
  background: rgba(255, 255, 255, .035);
}
.host-card-t {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-500);
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--hm-ink);
}
.host-card-b {
  margin: 0;
  font-size: var(--fs-300);
  line-height: 1.6;
  color: var(--hm-muted);
}

/* ---- the footer -----------------------------------------------------------
   Replaces templates/footer.html, deleted with the old marketing layer and never rebuilt —
   which left /privacy and /terms with nothing linking them, so they were unreachable to
   visitors AND invisible to the crawler's meta audit, which only covers pages it can find. */
.st-foot {
  position: relative;
  z-index: 1;
  padding: var(--sp-8) var(--sp-5) var(--sp-7);
  background: var(--hm-canvas);
  border-top: 1px solid var(--hm-edge);
}
.st-foot-in {
  max-width: 1080px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
}
.st-foot-mark {
  margin: 0;
  font-size: var(--fs-200);
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--hm-ink);
}
.st-foot-mark span { color: var(--ink-500); font-weight: 400; }
.st-foot-nav { display: flex; flex-wrap: wrap; gap: var(--sp-5); }
.st-foot-nav a {
  font-size: var(--fs-200);
  color: var(--hm-muted);
  text-decoration: none;
}
.st-foot-nav a:hover { color: var(--hm-ink); text-decoration: underline; }
.st-foot-nav a:focus-visible { outline: 2px solid #fff; outline-offset: 3px; border-radius: var(--r-sm); }

@media (max-width: 860px) {
  .sec-inner--flip > .sec-copy { order: 0; }
  .host-grid { text-align: center; }
}

/* ---- the hand-off ---------------------------------------------------------
   The first section after the pinned sequence releases. Deliberately plain: after four
   sections of pinned motion the page needs to feel like a page again, and that contrast
   IS the effect. Nothing here is fixed, scrubbed or observed. */
/* Transparent so the burst — which sits at -2, below all in-flow content — reads through
   as this section's background. Its own type and button are in-flow and therefore paint
   above the blended layer, so they can never lose contrast to whatever colour lands
   behind them. */
.sec--end { background: transparent; }
.sec-end-in { text-align: center; max-width: 46ch; }
.sec-end-eyebrow {
  margin: 0;
  font-size: var(--fs-100);
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: #9db4ff;
}
.sec-end-body {
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-400);
  line-height: 1.6;
  color: var(--hm-muted);
}
.sec-end-note {
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-100);
  color: var(--ink-500);
}
.sec-end-title {
  margin: var(--sp-4) 0 0;
  font-size: clamp(34px, 5.4vw, 62px);
  line-height: 1.06;
  letter-spacing: -.02em;
  color: var(--hm-ink);
}

/* ---- asking for the tap ---------------------------------------------------
   The pill is the control — not a button underneath it. `.pb-hit` covers the pill and is
   the real one: a <button>, focusable, labelled, Enter/Space operable, and `hidden` until
   the bar assembles so it stays out of the tab order rather than merely invisible. That
   is also why .pb itself is no longer aria-hidden: a focusable element inside an
   aria-hidden ancestor is unreachable, so its decorative parts are hidden individually
   instead.

   The cue sits in the pill's own open right-hand space, so what you are told to press and
   what you press are the same object. */
.pb-cue {
  position: absolute;
  z-index: 3;
  right: calc(26px * var(--pb-s));
  top: 50%;
  translate: 0 -50%;
  display: grid;
  grid-template-columns: auto auto;
  align-items: center;
  column-gap: calc(10px * var(--pb-s));
  opacity: 0;
  transition: opacity var(--t-room) var(--ease-standard);
  pointer-events: none;
}
.pb.is-asking .pb-cue { opacity: 1; }
/* The message window gives up the room the cue needs, rather than the two overprinting
   each other — the slot already truncates, so the headline just gets shorter. */
.pb.is-asking .pb-slot { padding-right: calc(178px * var(--pb-s)); }
.pb-slot { transition: padding-right var(--t-room) var(--ease-standard); }
.pb-cue-ring {
  grid-row: 1 / 3;
  width: calc(19px * var(--pb-s));
  height: calc(19px * var(--pb-s));
  border-radius: 50%;
  border: 2px solid rgba(226, 236, 255, .85);
  box-shadow: 0 0 14px rgba(140, 175, 255, .55);
}
.pb-cue-say {
  font-size: calc(13.5px * var(--pb-s));
  line-height: 1.2;
  font-weight: 600;
  color: var(--hm-ink);
  white-space: nowrap;
}
.pb-cue-skip {
  font-style: normal;
  font-size: calc(9.5px * var(--pb-s));
  line-height: 1.3;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-500);
  white-space: nowrap;
}

/* A slow breath on the ring only — enough to read as waiting for you, not enough to
   compete with the mark. Transform only. */
@media (prefers-reduced-motion: no-preference) {
  .pb.is-asking .pb-cue-ring { animation: pb-wait 1.9s var(--ease-standard) infinite; }
}
@keyframes pb-wait {
  0%, 100% { scale: 1;    opacity: .85; }
  50%      { scale: 1.18; opacity: 1; }
}

/* The hit target: the whole pill, invisible, only live while it is being asked for.
   FIXED and positive z-index, parked over the pill by JS — see the markup for why it
   cannot simply live inside it. */
.pb-hit {
  position: fixed;
  z-index: 6;
  border: 0;
  padding: 0;
  border-radius: inherit;
  background: none;
  color: transparent;
  font: inherit;
  cursor: pointer;
  pointer-events: auto;
}
.pb-hit:focus-visible { outline: 2px solid #fff; outline-offset: 3px; border-radius: 999px; }
/* Gives under a scroll push and springs back. `translate` because .pb is already using
   `transform` for its centring and `scale` for the press. */
.pb { translate: 0 var(--pb-push, 0px); transition: translate var(--t-room) var(--ease-spring); }

/* ---- the tap --------------------------------------------------------------
   The last beat of the sequence. In the product this pill is a BUTTON — it opens the
   Party Room — so the sequence ends by pressing it: a reticle drops onto the open half of
   the pill, presses, and the pill answers with a ripple across its face and a shockwave
   off its edge.

   This is fired ONCE on a scroll threshold and then plays on its own clock, rather than
   being scrubbed like everything above it. A press scrubs badly: dragged backwards it
   un-presses and the shockwave sucks back in, which reads as a bug. It re-arms when you
   scroll back past the threshold, so the beat is repeatable like the photo stack.

   Everything animates `scale` and `translate` — the INDEPENDENT transform properties, not
   `transform` — because .pb is already using `transform` for its centring and an
   animation on it would clobber that. Browsers without them simply skip the scale. */
.pb-tap {
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  pointer-events: none;
}
/* Two discs of light spreading from the touch point across the pill's face, clipped by
   the pill exactly as a material ripple is. The second trails the first by 120ms so it
   reads as a wave with depth rather than one flat disc. */
.pb-tap i {
  position: absolute;
  /* Where the press actually landed, written by JS as a percentage of the pill's box.
     Falls back to dead centre for the keyboard and scroll-skip paths. */
  top: var(--pb-tapy, 50%);
  left: var(--pb-tapx, 50%);
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  translate: -50% -50%;
  scale: 0;
  opacity: 0;
  background: radial-gradient(circle,
              rgba(190, 210, 255, .34) 0%,
              rgba(47, 107, 255, .18) 42%,
              transparent 70%);
}
.pb.is-tapped .pb-tap i { animation: pb-ripple var(--pb-seq) cubic-bezier(.14, .8, .24, 1) both; }
.pb.is-tapped .pb-tap i:nth-child(2) { animation-delay: 90ms; }
@keyframes pb-ripple {
  0%, 26% { opacity: 0; scale: 0; }
  31%     { opacity: .98; }
  62%     { opacity: 0; scale: 1.8; }
  100%    { opacity: 0; scale: 1.8; }
}

/* The reticle: falls in from above actual size, lands, presses, and rebounds away. */
.pb-cursor {
  position: absolute;
  top: var(--pb-tapy, 50%);
  left: var(--pb-tapx, 50%);
  width: calc(34px * var(--pb-s));
  height: calc(34px * var(--pb-s));
  border-radius: 50%;
  translate: -50% -50%;
  border: 2px solid rgba(220, 230, 255, .92);
  box-shadow: 0 0 20px rgba(120, 160, 255, .6), inset 0 0 14px rgba(120, 160, 255, .4);
  scale: 2.6;
  opacity: 0;
}
/* THE CHOREOGRAPHY. Every element in the tap runs on ONE 1900ms clock (--pb-seq) and is
   staged by percentage inside it, so nothing can drift out of step — there is no second
   timer anywhere in the sequence to disagree with the first. The marks:

     0 -   171ms   the reticle SNAPS in from 3.4x, hard decelerate
     171 - 304ms   HOLD — the beat that makes the arrival read as a stop, not a glide
     304 - 399ms   PRESS — reticle and pill compress together, fast and linear
     399 - 494ms   HOLD — compressed. The pause before the release is what sells it.
     494ms         EVERYTHING FIRES: rebound, ripples, shockwave, burst, ring flare
     494 - 1406ms  the colour floods, overshoots, and SNAPS back to its resting state
     1406-1900ms   settle

   The holds are literal: the same value at two adjacent keyframes. `animation-timing-
   function` inside a keyframe sets the easing for the segment that STARTS there, which is
   how each beat gets its own character — a hard-out snap, a linear compression, a
   springy rebound — inside a single animation. */
.pb.is-tapped .pb-cursor { animation: pb-reticle var(--pb-seq) linear both; }
@keyframes pb-reticle {
  0%   { opacity: 0; scale: 3.4; animation-timing-function: cubic-bezier(.16, 1, .3, 1); }
  9%   { opacity: 1; scale: 1; }
  16%  { opacity: 1; scale: 1;   animation-timing-function: cubic-bezier(.7, 0, .9, .35); }
  21%  { opacity: 1; scale: .66; }
  26%  { opacity: 1; scale: .66; animation-timing-function: cubic-bezier(.16, 1, .3, 1); }
  34%  { opacity: .8; scale: 1.7; }
  46%  { opacity: 0; scale: 2.4; }
  100% { opacity: 0; scale: 2.4; }
}

/* The pill's own answer: it gives under the press, rebounds past its size, and throws a
   ring of light off its edge. box-shadow is used for the shockwave precisely because it
   paints OUTSIDE the element and so escapes the pill's own overflow clip. */
.pb.is-tapped { animation: pb-press var(--pb-seq) linear both; }
@keyframes pb-press {
  0%, 16% { scale: 1;      box-shadow: inset 0 1px 0 rgba(255,255,255,.07), 0 30px 70px -30px rgba(0,0,0,.9), 0 0 0 0 rgba(47,107,255,0); }
  21%     { scale: .962;   box-shadow: inset 0 1px 0 rgba(255,255,255,.07), 0 30px 70px -30px rgba(0,0,0,.9), 0 0 0 0 rgba(47,107,255,.55); }
  26%     { scale: .962;   animation-timing-function: cubic-bezier(.16, 1, .3, 1); }
  31%     { scale: 1.032; }
  38%     { scale: .992; }
  45%     { scale: 1;      box-shadow: inset 0 1px 0 rgba(255,255,255,.07), 0 30px 70px -30px rgba(0,0,0,.9), 0 0 0 34px rgba(47,107,255,0); }
  100%    { scale: 1;      box-shadow: inset 0 1px 0 rgba(255,255,255,.07), 0 30px 70px -30px rgba(0,0,0,.9), 0 0 0 34px rgba(47,107,255,0); }
}

@media (max-width: 900px) {
  .pb {
    --pb-s: .78;
    width: min(440px, calc(100vw - 32px));
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The lights hold their opening position and the reel cuts rather than rolls. Both
     still SHOW their content — the pill is a readout, so stopping it must not empty it.
     The track is halted mid-sweep so the visible copy is the one that is on screen. */
  .pb-track { animation: none; transform: translate3d(50%, 0, 0); }
  .pb-lights i { animation: none; }
  .pb-reel { transition: none; }
  /* The tap is pure motion with nothing to read, so it is dropped rather than frozen —
     and with it the burst and, in the script, the scroll hold. */
  .pb.is-tapped, .pb.is-tapped .pb-tap i, .pb.is-tapped .pb-cursor { animation: none; }
  .pb-tap, .pb-burst { display: none; }
}

/* ---- the stack ------------------------------------------------------------
   Each pass over the trigger plucks a photo out of the live stream and snaps it onto a
   pile here. Cards are NOT clipped to the media box — the pile is meant to spill a
   little past it, which is what makes it read as loose photographs rather than as a
   cropped gallery. Their geometry (size, resting offset, flight) is all set from JS,
   because it depends on the source photo picked at that moment. */
.sec-stack {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.sec-card {
  position: absolute;
  left: 0;
  top: 0;
  border-radius: var(--hm-radius);
  overflow: hidden;
  transform-origin: 50% 50%;
  box-shadow:
    inset 0 0 0 1px var(--hm-edge),
    0 2px 4px rgba(0, 0, 0, .5),
    0 22px 50px -14px rgba(0, 0, 0, .75);
  will-change: transform;
}
.sec-card img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The flyer: the card in transit, from the photo in the hero to its place on the pile.
   It is a SEPARATE fixed-position element rather than the card itself animating, and that
   is not a stylistic choice — .sec has overflow:hidden to contain the ambient glow, and
   the flight begins well above the section's top edge, so a card animating inside the
   section gets its whole first half clipped away and appears to materialise at the
   section boundary instead of coming from the stream. Living on <body> it is clipped by
   nothing. z-index matches the old traveller so it still passes UNDER the wordmark. */
.sec-fly {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 2;           /* see the stacking note above .hm-field */
  border-radius: var(--hm-radius);
  overflow: hidden;
  transform-origin: 50% 50%;
  pointer-events: none;
  will-change: transform;
  box-shadow:
    inset 0 0 0 1px var(--hm-edge),
    0 2px 4px rgba(0, 0, 0, .5),
    0 22px 50px -14px rgba(0, 0, 0, .75);
}
.sec-fly img { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (prefers-reduced-motion: reduce) {
  /* No flight, and no pile: the section keeps its resting image. */
  .sec-stack { display: none; }
  .sec-media.has-stack .sec-photo { opacity: 1; }
}

/* ---- responsive -----------------------------------------------------------
   The stream is scaled rather than re-authored: --hm-scale shrinks every photo from
   its single set of dimensions, and --sx/--ex are already in vw so the spread and the
   exit vectors track the viewport for free. Below the tablet breakpoint the photos
   marked --opt drop out entirely: sixteen at phone width is both visually crowded and
   sixteen simultaneous composited layers on the weakest hardware. */
:root { --hm-scale: 1; }

@media (max-width: 1100px) {
  :root { --hm-scale: .8; }
}
@media (max-width: 760px) {
  :root { --hm-scale: .66; }
  .hm { padding: var(--sp-5) var(--sp-4); }
  .hm-pic--opt { display: none; }
}
@media (max-width: 520px) {
  :root { --hm-scale: .56; }
}

/* ---- reduced motion -------------------------------------------------------
   The field is HIDDEN here rather than frozen. Every photo in this composition is
   positioned by its animation — they all share one anchor point at the nozzle — so
   simply stopping the animation would pile all nine on top of each other at the
   bottom edge. There is no meaningful still frame of a stream. The field is purely
   decorative (aria-hidden, and it states nothing the card doesn't), so removing it
   leaves the page complete: wordmark, tagline and action on the dotted canvas. */
@media (prefers-reduced-motion: reduce) {
  .hm-field { display: none; }
  .hm-cta { transition: none; }
  .st-bar::before, .st-bar::after { transition: none; }
}
