/* =========================================================
   Rockwall Fitness — gym site
   ========================================================= */

/* ---------- tokens ---------- */
:root {
  --black: #0a0a0a;
  --ink: #111;
  --white: #f2f2f2;
  --accent: #b6d433;
  /* The acid green is a light colour: legible on black, invisible on white.
     Use --accent-ink for accent text sitting on the white cards. */
  --accent-ink: #5a6b12;
  /* Error states need to read as errors, not as brand colour. */
  --error: #ff5a3c;
  --gray: #8a8a8a;
  --rule: #2a2a2a;
  --lift: #151515;

  --font-display: "Antonio", "Arial Narrow", sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* display type — huge, tight. --fs-display is reserved for the hero and the
     CTA footer; every section heading in between shares --fs-section. */
  --fs-display: clamp(4rem, 11vw, 11rem);
  --fs-section: clamp(3rem, 8.6vw, 8.5rem);
  --fs-display-sm: clamp(2.6rem, 6.4vw, 5.6rem);
  --fs-row: clamp(2.4rem, 6vw, 5.4rem);
  --fs-marquee: clamp(2.6rem, 7vw, 6.5rem);
  --lh-display: 0.85;

  --gutter: clamp(20px, 4vw, 72px);
  --nav-h: 76px;
  --section-y: clamp(90px, 12vw, 190px);

  --duotone: grayscale(1) contrast(1.1) brightness(0.7);
  /* small inline photos are graphic elements, not backgrounds — they keep more
     of their own light so they do not read as holes in the layout */
  --duotone-sm: grayscale(1) contrast(1.05) brightness(0.95);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  overflow-x: clip;
  background: var(--black);
  color: var(--white);
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { display: block; max-width: 100%; height: auto; }

/* Every control here is built for repeat taps — the Fuel Lab menu says "tap
   twice for two", and the tray steppers exist to be pressed over and over. On
   iOS a second tap inside ~300ms is a double-tap zoom, so it gets eaten.
   `manipulation` keeps panning and pinch-zoom and drops only the double-tap
   gesture, which nothing on this page wants. */
.chip,
.food-chip,
.step-btn,
.game-btn,
.seg-opt,
.tray-clear,
.offset-cycle { touch-action: manipulation; }

/* The platform tap flash is a light grey rectangle. On a near-black panel it
   is the brightest thing on screen for 100ms, and it ignores every radius and
   inset the components draw. Every interactive element here already has its
   own pressed or checked state. */
a,
button,
label,
input,
summary { -webkit-tap-highlight-color: transparent; }

/* <picture> is only a wrapper for the WebP <source>. Without this it is an
   inline box, which becomes the img's containing block and breaks every
   percentage height in the galleries. display:contents puts the img straight
   back in its parent's layout, so the markup change costs nothing. */
picture { display: contents; }

a { color: inherit; text-decoration: none; }

button, input { font: inherit; color: inherit; }

h1, h2, h3, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }

::selection { background: var(--accent); color: var(--black); }

/* one visible focus ring for every interactive element, on any ground */
:where(a, button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* announced by screen readers, never painted */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- shared type ---------- */
.display,
.row-title,
.tier-name,
.accordion-name,
.bonus-title,
.mq-word,
.cta-word,
.nav-logo,
.footer-logo {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  line-height: var(--lh-display);
  letter-spacing: 0.005em;
}

.display {
  font-size: var(--fs-display);
}

/* every section heading runs at one scale — the hero and the CTA footer are the
   only places the larger --fs-display is allowed to appear */
.section-title,
.locations-title,
.classes-sticky .display,
.perks-title {
  font-size: var(--fs-section);
}

.section-title { margin-bottom: clamp(40px, 6vw, 90px); }

.meta-label,
.group-label,
.cta-label {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--gray);
}

.brk {
  display: inline-block;
  color: inherit;
  transition: transform 0.3s var(--ease);
}

/* one shared micro-interaction: the [+] nudges right on every call to action */
.nav-join:hover .brk,
.more:hover .brk,
.tier-buy:hover .brk,
.cta-submit:hover .brk,
#join-cta:hover .brk { transform: translateX(4px); }

.accent { color: var(--accent); }

/* line masks for clip reveals — JS animates the inner span.
   The mask inherits the display font-size, so its em padding tracks the type. */
.line-mask {
  display: block;
  overflow: hidden;
  /* the 0.85 display line-height crops glyph edges inside the overflow mask —
     pad both edges and pull them back so the box metrics stay identical */
  padding: 0.14em 0 0.14em;
  margin: -0.14em 0 -0.14em;
}
.line-mask > * { display: block; }

/* photo treatment — full-bleed media sits back behind the type */
.hero-media img,
.bonus-bg img,
.loc-photo img {
  filter: var(--duotone);
}

/* inline photos set into a line of type keep their own light */
.mq-img,
.cta-inline-img {
  filter: var(--duotone-sm);
}

/* =========================================================
   PRELOADER
   Only ever painted for a scripted visit (html.js), always removed by
   html.is-loaded — which JS sets when the reveal runs and a head-side timer
   sets regardless, so a failed script can never leave the curtain down.
   ========================================================= */
.preloader { display: none; }

.js .preloader {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background: var(--black);
  transition: opacity 0.5s var(--ease), visibility 0.5s var(--ease);
}

.js.is-loaded .preloader {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.pre-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  width: min(320px, 68vw);
}

/* the wordmark wipes in rather than fading — same clip language as the headings */
.pre-mark {
  display: block;
  overflow: hidden;
}

.pre-mark img {
  height: 34px;
  width: auto;
}

.pre-bar {
  width: 100%;
  height: 1px;
  background: var(--rule);
  overflow: hidden;
}

.pre-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
}

.pre-count { color: var(--accent); }

/* Reduced motion skips the whole thing — there is no curtain to lift. */
@media (prefers-reduced-motion: reduce) {
  .js .preloader { display: none; }
}

/* So does a phone. Measured warm on localhost: first contentful paint at 60ms,
   is-loaded at 972ms, plus the 0.5s fade — about 1.47s of black over a page
   that was ready in a tenth of that. Cold on mobile data it is worse, because
   initPreloader() cannot run until the deferred motion layer lands, so the
   2.6s (now 1.5s) failsafe becomes the normal path rather than the fallback.
   The curtain is a nice arrival on a desktop; on a phone it is the whole cost
   of the first impression and none of the benefit. The JS gate in
   initPreloader() matches this breakpoint exactly. */
@media (max-width: 640px) {
  .js .preloader { display: none; }
}

/* =========================================================
   CUSTOM CURSOR + FILM GRAIN
   Both are chrome: they never take pointer events and they never carry
   information. The cursor is only mounted by JS on a hover-capable pointer
   without reduced motion, so touch and keyboard visitors keep the native one.
   ========================================================= */
.cursor-dot,
.cursor-ring { display: none; }

.has-cursor .cursor-dot,
.has-cursor .cursor-ring {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 300;
  pointer-events: none;
  will-change: transform;
}

.has-cursor .cursor-dot {
  display: block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  transition: opacity 0.3s var(--ease);
}

/* The ring box is a fixed size so the centring transform never goes stale —
   only the shape inside it grows. */
.has-cursor .cursor-ring {
  display: grid;
  place-items: center;
  width: 96px;
  height: 96px;
}

.ring-shape {
  grid-area: 1 / 1;
  width: 38px;
  height: 38px;
  border: 1px solid rgba(182, 212, 51, 0.5);
  border-radius: 50%;
  transition: width 0.4s var(--ease), height 0.4s var(--ease),
    background-color 0.4s var(--ease), border-color 0.4s var(--ease);
}

.cursor-label {
  grid-area: 1 / 1;
  place-self: center;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--black);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}

.cursor-ring.is-hover .ring-shape {
  width: 62px;
  height: 62px;
  border-color: var(--accent);
}

.cursor-ring.is-view .ring-shape {
  width: 92px;
  height: 92px;
  background: var(--accent);
  border-color: var(--accent);
}
.cursor-ring.is-view .cursor-label { opacity: 1; }

.cursor-dot.is-out,
.cursor-ring.is-out { opacity: 0; }

/* The native arrow gives way to the dot — except where the caret is the whole
   point of the control. */
.has-cursor body { cursor: none; }
.has-cursor input[type="text"],
.has-cursor input[type="tel"],
.has-cursor input[type="email"] { cursor: text; }
.has-cursor .lab-range { cursor: pointer; }

.grain {
  position: fixed;
  inset: -80px;
  z-index: 90;
  pointer-events: none;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  animation: grain-drift 6s steps(6) infinite;
}

/* six discrete jumps rather than a smooth pan — film, not a moving texture */
@keyframes grain-drift {
  0% { transform: translate(0, 0); }
  16% { transform: translate(-22px, 14px); }
  33% { transform: translate(18px, -26px); }
  50% { transform: translate(-14px, -12px); }
  66% { transform: translate(24px, 18px); }
  83% { transform: translate(-18px, 22px); }
  100% { transform: translate(0, 0); }
}

/* =========================================================
   NAV
   ========================================================= */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--nav-h);
  /* grid rather than space-between: the two 1fr rails keep the pill optically
     centred in the viewport regardless of how wide the logo or the CTA run */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 0 var(--gutter);
  will-change: transform;
}

/* JS adds .is-stuck past the fold and .is-hidden while scrolling down */
.nav.is-stuck {
  background: linear-gradient(to bottom, rgba(10, 10, 10, 0.92), rgba(10, 10, 10, 0));
}
.nav.is-hidden { pointer-events: none; }

/* Logo is an image (white + green lockup, so it needs a dark ground). Flex
   kills the inline-baseline gap under the img. */
.nav-logo,
.footer-logo {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

/* the lockup is 24-30px tall, so the anchor's box was the same. Centring was
   already on, so the extra height falls either side of the image and nothing
   moves visually. */
.nav-logo { min-height: 44px; }

.nav-logo img { height: 30px; width: auto; }
.footer-logo img { height: 40px; width: auto; }

.nav-logo img,
.footer-logo img { transition: opacity 0.3s var(--ease); }
.nav-logo:hover img,
.footer-logo:hover img { opacity: 0.75; }

.nav-pill {
  display: flex;
  align-items: center;
  gap: clamp(14px, 2vw, 30px);
  padding: 12px 26px;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: rgba(26, 26, 26, 0.85);
  backdrop-filter: blur(10px);
}

.nav-pill a {
  position: relative;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--white);
  transition: color 0.25s var(--ease);
}
.nav-pill a:hover { color: var(--accent); }

/* the underline draws from the left rather than switching on */
.nav-pill a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.3s var(--ease);
}
.nav-pill a:hover::after,
.nav-pill a:focus-visible::after { transform: scaleX(1); }

.nav-join {
  justify-self: end;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.12em;
  color: var(--accent);
  white-space: nowrap;
  transition: opacity 0.25s var(--ease);
}
.nav-join:hover { opacity: 0.7; }

/* =========================================================
   HERO
   ========================================================= */
#hero { position: relative; }

.hero-type {
  position: relative;
  z-index: 3;
  padding: calc(var(--nav-h) + clamp(40px, 8vh, 110px)) var(--gutter) 0;
}

.hero-type h1 {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: var(--fs-display);
  line-height: var(--lh-display);
}

.line1,
.line2 {
  font-size: inherit;
  line-height: inherit;
  color: var(--white);
}

.line2 { color: var(--accent); }

/* second line drops over the media */
.mask2 {
  margin-bottom: calc(var(--fs-display) * -0.42);
}

.hero-media {
  position: relative;
  z-index: 1;
  overflow: hidden;
  height: clamp(420px, 66vh, 900px);
}

/* Media that drifts on scroll is oversized so the parallax never
   exposes an edge. JS shifts these ±10% (hero) / ±6% ([data-parallax]). */
.hero-media img {
  position: relative;
  top: -14%;
  width: 100%;
  height: 128%;
  object-fit: cover;
  will-change: transform;
}

/* The client's 10s reel, layered over the photograph rather than replacing it.
   Same oversized box as the img so the ±10% parallax has the same room and
   never exposes an edge, same duotone so it belongs to the same set, and it
   sits under .hero-media::after (z-index 2) so the scrim keeps working over
   moving footage exactly as it does over the still. It starts at opacity 0 and
   is only faded in once the element reports `playing`, so a fetch that stalls,
   a decode that fails or an autoplay the browser refuses all leave the
   photograph on screen with nothing to notice. */
.hero-video {
  position: absolute;
  top: -14%;
  left: 0;
  z-index: 1;
  width: 100%;
  height: 128%;
  object-fit: cover;
  filter: var(--duotone);
  opacity: 0;
  transition: opacity 0.9s var(--ease);
  pointer-events: none;
  will-change: transform, opacity;
}
.hero-video.is-playing { opacity: 1; }

/* the hero photo is bright at the top (ceiling and signage) and the second
   display line drops onto it — this scrim keeps that line readable */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 40%;
  z-index: 2;
  background: linear-gradient(to bottom, var(--black), rgba(10, 10, 10, 0.6) 45%, transparent);
  pointer-events: none;
}

[data-parallax] { overflow: hidden; }

[data-parallax] img {
  position: relative;
  top: -12%;
  width: 100%;
  height: 124%;
  object-fit: cover;
  will-change: transform;
}

/* accent circle CTA */
#join-cta {
  position: absolute;
  right: clamp(16px, 5vw, 90px);
  bottom: clamp(-40px, -3vw, -20px);
  z-index: 5;
  display: grid;
  place-items: center;
  width: clamp(150px, 22vw, 260px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--accent);
  color: var(--black);
  will-change: transform;
}

/* inset scales with the circle so the ring stays a ring, not a rim */
#join-cta .ring {
  position: absolute;
  inset: 12%;
  border: 1px dashed rgba(10, 10, 10, 0.42);
  border-radius: 50%;
  pointer-events: none;
}

.join-cta-label {
  font-family: var(--font-body);
  font-size: clamp(12px, 1.3vw, 15px);
  font-weight: 500;
  letter-spacing: 0.16em;
  line-height: 1.35;
  text-transform: uppercase;
  text-align: center;
  /* label wraps to two lines inside the circle — keep it off the edge */
  max-width: 9em;
  padding: 0 14px;
}

/* =========================================================
   CLASSES
   ========================================================= */
#classes {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  column-gap: clamp(24px, 5vw, 90px);
  padding: calc(var(--section-y) + 40px) var(--gutter) var(--section-y);
}

.classes-head { display: contents; }

.classes-sticky {
  grid-column: 1;
  grid-row: 1 / span 4; /* intro row + the three class groups */
  position: sticky;
  top: calc(var(--nav-h) + 24px);
  align-self: start;
}

.classes-intro {
  grid-column: 2;
  grid-row: 1;
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 14px;
  align-items: start;
  max-width: 46ch;
  margin-bottom: clamp(50px, 8vw, 110px);
}

.classes-intro p {
  font-size: 15px;
  color: #cfcfcf;
}

.bracket {
  font-family: var(--font-display);
  font-size: 34px;
  line-height: 0.8;
  color: var(--accent);
}

.class-group {
  grid-column: 2;
  margin-bottom: clamp(48px, 7vw, 96px);
}
.class-group:last-child { margin-bottom: 0; }

.group-label {
  position: relative;
  padding-bottom: 14px;
  /* the visible rule is the ::after below so it can draw itself in on reveal —
     the transparent border keeps the box metrics identical either way */
  border-bottom: 1px solid transparent;
  margin-bottom: 8px;
}

.group-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--rule);
  transform-origin: left center;
}

/* JS arms the rule at boot; dropping the class draws it left to right. Only
   the un-armed state carries a transition, so arming snaps instead of playing
   the collapse backwards on load. */
.group-label.is-armed::after { transform: scaleX(0); }

.group-label:not(.is-armed)::after {
  transition-property: transform;
  transition-duration: 0.9s;
  transition-timing-function: var(--ease);
  transition-delay: 0.25s;
}

/* rows */
.class-row {
  border-bottom: 1px solid var(--rule);
  padding: clamp(10px, 1.4vw, 18px) 0;
  cursor: pointer;
  transition: border-color 0.35s var(--ease);
}

.row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.row-title {
  font-size: var(--fs-row);
  color: var(--white);
  will-change: transform;
  transition: color 0.35s var(--ease);
}

.row-plus {
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--gray);
  transition: color 0.35s var(--ease);
}

/* JS sets height:0 at init and tweens it open. The base rule stays open so the
   meta is still readable if the script never runs. */
.row-meta {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(20px, 4vw, 56px);
  overflow: hidden;
}

.meta-pair {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 0 18px;
}

.meta-value {
  font-size: 14px;
  font-weight: 500;
  color: var(--white);
}

/* open state — JS tweens the border colour and the title offset;
   these rules carry the same look when JS has not loaded. */
.class-row.open,
.class-row.is-open { border-bottom-color: var(--accent); }

.class-row:hover .row-title,
.class-row.open .row-title,
.class-row.is-open .row-title { color: var(--accent); }

.class-row:hover .row-plus,
.class-row.open .row-plus,
.class-row.is-open .row-plus { color: var(--accent); }

/* =========================================================
   MARQUEE
   ========================================================= */
#marquee {
  padding: clamp(30px, 5vw, 60px) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.marquee-row {
  display: flex;
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
  padding: clamp(10px, 1.6vw, 20px) 0;
}

.marquee-track {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: clamp(20px, 3vw, 46px);
  padding-right: clamp(20px, 3vw, 46px);
  white-space: nowrap;
  will-change: transform;
}

.mq-word {
  font-size: var(--fs-marquee);
  color: var(--white);
}
.mq-word.accent { color: var(--accent); }

.mq-star {
  font-size: clamp(14px, 1.6vw, 24px);
  color: var(--accent);
}

.mq-img {
  width: 180px;
  height: 110px;
  object-fit: cover;
  flex: 0 0 auto;
}

/* =========================================================
   STUDENTS
   A slim band rather than a full section — one photo, one claim, one door in.
   ========================================================= */
#students {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  align-items: stretch;
  border-bottom: 1px solid var(--rule);
}

/* [data-parallax] sizes the image oversized for the drift, so the box needs a
   height of its own to drift inside. The box only carries a min-height — it
   stretches to the copy column when that runs taller — so the oversized image
   is positioned absolutely: a percentage height against a min-height-only
   parent would otherwise collapse to auto and lose the framing. */
.student-media {
  position: relative;
  min-height: clamp(300px, 36vw, 560px);
}

.student-media img {
  position: absolute;
  top: -12%;
  left: 0;
  width: 100%;
  height: 124%;
  object-fit: cover;
  filter: var(--duotone);
}

.student-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(16px, 1.8vw, 24px);
  padding: clamp(44px, 6vw, 96px) var(--gutter);
}

.student-title {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  line-height: var(--lh-display);
  font-size: var(--fs-display-sm);
}

.student-lede {
  max-width: 46ch;
  font-size: 15px;
  color: #cfcfcf;
}

.student-points {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 14px;
  color: #bdbdbd;
}

.student-cta {
  margin-top: 6px;
  padding: 14px 30px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--black);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.student-cta:hover { transform: translateY(-2px); opacity: 0.92; }
.student-cta:hover .brk { transform: translateX(4px); }

.student-note { color: #6f6f6f; }

/* =========================================================
   BONUSES / PERKS
   ========================================================= */
#bonuses {
  position: relative;
  overflow: hidden;
  padding: var(--section-y) var(--gutter);
}

.bonus-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.bonus-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.22;
}

.bonus-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 60% at 50% 40%, rgba(182, 212, 51, 0.10), transparent 70%),
    linear-gradient(to bottom, var(--black), rgba(10, 10, 10, 0.55) 40%, var(--black));
}

.bonus-inner {
  position: relative;
  z-index: 1;
  max-width: 1400px;
  margin: 0 auto;
}

/* white heading with the second line in accent — the whole thing in accent
   fought the acid swooshes on the cards below it */
.perks-title {
  color: var(--white);
  text-align: center;
}

.bonus-cluster {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 34px);
  /* clears the heading: the rotated cards used to ride up over it */
  margin-top: clamp(28px, 3vw, 52px);
  align-items: start;
}

.bonus-card {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 26px 24px 22px;
  background: var(--white);
  color: var(--ink);
  border-radius: 2px;
  will-change: transform;
  /* GSAP owns the card transform (base tilt + scrub drift), so the hover
     response has to live somewhere else — the shadow and the swoosh */
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0);
  transition: box-shadow 0.4s var(--ease);
}

.bonus-card:hover { box-shadow: 0 20px 46px rgba(0, 0, 0, 0.45); }

.card-1 { transform: rotate(-5deg); margin-top: 40px; }
.card-2 { transform: rotate(3.5deg); margin-top: 18px; }
.card-3 { transform: rotate(-2.5deg); margin-top: 56px; }
.card-4 { transform: rotate(4.5deg); margin-top: 14px; }

.swoosh {
  position: absolute;
  top: -56px;
  right: -56px;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background: var(--accent);
  pointer-events: none;
  transition: transform 0.5s var(--ease);
}

.bonus-card:hover .swoosh { transform: scale(1.22); }

.bonus-num {
  position: relative;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.18em;
  color: var(--ink);
}

.bonus-title {
  font-size: clamp(1.5rem, 2.1vw, 2.3rem);
  line-height: 0.95; /* two-line titles need more room than the display leading */
  color: var(--ink);
  max-width: 14ch;
}

.bonus-copy {
  font-size: 14px;
  line-height: 1.5;
  color: #4a4a4a;
}

.bonus-strip {
  overflow: hidden;
  height: 96px;
}
/* the strips sit on white card stock, so they carry less of the dark treatment */
.bonus-strip img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(0.95) brightness(1.18);
}

.more {
  margin-top: auto;
  padding-top: 10px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--ink);
  border-top: 1px solid rgba(17, 17, 17, 0.18);
  transition: color 0.25s var(--ease);
}
.more:hover { color: var(--accent-ink); }

/* =========================================================
   MEMBERSHIPS
   ========================================================= */
#memberships {
  padding: var(--section-y) var(--gutter);
}

.tier-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 1px solid var(--rule);
}

.tier-card {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: clamp(24px, 2.4vw, 40px) clamp(18px, 1.8vw, 30px);
  border-left: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  transition: background-color 0.35s var(--ease);
}
.tier-card:last-child { border-right: 1px solid var(--rule); }

.tier-card:hover { background: var(--lift); }

.tier-index {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  color: var(--gray);
  transition: color 0.3s var(--ease);
}
.tier-card:hover .tier-index { color: var(--accent); }

/* Real tier names ("PERSONAL TRAINING", "TRANSFORMATION") are far longer than
   a one-word label, so this runs smaller and is allowed to break. */
.tier-name {
  font-size: clamp(1.7rem, 2.7vw, 2.7rem);
  color: var(--white);
  overflow-wrap: break-word;
}

.tier-features {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 14px;
  color: #bdbdbd;
}

.tier-price {
  margin-top: auto;
  padding-top: 22px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(2rem, 3vw, 3.1rem);
  line-height: 1;
  color: var(--white);
}
.tier-price .per {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.1em;
  color: var(--gray);
}

/* "on enquiry" is a phrase, not a number. Set as a label rather than a shrunken
   price so it stops reading as a broken figure next to FREE. */
.tier-price.is-enquiry {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #cfcfcf;
  padding-top: 30px;
}

/* the one real price on the page gets the accent */
.tier-card:first-child .tier-price { color: var(--accent); }

.tier-buy {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  color: var(--accent);
  transition: opacity 0.25s var(--ease);
}
.tier-buy:hover { opacity: 0.65; }

/* =========================================================
   LOCATIONS
   ========================================================= */
#locations {
  padding: var(--section-y) var(--gutter);
}

.accordion { border-top: 1px solid var(--rule); }

.accordion-item { border-bottom: 1px solid var(--rule); }

/* the heading is structural only — the button carries the type */
.accordion-heading { margin: 0; font: inherit; }

.accordion-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  width: 100%;
  padding: clamp(16px, 2vw, 28px) 0;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
}

.accordion-name {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  line-height: var(--lh-display);
  font-size: clamp(2rem, 5vw, 4.6rem);
  color: var(--white);
  transition: color 0.3s var(--ease);
}

.accordion-header:hover .accordion-name { color: var(--accent); }

.accordion-icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  font-size: 20px;
  line-height: 1;
  color: var(--accent);
  will-change: transform;
  transition: border-color 0.3s var(--ease);
}

.accordion-header:hover .accordion-icon { border-color: var(--accent); }

.accordion-item.open .accordion-icon,
.accordion-item.is-open .accordion-icon {
  border-color: var(--accent);
}

/* JS collapses these at init and tweens height 0 <-> auto. Left open in the
   base rule so the address and hours survive a failed script load. */
.accordion-body {
  overflow: hidden;
}
.accordion-item.open .accordion-body,
.accordion-item.is-open .accordion-body { height: auto; }

.accordion-item.open .accordion-icon,
.accordion-item.is-open .accordion-icon { transform: rotate(45deg); }

.accordion-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(16px, 2.4vw, 40px);
  padding: 6px 0 clamp(28px, 3.2vw, 48px);
}

.contact-card {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.contact-block p {
  margin-top: 6px;
  font-size: 14px;
  color: var(--white);
  max-width: 30ch;
}
.contact-block a { transition: color 0.25s var(--ease); }
.contact-block a:hover { color: var(--accent); }

/* height comes from [data-parallax] img so the drift has room to move */
.loc-photo {
  overflow: hidden;
  height: clamp(180px, 22vw, 280px);
}

.map-tile {
  position: relative;
  overflow: hidden;
  height: clamp(180px, 22vw, 280px);
  background: var(--ink);
  border: 1px solid var(--rule);
}

/* The address panel used to carry a live Google Maps iframe. It cost 1.72 MB
   over 25 third-party requests to draw one pin in a 354x180 box, and being an
   out-of-process iframe with touch-action: auto it won any swipe that started
   on it, so a thumb-drag panned the map instead of scrolling the page. Both
   panels now use the drawn tile, which SPEC already called "a graphic there,
   not a map" — and the tile keeps the directions link, which is the only thing
   anyone wanted from the embed. */

.map-link {
  position: absolute;
  left: 12px;
  bottom: 12px;
  padding: 9px 14px;
  background: rgba(10, 10, 10, 0.88);
  border: 1px solid var(--rule);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  transition: border-color 0.3s var(--ease);
}
.map-link:hover { border-color: var(--accent); }
.map-link:hover .brk { transform: translateX(4px); }

.map-tile .ring {
  position: absolute;
  top: 50%;
  left: 50%;
  border: 1px solid #363636;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
.map-tile .r1 { width: 34%; aspect-ratio: 1; }
.map-tile .r2 { width: 62%; aspect-ratio: 1; }
.map-tile .r3 { width: 92%; aspect-ratio: 1; }

.map-tile .marker {
  position: absolute;
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1;
  color: var(--accent);
}
.map-tile .m1 { top: 34%; left: 40%; }
.map-tile .m2 { top: 58%; left: 62%; }
.map-tile .m3 { top: 22%; left: 70%; }

/* The tile draws itself the first time it scrolls into view: rings breathe out
   from the centre, then the markers land. JS arms the tile at boot and drops
   .is-armed on enter, so without the script the finished state is what shows.
   The rings keep their percentage centring — only the scale is animated. */
.map-tile .marker { transform: scale(1); }

/* Only the un-armed state transitions, so arming at boot snaps rather than
   playing the build-in backwards. Longhand, not the shorthand: the shorthand
   would reset the per-element transition-delay that staggers the tile. */
.map-tile:not(.is-armed) .ring,
.map-tile:not(.is-armed) .marker {
  transition-property: transform, opacity;
  transition-duration: 0.9s;
  transition-timing-function: var(--ease);
}

.map-tile.is-armed .ring {
  transform: translate(-50%, -50%) scale(0.7);
  opacity: 0;
}
.map-tile.is-armed .marker {
  transform: scale(0.4);
  opacity: 0;
}

.map-tile .r2 { transition-delay: 0.09s; }
.map-tile .r3 { transition-delay: 0.18s; }
.map-tile .m1 { transition-delay: 0.3s; }
.map-tile .m2 { transition-delay: 0.38s; }
.map-tile .m3 { transition-delay: 0.46s; }

/* =========================================================
   RESULTS
   One member, one real set of numbers, and his own words carrying the section.
   There is deliberately no before/after pair here: the only transformation
   images in the client's archive were theme demo stock, and a stock body under
   a BEFORE label would be a claim the gym cannot stand behind. The quote is the
   hero, the stat rail is the weight, the photograph is texture — captioned as
   what it is, a coach on the floor, and never as the member being quoted.
   ========================================================= */
#results {
  padding: var(--section-y) var(--gutter);
  border-top: 1px solid var(--rule);
}

.results-label {
  display: block;
  margin-bottom: clamp(18px, 2vw, 30px);
}

.results-title { margin-bottom: clamp(40px, 5vw, 80px); }

.results-lead {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  column-gap: clamp(28px, 5vw, 80px);
  align-items: end;
}

/* the quote runs at display scale — it is the loudest thing in the section */
.pull-quote p {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  line-height: 0.98;
  font-size: clamp(1.9rem, 4.2vw, 4.2rem);
  color: var(--white);
  max-width: 17ch;
}

.quote-mark {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(3rem, 7vw, 6rem);
  /* the glyph sits high in its box; a half line-height pulls the quote up
     under it instead of leaving a hole */
  line-height: 0.5;
  color: var(--accent);
  margin-bottom: 0.22em;
}

.pull-quote cite {
  display: block;
  margin-top: clamp(20px, 2.4vw, 32px);
  font-size: 13px;
  font-style: normal;
  letter-spacing: 0.08em;
  color: var(--gray);
}

.results-cta {
  display: inline-block;
  margin-top: clamp(22px, 2.6vw, 34px);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--rule);
  transition: border-color 0.3s var(--ease);
}
.results-cta:hover { border-color: var(--accent); }
.results-cta:hover .brk { transform: translateX(4px); }

/* height lives on the box so [data-parallax] has room to drift the image */
.results-photo-media {
  height: clamp(260px, 32vw, 460px);
  border: 1px solid var(--rule);
}

.results-photo-media img {
  filter: var(--duotone);
  transition: filter 0.6s var(--ease);
}

.results-photo:hover .results-photo-media img { filter: none; }

.results-photo figcaption {
  margin-top: 12px;
  transition: color 0.3s var(--ease);
}
.results-photo:hover figcaption { color: var(--accent); }

/* the closing band: three real numbers at full width, carrying the horizontal
   weight the image pair used to */
.stat-rail {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: clamp(44px, 5.5vw, 88px);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
  padding: clamp(24px, 3vw, 42px) clamp(16px, 2.4vw, 36px);
  border-left: 1px solid var(--rule);
}
.stat:first-child { border-left: 0; padding-left: 0; }

.stat-v {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  line-height: 0.9;
  font-size: clamp(2rem, 4.6vw, 4.4rem);
  color: var(--accent);
}

/* Antonio has no arrow, so this one glyph is set in the body face and pulled
   back to the display weight optically */
.stat-arrow {
  font-family: var(--font-body);
  font-size: 0.7em;
  color: var(--gray);
}

/* =========================================================
   BURN LAB
   An instrument, not a form: one bordered panel split into an input bay and a
   readout bay, thin rules throughout, every control numbered like a channel.
   ========================================================= */
#burn-lab {
  padding: var(--section-y) var(--gutter);
  border-top: 1px solid var(--rule);
}

.lab-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(24px, 5vw, 90px);
  align-items: end;
  margin-bottom: clamp(40px, 5vw, 80px);
}

.lab-title { margin-bottom: 0; }

.lab-intro {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 14px;
  align-items: start;
  max-width: 46ch;
  padding-bottom: 0.4em;
}

.lab-intro p {
  font-size: 15px;
  color: #cfcfcf;
}

.lab-panel {
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(0, 0.88fr);
  border: 1px solid var(--rule);
}

.lab-inputs {
  display: flex;
  flex-direction: column;
  gap: clamp(28px, 3vw, 44px);
  padding: clamp(22px, 2.6vw, 44px);
  border-right: 1px solid var(--rule);
}

/* fieldset only groups the controls — it carries no chrome of its own */
.lab-field {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.lab-field legend {
  padding: 0;
  margin-bottom: 16px;
}

/* a legend shrink-wraps by default, so the width is what lets the hint sit at
   the far end of the label row */
.lab-legend {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
}

.rail-hint {
  flex: 0 0 auto;
  color: var(--accent);
  transition: opacity 0.35s var(--ease);
}

.rail-arrow {
  display: inline-block;
  animation: rail-nudge 1.6s ease-in-out infinite;
}

@keyframes rail-nudge {
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(4px); }
}

.lab-dials {
  display: flex;
  flex-direction: column;
  gap: clamp(26px, 3vw, 40px);
}

/* ---- machine picker ---- */

/* The rail clips its last chip mid-tile, which reads as a broken layout rather
   than as a scroller. The fades say "there is more this way" — the trailing one
   is on by default (a ten-chip rail always overflows before JS runs) and both
   are driven by the state classes JS puts on the fieldset. */
.chip-rail-wrap { position: relative; }

.chip-rail-wrap::before,
.chip-rail-wrap::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 14px; /* clears the scrollbar gutter */
  width: clamp(36px, 7%, 80px);
  pointer-events: none;
  transition: opacity 0.35s var(--ease);
}

.chip-rail-wrap::before {
  left: 0;
  background: linear-gradient(to left, rgba(10, 10, 10, 0), var(--black));
  opacity: 0;
}

.chip-rail-wrap::after {
  right: 0;
  background: linear-gradient(to right, rgba(10, 10, 10, 0), var(--black));
  opacity: 1;
}

.lab-machines.is-scrolled .chip-rail-wrap::before { opacity: 1; }
.lab-machines.at-end .chip-rail-wrap::after { opacity: 0; }
.lab-machines.at-end .rail-hint { opacity: 0; }

.chip-rail {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  /* Momentum reaching either end chains to the page by default, and a
     horizontal chain at the screen edge is the back-gesture on iOS and Chrome
     Android. This rail is 1206px in 308 and the gallery is 4.6 screens wide, so
     hitting the end is the normal case, not the edge case: leaving the section
     by accident is one flick away. `contain` stops at the rail. */
  overscroll-behavior-inline: contain;
  padding-bottom: 14px;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  scrollbar-color: var(--rule) transparent;
  -webkit-overflow-scrolling: touch;
}
.chip-rail::-webkit-scrollbar { height: 3px; }
.chip-rail::-webkit-scrollbar-thumb { background: var(--rule); }

.chip {
  position: relative;
  flex: 0 0 auto;
  width: clamp(108px, 11.5vw, 132px);
  cursor: pointer;
  scroll-snap-align: start;
}

/* kept in the tab order and in the accessibility tree — only painted out */
.chip input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  pointer-events: none;
}

/* The tile is portrait: every source frame is a standing machine or a standing
   body, and a letterbox crop of one throws away the half that identifies it.
   The duotone sits on the img rather than on .chip-media, so the marks drawn on
   top of the tile — the scrim, the sourced-corner hairline — keep their own
   colour instead of being greyed out with the photograph. */
.chip-media {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background: #0d0d0d;
  border: 1px solid var(--rule);
  transition: border-color 0.3s var(--ease);
}

.chip-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--duotone);
  transition: filter 0.4s var(--ease), transform 0.6s var(--ease);
}

/* ten photographs of ten different rooms, pulled into one set by one scrim */
.chip-media::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(10, 10, 10, 0.5), rgba(10, 10, 10, 0) 45%, rgba(10, 10, 10, 0.55));
  pointer-events: none;
  transition: opacity 0.35s var(--ease);
}

/* Honesty mark. Five of these ten activities have no photograph in the client's
   archive, so their tiles carry licensed stock of the activity instead of a
   frame from Rockwall's own floor. Those tiles get a hairline chamfer across the
   bottom-left corner — a drafting mark, not a badge: it is legible if you look
   at one tile and invisible in the rhythm of the rail. See img/CREDITS.txt. */
.chip.is-sourced .chip-media::after {
  content: "";
  position: absolute;
  left: -7px;
  bottom: 10px;
  width: 30px;
  height: 1px;
  background: rgba(242, 242, 242, 0.38);
  transform: rotate(45deg);
  transition: background-color 0.3s var(--ease);
}

.chip-name {
  position: relative;
  display: block;
  margin-top: 12px;
  padding-top: 10px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray);
  transition: color 0.3s var(--ease);
}

/* the rule over the name is the channel indicator: a hairline while the chip is
   idle, an accent bar drawn from the left once it is the selected machine */
.chip-name::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1px;
  background: var(--rule);
}

.chip-name::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease);
}

.chip-mark {
  position: absolute;
  top: 8px;
  right: 8px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--accent);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}

.chip:hover .chip-media { border-color: #4a4a4a; }
.chip:hover .chip-media img { filter: var(--duotone-sm); transform: scale(1.05); }
.chip:hover .chip-media::before { opacity: 0.65; }
.chip:hover .chip-name { color: var(--white); }

.chip input:checked ~ .chip-media { border-color: var(--accent); }
.chip input:checked ~ .chip-media img { filter: none; transform: scale(1.03); }
.chip input:checked ~ .chip-media::before { opacity: 0.45; }
.chip.is-sourced input:checked ~ .chip-media::after { background: rgba(182, 212, 51, 0.75); }
.chip input:checked ~ .chip-name { color: var(--accent); }
.chip input:checked ~ .chip-name::after { transform: scaleX(1); }
.chip input:checked ~ .chip-mark { opacity: 1; transform: translateY(0); }

/* the input itself is 1px, so the ring has to be drawn on the tile — and the
   shared focus ring has to come off the 1px box, or it lands as a green speck
   in the corner of the chip */
.chip input:focus-visible ~ .chip-media {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.chip input:focus-visible,
.seg-opt input:focus-visible { outline: none; }

/* ---- sliders ---- */
.lab-dial-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 10px;
}

.lab-dial-value {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.4rem, 2.2vw, 2rem);
  line-height: 1;
  color: var(--white);
}

/* --fill-n is written by JS on every input, 0..1, so the track shows the
   travelled part. It is a bare number rather than a percentage because the
   accent has to stop where the *thumb centre* is, and the thumb centre is not
   a flat fraction of the track — see --fill-stop below. */
.lab-range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 26px;
  margin: 0;
  background: none;
  cursor: pointer;
  /* The thumb is a transparent grab pad with the visible dot painted into it,
     so this one number is the whole touch target. */
  --thumb: 18px;
  /* Webkit insets thumb travel by half the thumb at each end: the centre runs
     from thumb/2 to trackWidth - thumb/2, never 0 to 100%. A flat percentage
     fill therefore overshoots the dot by up to half a thumb at the extremes -
     9px as drawn, 22px once the pad is 44px, which is a visible detached bar
     of accent past the dot. This puts the stop under the centre at every
     value. */
  --fill-stop: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--fill-n, 0.5));
  /* These dials are horizontal, and the default `auto` lets the browser claim
     a touch that starts on one for the page scroll. The real hazard is not a
     vertical swipe stealing a horizontal one - that arbitration is right - it
     is a *diagonal* drag, which is what a thumb crossing a 308px dial actually
     produces, being claimed as a page scroll with no way back. `pan-y` leaves
     vertical scrolling alone and gives everything else to the slider;
     pinch-zoom stays with the browser because zoom is never ours to take. */
  touch-action: pan-y pinch-zoom;
}

.lab-range::-webkit-slider-runnable-track {
  height: 2px;
  background: linear-gradient(
    to right,
    var(--accent) 0 var(--fill-stop),
    var(--rule) var(--fill-stop) 100%
  );
}

/* The dot is painted into the pad by a radial gradient rather than drawn as a
   bordered box, so the pad can be any size without the dot changing: accent
   out to 5px, the black ring to 9px, transparent after that. At --thumb: 18px
   that is pixel for pixel the 18px box with a 4px black border it replaces. */
.lab-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--thumb);
  height: var(--thumb);
  /* centres the dot on the 2px track whatever the pad measures */
  margin-top: calc((2px - var(--thumb)) / 2);
  border: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    var(--accent) 0 5px, var(--black) 5px 9px, transparent 9px);
  transition: transform 0.25s var(--ease);
}
.lab-range:hover::-webkit-slider-thumb { transform: scale(1.16); }

/* Firefox draws its thumb a size smaller by design and centres it on the
   track itself, so it needs no margin — only the same painted-dot treatment at
   its own stops (6px disc, 14px overall). */
.lab-range::-moz-range-track { height: 2px; background: var(--rule); }
.lab-range::-moz-range-progress { height: 2px; background: var(--accent); }
.lab-range::-moz-range-thumb {
  width: var(--thumb);
  height: var(--thumb);
  border: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    var(--accent) 0 3px, var(--black) 3px 7px, transparent 7px);
}

.lab-scale {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #5f5f5f;
}

/* ---- intensity segments ---- */
.seg {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border: 1px solid var(--rule);
}

.seg-opt {
  position: relative;
  cursor: pointer;
  border-left: 1px solid var(--rule);
}
.seg-opt:first-child { border-left: 0; }

.seg-opt input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
  pointer-events: none;
}

.seg-opt span {
  display: block;
  padding: 12px 6px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-align: center;
  color: var(--gray);
  transition: color 0.3s var(--ease), background-color 0.3s var(--ease);
}

.seg-opt:hover span { color: var(--white); }
.seg-opt input:checked ~ span { background: var(--accent); color: var(--black); }
.seg-opt input:focus-visible ~ span { outline: 2px solid var(--accent); outline-offset: 2px; }

.lab-mode {
  margin-top: 12px;
  font-size: 13px;
  color: #9d9d9d;
}
.lab-mode strong {
  font-weight: 500;
  color: var(--accent);
}

/* ---- readout ---- */
.lab-readout {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: clamp(22px, 2.6vw, 44px);
  background: #0c0c0c;
}

.kcal {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}

/* the mask carries the type size so the .line-mask em padding — the thing that
   stops the overflow crop from shaving the digits — scales with the counter */
.kcal-mask {
  display: inline-block;
  font-family: var(--font-display);
  font-size: clamp(4rem, 11vw, 8.5rem);
  line-height: 0.85;
}

.kcal-num {
  display: block;
  font-family: inherit;
  font-weight: 700;
  font-size: inherit;
  line-height: inherit;
  letter-spacing: -0.01em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.kcal-unit {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gray);
}

.lab-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  padding: 16px 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.lab-stat {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.lab-stat-v {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.2rem, 1.9vw, 1.7rem);
  line-height: 1;
  color: var(--white);
}

.food-label { margin-top: 4px; }

.food-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.food-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding-bottom: 10px;
  border-bottom: 1px solid #1c1c1c;
}

.food-qty {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.food-name {
  font-size: 14px;
  color: var(--white);
}

.food-note {
  margin-left: auto;
  font-size: 12px;
  letter-spacing: 0.08em;
  color: #6f6f6f;
}

.lab-disclaimer {
  margin-top: auto;
  padding-top: 12px;
  font-size: 12px;
  line-height: 1.5;
  color: #6f6f6f;
}

/* =========================================================
   FUEL LAB
   Same instrument family as the Burn Lab — bordered panel, hairline rules,
   numbered channels, Antonio numerals — but deliberately not the same
   composition. The Burn Lab splits vertically into inputs | readout. This one
   is three horizontal bands: the menu runs full width across the top (a tray
   grows by tapping, so the picker wants width, not a rail), the tray and its
   readout share the middle, and the game takes the bottom.
   ========================================================= */
#fuel-lab {
  padding: var(--section-y) var(--gutter);
  border-top: 1px solid var(--rule);
}

.fuel-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(24px, 5vw, 90px);
  align-items: end;
  margin-bottom: clamp(40px, 5vw, 80px);
}

.fuel-title { margin-bottom: 0; }

.fuel-intro {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 14px;
  align-items: start;
  max-width: 46ch;
  padding-bottom: 0.4em;
}

.fuel-intro p {
  font-size: 15px;
  color: #cfcfcf;
}

.fuel-panel { border: 1px solid var(--rule); }

/* ---- 01: the menu ---- */
.fuel-menu {
  min-width: 0;
  margin: 0;
  padding: clamp(22px, 2.6vw, 44px);
  border: 0;
  border-bottom: 1px solid var(--rule);
}

.fuel-menu legend { padding: 0; margin-bottom: 16px; }

.menu-hint { color: #6f6f6f; }

/* Every cell carries its right and bottom hairline and the grid carries the
   opening two, so a wrapped auto-fill grid still closes into one ruled sheet
   with no doubled lines. */
.menu-grid { display: grid; }

.menu-grid.is-built {
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  border-top: 1px solid var(--rule);
  border-left: 1px solid var(--rule);
}

.fuel-nojs {
  font-size: 13px;
  color: var(--gray);
  max-width: 60ch;
}

.food-chip {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  min-width: 0;
  padding: 15px 14px 17px;
  background: none;
  border: 0;
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  text-align: left;
  cursor: pointer;
  transition: background-color 0.3s var(--ease);
}

.fc-kcal {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.8rem;
  line-height: 1;
  color: var(--white);
  font-variant-numeric: tabular-nums;
  transition: color 0.3s var(--ease);
}

.fc-name {
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gray);
  transition: color 0.3s var(--ease);
}

/* the count only exists once something is on the tray */
.fc-count {
  position: absolute;
  top: 0;
  right: 0;
  padding: 3px 7px 4px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1;
  color: var(--black);
  background: var(--accent);
}

.food-chip:hover { background: var(--lift); }
.food-chip:hover .fc-kcal { color: var(--accent); }
.food-chip:hover .fc-name { color: var(--white); }

.food-chip.is-on { background: var(--lift); }
.food-chip.is-on .fc-kcal { color: var(--accent); }
.food-chip.is-on .fc-name { color: var(--white); }

/* ---- 02: the tray, 03: the readout ---- */
.fuel-bays {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
}

.fuel-tray-bay {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: clamp(22px, 2.6vw, 44px);
  border-right: 1px solid var(--rule);
}

.tray-clear {
  flex: 0 0 auto;
  padding: 0;
  background: none;
  border: 0;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray);
  cursor: pointer;
  transition: color 0.3s var(--ease);
}
.tray-clear:hover { color: var(--accent); }

.tray-list { display: flex; flex-direction: column; }

.tray-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid #1c1c1c;
}

.tray-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

.tray-name {
  font-size: 14px;
  color: var(--white);
}

.tray-sub {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #6f6f6f;
  font-variant-numeric: tabular-nums;
}

.tray-steps {
  display: flex;
  align-items: center;
  gap: 2px;
}

.step-btn {
  width: 28px;
  height: 28px;
  padding: 0;
  background: none;
  border: 1px solid var(--rule);
  color: var(--gray);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.step-btn:hover { color: var(--accent); border-color: var(--accent); }
.step-btn.is-drop { margin-left: 6px; border-color: transparent; }

.tray-count {
  min-width: 24px;
  text-align: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.1rem;
  color: var(--white);
  font-variant-numeric: tabular-nums;
}

.tray-kcal {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.tray-empty {
  font-size: 13px;
  color: #6f6f6f;
}

.fuel-readout {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: clamp(22px, 2.6vw, 44px);
  background: #0c0c0c;
}

/* a tray runs to four digits where a session rarely passes three, so the plate
   counter sits a size below the Burn Lab's and keeps the same face */
.plate-mask { font-size: clamp(3.4rem, 8vw, 6.4rem); }

.fuel-weight { margin-top: 4px; }

.offset-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin-top: 4px;
}

.offset-cycle {
  flex: 0 0 auto;
  padding: 0;
  background: none;
  border: 0;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  transition: opacity 0.3s var(--ease);
}
.offset-cycle:hover { opacity: 0.7; }

.offset-list { display: flex; flex-direction: column; gap: 10px; }

.offset-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  padding-bottom: 10px;
  border-bottom: 1px solid #1c1c1c;
}

.offset-min {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.offset-unit {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gray);
}

.offset-name {
  font-size: 14px;
  color: var(--white);
}

.offset-mode {
  margin-left: auto;
  font-size: 12px;
  letter-spacing: 0.08em;
  color: #6f6f6f;
}

.fuel-frame {
  font-size: 13px;
  line-height: 1.55;
  color: #9d9d9d;
}

/* ---- 04: price the plate ---- */
.fuel-game {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.44fr);
  border-top: 1px solid var(--rule);
}

.game-prompt {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
  margin: 0;
  padding: clamp(22px, 2.6vw, 44px);
  border: 0;
  border-right: 1px solid var(--rule);
}

.game-prompt legend { padding: 0; }

.game-hint { color: #6f6f6f; }

.game-lede {
  font-size: 14px;
  color: #9d9d9d;
  max-width: 52ch;
}

.game-food {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: clamp(2rem, 5vw, 3.6rem);
  line-height: 0.9;
  color: var(--white);
}

.game-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: auto;
}

.game-btn {
  padding: 12px 18px;
  background: none;
  border: 1px solid var(--rule);
  color: var(--white);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.3s var(--ease), background-color 0.3s var(--ease),
    border-color 0.3s var(--ease);
}
.game-btn:hover { border-color: var(--accent); color: var(--accent); }

.game-btn.is-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--black);
}
.game-btn.is-primary:hover {
  background: none;
  color: var(--accent);
}

/* once locked the dial is a readout, not a control */
.game-guess .lab-range:disabled { cursor: default; }

/* the guess readout doubles as the reveal: it travels from what you called to
   what the plate actually is, and turns accent when it lands on the truth */
.lab-dial-value.is-actual { color: var(--accent); }

.game-verdict {
  min-height: 3.2em;
  font-size: 14px;
  line-height: 1.55;
  color: #cfcfcf;
}
.game-verdict strong {
  font-weight: 500;
  color: var(--accent);
}

.game-score {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: clamp(22px, 2.6vw, 44px);
  background: #0c0c0c;
}

.score-num {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.6rem, 6vw, 4.4rem);
  line-height: 0.85;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.score-of {
  margin-left: 8px;
  font-size: 0.36em;
  letter-spacing: 0.1em;
  color: var(--gray);
}

.round-dots { display: flex; gap: 6px; }

.dot {
  flex: 1 1 0;
  height: 6px;
  background: var(--rule);
  transition: background-color 0.35s var(--ease);
}
.dot.is-hit { background: var(--accent); }
.dot.is-near { background: #5d6a2a; }
.dot.is-wide { background: #3a3a3a; }

.score-meta {
  display: flex;
  gap: 26px;
  margin: 0;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
}

.score-pair { display: flex; flex-direction: column; gap: 5px; }

.score-v {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--white);
  font-variant-numeric: tabular-nums;
}

.score-note {
  margin-top: auto;
  font-size: 11px;
  line-height: 1.5;
  color: #6f6f6f;
}

/* =========================================================
   THE FLOOR — horizontal gallery
   Base state is a native horizontal scroller, which is what touch and reduced
   motion keep. JS adds .is-pinned on pointer-fine desktops and drives the same
   track from the scroll position instead.
   ========================================================= */
#floor {
  padding: var(--section-y) 0;
  border-top: 1px solid var(--rule);
  overflow: hidden;
}

#floor.is-pinned {
  padding: clamp(60px, 9vh, 120px) 0;
}

.floor-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 18px;
  padding: 0 var(--gutter);
  margin-bottom: clamp(26px, 3vw, 48px);
}

.floor-title { margin-bottom: 0; }

.floor-hint { padding-bottom: 0.4em; }

.floor-viewport {
  overflow-x: auto;
  overflow-y: hidden;
  /* 5004px of gallery in 390 — 4.6 screens of swiping, so reaching the end is
     the normal case. Chaining there is the browser back-gesture. */
  overscroll-behavior-inline: contain;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: var(--rule) transparent;
}
.floor-viewport::-webkit-scrollbar { height: 3px; }
.floor-viewport::-webkit-scrollbar-thumb { background: var(--rule); }

/* under the pin the track is transform-driven, so the native scrollbar goes */
#floor.is-pinned .floor-viewport {
  overflow: hidden;
  scrollbar-width: none;
}
#floor.is-pinned .floor-viewport::-webkit-scrollbar { display: none; }

.floor-track {
  display: flex;
  align-items: flex-end;
  gap: clamp(14px, 2vw, 34px);
  width: max-content;
  padding: 0 var(--gutter) 18px;
  will-change: transform;
}

.floor-item {
  flex: 0 0 auto;
  scroll-snap-align: center;
}

/* height varies to give the strip a rhythm; the aspect ratio then decides the
   width, so nothing has to be measured and nothing shifts on load */
.floor-media {
  overflow: hidden;
  height: var(--h);
  aspect-ratio: var(--ar);
  border: 1px solid var(--rule);
}

.is-land { --ar: 3 / 2; }
.is-port { --ar: 2 / 3; }

.h1 { --h: clamp(280px, 50vh, 520px); }
.h2 { --h: clamp(230px, 41vh, 430px); }
.h3 { --h: clamp(320px, 58vh, 580px); }

.floor-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--duotone);
  transition: filter 0.6s var(--ease), transform 0.9s var(--ease);
}

/* the premium touch: colour comes back under the pointer */
.floor-item:hover .floor-media img {
  filter: none;
  transform: scale(1.04);
}

.floor-item figcaption {
  margin-top: 12px;
  transition: color 0.3s var(--ease);
}
.floor-item:hover figcaption { color: var(--accent); }

/* =========================================================
   CTA FOOTER
   ========================================================= */
#cta-footer {
  padding: var(--section-y) var(--gutter) clamp(70px, 9vw, 130px);
  border-top: 1px solid var(--rule);
}

.cta-label {
  display: block;
  margin-bottom: clamp(24px, 3vw, 44px);
}

.cta-type { margin-bottom: clamp(46px, 6vw, 90px); }

.cta-line {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: clamp(12px, 1.6vw, 28px);
  font-size: var(--fs-display);
  line-height: var(--lh-display);
}

.cta-line .line-mask { display: inline-block; }

.cta-word {
  font-size: inherit;
  color: var(--white);
}
.cta-word.accent { color: var(--accent); }

.cta-inline-img {
  width: 220px;
  height: 120px;
  object-fit: cover;
  margin-bottom: 0.16em;
  flex: 0 0 auto;
}

.cta-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: clamp(18px, 2.4vw, 40px);
  max-width: 860px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1 1 240px;
}

/* the label picks up the accent with its own input */
.field .meta-label { transition: color 0.3s var(--ease); }
.field:focus-within .meta-label { color: var(--accent); }

.field input {
  width: 100%;
  padding: 12px 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  font-size: 16px;
  color: var(--white);
  transition: border-color 0.3s var(--ease);
}
.field input::placeholder { color: #8a8a8a; }
.field input:focus {
  outline: none;
  border-color: var(--accent);
}
/* JS flags a bad field with .is-error and the sent form with .is-sent */
.field input.is-error { border-color: var(--error); }
.field input.is-error::placeholder { color: var(--error); }

.cta-form.is-sent .field { opacity: 0.45; }

/* full-width row under the fields; empty until the handler has something to say */
.form-note {
  flex: 1 1 100%;
  min-height: 1.2em;
  font-size: 13px;
  color: var(--error);
}
.cta-form.is-sent .form-note { color: var(--accent); }

.cta-submit {
  flex: 0 0 auto;
  padding: 16px 34px;
  background: var(--accent);
  color: var(--black);
  border: 0;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
/* the magnet owns this button's transform, so hover answers in opacity alone */
.cta-submit:hover { opacity: 0.92; }

/* =========================================================
   FOOTER
   ========================================================= */
footer {
  padding: clamp(40px, 5vw, 70px) var(--gutter) clamp(30px, 4vw, 50px);
  border-top: 1px solid var(--rule);
}

.footer-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 24px;
}

.footer-pill { background: none; }

.footer-social {
  display: flex;
  gap: 22px;
  font-size: 13px;
  color: var(--gray);
}
.footer-social a { transition: color 0.25s var(--ease); }
.footer-social a:hover { color: var(--accent); }

.footer-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: clamp(34px, 5vw, 60px);
  padding-top: 22px;
  border-top: 1px solid var(--rule);
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--gray);
}

.footer-credit {
  color: var(--gray);
  transition: color 0.25s var(--ease);
}
.footer-credit:hover { color: var(--accent); }

.tagline { color: var(--accent); }

/* =========================================================
   RESPONSIVE
   ========================================================= */

/* Once there is room, the closing statement and the form sit side by side
   instead of the form trailing a long way below the type. */
@media (min-width: 1000px) {
  #cta-footer {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    column-gap: clamp(40px, 5vw, 90px);
    align-items: end;
  }
  .cta-label { grid-column: 1 / -1; }
  .cta-type  { grid-column: 1; margin-bottom: 0; }
  .cta-form  { grid-column: 2; max-width: none; }
  .cta-line  { font-size: clamp(3rem, 7vw, 7.5rem); }
  .cta-inline-img { width: 170px; height: 96px; }
  /* the form column is narrow, so the fields stack rather than sharing a row */
  .field { flex: 1 1 100%; }
  .cta-submit { width: 100%; }
}

@media (max-width: 1100px) {
  .bonus-cluster {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(20px, 4vw, 40px);
    margin-top: clamp(24px, 3vw, 44px);
  }
  .card-1, .card-3 { margin-top: 26px; }

  .tier-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tier-card:nth-child(2) { border-right: 1px solid var(--rule); }
  .tier-card:last-child { border-right: 1px solid var(--rule); }

  .accordion-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .contact-card { grid-column: 1 / -1; flex-direction: row; flex-wrap: wrap; gap: 32px; }
}

/* The lab stops being two bays and becomes one column: inputs first, then the
   readout, so the number lands directly under the control you just moved.
   The transformation pair stacks over its numbers at the same width. */
@media (max-width: 980px) {
  .results-lead {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(30px, 5vw, 50px);
  }
  .pull-quote p { max-width: none; }

  .lab-head {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(24px, 4vw, 40px);
  }
  .lab-intro { max-width: none; padding-bottom: 0; }

  .lab-panel { grid-template-columns: minmax(0, 1fr); }
  .lab-inputs { border-right: 0; border-bottom: 1px solid var(--rule); }

  /* The Fuel Lab collapses band by band: menu, tray, readout, then the game
     over its score — the total still lands directly under the tray it counts. */
  .fuel-head {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(24px, 4vw, 40px);
  }
  .fuel-intro { max-width: none; padding-bottom: 0; }

  .fuel-bays,
  .fuel-game { grid-template-columns: minmax(0, 1fr); }
  .fuel-tray-bay,
  .game-prompt { border-right: 0; border-bottom: 1px solid var(--rule); }
  .game-verdict { min-height: 0; }
}

@media (max-width: 900px) {
  :root { --nav-h: 66px; }

  .nav .nav-pill {
    gap: 14px;
    padding: 9px 18px;
  }
  .nav .nav-pill a { font-size: 12px; }

  #classes { grid-template-columns: minmax(0, 1fr); }
  .classes-head { display: block; }
  .classes-sticky { position: static; margin-bottom: 34px; }
  .classes-intro,
  .class-group { grid-column: 1; }
  .classes-intro { max-width: none; }

  .mask2 { margin-bottom: calc(var(--fs-display) * -0.3); }

  /* the band stacks: photo on top, then the pitch */
  #students { grid-template-columns: minmax(0, 1fr); }
  .student-media { min-height: clamp(220px, 46vw, 340px); }
}

/* The pill stays on small screens — hiding it left the page with no navigation
   at all. The logo keeps its rail, the pill takes the rest and scrolls sideways
   once the six links overrun it, and JOIN NOW drops (the same call sits in the
   hero circle and again in the CTA footer). Two more destinations than the
   original four means this has to start well before the phone widths. */
@media (max-width: 820px) {
  .nav {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 12px;
  }
  .nav-logo img { height: 24px; }
  .nav-join { display: none; }

  .nav .nav-pill {
    justify-self: end;
    gap: 12px;
    /* the vertical padding moves onto the links so their own box carries the
       touch target — see the min-height below */
    padding: 0 14px;
    /* against the grid track, not the viewport — a vw cap lets the pill
       overflow its column and slide under the logo. The 18px it gives back is
       the clear rail the nudge arrow sits in, so the arrow never lands on a
       half-faded word. */
    max-width: calc(100% - 18px);
    margin-right: 18px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* the same chain-to-back-gesture as the two rails: this one is the site's
       only navigation, so flicking out of it is the worst place to land */
    overscroll-behavior-inline: contain;
    /* a flick lands on a link rather than mid-word */
    scroll-snap-type: x proximity;
    /* both the snap points and the browser's own scroll-into-view stop short
       of the fades, so a focused or snapped link is never parked under one */
    scroll-padding-inline: 16px 26px;
    /* Seven links in a 180px box means five of them start off-screen, and the
       scrollbar is hidden. The fade is the Burn Lab chip rail's affordance
       moved onto a rounded silhouette: a mask rather than ::before/::after
       overlays, because a rectangular gradient over a pill reads as a seam.
       The widths are custom properties so the JS state classes can retract
       each end once there is nothing more that way. */
    --pill-fade-l: 0px;
    --pill-fade-r: 26px;
    -webkit-mask-image: linear-gradient(to right,
      transparent 0, #000 var(--pill-fade-l),
      #000 calc(100% - var(--pill-fade-r)), transparent 100%);
            mask-image: linear-gradient(to right,
      transparent 0, #000 var(--pill-fade-l),
      #000 calc(100% - var(--pill-fade-r)), transparent 100%);
  }
  .nav.pill-scrolled .nav-pill { --pill-fade-l: 16px; }
  .nav.pill-at-end .nav-pill { --pill-fade-r: 0px; }

  /* The fades exist to tell a thumb there is more sideways. While someone is
     tabbing the pill they only obscure the thing that has focus, and the
     minimum scroll that brings a link into view can still leave it inside one.
     So the pill shows its whole self for as long as focus is in it. */
  .nav .nav-pill:focus-within { --pill-fade-l: 0px; --pill-fade-r: 0px; }

  .nav .nav-pill::-webkit-scrollbar { display: none; }

  .nav .nav-pill a {
    font-size: 12px;
    white-space: nowrap;
    /* start, like the chip rail's chips: a centre-aligned snap point fights
       the browser's own scroll-into-view when a link is focused, and pulls
       the half-revealed one straight back off the edge */
    scroll-snap-align: start;
  }

  /* A fade says "there is more" quietly enough to miss, and missing it costs
     five of the seven destinations. The nudge is the .rail-hint arrow from the
     chip rail, pinned outside the scroller so it cannot scroll away, sitting
     over the faded end where there is no readable text to collide with. It
     retracts with the fade once the pill is at its end. */
  .nav::after {
    content: "→";
    position: absolute;
    right: var(--gutter);
    top: 50%;
    z-index: 1;
    margin-top: -0.62em;
    font-family: var(--font-body);
    font-size: 13px;
    line-height: 1.2;
    color: var(--accent);
    pointer-events: none;
    transition: opacity 0.35s var(--ease);
    animation: rail-nudge 1.6s ease-in-out infinite;
  }
  .nav.pill-at-end::after { opacity: 0; }

  /* =======================================================
     HERO ON A PHONE
     Client, on the reel as first shipped: "push the video down, half is gone
     because of tint, and there is unnecessary space on mobile." Three
     sentences, and measurement says three separate causes — all of them real.
     ======================================================= */

  /* 1. The space. 133px of padding above the type is a desktop proportion; at
        390 it left a 59px black gap between the nav and the headline, on top
        of the black band the type already sits in. The display size does not
        move — only the air above it. */
  .hero-type { padding-top: calc(var(--nav-h) + clamp(18px, 3.5vh, 40px)); }

  /* 2. The height. More of the screen goes to the footage. Safe to grow only
        because of 3 below: without the explicit crop, a taller box flips
        object-fit: cover from cropping vertically to cropping horizontally,
        and the phone would get *more* ceiling, not less. */
  .hero-media { height: clamp(52vh, 60vh, 64vh); }

  /* 3. The framing — the real "push the video down". The portrait crop is
        ceiling-heavy: measured band by band, the top third of every frame is
        joists, ducting and an air-conditioner, and the equipment, floor and
        people are all in the lower two thirds. object-fit: cover centres, so a
        phone was watching the ceiling. This is a crop, not a re-encode: the
        element is made much taller than its box and pulled up, so the window
        lands on rows 30%-86% of the source frame. The overflow either side
        still clears the parallax's ±10%. */
  .hero-video {
    height: 155%;
    top: -36%;
  }

  /* 4. The tint. The scrim is 40% of the media starting at solid --black: on a
        439px box that painted out the top 176px, and the client counted it as
        "half gone" and as more empty space. It is only there to keep BELONGS.
        readable, and that line overlaps the media by **9px** at these widths —
        176px of black to protect 9px of type.
        It cannot simply be lightened for everyone, though: with the scrim off,
        the fallback *photograph* measures 2.40:1 behind that line, under the
        3:1 large-text floor. The photo genuinely needs it. So the light scrim
        is scoped to the video actually playing, where the same measurement is
        11.88:1 with no scrim at all. Where :has() is unsupported the selector
        never matches and the heavy scrim stays — the safe direction. */
  .hero-media:has(.hero-video.is-playing)::after {
    height: 88px;
    background: linear-gradient(to bottom,
      rgba(10, 10, 10, 0.92),
      rgba(10, 10, 10, 0.4) 40%,
      transparent);
  }

  /* The Floor's frames are sized by height with the aspect ratio deciding the
     width, which is right on a desktop and backwards on a phone: a taller
     phone makes them wider. Landscape frames measured 557 at 360, 734 at 390
     and 811 at 430 — 1.89x the viewport — so six of the ten could never be
     seen whole, and the first bled off both edges with no neighbour showing,
     which reads as a full-bleed photo rather than as a rail. It also made
     scroll-snap-align: center inert, since a snap area larger than the
     snapport cannot force a rest position.

     Below 820 the axis flips: width leads, the aspect ratio gives the height
     back, and the vw caps keep the tallest frame inside the screen with a
     neighbour peeking. The h1/h2/h3 rhythm is preserved — h3 stays the big
     tier, h2 the small one. */
  .floor-media { height: auto; width: var(--w); }
  .is-land.h1 .floor-media { --w: min(78vw, 420px); }
  .is-land.h2 .floor-media { --w: min(68vw, 360px); }
  .is-land.h3 .floor-media { --w: min(86vw, 460px); }
  .is-port.h1 .floor-media { --w: min(46vw, 240px); }
  .is-port.h2 .floor-media { --w: min(40vw, 210px); }
  .is-port.h3 .floor-media { --w: min(52vw, 270px); }
}

/* =========================================================
   TOUCH TARGETS (WCAG 2.5.8)
   Width alone does not describe a touch device: the same phone that is 390px
   wide in portrait is 844px wide in landscape, with the same thumb on it. So
   these fire below 820px, where they can be measured in a browser, AND on any
   coarse pointer at any width. Nothing in here is a layout rule — the mobile
   header layout stays width-gated above, so a 1024px tablet keeps the desktop
   header rather than losing JOIN NOW to a media query about fingers.
   ========================================================= */
@media (max-width: 820px), (pointer: coarse) {
  /* the pill's 8px of vertical padding was on the pill, so the links inside it
     measured 46-83.6 x 18.6. The padding moves onto them; the pill's own
     height is unchanged, 44 + 2px border either way. */
  .nav .nav-pill,
  .footer-pill { padding-top: 0; padding-bottom: 0; }
  .nav .nav-pill a,
  .footer-pill a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
  /* the underline rides the text, not the taller box */
  .nav .nav-pill a::after,
  .footer-pill a::after { bottom: 10px; }
  /* the footer pill wraps at 640, and 44px rows do not need 18px between them
     as well — only the row gap changes, the column gap is the pill's own */
  .footer-pill { row-gap: 4px; }

  /* Everything below is the same problem in eight more places, all measured at
     390: a 12-14px label whose box is the label. Where the element's own box is
     also the visual - a rule under it, a bordered plate - the padding comes
     with a matching negative margin, so the target grows and the layout does
     not move. */

  /* 17px, and for a gym tap-to-call is the conversion path. The links are
     stacked on <br> inside a <p>, so each becomes its own 44px line box. */
  .contact-block a {
    display: inline-block;
    min-height: 44px;
    line-height: 44px;
  }

  /* 19px, on all four tier cards, three of them tel: links */
  .tier-buy {
    display: inline-block;
    padding: 13px 0;
    margin: -13px 0;
  }

  /* 20px and 19px, bare text in the footer rows */
  .footer-credit {
    display: inline-block;
    padding: 13px 0;
    margin: -13px 0;
  }

  /* 18.6px each, bare text buttons in the Fuel Lab */
  .tray-clear,
  .offset-cycle {
    padding: 13px 4px;
    margin: -13px -4px;
  }

  /* 42.6px — misses by 1.4 */
  .seg-opt span { padding: 13px 6px; }

  /* 35.5px at its shortest. The border-top is the visual, so the box grows
     downward from it rather than the rule moving. */
  .more {
    display: flex;
    align-items: center;
    min-height: 44px;
  }

  /* 25.6px. The bottom border is the underline and must not move, so all of
     the height goes on top and comes back out of the margin: the 22px gap
     above it at every width in this range is unchanged. */
  .results-cta {
    padding-top: 19px;
    margin-top: 3px;
  }

  /* 37px, absolutely placed inside the map tile, so it grows for free */
  .map-link { padding: 13px 14px; }

  /* Every slider on the page - both Burn Lab dials, the Fuel Lab body weight
     and the Price the Plate guess - measured 308 x 26 with an 18px thumb.
     The 26px box is only a target on Chrome Android, which jumps-to-tap; iOS
     Safari has no jump-to-tap, so the drag has to *begin on the thumb*, and
     the thumb was an 18px box around a 10px dot. That was the worst target on
     the site. Both numbers go to 44: the box, and the transparent pad around
     the painted dot. Neither the 2px track nor the dot moves. */
  .lab-range { height: 44px; --thumb: 44px; }

  /* the end labels keep their distance from the hairline rather than from the
     input's new edge. pointer-events: none so the 9px they take back is still
     live slider - they are labels, nothing taps them. */
  .lab-scale { margin-top: -9px; pointer-events: none; }

  /* The tray's − / + / × measured 28 x 28. The mark here is not just the glyph
     but the hairline square around it, and a 44px square with a 14px glyph in
     it reads nothing like the rest of this instrument. So the button grows to
     44px and hands the square to an inset ::before, which keeps the drawn
     control at exactly the 28px it is everywhere else while the target
     underneath it is full size. The gap goes to 0 because the buttons now
     carry 8px of their own on every side. */
  .step-btn {
    position: relative;
    width: 44px;
    height: 44px;
    border-color: transparent;
  }
  .step-btn::before {
    content: "";
    position: absolute;
    inset: 8px;
    border: 1px solid var(--rule);
    transition: border-color 0.25s var(--ease);
  }
  .step-btn:hover::before { border-color: var(--accent); }
  .step-btn::before { border-color: var(--rule); }
  /* × empties the row and sat 8px from +, which is the repeat-tap control.
     At 44px the two targets would abut outright, so the destructive one gets
     a dead zone: 12px of nothing between the boxes, 28px between the drawn
     squares. */
  .step-btn.is-drop { margin-left: 12px; }
  .step-btn.is-drop::before { border-color: transparent; }
  .tray-steps { gap: 0; }
}

@media (max-width: 640px) {
  :root {
    --fs-display: clamp(3.2rem, 15vw, 6rem);
    --fs-section: clamp(2.8rem, 13vw, 5.2rem);
    --fs-row: clamp(2rem, 9vw, 3.4rem);
    --fs-marquee: clamp(2.2rem, 11vw, 4rem);
  }

  .bonus-cluster { grid-template-columns: minmax(0, 1fr); margin-top: 56px; }
  .card-1, .card-2, .card-3, .card-4 { margin-top: 0; }
  .card-1 { transform: rotate(-3deg); }
  .card-2 { transform: rotate(2.5deg); }
  .card-3 { transform: rotate(-2deg); }
  .card-4 { transform: rotate(3deg); }

  .tier-grid { grid-template-columns: minmax(0, 1fr); }
  .tier-card,
  .tier-card:nth-child(2),
  .tier-card:last-child { border-right: 1px solid var(--rule); }

  .accordion-grid { grid-template-columns: minmax(0, 1fr); }
  .contact-card { flex-direction: column; gap: 22px; }

  .row-meta { gap: 20px; }
  .meta-pair { padding: 10px 0 14px; }

  /* three stat columns are too narrow to read at 360px */
  .lab-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 14px; }
  /* the rail turns into stacked rows, each rule now horizontal */
  .stat-rail { grid-template-columns: minmax(0, 1fr); }
  .stat {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    padding: 20px 0;
    border-left: 0;
    border-top: 1px solid var(--rule);
  }
  .stat:first-child { border-top: 0; }
  .food-note { margin-left: 0; flex-basis: 100%; }

  /* two menu cells to a row, and the tray row gives its steppers their own
     line so the food's name is never the thing that gets squeezed */
  .menu-grid.is-built { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
  .fc-kcal { font-size: 1.5rem; }

  .tray-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
  .tray-steps { grid-column: 1 / -1; grid-row: 2; justify-self: start; }
  .offset-mode { margin-left: 0; flex-basis: 100%; }
  .score-meta { gap: 20px; }

  /* full-bleed strip between the lines rather than a stamp beside them */
  .cta-inline-img { width: 100%; height: 110px; margin: 4px 0 8px; }
  .cta-form { gap: 22px; }
  .cta-submit { width: 100%; }

  .footer-top { flex-direction: column; align-items: flex-start; }
  .footer-pill {
    flex-wrap: wrap;
    padding: 0;
    border: 0;
    background: none;
    gap: 18px;
  }

  #join-cta {
    right: var(--gutter);
    bottom: -28px;
    width: 132px;
  }
  /* 10px sat under the 11px floor the rest of the system holds, and a 9em cap
     on a 132px circle broke the label after the hyphen: FREE 1- / DAY / TRIAL.
     Three lines in the hero's only conversion target. */
  .join-cta-label { font-size: 11px; letter-spacing: 0.12em; max-width: 11em; }
}

/* Turn the same phone sideways and it is 844px wide, out of the block above,
   and the label goes back to three lines broken after the hyphen — this time
   in a 186px circle with room to spare, because the 9em cap is the constraint
   rather than the circle. A viewport both under 900px wide and under 520px
   tall is a phone in landscape; no desktop window is that short. */
@media (max-width: 900px) and (max-height: 520px) {
  .join-cta-label { max-width: 11em; }
}

@media (max-width: 400px) {
  :root { --gutter: 18px; }
  .nav-logo img { height: 24px; }
  .footer-logo img { height: 32px; }
  .mq-img { width: 120px; height: 78px; }
}

/* =========================================================
   REDUCED MOTION
   ========================================================= */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  /* the `*` selector above never reaches pseudo-elements, and a surviving delay
     would leave a staggered transition looking stuck rather than instant */
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  *,
  *::before,
  *::after {
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }
  .marquee-track { will-change: auto; }
}
