/* ===================================================================
   Crosstown Social — shared site stylesheet (Retro Soul theme)
   Used by index (crosstown-social-landing.html) and all subpages.
   =================================================================== */

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

/* ============ SCROLL SNAPPING (slide sequence) ============ */
/* Native CSS scroll-snap rather than a JS scroll handler, deliberately: the
   browser owns the wheel/trackpad/touch momentum curves, so a phone drag and
   a desktop flick both feel right without reimplementing inertia. Every JS
   version of this fights the platform's own scrolling and reads as sticky on
   iOS in particular.
   overflow-x lives HERE now, not on body. It was on body, and body overflow
   propagates to the viewport, which in several browsers stops
   scroll-snap-type on the root from taking effect at all. Moving it up keeps
   the horizontal clamp and leaves html unambiguously the snap container.
   The baseline is NONE, not proximity. site.js adds .snap-slides (mandatory)
   only while the viewport is inside the slide sequence, and there is
   deliberately no snapping whatsoever once you're past the last slide - the
   testimonial, enquiry form and footer are ordinary page content and should
   scroll like it.
   Don't be tempted to use `proximity` anywhere here. Proximity only pulls
   when a gesture happens to end near a snap point and leaves a dead band in
   the middle where the page just sits between two slides - which is the exact
   symptom this was meant to prevent. It is mandatory or nothing.
   2026-07-26: .snap-slides is only applied on TOUCH devices now. Mouse and
   trackpad get no native snapping at all - Chrome jumps a whole slide per
   wheel notch under mandatory, which was too abrupt - and use a JS
   completion nudge instead. See the SCROLL SNAPPING block in site.js; the
   reasoning and the two rejected desktop approaches are documented there. */
html {
  /* auto, NOT smooth. site.js tweens every programmatic scroll itself (arrow
     links, nav anchors, the desktop completion nudge) because the native
     smooth duration is browser-chosen, short, and not adjustable by any CSS
     property or API - it read as jerky. Setting smooth here would make the
     browser animate on top of the tween, and the two curves fight.
     See the SMOOTH SCROLL TWEEN block in site.js. */
  scroll-behavior:auto;
  overflow-x:hidden;
  scroll-snap-type:none;
}
html.snap-slides { scroll-snap-type:y mandatory; }
/* Only the full-viewport panels are snap targets. Nothing below them is, so
   the testimonial, enquiry form and footer scroll completely normally.
   scroll-snap-stop:always (2026-07-26) forbids a fling from skipping OVER a
   snap point, so one swipe advances at most one slide however hard it's
   thrown. Callum found it too easy to fly past a whole slide on touch.
   This is the native way to get a one-slide-per-gesture lock, and it feels
   right because the browser reconciles it with its own momentum curve - a
   JS equivalent has to cancel that momentum and reads as the page fighting
   you. It only has any effect where scroll-snap-type is active, which in
   this setup is touch devices; desktop has no native snapping. */
.hero, #listen, #about, #packages { scroll-snap-align:start; scroll-snap-stop:always; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration:0.01ms !important; animation-iteration-count:1 !important; transition-duration:0.01ms !important; }
  .gallery-track { animation-duration:110s !important; animation-iteration-count:infinite !important; }
  /* Snapping itself stays mandatory; only motion is reduced. The smooth
     scroll is handled in site.js now (tweenScrollTo returns instantly under
     this preference), so there's nothing left to disable here - the
     scroll-behavior line below is kept as a belt-and-braces default.
     2026-07-26: this block used to also downgrade snapping to proximity, and
     that was a real bug rather than a nicety - Windows reports
     prefers-reduced-motion:reduce whenever "Animation effects" is switched
     off in Accessibility settings, which is a common and completely
     unremarkable thing for someone to have done. Those users silently got
     proximity, i.e. the dead band in the middle where the page parks between
     two slides, and it looked like the snapping was simply broken.
     What this preference asks for is no gratuitous ANIMATION, not a
     different layout behaviour. With scroll-behavior:auto the snap lands
     instantly instead of gliding, which satisfies it properly. */
  html { scroll-behavior:auto; }
}

body {
  background:var(--bg); color:var(--fg);
  font-family:var(--font-body);
  transition:background .4s ease, color .4s ease;
  /* overflow-x moved to html - see the scroll-snap block above. */
  --bg:#f6ead8; --bg2:#fdf6e9; --bg3:#efd9b8;
  --fg:#3a1c17; --muted:#7a5142;
  --accent:#6b1f2a; --accent2:#d9931e;
  --btn-fg:#f6ead8;
  --font-display:'Abril Fatface',serif; --font-body:'Space Grotesk',sans-serif;
  --radius-btn:999px; --radius-card:16px;
  --tt-display:none; --ls-display:0em; --fw-display:400;
  --card-bg:#fdf6e9; --card-border:#6b1f2a;
  /* Parallax overscan, as a fraction - how far .parallax-img overhangs its
     section on each side. This is the hard ceiling on parallax travel:
     site.js's PARALLAX_TRAVEL_PCT (0.20) must stay below it. The two are one
     setting split across two files, and them drifting apart is precisely
     what caused the original grey-bar bug, so change them together. */
  --parallax-inset:0.25;
}
img { display:block; max-width:100%; }
a { color:inherit; text-decoration:none; }
:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }

/* ============ ACCESSIBILITY: skip link ============ */
.skip-link {
  position:absolute; top:-100px; left:12px; z-index:1000;
  background:var(--fg); color:var(--bg); padding:12px 20px; border-radius:8px;
  font-size:13px; font-weight:700; letter-spacing:.04em; transition:top .15s ease;
}
.skip-link:focus { top:12px; }

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

/* ============ ONE-PAGE ANCHOR NAV ============ */
/* The nav is fixed, so a plain #anchor jump would land with the section
   title hidden underneath it - scroll-margin-top pushes the landing point
   down by roughly the nav's height (using the larger unscrolled height as a
   safe upper bound) so the heading lands clear.
   2026-07-26: this used to cover #about/#listen/#packages too, and had to be
   removed from them when scroll snapping went in. scroll-margin also offsets
   the SNAP position, so an 88px margin made every slide snap 88px down its
   own start - leaving a permanent 88px strip of the previous slide visible
   at the top of the screen at every rest position. The slides don't need the
   offset anyway: their headings are vertically centred, not near the top,
   and the nav is deliberately transparent over them. #contact is a normal
   content section with a heading close to its top edge, so it keeps it. */
#contact { scroll-margin-top:88px; }

/* ============ NAV ============ */
.nav {
  position:fixed; top:0; left:0; right:0; z-index:100;
  display:flex; align-items:center; justify-content:space-between;
  padding:22px clamp(20px,4vw,48px);
  transition:background .3s ease, padding .3s ease, backdrop-filter .3s ease, border-color .3s ease;
  border-bottom:1px solid transparent;
  /* At the very top the nav sits directly over a dark video/photo, so its own
     text needs to be light regardless of the page's normal (light-bg) theme.
     Once scrolled, the nav gets a solid cream backing, so text switches back
     to the normal dark theme colours for contrast against that. */
  --nav-fg: var(--bg);
  --nav-accent: var(--accent2);
}
.nav.scrolled {
  background:var(--bg); background:color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter:blur(10px); padding:14px clamp(20px,4vw,48px); border-color:var(--card-border);
  --nav-fg: var(--fg);
  --nav-accent: var(--accent);
}
.logo { font-family:var(--font-display); font-weight:var(--fw-display); font-size:19px; letter-spacing:.03em; text-transform:var(--tt-display); color:var(--nav-fg); }
.logo em { font-style:normal; color:var(--nav-accent); }
.nav-links { display:flex; gap:30px; align-items:center; }
/* :not(.nav-cta) on both rules below, fixed 2026-07-27: the Enquire pill
   lives inside .nav-links markup-wise (so the mobile-menu/nav-links styling
   doesn't have to be duplicated), which meant it was quietly picking up the
   plain text links' opacity:.85 - and would have picked up their hover
   colour swap to --nav-accent too, fighting its own --btn-fg. At the top of
   the page that 15% see-through let the busy hero photo bleed through the
   burgundy fill unevenly, reading as a dirty gradient instead of a clean
   solid chip - barely visible once scrolled, because a cream backdrop
   bleeding through cream-adjacent burgundy is a much smaller shift. The CTA
   already declares its own solid background/color below; it was never
   supposed to inherit either rule. */
.nav-links a:not(.nav-cta) { font-size:12.5px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; opacity:.85; color:var(--nav-fg); }
.nav-links a.current:not(.nav-cta), .nav-links a:hover:not(.nav-cta) { opacity:1; color:var(--nav-accent); }
.nav-cta { background:var(--accent); color:var(--btn-fg); padding:11px 20px; border-radius:var(--radius-btn); font-size:12px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
.nav-toggle { display:none; width:26px; height:20px; position:relative; cursor:pointer; background:none; border:none; }
.nav-toggle span, .nav-toggle span::before, .nav-toggle span::after { content:''; position:absolute; left:0; width:100%; height:2px; background:var(--nav-fg); transition:transform .2s ease, opacity .2s ease, background .2s ease; }
.nav-toggle span { top:9px; }
.nav-toggle span::before { top:-8px; }
.nav-toggle span::after { top:8px; }
body.menu-open .nav-toggle span { background:transparent; }
body.menu-open .nav-toggle span::before { transform:translateY(8px) rotate(45deg); background:var(--fg); }
body.menu-open .nav-toggle span::after { transform:translateY(-8px) rotate(-45deg); background:var(--fg); }

/* Bug fixed 2026-07-27: opening the mobile menu at the very top of the page
   (nav not yet .scrolled) left "Crosstown" unreadable - cream text (--nav-fg
   defaults to --bg for contrast against the dark hero) sitting on top of the
   mobile-menu panel's own cream background (also --bg). "Social" stayed
   visible by coincidence, since --nav-accent is mustard at the top and that
   reads fine on cream too. The nav bar itself doesn't get a background here
   (that's the whole point of it being transparent over the hero) - the fix
   is switching its text colour variables to the dark/scrolled pair whenever
   the menu is open, regardless of actual scroll position, since the menu
   panel behind it is always cream. Scrolled-down + menu-open already worked,
   because .scrolled's dark colours were already active. */
body.menu-open .nav {
  --nav-fg: var(--fg);
  --nav-accent: var(--accent);
}

.mobile-menu {
  position:fixed; inset:0; z-index:90; background:var(--bg);
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:26px;
  transform:translateY(-100%); transition:transform .4s ease, visibility 0s linear .4s;
  visibility:hidden; /* keeps its links out of the tab order while closed, not just out of view */
}
body.menu-open .mobile-menu { transform:translateY(0); visibility:visible; transition:transform .4s ease; }
.mobile-menu a { font-family:var(--font-display); font-size:26px; text-transform:var(--tt-display); }
.mobile-menu .nav-cta { font-family:var(--font-body); }

@media (max-width:820px) {
  .nav-links { display:none; }
  .nav-toggle { display:block; }
}

/* ============ HERO (landing page, video) ============ */
.hero { position:relative; height:100svh; min-height:580px; display:flex; align-items:flex-end; overflow:hidden; }
.hero video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0; }
/* 2026-07-26: was a 3-stop gradient (.2 -> .5 -> .94, rgba(6,6,8)) - a
   different shape and tone from the 2-stop scrim now used uniformly on
   Listen/About/Packages (.parallax-overlay / .player-vignette, rgba(4,4,6)
   .4 -> .72). Matched here too so the hero->Listen handoff doesn't look
   like a different treatment from the other slide-to-slide transitions. */
/* 2026-07-26 (later): bottom stop moved .72 -> .66 to match the first stop
   of .parallax-overlay / .player-vignette, so the hero->Listen seam has no
   brightness step either. Top stop stays .4 - nothing sits above the hero,
   so it doesn't need to match anything, and the lighter top keeps the video
   readable behind the nav. */
.hero::after {
  content:''; position:absolute; inset:0; z-index:1;
  background:linear-gradient(180deg, rgba(4,4,6,.4), rgba(4,4,6,.66));
}
.hero-content { position:relative; z-index:2; padding:0 clamp(20px,4vw,64px) clamp(56px,9vh,100px); max-width:920px; text-align:left; }
.eyebrow { color:var(--accent2); font-family:var(--font-body); font-size:12px; letter-spacing:.2em; text-transform:uppercase; margin-bottom:20px; font-weight:700; }
/* Hero eyebrow, 2026-07-26: was a single .eyebrow line reading
   "Live wedding & event band · Sydney to South Coast". Split into two lines
   with the offer bigger and the region stacked beneath it, so the first
   thing read is what the band IS rather than a run-on with a middot in it.
   Deliberately still smaller than .hero-title - it's a supporting line, and
   competing with the band name would flatten the hierarchy.
   The region line is white rather than mustard: two mustard lines of
   different sizes read as one broken heading, whereas the colour change
   makes the subordination obvious at a glance. */
.hero-eyebrow { margin-bottom:22px; }
.hero-eyebrow-main {
  display:block; color:var(--accent2); font-family:var(--font-body);
  font-size:clamp(18px,2.8vw,26px); font-weight:700;
  letter-spacing:.16em; text-transform:uppercase; line-height:1.25;
}
.hero-eyebrow-sub {
  display:block; margin-top:9px; color:rgba(255,255,255,.82);
  font-family:var(--font-body); font-size:clamp(11px,1.25vw,13px); font-weight:600;
  letter-spacing:.24em; text-transform:uppercase; line-height:1.3;
}
.hero-title { font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(54px,11vw,150px); line-height:.9; color:#fdfcf9; text-transform:var(--tt-display); letter-spacing:var(--ls-display); }
.hero-title span { display:block; }
.hero-tagline { font-family:var(--font-display); font-style:italic; font-size:clamp(18px,2.6vw,28px); color:var(--accent2); margin-top:18px; }
.hero-sub { margin-top:24px; max-width:520px; font-size:15.5px; line-height:1.7; color:rgba(255,255,255,.85); }
.hero-actions { margin-top:34px; display:flex; gap:14px; flex-wrap:wrap; }
.btn-primary, .btn-ghost { display:inline-block; padding:15px 28px; border-radius:var(--radius-btn); font-weight:700; font-size:13px; letter-spacing:.08em; text-transform:uppercase; cursor:pointer; border:none; font-family:var(--font-body); }
.btn-primary { background:var(--accent); color:var(--btn-fg); }
.btn-ghost { border:1.5px solid rgba(255,255,255,.55); color:#fff; background:transparent; }
/* Scroll cue: an <a> to the next section, not a decorative div. Anchor
   rather than a button because it works with the keyboard and middle-click
   and degrades to a plain jump if JS fails. site.js intercepts the click and
   animates it with tweenScrollTo (see the SMOOTH SCROLL TWEEN block there) -
   the native scroll-behavior:smooth it originally relied on was too fast and
   not adjustable, so scroll-behavior is now `auto` and the tween owns the
   motion. Under prefers-reduced-motion the tween jumps instantly instead.
   site.js still writes opacity/pointer-events inline for the fade, so any
   hover state has to live on the inner chevron rather than on .scroll-cue -
   an inline style would beat a stylesheet :hover rule on the element. */
.scroll-cue { display:block; position:absolute; bottom:26px; left:50%; transform:translateX(-50%); z-index:2; transition:opacity .15s linear; }
/* Enlarges the tap target to roughly 65x47px without moving the arrow: a
   12px chevron is far below a usable touch target, but padding on the
   element itself would shift it off its 26px baseline. Kept clear of the
   #packages CTA stack above it - see the note on .slide-cta-stack. */
.scroll-cue::before { content:''; position:absolute; inset:-10px -24px; }
.scroll-cue:focus-visible { outline:2px solid #fff; outline-offset:4px; border-radius:4px; }
.scroll-cue .chevron-down { display:block; animation:chevronBounce 1.8s ease-in-out infinite; }
.scroll-cue .chevron-down i { display:block; width:12px; height:12px; border-right:2px solid rgba(255,255,255,.85); border-bottom:2px solid rgba(255,255,255,.85); transform:rotate(45deg); transition:transform .18s ease, border-color .18s ease; }
.scroll-cue:hover .chevron-down i { transform:rotate(45deg) scale(1.25); border-color:#fff; }
@keyframes chevronBounce { 0%,100%{ transform:translateY(0); opacity:.5; } 50%{ transform:translateY(8px); opacity:1; } }
.media-toggle {
  position:absolute; z-index:2; bottom:26px; right:clamp(20px,4vw,48px);
  width:42px; height:42px; border-radius:50%; border:1.5px solid rgba(255,255,255,.55);
  background:rgba(6,6,8,.35); color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center;
  padding:0;
}
.media-toggle:hover { background:rgba(6,6,8,.55); }
.media-toggle .icon-pause, .media-toggle.paused .icon-play { display:flex; }
.media-toggle .icon-play, .media-toggle.paused .icon-pause { display:none; }
/* Pure CSS icons instead of unicode glyphs — unicode ▶ / ❚❚ carry inconsistent
   built-in side-bearing per font, so flexbox centers their box, not their ink,
   and they end up looking off-centre. Hand-drawn shapes center exactly. */
.media-toggle .icon-pause { align-items:center; justify-content:center; gap:3px; width:12px; height:12px; }
.media-toggle .icon-pause i { display:block; width:3px; height:12px; background:#fff; border-radius:1px; }
.media-toggle .icon-play {
  align-items:center; justify-content:center;
  width:0; height:0;
  border-top:6px solid transparent; border-bottom:6px solid transparent;
  border-left:10px solid #fff;
  margin-left:3px; /* optical centering: a right-pointing triangle's visual weight sits left of its bounding box */
}

/* ============ PAGE HERO (subpages, static image) ============ */
.page-hero { position:relative; padding:168px clamp(20px,4vw,48px) 84px; overflow:hidden; }
.page-hero-bg { position:absolute; inset:0; background-size:cover; background-position:center; z-index:0; }
.page-hero::after {
  content:''; position:absolute; inset:0; z-index:1;
  background:linear-gradient(180deg, rgba(6,6,8,.35) 0%, rgba(6,6,8,.55) 55%, rgba(6,6,8,.88) 100%);
}
/* The three .page-hero-content rules below are currently UNUSED: tailor.html
   is the only page with a .page-hero and its header went image-only on
   2026-07-27, so there is no text inside it any more. Kept rather than
   deleted because .page-hero and .page-hero-bg are still live and this is the
   rest of that same component - a future subpage wanting a captioned header
   would need exactly these back. */
.page-hero-content { position:relative; z-index:2; max-width:820px; }
.page-hero-content .eyebrow { color:var(--accent2); }
.page-hero-content h1 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(40px,7vw,84px); line-height:1.03; color:#fdfcf9; text-transform:var(--tt-display); margin-top:14px; }
.page-hero-content p { margin-top:20px; max-width:580px; font-size:16px; line-height:1.7; color:rgba(255,255,255,.88); }

/* ============ SECTIONS ============ */
.section { padding:104px clamp(20px,4vw,48px); max-width:1240px; margin:0 auto; }
.section-head { max-width:640px; margin-bottom:48px; }
.section-head.center { max-width:720px; margin-left:auto; margin-right:auto; text-align:center; }
.section-head .eyebrow { color:var(--accent); }
/* h1 shares this with h2. tailor.html's header is image-only, so the page's
   real headline is the h1 in the first section - it needs the section-heading
   size, not .page-hero-content h1's 40-84px display size. margin-top:10px
   exists to sit under an eyebrow; harmless when the heading is alone. */
.section-head h1, .section-head h2 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(30px,4.5vw,48px); text-transform:var(--tt-display); margin-top:10px; color:var(--fg); }
.section-head p { margin-top:16px; color:var(--muted); font-size:15.5px; line-height:1.65; }
.section p.body-text { font-size:16px; line-height:1.75; color:var(--fg); max-width:700px; }
.section p.body-text + p.body-text { margin-top:18px; }

/* ============ SPLIT (image + text) ============ */
.split { display:grid; grid-template-columns:1fr; gap:44px; align-items:center; }
@media (min-width:860px) { .split { grid-template-columns:1.05fr 1fr; } .split.reverse { grid-template-columns:1fr 1.05fr; } .split.reverse .split-media { order:2; } }
.split-media img { border-radius:var(--radius-card); width:100%; display:block; object-fit:cover; }
.split-text h2 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(28px,4.2vw,42px); text-transform:var(--tt-display); color:var(--fg); margin-bottom:18px; }
.split-text p { font-size:15.5px; line-height:1.75; color:var(--muted); }
.split-text p + p { margin-top:14px; }

/* ============ PARALLAX (image + centered text over photo) ============ */
/* 2026-07-26: matches .hero exactly (100svh / min-height:580px) - was
   84vh/540px, then briefly calc(100svh - 88px) to dodge the fixed nav, but
   that left a visible height mismatch against the hero ("weird in-betweeny
   stuff"). The nav overlays these slides the exact same transparent-then-
   solid way it already overlays the hero, so there's no need to subtract
   its height - full-viewport-to-full-viewport is the consistent version. */
/* 2026-07-26: briefly had clip-path:inset(0) added here to chase a dark-bleed
   report, but that turned out to make things worse - screenshots showed a
   visible grey bar appearing at this same boundary during scroll once
   clip-path was added, so it's been reverted. Kept overflow:hidden (the
   original, working approach) and added an explicit background:var(--bg)
   below as a defensive fallback instead - if a rendering gap ever reveals
   this section's own background for a frame, it should show the page's
   cream tone rather than an undefined/browser-default colour. */
.parallax-section { position:relative; height:100svh; min-height:580px; overflow:hidden; background:var(--bg); display:flex; align-items:center; justify-content:center; }
.parallax-section.short { height:56vh; min-height:400px; }
/* PARALLAX OVERSCAN - read the long comment above updateParallax() in
   site.js before changing this.
   The vertical inset (--parallax-inset, 0.25 = 25%) is the image's overhang
   beyond its frame, and it is the hard ceiling on how far the parallax can
   travel: site.js's PARALLAX_TRAVEL_PCT (0.20) must stay below it, and the
   5-point gap is deliberate headroom for sub-pixel rounding. Percentage
   insets resolve against the section's height, so the image box is 1.5x the
   section tall and background-size:cover zooms the photo to suit; that zoom
   is the cost of the effect and is why the earlier -38% attempt looked like
   the parallax had stopped.
   site.js sets BOTH transform and clip-path on this element every frame, so
   don't add either here - they will be overwritten. The clip is what carves
   the seam gap out of the photo's top edge; see .seam-fill below.
   Horizontal -6% is incidental - the JS only ever translates on Y. */
.parallax-img {
  position:absolute;
  inset:calc(var(--parallax-inset) * -100%) -6%;
  background-size:cover; background-position:center;
  will-change:transform;
}
/* SYMMETRIC SCRIM - the seam depends on this, don't revert it to a simple
   top-to-bottom ramp.
   This was .4 at the top down to .72 at the bottom. That put a slide's dark
   bottom edge directly against the next slide's light top edge, a 2x jump in
   brightness at every seam (a mid-tone pixel goes luminance 34 to 69 across
   it). That step was the "hard line between the two photos" - and no amount
   of blurring hides it, because blur smooths each side independently and
   leaves the discontinuity between them untouched.
   Matching both edges at .66 still matters now that a gap sits between the
   slides rather than a direct join: the gap's top edge (departing photo) and
   bottom edge (arriving photo) are both scrimmed at .66, so the two sides of
   the opening are the same darkness. An asymmetric ramp would make one lip
   of the gap visibly darker than the other. The lighter .5 midpoint keeps
   the photo from being flattened and still backs the centred text at roughly
   its previous value (mid was ~.56 before).
   Note this scrim also covers .seam-fill, which sits behind the photo - so
   the blurred fill is darkened by .66 too. That is why .seam-fill carries a
   brightness() boost; if the gap ever looks muddy, raise that rather than
   lightening this gradient, which would unbalance the gap's two lips.
   If these numbers change, keep the FIRST and LAST stops equal to each other
   and equal to .hero::after's last stop. */
.parallax-overlay {
  position:absolute; inset:0; z-index:1;
  background:linear-gradient(180deg, rgba(4,4,6,.66) 0%, rgba(4,4,6,.5) 50%, rgba(4,4,6,.66) 100%);
}

/* SEAM FILL - the blurred image behind the gap between two slides.
   site.js clips each photo's top edge to carve a gap that opens as a
   transition begins and closes as the slide centres; this layer sits behind
   the photo and is what you see through that gap. See the SEAM_BAND_PCT
   comment in site.js for the full history.
   The gap has HARD edges on both sides - the photo above ends crisply, the
   photo below begins crisply. That is deliberate and was Callum's explicit
   call after a version that blurred the photos into each other was rejected
   ("a bit crappy"). Only the FILL is blurred, never the slides themselves.
   Why blur an image rather than use a flat colour: a flat band read as
   jarring, a hard slab dropped between two photographs. A heavily blurred
   copy of a slide photo is the standard letterbox-blur fill used behind
   mismatched-aspect video and album art - it carries the photo's own colours,
   so the gap belongs to the images either side of it instead of interrupting
   them.
   THE INSET MUST MATCH .parallax-img EXACTLY. site.js slides this layer to
   put a chosen slice of the photo in the gap, and that maths assumes this box
   is the same size and position as the photo's, so the two frame the image
   identically. Changing one without the other puts the wrong slice in the
   gap. site.js sets transform here every frame, so don't set one in CSS.
   The overscan is doing real work beyond that: filter:blur() samples past an
   element's edges and fades to transparent there, so the negative inset keeps
   those soft edges outside the visible gap. Without it you get a pale halo. */
.seam-fill {
  position:absolute;
  inset:calc(var(--parallax-inset) * -100%) -10%;
  z-index:0; pointer-events:none;
  background-size:cover; background-position:center;
  filter:blur(54px) saturate(1.35) brightness(1.45);
}

.about-content { position:relative; z-index:2; max-width:700px; text-align:center; padding:0 24px; color:#fdfcf9; }
/* Slide eyebrows (About / Listen / The Big Night) run larger than the shared
   .eyebrow's 12px, 2026-07-26. Scoped to the slide contexts rather than
   changed on .eyebrow itself, because that class is also used on
   tailor.html section headings, where 12px is right against a cream
   background - these three sit over full-bleed photos and need more presence.
   The Listen slide's copy of this lives on .carousel-heading .eyebrow; keep
   the two in step. */
.about-content .eyebrow { color:var(--accent2); font-size:clamp(13px,1.5vw,15px); }
.about-content h2 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(32px,5vw,54px); text-transform:var(--tt-display); margin:14px 0 22px; }
.about-content p { font-size:16.5px; line-height:1.75; color:rgba(255,255,255,.9); }
.tag-row { margin-top:30px; display:flex; flex-wrap:wrap; gap:10px; justify-content:center; }
.tag-row span { border:1px solid rgba(255,255,255,.45); color:#fff; font-size:11.5px; letter-spacing:.1em; text-transform:uppercase; padding:9px 17px; border-radius:999px; }
.about-link { display:inline-block; margin-top:30px; font-size:13px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#fff; border-bottom:1px solid var(--accent2); padding-bottom:3px; }

/* Bottom-middle stack on the #packages "Your Big Night" panel - originally
   an icon row (Instagram, Spotify) above the CTA button; Instagram moved to
   the About slide 2026-07-26 and the Spotify placeholder was dropped
   entirely 2026-07-27, leaving just the button. Lifted from bottom:56px to
   104px the same day so it doesn't sit right at the bottom edge now that
   there's no icon row above it filling that space. */
.slide-cta-stack {
  position:absolute; left:50%; bottom:104px; transform:translateX(-50%); z-index:2;
  display:flex; flex-direction:column; align-items:center; gap:26px;
}
.social-icon-link {
  display:flex; flex-direction:column; align-items:center; gap:10px;
  text-decoration:none;
}
.social-icon-link svg {
  width:60px; height:60px; border-radius:50%; padding:16px; box-sizing:border-box; display:block;
  border:1.5px solid rgba(255,255,255,.55); background:rgba(6,6,8,.35); color:#fff;
  transition:background .15s ease, border-color .15s ease;
}
.social-icon-link:hover svg { background:rgba(6,6,8,.55); border-color:#fff; }
/* Icon-only variant, 2026-07-26: the Instagram link moved from the
   What We Offer slide's CTA stack onto the About slide and lost its "Gallery"
   caption, so it's now just the glass circle. .social-icon-link is a column
   flex with a 10px gap for the label underneath - with no label that gap
   would still add stray space below, hence gap:0. inline-flex so it shrinks
   to the icon instead of spanning the centred text column, and the top
   margin gives it room under the paragraph. */
.social-icon-link.icon-only { display:inline-flex; gap:0; margin-top:30px; }
/* NOTE: a .social-icon-link.on-light variant briefly existed here for a single
   large Spotify icon on tailor.html. That icon was replaced by the .setlist-row
   list below the same day, leaving the variant unused, so it was deleted rather
   than left as dead code (same call as .social-icon-row before it). If an icon
   link is ever needed on a light background again, note that everything above
   assumes a dark full-bleed photo behind it - the translucent white border and
   dark glass fill read as invisible and muddy against cream, and need swapping
   for the card palette. */
.social-icon-label {
  font-family:var(--font-display); font-style:italic; font-size:24px;
  color:#fdfcf9; opacity:.92; letter-spacing:var(--ls-display);
}
@media (max-width:640px) {
  /* 2026-07-26: was bottom:36px. Every slide now carries a .scroll-cue at
     bottom:26px, and the chevron's animated bounce reaches roughly 43px up,
     so 36px put the CTA stack straight on top of it. 58px clears it with a
     usable gap. If the cue's position or size changes, re-check this. */
  .slide-cta-stack { bottom:96px; gap:18px; }
  .social-icon-link svg { width:48px; height:48px; padding:13px; }
  .social-icon-label { font-size:19px; }
}

/* Persistent heading overlaid on the Listen carousel (#listen) - unlike the
   per-slide title (which changes with the active slide), this sits fixed at
   the top of the frame regardless of which of the 6 slides is showing. */
/* 2026-07-26: was pinned near the top (top:32px); moved to vertical-center
   to match how the About/Packages slides position their eyebrow+heading
   (.about-content, centred via its parent's flex). The play button was
   pushed down below centre (see .player-hero-play) so the two don't overlap. */
/* 2026-07-26: the heading used to be vertically centred and the play button
   was pushed DOWN to 66% to clear it. Callum wanted the button dead centre
   instead, so this is now anchored by its BOTTOM edge just above the middle.
   Anchoring the bottom rather than setting a top percentage matters: the
   heading wraps to two or three lines on narrow screens, and a bottom anchor
   grows upward, so it can never creep down into the button however long the
   copy gets. */
/* 2026-07-27: 48px offset above centre eased to 24px, then to 8px, chasing
   the gap to the play button - but that also dragged the eyebrow down with
   it (same flow block, eyebrow directly above h2), until it sat well below
   where About's eyebrow sits. Fix: the eyebrow's own margin-bottom (10px ->
   48px) now does the work of separating it from the heading, so the two can
   be tuned independently - eyebrow position no longer rides on wherever h2
   needs to land. h2 stays bottom-anchored (wrap-grows-upward safety intact),
   now at calc(50% + 26px), which centres it between the eyebrow (matching
   About's eyebrow height) and the play button (top:55%) - nudged up from
   15px same day, still slightly low. */
.carousel-heading {
  position:absolute; bottom:calc(50% + 26px); left:0; right:0; z-index:5;
  text-align:center; padding:0 24px; pointer-events:none;
}
/* Matches .about-content .eyebrow - see the note there. */
.carousel-heading .eyebrow {
  color:var(--accent2); font-size:clamp(13px,1.5vw,15px); letter-spacing:.2em; text-transform:uppercase;
  font-weight:700; margin-bottom:48px; text-shadow:0 1px 6px rgba(0,0,0,.4);
}
.carousel-heading h2 {
  font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(28px,4.5vw,46px);
  color:#fdfcf9; text-shadow:0 2px 10px rgba(0,0,0,.5); text-transform:var(--tt-display);
}
/* Added 2026-07-27: h2 reverted to "Chemistry you can hear", with the old
   heading text ("Press play to hear live recordings") demoted to this
   smaller line underneath - same idea as .about-content p, sized down and
   with a text-shadow added since this sits directly on a photo rather than
   the About slide's darker overlay. .carousel-heading's own bottom-anchor
   wasn't retuned for the extra line - it still anchors by the LAST child
   (now this p, not h2), which keeps the same wrap-grows-upward safety
   against the play button; the block as a whole just sits a little higher
   as a result. Revisit spacing if that reads oddly. */
.carousel-heading p {
  font-size:16px; line-height:1.6; color:rgba(255,255,255,.9);
  text-shadow:0 1px 6px rgba(0,0,0,.4); margin-top:14px;
}
@media (max-width:640px) {
  .carousel-heading h2 { font-size:clamp(24px,7vw,34px); }
}

/* ============ GALLERY SCROLLER (auto-scroll ribbon) ============ */
.gallery-outer { width:100vw; margin-left:calc(50% - 50vw); overflow:hidden; padding:6px 0; cursor:grab; }
.gallery-outer.dragging { cursor:grabbing; }
.gallery-track { display:flex; gap:18px; width:max-content; padding:0 clamp(20px,4vw,48px); animation:galleryScroll 110s linear infinite; will-change:transform; }
.gallery-track img { height:380px; width:auto; border-radius:var(--radius-card); object-fit:cover; flex-shrink:0; -webkit-user-drag:none; user-select:none; pointer-events:none; }
@keyframes galleryScroll { from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
@media (max-width:640px) { .gallery-track img { height:260px; } }
.gallery-cta { margin-top:8px; padding:0 clamp(20px,4vw,48px); max-width:1240px; margin-left:auto; margin-right:auto; }
.gallery-toggle {
  display:flex; align-items:center; gap:8px; margin:14px clamp(20px,4vw,48px) 0;
  border:1px solid var(--card-border); background:var(--card-bg); color:var(--fg);
  padding:8px 16px; border-radius:999px; font-size:12px; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase; cursor:pointer; width:fit-content;
}
.gallery-toggle:hover { border-color:var(--accent); color:var(--accent); }

/* ============ CHIP ROW (genre/style tags on light backgrounds) ============ */
.chip-row { display:flex; flex-wrap:wrap; gap:10px; justify-content:center; margin-top:26px; }
.chip-row span { border:1px solid var(--card-border); color:var(--accent); font-size:11.5px; letter-spacing:.1em; text-transform:uppercase; padding:9px 17px; border-radius:999px; background:var(--card-bg); }

.listen-section { padding-top:36px; }

/* ============ SLIDESHOW (Listen page — photo + audio per slide) ============ */
/* Full-bleed breakout: escapes the .section's max-width/padding so the frame
   runs edge-to-edge of the viewport regardless of where it's nested, without
   having to touch the section's own padding (the CTA link below it stays
   normally contained). */
.slideshow {
  position:relative; width:100vw; left:50%; right:50%;
  margin-left:-50vw; margin-right:-50vw; max-width:100vw;
}
.slide { display:none; }
.slide.active { display:block; }
/* Big, near-full-height frame instead of a narrow centred card - the photo
   is the page here, not a thumbnail floating in a box.
   2026-07-26: dropped the earlier blurred-backdrop-layer + object-fit:contain
   + mask-fade approach (it kept reading as a hard cut no matter how the
   scale/mask stops were tuned). Simplified to plain object-fit:cover, same
   technique the About/Packages parallax slides already use - the photo just
   fills the whole frame edge-to-edge, so there's no letterboxing gap left to
   blur/fade/disguise in the first place.
   2026-07-26 (later same day): the photo itself is now a .parallax-img
   background-div (data-bg), not a plain <img> - matching
   About/Packages exactly, so this slide gets the same scroll-linked
   parallax drift they do (a real <img> can't reliably size itself off the
   inset:-12% -6% overscan trick the way a background-image div can, which
   is why the rest of the site already used divs for this). No extra CSS
   needed here - .parallax-img and the JS parallax loop are already generic. */
/* Height matches .parallax-section and .hero exactly (100svh / min-height:
   580px, 2026-07-26) so this carousel panel, the About panel and the
   What-We-Offer panel all read as the same full-viewport size in the
   sequence - see the note above .parallax-section for why this dropped the
   nav-height subtraction it briefly had. */
/* background was #000 (black) - a leftover from the old letterboxing setup.
   .parallax-section (About/Packages) has no explicit background of its own,
   so it shows the page's cream --bg through any parallax overscan gaps;
   .slide-photo needs the same var(--bg) so Listen matches instead of
   flashing black at the edges during scroll. */
.slide-photo {
  position:relative; width:100%; height:100svh; min-height:580px;
  overflow:hidden; background:var(--bg);
}
.slide-badge {
  position:absolute; top:20px; left:20px; z-index:3; background:rgba(6,6,8,.55); color:#fdfcf9;
  font-size:10.5px; letter-spacing:.12em; text-transform:uppercase; padding:7px 13px; border-radius:999px;
}

/* 2026-07-26: was a 4-stop gradient with a fully clear middle band (dark top,
   clear centre, dark bottom) - a leftover from the old carousel design and
   the only slide overlay on the page not using the plain top-to-bottom
   two-stop scrim (.parallax-overlay, used on About/Packages). Standard
   practice for a photo-with-text overlay is a single consistent gradient,
   not a banded one, so this matches .parallax-overlay's values exactly for a
   uniform look across all three slide transitions. Still pointer-events:none
   and still named .player-vignette since the class is referenced from the
   HTML/JS as-is.
   2026-07-26 (later): tracks .parallax-overlay's switch to a SYMMETRIC
   scrim - see the long note there for why the edges must match. */
.player-vignette {
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:linear-gradient(180deg, rgba(4,4,6,.66) 0%, rgba(4,4,6,.5) 50%, rgba(4,4,6,.66) 100%);
}
/* Wrapper no longer positions anything itself - the icon and title are each
   positioned independently below, so hiding the icon after it's pressed
   can't reflow or shift the title. */
.player-center { display:contents; }
/* Hero play icon: no button chrome at all, just the triangle with a slight
   drop shadow for legibility. 2026-07-26: now a persistent play/pause
   control (was a one-shot "start" trigger that hid itself for good after
   first click) - it swaps its own icon and stays put, see .playing below.
   Also 2026-07-26: pushed down below true-centre (was top:50%) so it clears
   the eyebrow+heading text, which now sits vertically centred to match the
   other slides (see .carousel-heading); and made more transparent by
   default (was fully opaque) so it reads as a lighter-touch overlay control. */
/* top:55%, not 50% - this is optical centring, not a mistake, so don't
   "correct" it back.
   The slide's true midpoint is 50% and the button was set there, but it read
   as sitting high. The cause isn't a layout offset (.slideshow and
   .slide-photo are both exactly 100svh, so their midpoints do coincide) - it's
   that the composition is top-heavy: there's an eyebrow and a heading above
   the button and nothing at all below it until the seek bar appears. The eye
   balances a group, not a single element, so a geometrically centred button
   in that arrangement looks too high. Nudging it below centre is the standard
   fix and is why it was originally at 66%.
   55% is the compromise between that old 66% (which read as clearly low once
   the heading moved above it) and a true 50%. This is the one value to
   adjust if the heading's length or position changes again. */
.player-hero-play {
  position:absolute; top:55%; left:50%; transform:translate(-50%, -50%); z-index:2;
  border:none; background:transparent; padding:0; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  filter:drop-shadow(0 2px 8px rgba(0,0,0,.5)); opacity:.78; transition:opacity .2s ease;
}
/* Opacities raised across the board 2026-07-26 (were .5 / .65 / .85 / .9).
   The relationship is unchanged - playing reads slightly stronger than
   paused, hover stronger again - just less see-through overall. */
.player-hero-play:hover { opacity:.95; }
.player-hero-play.playing { opacity:.86; }
.player-hero-play.playing:hover { opacity:1; }
.player-hero-play[hidden] { display:none; }
.player-hero-play .icon-play-bare {
  width:0; height:0; border-top:26px solid transparent; border-bottom:26px solid transparent;
  border-left:42px solid #fff; margin-left:9px;
}
.player-hero-play.playing .icon-play-bare { display:none; }
.player-hero-play .icon-pause-bare { display:none; align-items:center; justify-content:center; gap:11px; width:52px; height:52px; }
.player-hero-play.playing .icon-pause-bare { display:flex; }
.player-hero-play .icon-pause-bare i { display:block; width:9px; height:52px; background:#fff; border-radius:2px; }
.player-title {
  position:absolute; left:0; right:0; top:78%; transform:translateY(-50%); z-index:2;
  text-align:center; padding:0 24px;
  color:#fdfcf9; font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(34px,5vw,64px);
  text-transform:var(--tt-display); letter-spacing:var(--ls-display);
  opacity:.85; text-shadow:0 2px 10px rgba(0,0,0,.5);
}
.player-seek-wrap {
  position:absolute; left:0; right:0; bottom:0; z-index:2;
  display:flex; align-items:center; gap:14px;
  padding:22px 24px 20px;
  background:linear-gradient(0deg, rgba(6,6,8,.6) 0%, transparent 100%);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .25s ease, visibility 0s linear .25s;
}
.player-seek-wrap.visible {
  opacity:1; visibility:visible; pointer-events:auto;
  transition:opacity .25s ease, visibility 0s linear 0s;
}
.player-seek { flex:1; min-width:0; }
.player-seek, .player-volume {
  -webkit-appearance:none; appearance:none; height:4px; border-radius:999px;
  background:rgba(255,255,255,.35); accent-color:var(--accent2); cursor:pointer; display:block; margin:0;
}
.player-seek::-webkit-slider-runnable-track, .player-volume::-webkit-slider-runnable-track { height:4px; border-radius:999px; background:rgba(255,255,255,.35); }
.player-seek::-webkit-slider-thumb, .player-volume::-webkit-slider-thumb {
  -webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:50%;
  background:#fdfcf9; border:none; margin-top:-5px; cursor:pointer;
}
.player-seek::-moz-range-track, .player-volume::-moz-range-track { height:4px; border-radius:999px; background:rgba(255,255,255,.35); }
.player-seek::-moz-range-thumb, .player-volume::-moz-range-thumb { width:14px; height:14px; border-radius:50%; background:#fdfcf9; border:none; cursor:pointer; }
.player-mute {
  flex-shrink:0; width:32px; height:32px; padding:0; border:none; background:transparent;
  color:#fff; opacity:.85; cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.player-mute:hover { opacity:1; }
.player-mute svg { width:18px; height:18px; display:block; fill:currentColor; }
.player-mute .icon-mute { display:none; }
.player-mute.muted .icon-vol { display:none; }
.player-mute.muted .icon-mute { display:block; }
.player-volume { flex-shrink:0; width:76px; }
/* Volume SLIDER is desktop-only. Two reasons, and the second is the
   load-bearing one: phones and tablets have hardware volume keys that do this
   job better, and iOS ignores HTMLMediaElement.volume outright - it's
   read-only there - so on iPhone/iPad the slider isn't merely redundant, it's
   a dead control that visibly moves and changes nothing.

   The mute button deliberately STAYS on touch: audio.muted does work on iOS,
   and one tap to silence is a different affordance from holding a hardware
   key down. Don't fold it into this rule without checking that's wanted.

   Matched on pointer, not width, to match how site.js picks its scroll
   mechanism - so a large tablet is still treated as touch, and a narrow
   desktop window still gets the slider. */
@media (pointer: coarse) {
  .player-volume { display:none; }
}
@media (max-width:640px) {
  .player-hero-play .icon-play-bare { border-top-width:18px; border-bottom-width:18px; border-left-width:29px; }
  .player-hero-play .icon-pause-bare { width:36px; height:36px; gap:8px; }
  .player-hero-play .icon-pause-bare i { width:6px; height:36px; }
  .player-seek-wrap { padding:16px 18px 16px; gap:10px; }
  .player-volume { width:52px; }
}

/* Nav arrows now sit inset over the photo itself (the frame runs full-bleed,
   so there's no page margin outside it to float them in anymore). Dark
   translucent chrome keeps them legible against any photo. */
.slide-nav-btn {
  position:absolute; top:50%; transform:translateY(-50%); z-index:3;
  background:rgba(6,6,8,.4); border:1.5px solid rgba(255,255,255,.5); color:#fff;
  width:48px; height:48px; border-radius:50%; font-size:20px; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  backdrop-filter:blur(4px); transition:background .15s ease, border-color .15s ease;
}
.slide-nav-btn:hover { background:rgba(6,6,8,.6); border-color:#fff; }
.slide-prev { left:20px; }
.slide-next { right:20px; }
@media (max-width:640px) {
  .slide-nav-btn { width:40px; height:40px; }
  .slide-prev { left:10px; } .slide-next { right:10px; }
}
.slide-dots { display:flex; justify-content:center; gap:4px; margin-top:22px; }
/* Each dot's clickable button is a full 24px touch target (WCAG 2.5.8); the
   visible dot inside it stays small via ::after so the row doesn't look
   oversized. */
.slide-dots button {
  width:24px; height:24px; border-radius:50%; background:none; border:none; cursor:pointer;
  padding:0; position:relative; display:flex; align-items:center; justify-content:center;
}
.slide-dots button::after {
  content:''; width:8px; height:8px; border-radius:50%; background:var(--card-border); opacity:.4;
  transition:opacity .15s ease, transform .15s ease;
}
.slide-dots button:hover::after { opacity:.7; }
.slide-dots button.active::after { background:var(--accent); opacity:1; transform:scale(1.2); }

/* ============ FULL PHOTO GRID (gallery subpage) ============ */
.photo-grid { columns:3 260px; column-gap:16px; padding:0 clamp(20px,4vw,48px); max-width:1400px; margin:0 auto; }
.photo-grid img { width:100%; border-radius:var(--radius-card); margin-bottom:16px; break-inside:avoid; display:block; }
@media (max-width:700px) { .photo-grid { columns:2 160px; } }

/* ============ CARD GRID (services / repertoire / packages) ============ */
.services-grid, .card-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:22px; margin-top:8px; }
.service-card, .info-card { background:var(--card-bg); border:1px solid var(--card-border); border-radius:var(--radius-card); padding:30px 26px; }
.service-card h3, .info-card h3 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:19px; margin-bottom:12px; text-transform:var(--tt-display); color:var(--fg); }
.service-card p, .info-card p { font-size:14px; line-height:1.65; color:var(--muted); }

/* Group label above a card grid (tailor.html: "Standard Booking",
   "Customise Your Evening"). Sized deliberately between .section-head h2
   (clamp 30-48px) and a card's own 19px title, so it subdivides the section
   without shouting over it or getting lost among the cards. The margin pair
   below does the spacing: grids sit tight under their own label, with a wide
   gap before the next label starts a new group.

   These are <h2> and the cards under them are <h3>. That was briefly h3/h4,
   when this section still opened with a .section-head h2 ("Put together the
   right sound for your day"). That intro was removed 2026-07-27, which left
   the page jumping h1 -> h3, so everything moved up a level. If a section
   heading is ever reinstated above these, they need to go back to h3/h4. */
.group-head {
  font-family:var(--font-display); font-weight:var(--fw-display);
  font-size:clamp(20px,2.6vw,26px); text-transform:var(--tt-display);
  color:var(--fg); margin-bottom:18px;
}
.services-grid + .group-head, .card-grid + .group-head { margin-top:52px; }

/* ============ SETLIST ROWS (tailor.html) ============ */
/* Name on the left, Spotify glyph on the right, one row per playlist.
   The whole row is the anchor, not just the glyph: at 26px the icon alone
   falls under WCAG 2.5.5's target-size guidance and is fiddly on a phone,
   while the row's 18px vertical padding gives roughly a 60px target for an
   identical visual result.

   Hairlines are drawn with border-bottom on every row plus border-top on the
   first, rather than a border on the <li>, so the rules line up with the
   padded link box and hover repaints cleanly against them. The 640px cap
   matches .section-head's measure so the list sits under the intro copy
   rather than running the full 1240px section width - four short names
   stretched that wide would leave the glyph stranded far from its label. */
.setlist-list { list-style:none; max-width:640px; margin-top:8px; }
.setlist-row {
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:18px 4px; text-decoration:none; color:var(--fg);
  border-bottom:1px solid var(--card-border);
  transition:color .15s ease;
}
.setlist-list li:first-child .setlist-row { border-top:1px solid var(--card-border); }
.setlist-name {
  font-family:var(--font-display); font-weight:var(--fw-display);
  font-size:clamp(17px,2vw,20px); text-transform:var(--tt-display);
  letter-spacing:var(--ls-display);
}
.setlist-row svg { width:26px; height:26px; flex-shrink:0; color:var(--muted); transition:color .15s ease; }
.setlist-row:hover, .setlist-row:focus-visible { color:var(--accent); }
.setlist-row:hover svg, .setlist-row:focus-visible svg { color:var(--accent); }
.services-cta { margin-top:38px; }
.link-arrow { color:var(--accent); font-weight:700; font-size:13px; letter-spacing:.05em; text-transform:uppercase; border-bottom:1px solid var(--accent); padding-bottom:3px; }

/* ============ QUOTE ============ */
/* 2026-07-26: vertical padding was a flat 110px, which made this the tallest
   block on the page for two lines of quote. Now scales with the viewport,
   same treatment as .cta-final below. The horizontal padding is unchanged -
   the 840px max-width is what sets the measure, and narrowing that would push
   the quote onto more lines and undo the height saving. */
.quote-section { padding:clamp(44px,6vw,64px) clamp(20px,4vw,48px); text-align:center; max-width:840px; margin:0 auto; }
.quote-section blockquote { font-family:var(--font-display); font-weight:var(--fw-display); font-style:italic; font-size:clamp(22px,3.6vw,34px); line-height:1.42; color:var(--fg); }
.quote-section cite { display:block; margin-top:16px; font-family:var(--font-body); font-style:normal; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }

/* ============ FINAL CTA ============ */
/* 2026-07-26: padding was a flat 100px top and bottom, which left the banner
   very tall for two short lines of text. Reduced twice - to
   clamp(52px,7vw,72px), then to the values below. Now scales with the
   viewport so it stays proportionate on phones as well. */
.cta-final { background:var(--accent); color:var(--btn-fg); text-align:center; padding:clamp(36px,5vw,52px) 24px; }
.cta-final h2 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:clamp(32px,5vw,56px); text-transform:var(--tt-display); margin-bottom:14px; }
/* The paragraph carried a 32px bottom margin to separate it from a button
   that used to sit here. There's no button in this section any more, so that
   margin was unbalanced dead space below the text - which is what made the
   block read as top-heavy rather than centred. The :not(:last-child) form
   keeps the spacing correct automatically if a button is ever added back. */
.cta-final p { max-width:480px; margin:0 auto; font-size:15.5px; line-height:1.65; }
.cta-final p:not(:last-child) { margin-bottom:32px; }
.cta-final .btn-primary { background:var(--btn-fg); color:var(--accent); }

/* ============ ENQUIRY FORM (contact page) ============ */
/* Single-column now that the contact-panel sidebar was removed (2026-07-26)
   - was a 2-col grid (form + panel) on wider screens, form now just centers
   in a form-sized column on its own. */
.enquiry-layout { display:grid; grid-template-columns:1fr; gap:56px; max-width:640px; margin:0 auto; padding:0 clamp(20px,4vw,48px); }
.enquiry-form { display:grid; gap:20px; }
.form-row { display:grid; gap:20px; }
@media (min-width:560px) { .form-row.two { grid-template-columns:1fr 1fr; } }
.form-field label { display:block; font-size:11.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin-bottom:8px; font-weight:700; }
.form-field input, .form-field textarea, .form-field select {
  width:100%; padding:14px 16px; border-radius:10px; border:1.5px solid var(--card-border);
  background:var(--card-bg); color:var(--fg); font-family:var(--font-body); font-size:14.5px;
}
.form-field textarea { min-height:140px; resize:vertical; }
.form-field input:focus, .form-field textarea:focus, .form-field select:focus { outline:none; border-color:var(--accent); }
.form-status { font-size:13.5px; color:var(--accent); min-height:20px; }
.contact-panel { background:var(--card-bg); border:1px solid var(--card-border); border-radius:var(--radius-card); padding:32px 28px; align-self:start; }
.contact-panel h2 { font-family:var(--font-display); font-weight:var(--fw-display); font-size:20px; margin-bottom:18px; color:var(--fg); }
.contact-panel .contact-line { display:block; margin-bottom:14px; font-size:14.5px; }
.contact-panel .contact-line a:hover { color:var(--accent); }
.contact-panel .contact-line span.label { display:block; font-size:10.5px; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); margin-bottom:4px; }

/* ============ FOOTER ============ */
footer { padding:60px clamp(20px,4vw,48px) 32px; max-width:1240px; margin:0 auto; }
.footer-top { display:flex; flex-wrap:wrap; justify-content:space-between; gap:40px; padding-bottom:44px; }
.footer-brand .logo { font-size:22px; }
.footer-brand p { margin-top:14px; max-width:280px; font-size:13.5px; line-height:1.6; color:var(--muted); }
.footer-col .footer-heading { font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-bottom:16px; font-weight:700; }
.footer-links { display:flex; flex-direction:column; gap:10px; }
.footer-links a { font-size:14px; }
.footer-links a:hover { color:var(--accent); }
.footer-bottom { border-top:1px solid var(--card-border); padding-top:22px; display:flex; justify-content:space-between; flex-wrap:wrap; gap:10px; font-size:12px; color:var(--muted); }
