@import url('fonts.css');

/* ============================================================================
   SWIRL WORLD — version 8, "SUNDAE BAR / ORBIT"
   Children's picture book on top of a Swiss grid.
   Mobile-first. Every length below is authored for a 390px viewport and
   allowed to grow; the display type is allowed to crop at the edges.
   ========================================================================= */

:root {
  /* --- Surface ------------------------------------------------------------
     Deep purple is the page. It carries everything that is not the hero.
     There is no white background anywhere; white appears only as card stock. */
  --purple:        #522394;
  --purple-deep:   #3B1769;
  --purple-lift:   #632CAF;

  /* --- Accents. Never more than three in one section. ------------------- */
  --sage:          #92D66F;
  --forest:        #195E1C;
  --forest-deep:   #0E3A10;
  --coral:         #F15B40;
  --orange:        #ED934E;
  --magenta:       #E72F63;
  --sky:           #88C1F8;
  --pink:          #F6B1CF;

  /* --- Ink --------------------------------------------------------------- */
  --cream:         #FFF4E4;
  --ink:           #23103F;   /* body text on light surfaces */
  --card:          #FFFFFF;   /* card stock only, never a page background */

  /* --- Type -------------------------------------------------------------- */
  --display: 'Luckiest Guy', 'Arial Black', sans-serif;
  /* Century Gothic first: it is installed on most Windows/Office machines and
     is the real brief face. Jost is the bundled OFL stand-in. */
  --text: 'Century Gothic', 'Jost', 'Questrial', system-ui, sans-serif;

  /* --- Shape. Everything is a pill or a soft-cornered square. ------------- */
  --r-pill: 999px;
  --r-card: 32px;
  --r-soft: 20px;

  /* --- Motion ------------------------------------------------------------
     --ease-swirl is Swirl World's own token, copied verbatim from the brand
     design system, where it is documented as "playful overshoot" and the
     house rule is that motion should feel like soft-serve: smooth,
     continuous, a little bouncy at the end. Generic design linters flag
     overshoot as dated; here it is the brand's motion signature and the
     brief asks the intro logo to bounce off a surface. Do not flatten it to
     an ease-out without the brand owner's say-so. */
  --ease-swirl: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* Page transition timing. The mark lands fractionally after the arc does,
     so the spin resolves last and the eye finishes on the header rather than
     on the sweep. */
  --nav-ms: 620ms;
  --nav-land-ms: 760ms;
  --ease-out:   cubic-bezier(0.22, 1, 0.36, 1);

  /* --- The live hero surface. Both rewritten by the flavour switcher.
     --mix-deep is a darker shade of the SAME hue, used for the flavour button
     so the control reads as belonging to the surface behind it. The rest of
     the page keeps forest green. */
  --mix-bg: var(--sage);
  --mix-deep: #3E7A22;

  /* Tall enough that the oversized mark clears the top edge completely.
     At 76px the logo was taller than the bar and clipped against the
     viewport. */
  --header-h: 96px;
}

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto; /* the scroll story must not be animated past */
  /* The display type and the wreath are MEANT to bleed past the edges. This
     is what stops that intent from becoming a sideways scrollbar. */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--purple);
  color: var(--cream);
  font-family: var(--text);
  font-size: 19px;
  line-height: 1.55;
  overflow-x: clip;
  /* the page is one continuous narrative; nothing here bounces */
  overscroll-behavior-y: none;
}

/* height:auto is load-bearing, not tidiness: the width/height attributes on
   the logo are presentational hints, and setting only `width` in CSS leaves
   the height hint in force — which gave the footer a 2801px logo. */
img, svg, canvas { display: block; max-width: 100%; }
img { height: auto; }
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }
a { color: inherit; text-decoration: none; }

:focus-visible {
  outline: 3px solid var(--cream);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Entry animations are gated on .js so the page is simply visible when
   scripting is off, rather than permanently faded out. */
.js .reveal { opacity: 0; transform: translateY(22px); }
.reveal { transition: opacity 500ms var(--ease-out), transform 500ms var(--ease-out); }
.reveal.is-in { opacity: 1; transform: none; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ============================================================================
   TYPE SCALE
   Display is uppercase, enormous, tracking normal, and allowed to crop.
   ========================================================================= */

.display {
  font-family: var(--display);
  text-transform: uppercase;
  font-weight: 400;
  letter-spacing: 0.005em;   /* Luckiest Guy is already tight; do not track it */
  line-height: 0.84;
  margin: 0;
  /* the picture-book ink line. This is what makes cream legible on sage
     without changing either brief-specified colour. */
  -webkit-text-stroke: 0;
  text-shadow:
    0 3px 0 rgba(14, 58, 16, 0.22),
    0 0 34px rgba(14, 58, 16, 0.16);
}

.eyebrow {
  font-family: var(--text);
  text-transform: uppercase;
  font-size: 16px;
  font-weight: 500;
  letter-spacing: 0.14em;
  margin: 0;
}

.body {
  font-size: 19px;
  line-height: 1.58;
  margin: 0;
  max-width: 34ch;         /* generous measure, never a full-bleed paragraph */
}

/* ============================================================================
   HEADER — transparent, overlays the hero.
   Three independent zones. The logo is positioned against the VIEWPORT, not
   against its flex siblings, so it is exactly centred whatever the sides do.
   ========================================================================= */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--header-h);
  /* Above the full-screen menu (60), not below it. The burger becomes the
     close control, so burying it under the panel it opened leaves Escape as
     the only way out. */
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 14px;
  pointer-events: none; /* re-enabled per control, so the hero stays draggable */
}
.header > * { pointer-events: auto; }

/* Transparent over the hero, as briefed. Past the hero it picks up a soft
   scrim, because a fixed bar with no backdrop collides with running copy. */
.header::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--purple) 0%, rgba(82, 35, 148, 0.86) 58%, rgba(82, 35, 148, 0) 100%);
  opacity: 0;
  transition: opacity 320ms var(--ease-out);
  pointer-events: none;
}
.is-scrolled .header::before { opacity: 1; }
/* With a menu open the panel is the surface — the header goes fully clear.
   Both pages put is-scrolled and is-locked on <body>; the doubled selector
   outranks the scrolled rule outright, and the scrim's own fade is cancelled
   so it cannot linger for 320ms over the falling bubbles. */
body.is-locked .header::before,
body.is-scrolled.is-locked .header::before {
  opacity: 0;
  transition: none;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* Simulated bubbles are hidden until the first frame has placed them above
   the viewport, so they fall in rather than popping into place. */
.bubs.is-sim:not(.is-placed) .bub { visibility: hidden; }

.burger {
  width: 52px; height: 52px;
  border-radius: var(--r-pill);
  display: grid;
  place-content: center;
  gap: 5px;
  transition: background 200ms var(--ease-out), transform 200ms var(--ease-swirl);
}
.burger span {
  display: block;
  width: 24px; height: 3px;
  border-radius: var(--r-pill);
  background: var(--cream);
  transition: transform 260ms var(--ease-swirl), opacity 160ms linear;
}
.burger:hover { background: rgba(255, 244, 228, 0.16); }
.burger:active { transform: scale(0.94); }
.burger[aria-expanded='true'] span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.burger[aria-expanded='true'] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded='true'] span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

.brand {
  position: absolute;
  left: 50%;
  /* nudged below the optical centre so the mark's own top clearance reads,
     rather than sitting tight against the viewport edge */
  top: 54%;
  transform: translate(-50%, -50%);
  width: 92px;              /* still the largest thing in the bar */
  pointer-events: auto;
}
.brand img { width: 100%; height: auto; }
/* During the intro the header mark is held invisible; the flying intro logo
   occupies this exact box and then hands over. */
.is-intro .brand { opacity: 0; }

.visit { position: relative; }
/* No pill, no fill: plain type in the bar. The header is transparent over the
   hero and the mark is the only object in it — a solid green capsule beside
   the logo was competing with it. */
.visit__btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 48px;
  padding: 0 10px;
  background: none;
  color: #fff;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: opacity 200ms var(--ease-out);
}
.visit__btn:hover { opacity: 0.7; }
.visit__caret { transition: transform 260ms var(--ease-swirl); }
.visit__btn[aria-expanded='true'] .visit__caret { transform: rotate(180deg); }

/* VISIT used to open a small dropdown pinned under its button while the
   burger opened the full screen — one site with two different ideas of what a
   menu is. Both are `.menu` now, so the rules that styled the dropdown are
   gone rather than left to rot. */

/* --- full-screen menus ---------------------------------------------------- */
.menu {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--purple-deep);
  padding: calc(var(--header-h) + 24px) 22px 40px;
  display: grid;
  align-content: start;
  gap: 4px;
  transition: opacity 300ms var(--ease-out), transform 300ms var(--ease-out);
}
.menu[hidden] { display: none; }
.menu.is-closed { opacity: 0; transform: translateY(-12px); }
.menu__item {
  font-family: var(--display);
  text-transform: uppercase;
  /* Sized to the LONGEST item, which is now "FUNDRAISING" at 6.11x the font
     size — not "SASKATOON" at 5.411x, which is what 15vw was set for. At 15vw
     the F and the G ran past both edges and the panel scrolled sideways. */
  font-size: clamp(1.9rem, 13.4vw, 4rem);
  line-height: 1.06;
  color: var(--cream);
  padding: 6px 0;
  transition: color 180ms var(--ease-out), transform 180ms var(--ease-swirl);
}
.menu__item:hover { color: var(--sage); transform: translateX(8px); }
.menu__note {
  margin-top: 26px;
  font-size: 18px;
  color: rgba(255, 244, 228, 0.7);
  max-width: 30ch;
}

/* The sitemap has a second level — Treats, and each city — so the panels need
   somewhere to put it. Sub-links are pills rather than more display type: at
   this size a second row of Luckiest Guy reads as another top-level choice
   and the hierarchy disappears. */
.menu__group { display: grid; gap: 2px; }
.menu__subs { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0 14px; }
.menu__minor { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.menu__sub {
  display: inline-block;
  padding: 8px 14px;
  border-radius: 999px;
  background: rgba(255, 244, 228, 0.14);
  color: var(--cream);
  font-size: 0.98rem;
  font-weight: 700;
  transition: background 160ms var(--ease-out), transform 160ms var(--ease-swirl);
}
.menu__sub:hover { background: var(--forest); transform: scale(1.04); }

/* The panels can now outgrow a short screen, so they scroll — and the page
   behind them must not, or the menu drags the page with it. */
.menu { overflow-y: auto; overscroll-behavior: contain; }
body.is-locked { overflow: hidden; }

/* ============================================================================
   THE STAGE — one WebGL canvas carrying the earth, the cup, the sprinkles
   and the cherry. Fixed, so the cup is welded to the viewport for the whole
   narrative. It sits above section text and below the orbit cards.
   ========================================================================= */

.stage {
  position: fixed;
  /* Sized to the LARGE viewport, not `inset: 0`. On iPhone Safari a fixed
     box with inset 0 is the SMALL viewport while the toolbar is up, so the
     canvas ended exactly at the toolbar's top edge: the cup's lower quarter
     was never painted behind the bar, and when the bar collapsed on scroll
     the canvas grew, the renderer re-framed 120ms later, and the cup jumped
     and stayed cut off. 100lvh is the toolbar-collapsed height and never
     changes while scrolling, so the cup is painted under the bar from the
     first frame, the bar simply slides off it, and nothing is re-measured
     mid-scroll. The hero's own "lower edge cuts the bottom quarter" framing
     is now solved against that stable edge. */
  inset: 0 0 auto 0;
  height: 100vh;
  height: 100lvh;
  z-index: 5;
  pointer-events: none;
}
/* The earth's own canvas, under the cards (4) while the cup's stays over
   them. The planet is scenery — the cards are read against it — but the cup
   is the product and leads everything. No z-index between 3 and 5 can put one
   canvas on both sides of the cards, so there are two. */
.stage--back { z-index: 3; }
/* New Year's / Canada Day bursts (main.js `fireworks`): over the hero and the
   cup, under the menus and the header. Removed from the DOM after 3s. */
.fireworks {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 40;
  pointer-events: none;
}
.stage canvas { width: 100%; height: 100%; }

/* The photoreal still does double duty: it is the poster that holds the hero
   while the 3.3MB mesh streams in, and it is the whole product when WebGL is
   unavailable. Same file either way — it costs 191KB and it means the hero is
   never empty. */
.cup-fallback {
  position: fixed;
  left: 50%;
  /* Anchored to the large viewport's lower edge (same reasoning as .stage),
     so the poster and the mesh it stands in for are cropped by the same
     line and neither moves when the Safari toolbar collapses. */
  top: calc(100vh + 12vh);
  top: calc(100lvh + 12lvh);
  width: min(72vw, 392px);
  transform: translate(-50%, -100%);
  z-index: 5;
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms var(--ease-out);
  filter: drop-shadow(0 24px 34px rgba(24, 8, 48, 0.34));
}
.is-cup-loading .cup-fallback { opacity: 1; }
.no-webgl .cup-fallback { opacity: 1; transition: none; }
.no-webgl .stage { display: none; }

/* ============================================================================
   HERO
   ========================================================================= */

.hero {
  position: relative;
  min-height: 100svh;
  background: var(--mix-bg);
  transition: background 620ms var(--ease-out);
  display: grid;
  /* Content sits above centre: the lower third belongs to the product. */
  align-content: start;
  padding: calc(var(--header-h) + 5svh) 0 0;
  overflow: clip;
  /* No `isolation: isolate` here. It would scope the hero's children into
     their own stacking context, which put the bubbles under the fixed canvas
     no matter what z-index they carried — the cup drew straight over their
     text. Flat ordering lets wreath (1) and headline (3) sit behind the cup
     (5) while the bubbles (6) stay in front of it. */
}

.hero__head {
  position: relative;
  z-index: 3;
  text-align: center;
  color: var(--cream);
}
/* The display type is allowed to kiss the edges — that is the look — but a
   line that loses a whole letter reads as breakage, not as a crop. These
   sizes are set so the longest line just touches at 375px. */
/* Sized to FIT, not to crop. The binding line is "CANADA'S", whose advance in
   Luckiest Guy is 4.408x the font size; 21vw of 390px is 82px, giving 361px
   inside a 390px viewport. Every other line is shorter, so one size serves
   them all. The svh term takes over on wide, short viewports, where 21vw
   would push the fourth line below the fold. */
.hero__line {
  display: block;
  white-space: nowrap;
  /* The svh term binds only on wide, short viewports — on a phone 20vw always
     wins, so this number is effectively the desktop size. At 12.2 the four
     lines ate 66% of the height and left the cup nowhere to sit. */
  font-size: clamp(2.6rem, min(20vw, 9.6svh), 15rem);
}

/* --- thought bubbles -----------------------------------------------------
   Two per mix, one each side at different heights, flanking the product.
   Deep purple on cream: these are the only small text in the hero, so they
   carry the contrast the display type is allowed to spend on scale. */
.bubbles {
  position: absolute;
  inset: 0;
  z-index: 6;   /* in front of the cup: copy is never occluded by product */
  pointer-events: none;
}

.bubble {
  position: absolute;
  /* --btop is set per bubble by the switcher. The scale and offset let a
     breakpoint move the pair AND tighten the gap between them without
     JavaScript having to know anything about viewports — on desktop the band
     between the headline and the carousel is far shallower than on a phone,
     so the two cannot stay ten points apart. */
  top: calc(var(--btop, 54%) * var(--bscale, 1) + var(--bshift, 0%));
  /* Narrow enough that neither bubble crosses the swirl, which widens as it
     descends: the left one sits where the cone is ~8% of the viewport wide,
     the right one slightly lower where it is still under 15%. */
  width: min(44vw, 232px);
  margin: 0;
  padding: 13px 16px 14px;
  border-radius: 24px;
  background: var(--cream);
  color: var(--purple);
  font-size: clamp(15px, 4.4vw, 20px);
  font-weight: 700;
  line-height: 1.18;
  text-align: center;
  text-wrap: balance;
  box-shadow: 0 12px 26px rgba(24, 8, 48, 0.18);
  animation: illo-bob 6.8s var(--delay, 0s) ease-in-out infinite alternate;
}
/* the two trailing dots that make it a thought bubble rather than a label */
.bubble::before,
.bubble::after {
  content: '';
  position: absolute;
  border-radius: 50%;
  background: var(--cream);
}
.bubble::before { width: 14px; height: 14px; bottom: -9px; }
.bubble::after  { width: 8px;  height: 8px;  bottom: -22px; }

.bubble--l { left: 3%; }
.bubble--l::before { right: 20px; }
.bubble--l::after  { right: 10px; }

.bubble--r { right: 3%; }
.bubble--r::before { left: 20px; }
.bubble--r::after  { left: 10px; }

@media (prefers-reduced-motion: reduce) {
  .bubble { animation: none; }
}

/* --- the wreath ----------------------------------------------------------
   Ten hand-placed slots. Not a grid: every value below was set by eye, and
   the middle band is deliberately left clear for the product. */
.wreath {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
/* Three nested elements, one transform channel each — none of them can be
   collapsed. The OUTER .illo is what JavaScript moves for scroll parallax;
   .illo__drop carries the one-shot drop-in; the INNER art carries the endless
   idle animation. Sharing an element means the parallax write clobbers the
   keyframes every frame, or the drop and the idle loop overwrite each other. */
.illo {
  position: absolute;
  width: min(var(--w), 252px);
  left: var(--x);
  top: var(--y);
}

.illo__drop {
  display: block;
  /* Held at the from-state until the wreath is released, then each piece runs
     on its own delay. --drop-span compresses the scatter for flavour changes,
     where a two-second cascade would just feel broken. */
  animation: illo-drop 620ms calc(var(--drop-delay, 0s) * var(--drop-span, 1)) both var(--ease-out);
  animation-play-state: paused;
}
.wreath.is-in .illo__drop { animation-play-state: running; }

@keyframes illo-drop {
  from { opacity: 0; translate: 0 -54px; scale: 0.82; }
  62%  { opacity: 1; }
  to   { opacity: 1; translate: 0 0; scale: 1; }
}

.illo__drop > * {
  display: block;
  width: 100%;
  transform: rotate(var(--r, 0deg));   /* the hand-placed tilt */
  transform-origin: 50% 50%;
  animation-duration: var(--dur, 24s);
  animation-delay: var(--delay, 0s);
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform, rotate, translate;
}

/* Slow continuous turn. */
.illo[data-anim='spin'] .illo__drop > * { animation-name: illo-spin; }
@keyframes illo-spin { to { rotate: 360deg; } }

/* Drifts right across the hero and back, breathing scale as it goes. */
.illo[data-anim='drift'] .illo__drop > * {
  animation-name: illo-drift;
  animation-direction: alternate;
  animation-timing-function: ease-in-out;
}
@keyframes illo-drift {
  from { translate: 0 0; rotate: -8deg; }
  to   { translate: var(--travel, 46vw) -6vh; rotate: 8deg; }
}

/* Mostly still, then a sudden flip — the surprise is in the long pause. */
.illo[data-anim='flip'] .illo__drop > * { animation-name: illo-flip; }
@keyframes illo-flip {
  0%, 62%   { transform: rotate(var(--r, 0deg)) rotateY(0deg); }
  78%       { transform: rotate(var(--r, 0deg)) rotateY(180deg); }
  94%, 100% { transform: rotate(var(--r, 0deg)) rotateY(360deg); }
}

/* Gentle float, the quiet majority of the wreath. */
.illo[data-anim='bob'] .illo__drop > * {
  animation-name: illo-bob;
  animation-direction: alternate;
  animation-timing-function: ease-in-out;
}
@keyframes illo-bob {
  from { translate: 0 0;      rotate: -5deg; }
  to   { translate: 0 -2.6vh; rotate: 5deg; }
}

/* --- flavour switcher ----------------------------------------------------- */
.mixer {
  position: fixed;
  left: 50%;
  bottom: calc(18px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  width: min(76vw, 380px);
  transition: opacity 360ms var(--ease-out), transform 360ms var(--ease-out);
}
.mixer.is-gone { opacity: 0; transform: translate(-50%, 26px); pointer-events: none; }

.mixer__label {
  flex: 1 1 auto;
  min-width: 0;
  height: 58px;
  padding: 0 18px;
  border-radius: var(--r-pill);
  background: var(--mix-deep);
  color: var(--cream);
  display: grid;
  place-content: center;
  text-align: center;
  font-family: var(--display);
  text-transform: uppercase;
  /* Sized from the measured advances: the widest label, "What's your mix?",
     is 8.606x the font size, and the pill has 261px of inner width at 390px —
     so 7.2vw sets it just inside a single line with margin to spare. */
  font-size: clamp(1.1rem, 7.2vw, 2.4rem);
  letter-spacing: 0.01em;
  /* Still allowed to wrap rather than truncate, in case a future label runs
     long — these are brand lines, not UI strings. */
  line-height: 0.98;
  text-wrap: balance;
  box-shadow: 0 10px 26px rgba(18, 8, 32, 0.34);
  transition: transform 200ms var(--ease-swirl), background 400ms var(--ease-out), filter 200ms var(--ease-out);
}
.mixer__label:hover { transform: scale(1.03); filter: brightness(0.9); }

/* --- flavour carousel ----------------------------------------------------
   The mixes either side of the live one, small, each carrying its own change
   arrow. Fixed like the label, and floating on the same rhythm as the cup so
   the three read as one object rather than as product plus chrome. */
.carousel {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(82px + env(safe-area-inset-bottom, 0px));
  z-index: 8;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  padding: 0 2vw;
  pointer-events: none;
  transition: opacity 360ms var(--ease-out), transform 360ms var(--ease-out);
}
.carousel.is-gone { opacity: 0; transform: translateY(26px); pointer-events: none; }

.carousel__side {
  position: relative;
  /* smaller than it looks like it should be: the lower bubble now sits at 62%
     and needs the room above these */
  width: min(21vw, 104px);
  pointer-events: auto;
  filter: drop-shadow(0 14px 20px rgba(24, 8, 48, 0.34));
  transition: transform 220ms var(--ease-swirl);
  animation: cup-float 7.6s ease-in-out infinite alternate;
}
.carousel__side--next { animation-delay: -3.4s; }
.carousel__side:hover { transform: scale(1.06); }
.carousel__side:active { transform: scale(0.95); }
.carousel__cup { width: 100%; opacity: 0.94; }

@keyframes cup-float {
  from { translate: 0 0; }
  to   { translate: 0 -11px; }
}

.carousel__arrow {
  position: absolute;
  left: 50%;
  top: 54%;
  transform: translate(-50%, -50%);
  width: 44px;
  height: 44px;
  border-radius: var(--r-pill);
  display: grid;
  place-content: center;
  background: var(--cream);
  color: var(--mix-deep);
  box-shadow: 0 6px 16px rgba(24, 8, 48, 0.32);
  transition: background 200ms var(--ease-out), color 200ms var(--ease-out);
}
.carousel__side:hover .carousel__arrow { background: var(--mix-deep); color: var(--cream); }
.carousel__arrow svg { width: 21px; height: 21px; }

@media (prefers-reduced-motion: reduce) {
  .carousel__side { animation: none; }
}

/* ============================================================================
   EXPLAINER — the brand's voice.
   ========================================================================= */

.explainer {
  position: relative;
  z-index: 3;
  background: var(--purple);
  /* Tightened at the foot: the headline used to be followed by most of a
     screen of empty purple before the first card arrived. */
  padding: clamp(84px, 22vw, 168px) 20px clamp(36px, 9vw, 76px);
  text-align: center;
  --swell: var(--purple);
}

/* Section transitions are a wide, shallow convex arc — the top of a very big
   circle — so one colour block swells up into the one above it. */
.swell { position: relative; }
.swell::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 1px;
  /* Just wider than the viewport, so the ellipse's widest point lands at the
     screen edges and the whole crown is visible: the arc rises from nothing
     at both sides to its apex at centre. At 190% only the flat top of a very
     large ellipse showed, and the transition read as a straight edge. */
  width: 104%;
  /* Height is capped rather than tied to the width by aspect-ratio: at 1280px
     a 3.6:1 ellipse was 370px tall and swallowed the bottom of the hero. */
  height: clamp(96px, 28.7vw, 168px);
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--swell);
  z-index: -1;
}

/* Binding line is "SWIRL AROUND" at 6.81x the font size: 13.6vw of 390px is
   53px, giving 361px inside the viewport. */
.explainer__head {
  color: var(--cream);
  font-size: clamp(2rem, min(13.6vw, 11svh), 9rem);
}
/* Two lines, as briefed, each held on one line. */
.explainer__head span {
  display: block;
  white-space: nowrap;
}
.explainer__body {
  margin: 26px auto 0;
  color: rgba(255, 244, 228, 0.82);
  text-align: center;
}

/* ============================================================================
   ORBIT — four cards ride a shallow arc around a very large earth.
   The section is tall; its inner stage is sticky, so scrolling moves the
   cards along the arc instead of moving the page furniture.
   ========================================================================= */

.orbit {
  position: relative;
  /* 340svh across five cards is ~68svh each. At 460 the gap between cards was
     a long stretch of empty purple. */
  height: 340svh;
  background: var(--purple);
}
.orbit__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: clip;
  /* `position: sticky` creates a stacking context whatever its z-index, so
     the cards inside are trapped in it and this value orders the whole lot at
     once — their own z-index only sorts them against each other.

     4 puts them UNDER the canvas at 5, so the cup passes in front of the
     cards. This used to be 6 to keep the falling sprinkles off the card copy;
     the cup leading is now the point, and the sprinkles come with it. */
  z-index: 4;
}

/* Toppings orbiting with the earth, in the band above the cards.
   z-index 2 keeps them right at the back — under the earth (3), the cards (4)
   and the cup (5), so all three pass in front of them. They are atmosphere,
   not content. Fixed and outside the orbit stage, because the stage's sticky
   stacking context would otherwise trap them above the canvas whatever
   z-index they carried. */
.orbit__toppings {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  visibility: hidden;
}
.orbit__topping {
  position: absolute;
  left: 50%;
  top: 0;
  width: min(17vw, 104px);
  margin-left: min(-8.5vw, -52px);
  will-change: transform, opacity;
}
.orbit__topping img { width: 100%; }

.card {
  position: absolute;
  left: 50%;
  top: 34%;
  z-index: 6;
  /* One source of truth for the width, and the offset derived from it. The
     two used to be written separately as `min(76vw,340px)` and
     `min(-38vw,-170px)`, which disagree wherever the two min() arms swap:
     at 390px that put the card 22px left of centre, so its neighbours peeked
     by different amounts on each side. */
  --card-w: min(76vw, 340px);
  width: var(--card-w);
  margin-left: calc(var(--card-w) / -2);
  padding: 26px 24px 28px;
  border-radius: var(--r-card);
  background: var(--card);
  color: var(--ink);
  box-shadow: 0 26px 54px rgba(18, 6, 40, 0.44);
  will-change: transform, opacity;
}
.card__tab {
  display: inline-block;
  padding: 7px 15px;
  border-radius: var(--r-pill);
  background: var(--forest);
  color: var(--cream);
  font-size: 15px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.card__head {
  margin: 16px 0 0;
  font-family: var(--display);
  text-transform: uppercase;
  font-size: clamp(1.9rem, 8.4vw, 2.7rem);
  line-height: 0.92;
  color: var(--purple);
  text-shadow: none;
}
.card__body {
  margin: 13px 0 0;
  font-size: 18px;
  line-height: 1.5;
  color: #3B2C52;
}

/* ============================================================================
   FOOTER — the narrative ends and hands off to a static grid.
   ========================================================================= */

.footer {
  position: relative;
  z-index: 7;
  background: var(--forest);
  color: var(--cream);
  padding: clamp(80px, 22vw, 150px) 22px calc(38px + env(safe-area-inset-bottom, 0px));
  --swell: var(--forest);
}
.footer__grid {
  display: grid;
  gap: 34px;
  max-width: 900px;
  margin: 0 auto;
}
.footer__mark { width: 116px; }
.footer__line {
  font-family: var(--display);
  text-transform: uppercase;
  /* "SEE YOU" is 3.522x; 19vw of 390px is 74px = 261px in a 346px column */
  font-size: clamp(2rem, 19vw, 5rem);
  line-height: 0.92;
  margin: 0;
}
.footer__nav { display: flex; flex-wrap: wrap; gap: 10px; }
.footer__pill {
  padding: 11px 20px;
  border-radius: var(--r-pill);
  background: rgba(255, 244, 228, 0.14);
  font-size: 17px;
  transition: transform 200ms var(--ease-swirl), background 200ms var(--ease-out), color 200ms var(--ease-out);
}
.footer__pill:hover { transform: scale(1.03); background: var(--cream); color: var(--forest); }
.footer__legal {
  font-size: 15px;
  color: rgba(255, 244, 228, 0.62);
  line-height: 1.5;
}
/* The allergen policy is the one link in the legal block that someone may be
   reading for a health reason, so it is underlined and at full strength
   rather than sharing the muted colour around it. */
.footer__legal-link {
  color: var(--cream);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.footer__legal-link:hover { text-decoration-thickness: 2px; }

/* ============================================================================
   INTRO — logo spins twice, bounces, and rises into the header. Under 2s.
   ========================================================================= */

.intro {
  position: fixed;
  inset: 0;
  z-index: 95;
  background: var(--purple);
  display: grid;
  place-items: center;
  perspective: 1200px;
}
.intro[hidden] { display: none; }
.intro__logo {
  width: 250px;
  max-width: 62vw;
  transform-style: preserve-3d;
  will-change: transform;
}
/* the surface the mark bounces off — a shallow cream arc, briefly visible */
.intro__floor {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 340px;
  max-width: 84vw;
  aspect-ratio: 4 / 1;
  border-radius: 50%;
  background: rgba(255, 244, 228, 0.14);
  transform: translate(-50%, 128px) scaleX(0.2);
  opacity: 0;
}

/* ============================================================================
   RESPONSIVE — the page is authored for mobile. Above 720px it simply gets
   more air; the composition does not change, because the composition is the
   point.
   ========================================================================= */

@media (min-width: 720px) {
  /* Still taller than the mark — the logo must not clip — but no taller than
     it needs to be: on a short landscape viewport every pixel the header
     takes is a pixel the product cannot have. */
  :root { --header-h: 112px; }
  body { font-size: 21px; }
  .brand { width: 118px; }
  .visit__btn { font-size: 19px; }
  .hero { padding-top: calc(var(--header-h) + 2.5svh); }
  .body, .card__body { font-size: 21px; }
  .card { --card-w: 400px; top: 30%; }
  .mixer { width: min(56vw, 460px); }
  .mixer__label { height: 64px; }
  /* Smaller and lower than the phone's proportions, to open up the band
     between the headline and the carousel that the bubbles have to live in. */
  .carousel { padding: 0 4vw; bottom: calc(40px + env(safe-area-inset-bottom, 0px)); }
  .carousel__side { width: min(12vw, 132px); }
  .carousel__arrow { width: 54px; height: 54px; }
  .carousel__arrow svg { width: 25px; height: 25px; }
  /* On a wide viewport the bubbles have to rise or they land on the carousel.
     They are also pinned to the CENTRE rather than the viewport edges: at
     `left/right: 3%` on a 1280px screen they drifted to the far margins and
     lost all relationship to the product. Anchoring them 90px either side of
     centre puts their inner edge just inside the cup, the same slight overlap
     they have on a phone. */
  /* Wider so each claim sets in two lines rather than three — a shorter
     bubble is what makes the shallow desktop band workable at all. */
  .bubble { width: min(30vw, 340px); --bscale: 0.86; --bshift: 8.6%; }
  /* 11svh, not a fixed pixel offset: the cup is sized from the viewport
     HEIGHT, so its half-width is ~13.7svh. Anchoring in the same unit keeps
     the same slight overlap on a short laptop and a tall monitor alike. */
  .bubble--l { left: auto; right: calc(50% + 11svh); }
  .bubble--r { right: auto; left: calc(50% + 11svh); }
}

@media (min-width: 1100px) {
  :root { --header-h: 118px; }
  .brand { width: 122px; }
}

/* Short landscape windows. Below about 700px of height the band between the
   headline and the carousel is too shallow for two staggered bubbles, so the
   carousel gives up some size and the pair sits closer together. Their
   stagger narrows, but nothing collides. */
@media (min-width: 720px) and (max-height: 700px) {
  .carousel { bottom: calc(28px + env(safe-area-inset-bottom, 0px)); }
  .carousel__side { width: min(8vw, 88px); }
  .carousel__arrow { width: 42px; height: 42px; }
  .carousel__arrow svg { width: 19px; height: 19px; }
  .bubble { width: min(26vw, 290px); --bscale: 0.65; --bshift: 21%; }
}

/* Reduced motion: the scroll story stays — it is user-driven, and it is the
   page — but nothing moves on its own. The intro is skipped outright. */
@media (prefers-reduced-motion: reduce) {
  .reveal, .illo { transition-duration: 1ms !important; }
  .js .reveal { opacity: 1; transform: none; }
  .illo__drop { animation: none !important; opacity: 1; }
  .illo > * > * { animation: none !important; }
}

/* ============================================================================
   PAGE TRANSITIONS
   ----------------------------------------------------------------------------
   Cross-document view transitions: no framework, no SPA, still a static site.
   Browsers without support navigate instantly, which is a fallback rather
   than a breakage.

   THE VIEW-MASTER
   The page is pinned to a large invisible wheel whose hub sits below the
   bottom of the screen. Taking a link turns the wheel: the page you were on
   swings off to the left and the next one comes round from the right on the
   same arc. The header does not travel with it — it is the housing, the wheel
   turns behind it.

   `mix-blend-mode: normal` is load-bearing and not tidiness. The UA default
   is `plus-lighter` on both snapshots, which is right for the cross-fade it
   ships with and wrong for everything else: with both layers at full opacity
   the colours ADD, so a purple page over a sage one blows out to near-white
   and the whole transition reads as a flash rather than as movement. That is
   what hid the first version of this.
   ========================================================================= */
@view-transition { navigation: auto; }

/* The gaps the wheel opens at the corners show this, so it wants to be the
   colour of the page arriving — which is what it is, because the incoming
   document's stylesheet owns the pseudo tree. */
::view-transition { background: var(--surface, var(--purple)); }

/* --- the header is the housing -------------------------------------------
   Lifted into its own group so the wheel turns behind it instead of taking
   it along. Its new snapshot is live, which is what lets the logo carry on
   spinning through the transition rather than freezing mid-turn. */
.header { view-transition-name: chrome; }
::view-transition-group(chrome) { animation-duration: var(--nav-ms); }
::view-transition-old(chrome),
::view-transition-new(chrome) { animation: none; mix-blend-mode: normal; }
::view-transition-old(chrome) { opacity: 0; }

/* --- the wheel ------------------------------------------------------------
   Hub half a viewport below the bottom edge, so the arc is wide and the
   motion reads as rotation rather than as a slide.

   Both numbers move with the shape of the screen. The horizontal distance a
   point travels is R·sinθ, and R is measured in viewport HEIGHTS while the
   distance it has to cover is a viewport WIDTH — so one fixed angle that
   clears a phone leaves a laptop barely twitching.

   Each pair is solved so a point at the middle of the screen travels about
   one screen-width — far enough to clear, not so far that the page is flung.
   R is the hub distance minus the half-viewport, and travel is R·sinθ:

     phone   430x932   R = 1.0H =  932px, sin26 = 0.44 →  408px = 0.95w
     tablet  768x1024  R = 1.6H = 1638px, sin28 = 0.47 →  768px = 1.00w
     laptop  1440x900  R = 3.3H = 2970px, sin32 = 0.53 → 1574px = 1.09w

   The hub goes DEEPER on wide screens rather than the angle going steeper:
   a bigger wheel covers the extra width at a gentler tilt, and past about 40
   degrees a full-width rectangle stops reading as a turning wheel and starts
   reading as the page falling over. */
:root { --nav-hub: 150%; --nav-swing: 26deg; }
@media (min-aspect-ratio: 3/4) { :root { --nav-hub: 210%; --nav-swing: 28deg; } }
@media (min-aspect-ratio: 1/1) { :root { --nav-hub: 380%; --nav-swing: 32deg; } }

::view-transition-group(root) { animation-duration: var(--nav-ms); }
::view-transition-old(root),
::view-transition-new(root) {
  mix-blend-mode: normal;
  transform-origin: 50% var(--nav-hub);
  backface-visibility: hidden;
}
::view-transition-old(root) {
  animation: wheel-out var(--nav-ms) cubic-bezier(0.5, 0, 0.75, 0.4) forwards;
}
::view-transition-new(root) {
  animation: wheel-in var(--nav-ms) cubic-bezier(0.2, 0.85, 0.3, 1) forwards;
}

@keyframes wheel-out {
  from { transform: rotate(0deg); opacity: 1; }
  to   { transform: rotate(calc(var(--nav-swing) * -1)); opacity: 0; }
}
@keyframes wheel-in {
  from { transform: rotate(var(--nav-swing)); opacity: 0; }
  30%  { opacity: 1; }
  to   { transform: rotate(0deg); opacity: 1; }
}

/* Back turns the wheel the other way, so it unwinds rather than repeats. */
.is-nav-back::view-transition-old(root) { animation-name: wheel-out-back; }
.is-nav-back::view-transition-new(root) { animation-name: wheel-in-back; }
@keyframes wheel-out-back {
  from { transform: rotate(0deg); opacity: 1; }
  to   { transform: rotate(var(--nav-swing)); opacity: 0; }
}
@keyframes wheel-in-back {
  from { transform: rotate(calc(var(--nav-swing) * -1)); opacity: 0; }
  30%  { opacity: 1; }
  to   { transform: rotate(0deg); opacity: 1; }
}

/* --- headline hand-off ----------------------------------------------------
   The menu items and the page titles are the same words in the same typeface
   at different sizes, so the word you tapped can simply travel into place.
   The name is assigned to the tapped item at click time by js/transition.js —
   it cannot live on every menu item at once, because a view-transition-name
   has to be unique in the document. */
.page__main > .page__head:first-child .page__title { view-transition-name: headline; }
/* NOT on the pages that open with the globe. A named element is snapshotted
   on its own, as if it were the root of a stacking context — so an ancestor's
   opacity does not reach it. The location pages hold `.page__main` at opacity
   0 behind the arrival, but the title's own snapshot still played in at full
   strength over the turning globe for the length of the arc, then vanished
   with the transition: the "title flashes above the globe" bug. Left in the
   root snapshot it is hidden with everything else until the arrival lifts. */
.page--film .page__main > .page__head:first-child .page__title { view-transition-name: none; }
::view-transition-group(headline) {
  animation-duration: var(--nav-ms);
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
/* Cross-fade the two words as one morphs into the other. Without this the
   old word holds full opacity all the way and you see two headlines. */
::view-transition-old(headline) { animation: nav-word-out calc(var(--nav-ms) * 0.55) ease forwards; }
::view-transition-new(headline) { animation: nav-word-in var(--nav-ms) ease forwards; }
@keyframes nav-word-out { to { opacity: 0; } }
@keyframes nav-word-in { from { opacity: 0; } 60% { opacity: 1; } }

/* --- the mark spins -------------------------------------------------------
   Starts the instant a link is taken, on the page being left, and lands on
   the page being arrived at. Because the two are separate documents the spin
   cannot be one continuous animation, so it is two: a fast loop that runs out
   the old page's life, and a decelerating three turns that finishes square as
   the arc completes. A whole number of turns matters — anything else lands
   the logo crooked. */
.brand { perspective: 900px; }
.brand img { backface-visibility: visible; }
.brand img.is-spinning { animation: mark-spin 420ms linear infinite; }
.brand img.is-landing { animation: mark-land var(--nav-land-ms) cubic-bezier(0.14, 0.8, 0.2, 1) forwards; }
@keyframes mark-spin { to { transform: rotateY(360deg); } }
@keyframes mark-land { from { transform: rotateY(0deg); } to { transform: rotateY(1080deg); } }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  .brand img.is-spinning, .brand img.is-landing { animation: none !important; }
}

/* --- VISIT: three bubbles -------------------------------------------------
   The panel behind the word VISIT. Three shops as three floating bubbles, a
   green strip of policy links across the foot, and nothing else — no lede, no
   addresses, nothing that needs a scrollbar. The whole thing is sized to fit
   one screen because a navigation panel you have to scroll is a navigation
   panel that has failed.

   The photographs are the same ones the location pages open on and the live
   OPEN/CLOSED is the same clock the store banner reads, so this stays a view
   of the site rather than a second design. */
.menu--visit {
  /* The base .menu scrolls. This one must not, ever. */
  overflow: hidden;
  display: grid;
  grid-template-rows: 1fr auto;
  gap: 0;
  padding: var(--header-h) 0 0;
  align-content: stretch;
}

/* On a phone the bubbles need every pixel of height they can get, so the play
   area runs up behind the header rather than starting below it. The header is
   transparent over this panel and sits above it, so a bubble drifting under
   the logo reads as depth; its controls stay on top and stay clickable. */
@media (max-width: 46rem) {
  .menu--visit { padding-top: 10px; }
}

/* The play area. Bubbles are positioned inside it in pixels by the simulation
   in js/site.js, so it needs to be a positioned box of known size. */
.bubs {
  position: relative;
  overflow: hidden;
  /* Diameter, capped against BOTH axes so three still fit a short laptop
     window in landscape, where height is the scarce dimension.

     The floor is what does the work on a phone: 30vw of a 390px screen is
     only 117px, so the clamp's minimum is the mobile size and the vw/svh terms
     take over from about a tablet up. */
  --d: clamp(248px, min(62vw, 38svh), 380px);
}

.bub {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--d);
  height: var(--d);
  border-radius: 50%;
  overflow: hidden;
  /* The accent survives from the previous pass as a ring — one saturated
     colour per city, now following the circle. */
  border: 5px solid var(--accent);
  box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.8);
  /* Positioned by transform rather than left/top: it is the only property the
     compositor can move without laying the page out again, and this runs at
     60fps with three of them on screen. */
  will-change: transform;
  /* The whole bubble is draggable, so the browser must not claim the gesture
     first. `touch-action` stops it becoming a scroll or a pinch; the two
     user-drag rules stop something subtler — a bubble contains a link and an
     image, and dragging either of those starts the browser's OWN drag-and-drop.
     That fires `pointercancel` a few frames in, the capture is lost, and the
     bubble stops dead halfway through the throw. */
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}
.bub a, .bub img { -webkit-user-drag: none; }
.bub.is-drag { cursor: grabbing; z-index: 2; }
.bub.is-drag .bub__shot { transform: scale(1.03); }
.bub__shot { transition: transform 220ms var(--ease-out); }
.bub__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 40%;
}

/* The upper three quarters: the city and whether it is open, centred. The
   wash is what makes the type legible over a photograph that changes from
   shop to shop — the alternative is hoping every storefront happens to be
   dark behind the words. */
.bub__main {
  position: absolute;
  inset: 0 0 25% 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 6px;
  padding: 0 12%;
  text-align: center;
  color: var(--cream);
  /* No wash over the shopfront (Tim, Aug 2026): the photograph shows at full
     brightness, exactly like the main-menu circles, and the city name earns
     its legibility from the same heavy drop shadow those labels carry. The
     OPEN/CLOSED badge keeps its own dark pill. */
  background: none;
}
.bub__city {
  font-family: var(--display);
  font-size: clamp(1.05rem, calc(var(--d) * 0.135), 2rem);
  line-height: 1;
  text-transform: uppercase;
  text-shadow:
    0 2px 3px rgba(0, 0, 0, 0.9),
    0 4px 10px rgba(0, 0, 0, 0.7),
    0 8px 26px rgba(0, 0, 0, 0.55);
}

/* The lower quarter, as a chord of the circle: the parent clips to a circle,
   so a full-width block pinned to the bottom takes the shape of the bottom
   cap without any clip-path of its own. */
.bub__go {
  position: absolute;
  inset: auto 0 0 0;
  height: 25%;
  display: grid;
  place-items: center;
  padding-bottom: 4%;
  background: var(--sage);
  color: var(--forest);
  font-weight: 700;
  font-size: clamp(0.72rem, calc(var(--d) * 0.072), 1rem);
  transition: filter 180ms var(--ease-out);
}
.bub__go:hover, .bub__go:focus-visible { filter: brightness(1.1); }

.status--mini {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--r-pill, 999px);
  background: rgba(24, 8, 48, 0.62);
  font-size: clamp(0.62rem, calc(var(--d) * 0.058), 0.82rem);
  font-weight: 700;
}
.status--mini .status__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: 0.5; }
.status--mini.is-open { color: var(--sage); }
.status--mini.is-shut { color: var(--coral); }
.status--mini.is-open .status__dot,
.status--mini.is-shut .status__dot { opacity: 1; }

/* Policy links: a green strip the full width of the viewport, dark green on
   hero green. Deliberately a different material from the bubbles — these are
   documents, not destinations. */
.visit-strip {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  padding: clamp(12px, 2.2vh, 20px) clamp(18px, 4vw, 48px);
  background: var(--sage);
}
.visit-strip a {
  color: var(--forest);
  font-weight: 700;
  font-size: clamp(0.86rem, 2.4vw, 1.05rem);
  text-underline-offset: 3px;
}
.visit-strip a:first-child { text-align: left; }
.visit-strip a:last-child { text-align: right; }
.visit-strip a:hover { text-decoration: underline; }

/* Reduced motion gets the same three bubbles, laid out and still. The
   simulation never starts — see js/site.js — so this is the whole layout, not
   a first frame waiting to move. */
@media (prefers-reduced-motion: reduce) {
  .bubs {
    display: flex;
    flex-wrap: wrap;
    align-content: center;
    justify-content: center;
    gap: clamp(12px, 3vw, 32px);
    padding: 16px;
  }
  .bub { position: relative; will-change: auto; }
}

/* ============================================================================
   THE MAIN MENU AS BUBBLES — same physics as VISIT, no photographs. Each
   destination is a coloured circle at its own scale; the big two carry an
   "Order now" strip in the bottom cap, exactly as "Now Swirling" does.
   ========================================================================= */
.menu--main {
  overflow: hidden;
  display: grid;
  grid-template-rows: 1fr;
  padding: var(--header-h) 0 0;
}
/* ~9% smaller than the first pass (216/64vw/51svh/449) — Tim, Aug 2026 —
   with the label grown to compensate, below. */
.menu--main .bubs { --d: clamp(198px, min(58vw, 46svh), 410px); }

.mbub {
  width: calc(var(--d) * var(--s, 1));
  height: calc(var(--d) * var(--s, 1));
  background: var(--accent);
  border-color: rgba(255, 255, 255, 0.6);
  color: #fff;
}
/* Photo-filled bubbles: cropped to the circle, shown at full brightness —
   no screen. The label earns its legibility from its own drop shadow
   instead (below). */
.mbub .bub__shot { filter: none; }
/* Main-menu circles show their photographs unscreened: no radial wash, no
   dim — the label's heavy drop shadow does all the legibility work. */
.mbub .bub__main { inset: 0; text-decoration: none; color: inherit; background: none; }
.mbub.has-go .bub__main { inset: 0 0 20% 0; }
.mbub .bub__city {
  /* 0.125 → 0.14 of the diameter: the circles shrank, the word did not.
     0.14 is the ceiling — FUNDRAISING is 6.11× the font size wide and at
     0.15 it touched the rim. */
  font-size: clamp(0.66rem, calc(var(--d) * var(--s, 1) * 0.14), 2.1rem);
  text-align: center;
  padding: 0 5%;
  /* heavy drop shadow: with the screen gone, the label alone carries the
     contrast over full-brightness photos */
  text-shadow:
    0 2px 3px rgba(0, 0, 0, 0.9),
    0 4px 10px rgba(0, 0, 0, 0.7),
    0 8px 26px rgba(0, 0, 0, 0.55);
}
.mbub .bub__go {
  height: 20%;
  font-size: clamp(0.55rem, calc(var(--d) * var(--s, 1) * 0.075), 0.95rem);
}


/* ===========================================================================
   TOP-IT-UP TUESDAY
   ---------------------------------------------------------------------------
   The Tuesday hero line, the two end-of-page sections, and the shared .kin
   "Colourful Scramble" headline (also used as the TIUT page's hero, which is
   why it lives here in the sitewide sheet). The sections sit after the scroll
   story's exit — the cup anchors its exit to #tiut-videos.
   ======================================================================== */

/* "IT'S" — its own line at half the display size. Same fitting rules as
   .hero__line, halved. */
.hero__line--sm { font-size: clamp(1.3rem, min(10vw, 4.8svh), 7.5rem); }

/* --- the boomerang pair ---------------------------------------------------
   Two portrait loops, edge to edge, pulled up so they arrive right as the
   last orbit card departs (the exit re-derives its end from this section's
   real position, so the choreography code is untouched). Each clip's INNER
   edge — the side facing the centre of the viewport — is cropped 10%: the
   box is 10% narrower than 9:16, object-fit covers, and object-position
   pins the outer edge so all the loss comes off the middle. Net effect:
   taller video in the same slot. */
.tiutv { position: relative; }
/* homepage only: pulled up so the pair arrives as the last orbit card
   departs; the TIUT page uses .tiutv in normal flow */
.tiutv--pull { margin-top: -30svh; }
.tiutv__pair { display: flex; width: 100%; }
.tiutv__vid {
  display: block;
  width: 50%;
  height: auto;
  aspect-ratio: 81 / 160;   /* 9:16 with 10% of the width cropped away */
  object-fit: cover;
  box-sizing: border-box;
  border: 1px solid #000;
  background: #000;
}
.tiutv__vid:first-child { object-position: left center; }
.tiutv__vid:last-child { object-position: right center; }

/* --- the Colourful Scramble ------------------------------------------------
   Sixteen letters, each its own typographic rule-break: mismatched scale,
   tilt, baseline, weight (faked with a same-colour stroke — Luckiest Guy has
   one weight) and solid-vs-outline style. Entry: each letter pops in on its
   own beat while its colour cycles fast and decelerates to a halt on the
   final colour. Base styles ARE the final state, so reduced motion and
   no-animation paths land finished. */
.kin { position: relative; z-index: 1; line-height: 0.92; }
.kin__word {
  display: block;
  white-space: nowrap;
  font-size: clamp(2.4rem, 18.5vw, 15rem);
}
.kin__word + .kin__word { font-size: clamp(2.7rem, 22vw, 17.5rem); }
.kin b {
  display: inline-block;
  font-weight: 400;
  margin-inline: -0.014em;
  color: var(--c, var(--cream));
  -webkit-text-stroke: var(--wt, 0) var(--c, var(--cream));
  transform: translateY(calc(var(--dy, 0) * 1em)) scale(var(--s, 1)) rotate(var(--r, 0deg));
}
.kin b.kin--out {   /* outranks `.kin b`, which would zero the stroke width */
  -webkit-text-fill-color: transparent;
  -webkit-text-stroke: 0.042em var(--c, var(--cream));
  /* .display's picture-book drop shadow paints the letter's silhouette,
     which reads as a solid dark letter behind a transparent fill */
  text-shadow: none;
}

/* hidden until the section goes live — homepage only; the page's .kin is
   never inside .tiutk, so it is born visible and plays on load */
.js .tiutk:not(.is-live) .kin b { opacity: 0; }

.is-live .kin b,
.kin.is-live b {
  animation:
    kin-pop 0.62s cubic-bezier(0.3, 1.55, 0.45, 1) calc(var(--i) * 75ms) both,
    kin-colour 1.05s linear calc(var(--i) * 75ms) both;
}

@keyframes kin-pop {
  0% {
    opacity: 0;
    transform: translateY(0.55em) scale(0) rotate(calc(var(--r, 0deg) - 26deg));
  }
  55% {
    opacity: 1;
    transform: translateY(calc(var(--dy, 0) * 1em - 0.05em))
               scale(calc(var(--s, 1) * 1.17)) rotate(var(--r, 0deg));
  }
  100% {
    opacity: 1;
    transform: translateY(calc(var(--dy, 0) * 1em)) scale(var(--s, 1)) rotate(var(--r, 0deg));
  }
}

/* Hard colour cuts whose intervals stretch — fast cycling that slows to a
   halt on each letter's own final colour. Stroke colour rides along so
   outline letters cycle too. */
@keyframes kin-colour {
  0%, 5.9%   { color: #92D66F; -webkit-text-stroke-color: #92D66F; }
  6%, 11.9%  { color: #88C1F8; -webkit-text-stroke-color: #88C1F8; }
  12%, 18.9% { color: #F15B40; -webkit-text-stroke-color: #F15B40; }
  19%, 26.9% { color: #ED934E; -webkit-text-stroke-color: #ED934E; }
  27%, 36.9% { color: #F6B1CF; -webkit-text-stroke-color: #F6B1CF; }
  37%, 49.9% { color: #FFF4E4; -webkit-text-stroke-color: #FFF4E4; }
  50%, 65.9% { color: #88C1F8; -webkit-text-stroke-color: #88C1F8; }
  66%, 85.9% { color: #92D66F; -webkit-text-stroke-color: #92D66F; }
  86%, 100%  { color: var(--c, #FFF4E4); -webkit-text-stroke-color: var(--c, #FFF4E4); }
}

/* --- the kinetic section ---------------------------------------------------
   Quiet plum canvas; the scramble and the topping art are the loud things. */
.tiutk {
  --tiut-plum: #2A123F;
  position: relative;
  overflow: hidden;
  background: var(--tiut-plum);
  color: var(--cream);
  text-align: center;
  padding: clamp(84px, 15vh, 160px) 20px clamp(72px, 11vh, 130px);
}

/* --- the topping art: pop in, float, and ride the 2s diagonal wave --------
   Three layers so the transforms never fight: the slot places and tilts
   (and carries the entry pop), the float layer bobs forever, the image
   itself takes the bounce-and-swap hit. */
.tiutk__art { position: absolute; inset: 0; pointer-events: none; }
.tiutk__illo {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  min-width: 54px;
  max-width: 150px;
  transform: scale(0) rotate(var(--r, 0deg));
  opacity: 0;
  transition: transform 700ms cubic-bezier(0.34, 1.7, 0.64, 1),
              opacity 260ms ease-out;
  transition-delay: calc(var(--i) * 55ms);
}
.tiutk.is-live .tiutk__illo { transform: scale(1) rotate(var(--r, 0deg)); opacity: 1; }
.tiutk__fl {
  display: block;
  animation: kin-float var(--fd, 5.4s) ease-in-out infinite alternate;
}
.tiutk__illo img { display: block; width: 100%; }
.tiutk__illo img.is-hit { animation: kin-bounce 0.28s cubic-bezier(0.3, 1.4, 0.5, 1); }

@keyframes kin-float {
  from { transform: translateY(-7px); }
  to   { transform: translateY(7px); }
}
@keyframes kin-bounce {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.42); }
  100% { transform: scale(1); }
}

/* the words under the type — as few as possible */
.tiutk__line {
  position: relative; z-index: 1;
  font-weight: 600;
  font-size: clamp(1.05rem, 2.4vw, 1.45rem);
  margin: clamp(22px, 4.5vh, 44px) auto 4px;
  max-width: 36ch;
}
.tiutk__one {
  position: relative; z-index: 1;
  margin: 0 auto 8px;
  font-size: clamp(1.1rem, 2.6vw, 1.55rem);
}
.tiutk__one b { color: var(--sage); font-weight: 700; }
.tiutk__one span { font-size: 0.78em; opacity: 0.75; }
.tiutk__price {
  position: relative; z-index: 1;
  font-size: clamp(0.95rem, 2vw, 1.15rem);
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
  margin: 0 auto 28px;
}
.tiutk__cta {
  position: relative; z-index: 1;
  display: inline-block;
  background: var(--sage);
  color: var(--forest-deep);
  font-weight: 700;
  font-size: clamp(1rem, 2.2vw, 1.2rem);
  padding: 15px 30px;
  border-radius: 999px;
  text-decoration: none;
  transition: transform 180ms var(--ease-swirl), filter 180ms ease-out;
}
.tiutk__cta:hover { transform: scale(1.05); filter: brightness(1.08); }

@media (prefers-reduced-motion: reduce) {
  .kin b { animation: none !important; opacity: 1 !important; }
  .tiutk__illo { transition: none; opacity: 1; transform: scale(1) rotate(var(--r, 0deg)); }
  .tiutk__fl { animation: none; }
  .tiutk__illo img.is-hit { animation: none; }
}
