/* ===========================
   GRIPswich — Base Styles
   Color palette carried over from the gym's previous website:
   bg: gray-900 #111827 / gray-800 #1f2937 / gray-950 #030712
   orange: #f97316 (500) / #fb923c (400)
   teal: #14b8a6 (500) / #2dd4bf (400)
   =========================== */

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  /* The public site is dark by design — there is no light theme to switch to.
     Saying so stops a browser inventing one. A page that never declares a
     color-scheme is fair game for Chrome's Auto Dark Theme and for the "dark
     mode" extensions people run, which repaint it: verified 24 Aug 2026, that
     turned the teal/orange brand logo white and flattened the hero. Declaring
     dark also hands every NATIVE control — date pickers, select popups,
     scrollbars — the dark rendering that five scattered rules used to patch one
     element at a time (one here, three in groups.css, one inline on a <select>);
     all five are gone. 500.html and console/invoice_print.html keep their own,
     because they are standalone documents that load no stylesheet at all. */
  color-scheme: dark;

  --clr-bg:       #111827;  /* gray-900 */
  --clr-bg-alt:   #1f2937;  /* gray-800 */
  --clr-bg-dark:  #030712;  /* gray-950 */
  --clr-surface:  #1f2937;  /* gray-800 for cards */
  --clr-border:   #374151;  /* gray-700 */

  --clr-orange:   #f97316;  /* orange-500 */
  --clr-orange-h: #ea6c0a;  /* orange-600 */
  --clr-orange-4: #fb923c;  /* orange-400 (hover text) */

  --clr-teal:     #14b8a6;  /* teal-500 */
  --clr-teal-h:   #0d9488;  /* teal-600 */
  --clr-teal-4:   #2dd4bf;  /* teal-400 */

  --clr-text:     #f3f4f6;  /* gray-100 */
  --clr-text-2:   #e5e7eb;  /* gray-200 */
  --clr-muted:    #9ca3af;  /* gray-400 */
  --clr-muted-3:  #d1d5db;  /* gray-300 */

  --font: 'Inter', system-ui, sans-serif;
  --nav-h: 6rem;            /* nav height; logo drives it. Was 7rem — trimmed so
                              the sticky chrome eats less of a short/landscape
                              viewport (audit C16). Grows on wide screens below. */
  /* Full-width layout: content spans the page rather than a centred 80rem
     ribbon. --gutter is the side margin — small on phones, generous on desktop
     so text never runs to the screen edge. Doubled from the first pass to pull
     the content in from the edges a little more. */
  --gutter: clamp(2rem, 8vw, 10rem);
  /* Reading measure: sections go full width, but a run of body prose still caps
     its line length here so it stays readable on a 2560px screen (audit
     C39/C10/C42/C49). ~68 characters at the base body size. */
  --measure: 68ch;
  --radius: 8px;
}

/* The nav can afford its taller, brand-sized logo once the full link row fits
   on one line — measured at 1080px, so that's the switch for all nav chrome. */
@media (min-width: 1080px) { :root { --nav-h: 7rem; } }

/* Landscape phones (~390px tall). The visit bar + nav ride together in one
   pinned .site-chrome block, which came to 142px of a 393px screen on the
   public site and 201px in the portal — half the viewport, permanently, with
   the hamburger drawer's last items pushed below the fold (device audit H1).
   Same max-height: 560px axis the heroes and the waiver pane already use. */
@media (max-height: 560px) {
  :root { --nav-h: 3.5rem; }
  .nav__logo img { height: 3rem; }
}

html { scroll-behavior: smooth; }

body {
  font-family: var(--font);
  background: var(--clr-bg);
  color: var(--clr-text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main { flex-grow: 1; }

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

/* Links used to be given no colour at all here, only their underline removed —
   so any anchor without a .btn or nav class fell back to the browser default
   #0000EE on the #111827 ground: 1.89:1, with no underline either, leaving no
   affordance whatsoever (device audit M3). Nav, buttons and cards set their own
   colour at higher specificity and are unaffected. */
a { color: var(--clr-teal-4); text-decoration: none; }

/* A link inside running prose keeps its underline — colour alone is not a
   reliable signal, and a link mid-sentence has nothing else marking it. Lists
   (footer, nav) are left alone: there, position already says "link". */
p a:not(.btn) { text-decoration: underline; text-underline-offset: 0.15em; }
p a:not(.btn):hover { color: var(--clr-orange-4); }

/* ===========================
   Container
   =========================== */

/* Full-width by default — content fills the viewport, held off the edges by the
   fluid --gutter. margin-inline:auto is a no-op at full width, but re-centres any
   container that opts into a max-width (e.g. the reading-column sections on
   first-visit / waiver use `max-width: 56rem`), so those sit in the middle of the
   page rather than stranded at the left gutter. */
.container {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Reading sections cap their own width for readability (the 46–68rem inline
   max-widths on first-visit / waiver / youth). The big page gutter is for
   full-bleed content — applied on top of a max-width it double-insets the
   content into a skinny centre column, so these get a small edge padding only
   (the max-width already holds them off the edges on wide screens). */
.container--read { padding-inline: clamp(1.25rem, 4vw, 2rem); }

/* ===========================
   Buttons
   =========================== */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 20px;
  border-radius: var(--radius);
  font-size: 0.875rem;
  font-weight: 700;
  font-family: var(--font);
  cursor: pointer;
  border: 1px solid transparent;
  transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s;
  white-space: nowrap;
}

/* Narrow phones: a nowrap label that is wider than the column it sits in has
   nowhere to go, so it hangs off the right of whatever holds it. At 320px that
   was three separate breakages from one rule — "View All Pricing &
   Memberships" pushed the home page 5px sideways, "Create Your Account & Sign
   the Waiver" pushed first-visit 18px, and inside the home cards "Explore Youth
   Programs" simply stuck 21px out through the side of its card. The three
   class-feedback rating buttons hit it from the other direction: they are
   `flex: 1 1 0; min-width: 0` thirds, so nowrap spilled each label out of its
   own button.

   At 26rem and under, the label is allowed onto a second line instead. That
   band covers the 390px phone as well as the 320px one, and deliberately so —
   nothing visibly changes there, because `normal` only breaks a line the text
   genuinely cannot fit on, and every one of these labels still fits on one at
   390. Above 26rem the widest label in the site clears the column with room to
   spare, so the rule has nothing to do and stays out of the way. */
@media (max-width: 26rem) {
  .btn { white-space: normal; text-align: center; }
}

.btn--orange {
  background: var(--clr-orange);
  /* Dark ink, not white. White on #f97316 is 2.80:1 and on #14b8a6 is 2.49:1 —
     both well under the 4.5:1 WCAG AA needs, on the site's two primary calls to
     action (device audit M4). Inking them with the page ground keeps the brand
     hues exactly as they are and lands at 6.3:1 / 7.1:1. */
  color: var(--clr-bg);
}
.btn--orange:hover { background: var(--clr-orange-h); }

.btn--orange svg { transition: transform 0.15s; }
.btn--orange:hover svg { transform: translateX(3px); }

.btn--lg {
  padding: 12px 32px;
  font-size: 0.9375rem;
  border-radius: 6px;
}

.btn--teal {
  background: var(--clr-teal);
  color: var(--clr-bg);   /* see .btn--orange — 2.49:1 as white, 7.1:1 as ink */
}
.btn--teal:hover { background: var(--clr-teal-h); }

.btn--teal-outline {
  background: transparent;
  color: var(--clr-teal-4);
  border-color: var(--clr-teal-4);
}
.btn--teal-outline:hover {
  background: rgba(45, 212, 191, 0.1);
}

/* ---- Skip link ----
   First tab stop on every page, off-screen until focused (device audit L1). */
.skip-link {
  position: absolute; left: .75rem; top: -4rem; z-index: 100;
  padding: .6rem 1.1rem; border-radius: var(--radius);
  background: var(--clr-orange); color: var(--clr-bg);
  font-weight: 700; text-decoration: none;
  transition: top .15s;
}
.skip-link:focus { top: .75rem; }

/* ===========================
   Nav
   =========================== */

.nav {
  background: rgba(17, 24, 39, 0.8);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: 1px solid var(--clr-border);
}

.nav__inner {
  /* Full-width like everything else — the nav row spans the page with the same
     fluid gutter, no centred 80rem cap. */
  padding-inline: var(--gutter);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* Desktop: logo, links and account actions sit together as one centred cluster
   with even spacing between them — not pinned to the far edges — so the bar
   reads as a single unit rather than three spaced-apart sections. Below 1080px
   the bar stays flex (logo + hamburger, space-between) because the links and
   actions collapse into the drawer. */
@media (min-width: 1080px) {
  .nav__inner {
    justify-content: center;
    gap: 2.5rem;
  }
}

.nav__logo img {
  height: 5rem;
  width: auto;
}

@media (min-width: 1080px) { .nav__logo img { height: 7rem; } }

.nav__links {
  display: none;
  align-items: center;
  gap: 1.5rem;
}

/* The full nav appears only once it genuinely fits on one line — measured at
   1080px. Below that, iPad portrait included, it wrapped mid-label (audit C05),
   so those widths get the hamburger drawer instead. */
@media (min-width: 1080px) { .nav__links { display: flex; gap: 2rem; } }

.nav__links a {
  font-size: 0.8125rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  /* Three-state colour scheme: an unselected link is white, hover paints it
     orange, and the current page is teal (below) — so "where I'm pointing"
     and "where I am" never share a colour. */
  color: #fff;
  position: relative;
  transition: color 0.15s;
}
.nav__links a:hover { color: var(--clr-orange-4); }
/* The active tab wins over hover: same specificity, declared later, so the
   current page stays teal even under the cursor. */
.nav__links a.active { color: var(--clr-teal-4); }

.nav__links a.active::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  bottom: -8px;
  height: 2px;
  background: var(--clr-teal-4);
  border-radius: 2px;
}

/* Account actions (Member Login + Join Now) are desktop chrome; below 1080px
   they live in the hamburger drawer instead, same breakpoint as the links. */
/* Below 1080px the account pair collapses — but NOT the account button itself.
   Signing in is the most common thing a returning member does on a phone, and
   with the whole cluster hidden the only ways to it were the hamburger (one
   tap, if you think to open it) and the FOOTER, six thousand pixels down. So
   the button stays, between the logo and the hamburger, and only the Join CTA
   folds away into the drawer — Join is a once-ever action and it is the first
   thing in the menu.

   One button, two jobs: it says "Member Login" signed out and "My Details"
   signed in (see base.html), which is the same affordance in the same place
   either way rather than something that appears and disappears. */
.nav__actions { display: flex; align-items: center; gap: .5rem; }
.nav__actions .nav__join { display: none; }
@media (min-width: 1080px) {
  .nav__actions { display: flex; align-items: center; gap: 0.75rem; }
  .nav__actions .nav__join { display: inline-flex; }
}

.nav__hamburger {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--clr-muted-3);
  /* 44px minimum touch target — it's the only nav on phone and tablet (C27). */
  width: 44px;
  height: 44px;
  margin-right: -4px;
  transition: color 0.15s;
}
.nav__hamburger:hover { color: var(--clr-orange-4); }

@media (min-width: 1080px) { .nav__hamburger { display: none; } }

/* The full nav appears at 1080px, which on an iPad Air or Pro in landscape is a
   TOUCH screen — and its links were 21px of text height. Touch is inferred from
   width here, so the fix is to ask the pointer directly (device audit M6). */
@media (pointer: coarse) {
  .nav__links a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* Mobile menu */
.nav__mobile {
  display: none;
  flex-direction: column;
  gap: 0;
  border-top: 1px solid var(--clr-border);
  background: var(--clr-bg);
  padding: 16px var(--gutter);
  /* Cap the drawer to the space under the sticky chrome and let it scroll —
     otherwise on a short/landscape phone it runs off the bottom and the last
     items (Member Login, Join Now) can never be reached (audit C02). dvh tracks
     the real viewport as mobile browser bars collapse; vh is the fallback. */
  max-height: calc(100vh - var(--nav-h) - var(--visitbar-h, 46px) - 16px);
  max-height: calc(100dvh - var(--nav-h) - var(--visitbar-h, 46px) - 16px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.nav__mobile.is-open { display: flex; }

/* When the drawer is open, lock the page behind it so a scroll gesture moves
   the menu, not the page under it (paired with the JS toggle in main.js). */
body.nav-open { overflow: hidden; }

.nav__mobile a {
  padding: 14px 8px;
  color: var(--clr-muted-3);
  font-size: 1rem;
  font-weight: 500;
  border-bottom: 1px solid var(--clr-border);
  transition: color 0.15s;
}
/* The Join Now CTA is the one <a> that's a real button — centre its label and
   give it button padding rather than the row's, so it doesn't read as a
   full-width bar with the text pinned left (audit C66). */
.nav__mobile a.btn { justify-content: center; border-bottom: none; margin-top: 14px; padding: 14px 20px; }
.nav__mobile a:hover { color: var(--clr-orange-4); }

/* ===========================
   Hero
   =========================== */

.hero {
  position: relative;
  /* svh tracks the real viewport (mobile browser bars collapse); the cap stops
     the hero becoming a near-empty photo island on tall desktops (C60).
     min-height keeps the CTA clearing the fold on small/landscape phones
     instead of being pushed under it (audit C15). Each value is the audited
     baseline (85svh / 900px / 34rem) scaled taller per successive requests
     (+30%, then +20% again) — each value multiplied together, so the bump is
     a uniform ~+56% over baseline across desktop, laptop and phone rather than
     only where one term governs. Content stays vertically centred via the
     flex rules below, so the headline + CTA sit mid-hero even though the band
     now runs past the viewport on standard screens. */
  height: min(132svh, 1404px);
  min-height: 52.8rem;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  overflow: hidden;
}

/* Landscape / short phones: don't demand 34rem of height when the whole screen
   is ~390px — let the hero shrink to the viewport so content below is reachable
   (audit C15/C17). */
@media (max-height: 560px) {
  .hero { min-height: 0; height: auto; padding-block: 3rem; }
}

/* Phones and tablets: the hero is exactly as tall as its own words.
   132svh is a DESKTOP measurement. Below 1080px the band above the hero is
   four stacked strips — announcement, notice, opening-hours status, nav — and
   on a 390x844 phone that came to 422px before the photo even started. The
   hero then ran 1114px with the headline centred inside it, so 349px of bare
   photo sat above the words and the same below: the first thing anyone saw on
   opening gripswich.au was the ceiling ducting, and "The centre of Ipswich's
   climbing community" began 771px down a 844px screen (Keiran, 2026-09-09).
   Height auto makes the section the height of the content, which puts the
   headline straight under the nav and, because object-fit crops what it is
   given, swaps the roof for the climbers the photo is meant to show.
   1080px is the same line the nav switches on, so "mobile" means one thing.

   The padding is the second half of that fix and went in a day later (Keiran,
   2026-09-10). Height auto with no padding is literally the height of the
   words, which put the headline hard against the nav above it and the CTA hard
   against the band below — the section read as a caption wedged between two
   others rather than as the top of the page. 1rem is enough to separate them
   and small enough that the dead-photo problem above does not come back: the
   band it adds is 32px against the 698px this rule removed. */
@media (max-width: 1079px) {
  .hero { height: auto; min-height: 0; padding-block: 1rem; }
}

.hero__overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 1;
}

.hero__bg-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The hero photo's people sit left-of-centre and low in the frame. Wide
     viewports only crop vertically (Y bites, X doesn't) and narrow ones only
     crop horizontally (X bites, Y doesn't), so one pair holds both: keep the
     climbers on phones, drop dead ceiling on desktop. */
  object-position: 30% 58%;
  z-index: 0;
}

.hero__content {
  position: relative;
  z-index: 2;
  padding-inline: 16px;
}

.hero__content h1 {
  /* Floor lowered to 2.25rem and slope raised to 9vw so the long five-word
     headline doesn't freeze at 40px and stack into four near-single-word lines
     on a 320–375px phone (audit C35). 8vw also saturated its cap at a 900px
     viewport, leaving the headline a small island on desktop — the ≥1280px rule
     below resumes the scale (audit C60). */
  /* Every clamp term is the prior value ×0.8, i.e. a uniform 20%-smaller
     headline at every viewport width, per request. */
  font-size: clamp(1.8rem, 7.2vw, 3.6rem);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  line-height: 1.02;
  margin-bottom: 16px;
}

.hero__content p {
  /* Also ×0.8 across the clamp — 20% smaller than before, matching the h1. */
  font-size: clamp(0.84rem, 2vw, 1.08rem);
  color: var(--clr-text-2);
  max-width: 46rem;
  margin-inline: auto;
  margin-bottom: 16px;
}

/* Wide desktops: resume scaling the hero above the point 8vw saturated, floored
   at today's values so nothing below 1280px changes (audit C60). Both clamps
   are the prior wide-desktop values ×0.8 to hold the 20% reduction up here too. */
@media (min-width: 1280px) {
  .hero__content h1 { font-size: clamp(3.6rem, 3.68vw, 5.2rem); }
  .hero__content p  { font-size: clamp(1.08rem, 0.92vw, 1.28rem); }
}

.hero__badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--clr-teal-4);
  font-size: 1.125rem;
  font-weight: 600;
  margin-bottom: 2rem;
}

/* teal-to-orange gradient on the key phrase — both brand colours, one statement.
   box-decoration-break: clone makes each wrapped line paint the full gradient
   instead of getting one sliced portion of it. */
.hero__highlight {
  background: linear-gradient(90deg, var(--clr-teal-4), var(--clr-orange-4));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* Belt and braces on the biggest piece of type on the site. `color: transparent`
   is only legible because the gradient paints through it — strip the gradient
   and half the h1 renders in nothing at all. badges.css guards its shiny label
   the same way; the headline gets both guards because two different things
   strip it: an engine with no background-clip support, and forced-colors, which
   drops background-image but does NOT reset -webkit-text-fill-color, so the
   glyphs stay transparent unless we hand them back to currentColor. */
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .hero__highlight { color: var(--clr-teal-4); background: none; }
}
@media (forced-colors: active) {
  .hero__highlight {
    background: none;
    color: CanvasText;
    -webkit-text-fill-color: currentColor;
  }
}

/* breathing room now the CTA sits directly under the tagline */
.hero__cta { margin-top: 0.75rem; }

/* proof chips over the hero photo — frosted so they read on any image */
.hero__chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  list-style: none;
  margin-block: 0.75rem 0.5rem;
}

.hero__chips li {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1.1rem;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  background: rgba(17, 24, 39, 0.55);
  backdrop-filter: blur(4px);
  color: var(--clr-text-2);
  /* ×0.8 with the headline and paragraph — 20% smaller chip labels. */
  font-size: 0.75rem;
  font-weight: 600;
}

.hero__chips svg { color: var(--clr-teal-4); flex-shrink: 0; }

/* "No bookings required — just show up!" — a teal callout between the proof
   chips and the CTA. Kept at its own size (not part of the 20% shrink) so it
   stays legible now that the chips above it are smaller. Selector carries the
   parent class so it beats `.hero__content p`, which would otherwise win on
   specificity and repaint this grey at the paragraph size. */
.hero__content .hero__nobook {
  color: var(--clr-teal-4);
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: 0.01em;
  max-width: none;
  margin: 0.4rem auto 0;
}

/* ===========================
   Audience Cards
   =========================== */

.cards-section {
  padding-block: 5rem;
  background: var(--clr-bg-alt);
}

/* Capped and centred rather than full-bleed: the three cards cluster in the
   middle of the page instead of stretching landscape across it, and the wide
   gap between them makes each card narrow — a squarish (not square) portrait
   whose exact height is set by its own copy. auto-fit + a min(100%, …) floor
   still collapses 3 → 2 → 1 columns on narrower screens and never overflows
   (the floor is what fixed the old repeat(3,1fr) overflow, audit C01). */
.cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 3rem;
  max-width: 70rem;
  margin-inline: auto;
}

.card {
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  transition: border-color 0.2s;
}
.card:hover { border-color: var(--clr-teal-4); }

.card__icon {
  color: var(--clr-teal-4);
  margin-bottom: 1rem;
}

/* h2 and h3 styled identically on purpose. The home page's three audience
   cards are top-level sections of that page and are marked up as h2 (an h1
   followed by h3s skips a level, which a screen reader reads as a missing
   section); the password-reset cards sit under their own heading and stay h3.
   Both must look the same — the card is a visual unit, not an outline level. */
.card h2,
.card h3 {
  font-size: 1.5rem;
  font-weight: 700;
  color: #fff;
  margin-bottom: 0.75rem;
}

.card p {
  color: var(--clr-muted);
  font-size: 0.9375rem;
  flex-grow: 1;
  margin-bottom: 1.5rem;
  max-width: none;
}

/* Wrapper added around each audience card's icon/heading/copy/button when the
   video overlay landed. It has to carry the flex layout the card itself used to
   apply directly, because .card p's `flex-grow: 1` — the thing that pushes every
   card's button down to a common baseline — only works on a direct flex child.
   Moving the copy one level down without this would let three cards of unequal
   copy length end up with three buttons at three different heights.

   Also styled for the cards that have NO video: this rule is unconditional, so
   the markup is the same shape whether or not a clip has been chosen, and the
   home page cannot look different depending on what is in the library. */
.card__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  flex: 1 1 auto;
  width: 100%;
}

/* ===========================
   Audience-card video
   ===========================

   Scroll a card into view, wait a second, and its clip fades in over the copy;
   hover it and the copy comes straight back. static/js/home_video.js adds
   `is-playing` once frames are actually going up, and everything below is what
   that one class does.

   The clip is an OVERLAY on top of the card, not a replacement for it. That is
   what makes the swap safe: the button underneath never stops existing, never
   stops being focusable, and — because this layer takes no pointer events —
   hovering the video is hovering the card, so the :hover that brings the copy
   back cannot be defeated by the thing covering it. */

.card--video {
  position: relative;
  overflow: hidden;
  /* Own stacking context, so .card__body's z-index is scoped to this card and
     cannot interact with the sticky header or the visit bar above it. */
  isolation: isolate;
}

.card__video {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.45s ease;
  /* Load-bearing, not a tidy-up — see the block comment above. */
  pointer-events: none;
  background: var(--clr-bg-dark);
}

.card__video video {
  display: block;
  width: 100%;
  height: 100%;
  /* The card is a squarish portrait and the clip is whatever the phone shot;
     cover fills the card and crops, which for a climber on a wall is right —
     `contain` would letterbox a 16:9 clip into a thin strip across the middle. */
  object-fit: cover;
}

.card--video .card__body {
  position: relative;
  z-index: 1;
  transition: opacity 0.3s ease;
}

/* The swap itself, gated on a real pointer.

   A touch screen has no hover, so on a phone this rule set would leave the copy
   at opacity 0 with an invisible-but-tappable button underneath it — the worst
   version of every state here. The coarse-pointer block below shows the clip
   AND the copy instead, which is a different design for a different input
   rather than a degraded copy of this one. */
@media (hover: hover) and (pointer: fine) {
  .card--video.is-playing .card__video { opacity: 1; }
  /* The whole card body goes, headings included, which is the brief: the card
     BECOMES the clip and hovering brings it back. Worth knowing the cost —
     while three clips are playing, "First Timers / Experienced / Kids & Youth"
     is the page's main wayfinding and none of it is on screen for anyone who
     is not pointing at a card.

     If that turns out to be the wrong trade, the smaller version is to fade the
     copy and the button but keep the headings: change the line below to target
     `.card__body > :not(h2)` instead of `.card__body`, and give the h2 a
     text-shadow so it holds up over moving video. Nothing else needs touching. */
  .card--video.is-playing .card__body { opacity: 0; }

  /* :focus-within is the keyboard half of the same rule. Tab towards the card's
     button and the copy is back before focus lands on it. */
  .card--video.is-playing:hover .card__video,
  .card--video.is-playing:focus-within .card__video { opacity: 0; }
  .card--video.is-playing:hover .card__body,
  .card--video.is-playing:focus-within .card__body { opacity: 1; }
}

/* Touch and stylus: the clip gets its OWN BAND inside the card — under the
   heading, above the copy and the button — rather than playing behind them.

   It used to play behind the copy under a scrim, because a finger cannot hover
   to bring hidden text back and nothing was allowed to be unreadable. That was
   the right constraint and the wrong answer to it. Making the text survive over
   moving video took a scrim reaching 88% black at the bottom, and 88% black is
   not a video any more — it is a dark texture behind a paragraph. Both things
   were degraded to share one rectangle.

   So they stop sharing it. The clip is fully visible because nothing is on top
   of it, and the copy is fully readable because it is on the card's own
   background — no scrim, no text-shadow, no colour override, and the mid-grey
   body text can go back to being mid-grey. The card is taller for it, which on
   a phone costs a scroll and is worth it.

   Desktop is untouched: with a real pointer the card still BECOMES the clip and
   hovering brings the copy back, which is the better design where hover exists.

   The heading sits ABOVE the band rather than under it, so a card reads the way
   an article does: what this is, then the picture of it, then the words and the
   way in. The band used to open the card, which cost a scrolling visitor the
   one line that says which of the three cards they have arrived at — three
   near-identical clips of climbing, and the labels all below the fold of the
   card. The button follows the copy for the same reason it always did: it is
   the last thing you read, so it is the last thing on the card.

   4/3 is a compromise across clips we do not control. A phone-shot 9:16 clip
   would show more of itself in a taller band, but the same band has to hold a
   16:9 GoPro clip without becoming a letterboxed strip. Change the one value if
   the library settles on portrait. */
@media (hover: none), (pointer: coarse) {
  /* The heading has to climb ABOVE the band, and it lives one level down inside
     .card__body. `order` cannot lift it there — order sorts an element among its
     own siblings, and the band is a sibling of its PARENT, so no value put on
     the h2 can ever place it against the clip.

     `display: contents` is what removes that wall: the wrapper's own box goes
     away and its children become flex items of .card itself, in the same
     ordering context as .card__video. Chosen over restructuring the markup
     because the wrapper still has a job on the other side of this query — the
     desktop overlay fades `.card__body` as one unit, headings included, and
     splitting the heading out of it would quietly change that design too.

     Nothing else about the card depends on the box existing. .card already
     centres its items and repeats the text-align, and `.card p { flex-grow: 1 }`
     — the rule that pushes every button down to a common floor — works on a
     direct flex child either way. The wrapper's position/z-index/transition are
     read only by the desktop rules, which never run here. */
  .card--video .card__body { display: contents; }

  /* Only the head moves. The band, the copy and the button all stay at the
     default 0 and therefore in document order behind it, which means anything
     added to the card body later lands BELOW the clip without needing a rule —
     the safe default, since the head is the one part that has a reason to be
     above it. */
  .card--video .card__icon,
  .card--video h2 { order: -1; }

  .card__video {
    /* Out of the absolute layer and back into the flow. */
    position: relative;
    inset: auto;
    align-self: stretch;
    /* Negative side margins cancel the card's 2rem padding, so the band still
       goes edge to edge. Its top corners are square now that the heading is
       above it — there is no card corner left to round against, which is also
       why .card--video no longer drops its top padding. The 0.75rem above adds
       to the heading's own 0.75rem margin to match the 1.5rem below it. */
    margin: 0.75rem -2rem 1.5rem;
    aspect-ratio: 4 / 3;
    /* The wrapper is always visible now — it is a band of the card rather than
       an overlay that appears — so what fades in is the video inside it. */
    opacity: 1;
    /* What the band looks like before a byte of clip exists. Same treatment as
       the featured frame: a panel that has not started, rather than a black
       rectangle that reads as broken. */
    background:
      radial-gradient(
        120% 90% at 50% 0%,
        rgba(20, 184, 166, 0.14) 0%,
        rgba(20, 184, 166, 0) 60%
      ),
      var(--clr-bg-dark);
  }

  /* Absolute inside the (now relative) band. Without this the video keeps the
     base rule's height:100%, which against a parent sized by aspect-ratio is a
     cyclic dependency — it resolves to auto, the clip falls back to its own
     9:16 intrinsic ratio, and a 350px-wide card gets a 620px-tall band that
     drove the aspect-ratio straight off the element. */
  .card__video video {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity 0.45s ease;
  }
  .card--video.is-playing .card__video video { opacity: 1; }
}

/* ===========================
   Class photographs
   =========================== */

/* Two shots from one session, side by side on anything wide enough and stacked
   below that. auto-fit rather than a media query so the breakpoint is the
   content's own minimum rather than a device width somebody has to maintain:
   below ~46rem of container there is no room for two 4:3 photographs to be
   worth looking at, and they stack on their own. */
.class-shots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  gap: 1.25rem;
  margin-top: 3rem;
}

.class-shots__item {
  margin: 0;
}

/* The photographs carry their own intrinsic ratio from the width/height
   attributes in the markup, so no aspect-ratio is set here — cropping them to
   a uniform box would cut the climber out of the second one. */
.class-shots__item img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--clr-border);
}

.class-shots__item figcaption {
  margin-top: 0.6rem;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--clr-muted-3);
}

/* ===========================
   Member of the week
   =========================== */

.motw__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
}

.motw__frame {
  display: flex;
  justify-content: center;
}

/* No aspect-ratio here and none in the template's style attribute: the <video>
   carries width/height ATTRIBUTES from the stored dimensions, which give it an
   intrinsic ratio the browser applies before a byte has loaded. That is what
   lets max-width and max-height both work — clamp either one and the other
   follows — and it is why a portrait phone clip and a landscape GoPro clip can
   share these three rules instead of needing a modifier class each.

   Both caps are needed. Without max-height a 9:16 clip in a 30rem column would
   be a 53rem skyscraper; without max-width a 16:9 one would overflow it. */
.motw__video {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: min(70vh, 34rem);
  border-radius: var(--radius);
  border: 1px solid var(--clr-border);
  /* What the frame looks like before a single byte of video exists, which with
     preload="none" is every page load until something asks it to play. A flat
     near-black read as a broken embed; this reads as a panel that has not
     started yet, which is what it is. It is painted over the instant a poster
     or a first frame arrives, so it costs nothing when there is one. */
  background:
    radial-gradient(
      120% 90% at 50% 0%,
      rgba(20, 184, 166, 0.14) 0%,
      rgba(20, 184, 166, 0) 60%
    ),
    var(--clr-bg-dark);
}

/* The "how do I get on here" line under the feature.

   A rule above it rather than a panel around it: this is a footnote to the
   section, not a second thing competing with the clip and the Join button for
   the same attention. grid-column spans it across both columns at desktop
   widths and across the single one below 860px, so it is under the clip either
   way without needing its own breakpoint. */
.motw__invite {
  grid-column: 1 / -1;
  margin: 0;
  padding-top: 1.75rem;
  border-top: 1px solid var(--clr-border);
  text-align: center;
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--clr-muted-3);
}

/* The two words that decide whether a clip is usable at all. A portrait clip
   is the one thing the layout genuinely cannot work around — .motw__video
   clamps either dimension, but a landscape phone video in a 9:16 frame is
   letterboxed to a strip — so it is worth more than the rest of the sentence. */
.motw__invite strong {
  color: var(--clr-teal-4);
  font-weight: 600;
}

.motw__controls {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  margin-top: 0.75rem;
}

.motw__ctl {
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--clr-muted-3);
  background: transparent;
  border: 1px solid var(--clr-border);
  border-radius: 999px;
  padding: 0.35rem 0.95rem;
  cursor: pointer;
  transition: color 0.2s, border-color 0.2s;
}
.motw__ctl:hover { color: var(--clr-teal-4); border-color: var(--clr-teal-4); }
.motw__ctl:disabled { opacity: 0.4; cursor: default; }

.motw__eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--clr-teal-4);
  margin-bottom: 0.5rem;
}

.motw__name {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  font-weight: 800;
  line-height: 1.1;
  color: #fff;
  margin-bottom: 0.4rem;
}

.motw__headline {
  font-size: 1.125rem;
  color: var(--clr-muted-3);
  margin-bottom: 1rem;
}

.motw__blurb {
  color: var(--clr-muted);
  margin-bottom: 1rem;
  max-width: 46ch;
}

.motw__credit {
  font-size: 0.8rem;
  color: var(--clr-muted);
  margin-bottom: 1.25rem;
}

.motw__cta { align-self: start; }

/* One column below the point where two would make the video column narrower
   than a phone. Video first, then the words — the clip is the reason the
   section exists, and reversing them would put a heading above an empty box
   while it loads. */
@media (max-width: 860px) {
  .motw__inner { grid-template-columns: 1fr; }
  .motw__eyebrow,
  .motw__name,
  .motw__headline { text-align: center; }
  .motw__blurb { margin-inline: auto; text-align: center; }
  .motw__credit { text-align: center; }
  .motw__cta { align-self: center; }
  .motw__text { display: flex; flex-direction: column; }
}

/* Reduced motion, LAST so it outranks the pointer blocks above by source order
   — media queries add no specificity of their own.

   home_video.js already declines to autoplay under this setting, so in practice
   `is-playing` never appears. This is the belt to that braces: the rule that the
   copy stays put does not depend on a script running correctly, which is the
   only way to be sure a visitor who asked for less movement gets it. */
@media (prefers-reduced-motion: reduce) {
  .card--video .card__video { opacity: 0; }
  .card--video .card__body { opacity: 1; }
  .card__video,
  .card--video .card__body,
  .motw__ctl { transition: none; }
}

/* The same setting on a touch screen, where `opacity: 0` is not enough.
   With a pointer the band is an OVERLAY, so making it transparent gives back
   the card underneath and there is nothing left to see. On touch it is a slot
   in the flow with a height of its own, so a transparent one is a 4:3 hole
   sitting between the heading and the copy — it went unnoticed while the band
   opened the card and the hole read as extra padding above the icon.

   So it goes entirely, and the card is the text-and-button card it would have
   been if no clip had ever been placed on it. Which is the answer reduced
   motion is asking for: not a quieter video, no video.

   Written out as two full queries rather than nested inside the block above,
   because a nested @media needs CSS Nesting and this file targets further back
   than that. It sits after the pointer blocks for the same reason that block
   does: source order is what makes it outrank them. */
@media (prefers-reduced-motion: reduce) and (hover: none),
       (prefers-reduced-motion: reduce) and (pointer: coarse) {
  .card--video .card__video { display: none; }
}

/* ===========================
   Designed for Progress
   =========================== */

/* ===========================
   Social / Instagram gallery
   =========================== */

.social-section {
  padding-block: 5rem;
  background: var(--clr-bg);
}

/* Heading row: eyebrow + handle on the left, a Follow button on the right. */
.social-section__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: 2rem;
}

.social-section__eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--clr-teal-4);
  margin-bottom: 0.35rem;
}

.social-section__heading h2 {
  font-size: clamp(1.75rem, 3vw, 2.25rem);
  line-height: 1.15;
  font-weight: 700;
  color: #fff;
}

/* Horizontal, swipeable rail. scroll-snap gives the carousel feel with no JS.
   It spans the page (outside .container) and pads its ends with the gutter, so
   the first tile lines up under the heading and the last can scroll fully in;
   tiles peek off the right edge to signal there's more. */
.social-scroller {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0.5rem var(--gutter) 1.25rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: var(--gutter);
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

.social-scroller > li {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* Slim rail scrollbar so the "there's more" cue isn't a chunky native bar. */
.social-scroller::-webkit-scrollbar { height: 8px; }
.social-scroller::-webkit-scrollbar-track { background: transparent; }
.social-scroller::-webkit-scrollbar-thumb {
  background: var(--clr-border);
  border-radius: 999px;
}
.social-scroller::-webkit-scrollbar-thumb:hover { background: var(--clr-muted); }

.social-tile {
  position: relative;
  display: block;
  width: clamp(15rem, 42vw, 20rem);
  /* Portrait, like an Instagram post, so the rail reads as a feed. */
  aspect-ratio: 4 / 5;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--clr-surface);
}

.social-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s ease;
}
.social-tile:hover img { transform: scale(1.05); }

/* Darken the foot on hover so the imagery reads as interactive. */
.social-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(3, 7, 18, 0.4), transparent 45%);
  opacity: 0;
  transition: opacity 0.3s;
}
.social-tile:hover::after { opacity: 1; }

/* Instagram glyph, top-right, marking each tile as a link to the feed. */
.social-tile__badge {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  z-index: 1;
  color: #fff;
  opacity: 0.9;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.5));
}
.social-tile__badge svg { width: 22px; height: 22px; }

/* The final "follow us" card — the conversion tile at the end of the rail. */
.social-tile--cta {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: linear-gradient(135deg, var(--clr-orange), var(--clr-teal));
  color: #fff;
}
.social-tile__cta-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 1rem;
}
.social-tile__cta-inner svg { width: 40px; height: 40px; }
.social-tile__cta-inner strong { font-size: 1.35rem; font-weight: 900; }
.social-tile__cta-inner span { font-size: 0.95rem; opacity: 0.95; }

/* ===========================
   Cafe
   =========================== */

.cafe-section {
  padding-block: 5rem;
  background: var(--clr-bg-alt);
}

.cafe-section__inner {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3rem;
  align-items: center;
}

@media (min-width: 1024px) {
  .cafe-section__inner {
    grid-template-columns: 1fr 1fr;
  }
  .cafe-section__image { order: 1; }
  .cafe-section__text  { order: 0; }
}

.cafe-section__heading {
  display: flex;
  align-items: center;
  gap: 1rem;
  color: var(--clr-orange-4);
  margin-bottom: 1rem;
}

.cafe-section__heading h2 {
  font-size: 2.25rem;
  font-weight: 700;
  color: #fff;
  margin: 0;
}

.cafe-section__text p {
  font-size: 1.125rem;
  color: var(--clr-muted-3);
  max-width: none;
  margin-bottom: 1.5rem;
}

.cafe-section__image {
  /* Match the photo's real 3:2 so its height is set by the (liked) column
     width, not a fixed 400px. The fixed height letterboxed it wide on big
     screens and cropped it near-square on narrower desktops — both read as
     "squished". Tied to the aspect, the box never distorts or over-crops. */
  aspect-ratio: 3 / 2;
}

.cafe-section__image img {
  width: 100%;
  height: 100%;
  /* Box AR now equals the image's, so cover is a no-op crop — kept so a future
     photo of a different shape fills the frame instead of distorting. */
  object-fit: cover;
  border-radius: var(--radius);
}

/* ===========================
   Footer
   =========================== */

.footer {
  background: var(--clr-bg-dark);
  color: var(--clr-muted);
  border-top: 1px solid var(--clr-border);
}

.footer__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem 4rem;
  padding-block: 3rem;
}

@media (min-width: 640px) {
  /* Links + Connect pair up before the whole thing goes three-across, so the
     narrow Connect column isn't a mostly-empty third of the row (audit C57). */
  .footer__grid { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 1024px) {
  /* Blurb wide on the left; the middle column gets enough room for the quick
     links to sit in two sub-columns rather than one tall stack (see below). */
  .footer__grid { grid-template-columns: 2fr 1.6fr 1fr; }
}

.footer__grid h3 {
  color: #fff;
  font-size: 1.125rem;
  font-weight: 700;
  margin-bottom: 1rem;
}

.footer__grid > div > p {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--clr-muted);
  max-width: none;
}

/* Quick links flow into two content-sized columns at every width. Seven links
   in one vertical stack made this the tallest footer column — stretching the
   footer and leaving blank space under the shorter blurb/social columns — and
   was a long scroll on mobile. Two columns halves the height everywhere. */
.footer__grid ul {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, auto);
  justify-content: start;
  gap: 0 2rem;
}

.footer__grid ul a {
  font-size: 0.875rem;
  color: var(--clr-muted);
  transition: color 0.15s;
}
.footer__grid ul a:hover { color: var(--clr-orange-4); }

.footer__social {
  display: flex;
  gap: 1rem;
  margin-top: 0.5rem;
}

.footer__social a {
  color: var(--clr-muted);
  transition: color 0.15s;
}
.footer__social a:hover { color: var(--clr-orange-4); }

.footer__bottom {
  border-top: 1px solid var(--clr-border);
  padding-block: 2rem;
  text-align: center;
}

.footer__bottom p {
  font-size: 0.875rem;
  color: var(--clr-muted);
  max-width: none;
}

/* Build credit sits under the copyright, deliberately quieter than it. */
.footer__credit {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
}

.footer__credit a {
  color: var(--clr-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color 0.15s;
}
.footer__credit a:hover { color: var(--clr-orange-4); }

/* ===========================
   Shared inner-page utilities
   =========================== */

.py-20 { padding-block: 5rem; }
.py-16 { padding-block: 4rem; }
.bg-900 { background: var(--clr-bg); }
.bg-800 { background: var(--clr-bg-alt); }
.text-center { text-align: center; }
.mb-8 { margin-bottom: 2rem; }
.mb-16 { margin-bottom: 4rem; }

.orange { color: var(--clr-orange-4); }

/* ---- Message page (403_csrf.html, lockout.html) ----
   Both templates have always asked for `.panel.panel--narrow` and nothing has
   ever defined either class, so both rendered as raw full-bleed prose: text
   touching both screen edges at 320px with no gutter at all, a 1440px measure
   on desktop, and — because the reset zeroes every margin — no space between
   the paragraphs. It is the same centred reading column the other short
   standalone pages (unsubscribe, the thanks pages) assemble by hand out of
   `.py-20` + `.container--read`, written once here so the two pages that name
   it get it. */
.panel {
  max-width: 40rem;
  margin-inline: auto;
  padding-block: 5rem;
  padding-inline: clamp(1.25rem, 4vw, 2rem);
}
.panel--narrow { max-width: 34rem; }
.panel h1 {
  font-size: clamp(1.75rem, 5vw, 2.25rem);
  font-weight: 800;
  margin-bottom: 1rem;
}
.panel p { margin-bottom: 1rem; }
.panel p:last-child { margin-bottom: 0; }

h2 { color: #fff; }

.section-header {
  text-align: center;
  margin-bottom: 3rem;
}
.section-header h2 { margin-bottom: 0.75rem; }
.section-header p { color: var(--clr-muted-3); max-width: 50ch; margin-inline: auto; }

/* ===========================
   Page Hero
   =========================== */

.page-hero {
  position: relative;
  /* Was a raw 60vh, which grew to 680–820px on an iPad and buried the page's
     actual content below the fold (audit C63). clamp keeps it a title banner:
     ~340px on a phone, ~460px on a desktop, never a full-screen photo. */
  height: clamp(20rem, 34vw, 30rem);
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  overflow: hidden;
}

/* Short title banner. Was a flat 400px that letterboxed to a 6:1 stripe on wide
   desktops (audit C61); now scales gently with width so the crop stays sane. */
.page-hero--short {
  height: clamp(18rem, 24vw, 28rem);
}

/* Slimmer banner for pages where the content below is the reason to visit. */
.page-hero--half {
  height: clamp(12rem, 20vw, 20rem);
}

/* Landscape / short viewports: a fixed-height banner eats the whole screen and
   the page opens with no content visible (audit C17). Collapse to a compact
   padded band so the first screen shows the banner AND what's under it. */
@media (max-height: 560px) {
  .page-hero,
  .page-hero--short,
  .page-hero--half { height: auto; min-height: 0; padding-block: 2.5rem; }
}

.page-hero__overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1;
}

.page-hero__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

.page-hero__content {
  position: relative;
  z-index: 2;
  padding-inline: 16px;
}

.page-hero__content h1 {
  font-size: clamp(2.5rem, 7vw, 4rem);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  line-height: 1;
}

/* ---- First-visit hero ----
   Every other hero on the site is a title over a photo at a fixed height. This
   one carries the only call to action on the page, so it is sized by its own
   content instead: a fixed 288px banner centred ~200px of stacked content in
   it, which spent ~44px of dead space above the title pushing the button down
   the page for no reason. `height: auto` with a min-height keeps the photo
   reading as a banner while letting the button ride as high as the content
   allows — which is the whole point of the page. */
.page-hero--fv {
  height: auto;
  min-height: clamp(15rem, 20vw, 22rem);
  /* Deliberately tight at the top and looser at the foot: everything above the
     button is height the reader has to scroll past to reach it, everything
     below it is not. */
  padding-block: clamp(1.5rem, 2.5vw, 2.25rem) clamp(2rem, 4vw, 3.5rem);
}

/* A flat 55% wash is enough for a 64px headline and not much else — the chip,
   the button's surround and the 14px note all sit over a brightly lit wall. A
   vertical ramp darkens where the text actually is and, at the foot, lands on
   the exact colour of the section below, so the hard seam that used to cut
   across the bottom of the photo disappears. */
.page-hero__overlay--fv {
  background: linear-gradient(
    180deg,
    rgba(3, 7, 18, 0.55) 0%,
    rgba(3, 7, 18, 0.78) 45%,
    rgba(17, 24, 39, 0.97) 100%
  );
}

/* The global reset zeroes heading margins, hence the explicit spacing. The gap
   above the button is tighter than the gaps below it, so the title and the
   action read as one block and the button sits as high as the title allows. */
.fv-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

/* The chip and the note are one supporting block under the button, not two
   separate beats. */
.fv-hero__chip + .fv-hero__note { margin-top: -0.375rem; }

/* The offer, as a chip directly under the button. Orange-400 on a near-opaque
   dark ground, not the old banner eyebrow's white-on-orange — that was 2.80:1,
   the same failure .btn--orange is inked dark to avoid. This reads ~8:1 and,
   being outlined rather than filled, leaves the button above it as the only
   filled orange thing on the page. */
.fv-hero__chip {
  display: inline-block;
  background: rgba(3, 7, 18, 0.72);
  border: 1px solid rgba(251, 146, 60, 0.55);
  color: var(--clr-orange-4);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: 5px 14px;
  border-radius: 999px;
}

/* Bigger than .btn--lg. It is the single action on the page and it competes
   with a full-bleed photograph; at the shared size it read as a caption. */
.fv-hero__btn {
  padding: 15px 34px;
  font-size: 1.0625rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}

.fv-hero__note {
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--clr-text-2);
  max-width: 40ch;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}

/* Landscape phone: the sticky chrome leaves ~390px for everything. Same
   max-height axis the heroes and site chrome already use. */
@media (max-height: 560px) {
  .page-hero--fv { min-height: 0; padding-block: 1.5rem; }
  .fv-hero { gap: 0.75rem; }
  .fv-hero__note { font-size: 0.8125rem; line-height: 1.3; max-width: 60ch; }
}

/* ===========================
   Pricing Hero
   =========================== */

.pricing-hero {
  padding: 7rem 0 4rem;
  background: var(--clr-bg);
  text-align: center;
}

.pricing-hero h1 {
  font-size: clamp(2.5rem, 6vw, 3.75rem);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  margin-bottom: 1rem;
}

/* The clamp above bottoms out at 2.5rem, and 2.5rem is wider than the page on
   the smallest phones: "MEMBERSHIPS" is one 11-character word at 900 weight
   with nothing to break on, 318px of it, against the 256px a 320px screen has
   left after the gutter. It ran 62px past the container and took the whole of
   /pricing/ 30px sideways with it. Under 24rem the floor is dropped and the
   heading tracks the viewport instead — 30px at 320, 36px at 384, which is
   within 4px of where the clamp picks it up again, so there is no visible step
   at the boundary and nothing at or above a 390px phone changes at all. */
@media (max-width: 24rem) {
  .pricing-hero h1 { font-size: 9.5vw; }
}

.pricing-hero p {
  font-size: 1.125rem;
  color: var(--clr-muted);
  max-width: 40rem;
  margin-inline: auto;
}

.pricing-section-title {
  font-size: 1.875rem;
  font-weight: 700;
  text-align: center;
  margin-bottom: 2rem;
}

/* ===========================
   Pricing Cards
   =========================== */

.pricing-grid {
  display: grid;
  /* Fluid 1 → 2 → 3 columns so tablets get a two-up row instead of full-width
     slabs stacked to 1024px (audit C11). */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
  gap: 1.5rem;
  /* `stretch` (the default) not `start`: the cards are flex columns with the
     CTA on marg-top:auto, so equal heights bottom-align every button and kill
     the ragged row (audit C12). */
  align-items: stretch;
  margin-bottom: 5rem;
}

/* Family/Under-Five only has two products — cap and centre them rather than
   letting two cards stretch across the whole page. */
@media (min-width: 1024px) {
  .pricing-grid--two {
    grid-template-columns: repeat(2, minmax(0, 24rem));
    justify-content: center;
  }
}

/* The featured card carries an orange banner above its heading; reserve the
   same height on the plain cards so all three price lines share a baseline
   (audit C12). */
@media (min-width: 1024px) {
  .pricing-grid > .pricing-card:not(.pricing-card--featured) .pricing-card__body { padding-top: calc(2rem + 37px); }
}

.pricing-card {
  background: var(--clr-bg);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 4px 24px rgba(0,0,0,0.3);
  display: flex;
  flex-direction: column;
}

.pricing-card--featured {
  outline: 2px solid var(--clr-orange);
}

.pricing-card__banner {
  background: var(--clr-orange);
  color: #fff;
  text-align: center;
  padding: 8px 16px;
  font-weight: 700;
  font-size: 0.875rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.pricing-card__body {
  padding: 2rem;
  display: flex;
  flex-direction: column;
  flex-grow: 1;
}

.pricing-card__body h3 {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 0.25rem;
}

.pricing-card__period {
  color: var(--clr-muted);
  font-size: 0.9rem;
  margin-bottom: 1rem;
  max-width: none;
}

.pricing-card__price {
  font-size: 3rem;
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.04em;
  margin-bottom: 1.5rem;
}

.pricing-card__features {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
  flex-grow: 1;
}

/* Hanging tick instead of a flex row. As a flex container the li turned its
   inline "how that works" link into a separate flex item, splitting the sentence
   into two ragged columns with an orphaned em dash (audit C13). A relative li
   with an absolutely-positioned tick lets the text and its inline link flow as
   one normal run. */
.pricing-card__features li {
  position: relative;
  padding-left: 1.5rem;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--clr-muted-3);
}

.pricing-card__features li::before {
  content: '✓';
  position: absolute;
  left: 0;
  top: 0;
  color: var(--clr-teal-4);
  font-weight: 700;
}

/* Week-to-week price, sitting just under the headline contract price.
   Negative margin pulls it up against the price it qualifies. */
.pricing-card__alt {
  margin-top: -1rem;
  margin-bottom: 1.25rem;
  font-size: 0.875rem;
  color: var(--clr-muted);
  max-width: none;
}

/* "Gear hire not included" — kept visually quiet but on every pass card, so
   nobody assumes shoes come with the ticket. */
.pricing-card__note {
  font-size: 0.8125rem;
  color: var(--clr-muted);
  border-top: 1px solid var(--clr-border);
  padding-top: 0.9rem;
  margin-bottom: 1.25rem;
  max-width: none;
}

/* Shoe warning under the pricing hero — the one thing a first-timer
   doesn't know to bring. */
.shoe-notice {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  text-align: left;
  margin-top: 1.75rem;
  padding: 0.9rem 1.4rem;
  border: 1px solid var(--clr-teal);
  border-radius: var(--radius);
  background: rgba(20, 184, 166, 0.08);
  color: var(--clr-text-2);
  font-size: 0.9375rem;
  max-width: 40rem;
}

.shoe-notice svg { color: var(--clr-teal-4); flex-shrink: 0; }
.shoe-notice strong { color: #fff; }

/* Essential-services / student discount, above the adult cards. Orange rather
   than the shoe-notice teal so it reads as an offer, not another warning. */
.discount-notice {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  max-width: 46rem;
  /* Top margin because it now also appears *below* a price table (first
     visit), not only above the adult cards (pricing). */
  margin: 2rem auto 2.5rem;
  padding: 1.1rem 1.5rem;
  border: 1px solid var(--clr-orange);
  border-radius: var(--radius);
  background: rgba(249, 115, 22, 0.08);
  color: var(--clr-muted-3);
  font-size: 0.9375rem;
}

.discount-notice svg {
  color: var(--clr-orange-4);
  flex-shrink: 0;
  /* nudge down so the icon centres on the title line, not the box */
  margin-top: 2px;
}

.discount-notice__text p { max-width: none; }

.discount-notice__title {
  font-weight: 700;
  color: #fff;
  margin-bottom: 0.35rem;
}

.discount-notice strong { color: var(--clr-orange-4); }

.discount-notice__fine {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  color: var(--clr-muted);
}

/* These links were falling through to the browser default #0000EE — dark blue
   on a near-black panel, i.e. invisible. The base `a` rule sets no colour, so
   any link in a component that doesn't style its own is unreadable. */
.discount-notice a,
.pricing-card__features a {
  color: var(--clr-teal-4);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.discount-notice a:hover,
.pricing-card__features a:hover { color: var(--clr-orange-4); }

/* The trial-hours banner used to live here. It moved to its own sheet
   (static/css/trialbanner.css) when it was extended to the console and the
   portal, which load console.css and so resolve none of this file's
   --clr-* tokens. */

.gear-hire-note {
  margin-top: 1.5rem;
  color: var(--clr-muted);
  font-size: 0.9375rem;
}

.pricing-card__btn {
  width: 100%;
  justify-content: center;
  margin-top: auto;
}

/* Home-page adult pricing teaser.
   Reuses .pricing-grid / .pricing-card verbatim so the home page and the
   pricing page can never drift apart visually. Only the spacing differs: on
   /pricing the grid ends its section, so its 5rem tail runs into the next
   band; here it is followed by a "see everything" line, which that tail would
   strand halfway down the page. */
.home-pricing__lede {
  text-align: center;
  color: var(--clr-muted-3);
  max-width: 46rem;
  /* Pulls up against .pricing-section-title's 2rem tail so the lede reads as
     part of the heading rather than as a floating first paragraph. */
  margin: -1rem auto 2.5rem;
}

.home-pricing .pricing-grid { margin-bottom: 2rem; }

/* On /pricing the cards sit on the lighter band, so their own --clr-bg reads
   as a raised surface. Here the section takes the darker band to keep this
   page's alternation (the sections either side are both bg-800), which is the
   same colour as the card — leaving only the drop shadow to separate them.
   Swapping the card up one step restores the contrast, inverted. */
.home-pricing .pricing-card { background: var(--clr-bg-alt); }

.home-pricing__foot {
  text-align: center;
  color: var(--clr-muted);
  font-size: 0.9375rem;
  max-width: 46rem;
  margin: 0 auto 1.25rem;
}

/* Gear Hire */
.gear-hire-grid {
  display: flex;
  gap: 2rem;
  justify-content: center;
  flex-wrap: wrap;
}

.gear-hire-card {
  background: var(--clr-bg-alt);
  border-radius: var(--radius);
  padding: 1.5rem 2.5rem;
  text-align: center;
}

.gear-hire-card__name {
  font-size: 1.125rem;
  color: var(--clr-muted-3);
  max-width: none;
  margin-bottom: 0.25rem;
}

.gear-hire-card__price {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--clr-teal-4);
  max-width: none;
}

/* ===========================
   First Visit page
   =========================== */

/* Shared column for this page's sections. Was an inline `style="max-width:
   68rem"` repeated on three containers. */
.fv-wrap { max-width: 72rem; }
.fv-wrap--narrow { max-width: 58rem; }

/* One row of four on desktop (1 → 2 → 4 columns).
   These were four bare columns of text separated by gaps: a 5rem teal circle,
   a heading and a paragraph, floating on the section ground with nothing to
   bound them. With ragged copy lengths that reads as four unequal runs of
   text, not as four steps — so they are cards now, on a raised surface, with
   the numeral as a badge instead of typed into the heading. Grid stretches
   them to a common height for free, which is what makes the row line up. */
.fv-steps {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
  list-style: none;
  counter-reset: none;
}

@media (min-width: 640px) {
  .fv-steps { grid-template-columns: 1fr 1fr; gap: 1.5rem; }
}

@media (min-width: 1024px) {
  .fv-steps { grid-template-columns: repeat(4, 1fr); }
}

.fv-step {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 1.5rem 1.375rem 1.625rem;
  transition: border-color 0.15s, transform 0.15s;
}

/* A first-timer reads this row top to bottom; the hover is only there so the
   cards feel like a set rather than four boxes. */
.fv-step:hover { border-color: var(--clr-teal); transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .fv-step { transition: border-color 0.15s; }
  .fv-step:hover { transform: none; }
}

.fv-step__head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* Teal, not orange. The page has exactly one orange call to action and four
   orange numerals beside it would dilute it — orange acts, teal informs. */
.fv-step__num {
  flex-shrink: 0;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  background: var(--clr-teal);
  color: var(--clr-bg);
  font-size: 1.0625rem;
  font-weight: 900;
  display: flex;
  align-items: center;
  justify-content: center;
}

.fv-step__icon { color: var(--clr-teal-4); opacity: 0.85; }

.fv-step h3 {
  font-size: 1.1875rem;
  font-weight: 700;
  line-height: 1.3;
}

/* In the four-up row two of the headings take one line and two take two, so
   the four bodies started at three different heights and the row read as
   ragged. Reserving two lines lines the paragraphs up without depending on the
   headings staying a particular length. Only at four-up: stacked or two-up,
   the cards are nowhere near each other and a reserved blank line is just a
   gap. */
@media (min-width: 1024px) {
  .fv-step h3 { min-height: 2.6em; }
}

.fv-step p {
  color: var(--clr-muted);
  font-size: 0.9375rem;
  line-height: 1.65;
  max-width: none;
}

/* The waiver conditions the deleted "Waiver Requirement" panel owned. A rule
   that governs the whole list, so it sits under the whole list. */
.fv-steps-note {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  margin: 1.75rem auto 0;
  max-width: var(--measure);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--clr-muted);
}

.fv-steps-note svg { flex-shrink: 0; margin-top: 0.2rem; color: var(--clr-teal-4); }
/* inline text links on dark sections */
.fv-link {
  color: var(--clr-teal-4);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.fv-link:hover { color: var(--clr-teal); }
/* ===========================
   First-visit: the free-week offer
   =========================== */

/* This was a full band: an orange eyebrow, a 2.75rem uppercase headline, three
   paragraphs and a repeat of the hero's button — about a phone screen of
   height to say "your first paid visit earns a free week, and so does your
   mate's". It is one line now, with the offer itself moved up into the hero
   chip where it sits beside the button it is meant to justify.

   The glow, not a background swap, is what makes the strip stand out; the
   gradient ramps the body colour into the exact colour of the section below so
   the join has no visible seam. */
.fv-offer {
  position: relative;
  padding-block: 2.25rem;
  background: linear-gradient(180deg, var(--clr-bg) 0%, var(--clr-bg-alt) 100%);
  overflow: hidden;
}

.fv-offer__glow {
  position: absolute;
  top: -11rem;
  left: 50%;
  transform: translateX(-50%);
  width: 42rem;
  height: 22rem;
  border-radius: 50%;
  background: var(--clr-orange);
  filter: blur(110px);
  opacity: 0.16;
  pointer-events: none;
}

.fv-offer__text {
  position: relative;
  z-index: 1;
  /* Matches the .container gutter so the line sits on the page's own column
     rather than running to the screen edge. */
  max-width: 58rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
  text-align: center;
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--clr-muted-3);
}

.fv-offer__text strong { color: var(--clr-orange-4); }

@media (max-width: 40rem) {
  .fv-offer { padding-block: 1.75rem; }
  .fv-offer__text { font-size: 0.9375rem; line-height: 1.6; }
}

/* Sits on the text baseline rather than above it — the strip is one sentence,
   so a block-level icon would double its height for decoration. */
.fv-offer__icon {
  vertical-align: -5px;
  margin-right: 0.375rem;
  color: var(--clr-orange-4);
}

/* Legible against the glow but deliberately quieter than the offer itself. */
.fv-offer__terms {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  opacity: 0.8;
  white-space: nowrap;
}
.fv-offer__terms:hover { opacity: 1; }

/* ===========================
   First-visit: rock-up pricing
   =========================== */

.fv-price-table {
  background: var(--clr-bg-alt);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 3rem;
}

.fv-price-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 1.5rem;
}
.fv-price-row + .fv-price-row { border-top: 1px solid var(--clr-border); }

.fv-price-row__label {
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--clr-text);
}

.fv-price-row__sub {
  color: var(--clr-muted);
  font-size: 0.875rem;
  font-weight: 400;
}

.fv-price-row__price {
  font-size: 1.375rem;
  font-weight: 900;
  color: var(--clr-teal-4);
  /* No `white-space: nowrap` here. The last row's price is not a single figure
     but a sentence — "$179 adult / $149 youth", 200px of it — and held on one
     line it could not shrink with the row: at 320px the label and the price
     together want 319px of a 278px row, and .fv-price-table is overflow:hidden
     for its rounded corners, so the price was not merely cramped, it was cut
     off mid-figure at "$179 adult / $1" (audit: clipped 41px at 320). Allowed
     to wrap it shrinks like the label beside it. The one-word prices above it
     have nothing to break on and are unaffected. */
  text-align: right;
}

/* Subheadings inside the price table. Day passes, the 10-visit pass and gear
   hire used to be a table, a muted row and a separate centred section with its
   own heading and two cards — three blocks to add up to answer one question.
   Labelled groups in one table say the same thing in a third of the height. */
.fv-price-group {
  background: var(--clr-bg);
  border-top: 1px solid var(--clr-border);
  padding: 0.7rem 1.5rem;
  margin: 0;
  max-width: none;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--clr-muted);
}

/* The first group opens the table, so it has no row above it to divide from. */
.fv-price-group:first-child { border-top: 0; }

.fv-fineprint {
  text-align: center;
  color: var(--clr-muted);
  font-size: 0.9375rem;
  max-width: 44rem;
  margin: 3rem auto 0;
}

.fv-member-cta__title {
  font-size: 2rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: #fff;
  margin-bottom: 1rem;
}

.fv-member-cta__lead {
  font-size: 1.125rem;
  color: var(--clr-muted-3);
  max-width: none;
  margin-bottom: 2rem;
}

/* Standfirst paragraph under a page hero — the "here's what this page is
   about" line, set larger than body copy. */
.page-lead {
  font-size: 1.125rem;
  line-height: 1.7;
  color: var(--clr-muted-3);
  max-width: none;
}

/* ===========================
   Public waiver page
   =========================== */

.wform-panel {
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 1.75rem;
  margin-bottom: 2rem;
}

.wform-panel h2,
.wform-panel h3 {
  font-size: 1.375rem;
  font-weight: 700;
  color: #fff;
  margin-bottom: 1rem;
}

.wform-panel p { color: var(--clr-muted-3); max-width: none; }

.wform-panel__version {
  color: var(--clr-muted);
  font-weight: 400;
  font-size: 1rem;
}

/* Scrollable legal text so the sign form stays within reach. Sized to the
   viewport rather than a fixed 340px slot: ~600px on a desktop, ~215px on a
   short landscape phone where the sticky nav leaves little room. A pane taller
   than the free space would trap the swipe, so it tracks the viewport (C24). */
.wform-doc {
  max-height: clamp(15rem, 55vh, 38rem);
  overflow-y: auto;
  /* Stop the page jerking when the reader hits the end of the waiver text. */
  overscroll-behavior: contain;
  background: var(--clr-bg-alt);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  font-size: 0.92rem;
  line-height: 1.65;
  color: var(--clr-muted-3);
}
.wform-doc p { margin-bottom: 0.9rem; max-width: none; }

/* Short/landscape viewports: a pane taller than the free space traps a swipe
   over it, so drop the scroller and let the waiver flow into the page instead
   (audit C24). Matches the 560px short-viewport axis used by the heroes. */
@media (max-height: 560px) {
  .wform-doc { max-height: none; overflow-y: visible; }
}

.wform {
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 1.75rem;
}

.wform h3 {
  font-size: 1.375rem;
  font-weight: 700;
  color: #fff;
  margin-bottom: 1.5rem;
}

.wform__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 1.25rem;
}
@media (min-width: 640px) {
  .wform__row { grid-template-columns: 1fr 1fr; }
}

.wform__field { margin-bottom: 1.25rem; }

.wform__field label {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--clr-text-2);
  margin-bottom: 0.4rem;
}

.wform__field input,
.wform__field select {
  width: 100%;
  background: var(--clr-bg-alt);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 0.65rem 0.85rem;
  color: var(--clr-text);
  font-family: var(--font);
  font-size: 1rem;
}
.wform__field input:focus,
.wform__field select:focus {
  outline: 2px solid var(--clr-teal);
  outline-offset: 0;
  border-color: var(--clr-teal);
}
/* Custom caret so the select matches the inputs on the dark surface. */
.wform__field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.85rem center;
  padding-right: 2.5rem;
}

.wform__guardian {
  border-top: 1px solid var(--clr-border);
  padding-top: 1.25rem;
  margin-bottom: 0.25rem;
}

.wform__hint {
  color: var(--clr-muted);
  font-size: 0.875rem;
  max-width: none;
  margin-bottom: 1.25rem;
}

.wform__agree {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin-block: 1.25rem 1.75rem;
  /* The tick and its (long) label share one line; the "you didn't tick this"
     message wraps onto its own below them via flex-basis: 100% — see
     .wform__agree-error's own rule. */
  flex-wrap: wrap;
}

.wform__agree input {
  margin-top: 0.3rem;
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--clr-teal);
  flex-shrink: 0;
}

.wform__agree label {
  color: var(--clr-muted-3);
  font-size: 0.9375rem;
  cursor: pointer;
  /* The row above says the tick and its label share one line. They did not:
     with `flex-wrap: wrap` a flex item is only shrunk AFTER it has been given a
     line of its own, and this label's auto basis is its max-content width — one
     unbroken 900px sentence — so it wrapped to line two at every viewport,
     desktop included, leaving the tick floating alone above a full-width
     paragraph. A zero basis makes it fit the first line by construction and
     wrap inside itself; min-width: 0 stops `min-width: auto` flooring it at the
     longest word. The error message still takes the next line on its own — it
     sets flex-basis: 100% for exactly that. */
  flex: 1 1 0;
  min-width: 0;
}

.wform__submit { width: 100%; justify-content: center; }

.wform__member-note {
  text-align: center;
  color: var(--clr-muted);
  font-size: 0.875rem;
  margin-top: 1.25rem;
  max-width: none;
}

.wform-link {
  color: var(--clr-teal-4);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.wform-link:hover { color: var(--clr-teal); }

.wform-success {
  background: var(--clr-bg);
  border: 1px solid var(--clr-teal);
  border-radius: var(--radius);
  padding: 2.5rem 2rem;
  text-align: center;
}

.wform-success__icon { color: var(--clr-teal-4); margin-bottom: 1rem; }
.wform-success__icon svg { margin-inline: auto; }

.wform-success h2 {
  font-size: 1.75rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: #fff;
  margin-bottom: 0.75rem;
}

.wform-success p { color: var(--clr-muted-3); max-width: none; }

.wform-success__steps {
  list-style: none;
  margin: 1.5rem auto;
  max-width: 30rem;
  text-align: left;
}

.wform-success__steps li {
  position: relative;
  padding-left: 1.75rem;
  color: var(--clr-text-2);
  margin-bottom: 0.6rem;
}
/* teal tick bullet — pseudo-element so the list needs no inline SVGs */
.wform-success__steps li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--clr-teal-4);
  font-weight: 900;
}

.wform-success__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  margin-block: 1.75rem 1.25rem;
}

.wform-success__more { color: var(--clr-muted); font-size: 0.875rem; }
.wform-success__more a {
  color: var(--clr-teal-4);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ===========================
   Classes page
   =========================== */

/* Fluid columns: fills the width and never orphans a lone third card on its own
   half-width row (audit C31). The two fixed breakpoints are deleted. Works for a
   three- or four-class lineup alike — the lineup is PUBLIC_CLASS_CATEGORIES in
   config/views.py. */
.classes-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: 1.5rem;
}

.class-card {
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 2rem;
  position: relative;
}

.class-card__badge {
  display: inline-block;
  background: var(--clr-teal);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 999px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 1rem;
}

.class-card__icon {
  color: var(--clr-teal-4);
  margin-bottom: 1rem;
}

.class-card h3 {
  font-size: 1.375rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
}

.class-card p {
  color: var(--clr-muted);
  font-size: 0.9375rem;
  max-width: none;
}

.classes-youth {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3rem;
  align-items: center;
}

@media (min-width: 1024px) {
  .classes-youth { grid-template-columns: 1fr 1fr; }
}

.classes-youth__text h2 { margin-bottom: 1rem; }
.classes-youth__text p { color: var(--clr-muted-3); max-width: none; margin-bottom: 1.5rem; }

.classes-youth__image img {
  width: 100%;
  height: 360px;
  object-fit: cover;
  border-radius: var(--radius);
}

/* ===========================
   Youth page
   =========================== */

.playon-banner {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  align-items: center;
}

/* iPad mini portrait (744px) sat just above the old 768px switch and inherited
   the stacked banner; 44rem (704px) lets it go side-by-side there too, and a
   larger root font falls back to the stacked layout gracefully (audit C30). */
@media (min-width: 44rem) {
  .playon-banner { grid-template-columns: 1fr auto; }
}

.playon-banner__text h2 { margin-bottom: 1rem; }
.playon-banner__text p { color: var(--clr-muted-3); max-width: none; margin-bottom: 1.5rem; }

.playon-banner__logo img {
  height: 80px;
  width: auto;
  object-fit: contain;
}

.programs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: 1.5rem;
}

.program-card {
  background: var(--clr-bg-alt);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 2rem;
}

.program-card__badge {
  display: inline-block;
  background: var(--clr-teal);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 999px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 1rem;
}

.program-card__icon {
  color: var(--clr-teal-4);
  margin-bottom: 0.75rem;
}

.program-card__age {
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--clr-orange-4);
  margin-bottom: 0.5rem;
}

.program-card h3 {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
}

.program-card p {
  color: var(--clr-muted);
  font-size: 0.9375rem;
  max-width: none;
}

/* Three real classes rather than two placeholder programs. */
@media (min-width: 900px) {
  .programs-grid--three { grid-template-columns: repeat(3, 1fr); }
}

/* Whole card is the link through to the timetable — a parent reading a
   class blurb wants times next, so the click target is the card, not a
   small link buried at the bottom. */
.program-card--link {
  display: flex;
  flex-direction: column;
  color: inherit;
  transition: border-color 0.15s, transform 0.15s;
}

.program-card--link:hover {
  border-color: var(--clr-teal);
  transform: translateY(-2px);
}

.program-card__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  /* auto pushes the CTA to the card foot, so all three line up even though
     the class blurbs are different lengths. */
  margin-top: auto;
  padding-top: 1rem;
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--clr-orange-4);
}

.program-card--link:hover .program-card__cta svg { transform: translateX(3px); }
.program-card__cta svg { transition: transform 0.15s; }

.class-dot--lg { width: 16px; height: 16px; margin-right: 0; }

/* ===========================
   Contact page
   =========================== */

.contact-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3rem 4rem;
  align-items: start;
}

/* Two columns from 768px, not 1024 — the details and the form both sit in a
   single stretched column on tablets otherwise, wasting the width (audit C18). */
@media (min-width: 768px) {
  .contact-grid { grid-template-columns: 1fr 1fr; }
}

.contact-details h2 { margin-bottom: 1rem; }
.contact-details > p { color: var(--clr-muted-3); max-width: none; margin-bottom: 2rem; }

.contact-info-list {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  margin-bottom: 2.5rem;
}

.contact-info-item {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
}

.contact-info-item__icon {
  flex-shrink: 0;
  color: var(--clr-teal-4);
  margin-top: 2px;
}

.contact-info-item__label {
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--clr-muted);
  margin-bottom: 2px;
  max-width: none;
}

.contact-info-item p,
.contact-info-item a {
  color: var(--clr-muted-3);
  font-size: 0.9375rem;
  max-width: none;
  transition: color 0.15s;
}

.contact-info-item a:hover { color: var(--clr-teal-4); }

.opening-hours h3 {
  font-size: 1.125rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
}

.opening-hours ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.opening-hours ul li {
  display: flex;
  justify-content: space-between;
  font-size: 0.9375rem;
  color: var(--clr-muted-3);
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--clr-border);
}

.opening-hours ul li:last-child { border-bottom: none; }

.contact-grid > div > h2 { margin-bottom: 1.5rem; }

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Spam honeypot on the contact form. Moved off-screen rather than
   display:none — bots routinely skip hidden fields but do fill this one.
   Absolute positioning also keeps it out of the form's flex gap. */
.form-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form-group label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--clr-muted);
}

.required { color: var(--clr-orange-4); }

.form-group input,
.form-group textarea {
  background: var(--clr-bg);
  border: 1px solid var(--clr-border);
  border-radius: 6px;
  padding: 12px 14px;
  color: var(--clr-text);
  font-family: var(--font);
  /* 16px: below it iOS Safari zooms the whole page on focus. 12px padding keeps
     the control clear of the 44px touch minimum (audit C20). */
  font-size: 1rem;
  outline: none;
  transition: border-color 0.15s;
  resize: vertical;
}

.form-group input:focus,
.form-group textarea:focus { border-color: var(--clr-teal); }

.btn--full { width: 100%; justify-content: center; }

/* A pressed button has to LOOK pressed. inline_actions.js disables every
   submitter while a POST is in flight, but a disabled .btn rendered exactly
   like a live one — same fill, same cursor, still lighting up on hover — so
   the member login popup gave no acknowledgement at all during the second or
   so a sign-in spends in PBKDF2, which is what makes someone press again.
   Mirrors the same block in console.css; the two shells have separate token
   sets (--clr-* here, --* there) so neither can be shared. */
.btn:disabled { opacity: .5; cursor: default; transform: none; }
/* Per variant — the hover rules above are equally specific and would otherwise
   still repaint a dead button under the finger. */
.btn--orange:disabled:hover { background: var(--clr-orange); }
.btn--teal:disabled:hover { background: var(--clr-teal); }
.btn--teal-outline:disabled:hover { background: transparent; }

/* ===========================
   Flash messages
   =========================== */

.flash-messages {
  position: fixed;
  /* Clear the visit-bar strip as well as the nav so the toast doesn't paint over
     the logo when it appears at the top of the page (audit C29). */
  top: calc(var(--nav-h) + 2.5rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(480px, 90vw);
}

.flash {
  padding: 12px 20px;
  border-radius: var(--radius);
  font-size: 0.9375rem;
  font-weight: 500;
  text-align: center;
}

.flash--success { background: var(--clr-teal); color: #fff; }
.flash--error   { background: #dc2626; color: #fff; }

/* ===========================
   Login popup
   =========================== */

/* Full-screen overlay; hidden until the Member Login buttons add .is-open.
   z-index clears the sticky nav (z-index 50). */
.login-modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.login-modal.is-open { display: flex; }

.login-modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(3, 7, 18, 0.72);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.login-modal__card {
  position: relative;
  width: 100%;
  max-width: 400px;
  /* Cap the card to the viewport and let it scroll internally. The overlay is
     position:fixed (can't scroll) and opening it locks body scroll via
     body.nav-open, so on a landscape/short phone the ~480px card would
     otherwise overflow the viewport with the close + Sign-in buttons clipped
     off-screen and unreachable. Mirrors the nav drawer / visit panel caps
     (100vh fallback first, then 100dvh where supported). On tall screens the
     cap never binds, so desktop is unchanged. */
  max-height: calc(100vh - 3rem);
  max-height: calc(100dvh - 3rem);
  overflow-y: auto;
  background: var(--clr-surface);
  border: 1px solid var(--clr-border);
  border-radius: 14px;
  padding: 2rem 1.75rem 1.75rem;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}

.login-modal__close {
  position: absolute;
  top: 0.6rem;
  right: 0.85rem;
  background: none;
  border: none;
  color: var(--clr-muted);
  font-size: 1.7rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.15s;
}
.login-modal__close:hover { color: var(--clr-text); }

.login-modal__head { text-align: center; margin-bottom: 1.25rem; }
/* display:block from the global img reset means text-align can't centre it —
   auto side margins do. */
.login-modal__logo { height: 48px; width: auto; margin: 0 auto 0.5rem; }
.login-modal__head h2 { font-size: 1.35rem; font-weight: 900; }
.login-modal__sub { color: var(--clr-muted); font-size: 0.9rem; margin-top: 0.25rem; }

/* Stack the fields with the same rhythm as the contact form. */
.login-modal__form { display: flex; flex-direction: column; gap: 1rem; }
/* The reused .flash box is edge-to-edge in its fixed container; inside the card
   it just needs a little breathing room above the form. */
.login-modal .flash { margin-bottom: 1rem; }

.login-modal__foot {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.1rem;
  font-size: 0.85rem;
}
.login-modal__foot a { color: var(--clr-muted-3); }
.login-modal__foot a:hover { color: var(--clr-orange-4); }

/* Its own centred row under the member links rather than a third item in that
   flex row: __foot is justify-content:space-between, so a third link squeezes
   the pair into an unreadable line on a phone. Smaller than them as well —
   this popup is for members, and the one staff-only thing in it should be
   findable without competing with the two links members actually need. */

/* ---- Public classes timetable (fed by the live scheduler) ----
   Week-as-columns: each day is its own column of sessions under a rule, so a
   whole week is scannable at a glance and a quiet day reads as a quiet day.
   auto-fill rather than auto-fit so columns keep their width and wrap onto a
   second row instead of stretching when the week is half empty. */
/* Explicit column counts, not auto-fill: auto-fill wrapped the week onto a
   second row (5 + 2) at common desktop widths, which reads as two half-weeks
   rather than one stretch of days. Six columns is the most that stays legible
   on one row — see TIMETABLE_DAYS in config/views.py, which must match. */
.timetable {
  display: grid;
  gap: 0 1.25rem;
  grid-template-columns: 1fr;
}

/* Even divisors of six (2 / 3 / 6) so a partial row is always balanced, never a
   lonely orphan day. The 6-up row is held back to 1300px — full-width gives it
   the room there for ~200px legible day columns, where the old 1180px threshold
   crammed each to ~170px and wrapped the class names (audit C14). Below that the
   week is a tidy two rows of three (audit C83). */
@media (min-width: 560px)  { .timetable { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 860px)  { .timetable { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1300px) { .timetable { grid-template-columns: repeat(6, 1fr); } }

/* "How to book" links down to #timetable, and the nav is sticky — without
   this the heading lands underneath it. */
#timetable { scroll-margin-top: calc(var(--nav-h) + 1rem); }

.timetable__day {
  border-top: 2px solid var(--clr-border);
  padding-block: 1rem 2rem;
}
.timetable__day--today { border-top-color: var(--clr-teal); }

.timetable__date { margin-bottom: .5rem; }

.timetable__weekday {
  display: block;
  font-size: .9rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--clr-text);
}
.timetable__day--today .timetable__weekday { color: var(--clr-teal-4); }

.timetable__daynum {
  display: block;
  font-size: .82rem;
  color: var(--clr-muted);
  letter-spacing: .06em;
}

/* Time on the left, class name beside it, arrow far right; coach and spaces
   stack underneath, right-aligned — the arrow is the affordance that the whole
   block is the booking link. */
.timetable__session {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: .1rem .55rem;
  padding: .75rem 0;
  border-top: 1px solid var(--clr-border);
  font-size: .92rem;
  color: inherit;
  transition: opacity .15s;
}
.timetable__session:hover { opacity: .65; }
.timetable__session--full { opacity: .4; }
.timetable__session--full:hover { opacity: .4; }

/* A block-booked program part-way through its block (Grip Squad). NOT dimmed
   like a full session: this row is still a live call to action — it leads to
   the interest list — and dimming it would file it with the ones there is no
   point tapping. Teal, because that is the site's "this is available" colour
   and joining the next block genuinely is. */
.timetable__session--block .timetable__spots {
  color: var(--clr-teal-4);
  font-weight: 700;
}
.timetable__session--block .timetable__arrow { color: var(--clr-teal-4); }

.timetable__time { font-weight: 800; color: var(--clr-text); }
.timetable__name { color: var(--clr-muted-3); }
.timetable__arrow { color: var(--clr-orange-4); align-self: center; }
.timetable__session--full .timetable__arrow { display: none; }

.timetable__coach,
.timetable__spots {
  grid-column: 1 / -1;
  text-align: right;
  font-size: .75rem;
  letter-spacing: .06em;
  color: var(--clr-muted);
}
.timetable__coach { text-transform: uppercase; }

.timetable__none { font-size: .82rem; color: var(--clr-muted); padding-top: .75rem; }

.timetable__foot {
  margin-top: 1.5rem;
  font-size: .875rem;
  color: var(--clr-muted);
}
.timetable__foot a { color: var(--clr-teal-4); text-decoration: underline; text-underline-offset: 2px; }

/* Numbered booking steps — the flow surprises people at the login prompt, so
   it's spelled out rather than left as a paragraph. */
.booking-steps {
  list-style: decimal;
  text-align: left;
  max-width: 30rem;
  margin: 0 auto .75rem;
  padding-left: 1.25rem;
  color: var(--clr-muted-3);
  font-size: .9375rem;
}
.booking-steps li { margin-bottom: .4rem; }
.booking-steps a { color: var(--clr-teal-4); text-decoration: underline; text-underline-offset: 2px; }

/* Cancellation policy — orange-keyed because it costs money to ignore. */
.policy-note {
  max-width: 46rem;
  margin: 1rem auto 0;
  padding: 1rem 1.25rem;
  border: 1px solid rgba(249, 115, 22, .35);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: rgba(249, 115, 22, .06);
  color: var(--clr-muted-3);
  font-size: .875rem;
  line-height: 1.6;
}
.policy-note strong { color: var(--clr-text); }
.class-card__price { color: var(--clr-teal-4); font-weight: 800; margin-top: .6rem; }

/* Age bracket under a class name — same treatment as the youth page chip. */
.class-card__age {
  font-size: .8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--clr-orange-4);
  margin-bottom: .5rem;
}

/* ---- Lineup card: the written answer to "what is this class?" ----
   These cards carry a pitch, a bullet list and a coach credit now, not a single
   description line, so the card becomes a flex column with a pinned foot. Copy
   comes from gym/class_copy.py; every block here is optional, because a class
   the owner adds in /admin/ has no entry there and renders the short way. */
.class-card {
  display: flex;
  flex-direction: column;
}

/* Age bracket and format on one line. Flex rather than two stacked paragraphs
   so a class with both reads as a pair of chips, and one with neither
   collapses to nothing rather than leaving a blank row. The age chip keeps its
   own margin from when the What's On cards used it as a lone <p>; that section
   is gone from classes.html, so inside .class-card__meta is its only use now. */
.class-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-bottom: .9rem;
}
.class-card__meta .class-card__age { margin-bottom: 0; }

/* The commitment — "10-week program" or "Book class by class". A filled pill
   rather than another uppercase label because it is the question somebody is
   actually asking of a class page, and the timetable below cannot answer it:
   every class looks like a bookable session down there, block or not. */
.class-card__format {
  font-size: .75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--clr-teal-4);
  background: rgba(20, 184, 166, .14);
  border-radius: 999px;
  padding: .2rem .6rem;
}

.class-card__label {
  font-size: .75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--clr-text-2);
  margin: 1.25rem 0 .6rem;
}

/* Hanging tick, same anatomy as .pricing-card__features: an absolutely
   positioned marker on a relative li, so the text flows as one run instead of
   breaking into ragged flex columns (audit C13). */
.class-card__learn {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.class-card__learn li {
  position: relative;
  padding-left: 1.5rem;
  font-size: .9375rem;
  line-height: 1.5;
  color: var(--clr-muted-3);
}

.class-card__learn li::before {
  content: '\2713';
  position: absolute;
  left: 0;
  top: 0;
  color: var(--clr-teal-4);
  font-weight: 700;
}

/* margin-top:auto is what makes a row of cards line their prices up when one
   class teaches four things and another teaches three. */
.class-card__foot { margin-top: auto; }

/* Who leads the class. Divided off from the bullets because it answers a
   different question — not "what will I do" but "who is teaching me". */
.class-card__coach {
  display: flex;
  /* flex-start, not center: the label wraps to two lines on a phone, and a
     vertically centred icon then floats beside the gap between them. */
  align-items: flex-start;
  gap: .45rem;
  margin-top: 1.4rem;
  padding-top: .95rem;
  border-top: 1px solid var(--clr-border);
  font-size: .875rem;
  font-weight: 600;
  color: var(--clr-text-2);
}
.class-card__coach svg { flex-shrink: 0; margin-top: .12rem; color: var(--clr-teal-4); }

/* How a 10-week program is actually booked. Under the price, quiet, because it
   is a correction to the pill above rather than a feature. */
.class-card__note {
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--clr-muted);
  margin-top: .4rem;
}

/* Booking instructions under the class price cards. Bordered rather than
   another card, so it reads as a footnote to the prices, not a fourth
   product. */
.booking-note {
  max-width: 46rem;
  margin: 1rem auto 0;
  padding: 1.75rem 2rem;
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  background: var(--clr-bg-alt);
  text-align: center;
}

.booking-note h3 { font-size: 1.125rem; font-weight: 800; margin-bottom: .75rem; }
.booking-note p { color: var(--clr-muted-3); font-size: .9375rem; max-width: none; margin-bottom: .6rem; }
.booking-note p:last-child { margin-bottom: 0; }
.booking-note a { color: var(--clr-teal-4); text-decoration: underline; text-underline-offset: 2px; }
.booking-note a:hover { color: var(--clr-orange-4); }
.booking-note__fine { font-size: .8125rem !important; color: var(--clr-muted) !important; }
.class-dot {
  display: inline-block; width: 10px; height: 10px; border-radius: 50%;
  margin-right: .45rem; vertical-align: baseline;
}

/* ===========================
   Legal / policy pages
   Long-form documents people read to answer one question, so the layout is
   built around finding that answer: a sticky contents rail on desktop, deep-
   linkable headings, and prose measured for reading rather than skimming.
   =========================== */

.legal-head {
  background: var(--clr-bg-dark);
  border-bottom: 1px solid var(--clr-border);
  padding-block: 4rem 3rem;
}

.legal-head h1 { font-size: clamp(2rem, 5vw, 3rem); line-height: 1.15; }

.legal-head__lead {
  color: var(--clr-muted-3);
  margin-top: .9rem;
  max-width: 46rem;
  font-size: 1.0625rem;
}

.legal-head__meta {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.25rem;
  margin-top: 1.25rem;
  font-size: .8125rem;
  color: var(--clr-muted);
}

.legal-head__meta strong { color: var(--clr-text-2); font-weight: 600; }

.legal-head__meta a {
  color: var(--clr-teal-4);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.legal-head__meta a:hover { color: var(--clr-orange-4); }

/* Contents rail + document. Single column until there's room for the rail to
   sit alongside without squeezing the measure. */
.legal-layout { display: block; padding-block: 3.5rem 5rem; }

@media (min-width: 1024px) {
  .legal-layout {
    display: grid;
    /* Contents rail + a reading column capped so long-form prose stays readable
       (~70ch) rather than running a 200-character line (audit C10/C80). The whole
       document is centred in the page (max-width + the .container auto margins)
       so it sits in the middle rather than stranded at the left gutter. */
    grid-template-columns: 16rem minmax(0, 1fr);
    gap: 4rem;
    align-items: start;
    max-width: 62rem;
  }
}

/* Centre the policy header on the same measure as the body below it, so the
   title, lead and first clause share one centred column. */
@media (min-width: 1024px) {
  .legal-head > .container { max-width: 62rem; }
}

.legal-toc { display: none; }

@media (min-width: 1024px) {
  .legal-toc {
    display: block;
    position: sticky;
    /* Clear the sticky nav so the contents heading and first links aren't hidden
       behind it when the rail pins (audit C07). */
    top: calc(var(--nav-h) + 1rem);
    /* Pushed below the nav, the tallest rail (~620px) overruns a short laptop
       viewport and its last clause links become unreachable — cap it to the
       space under the nav and let the rail scroll itself (audit C07). vh is the
       fallback; dvh tracks the real viewport where supported. */
    max-height: calc(100vh - var(--nav-h) - 3rem);
    max-height: calc(100dvh - var(--nav-h) - 3rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    border-left: 2px solid var(--clr-border);
    padding-left: 1.25rem;
  }
}

.legal-toc h2 {
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--clr-muted);
  margin-bottom: .9rem;
}

.legal-toc ol { list-style: none; display: flex; flex-direction: column; gap: .55rem; }

.legal-toc a {
  font-size: .875rem;
  color: var(--clr-muted-3);
  transition: color .15s;
  display: block;
}
.legal-toc a:hover { color: var(--clr-orange-4); }

/* The document itself. Measure in ch so lines stay readable (~70 characters)
   however wide the column gets on desktop (audit C10). */
.legal-doc { max-width: min(70ch, 100%); }

.legal-doc section + section { margin-top: 3rem; }

/* Contents links and shared deep links target the <section id>, not the <h2>,
   so the scroll offset has to live on the section — a scroll-margin on the h2 is
   inert because the h2 is never the fragment target (audit C03). Clears the
   sticky nav so the clause number lands in view rather than hidden behind it.
   Left un-queried so deep links work at every width, not just where the TOC
   shows. */
.legal-doc section[id] { scroll-margin-top: calc(var(--nav-h) + 1rem); }

.legal-doc h2 {
  font-size: 1.5rem;
  font-weight: 800;
  line-height: 1.25;
  margin-bottom: .9rem;
}

/* Numbered clause label ahead of each heading — what people quote back at you
   in a dispute ("clause 7"), so it has to be visible, not just an anchor. */
.legal-doc h2 .legal-doc__num {
  display: block;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--clr-teal-4);
  margin-bottom: .35rem;
}

.legal-doc h3 {
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--clr-text);
  margin: 1.6rem 0 .6rem;
}

.legal-doc p,
.legal-doc li {
  color: var(--clr-muted-3);
  font-size: .9375rem;
  line-height: 1.75;
  max-width: none;
}

.legal-doc p + p { margin-top: .9rem; }

.legal-doc ul,
.legal-doc ol {
  margin: .9rem 0 .9rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.legal-doc li::marker { color: var(--clr-muted); }

.legal-doc strong { color: var(--clr-text); font-weight: 700; }

.legal-doc a {
  color: var(--clr-teal-4);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.legal-doc a:hover { color: var(--clr-orange-4); }

/* Definition rows — "Membership means…", price tables, contact blocks. */
.legal-table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.1rem 0;
  font-size: .9375rem;
}

.legal-table th,
.legal-table td {
  text-align: left;
  padding: .7rem .9rem;
  border-bottom: 1px solid var(--clr-border);
  color: var(--clr-muted-3);
  vertical-align: top;
}

.legal-table th {
  color: var(--clr-text);
  font-weight: 700;
  font-size: .8125rem;
  text-transform: uppercase;
  letter-spacing: .04em;
}

/* `> *` not `td`: the row-header tables put a <th> in the first column, so a
   td-only reset left a dangling half-width rule under the last row (audit C71). */
.legal-table tr:last-child > * { border-bottom: none; }

/* Wide tables scroll themselves rather than pushing the page sideways. */
.legal-table-wrap { overflow-x: auto; }

/* Callout for the bits that cost money or block entry if ignored. Orange keeps
   it consistent with the class cancellation policy note. */
.legal-callout {
  margin: 1.25rem 0;
  padding: 1rem 1.25rem;
  border: 1px solid rgba(249, 115, 22, .35);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: rgba(249, 115, 22, .06);
}

.legal-callout p { color: var(--clr-muted-3); font-size: .9062rem; }
.legal-callout p + p { margin-top: .6rem; }

/* Teal variant for rights the member has (refunds, ACL guarantees, complaints)
   — deliberately a different colour from the ones that cost them money. */
.legal-callout--rights {
  border-color: rgba(20, 184, 166, .35);
  background: rgba(20, 184, 166, .06);
}

/* Plain-English summary at the top of a heavy clause. Not a substitute for the
   clause and labelled as such, but it's what most people will actually read. */
.legal-plain {
  margin: 0 0 1rem;
  padding-left: 1rem;
  border-left: 2px solid var(--clr-teal);
  color: var(--clr-text-2);
  font-size: .9375rem;
}

.legal-plain strong { color: var(--clr-teal-4); }

/* Unfilled business detail (entity name, ABN, phone). Loud on purpose: these
   must not reach production unnoticed. Setting the env var removes it. */
.legal-todo {
  display: inline-block;
  padding: .05rem .4rem;
  border: 1px dashed var(--clr-orange);
  border-radius: 4px;
  background: rgba(249, 115, 22, .12);
  color: var(--clr-orange-4);
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .02em;
}

/* Cross-links to the other policy pages, at the foot of each document. */
.legal-more {
  margin-top: 3.5rem;
  padding-top: 2rem;
  border-top: 1px solid var(--clr-border);
}

.legal-more h2 { font-size: 1.125rem; margin-bottom: 1rem; }

.legal-more__grid {
  display: grid;
  gap: .75rem;
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .legal-more__grid { grid-template-columns: repeat(2, 1fr); }
}

.legal-more__card {
  display: block;
  padding: .9rem 1.1rem;
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  background: var(--clr-bg-alt);
  transition: border-color .15s;
}

.legal-more__card:hover { border-color: var(--clr-teal); }

.legal-more__card span {
  display: block;
  font-weight: 700;
  color: var(--clr-text);
  font-size: .9375rem;
}

.legal-more__card small {
  display: block;
  margin-top: .2rem;
  color: var(--clr-muted);
  font-size: .8125rem;
  line-height: 1.5;
}

/* Footer legal row — a quieter strip under the quick links, so the policy
   pages are reachable from every page without competing with Pricing. */
.footer__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .4rem 1.1rem;
  margin-top: 1.25rem;
}

.footer__legal a {
  font-size: .8125rem;
  color: var(--clr-muted);
  transition: color .15s;
}
.footer__legal a:hover { color: var(--clr-orange-4); }

/* APP 5 collection notice under the waiver form. Small, but it has to be
   readable — it's the notice itself, not a footnote about one. */
.wform__collection {
  margin-top: 1.1rem;
  padding: .85rem 1rem;
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  background: var(--clr-bg-alt);
  color: var(--clr-muted-3);
  font-size: .8125rem;
  line-height: 1.6;
}

.wform__collection a {
  color: var(--clr-teal-4);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.wform__collection a:hover { color: var(--clr-orange-4); }

/* ===========================================================================
   Full-width reconciliation + audit fixes
   Appended so these rules win on source order over the earlier
   `max-width: none` / default declarations they correct. Grouped by concern.
   =========================================================================== */

/* ---- Readable measure on full-bleed prose (audit C39/C10/C42/C49) ----
   Sections now span the page, so a run of body text that isn't inside a bounded
   card/column must cap its own line length or it runs 120–160 characters on a
   desktop. Blocks already inside a narrow card are unaffected (they're smaller
   than the cap anyway). Centred intros keep their auto margins. */
.page-lead,
.cafe-section__text p,
.classes-youth__text p,
.playon-banner__text p,
.contact-details > p,
.fv-member-cta__lead { max-width: var(--measure); }

.fv-fineprint,
.section-header p { max-width: var(--measure); margin-inline: auto; }

/* Privacy collection notice was the smallest copy on the site at 13px and ran
   long; lift it to 14px and cap the measure (audit C49/C42). */
.wform__collection { font-size: .875rem; max-width: var(--measure); }
.legal-callout p { font-size: .9375rem; }

/* ---- Typography rhythm (audit C21/C51) ----
   Section <h2>s fell back to the UA 24px — the same size as the card <h3>s under
   them — and sat flush against their lead paragraph. Give them a real size and
   breathing room. Scoped to marketing sections so the legal/hero h2s (which set
   their own size) are untouched. */
.cafe-section__heading h2,
.classes-youth__text h2,
.playon-banner__text h2 { font-size: clamp(1.75rem, 3vw, 2.25rem); line-height: 1.15; }

.section-header h2 { font-size: clamp(1.875rem, 3.2vw, 2.5rem); }

/* ---- Touch targets (audit C25/C26/C27/C54/C55/C85) ----
   Finger-sized hit areas for the controls a thumb actually uses. Inline prose
   links are left alone; these are the navigation/footer/utility controls. */
.footer__grid ul a,
.footer__legal a { display: inline-block; padding-block: .5rem; }

.footer__social a { padding: .4rem; margin: -.4rem; }

/* mailto on the contact page is the one alternative to the form — make it a
   proper target, not a 19px line (audit C85). */
.contact-info-item a { display: inline-block; padding-block: .25rem; }

/* ---- Waiver: doc then form, one column down the page ----
   A single stacked column reads top-to-bottom (read the waiver, then sign) at a
   comfortable width, rather than two narrow columns squeezed side by side. */
.wform-layout { display: grid; gap: 2rem; }

/* ---- Waiver inline validation (audit C75) ----
   The submit posts to #sign so the browser lands on the form, not the hero
   ~1,450px above; scroll-margin clears the sticky nav. */
.wform { scroll-margin-top: calc(var(--nav-h) + 1rem); }

.wform__errors {
  margin-bottom: 1.5rem;
  padding: .9rem 1.1rem;
  border: 1px solid #dc2626;
  border-left-width: 3px;
  border-radius: var(--radius);
  background: rgba(220, 38, 38, .1);
  color: var(--clr-text-2);
  font-size: .9375rem;
}
.wform__errors ul { margin: .4rem 0 0 1.1rem; display: flex; flex-direction: column; gap: .25rem; }
.wform__errors strong { color: #fca5a5; }
/* The summary is what the script moves focus to on a failed submit; it is not
   a control, so it must not paint the focus ring a keyboard user is entitled to
   read as "you are on a button". */
.wform__errors:focus { outline: none; }
.wform__errors a { color: #fca5a5; text-decoration: underline; }
.wform__errors a:hover { color: #fff; }

/* ---- Required fields, and the ones that were missed ----
   Two separate jobs, deliberately kept visually distinct: the star says "you
   will have to fill this in" BEFORE anyone submits, and the red outline says
   "you didn't" afterwards. A form that only does the second teaches the person
   its rules by rejecting them. */
.wform__req {
  color: #f87171;
  font-weight: 700;
  /* Nudged up: a bare "*" sits on the baseline and reads as part of the word. */
  vertical-align: .1em;
  margin-left: .1em;
}
.wform__legend {
  font-size: .8125rem;
  color: var(--clr-muted);
  margin-bottom: 1.25rem;
}

/* The failed box itself. Border AND a tinted background AND the message below,
   because border colour alone is the one cue a red-green colour-blind reader
   cannot rely on (roughly 1 in 12 men) — WCAG 1.4.1. */
.wform__field--error > input,
.wform__field--error > select,
.wform__field--error > textarea {
  border-color: #ef4444;
  background: rgba(239, 68, 68, .07);
}
.wform__field--error > input:focus,
.wform__field--error > select:focus,
.wform__field--error > textarea:focus {
  border-color: #ef4444;
  box-shadow: 0 0 0 3px rgba(239, 68, 68, .25);
}
/* The CAPTION only. `.wform__check` is also a direct-child label (the "I don't
   have a postcode" tick), and turning its text red says the tick is what failed
   when the tick is the way OUT of the failure. */
.wform__field--error > label:not(.wform__check) { color: #fca5a5; }

/* The sentence under the box. Rendered by the script on a client-side catch and
   present in the markup on a server bounce, so both paths look identical. */
.wform__field-error,
.wform__agree-error {
  display: flex;
  align-items: flex-start;
  gap: .35rem;
  margin-top: .4rem;
  font-size: .8125rem;
  font-weight: 600;
  line-height: 1.45;
  color: #fca5a5;
}
/* The warning glyph is decoration on top of text that already says it. */
.wform__field-error::before,
.wform__agree-error::before { content: "\26A0"; flex-shrink: 0; }
/* The agree row is a flex line (tick | long label), so a message inside it has
   to take the whole of the next one — whichever of the two class names it
   carries. Server-rendered markup uses .wform__agree-error; join_validate.js
   writes .wform__field-error everywhere for one code path. */
.wform__agree-error,
.wform__agree > .wform__field-error { flex-basis: 100%; }

/* The two agreement ticks and the per-child photo consent are not .wform__field
   rows, so they get their own outlined-block treatment. */
.wform__agree--error,
.wform__consent--error {
  border: 1px solid #ef4444;
  border-radius: var(--radius);
  background: rgba(239, 68, 68, .07);
  padding: .75rem;
}
.wform__minor--error { border-color: #ef4444; }

/* ---- Standing notice (the under-18 rule at the top of the join form) ----
   Orange rather than the red the error box uses: nothing has gone wrong yet,
   and a page that greets everybody with a red panel trains them to ignore the
   red panel that means something. */
.wform__notice {
  margin-bottom: 1.5rem;
  padding: .9rem 1.1rem;
  border: 1px solid var(--clr-orange);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: rgba(249, 115, 22, .12);
  color: var(--clr-text-2);
  font-size: .9375rem;
  line-height: 1.55;
}
.wform__notice strong { color: var(--clr-orange-4); }

/* ---- Waiver intake form (Urban Climb-style fields) ---- */

/* Grouped sections with a heading, separated by a rule. */
.wform__section {
  border-top: 1px solid var(--clr-border);
  margin-top: 1.75rem;
  padding-top: 1.5rem;
}
.wform__section-title {
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--clr-text);
  margin-bottom: .35rem;
}
.wform__section-note {
  font-size: .875rem;
  color: var(--clr-muted);
  margin-bottom: 1.25rem;
}

/* Per-field helper text (Urban Climb explains why each field is collected). */
.wform__help {
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--clr-muted);
  margin-top: .4rem;
}

/* "Australia" country tag beside phone/postcode labels. */
.wform__country {
  display: inline-block;
  margin-left: .4rem;
  padding: .1rem .5rem;
  border-radius: 999px;
  background: var(--clr-bg-alt);
  border: 1px solid var(--clr-border);
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--clr-muted);
  vertical-align: middle;
}

/* Inline checkbox rows (marketing opt-in, "no postcode"). */
.wform__check {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  margin-top: .75rem;
  color: var(--clr-muted-3);
  font-size: .9375rem;
  cursor: pointer;
}
.wform__check input {
  width: 1.1rem;
  height: 1.1rem;
  margin-top: .15rem;
  flex-shrink: 0;
  accent-color: var(--clr-teal);
  cursor: pointer;
}

/* Under-18 dependent cards. The "Under 18 · N" heading is a CSS counter, so it
   renumbers itself when a block is added or removed. */
#minorList { counter-reset: minor; }

.wform__minor {
  counter-increment: minor;
  background: var(--clr-bg-alt);
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  padding: 1.25rem;
  margin-bottom: 1rem;
}
.wform__minor .wform__field input,
.wform__minor .wform__field select { background: var(--clr-bg); }

.wform__minor-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}
.wform__minor-title {
  font-weight: 800;
  font-size: .9375rem;
  color: var(--clr-teal-4);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.wform__minor-title::before { content: "Under 18 · " counter(minor); }

.wform__minor-remove {
  background: none;
  border: none;
  color: var(--clr-muted);
  font-family: var(--font);
  font-size: .8125rem;
  font-weight: 600;
  cursor: pointer;
  padding: .35rem .5rem;
  min-height: 32px;
  transition: color .15s;
}
.wform__minor-remove:hover { color: var(--clr-orange-4); }

/* Per-child photo-consent question + Yes/No. */
.wform__consent {
  border: none;
  margin-top: 1rem;
  padding: 0;
}
.wform__consent legend {
  font-size: .875rem;
  line-height: 1.5;
  color: var(--clr-muted-3);
  padding: 0;
  margin-bottom: .6rem;
}
.wform__radios { display: flex; gap: .75rem; }
.wform__radio {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem 1.1rem;
  border: 1px solid var(--clr-border);
  border-radius: var(--radius);
  color: var(--clr-muted-3);
  font-size: .9375rem;
  cursor: pointer;
  transition: border-color .15s, color .15s;
}
.wform__radio input { accent-color: var(--clr-teal); cursor: pointer; }
.wform__radio:has(input:checked) {
  border-color: var(--clr-teal);
  color: var(--clr-text);
}

/* Add-under-18 button sits under the list. */
.wform__add-minor { margin-top: .25rem; }

/* Respect users who ask for less motion on the hero gradient/hover transitions
   already covered elsewhere; nothing added here animates. */

/* ---- Touch devices: finger-sized hit areas (audit follow-up) ----
   Coarse-pointer only, so the desktop layout the site was tuned for stays
   pixel-identical; phones/tablets get >=44px targets on the controls a thumb
   actually uses. */
@media (pointer: coarse) {
  /* Login popup close was a ~27px glyph — give it a real 44px box in the
     corner (glyph stays centred). */
  .login-modal__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    top: .25rem;
    right: .35rem;
  }

  /* Waiver text/select/date/textarea controls to the 44px touch minimum.
     Checkboxes and radios are excluded — they size themselves and shouldn't be
     stretched. */
  .wform input:not([type="checkbox"]):not([type="radio"]),
  .wform select,
  .wform textarea { min-height: 44px; }

  /* Per-child photo-consent Yes/No pills and the remove-child control. */
  .wform__radio { min-height: 44px; }
  .wform__minor-remove { min-height: 44px; }
}

/* ==========================================================================
   THE WALL (/the-wall/)
   The one public page rendered straight off live setting data. Each card is a
   photograph of a real wall with the count and circuit spread underneath, so
   the page is only ever as out of date as the last reset that was logged.
   ========================================================================== */
.wall-upcoming { color: var(--clr-teal-4); font-weight: 600; }

.wallgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: 1.5rem;
}

.wallcard {
  background: var(--clr-surface);
  border: 1px solid var(--clr-border);
  border-radius: 14px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* Anchor targets: /w/<slug>/ deep-links a walk-in straight to their wall, and
     without this the sticky nav lands on top of the card they were sent to. */
  scroll-margin-top: calc(var(--nav-h) + 1rem);
}

.wallcard__img {
  display: block;
  width: 100%;
  /* height:auto is load-bearing, not tidiness. The <img> carries width/height
     attributes (so the card reserves its space before the photo loads and the
     page doesn't jump), and those attributes are presentational hints that map
     to CSS width/height — a specified height makes aspect-ratio inert, which
     rendered every card at the photo's full 960px intrinsic height. */
  height: auto;
  /* Fixed ratio so a row of cards lines up even though the photos are shot at
     whatever the phone gave us; the walls are wide, so 3:2 crops least. */
  aspect-ratio: 3 / 2;
  object-fit: cover;
  background: var(--clr-bg-dark);
}

.wallcard__body { padding: 1.1rem 1.25rem 1.35rem; display: flex; flex-direction: column; gap: .5rem; }
.wallcard__name { font-size: 1.35rem; margin: 0; }
.wallcard__blurb { color: var(--clr-muted-3); margin: 0; max-width: var(--measure); }
.wallcard__stat { margin: 0; color: var(--clr-text-2); }
.wallcard__fresh {
  display: inline-block; margin-left: .5rem; padding: .05rem .5rem;
  border-radius: 999px; background: var(--clr-teal); color: #04211d;
  font-size: .8rem; font-weight: 800;
}
.wallcard__reset { margin: 0; color: var(--clr-muted); font-size: .9rem; }

/* Circuit spread. Tag colour IS the grade at Gripswich, so these carry their
   band colour inline from GRADE_BANDS rather than a class per grade — one
   mapping, in Python, used by every surface. */
.wallcard__spread { list-style: none; display: flex; flex-wrap: wrap; gap: .35rem; margin: 0; padding: 0; }
.wallchip {
  display: inline-flex; align-items: baseline; gap: .15rem;
  padding: .15rem .5rem; border-radius: 6px;
  font-weight: 800; font-size: .85rem; font-variant-numeric: tabular-nums;
  border: 1px solid rgba(0, 0, 0, .25);
}
.wallchip__n { font-size: .72rem; font-weight: 600; opacity: .75; }

.wall-footnote {
  margin-top: 2.5rem;
  color: var(--clr-muted-3);
  max-width: var(--measure);
}

/* --- Name release countdown on the portal Problems page ------------------- */
/* Plain list, one wall a line: it is read once a week to see whose turn it is,
   not scanned, so a grid would be more layout than the content earns. */
.reveal-list { list-style: none; margin: .5rem 0 0; padding: 0; }
.reveal-list li { padding: .25rem 0; font-size: .92rem; }


/* The header account button on a phone: smaller than the desktop CTA it shares
   markup with, because it sits between a logo and a hamburger on a 360px row. */
@media (max-width: 1079px) {
  .nav__actions .nav__login {
    padding: .45rem .7rem; font-size: .82rem; white-space: nowrap;
  }
}
@media (max-width: 380px) {
  .nav__actions .nav__login { padding: .4rem .55rem; font-size: .76rem; }
}
