/* ============================================================================
   UNIÓN , the SITE's layout. 08 site/ owns this file.

   LAYOUT ONLY, AND IN TOKENS ONLY. No hex, no font name, no easing curve, no
   duration is written here: every value is a var() off the design system's
   tokens.css, or a plain geometric number (a column count, a percentage, a
   max-width) that carries no brand decision.

   What belongs here: the section wrapper, the grounds and their alternation,
   the gutters, the fold rhythm, the asymmetric grids, the phone's own layout,
   the shell's placement. What does NOT belong here: anything that changes how a
   component looks. A component's appearance is fixed by the design system and
   arrives through dist/components.css, lifted verbatim. If a component looks
   wrong, it is fixed in 04 design-system/ by whoever owns that lane, never here.
   ============================================================================ */

/* ------------------------------------------------ THE INCLUDE WRAPPER ------
   build.py wraps every included component in its own spec wrapper, carrying
   the component's id (which its lane JS finds it by) and the `.spec-stage`
   classes (which `lineup.css` keys a container query off:
   `.spec-stage:has(.e1-hd){container-type:inline-size}`, and every size inside
   L-dayboard is a cqw of it).

   But `.spec-stage` is also the SPEC PAGE's own presentation: up to 72px of
   padding, a gutter, a min-height and a background colour. That is
   documentation chrome for a component sitting alone on a white page, and on a
   real page it is a second inset inside a fold that already has one. It pushed
   the hero's facts strip 51px past the viewport at 1440.

   So the wrapper keeps the class and loses the chrome. The container query
   survives, because `container-type` is not reset here.
   ---------------------------------------------------------------------------- */
[data-include].spec-stage{padding:0;min-height:0;background:none}
/* the ground modifiers are the SPEC page saying "show this on coral". The page
   decides its own grounds, fold by fold, so they do not travel either. */
[data-include].spec-stage--black,
[data-include].spec-stage--pink,
[data-include].spec-stage--coral,
[data-include].spec-stage--yellow{background:none}
/* ...but a component lifted from a BLACK stage still expects cream ink on a
   dark ground. Where the page puts it on a dark fold that is already true; the
   one it is not true for is a black-stage component on a light fold, and no
   page does that. Keep the ink rule so the two stay in step. */
[data-include].spec-stage--black{color:inherit}

/* -------------------------------------------------------- visually hidden --
   .u-sr is the site's one screen-reader-only utility. The live region
   (#site-live) announces the waitlist, the checkout steps and the travel mode
   switch; it must reach a screen reader and must never be seen. It was used on
   eight pages and defined nowhere, so the live region printed as visible text
   at the top of every <main>.

   The one-pixel clip rectangle, not display:none and not visibility:hidden:
   both of those take the element out of the accessibility tree and no
   announcement would ever be made.
   ---------------------------------------------------------------------------- */
.u-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
  border:0}

/* ---------------------------------------------------------------- the page --
   The shell's sticky pieces resolve against the viewport, which is what the
   design system says should happen once a component leaves its spec frame.
   ---------------------------------------------------------------------------- */
/* The phone's ticket bar is FIXED to the bottom of the viewport, because it is
   the phone's one permanent affordance and it is meant to follow you. Being
   fixed it sits over whatever the page ends with, so the page reserves its
   height at the end of the document. */
body{background:var(--ground);color:var(--ink);margin:0;
  --chrome-ticket:0px;
  padding-bottom:var(--chrome-ticket)}

/* ------------------------------------------------------------- THE SECTION --
   One section is one fold, one dominant idea. The wrapper gives the gutter and
   the vertical rhythm; the ground is a separate class so the two never fight.
   ---------------------------------------------------------------------------- */
.s{position:relative;padding-block:var(--s9);
  background:var(--ground);color:var(--ink)}
.s--tight{padding-block:var(--s7)}
.s--flush{padding-block:0}
.s__in{max-width:var(--measure);margin-inline:auto;padding-inline:var(--gutter);
  /* the river and the pattern slots size against this, so it must be a
     container in its own right */
  container-type:inline-size}
.s__in--wide{max-width:1680px}

/* the fold's own head: an eyebrow, the one headline, and nothing else. */
.s__eye{display:block;margin:0 0 var(--s4);
  font:700 var(--size-micro)/1.35 var(--font);
  text-transform:uppercase;letter-spacing:.14em}
.s__h{margin:0 0 var(--s6);
  font:900 var(--size-display)/.9 var(--font);
  font-stretch:var(--wdth-condensed);text-transform:uppercase;
  /* the acute on Ó sits above the line box at this line-height. base.css gives
     every display voice padding-top and overflow:visible. Do not remove it. */
  padding-top:.22em;overflow:visible}
.s__h--head{font-size:var(--size-head);line-height:1}
.s__lead{margin:0 0 var(--s7);max-width:56ch;
  font-size:var(--size-body);line-height:1.5}

/* ------------------------------------------------------------- THE GROUNDS --
   Coral carries the hero and nothing else carries it on the same page. Two
   neighbouring sections never share a ground: .s + .s of the same ground is a
   build error and test.py checks for it.
   ---------------------------------------------------------------------------- */
.g-cream{--ground:var(--cream);--ink:var(--black);--focus-colour:var(--black)}
.g-coral{--ground:var(--coral);--ink:var(--black);--focus-colour:var(--black)}
.g-pink{--ground:var(--pink);--ink:var(--black);--focus-colour:var(--black)}
.g-yellow{--ground:var(--yellow);--ink:var(--black);--focus-colour:var(--black)}
.g-black{--ground:var(--black);--ink:var(--cream);
  --focus-colour:var(--focus-colour-on-dark)}

/* Cream on pink and cream on coral are a DISPLAY treatment only: a headline may
   take it, a paragraph may not. This class is the only way to ask for it, and
   it reaches a display voice only.
   MEASURED, and the measurement narrows the rule. Against our own tokens:
     cream on pink   3.16:1  , clears the 3.0 large-text bar, so it is allowed
     cream on coral  2.27:1  , does NOT clear it, even as display
     black on coral  6.82:1
   So coral keeps black type. The treatment is pink's. This is not a preference;
   test.py fails the page at 2.27:1 and it is right to. */
.g-pink .u-display-invert{color:var(--cream)}

/* The one pill is pink. On a pink ground it would lose its shape and the page
   would lose its primary action, so on pink it takes the black slab with cream
   type (15.51:1). The ghost pill has no fill and is left alone. */
.g-pink .u-pill:not(.u-pill--ghost){background:var(--black);color:var(--cream)}

/* -------------------------------------------------------------- THE RIBBON --
   The pattern is held right back. A ribbon band sits at TWO or THREE section
   joins per page, never as a field. Each band is a window the river is laid
   across, so it reads as a drawn stroke passing behind the page rather than as
   a tiled swatch.
   ---------------------------------------------------------------------------- */
/* WHAT THIS USED TO BE, AND WHY IT WAS WRONG.
   A rectangular strip: a fixed height, `overflow:hidden`, and a hard black
   rule across the top and the bottom. Every stroke the river laid in it was
   sliced flat by those two lines. That is the swatch construction Louis has
   now ruled against three times , "a pattern shown as a rectangle is a
   swatch" , and its sharper half, the boundary rule:

     "I don't think the pattern should ever be cropped off and just stop
      suddenly. They have to fade into it or they have to bleed off the page."

   The design system says the same thing in its own comments: "the window must
   be taller than the pattern, or the mass gets cut top and bottom."

   WHAT IT IS NOW: the one treatment Louis has approved, the river seen through
   a WINDOW THAT FADES AT ITS EDGES (tickets T2). Three changes, each doing a
   job:
     1. NO RULES. The two black lines were the hard cut. A join does not need
        a drawn boundary; the change of ground already is one.
     2. THE WINDOW IS TALLER THAN THE STROKES, so the mass sits inside it with
        room rather than filling it to the brim. The river is laid at a height
        that leaves headroom (see --rv-h), so most strokes end in their own
        round cap inside the window and are never cut at all.
     3. WHERE A STROKE DOES REACH AN EDGE, THE EDGE FADES, on both axes, so it
        dissolves into the ground instead of stopping on a straight line.
   Ink budget is unchanged: same band count (two joins a page), same strokes. */
.s-ribbon{position:relative;height:clamp(84px,11vw,168px);
  /* clipped horizontally only, so a stroke may still BLEED off the page,
     which is the other legal ending. The fade below does the rest. */
  overflow-x:clip;overflow-y:hidden;
  background:var(--ground)}
/* the slot takes the BAND's height, through the system's own variable. Left at
   its 150px default it would stand proud of an 84px band on a phone and short
   of a 168px one on desktop, and either way the river would be cut by the slot
   rather than faded by it. Same defect as the fill panel's, same fix. */
.s-ribbon > .pattern-slot{position:absolute;inset:0;
  --pattern-height:100%;height:100%;
  -webkit-mask-image:linear-gradient(90deg,transparent,black 11%,black 89%,transparent),
    linear-gradient(transparent,black 16%,black 84%,transparent);
  mask-image:linear-gradient(90deg,transparent,black 11%,black 89%,transparent),
    linear-gradient(transparent,black 16%,black 84%,transparent);
  -webkit-mask-composite:source-in;mask-composite:intersect}
@media (max-width:640px){
  .s-ribbon{height:clamp(64px,17vw,104px)}
}

/* ---------------------------------------------------------------- THE HERO --
   The one fold that is allowed to be the biggest thing on the page. Coral.
   Asymmetric: the wordmark and the squash line take the left two thirds, the
   facts and the countdown stack down the right, and the buttons sit under the
   line rather than centred beneath everything.
   ---------------------------------------------------------------------------- */
/* .hero is a GRID, not a flex row. As a flex container its child .s__in became
   a flex item with no basis, shrank to min-content, and the hero grid inside it
   resolved to `0px 300px`: the wordmark and the whole left column measured 0px
   wide and the lead paragraph came out one word per line. A grid with a single
   full-width column keeps .s__in at its own max-width and still centres the
   fold vertically. */
.hero{min-height:calc(100svh - 120px);display:grid;align-content:center;
  padding-block:var(--s8) var(--s9)}
.hero > .s__in{width:100%}
.hero__grid{display:grid;gap:var(--s7) var(--s7);
  /* THE SIDE COLUMN IS AS WIDE AS THE CLOCK IN IT (measured 21 Sep). The
     countdown's four flip cards are a fixed 481px; the column guaranteed only
     300, so from 1100 to about 1400 wide it resolved narrower than its own
     contents and the clock and the fact lines ran ~80px past the content edge,
     off the screen at 1280. 490 holds them at every width this grid is live
     (it goes single column at 1024). */
  grid-template-columns:minmax(0,1.55fr) minmax(490px,.95fr);
  align-items:start}
/* the wordmark is an inline custom element until union.js injects its SVG, so
   it is given a block box and an explicit width, or it measures 0 and the mark
   simply is not there. */
/* THE FOCUS. Louis fixed this hierarchy: the wordmark is the one thing this
   fold is for, and everything else is visibly subordinate. So it takes the
   FULL width of its column rather than capping at 760px, and the column is the
   wide one. At 390 that is the whole gutter box, which is the largest the mark
   can be, and the squint test is the check: if anything else reads first, this
   is wrong. `h1`, because it is the page's heading; the accessible name is the
   .u-sr text beside it, since the drawn mark carries no text of its own. */
.hero__mark{display:block;width:100%;margin:0}
.hero__mark u-svg{display:block;width:100%}
.hero__mark svg{display:block;width:100%;height:auto}

/* SECOND: the date and the place. One line, set small and wide-tracked against
   the mark above it, so it reads as a caption to the mark rather than as a
   second headline. It is information on coral, so it is BLACK, per the system:
   cream on coral is display only. */
.hero__when{margin:var(--s5) 0 0;
  font:700 var(--size-small)/1.3 var(--font);
  font-stretch:var(--wdth-condensed);text-transform:uppercase;
  letter-spacing:.08em;color:var(--black)}

/* THIRD: the one action. */
.hero__acts{display:flex;flex-wrap:wrap;gap:var(--s4);margin-top:var(--s6)}
.hero__side{display:grid;gap:var(--s6);align-content:start;
  /* the facts strip and the countdown are two separate objects, not one card */
  padding-top:var(--s4)}
.hero__squash{margin-top:var(--s6)}

/* ---------------------------------------------------------- ASYMMETRIC GRIDS
   The rule the mockups enforce: nothing on this site is a row of equal cards
   centred in its column. Every multi-item fold is uneven on purpose.
   ---------------------------------------------------------------------------- */
/* three stages: the first is the wide one, the other two share the rest */
.grid-stages{display:grid;gap:var(--s6);
  grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr);
  align-items:end}
.grid-stages > :nth-child(1){grid-row:span 2}

/* a fold that is one big thing and one narrow column beside it */
.grid-lead{display:grid;gap:var(--s7);
  grid-template-columns:minmax(0,1fr) minmax(260px,.55fr);align-items:start}
.grid-lead--flip{grid-template-columns:minmax(260px,.55fr) minmax(0,1fr)}

/* the park: a tall picture, a short picture offset down, and the facts beside */
.grid-park{display:grid;gap:var(--s5);
  grid-template-columns:minmax(0,1.1fr) minmax(0,.75fr) minmax(0,1fr);
  align-items:start}
/* Until real park photography exists, the two picture cells carry the pack's
   own numbers at display size: a figure the eye can read across the fold, which
   is what a photograph would have been doing. Two different shapes, dropped at
   two different heights, so the fold is still asymmetric. */
/* each figure gets its own GROUND, or a yellow box on a yellow fold is an empty
   outline: the first render had the number invisible inside a drawn rectangle. */
.park-figure{display:grid;align-content:end;gap:var(--s2);
  aspect-ratio:5/4;padding:var(--s5);
  background:var(--coral);color:var(--black);
  border:var(--rule-heavy) solid var(--black)}
.park-figure--b{aspect-ratio:1/1;margin-top:var(--s8);
  background:var(--black);color:var(--cream)}
.park-figure__n{font:900 var(--size-display)/.82 var(--font);
  font-stretch:var(--wdth-condensed);padding-top:.22em;overflow:visible}
.park-figure__u{font:700 var(--size-micro)/1.35 var(--font);
  text-transform:uppercase;letter-spacing:.14em}

/* ------------------------------------------------------------- THE SHELL ---- */
/* THE MARQUEE AND THE STRIP RUN WIDER THAN THE PAGE, ON PURPOSE: each is two
   identical halves so the loop is seamless, and that max-content width
   propagates outward unless it is stopped. docs/TRAPS.md records the same
   failure (a dismiss button measured 645px off screen). The fix is min-width:0
   on every box from the container down to the track. Three declarations each,
   and only here: the components themselves are untouched. */
.n-strip--site{position:relative;z-index:var(--z-overlay);min-width:0;overflow:hidden}
.n-strip--site .d-totem__bar--alert{min-width:0}
.n-strip--site .d-totem__track{min-width:0}
/* ------------------------------------------------- THE ROLLING BANNER ------
   THE BAND OWNS THE GROUND. The pre-drawn lockup carries its pink inside the
   artwork, so tiling it over an unpainted band let a sub-pixel seam between
   two cloned tiles show the cream page through: thin white gaps, visible on a
   phone. Painting the BAND pink and drawing the marks transparent over it
   makes a gap impossible: the worst a seam can be is more of the band's own
   pink. That is a construction fix, not a rounding fix, which is why it holds
   at every width and every phase of the loop.
   ---------------------------------------------------------------------------- */
.s-marquee{min-width:0;overflow:hidden;
  background:var(--pink);
  border-top:var(--rule) solid var(--black);
  border-bottom:var(--rule) solid var(--black)}
/* one repeat of the lockup: the wordmark, then the soundwave, at the spacing
   the lockup's own geometry uses (a 222-unit wordmark and a 95-unit icon on a
   ~712-unit repeat, so the gap is a little over a third of the wordmark). */
.s-marquee__set{display:flex;align-items:center;flex:0 0 auto;
  gap:clamp(var(--s5),4vw,var(--s7));
  padding-right:clamp(var(--s5),4vw,var(--s7))}
.s-marquee .s-marquee__word{height:var(--banner-h,64px);width:auto;flex:0 0 auto}
.s-marquee .s-marquee__icon{height:calc(var(--banner-h,64px) * .42);width:auto;
  flex:0 0 auto}
.s-marquee__set u-svg svg{height:100%;width:auto;display:block}
/* the run travels on the compositor and is measured in whole pixels by
   site.js, so a fractional set width cannot accumulate into a visible step */
.s-marquee .n-marquee__run{will-change:transform}
.s-head{position:sticky;top:0;z-index:var(--z-raise)}

/* THE HEADER'S OWN BREAKPOINT. navigation.css switches .n-head to its burger
   form at a container width of 640px, sized for the spec page's frame, which
   carries ONE link. This site's header carries five, plus the EN/ES switch and
   the pill, and at 768 "Info" and "News" ran underneath the switch; the hit test
   caught it as two covered controls.
   So the site raises the switch to 900px for its OWN header. This changes where
   the header changes shape, which is layout, and layout is this file's to
   decide. It does not change how any part of the header looks: the same classes,
   the same burger, the same menu. */
@container head (max-width: 900px){
  .s-head .n-head__links{display:none}
  .s-head .n-head__burger{display:grid}
  .s-head .n-head__mark{width:var(--mark-sm,108px)}
  /* the pill goes too: at the burger width the header carries the mark and the
     burger, and the ticket action is in the menu and on the phone ticket bar. */
  .s-head .n-head__end .u-pill{display:none}
  /* and so does the language switch: at 390 it is 162px of a 390px bar, beside
     a burger, for a decision nobody makes in the header. It moves into the menu,
     where the same device has room to be pressed. */
  .s-head .n-head__end .c-lang{display:none}

  /* THE BURGER IS FAR RIGHT, AND IT IS THE LAST THING IN THE ROW.
     With the links, the pill and the switch all gone above, .n-head__end holds
     the burger alone, and margin-left:auto pushes that cell hard against the
     header's own right padding, which IS the page gutter. Nothing may sit
     outboard of it. 48px square, which clears the system's 44px thumb pad with
     room, and no padding of its own to push it inboard.
     Gated: test.py asserts the button's right edge sits within one gutter of
     the viewport's right edge at 390 and 768, and that it measures 48px. */
  .s-head .n-head__end{margin-left:auto;justify-content:flex-end;
    padding-right:0;gap:0}
  .s-head .n-head__burger{width:48px;height:48px;margin-right:0;
    margin-left:auto;padding:0;place-items:center}
}

/* the language switch's phone home: inside the menu, above the flags, where a
   162px device fits and a thumb can reach both halves. The same C-lang device,
   same classes, placed. */
.n-menu .c-lang{display:none}
@media (max-width:900px){
  .n-menu .c-lang{display:grid;margin:var(--s4) var(--s4) 0;justify-self:start}
}

/* the phone menu needs a fixed positioned ancestor, because .n-menu is
   position:absolute;inset:0 and on the spec page the frame supplied one. */
.s-menu-host{position:fixed;inset:0;z-index:var(--z-chrome);
  pointer-events:none;visibility:hidden}
.s-menu-host:has(.n-menu[data-open="1"]){pointer-events:auto;visibility:visible}
.s-menu-host .n-menu{pointer-events:auto}

/* THE PAGE ENDS ON THE FOOTER'S OWN GROUND. This row used to be transparent, so
   the body's cream showed as a bare strip UNDER the black footer and the cookie
   bar: the last thing on every page was a band of the wrong ground. It paints
   the footer's black edge to edge and holds the button on the page's own
   measure. */
.s-totop{display:flex;justify-content:flex-end;background:var(--black);
  padding:var(--s5) max(var(--gutter), var(--edge))}

/* the sticky ticket bar is the PHONE's, and only the phone's */
.s-ticketbar{display:none}

/* THE COOKIE BAR TAKES ITS OWN SPACE. The component ships position:absolute so
   it can sit at the bottom of its spec frame. On a real page the obvious move is
   position:fixed, and that is the wrong one: the design system's own note on
   this component says it is "a notice, not a dialog: it does not trap focus and
   it does not block the page behind it". A fixed bar DOES block the page behind
   it, and the hit test proved it, covering the footer's legal links and the
   back-to-top button on every page at 1440 and 1280.
   So it flows at the end of the document instead. It is still the first thing
   you meet at the bottom, it still dismisses, and it covers nothing. */
.s-cookie{position:static;z-index:var(--z-overlay)}
.s-cookie[hidden]{display:none}

/* ---------------------------------------------------------- THE STATE SWITCH
   ?state=onsale flips <html data-state>. Everything marked data-show renders in
   one state only. Both states are complete pages; neither is a stub.
   ---------------------------------------------------------------------------- */
/* ONLY EVER HIDE. Never re-declare a display to reveal.
   The first version hid everything with display:none and revealed with
   `display:revert`, which reverts to the UA DEFAULT, not to the design system's
   value: an <a> the system had made inline-flex came back as plain `inline`,
   silently ignoring min-height, and every footer link in the inactive state
   measured 146x17 instead of a 44px thumb pad. The gate caught it.
   So the rule only ever matches the element that should be HIDDEN. An element
   that should show is never selected at all, and keeps whatever display its
   component gave it, untouched. */
:root[data-state="onsale"] [data-show="pre"],
:root[data-state="pre"] [data-show="onsale"]{display:none}
/* before site.js has set data-state, nothing with a state is shown, so the page
   never flashes both versions. */
:root:not([data-state]) [data-show]{display:none}

/* ------------------------------------------------------- THE SCRUB PROGRESS
   A hairline that fills as the page is read. Its width is driven from site.js
   through --scrub, so no duration or easing is written anywhere.
   ---------------------------------------------------------------------------- */
.s-scrub{position:fixed;left:0;top:0;height:var(--rule-heavy);
  width:100%;transform-origin:0 50%;transform:scaleX(var(--scrub,0));
  background:var(--primary);z-index:var(--z-chrome);pointer-events:none}

/* --------------------------------------------------------- THE PINNED STAGE
   Home's stage panels are pinned and swapped by ScrollTrigger. The layout here
   only builds the stack; the motion is site.js, and if GSAP never loads the
   panels simply sit one under the other and the page still reads.
   ---------------------------------------------------------------------------- */
.pin-stage{position:relative}
.pin-stage__panel{padding-block:var(--s7)}
.pin-stage.is-pinned{min-height:100svh;display:grid;align-content:center}
.pin-stage.is-pinned .pin-stage__panel{grid-area:1/1;opacity:0;padding-block:0}
.pin-stage.is-pinned .pin-stage__panel.is-on{opacity:1}

/* ================================================================== 1024 ==== */
@media (max-width:1024px){
  .s{padding-block:var(--s8)}
  .hero__grid{grid-template-columns:minmax(0,1fr);gap:var(--s6)}
  .hero__side{padding-top:0}
  .grid-stages{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  .grid-stages > :nth-child(1){grid-row:auto;grid-column:1/-1}
  .grid-lead,.grid-lead--flip{grid-template-columns:minmax(0,1fr)}
  .grid-park{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  .park-figure--b{margin-top:0}
}

/* ================================================================== 640 =====
   THE PHONE IS ITS OWN LAYOUT, not the desktop squeezed. One column, a shorter
   fold, the hero's proportions rebuilt rather than reflowed, the ticket bar
   present and the desktop links gone.
   ============================================================================ */
@media (max-width:640px){
  .s{padding-block:var(--s7)}
  .s__h{margin-bottom:var(--s5)}
  .s__lead{margin-bottom:var(--s6)}

  /* the hero at 390 is the mark, the line, one button and the facts. The
     countdown moves under the facts rather than beside them, and the hero
     stops trying to fill the viewport: a short hero that is completely read
     beats a tall one that is half read. */
  .hero{min-height:0;padding-block:var(--s6) var(--s7)}
  .hero__grid{gap:var(--s6)}
  .hero__acts{flex-direction:column;align-items:stretch;margin-top:var(--s6)}
  .hero__acts > *{width:100%}
  .hero__when{margin-top:var(--s4)}

  .grid-stages,.grid-park{grid-template-columns:minmax(0,1fr)}
  .grid-stages > :nth-child(1){grid-column:auto}

  /* the bar is the phone's one permanent affordance, so the page ends above it */
  .s-ticketbar{display:flex;position:fixed;left:0;right:0;bottom:0;
    z-index:var(--z-overlay)}
  /* THE BAR IS FIXED, SO IT HANGS OVER THE END OF THE DOCUMENT. At full scroll
     the last ~84px of the page is permanently underneath it and no amount of
     scrolling frees it: the hit test caught the footer's link columns and then
     the cookie bar's own two buttons sitting there.
     `padding-bottom` on <body> does not help, because the body's padding is
     inside its own box and the fixed bar simply paints on top of it. The
     clearance has to be on the LAST thing in the document, so everything above
     it is pushed clear. --chrome-ticket stays declared for the fallback. */
  body{--chrome-ticket:84px;padding-bottom:0}
  .s-totop{margin-bottom:calc(var(--chrome-ticket) + var(--s5))}
  .s-totop{padding-block:var(--s5)}

  /* every target is at least a thumb pad. The prototype switch is held back
     everywhere else on the page, but NOT here: a control you cannot press is
     not discreet, it is broken. */
  .n-foot__col a,.n-foot__legal a,.n-foot__social a{min-height:var(--tap-min)}
}

/* ==========================================================================
   THE SCROLL ENGINE'S LAYOUT
   Every verb the engine drives has its REST STATE here, and the rest state is
   the FINISHED state: with JavaScript off, with GSAP unreachable, or under
   reduced motion, the page below reads correctly and completely. Nothing here
   is decoration waiting to be animated into existence.
   ========================================================================== */

/* ---- a. SECTIONS THAT STACK.
   The fold slides up OVER the one before it, with native scroll and CSS
   sticky: no scroll-jacking, no per-frame JavaScript, and it keeps working
   with JS off. Each stacking section sticks to the top of the viewport while
   the next one climbs over it.

   The SHADOW LINE at the top edge is the system's own rule weight, so the
   join reads as a printed edge rather than a soft drop shadow, which the
   brief bans as an AI tell. */
/* THE STACK NEVER HELD, AND THIS IS WHY (measured 21 Sep: 0px of hold on every
   fold, at 1440 and 390). base.css sets `html{overflow-x:clip}` and
   `body{overflow-x:hidden}`. Because html's overflow is already non-visible,
   the body's is NOT propagated to the viewport: the body becomes its own scroll
   container, one that never scrolls, and every sticky fold sticks to IT, which
   is never. `clip` cuts the same sideways overflow without making a scroll
   container. With that one change the folds hold.

   A FOLD TALLER THAN THE SCREEN SCROLLS TO ITS OWN BOTTOM FIRST. At top:0 it
   would stick the moment its top arrives and the next fold would cover its
   lower half unread. --stack-top is `min(0, screen - fold)`, set by site.js, so
   the fold travels until its last line is on screen and holds THERE. */
body{overflow-x:clip}
/* with no JavaScript --stack-top is never set, and the fallback is a top so far
   up that the fold never sticks: it scrolls as a plain page and loses nothing */
[data-stack]{position:sticky;top:var(--stack-top,-9999px);
  /* the stack order is set from JS as --stack-i so a later section always
     paints over an earlier one, whatever order the DOM gives them */
  z-index:calc(var(--z-body) + var(--stack-i,0));
  border-top:var(--rule) solid var(--black)}
/* the first of a run does not need a rule: there is nothing above it to
   separate from */
[data-stack]:first-of-type{border-top:0}
/* a swap host that fell back to a stack on the phone behaves the same way */
[data-stack-fallback] [data-swap-slide]{position:sticky;top:0}

/* ---- b. TEXT THAT FILLS AS YOU SCROLL.
   The pinned statement. At rest every word is at full size and full opacity,
   which is the finished sentence: the scrub only takes it there from smaller.
   The host is a full screen so the pin has something to hold. */
/* LOUIS, 21 SEP: NO PIN, AND THEREFORE NO FULL SCREEN. The host used to be
   `min-height:100svh` because a pin needs a screen to hold. With the pin gone
   the fold is exactly as tall as its own type plus the section's own token
   padding, which is what removes the screen of dead ground Louis saw. It reads
   as a big typographic band rather than a statement marooned in cream. */
[data-fill]{position:relative;
  display:grid;align-content:center;overflow:hidden}
[data-fill] .fill__words{display:grid;gap:var(--s3);
  position:relative;z-index:var(--z-body)}
[data-fill-word]{display:block;
  /* the word is a transform target, so it gets its own compositor layer and
     the squash never reflows the line below it */
  will-change:transform;transform-origin:50% 100%}
/* the river sits behind the type, at the pattern layer, never over it.
   RULING 4. The host must clip (it is a pinned full-screen panel and a river
   bleeding out of it would widen the document), so the cut edge is MASKED TO
   A FADE on both axes , the system's own `.rv--fade-x`/`--fade-y` geometry,
   applied to the slot rather than re-derived. Before this, crop.py measured
   326px of full-strength ink lying flat along the left edge of .fill: a
   stroke sliced in mid-air, which reads as a mistake and not as artwork. */
/* `inset:0` alone was NOT enough and this cost a pass to find. `.pattern-slot`
   carries `height:var(--pattern-height,150px)` in devices.css, and an explicit
   height beats `inset`, so the slot resolved to 150px at the TOP of a 900px
   panel while UNION.river laid strokes across the panel's full height. The
   strokes then overflowed the slot's own `overflow:hidden` and were cut by it
   before the fade could ever reach them: the mask was fading an edge the ink
   was not stopping at. The height is set through the system's OWN variable,
   not overridden, so the slot stays the system's component. */
[data-fill] [data-river-slot]{position:absolute;inset:0;z-index:var(--z-river);
  --pattern-height:100%;height:100%;
  /* the fade reaches FAR IN, because a stroke is a long object: a 12% fade on
     a 390px panel is 47px, and a stroke that crosses the edge at a shallow
     angle is still at most of its strength when it clears it. Measured, not
     guessed: at 12% crop.py still read 68px of full-strength ink on the cut
     line; at 22/20% it reads none. The type sits in the middle of the panel,
     so a deeper fade costs the composition nothing. */
  -webkit-mask-image:linear-gradient(90deg,transparent,black 22%,black 78%,transparent),
    linear-gradient(transparent,black 26%,black 74%,transparent);
  mask-image:linear-gradient(90deg,transparent,black 22%,black 78%,transparent),
    linear-gradient(transparent,black 26%,black 74%,transparent);
  -webkit-mask-composite:source-in;mask-composite:intersect}

/* ---- c. A SMALL THING GROWS TO FULL BLEED.
   Driven on SCALE by the engine, never on width: a width tween relayouts every
   frame. The rest state is the grown one. */
[data-grow]{will-change:transform;transform-origin:50% 50%}

/* ---- d. PINNED SLIDES THAT SWAP.
   Unpinned, the slides are a plain readable stack, which is the no-GSAP and
   reduced-motion state. Pinned, they share one grid cell and take turns. */
[data-swap]{position:relative}
[data-swap-slide]{padding-block:var(--s7)}
[data-swap].is-pinned{min-height:100svh;display:grid;align-content:center}
/* THE SLIDE THAT IS NOT ON IS GENUINELY HIDDEN, not just transparent.
   Measured: with opacity:0 alone, the three slides share one grid cell and
   every word on all three sits on top of every word on the other two. The
   text-over-text gate read it as nine real collisions, and it was RIGHT to:
   opacity is not inherited as a hiding mechanism, the children are still
   painted, still hit-testable and still read by a screen reader.
   `visibility:hidden` takes the whole subtree out of paint, out of hit
   testing and out of the accessibility tree, and it still animates, which
   `display:none` does not. */
[data-swap].is-pinned [data-swap-slide]{grid-area:1/1;padding-block:0;
  opacity:0;visibility:hidden}
[data-swap].is-pinned [data-swap-slide].is-on{opacity:1;visibility:visible}

/* ---- e. THE PROGRESS LINE, AS A RIBBON STROKE.
   The host holds a real drawn stroke whose length the engine reveals with
   stroke-dashoffset, which is the system's DRAW verb. The --scrub fallback
   keeps a plain rule filling if the artwork never arrives. */
[data-progress]{position:relative;overflow:hidden}
[data-progress] u-svg,[data-progress] u-svg svg{display:block;width:100%;height:auto}
[data-progress] u-svg svg path{fill:none;stroke:var(--primary);
  stroke-width:var(--rule-heavy);stroke-linecap:round}
/* the fallback rule: only painted when there is no stroke to draw */
[data-progress]:not(:has(u-svg svg))::after{content:"";position:absolute;
  left:0;bottom:0;height:var(--rule-heavy);width:100%;
  transform-origin:0 50%;transform:scaleX(var(--scrub,0));
  background:var(--primary)}

/* ---- f. RIBBONS THAT DRAW ALONG THE SCROLL.
   Generic host. The strokes are its own; this only gives them a layer. */
[data-draw]{position:relative}
[data-draw] u-svg{will-change:transform}

/* ==========================================================================
   BRAND FURNITURE
   The utilities a page reaches for instead of hand-rolling the brand.
   ========================================================================== */

/* ---- THE SOUNDWAVE AS A SECTION MARKER.
   The system's only icon, and its floor is 40px. Below that it is repeated at
   native proportion rather than shrunk or stretched, which is why the height
   is clamped from 40px up and never down. */
.u-wave{display:block;height:clamp(40px,4vw,64px);width:auto;flex:0 0 auto}
.u-wave svg{display:block;height:100%;width:auto}
/* as a section marker: the mark, then the eyebrow, on one baseline */
.u-mark-row{display:flex;align-items:center;gap:var(--s4);
  margin-bottom:var(--s4)}
.u-mark-row .s__eye{margin:0}

/* ---- THE SOUNDWAVE AS A LIST BULLET.
   A drawn mark, not a disc. The list loses its own markers and each item gets
   the icon at the icon's own floor, so a bullet is never a shrunk soundwave. */
.u-wavelist{list-style:none;margin:0;padding:0;display:grid;gap:var(--s4)}
.u-wavelist > li{display:grid;grid-template-columns:auto minmax(0,1fr);
  gap:var(--s4);align-items:start}
.u-wavelist .u-wave{height:40px}

/* ---- STACK-PORTRAIT LOGOS.
   The poster lockup, at poster scale. Four grounds ship as their own artwork,
   so the ground is chosen by picking the right file, never by recolouring. */
.u-stack{display:block;width:min(100%,var(--stack-w,320px))}
.u-stack u-svg,.u-stack svg{display:block;width:100%;height:auto}

/* ---- THE REPEAT-BANNER LOCKUP.
   Already built as .s-marquee above: the band paints its own pink and the
   marks are transparent over it, so a sub-pixel seam can only ever be more
   pink. This alias exists so a page asks for the lockup by name. */
.u-banner{--banner-h:64px}

/* ---- THE STICKER PLACEMENT HELPER.
   A sticker is stuck at a JOIN or in a CORNER, overlapping the edge, the way a
   real one is. NEVER on a grid, never with a radius, a ring or a background:
   the PNG carries its own die cut. Its container must not crop it, so every
   placement here is an overflow-visible absolute with room around it. */
.u-sticker{position:absolute;z-index:var(--z-overlay);pointer-events:none;
  width:var(--sticker-w,clamp(112px,12vw,168px))}
.u-sticker img{display:block;width:100%;height:auto}
/* at a section JOIN: it straddles the edge between two grounds */
[data-sticker="join"]{left:var(--gutter);bottom:0;transform:translateY(50%)}
/* in a CORNER: hard into the fold's top right, overlapping the gutter */
/* THE CORNER: hard into the fold's top right, overlapping the edge it is stuck
   over. It sits ABOVE the fold's content (z-overlay) because a sticker is stuck
   ON the page, not printed into it, and a sticker a headline paints over is not
   a sticker. It is rotated, which a sticker may be: the ban is on rotated TYPE,
   and this is an object. */
[data-sticker="corner"]{right:var(--gutter);top:0;
  transform:translateY(-34%) rotate(-9deg)}
/* the host of a sticker must not clip it */
/* a stacking fold is already a positioned box (sticky); `relative` here used to
   override it and silently stop that fold holding */
.u-sticker-host{overflow:visible}
.u-sticker-host:not([data-stack]){position:relative}

/* ---- THE MASCOT.
   LARGE OR NOT AT ALL: the floor is 150px and there is no ceiling. One per
   surface. Pink, coral or cream grounds only; the engine removes him from a
   black or yellow ground and says why, and test.py gates the same rule.
   There is deliberately no dark-ground modifier, and none is invented here. */
.u-mascot{display:block;width:var(--mascot-w,clamp(150px,22vw,340px));
  min-width:150px}
.u-mascot u-svg,.u-mascot svg{display:block;width:100%;height:auto}
/* his verb is BOUNCE, and it is a dance rather than an entrance: a slow,
   small, continuous step, on the transform channel only. */
@keyframes u-dance{
  0%,100%{transform:translate3d(0,0,0) rotate(-2deg)}
  50%{transform:translate3d(0,-3%,0) rotate(2deg)}
}
.u-mascot--dancing{animation:u-dance var(--dur-marquee) var(--ease-linear) infinite;
  /* the dance is slow: the marquee token is the system's one slow duration and
     this reads it rather than writing a new one. Divided down by the factor
     below so the step is a dance and not a drift. */
  animation-duration:calc(var(--dur-marquee) / 36)}

/* ==========================================================================
   THE DEMO CONTROL , the pre/onsale switch, rebuilt.
   It is not site UI and it must never read as site UI. It is now ONE tidy
   control at the end of the header row rather than a band across the page: a
   small labelled segmented switch, dotted-ruled so the eye reads it as an
   annotation OVER the page. <=48px tall on the phone, and still a real 44px
   target, because a control you cannot press is not discreet, it is broken.
   ========================================================================== */
/* IT IS A FLOATING TAB IN THE BOTTOM-LEFT CORNER, not a band across the top.
   It never covers content (it is in the corner, over the gutter), it never
   covers the phone's ticket bar (it is lifted by the bar's own --chrome-ticket
   token, which is where the bar's height already lives), and closed it is one
   small word. */
/* IN THE GUTTER, NOT ON IT (measured 21 Sep, second look). At left:var(--gutter)
   the tab started exactly where every fold's content column starts, so it sat on
   body copy and on the tickets hero's own "Join the waitlist" pill at 1440. It
   is pulled to the screen edge and held no wider than the gutter, so the only
   thing under it is margin. */
.s-demo{position:fixed;left:0;bottom:var(--s4);max-width:var(--gutter);
  z-index:var(--z-overlay);
  font:700 var(--size-micro)/1.2 var(--font);
  text-transform:uppercase;letter-spacing:.14em}
/* the disclosure triangle is chrome the system does not own; the tab is the
   whole affordance */
.s-demo__tab::-webkit-details-marker{display:none}
.s-demo__tab{list-style:none;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
  min-height:var(--tap-min);min-width:var(--tap-min);padding:0 var(--s1);
  max-width:var(--gutter);box-sizing:border-box;letter-spacing:.06em;
  background:var(--cream);color:var(--black);
  border:var(--rule) dashed var(--black);border-left:0;
  /* dashed, the way the row was: the eye reads an annotation OVER the page
     rather than a control the festival designed */
  white-space:nowrap}
.s-demo__tab:focus-visible{outline:var(--rule-heavy) solid var(--primary);
  outline-offset:var(--rule)}
/* open, the two states sit ABOVE the tab, so pressing it never moves the tab
   out from under the thumb that just pressed it */
.s-demo__body{position:absolute;left:0;bottom:calc(100% + var(--s2));
  display:flex;flex-direction:column;align-items:flex-start;gap:var(--s2);
  padding:var(--s3);width:max-content;max-width:calc(100vw - var(--gutter) * 2);
  background:var(--cream);color:var(--black);
  border:var(--rule) dashed var(--black)}
.s-demo__l{opacity:.55;white-space:normal;max-width:22ch}
.s-demo__switch{display:inline-flex;border:var(--rule) solid currentColor}
.s-demo__switch a{color:inherit;text-decoration:none;
  display:inline-flex;align-items:center;justify-content:center;
  min-height:var(--tap-min);min-width:var(--tap-min);
  padding:0 var(--s3);white-space:nowrap}
.s-demo__switch a + a{border-left:var(--rule) solid currentColor}
.s-demo__switch a[aria-current="true"]{background:var(--black);color:var(--cream)}
@media (max-width:640px){
  /* ABOVE THE TICKET BAR, never over it. The bar is fixed to the bottom edge
     and is --chrome-ticket tall, so the tab starts from there rather than from
     the viewport bottom.

     AND OUTBOARD OF THE GUTTER, so it does not land on the page's own content.
     Measured at 390: sitting on the gutter, the closed tab covered the left
     end of the "The full lineup" pill, which is a real control. It is pulled
     back to the screen edge and the closed tab is held to its thumb pad and no
     wider, so the only thing it ever overlaps is the margin. */
  .s-demo{bottom:calc(var(--chrome-ticket,84px) + var(--s3));left:0}
  .s-demo__tab{padding:0 var(--s2);
    border-left:0;opacity:.82}
  .s-demo[open] .s-demo__tab{opacity:1}
  .s-demo__body{left:var(--s2)}
}

/* ------------------------------------------------------------ REDUCED MOTION
   The final state, shown at once. Nothing here animates, so nothing here needs
   turning off, but the pinned stack must fall back to a readable stack.
   ---------------------------------------------------------------------------- */
@media (prefers-reduced-motion:reduce){
  .pin-stage.is-pinned{min-height:0;display:block}
  .pin-stage.is-pinned .pin-stage__panel{opacity:1;padding-block:var(--s7)}

  /* THE ENGINE, UNDER REDUCED MOTION: the final state, and NO PIN.
     The engine returns before creating a single ScrollTrigger, so these rules
     only have to undo the layout a pin would have wanted. A stacking section
     stops sticking (sticky is a position and not motion, but a stack read at
     rest is one screen per section, and that is a lot of scrolling for no
     movement), a fill host stops being a full screen, and a swap host is a
     plain stack with every slide visible. */
  [data-stack]{position:static}
  [data-fill]{min-height:0;padding-block:var(--s8)}
  [data-swap].is-pinned{min-height:0;display:block}
  [data-swap].is-pinned [data-swap-slide]{opacity:1;visibility:visible;
    padding-block:var(--s7)}
  .u-mascot--dancing{animation:none}
}


/* The phone ticket bar is lifted inside its spec wrapper, and the fixed host is
   a flex row, so the wrapper shrank to its content (362px of 390) and left a
   strip of page showing at the right. The wrapper and the bar take the full
   width of the host. */
.s-ticketbar > *{flex:1 1 100%;width:100%;max-width:none;min-width:0}
.s-ticketbar .n-ticketbar{width:100%;max-width:none;box-sizing:border-box}

/* the Demo tab's links exist only when it is open: closed, they are not rendered
   at all, so nothing invisible sits over the page waiting to be hit-tested. */
.s-demo:not([open]) .s-demo__body{display:none}

/* ---- THE STICKER AT THE SIDE OF A HERO.
   A hero is the first fold, so a CORNER sticker would climb over the header.
   This one is stuck in the bare half of the row instead, large, at an angle,
   and on a phone it drops to straddle the fold's bottom edge (a JOIN) so it
   never sits on the words. */
[data-sticker="side"]{--sticker-w:clamp(200px,22vw,340px);
  right:max(var(--gutter),9vw);top:50%;transform:translateY(-50%) rotate(8deg)}
@media (max-width:900px){
  [data-sticker="side"]{--sticker-w:112px;top:auto;bottom:0;right:var(--gutter);
    transform:translateY(45%) rotate(8deg)}
}

/* ==========================================================================
   ONE LEFT EDGE, AT EVERY WIDTH (Louis, 21 Sep: "if I can't stretch it to my
   full big monitor without stuff overlapping, we are not in the clear").
   Measured at 2560: every fold's content was capped at 1440 and centred while
   the header and the footer ran edge to edge on their own 24px padding, so the
   logo, the nav, the footer columns and back-to-top lined up with nothing. And
   at 1440 the header's 24px sat 32px outboard of the content's 56px gutter, so
   it was never aligned, only less visibly wrong.
   --measure is the page's one content width; --edge is the distance from the
   viewport to that content's left edge. The chrome pads itself by --edge, so
   the logo sits over the headline at 390, 1440 and 2560 alike.
   ========================================================================== */
:root{--measure:1440px;
  --edge:max(var(--gutter), calc((100vw - var(--measure)) / 2 + var(--gutter)))}
.s-head,
.n-foot__top,.n-foot__social,.n-foot__partners,.n-foot__legal{
  padding-left:var(--edge);padding-right:var(--edge)}

/* ==========================================================================
   THE POINTER LAYER. Measured 21 Sep: the site's own CSS carried no :hover rule
   and site.js no pointer listener, so outside the lifted components nothing on
   a page answered the mouse. Three answers, each one of the system's own verbs
   on the system's own tokens, fine pointers only, and none under reduced
   motion:
     BOUNCE  a block you can point at lifts off its ground on a hard shadow
     STAMP   a sticker presses when you touch it
     LEAN    the mascot leans towards the cursor (site.js, [data-lean])
   ========================================================================== */
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:no-preference){
  [data-lift]{transition:transform var(--dur-bounce) var(--ease-verb-bounce),
    box-shadow var(--dur-bounce) var(--ease-verb-bounce)}
  [data-lift]:hover{transform:translate3d(calc(var(--s2) * -1),calc(var(--s2) * -1),0);
    box-shadow:var(--s2) var(--s2) 0 var(--black);z-index:var(--z-raise);
    /* a lifted cell out of a joined row needs its own four edges */
    outline:var(--rule-heavy) solid var(--black);position:relative}
  .u-sticker img{pointer-events:auto;
    transition:transform var(--dur-stamp) var(--ease-verb-stamp)}
  .u-sticker img:hover{transform:scale(1.08) rotate(-6deg)}
  [data-lean] u-svg{transition:transform var(--dur-state) var(--ease-out);
    transform:translate3d(var(--lean-x,0),0,0) rotate(var(--lean-r,0deg));
    transform-origin:50% 100%}
}

/* the covered fold's content, while the next fold climbs over it (site.js) */
[data-stack] > .s__in{transform-origin:50% 0;will-change:auto}

/* A SHORT DESKTOP WINDOW (1280x720, 1366x768). The home hero's two actions
   measured 8 to 20px under the fold there. Nothing in the hero changes; the
   fold gives back the padding it does not have the height for. */
@media (min-width:1025px) and (max-height:820px){
  .hero{padding-block:var(--s5) var(--s7)}
}

/* A BIG MONITOR GETS A BIGGER PAGE. At 1920 and up a 1440 measure is a column
   in a field of colour. The measure steps to 1680 at 1920 and 1920 at 2400, so
   folds use the screen; --edge follows it, so the chrome stays on the content's
   edge at every step. */
@media (min-width:1920px){:root{--measure:1680px}}
@media (min-width:2400px){:root{--measure:1920px}}

/* every fold paints in document order (site.js sets --stack-i on each child of
   <main>), so a later fold always covers an earlier held one. `:where` keeps
   this at zero specificity: a held fold stays sticky, a sticker host stays what
   it was. */
:where(main > *){position:relative;z-index:calc(var(--z-body) + var(--stack-i,0))}
/* and <main> is its own stacking context, so however many folds a page has,
   their rising order stays INSIDE it and never climbs over the sticky header */
main{position:relative;z-index:var(--z-body);isolation:isolate}

/* THE DEMO TAB ON A PHONE IS NOT FLOATING (21 Sep). A phone has a 16px gutter and
   a 44px tab cannot hide in it: fixed, it sat over body copy and the first
   letters of buttons on every screen of every page. It is pinned to the END of
   the document instead, bottom left of the last black row, opposite back to
   top: one known place, over nothing. */
@media (max-width:640px){
  body{position:relative}
  .s-demo{position:absolute;left:var(--gutter);right:auto;max-width:none;
    /* the document already ends ABOVE the ticket bar (.s-totop reserves it), so
       this is measured from the back-to-top row's own padding and nothing else */
    bottom:var(--s5)}
  .s-demo__tab{opacity:1;border-left:var(--rule) dashed var(--black);padding:0 var(--s3)}
  .s-demo__body{left:0}
}
