/* ============================================================
   NEXUS JAZZ — signature layer (loads after base.css)
   Night gallery: the band presented like an exhibition.
   Every value is a token from theme.css.
   ============================================================ */

/* Hero intro tempo. ONE beat = the interval between announced lines; every
   hero-intro transition below is expressed as a fraction of it, so changing
   the BPM re-times the whole sequence (fades included), not just the cues.
   The inline script in home.html overwrites this per-BPM (default 38 BPM →
   60/38 = 1.579s); this value is the no-JS fallback. */
:root { --beat: 1.579s; }

/* --hero-peek: how far short of the fold the hero stops (the black strip).
   --about-crest is no longer read anywhere (2026-07-28: the about section's
   wall title, which used to crest into this strip as the scroll cue, moved
   to the hero subtitle and was removed) — kept only in case a future title
   returns and wants the same crest. */
:root {
  --hero-peek: 5rem;
  --about-crest: 1.5rem;
}

/* ---------- Wordmark ---------- */
.brand-word {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-xl);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text);
  line-height: 1;
}

/* Type is the design: geometric sans runs slightly open everywhere */
body { letter-spacing: 0.012em; }
.ex-lede, .wall-caption, .article__body { font-weight: 300; }
.brand-sub {
  display: block;
  font-family: var(--font-body);
  font-size: 0.5625rem;
  font-weight: 500;
  letter-spacing: 0.5em;
  text-transform: uppercase;
  color: var(--color-primary);
  margin-top: var(--space-1);
}

/* The old wordmark intro loader was replaced by the hero's own
   "announcer's introduction" light sequence (see .hero-stage below),
   which now serves as the page-load moment. */

/* ---------- Nav: quiet chrome over the work ----------
   Transparent over the hero (the photo leads), then it fades in its own ground
   once the hero is past — scripts.js adds .is-grounded. The ground is a
   gradient that is MASKED away at the foot instead of ending on a border, so
   there's no hard line across the page; the blur is on the same masked layer,
   which is what keeps the transition seamless (a plain backdrop-filter would
   cut off squarely at the nav's bottom edge). */
.nav {
  /* NB: do NOT set `position` here — base.css makes the nav `sticky`, and
     overriding it to `relative` scrolls the whole bar away. Sticky is already
     a positioned ancestor, so ::before's inset works as-is. */
  background: transparent;
  border-bottom: none;
  box-shadow: none;
}
.nav::before {
  content: "";
  position: absolute;
  /* Extends 2rem BELOW the bar so the fade finishes past the nav's own box —
     page text then dissolves gradually as it scrolls under, instead of hitting
     full-strength ground the moment it reaches the bar. */
  inset: 0 0 -2rem 0;
  z-index: 0;
  pointer-events: none;
  /* A LIFTED ground, not a darker one: at #0A0908 the page is already almost
     black, so a black band is invisible over dark sections. Surface-2 reads as
     smoked glass over both the photos and the black. */
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-surface-2) 94%, transparent) 0%,
    color-mix(in srgb, var(--color-surface-2) 80%, transparent) 58%,
    color-mix(in srgb, var(--color-surface-2) 0%, transparent) 100%
  );
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 52%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 52%, transparent 100%);
  opacity: 0;
  transition: opacity var(--t-slow) var(--ease-out);
}
.nav.is-grounded::before { opacity: 1; }
.nav__inner { position: relative; z-index: 1; }   /* links sit above the ground */
.nav__inner { height: 76px; }
.nav__links a {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
  border-radius: 0;
  padding: var(--space-2) var(--space-3);
}
.nav__links a:hover {
  color: var(--color-text);
  background: transparent;
}
.nav__links a.active { color: var(--color-primary); }
.nav__admin { color: var(--color-primary); }
@media (max-width: 900px) {
  .nav__links {
    top: 76px;
    background: var(--color-secondary);
    border-bottom: 1px solid var(--color-border);
  }
  /* Desktop gives the CTA a wider button padding + margin-left to set it
     apart from the plain text links sitting beside it in a horizontal bar —
     stacked in the mobile dropdown that just reads as "Book the Band"
     starting further right than every link above it. Match the same
     left padding as the plain links so all the labels line up on one edge. */
  .nav__cta { margin-left: 0; }
  .nav__cta .btn--book { padding-left: var(--space-3); }
}

/* Book CTA — the one brass moment in the chrome */
.nav__links a.btn--book { color: var(--color-inverse-text); }
.nav__links a.btn--book:hover { color: var(--color-inverse-text); background: var(--color-primary-dark); }
.btn--book,
.btn--book:hover {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-inverse-text);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-size: var(--text-xs);
  font-weight: 600;
  padding: 0.75em 1.6em;
  border-radius: var(--radius-sm);
}
.btn--book:hover {
  background: var(--color-primary-dark);
  border-color: var(--color-primary-dark);
  transform: translateY(-1px);
}
.nav__cta { margin-left: var(--space-3); }

/* Brass buttons carry dark text everywhere (contrast) */
.btn-primary, .btn--primary, .btn--accent {
  color: var(--color-inverse-text);
}
.btn-primary:hover, .btn--primary:hover, .btn--accent:hover {
  color: var(--color-inverse-text);
}

/* ---------- Exhibition headers: index number + kicker + wall title ----- */
.ex-header { margin-bottom: var(--space-12); }
/* Heading-only header (no lede paragraph under it) — the default
   margin-bottom was tuned for a header + lede pair, so a bare heading
   leaves an oversized, awkward-looking gap before whatever follows. */
.ex-header--tight { margin-bottom: var(--space-6); }
.ex-kicker {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-primary);
  margin: 0 0 var(--space-4);
}
.ex-kicker::after {
  content: "";
  flex: 0 0 3rem;
  height: 1px;
  background: var(--color-border);
}
.ex-num { color: var(--color-text-light); font-variant-numeric: tabular-nums; }
/* Bigger statement variant — home's "See Us Live" heading, sitting alone
   over the shader with nothing else to anchor the section's visual weight. */
.live-title--big { font-size: var(--text-hero); }
/* A single word picked out in brass + italic — the site's own established
   "colour, not italics, for the one emphasis" rule (see the About section's
   CLAUDE.md history) applies to body copy, not a big display heading like
   this, so a heading is allowed the italic too. Used to give a bare
   two/three-word section title some visual interest without adding a
   whole lede paragraph back. */
.text-accent { color: var(--color-primary); font-style: italic; }
/* "Action" gets colour, not italic — a deliberate one-off warm red (not the
   error red, not brass) called for by the user specifically for this word;
   Live keeps the brass/italic .text-accent treatment above. */
.text-accent--live { color: #C1443A; font-style: normal; }

/* Decorative white music-note line art flanking the "See Us in Action"
   heading, over the viewing room's near-black ground — solid enough to read
   as line art rather than a faint texture. */
/* Tighter bottom padding than the shared .section-x default — this section
   hands off directly into the ivory Booking CTA below it, so the standard
   spacious S3 rhythm left an oversized black void between the button and the
   next section's content. Top stays spacious (unchanged). */
.viewing-room { position: relative; overflow: hidden; padding-bottom: var(--section-y-s1); }
.live-x__note {
  position: absolute;
  top: 50%;
  width: clamp(5rem, 9vw, 9rem);
  opacity: 0.4;
  pointer-events: none;
  transform: translateY(-50%);
}
.live-x__note--left { left: clamp(-1rem, 4vw, 4rem); }
.live-x__note--right { right: clamp(-1rem, 4vw, 4rem); transform: translateY(-50%) scaleX(-1); }
@media (max-width: 720px) {
  .live-x__note { display: none; }
}
.ex-title {
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: var(--text-3xl);
  line-height: var(--leading-tight);
  margin: 0;
  max-width: 20ch;
  text-wrap: balance;
}
.ex-lede {
  font-size: var(--text-lg);
  color: var(--color-text-muted);
  max-width: var(--max-width-prose);
  margin: var(--space-4) 0 0;
}

/* ============================================================
   Hero — "the announcer's introduction", editorial-mosaic build
   (imported from the Nexus Jazz Home Claude Design, project
   99bb8987-878d-49db-bfa9-c4411ab2507e; mosaic redesign 2026-07-24,
   spots removed + light-switch flip 2026-07-25).

   The section is a full-bleed exhibition wall that fills every pixel: a
   full-height FEATURE portrait (centre) that holds the announcer type,
   flanked by eight gallery TILES of other scenes. It plays as a theatre:
   the stage holds BLACK while the announcer names the act one line at a
   time on the darkness ("Welcome to the viewing of" → "Keenan Harmon" →
   "and the Nexus Jazz Group"), holds a beat — then the WHOLE stage flips
   on at once, like a light switch: feature portrait, all eight tiles,
   house-light wash and chrome (subtitle, buttons, nav, scroll cue) snap
   to full together. No follow-spots, no stagger — one clean cut to light.

   Driven by a data-phase attribute on :root, set synchronously by the
   inline script in home.html BEFORE the hero paints (no flash) and
   advanced by that same script's timers. The DEFAULT — no data-phase
   attribute at all — is the fully composed "alive" wall, so no-JS,
   reduced-motion and repeat visits all get the finished hero with nothing
   hidden. Every phase override below only SUBTRACTS from that composed
   default, so an interrupted sequence can only ever reveal MORE, never a
   black screen. The flip is instant because the lit elements carry NO
   transition (transition: none on the composed default) — subtractivity
   holds since that default already IS the end state.
   ============================================================ */
.hero-stage {
  position: relative;
  /* Fill the WHOLE viewport, pulled up under the sticky nav (negative
     margin equal to nav height) so the hero photo runs edge-to-edge behind
     the now-transparent nav instead of stopping at its own separate band.
     Total on-screen footprint (nav + hero) is still exactly 100svh — the
     nav-height is reclaimed via the negative margin, not added on top —
     so the CTAs still clear the fold. --nav-h is defined here and reused
     so the two never drift. */
  --nav-h: 77px;
  /* Stops --hero-peek short of the fold, leaving a black strip below. */
  height: calc(100svh - var(--hero-peek));
  margin-top: calc(-1 * var(--nav-h));
  min-height: 34rem;
  overflow: hidden;
  background: var(--color-secondary);
}
/* NB: the hero's next sibling is the Replay button, not the section — an
   adjacent-sibling rule here silently matches nothing. The about section sets
   its own top padding (see .about). */
.hero-stage + .section-x { padding-top: var(--section-y); }

/* ---- the stage picture: ONE full-bleed portrait that fills the whole
   section. Black until the final announced line, then it FADES up together
   with "and the Nexus Jazz Group" (phase 3) and stays for the finished hero. */
.hero-stage__feature {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  background: var(--color-secondary);
  opacity: 1; /* composed default = picture up */
  transition: opacity calc(var(--beat) * 0.85) var(--ease-out); /* fade-up on the last line */
}
/* <picture> is inline by default — display:contents drops its own box so the
   img inside lays out exactly as if it were a direct child (mobile-swap
   source added 2026-07-28, see home.html). */
.hero-stage__feature picture,
.about__frame picture { display: contents; }
.hero-stage__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* full-bleed crop of a tall portrait — sit the window high enough that the
     whole band's faces (standing row + seated Keenan) all clear the top. */
  object-position: center var(--hero-focal-y, 11%);
  filter: saturate(1.2) brightness(1.05) contrast(1.03);
}
/* legibility scrim over the picture: darkens the centre where the type sits
   and the top/bottom edges for framing, without flattening the faces. */
.hero-stage__scrim {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background:
    radial-gradient(125% 96% at 50% 50%, rgba(6,5,4,0.62) 0%, rgba(6,5,4,0.26) 44%, rgba(6,5,4,0) 76%),
    linear-gradient(to bottom, rgba(6,5,4,0.52) 0%, rgba(6,5,4,0.12) 28%, rgba(6,5,4,0.12) 60%, rgba(6,5,4,0.62) 100%);
}

/* ---- follow-spots: luminous white pools that CUT in on each of the two
   BLACK-STAGE lines (light-switch snap), then fade out as the picture rises
   on the last line — the picture IS the light on that beat, so no spot lands
   on the finished frame. They belong to the ANIMATION ONLY (opacity 0 by
   default). hero-intro.js parks each on its line via --spotN-left/top; the
   fallbacks below centre them for the no-JS case. */
.hero-stage__spot {
  position: absolute;
  z-index: 3;
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: screen;
  filter: blur(11px);
  opacity: 0;                              /* finished hero = no circles */
  /* Governs the fade-OUT, which runs UNDER the picture's fade-up (0.85 beat)
     and finishes first: the spots must be gone by the time the finished frame
     lands — a glow hanging over the revealed picture was the defect. Beat-
     relative so it stays correct at any BPM. */
  transition: opacity calc(var(--beat) * 0.35) linear;
}
.hero-stage__spot--1 {
  width: 360px; height: 360px; margin: -180px 0 0 -180px;
  left: var(--spot1-left, 50%); top: var(--spot1-top, 41%);
  background: radial-gradient(circle at 50% 50%, rgba(255,255,255,0.80) 0%, rgba(255,255,255,0.54) 30%, rgba(255,255,255,0.18) 54%, transparent 72%);
}
.hero-stage__spot--2 {
  width: 520px; height: 520px; margin: -260px 0 0 -260px;
  left: var(--spot2-left, 50%); top: var(--spot2-top, 50%);
  background: radial-gradient(circle at 50% 50%, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0.56) 30%, rgba(255,255,255,0.20) 54%, transparent 72%);
}

/* ---- the announcer's copy, centred in the stage (the spots land on each
   line, the picture fades up behind it). */
.hero-stage__inner {
  position: absolute;
  z-index: 6;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  width: 100%;
  max-width: 42rem;
  margin: 0 auto;
  padding: 0 clamp(1.5rem, 4vw, 3rem);
  text-align: center;
}
.hero-stage__welcome,
.hero-stage__name,
.hero-stage__group {
  transition: opacity calc(var(--beat) * 0.72) var(--ease-out),
              text-shadow calc(var(--beat) * 0.72) var(--ease-out);
}
.hero-stage__welcome {
  font-family: var(--font-body);
  /* shrink-wrap + auto-margins keep each announced line centred as its
     own text box (independent of the column width). */
  width: fit-content;
  max-width: 100%;
  font-size: clamp(0.66rem, 0.55rem + 0.7vw, 0.92rem);
  font-weight: 400;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: #efe7d6;
  margin: 0 auto clamp(0.7rem, 1.5vh, 1.1rem);
  /* Dark drop only — the earlier white-glow halo (a soft gradient bloom
     around the letters) is gone so the type reads crisp; the dark shadow
     is what keeps it legible over the bright spotlight core and photo. */
  text-shadow: 0 2px 22px rgba(0,0,0,0.72);
}
.hero-stage__title { margin: 0; font-weight: 400; }
.hero-stage__name {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  font-family: var(--font-heading);
  font-weight: 500;
  /* Sized to hold "Keenan Harmon" on ONE line at desktop widths so the
     block stays short enough to sit below his face; wraps only on narrow
     phones, where the mobile layout centres it over the portrait anyway. */
  font-size: clamp(2.4rem, 1rem + 4.4vw, 4.9rem);
  line-height: 0.98;
  letter-spacing: -0.01em;
  color: #f8f3e7;
  text-shadow: 0 2px 30px rgba(0,0,0,0.82);
}
.hero-stage__group {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin: clamp(0.6rem, 1.4vh, 1rem) auto 0;
  text-shadow: 0 2px 26px rgba(0,0,0,0.78);
}
.hero-stage__group-lead {
  display: block;
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(0.95rem, 0.78rem + 0.9vw, 1.45rem);
  color: #d9d0ba;
  margin-bottom: 0.2rem;
}
.hero-stage__group-name {
  display: block;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: clamp(1.5rem, 0.8rem + 2.6vw, 3rem);
  line-height: 1;
  letter-spacing: -0.005em;
  color: #f8f3e7;
}
.hero-stage__lower {
  /* rises WITH the picture (same duration, no delay) — see the phase block */
  transition: opacity calc(var(--beat) * 0.85) var(--ease-out);
  margin-top: clamp(1rem, 2.2vh, 1.6rem);
}
.hero-stage__sub {
  max-width: 38ch;
  margin: 0 auto;
  font-size: clamp(0.9rem, 0.82rem + 0.5vw, 1.05rem);
  font-weight: 300;
  line-height: 1.7;
  color: #efe9db;
  text-shadow: 0 2px 18px rgba(0,0,0,0.7);
}
.hero-stage__actions {
  display: flex;
  gap: var(--space-4);
  justify-content: center;
  flex-wrap: wrap;
  margin-top: clamp(1rem, 2vh, 1.5rem);
}

/* ---- replay control. */
.hero-stage__replay {
  position: fixed;
  bottom: 1.4rem;
  right: 1.4rem;
  z-index: 70;
  background: color-mix(in srgb, var(--color-secondary) 80%, transparent);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  padding: 0.55rem 1rem;
  font-family: var(--font-body);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  border-radius: var(--radius-sm);
  cursor: pointer;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: opacity calc(var(--beat) * 0.85) var(--ease-out), color .3s, border-color .3s;
}
.hero-stage__replay:hover {
  color: var(--color-primary);
  border-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
}
/* Doubles as "Skip intro" while the intro is actually playing (phases 0–2) —
   pure CSS swap keyed off data-phase, so the label is never wrong for a
   frame the way a JS-driven swap could be on a slow load. hero-intro.js
   reads data-phase at click time to jump straight to phase 3 instead of
   restarting. Unlike the rest of "chrome" this button is NOT hidden during
   those phases (see the phase block below) — it needs to stay visible and
   clickable to actually work as a skip control. */
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .hero-stage__replay-label { display: none; }
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .hero-stage__replay::after { content: "Skip intro"; }

/* =============== PHASE / INTRO ===============
   No data-phase attribute → the FINISHED hero: picture up, type + chrome in,
   NO circles. The intro plays only while the inline script sets data-phase.

   Beats (one beat apart, tempo set in BPM — see --beat at the top of this
   file): 0 black → 1 "Welcome…" (spot 1) → 2 "Keenan Harmon" (spot 2) →
   3 the WHOLE hero rises together: group line, picture, subtitle, CTAs, nav
   and replay, while the spots wash out. Phase 3 IS the finished hero — the
   sequence ends there and NOTHING fades in afterwards.

   The picture / type / chrome rules are SUBTRACTIVE — they only hide what the
   finished hero shows, so a stalled sequence can reveal more, never a black
   screen. The follow-spots are the one ADDITIVE layer (absent by default,
   switched ON only for phases 1–2): if JS never runs there are simply no
   circles, which IS the finished look — so additivity is safe here. */

/* Picture: black until the last line, then fades up WITH the group name. */
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .hero-stage__feature { opacity: 0; }

/* Announced lines fade in one at a time; the group line lands with the picture. */
:root[data-phase="0"] .hero-stage__welcome { opacity: 0; }
:root:is([data-phase="0"], [data-phase="1"]) .hero-stage__name { opacity: 0; }
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .hero-stage__group { opacity: 0; }

/* Follow-spots: cut ON (transition:none) on each line's cue while the stage is
   BLACK (phases 1–2). At phase 3 the rules drop away, so the default opacity 0
   + fade takes over and they wash out as the picture rises — the finished frame
   (and phase 3 onward) carries no glow at all. */
:root[data-phase="1"] .hero-stage__spot--1,
:root[data-phase="2"] .hero-stage__spot--1,
:root[data-phase="2"] .hero-stage__spot--2 { transition: none; }
:root:is([data-phase="1"], [data-phase="2"]) .hero-stage__spot--1 { opacity: 0.46; }
:root[data-phase="2"] .hero-stage__spot--2 { opacity: 0.50; }

/* Chrome (subtitle + CTAs, replay, site nav) — held only while the stage is
   BLACK, then it rises WITH the picture on the last beat: same duration, no
   delay, so phase 3 lands the complete hero in one move and nothing fades in
   afterwards (user, 2026-07-26). */
.nav { transition: opacity calc(var(--beat) * 0.85) var(--ease-out); }
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .hero-stage__lower { opacity: 0; }
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .nav {
  opacity: 0;
  pointer-events: none;
}

/* The PAGE BELOW the hero is part of the black stage: whatever shows past the
   fold (the about section's top edge) is held dark for the whole announcement
   and arrives on the SAME beat as the picture, nav and CTAs — one reveal, no
   second event (user, 2026-07-27). Subtractive like every other phase rule, so
   no-JS / reduced-motion / repeat visits show it composed. */
.about { transition: opacity calc(var(--beat) * 0.85) var(--ease-out); }
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) .about { opacity: 0; }
/* …and the page ground itself takes the hero's deeper black for the duration,
   otherwise the strip below the hero shows as a faint seam (--color-bg #0E0D0B
   against the stage's #0A0908) across an otherwise perfectly black screen. */
body { transition: background-color calc(var(--beat) * 0.85) var(--ease-out); }
:root:is([data-phase="0"], [data-phase="1"], [data-phase="2"]) body {
  background-color: var(--color-secondary);
}

/* ---- Tablet / mobile: same single full-bleed picture with the centred
   announcer type; a phone viewport is portrait so the tall image crops
   kindly. Just tighten the measure and lean the scrim on legibility. */
@media (max-width: 820px) {
  .hero-stage__inner { max-width: 34rem; }
  .hero-stage__sub { max-width: 40ch; margin: 0 auto; }
  .hero-stage__actions { justify-content: center; }
  .hero-stage__scrim {
    background:
      radial-gradient(150% 84% at 50% 50%, rgba(6,5,4,0.6) 0%, rgba(6,5,4,0.3) 46%, rgba(6,5,4,0) 82%),
      linear-gradient(to bottom, rgba(6,5,4,0.5) 0%, rgba(6,5,4,0.2) 34%, rgba(6,5,4,0.2) 62%, rgba(6,5,4,0.62) 100%);
  }
  .hero-stage__spot--2 { width: 420px; height: 420px; margin: -210px 0 0 -210px; }
}
@media (max-width: 480px) {
  .hero-stage__replay { display: none; } /* the CTAs need the room on a phone */
}

@media (prefers-reduced-motion: reduce) {
  .about, body { transition: none; }
  .hero-stage__img { animation: none; }
  .hero-stage__replay { display: none; }
  .hero-stage__welcome,
  .hero-stage__name,
  .hero-stage__group,
  .hero-stage__lower,
  .hero-stage__feature,
  .hero-stage__spot,
  .nav { transition: none; }
}

/* ---- legacy hero classes retained only where still shared ----
   .hero-x__reassure is reused by the closing CTA band; .btn--ghost is
   site-wide. The rest of the old single-photo hero was removed with
   the announcer rebuild. */
.hero-x__reassure {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  margin: var(--space-3) 0 0;
  text-shadow: 0 1px 12px rgba(0,0,0,0.6);
}

/* Ghost button on dark */
.btn--ghost {
  background: transparent;
  color: var(--color-text);
  border: 1px solid var(--color-text-light);
  border-radius: var(--radius-sm);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-size: var(--text-xs);
  font-weight: 500;
  padding: 0.8em 1.6em;
}
.btn--ghost:hover {
  border-color: var(--color-text);
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
  text-decoration: none;
}

/* ---------- Sections ---------- */
.section-x { padding-block: var(--section-y); }
.section-x--hairline { border-top: 1px solid var(--color-border); }

/* ============================================================
   ABOUT / INFO HUB (#sound) — the band's "about" and the page's
   signpost to the band, media and news. Two columns: a left copy
   column (left-aligned, NO italics — the brass emphasis is colour
   only) and a floating portrait on the right that drifts slowly so
   the section reads alive rather than static. Replaced the old
   centred "01 The Sound" wall-text block, 2026-07-26.
   ============================================================ */
.about {
  position: relative;
  overflow: hidden;                       /* clips the floater's drift */
  /* No wall title to crest into the hero's black strip anymore (2026-07-28,
     title moved to the hero subtitle) — plain section padding. */
  padding-top: var(--section-y);
  padding-bottom: calc(var(--section-y) * 1.35);
}
.about__grid {
  display: grid;
  gap: var(--space-16);   /* stacked: the picture needs room off the buttons */
  align-items: start;
}
@media (min-width: 900px) {
  .about__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
    column-gap: clamp(var(--space-16), 8vw, var(--space-32));
    row-gap: var(--space-16);
  }
}

/* A short measure is what makes long copy feel calm. */
.about__copy { max-width: 32rem; }

/* Body copy is set in the SERIF (--font-heading, Fraunces) rather than the UI
   sans: Jost at 300 read thin and drab at paragraph length (user, 2026-07-27).
   An old-style serif with real contrast is what makes a long editorial passage
   comfortable — the sans stays for labels, buttons and chrome. Broken into two
   paragraphs at the sentence break; words are the client's, unchanged. */
.about__lede {
  margin: 0;
  font-family: var(--font-heading);
  font-size: clamp(0.97rem, 0.92rem + 0.24vw, 1.09rem);
  font-weight: 400;
  line-height: 1.75;
  letter-spacing: 0.002em;
  color: color-mix(in srgb, var(--color-text) 84%, var(--color-secondary));
  max-width: 42ch;
}
.about__lede + .about__lede { margin-top: var(--space-6); }

.about__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-12);
}
/* Section anchor: brass OUTLINE, not brass fill — the filled brass button is
   reserved site-wide for booking, so this reads as second in the hierarchy. */
.about__cta {
  border: 1px solid var(--color-primary);
  color: var(--color-primary);
  background: transparent;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-size: var(--text-xs);
  font-weight: 600;
  padding: 0.75em 1.6em;
  border-radius: var(--radius-sm);
  transition: background var(--transition), color var(--transition), transform var(--transition);
}
.about__cta:hover {
  background: var(--color-primary);
  color: var(--color-inverse-text);
  transform: translateY(-1px);
  text-decoration: none;
}
/* ---- the floating portrait. The photo is a warm, bright room shot, so it
   gets pulled toward the night-gallery ground (desaturated a touch, dimmed,
   with a black gradient at the foot) and lit back up on hover. A brass
   hairline sits offset BEHIND the frame — the site's hairline motif with
   depth. The whole figure drifts on a long, slow loop: the "alive" cue,
   transform-only, off under prefers-reduced-motion. */
.about__media {
  position: relative;
  margin: 0;
  justify-self: center;
  width: min(100%, 25rem);
}
/* The drift lives on the INNER frame, never on .about__media: the figure
   carries .reveal, whose entrance uses `transform`, and a CSS animation on the
   same property always beats it — the photo would silently lose its reveal.
   Bonus: the ::before hairline stays anchored while the picture drifts against
   it, which reads as real depth instead of the whole unit sliding. */
.about__frame {
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  animation: about-float 11s var(--ease-in-out) infinite alternate;
}
/* brass hairline, offset UP-RIGHT behind the picture — up rather than down so
   it never crosses the caption sitting under the frame */
.about__media::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) auto var(--space-6);
  aspect-ratio: 4 / 5;
  border: 1px solid color-mix(in srgb, var(--color-primary) 40%, transparent);
  pointer-events: none;
}
/* foot gradient: sits the bright wooden floor into the black section */
.about__frame::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  background: linear-gradient(to bottom, rgba(10,9,8,0) 0%, rgba(10,9,8,0.30) 55%, rgba(10,9,8,0.72) 100%);
  pointer-events: none;
}
.about__media img {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center 24%;   /* keeps all five faces in the crop */
  filter: saturate(0.86) contrast(1.04) brightness(0.9);
  transition: filter var(--t-slow) var(--ease-out), transform var(--t-slow) var(--ease-out);
}
/* the band "warms up" — and the drift settles — under the pointer */
.about__media:hover .about__frame { animation-play-state: paused; }
.about__media:hover img {
  filter: saturate(1.02) contrast(1.03) brightness(1);
  transform: scale(1.03);
}
.about__media figcaption {
  position: relative;
  margin-top: var(--space-4);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-light);
}
@keyframes about-float {
  from { transform: translate3d(0, -9px, 0); }
  to   { transform: translate3d(0, 9px, 0); }
}

@media (max-width: 899px) {
  /* Stacked: the picture takes the column and the offset hairline is dropped —
     with nothing beside it the frame read as a stray box, not as depth. */
  .about__media { width: min(100%, 30rem); justify-self: start; }
  .about__media::before { content: none; }
  .about__media img { aspect-ratio: 3 / 4; }
}
@media (prefers-reduced-motion: reduce) {
  .about__frame { animation: none; }
}

/* Wall text — the statement */
.wall-text {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--text-2xl);
  line-height: 1.45;
  max-width: 30ch;
  margin: 0;
  text-wrap: balance;
}
.wall-text em { font-style: italic; color: var(--color-primary); }
.wall-caption {
  margin-top: var(--space-6);
  font-size: var(--text-base);
  color: var(--color-text-muted);
  max-width: var(--max-width-prose);
}

/* Darkens the simplex-noise shader mounted behind this section (see
   live-shader.js) so its brighter bands read as jewel-tones instead of a
   blown-out poster the event text has to fight — same job as the hero's
   own scrim. Inserted by JS right after the canvas. #live-shader-host gets
   `isolation:isolate` from the shader lib's own injected stylesheet
   (paper-shaders shader-mount.js), which makes z-index authoritative
   inside it — so DOM order does NOT decide paint order here, whatever
   this comment used to say. Under `isolation:isolate`, a non-positioned
   box like `.container` (z-index:auto) always paints BEFORE any
   explicitly z-indexed positioned box, even z-index:0 — so a scrim at
   z-index:0 painted OVER the event-row text instead of under it (the bug:
   text read as near-black, "readable" only on hover because the hover
   fill diluted the opaque scrim sitting on top of everything). The
   canvas itself is z-index:-1 (set by the same injected stylesheet); the
   scrim has to sit at that same negative tier — behind non-positioned
   content, above the canvas by DOM order — not at 0. */
#live-shader-host { position: relative; }
.live-shader__scrim {
  position: absolute; inset: 0; z-index: -1; pointer-events: none;
  /* Plain rgba, not color-mix() — Safari has been seen not resolving
     color-mix() against a custom property until a forced style recalc
     (e.g. on scroll), so the section briefly paints unscrimmed (full
     vivid shader colour = "light") before snapping to the real dark
     value a moment later. Same #0A0908/0.7 either way, just without
     the recalc dependency. Opacity eased from 0.82 (2026-07-27, third
     reference pass): the shader's own palette already caps its brightest
     band (amber) below the event text's cream colour, so the scrim's job
     is only to knock back saturation/vibrance, not carry all of the
     legibility work alone — at 0.82 it was crushing the palette's
     carefully-spaced dark bands into indistinguishable near-black,
     which read as "no definition" against the reference image. */
  background: rgba(10, 9, 8, 0.7);
}

/* ---------- Calendar embed (the /events/ "Live" page — a Google Calendar
   iframe, not a hand-entered event list) ---------- */
.calendar-embed {
  margin-top: var(--space-8);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--color-surface);
}
.calendar-embed iframe,
.calendar-embed table {
  display: block;
  width: 100% !important;
  height: min(48rem, 80vh) !important;
  border: 0 !important;
}


/* Featured gallery photos + one video — home's "See Us in Action" section.
   Admin picks items via GalleryPhoto/GalleryVideo.is_featured; section
   shows heading + button only when nothing is marked, so it never breaks
   with empty content. Same data-carousel-* JS API as the band page's
   .shots player-photo carousel (static/js/scripts.js) — just bigger and
   wider here, since this is a headline home-page showcase rather than a
   card-sized portrait next to a bio. Without JS every slide stacks in
   flow (nothing hidden); JS adds .is-live to crossfade + show controls. */
.action-carousel {
  position: relative;
  margin: 0 auto var(--space-8);
  max-width: 54rem;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--color-secondary);
}
.action-carousel__stage { height: 100%; }
.action-carousel__slide {
  display: block;
  height: 100%;
  margin: 0;
  position: relative;
}
.action-carousel__slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.action-carousel.is-live .action-carousel__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--t-slow) var(--ease-out);
}
.action-carousel.is-live .action-carousel__slide.is-current { opacity: 1; }
.action-carousel.is-live .action-carousel__arrow { display: grid; }
.action-carousel.is-live .action-carousel__dots { display: flex; }
.action-carousel__arrow,
.action-carousel__dots { display: none; }
/* foot scrim so the dots always read, even over a bright photo */
.action-carousel::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 5rem;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(10,9,8,0) 0%, rgba(10,9,8,0.6) 100%);
}
.action-carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  place-items: center;
  width: 3rem;
  height: 3rem;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--color-text) 34%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-secondary) 72%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
  transition: background var(--transition), border-color var(--transition),
              color var(--transition), opacity var(--transition), transform var(--transition);
}
.action-carousel__arrow span { display: block; margin-top: -0.12em; }
.action-carousel__arrow--prev { left: var(--space-4); }
.action-carousel__arrow--next { right: var(--space-4); }
.action-carousel__arrow:hover,
.action-carousel__arrow:focus-visible {
  opacity: 1;
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-secondary);
  transform: translateY(-50%) scale(1.08);
}
.action-carousel__dots {
  position: absolute;
  left: 0; right: 0; bottom: var(--space-4);
  z-index: 2;
  justify-content: center;
  gap: var(--space-2);
}
.action-carousel__dot {
  width: 0.6rem;
  height: 0.6rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-text) 45%, transparent);
  cursor: pointer;
  transition: background var(--transition), width var(--transition);
}
.action-carousel__dot.is-current { background: var(--color-primary); width: 1.6rem; }
@media (max-width: 640px) {
  .action-carousel { aspect-ratio: 4 / 3; }
  .action-carousel__arrow { width: 2.75rem; height: 2.75rem; }
}

/* ---------- Listen / Music (kept on the site's night-gallery dark) ----------
   2026-07-27: this used to be the one ivory "inverse" section; the client asked
   for booking to be the light section instead and music to stay black, so the
   inverse treatment moved to .cta-x below. .viewing-room now just takes the
   page's default dark background/text — no overrides needed. */

/* ---------- Photography: FULL COLOUR everywhere ----------
   The grayscale-at-rest / colour-on-hover duotone was removed 2026-07-27 at the
   user's request — every photo on the site now renders at full colour. The only
   image treatment left is a slight contrast lift and the hover scale. Do not
   reintroduce grayscale filters. */
.duo { overflow: hidden; border-radius: var(--radius-sm); margin: 0; position: relative; }
.duo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--t-slow) var(--ease-out);
}
.duo:hover img, .duo:focus-within img { transform: scale(1.02); }

/* ---------- Gallery grid + lightbox (dark) ---------- */
.gallery-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}
.gallery-item {
  padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-sm); overflow: hidden;
}
.gallery-item img {
  height: 17rem; width: 100%; object-fit: cover; display: block;
  transition: transform var(--t-slow) var(--ease-out);
}
.gallery-item:hover img, .gallery-item:focus-visible img { transform: scale(1.03); }
.gallery-modal {
  /* The global `* { margin: 0 }` reset (base.css) strips <dialog>'s own
     UA-stylesheet `margin: auto`, which is what centers a top-layer dialog
     by default — without it the dialog falls back to sitting pinned at its
     initial-containing-block position (top-left). Restore centering
     explicitly rather than carving out a reset exception. */
  position: fixed;
  inset: 0;
  margin: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0;
  background: var(--color-secondary);
  max-width: min(92vw, 70rem);
  max-height: 90vh;
  box-shadow: var(--shadow-lg);
}
.gallery-modal::backdrop { background: color-mix(in srgb, var(--color-secondary) 92%, transparent); }
.gallery-modal img { max-height: 85vh; width: auto; max-width: 100%; display: block; margin-inline: auto; }
.modal-close {
  position: absolute; top: var(--space-2); right: var(--space-2);
  z-index: 2;
  width: 2.25rem; height: 2.25rem;
  border-radius: var(--radius-full); border: 1px solid var(--color-border);
  background: var(--color-surface); color: var(--color-text);
  cursor: pointer; font-size: var(--text-lg); line-height: 1;
}
.modal-close:hover { border-color: var(--color-primary); color: var(--color-primary); }

/* ---- prev/next, same circular brass-hairline treatment as .shots__arrow */
.modal-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--color-text) 34%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-secondary) 72%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--color-text);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
  transition: background var(--transition), border-color var(--transition),
              color var(--transition), opacity var(--transition);
}
.modal-nav span { display: block; margin-top: -0.12em; } /* optical centring */
.modal-nav--prev { left: var(--space-3); }
.modal-nav--next { right: var(--space-3); }
.modal-nav:hover,
.modal-nav:focus-visible {
  opacity: 1;
  background: color-mix(in srgb, var(--color-secondary) 90%, transparent);
  border-color: var(--color-primary);
  color: var(--color-primary);
}
@media (max-width: 600px) {
  .modal-nav { width: 2.75rem; height: 2.75rem; }
  .modal-nav--prev { left: var(--space-2); }
  .modal-nav--next { right: var(--space-2); }
}
.empty-note { color: var(--color-text-light); font-size: var(--text-sm); }

/* ---------- Media embeds ---------- */
.embed-grid { display: grid; gap: var(--space-6); grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); }
.embed-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.embed-card iframe { display: block; width: 100%; border: 0; }
.embed-card--audio iframe { height: 152px; }
.embed-card figcaption {
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  border-top: 1px solid var(--color-border);
}

/* Video cards link out to YouTube rather than embedding — some videos
   restrict embedding to an approved domain list and throw an opaque
   error 153 in an iframe; a poster + play button always works. */
.video-link { display: block; text-decoration: none; color: inherit; }
.video-link__thumb {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  display: block;
  background: var(--color-secondary);
}
.video-link__thumb img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform var(--t-slow) var(--ease-out);
}
.video-link:hover .video-link__thumb img,
.video-link:focus-visible .video-link__thumb img { transform: scale(1.03); }
.video-link__play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 4rem; height: 4rem;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-secondary) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-text) 40%, transparent);
  display: flex; align-items: center; justify-content: center;
  transition: background var(--t-base) var(--ease-out), transform var(--t-base) var(--ease-out);
}
.video-link__play::after {
  content: "";
  width: 0; height: 0;
  border-style: solid;
  border-width: 0.7rem 0 0.7rem 1.15rem;
  border-color: transparent transparent transparent var(--color-text);
  margin-left: 0.2rem;
}
.video-link:hover .video-link__play,
.video-link:focus-visible .video-link__play {
  background: var(--color-primary);
  transform: translate(-50%, -50%) scale(1.08);
}
.video-link__meta {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--color-border);
  display: flex; flex-direction: column; gap: var(--space-1);
}
.video-link__label {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--color-text);
  line-height: var(--leading-tight);
}
.video-link__caption { font-size: var(--text-sm); color: var(--color-text-muted); }

/* ============================================================
   THE BAND — roster cards (picture left, bio right)
   Each player is a CARD: hairline border on the raised surface, picture in a
   fixed 4/5 frame on the left, bio on the right, everything visible without
   interaction. Members with more than one picture get a crossfade carousel
   (.shots) with brass-hairline arrows — same data-carousel-* API as the Miller
   Piano hero, but MANUAL (no autoplay beside reading copy).

   NOTE: the picture MUST stay inside a fixed aspect frame with object-fit:
   cover. Letting a 2048px press shot size itself is what made this page look
   broken once (2026-07-27) — huge full-bleed images with the bio stranded
   underneath.
   ============================================================ */
.roster {
  display: grid;
  gap: var(--space-8);
  margin-top: var(--space-12);
}
.player-card {
  display: grid;
  gap: var(--space-6);
  align-items: center;
}
@media (min-width: 780px) {
  .player-card {
    grid-template-columns: minmax(0, 23rem) minmax(0, 1fr);
    gap: clamp(var(--space-8), 3vw, var(--space-12));
  }
}

/* ---- the picture: always a 4/5 frame, whatever the source file */
.shots {
  position: relative;
  margin: 0;
  overflow: hidden;
  background: var(--color-secondary);
  aspect-ratio: 4 / 5;
}
.shots__stage,
.shots__slide { height: 100%; margin: 0; }
.shots img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.shots--empty {
  display: flex;
  align-items: center;
  justify-content: center;
}
.player-card__initial {
  font-family: var(--font-heading);
  font-size: var(--text-hero);
  color: var(--color-text-light);
  opacity: 0.45;
  user-select: none;
}

/* Without JS the slides stack in flow inside the frame (only the first is in
   view) and the controls stay hidden — nothing points at nothing. JS adds
   .is-live, which absolutely-positions the slides and shows the controls. */
.shots__arrow,
.shots__dots { display: none; }
.shots.is-live .shots__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--t-slow) var(--ease-out);
}
.shots.is-live .shots__slide.is-current { opacity: 1; }
.shots.is-live .shots__arrow { display: grid; }
.shots.is-live .shots__dots { display: flex; }
/* foot scrim so the dots read on a bright photo */
.shots.is-live::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 4.5rem;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(10,9,8,0) 0%, rgba(10,9,8,0.6) 100%);
}

.shots__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--color-text) 34%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-secondary) 72%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
  transition: background var(--transition), border-color var(--transition),
              color var(--transition), opacity var(--transition);
}
.shots__arrow span { display: block; margin-top: -0.12em; }   /* optical centring */
.shots__arrow--prev { left: var(--space-3); }
.shots__arrow--next { right: var(--space-3); }
.shots__arrow:hover,
.shots__arrow:focus-visible {
  opacity: 1;
  background: color-mix(in srgb, var(--color-secondary) 90%, transparent);
  border-color: var(--color-primary);
  color: var(--color-primary);
}
.shots__dots {
  position: absolute;
  left: 0; right: 0; bottom: var(--space-4);
  z-index: 2;
  justify-content: center;
  gap: var(--space-2);
}
.shots__dot {
  width: 0.4rem;
  height: 0.4rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-text) 45%, transparent);
  cursor: pointer;
  transition: background var(--transition), width var(--transition);
}
.shots__dot.is-current { background: var(--color-primary); width: 1.1rem; }

/* ---- the bio */
.player-card__role {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-primary);
}
.player-card__name {
  margin: var(--space-2) 0 var(--space-5);
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--text-2xl);
  line-height: 1.1;
  color: var(--color-text);
}
.player-card__bio {
  margin: 0;
  font-family: var(--font-heading);   /* the site's editorial reading face */
  font-size: clamp(0.97rem, 0.92rem + 0.24vw, 1.09rem);
  line-height: 1.75;
  color: color-mix(in srgb, var(--color-text) 84%, var(--color-secondary));
  max-width: 60ch;
}

@media (max-width: 779px) {
  /* stacked: cap the picture so it never dominates the card */
  .shots { max-width: 26rem; }
}
/* Touch devices need a real hit target — 2.25rem (36px) is under the ~44px
   minimum, fine with a precise mouse cursor but easy to miss with a thumb. */
@media (hover: none) {
  .shots__arrow, .modal-close { width: 2.75rem; height: 2.75rem; }
}
@media (prefers-reduced-motion: reduce) {
  .shots.is-live .shots__slide { transition: none; }
}

/* ============================================================
   NEWS POSTER CARDS (.post-cards / .post-card)
   One component, used by the home "News" section and the news index — the
   partial is templates/blog/_post_card.html, so the two can never drift.

   The card is a framed poster: cover on top (photo, or a TYPE PLATE when the
   post has no art — record-sleeve treatment rather than a grey placeholder,
   because posts without images are the norm for this client), then date,
   title, teaser and a read affordance. Chrome is hairlines and one brass
   accent; the lift on hover is the only elevation change.
   ============================================================ */
.post-cards {
  display: grid;
  gap: var(--space-8);
  grid-template-columns: 1fr;
}
@media (min-width: 620px)  { .post-cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .post-cards { grid-template-columns: repeat(3, 1fr); } }

/* The home page's "Latest News" passes up to 6 posts (2 full rows at the
   3-column desktop width) — on a single mobile column that's 6 full poster
   cards to scroll past before reaching anything else on the page. Trim the
   count to match how many columns are actually on screen, so it's always
   about one grid "row" deep; "All news" is right there for the rest.
   .post-cards--home only appears on the home section (see home.html) — the
   /news/ index page reuses plain .post-cards--retro for its full archive
   and must never be affected. */
@media (max-width: 619px) {
  .post-cards--home > div:nth-child(n+3) { display: none; }
}
@media (min-width: 620px) and (max-width: 999px) {
  .post-cards--home > div:nth-child(n+5) { display: none; }
}
.post-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  text-decoration: none;
  transition: transform var(--transition-md), border-color var(--transition-md),
              box-shadow var(--transition-md);
}
.post-card:hover,
.post-card:focus-visible {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}

/* ---- cover */
.post-card__cover {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--color-secondary);
}
.post-card__cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.9) contrast(1.04) brightness(0.92);
  transition: transform var(--t-slow) var(--ease-out), filter var(--t-slow) var(--ease-out);
}
.post-card:hover .post-card__cover img {
  transform: scale(1.04);
  filter: saturate(1.02) contrast(1.02) brightness(1);
}
/* the hairline frame INSIDE the cover — the site's gallery motif */
.post-card__wash {
  position: absolute;
  inset: var(--space-3);
  border: 1px solid color-mix(in srgb, var(--color-text) 26%, transparent);
  pointer-events: none;
  transition: border-color var(--transition-md);
}
.post-card:hover .post-card__wash {
  border-color: color-mix(in srgb, var(--color-primary) 60%, transparent);
}
.post-card__tag {
  position: absolute;
  left: var(--space-6);
  top: var(--space-6);
  z-index: 2;
  padding: 0.35em 0.75em;
  background: color-mix(in srgb, var(--color-secondary) 76%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-primary) 34%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  font-family: var(--font-body);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-primary);
}

/* ---- the no-photo cover: a Blue Note-ish type plate */
.post-card__plate {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;      /* composed poster, not text stranded at the foot */
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-12) var(--space-6);
  background:
    radial-gradient(110% 70% at 22% 12%, rgba(194,155,82,0.22) 0%, rgba(194,155,82,0) 60%),
    linear-gradient(158deg, #1B1712 0%, #0A0908 72%);
}
.post-card__plate-label {
  font-family: var(--font-body);
  font-size: 0.5625rem;
  font-weight: 600;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--color-text-light);
}
.post-card__plate-title {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: clamp(1.5rem, 1.1rem + 1.3vw, 2.1rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  color: var(--color-text);
  text-wrap: balance;
}
.post-card__plate-rule {
  width: 3.5rem;
  height: 1px;
  background: var(--color-primary);
  transition: width var(--transition-md);
}
.post-card:hover .post-card__plate-rule { width: 6rem; }

/* ---- body */
.post-card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-6) var(--space-8);
}
.post-card__date {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-light);
}
.post-card__title {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--text-xl);
  line-height: 1.2;
  color: var(--color-text);
  text-wrap: balance;
  transition: color var(--transition);
}
.post-card:hover .post-card__title { color: var(--color-primary); }
.post-card__excerpt {
  margin: 0;
  font-family: var(--font-heading);   /* same editorial serif as the about copy */
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--color-text-muted);
  /* three lines of teaser, whatever the copy length — keeps a tidy card grid */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.post-card__more {
  margin-top: auto;
  padding-top: var(--space-2);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-primary);
}
.post-card__more span {
  display: inline-block;
  transition: transform var(--transition);
}
.post-card:hover .post-card__more span { transform: translateX(4px); }

/* On a plate card the TITLE already sits on the cover, so the body heading
   would repeat it verbatim. Keep the <h3> for structure/screen readers and
   hide it visually — never delete it, the card would lose its heading. */
.post-card--plate .post-card__title {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .post-card,
  .post-card__cover img,
  .post-card__plate-rule,
  .post-card__more span { transition: none; }
  .post-card:hover { transform: none; }
  .post-card:hover .post-card__cover img { transform: none; }
}

/* ============================================================
   NEWS — RETRO BLUE BAND (home section + the /news/ index page share
   this theme, so the two can never drift)
   A deliberate break from the night-gallery black: one loud field in the
   client's chosen retro blue (#1d2ade) with cream poster cards — the
   site's one "pop" moment, boxed off by the black either side. Chunky
   offset shadows (no blur) + zero radius = screen-printed concert-flyer,
   not soft SaaS card. Photos run in a blue duotone until hover, when they
   snap to full colour — the site's duotone motif turned into a reveal
   micro-interaction instead of a static filter.
   ============================================================ */
.news-retro {
  --nr-blue: #1D2ADE;
  --nr-blue-deep: color-mix(in srgb, var(--nr-blue) 62%, black);
  --nr-blue-bright: color-mix(in srgb, var(--nr-blue) 68%, white);
  --nr-cream: #F4ECDC;
  /* one gap, reused above AND below the heading, so the field always reads
     as the text sitting centered in its own even box of air. */
  --nr-head-gap: clamp(2.5rem, 2rem + 2.5vw, 4rem);
  position: relative;
  background: radial-gradient(120% 140% at 12% -10%, var(--nr-blue-bright) 0%, var(--nr-blue) 45%, var(--nr-blue-deep) 100%);
}
/* less air above the heading than the rest of the site's section rhythm —
   this band announces itself immediately rather than easing in. Two cases:
   the home section IS both classes on one element (section-x's own
   padding-top wins here); the /news/ pages wrap a .page-head child instead,
   so that child's padding-top is reduced in the same rule below. Never let
   both apply to the same element — that would stack, not replace. */
.section-x.news-retro { padding-top: var(--nr-head-gap); }
.news-retro .page-head { padding-top: var(--nr-head-gap); }
/* The /news/ list page's page-head is a bare header (kicker+heading only) —
   its own bottom padding would stack with the heading's margin-bottom right
   below it (already --nr-head-gap), silently growing the bottom gap by an
   extra --space-12. Zero it there. (Not applied to the article page's
   page-head, which wraps the WHOLE article and needs its own bottom air.) */
.news-retro .page-head:has(+ .section-x--flush) { padding-bottom: 0; }

.news-retro__head { text-align: center; }
.news-retro__heading {
  margin: 0 0 var(--nr-head-gap);
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: clamp(2.5rem, 1.9rem + 3vw, 4.25rem);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--nr-cream);
}
.news-retro__kicker {
  display: inline-block;
  margin: 0 0 var(--space-4);
  padding: 0.3em 0.9em;
  background: var(--nr-blue-deep);
  color: var(--nr-cream);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  transform: rotate(-3deg);
}
.news-retro .empty-note {
  color: color-mix(in srgb, var(--nr-cream) 75%, transparent);
}
.news-retro__end {
  margin-top: var(--space-8);
  font-size: var(--text-sm);
  color: color-mix(in srgb, var(--nr-cream) 70%, transparent);
}
.news-retro__end a {
  color: var(--nr-cream);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* ---- individual article pages (blog/detail.html), same field + poster-frame
   language as the cards: cream/blue text swap for the copy that sits
   directly on the blue (not inside a cream card), and the featured image
   gets the same chunky frame + duotone-to-colour hover as the cards. */
.news-retro .article__meta { color: color-mix(in srgb, var(--nr-cream) 65%, transparent); }
.news-retro .article h1,
.news-retro .article__standfirst { color: var(--nr-cream); }
.news-retro .article__body { color: color-mix(in srgb, var(--nr-cream) 88%, transparent); }
/* The retro theme is cream/blue only — no brass — so the tag chips swap out
   of .tag-chip's default brass to match rather than clash with it. */
.news-retro .tag-chip {
  border-color: color-mix(in srgb, var(--nr-cream) 45%, transparent);
  color: var(--nr-cream);
}
.duo--retro {
  border: 3px solid var(--nr-cream);
  border-radius: 0;
  box-shadow: 8px 8px 0 0 var(--nr-blue-deep);
  background: var(--nr-blue-deep);
}
.duo--retro img {
  filter: grayscale(1) sepia(1) hue-rotate(175deg) saturate(2.4) brightness(0.95) contrast(1.05);
  transition: filter var(--t-slow) var(--ease-out), transform var(--t-slow) var(--ease-out);
}
/* The article page's own figure is the one full-size photo on the page, not
   a small thumbnail in a grid — it reads as a real photograph, not a poster
   card, so it shows full color straight away rather than behind a hover. */
.article__figure.duo--retro img { filter: none; }
.duo--retro:hover img,
.duo--retro:focus-within img {
  filter: none;
  transform: scale(1.03);
}

.post-cards--retro { position: relative; }
.post-cards--retro .post-card {
  --nr-rot: 0deg;
  background: var(--nr-cream);
  border: 3px solid var(--nr-blue-deep);
  border-radius: 0;
  box-shadow: 8px 8px 0 0 var(--nr-blue-deep);
  transform: rotate(var(--nr-rot));
  transition: transform var(--transition-md), box-shadow var(--transition-md), border-color var(--transition-md);
}
.post-cards--retro .post-card:nth-child(3n+2) { --nr-rot: -1.25deg; }
.post-cards--retro .post-card:nth-child(3n) { --nr-rot: 1.25deg; }
.post-cards--retro .post-card:hover,
.post-cards--retro .post-card:focus-visible {
  transform: rotate(0deg) translate(-4px, -6px);
  box-shadow: 12px 14px 0 0 var(--nr-blue-bright);
  border-color: var(--nr-blue-bright);
}

.post-cards--retro .post-card__cover img {
  filter: grayscale(1) sepia(1) hue-rotate(175deg) saturate(2.4) brightness(0.95) contrast(1.05);
}
.post-cards--retro .post-card:hover .post-card__cover img {
  filter: none;
  transform: scale(1.06);
}
.post-cards--retro .post-card__wash {
  border-color: color-mix(in srgb, var(--nr-cream) 35%, transparent);
}
.post-cards--retro .post-card:hover .post-card__wash {
  border-color: color-mix(in srgb, var(--nr-cream) 70%, transparent);
}
.post-cards--retro .post-card__tag {
  background: var(--nr-blue-deep);
  border: none;
  color: var(--nr-cream);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  font-weight: 700;
  transform: rotate(-3deg);
  transition: transform var(--transition-md), background var(--transition-md);
}
.post-cards--retro .post-card:hover .post-card__tag {
  transform: rotate(0deg);
  background: var(--nr-blue-bright);
}
.post-cards--retro .post-card__date { color: color-mix(in srgb, var(--nr-blue-deep) 60%, transparent); }
.post-cards--retro .post-card__title { color: var(--nr-blue-deep); }
.post-cards--retro .post-card:hover .post-card__title { color: var(--nr-blue-bright); }
.post-cards--retro .post-card__excerpt { color: color-mix(in srgb, var(--nr-blue-deep) 78%, transparent); }

/* The blue-duotone-to-colour reveal above is a HOVER micro-interaction — on a
   touch device there's no hover, so the photo would stay stuck in the blue
   filter forever. Devices with no hover capability skip the duotone and show
   the photo in full colour outright instead of a reveal that can never fire. */
@media (hover: none) {
  .post-cards--retro .post-card__cover img { filter: none; }
}
.post-cards--retro .post-card__more { color: var(--nr-blue-deep); }
.post-cards--retro .post-card:hover .post-card__more { color: var(--nr-blue-bright); }
.post-cards--retro .post-card__plate { background: linear-gradient(158deg, var(--nr-blue) 0%, var(--nr-blue-deep) 100%); }
.post-cards--retro .post-card__plate-label { color: color-mix(in srgb, var(--nr-cream) 70%, transparent); }
.post-cards--retro .post-card__plate-title { color: var(--nr-cream); }
.post-cards--retro .post-card__plate-rule { background: var(--nr-cream); }

.btn--retro {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  background: var(--nr-cream);
  color: var(--nr-blue-deep);
  border: 3px solid var(--nr-blue-deep);
  border-radius: 0;
  padding: 0.85em 1.75em;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  box-shadow: 6px 6px 0 0 var(--nr-blue-deep);
  transition: transform var(--transition-md), box-shadow var(--transition-md),
              background var(--transition-md), color var(--transition-md);
}
.btn--retro:hover,
.btn--retro:focus-visible {
  transform: translate(-3px, -3px);
  box-shadow: 9px 9px 0 0 var(--nr-blue-bright);
  background: var(--nr-blue-bright);
  color: var(--nr-cream);
  border-color: var(--nr-blue-bright);
  text-decoration: none;
}

@media (prefers-reduced-motion: reduce) {
  .post-cards--retro .post-card,
  .post-cards--retro .post-card__cover img,
  .post-cards--retro .post-card__tag,
  .btn--retro { transition: none; }
  .post-cards--retro .post-card:hover,
  .post-cards--retro .post-card:focus-visible { transform: rotate(var(--nr-rot)); }
  .btn--retro:hover { transform: none; }
  .duo--retro img { transition: none; }
  .duo--retro:hover img, .duo--retro:focus-within img { transform: none; }
}

/* Article body */
.article {
  max-width: var(--max-width-prose);
  margin-inline: auto;
}
.article__meta {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-light);
  margin-bottom: var(--space-4);
}
.article h1 {
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: var(--text-3xl);
  margin: 0 0 var(--space-8);
  text-wrap: balance;
}
/* Standfirst — the card's teaser, repeated as the story's opening promise.
   Serif, one step up from the body, so the article opens like the card closed. */
.article__standfirst {
  margin: var(--space-6) 0 var(--space-8);
  font-family: var(--font-heading);
  font-size: clamp(1.1rem, 1rem + 0.5vw, 1.35rem);
  font-weight: 400;
  line-height: 1.6;
  color: var(--color-text);
  max-width: 46ch;
}
.article__body { color: var(--color-text-muted); font-size: var(--text-lg); }
.article__body p { margin: 0 0 var(--space-6); }
.article__figure { margin-bottom: var(--space-8); }
.article__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-2) 0 var(--space-10);
  padding: 0;
}
.tag-chip {
  padding: 0.35em 0.9em;
  border: 1px solid color-mix(in srgb, var(--color-primary) 45%, transparent);
  border-radius: 999px;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-primary);
}

/* ---------- Interior page head ---------- */
.page-head { padding: var(--space-24) 0 var(--space-12); }
.page-head .ex-header { margin-bottom: 0; }
/* Content section directly after a page-head: no double stack */
.section-x--flush { padding-top: 0; }
/* Follows straight on from a section above with its own bottom padding
   already — halves the top gap so the two don't stack into a dead zone
   (Booking's FAQs, right under the form). */
.section-x--tight-top { padding-top: var(--section-y-s1); }

/* ---------- Django messages (night-gallery treatment, not the
   master-template light-mode defaults) ---------- */
.message {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--text-sm);
}
.message--success { border-left-color: var(--color-primary); }
.message--success::before { content: "✓  "; color: var(--color-primary); }
.message--error, .message--danger { border-left-color: var(--color-error); }
.message--warning { border-left-color: var(--color-warning); }
.message--info { border-left-color: var(--color-info); }

/* ---------- Forms on the dark ground ---------- */
.form-control {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
}
.form-control::placeholder { color: var(--color-text-light); }
.form-control:focus {
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
  background: var(--color-surface);
  color: var(--color-text);
}
.form-note { font-size: var(--text-xs); color: var(--color-text-light); }
label { color: var(--color-text-muted); font-size: var(--text-sm); }

/* ---------- Closing CTA band — ivory booking room (inverse section) ----------
   2026-07-27: swapped to the site's one light section (was .viewing-room,
   see note above) so Booking reads as the "gallery lobby" against the dark
   Music room before it. */
.cta-x {
  position: relative;
  background: var(--color-inverse-bg);
  color: var(--color-inverse-text);
  border-top: 1px solid var(--color-border);
  padding-block: var(--section-y);
  text-align: center;
  overflow: hidden;
}
.cta-x .ex-kicker { justify-content: center; color: color-mix(in srgb, var(--color-inverse-text) 72%, var(--color-inverse-bg)); }
.cta-x .ex-kicker::after { display: none; }
.cta-x h2 {
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: var(--text-3xl);
  margin: 0 0 var(--space-4);
  text-wrap: balance;
  color: var(--color-inverse-text);
}
.cta-x p {
  color: color-mix(in srgb, var(--color-inverse-text) 72%, var(--color-inverse-bg));
  max-width: var(--max-width-prose);
  margin: 0 auto var(--space-8);
}
.cta-x .hero-x__reassure {
  margin-top: var(--space-6);
  color: color-mix(in srgb, var(--color-inverse-text) 60%, var(--color-inverse-bg));
  text-shadow: none;
}

/* Line-art quartet, filling the booking room as a quiet backdrop — a hand-drawn
   touch so the ivory room doesn't read as an empty form landing. Spans the
   full section (object-fit: contain keeps the drawing's own proportions, so
   it never stretches or crops) at low opacity so the CTA button still wins
   the eye over it. */
.cta-x__art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0.14;
  pointer-events: none;
  z-index: 0;
}
.cta-x .container { position: relative; z-index: 1; }
@media (max-width: 700px) {
  .cta-x__art { opacity: 0.1; }
}

/* ---------- Ivory room — the all-white page style, huge line-art backdrop ----
   Same booking-lineart.png as the homepage's ivory CTA room (.cta-x__art), but
   blown up to fill the whole page rather than sit inside one section — the
   entire page is the "ivory room" here. Text/form colors mirror the .cta-x
   inverse-token pattern above so this reuses the site's one light palette
   instead of inventing new colors. Shared by Contact and Booking (2026-07-28) —
   both are single-column form pages, so they carry the exact same treatment. */
body.ivory-page { background: var(--color-inverse-bg); }
.ivory-page .page-head,
.ivory-page .section-x { position: relative; z-index: 1; }
/* Every .ex-title on the page needs the dark-on-ivory color (default .ex-title
   is styled for the site's dark ground and renders near-invisible here) — but
   only the page's own h1 gets the huge treatment. Booking's FAQ section also
   has an .ex-title (its "Frequently Asked Questions" h2), which must stay
   sub-heading scale, so the size bump is scoped to .page-head, not the color. */
.ivory-page .ex-title { color: var(--color-inverse-text); }
.ivory-page .page-head .ex-title {
  font-size: clamp(3rem, 2rem + 5vw, 6rem);
}
.ivory-page .form-control {
  background: color-mix(in srgb, var(--color-inverse-bg) 92%, var(--color-inverse-text));
  border-color: color-mix(in srgb, var(--color-inverse-text) 22%, var(--color-inverse-bg));
  color: var(--color-inverse-text);
}
.ivory-page .form-control::placeholder { color: color-mix(in srgb, var(--color-inverse-text) 45%, var(--color-inverse-bg)); }
.ivory-page .form-control:focus {
  background: color-mix(in srgb, var(--color-inverse-bg) 92%, var(--color-inverse-text));
  color: var(--color-inverse-text);
}
.ivory-page label { color: color-mix(in srgb, var(--color-inverse-text) 70%, var(--color-inverse-bg)); }
.ivory-page .main-content p { color: var(--color-inverse-text); }
/* Extra class beyond .main-content p's own specificity, so this wins over the
   general paragraph rule above instead of losing to it. */
.ivory-page .main-content .form-note { color: color-mix(in srgb, var(--color-inverse-text) 55%, var(--color-inverse-bg)); }
/* Nav has no dark ground to sit on at the top of this page (it only grounds
   after a little scroll) — force it on so the ivory-on-ivory links stay legible. */
.ivory-page .nav::before { opacity: 1; }

.ivory-page__art {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.06;
  pointer-events: none;
  z-index: 0;
}

/* Booking's FAQ accordion sits on the ivory ground too — .faq's default
   colors are tuned for the dark ground (see its own rule block), so they need
   light-ground counterparts here rather than a second component. */
.ivory-page .faq details { border-bottom-color: color-mix(in srgb, var(--color-inverse-text) 18%, var(--color-inverse-bg)); }
.ivory-page .faq summary { color: var(--color-inverse-text); }
.ivory-page .faq-a { color: color-mix(in srgb, var(--color-inverse-text) 60%, var(--color-inverse-bg)); }
/* .btn--ghost is hardcoded for the dark ground (see its own rule) — on this
   light ground the same colour is near-invisible outline-on-cream (404/500
   pages' "Back to home"). */
.ivory-page .btn--ghost { color: var(--color-inverse-text); border-color: color-mix(in srgb, var(--color-inverse-text) 40%, transparent); }
.ivory-page .btn--ghost:hover { border-color: var(--color-inverse-text); background: color-mix(in srgb, var(--color-inverse-text) 8%, transparent); }

.center-col { margin-inline: auto; }

/* ---------- Custom date picker (replaces the native date-input popup —
   OS calendar chrome can't be restyled at all) ----------
   Progressive enhancement, see date-picker.js: the real <input type="date">
   stays in the DOM (hidden) as the thing that actually submits; the trigger
   button reuses .form-control so it inherits the page's own light/dark
   form-field theming (e.g. .ivory-page .form-control) for free. The dropdown
   panel is a dark popover regardless of page background — same move as the
   gallery lightbox — so it reads as a floating card over either theme. */
.date-picker { position: relative; }
.date-picker__native {
  position: absolute; opacity: 0; width: 1px; height: 1px;
  overflow: hidden; pointer-events: none;
}
.date-picker__trigger {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); width: 100%; text-align: left; cursor: pointer;
  /* No color set here — .form-control already picks the right text colour
     per page (e.g. dark-on-ivory via .ivory-page .form-control). Setting
     one directly here previously hardcoded the dark-ground colour
     (var(--color-text), light/ivory), which read as invisible white text
     once a date was selected on a light-background page. Placeholder vs.
     filled is distinguished with opacity instead, same trick a real
     placeholder uses, so it stays legible on whichever ground it's on. */
}
.date-picker__value { opacity: 0.65; }
.date-picker__trigger.has-value .date-picker__value { opacity: 1; }
.date-picker__icon { display: flex; flex: 0 0 auto; opacity: 0.7; }
.date-picker__trigger:hover .date-picker__icon,
.date-picker__trigger[aria-expanded="true"] .date-picker__icon { opacity: 1; color: var(--color-primary); }

.date-picker__panel {
  position: absolute; top: calc(100% + var(--space-2)); left: 0; z-index: 40;
  width: min(19rem, 90vw);
  background: var(--color-secondary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  padding: var(--space-4);
}
.date-picker__head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: var(--space-3);
}
.date-picker__month {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: var(--text-lg);
  color: var(--color-text);
}
.date-picker__nav {
  width: 1.75rem; height: 1.75rem;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: none;
  color: var(--color-text-muted);
  font-size: 1rem; line-height: 1; cursor: pointer;
  transition: color var(--transition), border-color var(--transition);
}
.date-picker__nav:hover {
  color: var(--color-primary);
  border-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
}
.date-picker__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.date-picker__dow {
  text-align: center;
  padding-block: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-light);
}
.date-picker__day {
  aspect-ratio: 1;
  border: 0; border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.date-picker__day:hover { background: var(--color-surface); color: var(--color-primary); }
.date-picker__day.is-muted { color: var(--color-text-light); opacity: 0.4; cursor: default; }
.date-picker__day.is-muted:hover { background: none; color: var(--color-text-light); }
.date-picker__day.is-today { box-shadow: inset 0 0 0 1px var(--color-primary); }
.date-picker__day.is-selected { background: var(--color-primary); color: var(--color-inverse-text); font-weight: 600; }
.date-picker__day.is-selected:hover { background: var(--color-primary); color: var(--color-inverse-text); }

/* ---------- Footer ---------- */
.footer {
  background: var(--color-secondary);
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-16);
}
.footer__grid {
  display: grid;
  gap: var(--space-12);
  grid-template-columns: 1fr;
}
@media (min-width: 768px) {
  .footer__grid { grid-template-columns: 2fr 1fr; }
}
.footer__nav { display: flex; gap: clamp(var(--space-8), 6vw, var(--space-16)); flex-wrap: wrap; }
.footer__tagline { color: var(--color-text-muted); font-size: var(--text-sm); max-width: 34ch; }
.footer__place {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-light);
}
.footer__links { list-style: none; padding: 0; margin: 0; min-width: 0; }
.footer__links li { margin-bottom: var(--space-3); }
.footer__links a {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.footer__links a:hover { color: var(--color-primary); }
.footer__bottom {
  margin-top: var(--space-16);
  padding: var(--space-6) 0;
  border-top: 1px solid var(--color-border);
  font-size: var(--text-xs);
  color: var(--color-text-light);
}

/* ---------- FAQ (no-JS accordion, dark) ---------- */
.faq details { border-bottom: 1px solid var(--color-border); }
.faq summary {
  cursor: pointer;
  padding: var(--space-4) 0;
  font-weight: 500;
  color: var(--color-text);
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-4);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; color: var(--color-primary); font-size: var(--text-xl); line-height: 1; }
.faq details[open] summary::after { content: "–"; }
.faq-a { padding: 0 0 var(--space-4); color: var(--color-text-muted); }

/* ---------- Pending-content plate (empty states with a floor) ---------- */
.plate {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  padding: var(--space-16) var(--space-8);
  text-align: center;
  grid-column: 1 / -1;   /* a pending plate owns the whole wall */
}
.plate__glyph {
  display: block;
  font-family: var(--font-heading);
  font-size: var(--text-3xl);
  color: var(--color-primary);
  line-height: 1;
  margin-bottom: var(--space-4);
}
.plate p { color: var(--color-text-muted); max-width: 44ch; margin: 0 auto; }

/* Left-aligned prose column (forms share the page's left edge) */
.prose-col { max-width: var(--max-width-prose); }

/* ---------- Utilities ---------- */
.container--prose { max-width: var(--max-width-prose); margin-inline: auto; padding-inline: var(--container-pad); }
.center { text-align: center; }
.center .ex-kicker { justify-content: center; }
.center .ex-kicker::after { display: none; }
.center .ex-title, .center .ex-lede { margin-inline: auto; }
.mt-8 { margin-top: var(--space-8); }
.section-foot { margin-top: var(--space-12); }
.section-foot .btn + .btn { margin-left: var(--space-3); }
