/* ---- tokens.css ---- */
/* ============================================================================
   TOKENS — The County Lads

   Every value here was measured on the original site at innerWidth 2683 and is
   recorded in .scrape/DESIGN-NOTES.md. Components are written entirely against
   these variables; a hard-coded colour in a component file is a bug.

   Type and spacing use clamp() so they scale smoothly rather than stepping at
   959px and 569px the way the original did. The endpoints match the original
   exactly — see DESIGN-NOTES §1 for the deliberate difference.
   Formula: clamp(MIN, <intercept>rem + <slope>vw, MAX)
   ========================================================================= */

:root {
  /* ---- brand ----------------------------------------------------------
     The site is monochrome. The only brand colour anywhere is the peach in
     the logo badge, sampled from the artwork; it is used for focus rings and
     nothing else, exactly as sparingly as the original used colour (never). */
  --peach: #ffccaa;
  --peach-dim: rgba(255, 204, 170, 0.28);

  /* ---- neutrals, measured -------------------------------------------- */
  --ink: #303133;          /* big headings on light  (rgb 48,49,51)  */
  --black: #000000;        /* body text on light, hero band          */
  --near-black: #101213;   /* "New Release" band     (rgb 16,18,19)  */
  --footer: #141618;       /* footer band            (rgb 20,22,24)  */
  /* The original used #777777 for every muted label and caption, on white AND
     on the dark footer. Measured, that is 4.48:1 on white and 4.05:1 on
     #141618 — both just under the WCAG AA 4.5:1 needed for text this size
     (the footer labels are 17px bold, which is below the 18.66px bold that
     would qualify as large text). Two tokens instead of one, each nudged the
     minimum distance to pass: 4.54:1 and 5.25:1. */
  --muted: #767676;        /* labels and captions on light   (orig #777777) */
  --muted-on-dark: #8a8a8a;/* the same role on the dark band (orig #777777) */
  --white: #ffffff;
  --grey-soft: #f7f7f7;    /* the band that held the broken shortcode */
  --line: rgba(0, 0, 0, 0.12);
  --line-dark: rgba(255, 255, 255, 0.14);

  /* ---- type -----------------------------------------------------------
     Fahkwang for display and long-form copy; Inter for UI and small text.
     Both self-hosted — the original loaded neither. See base/fonts.css. */
  --font-display: "Fahkwang", Georgia, "Times New Roman", serif;
  --font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  /* Display scale. Originals: 125 / 100 / 75 / 50px at ≥960px, stepping to
     83.3 / 66.7 / 50 / 33.3 at ≤959, then 35px at ≤569.

     Each clamp is solved so it hits the original's value exactly at both ends:
     the ≤569px size at a 569px viewport, the ≥960px size at a 960px viewport.
     At 768px they land on 80.8 / 68.1 / 55.4 / 41.8px, sitting smoothly through
     the original's middle step instead of jumping. */
  --fs-display-xl: clamp(2.1875rem, -5.9982rem + 23.0179vw, 7.8125rem); /* 35 → 125 */
  --fs-display-lg: clamp(2.1875rem, -3.7244rem + 16.6240vw, 6.25rem);   /* 35 → 100 */
  --fs-display-md: clamp(2.1875rem, -1.4506rem + 10.2302vw, 4.6875rem); /* 35 →  75 */
  --fs-display-sm: clamp(2.0833rem, 0.5675rem + 4.2626vw, 3.125rem);    /* 33.3 → 50 */

  --fs-h2: 1.8125rem;      /* 29px — gallery credits          */
  --fs-h3: 1.0625rem;      /* 17px — footer column labels     */
  --fs-body: 0.9375rem;    /* 15px — body copy and footer     */
  --fs-story: 1.25rem;     /* 20px — the Our Story biography, at every width */
  --fs-nav: 0.875rem;      /* 14px — nav links, buttons       */
  --fs-small: 0.8125rem;   /* 13px — event meta               */

  --lh-display: 1.2;       /* 100px → 120px, measured         */
  --lh-body: 1.75;         /* 15px → 26.25px, measured        */
  --lh-story: 1.5;         /* 20px → 30px, measured 2026-10-13 */
  --lh-tight: 1.35;

  --fw-body: 400;
  --fw-bold: 600;          /* every heading on the site is 600 */

  /* ---- spacing --------------------------------------------------------
     The theme's rhythm is a strict 36 / 72 / 108 / 144 scale. Kept, with the
     largest two made fluid so a phone is not given 144px of dead space. */
  --space-1: 36px;
  --space-2: clamp(2.25rem, 1.5rem + 3.3333vw, 4.5rem);    /* 36 →  72 */
  --space-3: clamp(2.75rem, 1.4167rem + 5.9259vw, 6.75rem);/* 44 → 108 */
  --space-4: clamp(3.5rem, 1.6667rem + 8.1481vw, 9rem);    /* 56 → 144 */

  --container: 1404px;
  --gutter: 36px;          /* measured: .row is 1404 wide with 36px side
                              padding, giving the 1332px content box every
                              heading on the original lines up against */

  /* ---- grid minimums --------------------------------------------------
     Drive auto-fit grids so columns drop away on their own. The gallery is a
     CSS multi-column masonry; this is its column width. */
  --col-min-gallery: 300px;
  --col-min-events: 340px;

  /* ---- chrome ---------------------------------------------------------
     Header is 272px tall: a 200px logo with 36px above and below. It is
     absolute and transparent over the hero, and never becomes sticky —
     confirmed by scrolling the original to 300 / 1200 / 3000. */
  --header-h: 272px;
  --header-h-mobile: 104px;
  --logo-h: 200px;
  --logo-h-mobile: 50px;

  --radius: 0;             /* square-cornered throughout */
  --radius-embed: 12px;    /* the Spotify iframe's own corner radius */

  /* ---- motion, measured ----------------------------------------------- */
  --ease-cursor: cubic-bezier(0.165, 0.84, 0.44, 1);
  --dur-cursor: 0.4s;
  --ease-zoom: cubic-bezier(0.57, 0.21, 0.69, 1);
  --dur-zoom: 0.3s;
  --transition: 0.25s ease;

  /* ---- the one structural breakpoint ---------------------------------
     The original switches the nav to an off-canvas drawer at 959px (707 CSS
     rules sit behind that query). Media queries cannot read custom properties,
     so this documents the value and each query repeats it literally. */
  --bp-nav: 959px;
}

/* ---- base/fonts.css ---- */
/* ---- self-hosted webfonts ------------------------------------------------
   The original site specified Fahkwang and Inter throughout but never loaded
   either one — no @font-face rule, no Google Fonts link anywhere — so every
   visitor saw the browser default serif. Both faces are SIL OFL 1.1 and are
   served from this origin: no third-party request, nothing to break when a
   CDN changes, and no font traffic leaving to Google.

   Latin subset only. The site's entire non-ASCII vocabulary is – — ’ and ©,
   all of which are inside it. Anything outside the unicode-range simply falls
   back to the next family in the stack, per glyph.

   Inter ships as a variable font, so weights 400 and 600 are the same file and
   the same download — declared twice exactly as Google serves it.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Fahkwang";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/fahkwang-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Fahkwang";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/fahkwang-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/inter-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/inter-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---- base/reset.css ---- */
/* ---- reset & document defaults ------------------------------------------ */

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

html {
  -webkit-text-size-adjust: 100%;
  /* The original used the Lenis library for smooth scrolling. This is the part
     that actually matters on a one-page site — smooth travel to #bandbio,
     #music, #tour and #contact — with no library and no scroll hijacking.
     utilities.css turns it off under prefers-reduced-motion. */
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  font-weight: var(--fw-body);
  line-height: var(--lh-body);
  color: var(--black);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

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

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--transition), opacity var(--transition);
}

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }

/* Long single words — a venue name, a URL — must never push the page sideways.
   Display type is set very large, so this is not theoretical. */
h1, h2, h3, h4, h5, h6, p, li, dd, dt { overflow-wrap: break-word; }

/* The original theme had no visible focus style at all. */
a:focus-visible,
button:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--peach);
  outline-offset: 3px;
}

/* On a dark band the peach ring is the only thing with contrast; on white it
   needs a dark companion so it is not a pale ring on a pale ground. */
.section--light a:focus-visible,
.section--light button:focus-visible {
  outline-color: var(--ink);
  box-shadow: 0 0 0 5px var(--peach-dim);
}

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

.skip-link {
  position: absolute;
  top: -60px;
  left: 12px;
  z-index: 2000;
  padding: 12px 20px;
  background: var(--footer);
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: var(--fw-bold);
  transition: top var(--transition);
}
.skip-link:focus { top: 12px; }

/* ---- base/typography.css ---- */
/* ---- typography ----------------------------------------------------------
   Every size is fluid via clamp(), so there is not a single font-size media
   query in this stylesheet. See tokens.css for how each clamp was solved
   against the original's measured values.
   -------------------------------------------------------------------------- */

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 1rem;
  color: var(--ink);
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  line-height: var(--lh-display);
}

/* Display type. The original set these as `text-uppercase` on mixed-case
   source text, so the accessible name and the copy-pasted text stay
   "The County Lads" rather than shouting. Kept that way. */
.display {
  display: block;
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  line-height: var(--lh-display);
  text-transform: uppercase;
}
.display--xl { font-size: var(--fs-display-xl); }
.display--lg { font-size: var(--fs-display-lg); }
.display--md { font-size: var(--fs-display-md); }
.display--sm { font-size: var(--fs-display-sm); }

/* Long-form copy is Fahkwang on this site, not the UI font — measured
   15px / 26.25px on the original's paragraph blocks. The Our Story biography
   is the exception, 20px / 30px at weight 600: see components/story.css. */
.prose {
  font-family: var(--font-display);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}
.prose p { margin: 0 0 1.5rem; }
.prose p:last-child { margin-bottom: 0; }
.prose strong { font-weight: var(--fw-bold); }
.prose em { font-style: italic; }

/* Small grey caption under a photograph — "Photo AlZon Photography". */
.caption {
  margin: 14px 0 0;
  font-family: var(--font-display);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--muted);
}

/* Column label in the footer: Inter 17/600, #777. */
.label {
  margin: 0 0 14px;
  font-family: var(--font-ui);
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  color: var(--muted);
}

/* ---- base/layout.css ---- */
/* ---- layout primitives ---------------------------------------------------
   Container measured at 1404px with 18px of padding each side, giving the
   1368px content width every heading on the original lines up against.

   The grids use auto-fit + minmax, so they reflow on their own at every width.
   There are no media queries in this file and none are needed.
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* The theme's vertical rhythm, kept: quad top / double bottom for a section
   heading, no top / quad bottom for the content that follows it. */
.section { position: relative; padding: var(--space-4) 0; }
.section--head    { padding: var(--space-4) 0 var(--space-2); }
.section--body    { padding: 0 0 var(--space-4); }
.section--tight   { padding: var(--space-2) 0; }
.section--flush   { padding: 0; }

.section--light { background: var(--white); color: var(--black); }
.section--soft  { background: var(--grey-soft); color: var(--black); }
.section--dark  { background: var(--near-black); color: var(--white); }
.section--black { background: var(--black); color: var(--white); }

.section--dark h1, .section--dark h2, .section--dark h3,
.section--black h1, .section--black h2, .section--black h3 { color: var(--white); }

/* min(…, 100%) stops a column from ever being wider than its container, which
   is what would otherwise cause horizontal scroll on a small phone. */
.grid { display: grid; gap: var(--space-2); }

/* Two-column content splits. Collapse by width, not by breakpoint: below about
   760px the pair no longer fits and stacks itself. */
.split {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 4.5rem);
  align-items: center;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}
.split--top { align-items: start; }

/* ---- components/button.css ---- */
/* ---- buttons -------------------------------------------------------------
   The original had no button styling worth reproducing: its only "button" was
   `+ Read More`, a link with `href="#"` that did nothing (see DEFECTS B1).

   These are this rebuild's own, in the site's flat monochrome idiom — square
   corners, no shadow, 44px minimum height. They are used for the event pages'
   calendar links and the "all events" link.
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  padding: 10px 22px;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--fs-nav);
  font-weight: var(--fw-bold);
  line-height: 1.4;
  text-align: center;
  cursor: pointer;
  transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.btn:hover { background: var(--ink); color: var(--white); }

.btn--solid { background: var(--ink); color: var(--white); }
.btn--solid:hover { background: var(--black); color: var(--white); }

.btn--quiet {
  border-color: var(--line);
  color: var(--muted);
}
.btn--quiet:hover { background: var(--grey-soft); color: var(--ink); border-color: var(--ink); }

/* "All events →" with a sliding arrow. */
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-family: var(--font-ui);
  font-size: var(--fs-nav);
  font-weight: var(--fw-bold);
  color: var(--ink);
}
.arrow-link svg { flex: none; transition: transform var(--transition); }
.arrow-link:hover svg { transform: translateX(6px); }

/* ---- components/cursor.css ---- */
/* ---- custom cursor -------------------------------------------------------
   Measured on the original (`#uncode-custom-cursor.diff-style`):

     position: fixed; left: 0; top: 0; z-index: 2147483647;
     pointer-events: none; mix-blend-mode: difference;
     transform: translate3d(x, y, 0)   ← set by JS each frame, from mousemove
     transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1)
     child: 20 x 20, background #fff, border-radius 50%,
            transform translate(-10px, -10px)

   Net effect: a 20px white dot that trails the pointer with a 0.4s eased lag
   and inverts whatever is beneath it — black over the white gallery, white
   over the hero photograph. It does NOT change size over links; that was
   measured too (20 x 20 both over a nav link and over empty space).

   The trailing is a CSS transition on the wrapper, as on the original. The
   script writes the new position at most once per frame, in
   requestAnimationFrame, as the original's did; scripts/70-cursor.js says why
   (Safari, measured).

   The original gated its cursor rules behind
   `screen and (hover: hover) and (pointer: fine)`; the same gate is used here,
   in the script as well as the stylesheet.
   -------------------------------------------------------------------------- */

.cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 2147483647;
  pointer-events: none;
  mix-blend-mode: difference;
  opacity: 0;
  transition: transform var(--dur-cursor) var(--ease-cursor), opacity 0.25s ease-in;
  will-change: transform;
}

.cursor-dot::before {
  content: "";
  display: block;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--white);
  transform: translate(-10px, -10px);
}

.cursor-dot.is-visible { opacity: 1; }

/* No pointer to decorate on touch, and a lagging dot with reduced motion
   requested is exactly the thing being asked not to do. */
@media (hover: none), (pointer: coarse) {
  .cursor-dot { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cursor-dot { display: none; }
}

/* ---- components/header.css ---- */
/* ---- header --------------------------------------------------------------
   Measured on the original: 272px tall (a 200px logo with 36px above and
   below), position: absolute, fully transparent, sitting over the hero.

   It is NOT sticky and it does NOT shrink — confirmed by scrolling the
   original to 300 / 1200 / 3000px and re-measuring: the header stayed at
   top:-scrollY with an unchanged 272px height and a transparent background
   throughout. Reproducing that faithfully means there is no scroll listener
   here at all on desktop.
   -------------------------------------------------------------------------- */

/* ---- stacking, and why these numbers ------------------------------------
   .site-header carries a z-index, so it is a stacking context and everything
   inside it — including the drawer — is capped at the header's own level no
   matter how large the child's z-index is. The backdrop is a SIBLING of the
   header, so the two are compared at the root:

       .site-header   1100   header, and the drawer inside it
       .nav-backdrop  1090   the wash over the page
       page content   auto

   Get this the wrong way round and the backdrop paints over the open drawer:
   the menu sits under a black wash and every tap on it lands on the backdrop,
   which closes the menu. That is not visible in a screenshot and a scripted
   .click() will not catch it either — it was found by tinting the backdrop red
   and by hit-testing with elementFromPoint.

   The header staying above the wash is intentional: the logo and the close
   button remain legible while the drawer is open. */
.site-header {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 1100;
  background: transparent;
}

/* The transparent header's white 14px/600 links sit directly on the hero
   photograph, whose top third is bright sky — measured about 1.4:1 against
   white, far under the 4.5:1 AA needs. The original had this problem too and
   simply lived with it; reproducing an unreadable menu is not fidelity.

   A scrim anchored to the top of the header, fading out below the nav, brings
   it over 4.5:1 without darkening the photograph the way a full-height overlay
   would. The nav sits about 43% down the scrim, where the gradient is ~0.51 —
   measured 5.2:1 against the brightest sky in the photograph. Scoped so the
   solid variant never gets it. */
.site-header:not(.site-header--solid)::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: calc(var(--header-h) + 40px);
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 50%, rgba(0, 0, 0, 0) 100%);
  pointer-events: none;
}

@media (max-width: 959px) {
  .site-header:not(.site-header--solid)::before {
    height: calc(var(--header-h-mobile) + 32px);
  }
}

/* Everything in the header sits above its own scrim. */
.header-inner { position: relative; z-index: 1; }

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  min-height: var(--header-h);
  padding-top: var(--space-1);
  padding-bottom: var(--space-1);
}

.brand { flex: none; display: block; }
.brand img {
  width: auto;
  height: var(--logo-h);     /* 200px, measured — renders 197 x 200 */
}

.header-nav {
  display: flex;
  align-items: center;
  gap: 28px;
}

.nav-list {
  display: flex;
  align-items: center;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Inter 14/600, white, padding 0 14px — measured. The vertical padding is
   this rebuild's own: the original's links were 84 x 272 because they filled
   the header row, which is a fine target, but stating it explicitly keeps it
   true once the header stops being 272px tall on a phone. */
.nav-list a {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
  font-family: var(--font-ui);
  font-size: var(--fs-nav);
  font-weight: var(--fw-bold);
  color: var(--white);
}
.nav-list a:hover { opacity: 0.65; }

.header-social {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The original's social links were 25 x 272 with an 18px glyph. 18px is kept;
   the box is squared up to 44 so it is a real target once the header is short. */
.social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--white);
}
.social-link svg { width: 18px; height: 18px; display: block; }
.social-link:hover { opacity: 0.65; }

/* ---- drawer toggle ------------------------------------------------------ */

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--white);
  cursor: pointer;
}

.nav-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1090;                 /* below .site-header's 1100 — see above */
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--transition), visibility var(--transition);
}
.nav-backdrop.is-open { opacity: 1; visibility: visible; }

/* ---- the one structural breakpoint --------------------------------------
   959px, matching the original, where the horizontal nav becomes an
   off-canvas drawer and the logo drops from 200px to 50px. Media queries
   cannot read custom properties, so --bp-nav documents the value and this
   repeats it literally. */
@media (max-width: 959px) {
  .header-inner {
    min-height: var(--header-h-mobile);
    padding-top: 27px;
    padding-bottom: 27px;
  }

  .brand img { height: var(--logo-h-mobile); }   /* 50px, measured */

  .nav-toggle { display: inline-flex; }

  .header-nav {
    position: fixed;
    top: 0; right: 0; bottom: 0;
    z-index: 1100;
    width: min(360px, calc(100vw - 45px));
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: 84px 28px 32px;
    background: var(--footer);
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(100%);
    visibility: hidden;
    transition: transform 0.32s ease, visibility 0.32s ease;
  }
  .header-nav.is-open { transform: none; visibility: visible; }

  .nav-list { flex-direction: column; align-items: stretch; }
  .nav-list a {
    min-height: 52px;
    padding: 0;
    font-size: 1.0625rem;
    border-bottom: 1px solid var(--line-dark);
  }

  /* The original's drawer carried the Uncode demo placeholder contact block,
     live on every page. Replaced with the band's real addresses. The fake
     values themselves are recorded in .scrape/DEFECTS.md A2, not here — a
     comment ships to every visitor, and a decoy address in the stylesheet is
     still a decoy address on the site. */
  .drawer-contact {
    margin-top: auto;
    padding-top: 28px;
    color: var(--white);
    font-family: var(--font-ui);
  }
  /* Same reason as the footer: the shared .label grey is tuned for light
     backgrounds and does not clear 4.5:1 on this band. See tokens.css. */
  .drawer-contact .label { color: var(--muted-on-dark); }
  .drawer-contact p { margin: 0 0 4px; }
  .drawer-contact a { color: var(--white); text-decoration: underline; }

  .header-social { margin-top: 24px; }
  .social-link { color: var(--white); }

  .nav-close {
    position: absolute;
    top: 22px; right: 20px;
    width: 44px; height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--white);
    cursor: pointer;
  }
}

@media (min-width: 960px) {
  .drawer-contact,
  .nav-close { display: none; }
}

/* ---- solid variant -------------------------------------------------------
   The header is transparent by design, which works because the homepage puts
   a photograph behind it. On the events listing, an event page or the 404
   there is white behind it instead, and white 14/600 links on white is
   invisible — so those pages get the footer's own dark band at the same
   272px height. build.js picks the variant from the page's `hero` flag.
   -------------------------------------------------------------------------- */

.site-header--solid { background: var(--footer); }

/* ---- components/hero.css ---- */
/* ---- hero ----------------------------------------------------------------
   Full-viewport black band with the band photograph as a cover background at
   50% 50%. Measured: 1006px tall at a 1006px viewport height, i.e. 100vh,
   background-attachment: scroll — there is no parallax on the original.

   The header sits over it (position: absolute, transparent), so the hero's
   content is pushed down by the header height rather than the header pushing
   the hero down.
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 100svh;
  padding: calc(var(--header-h) + var(--space-2)) 0 var(--space-4);
  background-color: var(--black);
  color: var(--white);
  overflow: hidden;
}

.hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: url("/assets/img/band-walking-field.jpg");
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
}

.hero .container {
  position: relative;
  z-index: 1;
}

/* The original had no scrim: the photograph's sky is bright and the white type
   sits directly on it. A bottom-weighted wash is added here so the headline
   keeps its contrast at small sizes, where the type no longer covers enough of
   the image to read on its own. The TOP is left clear — the header draws its
   own scrim there (see header.css) and two would stack. */
.hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.3) 100%);
}

@media (max-width: 959px) {
  .hero { padding-top: calc(var(--header-h-mobile) + var(--space-2)); }
}

/* The mobile layout in portrait (a review of the staged site, 2026-10-13): the
   band's name sits just above the band's heads, in the sky, in the dark
   heading grey, and the white tagline starts just below their chins. Centred
   with the tagline, the name had landed on their faces at every size from
   360 x 640 to 768 x 1024.

   Both lines come from the photograph: 1067 x 1600, the tallest head's top at
   y 621, the chins at y 700. background-size: cover at 50% 50% scales it by
   s = max(width / 1067, height / 1600) about the hero's centre, so photo row y
   lands at 50% of the hero's height - (800 - y) * s. The hero is 100svh tall
   on these screens, so:
     heads  50svh - max(16.78vw, 11.19svh)    179 x 100 / 1067, 179 x 100 / 1600
     chins  50svh - max(9.37vw, 6.25svh)      100 x 100 / 1067, 100 x 100 / 1600
   Row one of the title runs from the top of the content box (the padding
   above) to 16px above the heads, and the name sits at its foot. Landscape is
   left as it was: a phone on its side shows the heads behind the header, with
   no sky above them to hold the name. */
@media (max-width: 959px) and (orientation: portrait) {
  .hero {
    --hero-heads: calc(50svh - max(16.78vw, 11.19svh));
    --hero-chins: calc(50svh - max(9.37vw, 6.25svh));
    align-items: stretch;
  }
  .hero .section-title--flip {
    display: grid;
    grid-template-rows: minmax(min-content, calc(var(--hero-heads) - 16px - var(--header-h-mobile) - var(--space-2))) auto;
    align-content: start;
  }
  .hero .section-title--light .section-title__eyebrow {
    align-self: end;
    color: var(--ink);
  }
  .hero .section-title__main {
    margin-top: calc(var(--hero-chins) - var(--hero-heads) + 24px);
  }
}

/* ---- components/section-title.css ---- */
/* ---- the staggered section title ----------------------------------------
   The signature of this design, and the thing most easily got wrong.

   Section titles are two lines, oppositely aligned, both spanning the full
   1368px content width so the alignment itself creates the stagger:

       THE COUNTY LADS                        ← 100px, left
                                  GALLERY     ← 125px, right

   Measured on the original: eyebrow left edge = column left; title right edge
   = column right. Nothing is indented — the alignment does all of it.

   The hero inverts the pattern (eyebrow right, subtitle left) via
   .section-title--flip, and "Our Story" uses a single line with no eyebrow.
   On the mobile layout "On Tour" is centred under its eyebrow instead
   (.section-title--centre-mobile, at the end of this file).
   -------------------------------------------------------------------------- */

.section-title {
  margin: 0;
  color: var(--ink);
}

.section-title__eyebrow {
  font-size: var(--fs-display-lg);   /* 35 → 100px */
  text-align: left;
}

.section-title__main {
  font-size: var(--fs-display-xl);   /* 35 → 125px */
  text-align: right;
}

/* Hero: the same two lines with the alignments swapped. */
.section-title--flip .section-title__eyebrow { text-align: right; }
.section-title--flip .section-title__main {
  font-size: var(--fs-display-sm);   /* 33.3 → 50px */
  text-align: left;
}

/* "Our Story" — one line, left, 75px, no eyebrow. */
.section-title--single {
  font-size: var(--fs-display-md);   /* 35 → 75px */
  text-align: left;
}

.section-title--light,
.section-title--light .display { color: var(--white); }

/* "On Tour" on the mobile layout: centred in the column, under "The County
   Lads", rather than right-aligned (a review of the staged site, 2026-10-13).
   "Gallery" keeps the stagger; the review asked only for this one. */
@media (max-width: 959px) {
  .section-title--centre-mobile .section-title__main { text-align: center; }
}

/* ---- components/music.css ---- */
/* ---- music / new release -------------------------------------------------
   Measured: a full-viewport band, background #101213, split 50/50. Left half
   is the dark text panel (heading, Spotify embed, copy); right half is
   05TheCountyLads-173-copy.jpg as a cover background, 1338 x 1006.

   Built as a two-column grid rather than a background on the row, so the photo
   half can become a real block above the text on a phone instead of a
   letterboxed strip.
   -------------------------------------------------------------------------- */

.music {
  background: var(--near-black);
  color: var(--white);
}

.music__inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  align-items: stretch;
  min-height: 100svh;
}

.music__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-4) clamp(1.25rem, 0.5rem + 3.5vw, 4.5rem);
}

.music__figure {
  background-image: url("/assets/img/band-173.jpg");
  background-size: cover;
  background-position: 50% 50%;
  min-height: clamp(260px, 45vw, 100%);
}

.music__title { color: var(--white); }

/* The original ran the player and the copy side by side inside the left half.
   The minimum and gap are tuned so the pair still fits at a 1440px viewport,
   where the panel's content box is about 600px, and stacks below that. */
.music__body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(258px, 100%), 1fr));
  gap: var(--space-1);
  align-items: start;
}

/* The Spotify player keeps the iframe's own 12px radius, from the original's
   inline style. aspect-ratio holds the 352px height without a fixed height
   that would letterbox on a narrow column. */
.music__embed {
  width: 100%;
  height: 352px;
  max-width: 500px;
  border: 0;
  border-radius: var(--radius-embed);
  display: block;
}

.music__copy { color: var(--white); }

/* ---- components/gallery.css ---- */
/* ---- gallery + lightbox --------------------------------------------------
   The original used Isotope for a masonry layout — 3 columns of 456px tiles
   inside the 1332px column, images 418px wide with a 19px gutter each side,
   variable heights (315 / 316 / 366 / 552px observed).

   Reproduced with CSS multi-column, which gives true masonry with the
   photographs' natural aspect ratios and no JavaScript at all. Column count
   falls out of the column width, so there is no breakpoint here either.

   Deliberate difference: CSS columns fill top-to-bottom per column, where
   Isotope filled roughly left-to-right. DOM order is unchanged, so keyboard
   and screen-reader order — and the lightbox's next/previous order — still
   follow the order the band arranged the photographs in.
   -------------------------------------------------------------------------- */

.gallery + .gallery { margin-top: var(--space-3); }

/* Inter 29/600 — measured. The photographer credit, not decoration. */
.gallery__credit {
  margin: 0 0 var(--space-2);
  font-family: var(--font-ui);
  font-size: var(--fs-h2);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  color: var(--black);
}

/* The original ran 3 columns of 456px tiles inside the 1332px content column.
   `column-width` is a target, not a maximum — the browser fits as many columns
   of at least that width as it can — so the clamp is what actually sets the
   count: 430px against a 1368px container gives 3, and it steps down to 2 and
   then 1 on its own. Measured ladder: 3 at 1440, 2 at 1024, 2 at 768, 1 at 360. */
.gallery-grid {
  column-width: clamp(280px, 30vw, 430px);
  column-gap: 38px;              /* 19px each side, measured */
}

.gallery-item {
  display: block;
  width: 100%;
  margin: 0 0 38px;
  padding: 0;
  border: 1px solid var(--line);  /* the original's .tmb-bordered */
  background: var(--grey-soft);
  cursor: pointer;
  break-inside: avoid;
  /* Chrome needs this too, or a tile can still be split across columns. */
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;
  overflow: hidden;               /* clips the hover zoom, as the original did */
  line-height: 0;
}

.gallery-item img {
  width: 100%;
  height: auto;
  /* Measured on the original: scale(1.05) over 0.3s
     cubic-bezier(0.57, 0.21, 0.69, 1). */
  transition: transform var(--dur-zoom) var(--ease-zoom);
}
.gallery-item:hover img,
.gallery-item:focus-visible img { transform: scale(1.05); }

@media (hover: none) {
  .gallery-item:hover img { transform: none; }
}

/* ---- lightbox -----------------------------------------------------------
   Replaces lightGallery. Scoped per gallery so next/previous never crosses
   from one photographer's set into the other's. */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: clamp(1.25rem, 0.5rem + 3vw, 2.5rem) 1.25rem;
  background: rgba(9, 10, 11, 0.95);
}
.lightbox.is-open { display: flex; }

.lightbox img {
  max-width: min(1200px, 92vw);
  max-height: 82vh;
  width: auto;
  object-fit: contain;
}

.lightbox__cap {
  position: absolute;
  bottom: 22px; left: 0; right: 0;
  text-align: center;
  color: rgba(255, 255, 255, 0.8);
  font-family: var(--font-ui);
  font-size: var(--fs-small);
}

.lightbox__btn {
  position: absolute;
  width: 48px; height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: rgba(255, 255, 255, 0.1);
  color: var(--white);
  cursor: pointer;
  transition: background var(--transition);
}
.lightbox__btn:hover { background: rgba(255, 255, 255, 0.22); }
.lightbox__close { top: 22px; right: 22px; }
.lightbox__prev { left: 22px; top: 50%; transform: translateY(-50%); }
.lightbox__next { right: 22px; top: 50%; transform: translateY(-50%); }

/* ---- components/story.css ---- */
/* ---- our story -----------------------------------------------------------
   Photograph left with its credit caption, prose right. Measured on the
   original at 1440px (2026-10-13): image 396px wide from x 54, text from x 522
   to 1386 — a 72px gap, then an 864px text column. The same at 2683px.

   The image column is the photo's own width, not half the row. Two equal grid
   columns put the 396px photo in a 630px cell, 306px from the text — the
   "lot of space between the image and the Story content" in the review of
   2026-10-13. Flex rather than grid so the pair still stacks by itself, with
   no media query, once 396px + the gap + 340px of text no longer fit.

   The original wrapped the entire 1,300-character biography in a single <h5>,
   20px / 30px at weight 600 at every width (measured at 390, 768, 1440 and
   2683). Here it is four paragraphs of prose with the text unchanged, at that
   size.
   -------------------------------------------------------------------------- */

.story {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(2rem, 1rem + 4vw, 4.5rem);   /* 72px from a 1400px viewport up */
  align-items: flex-start;
}

.story__figure {
  flex: 0 1 396px;       /* measured display width */
  min-width: 0;
  margin: 0;
}
.story__figure img {
  width: 100%;
  height: auto;
}

.story__copy {
  flex: 1 1 340px;
  min-width: 0;
  color: var(--black);
  font-size: var(--fs-story);
  font-weight: var(--fw-bold);
  line-height: var(--lh-story);
}

/* ---- components/events.css ---- */
/* ---- events --------------------------------------------------------------
   Replaces the band that showed a literal `[tribe_events view="list"]` on the
   live homepage. Everything here is generated by build.js from
   content/events.json — see .scrape/DEFECTS.md A1.
   -------------------------------------------------------------------------- */

.events { margin: 0; padding: 0; list-style: none; }

.event-row {
  padding: 0;
  border-top: 1px solid var(--line);
}
.events > .event-row:last-child { border-bottom: 1px solid var(--line); }

/* The whole row is the link, so the target is the row rather than the title.
   The grid lives on the ANCHOR, not the <li>: `display: contents` on an <a>
   leaves it with no box at all, which makes it unfocusable and leaves a focus
   ring nothing to draw around — all 102 event links were unreachable by
   keyboard. Measured before the fix: getBoundingClientRect() 0x0,
   .focus() did not move activeElement. */
.event-row__link {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(1rem, 0.5rem + 1.6vw, 2rem);
  padding: 22px 0;
  color: inherit;
}
.event-row:hover { background: var(--grey-soft); }

.event-date {
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  line-height: 1.05;
  text-align: center;
  color: var(--ink);
}
.event-date__month {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.event-date__day {
  display: block;
  font-size: 2.25rem;
}
.event-date__year {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: var(--fw-body);
  color: var(--muted);
}

.event-row__body { min-width: 0; }

.event-row__title {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-size: clamp(1.125rem, 1rem + 0.55vw, 1.5rem);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--ink);
}

.event-row__meta {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--muted);
}

.event-tag {
  flex: none;
  padding: 5px 12px;
  border: 1px solid var(--line);
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.event-row__go {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--ink);
}
.event-row:hover .event-row__go { transform: translateX(4px); }
.event-row__go { transition: transform var(--transition); }

/* On a narrow screen the three columns become date-above-body, with the
   chevron dropped — the whole row is still the link. */
@media (max-width: 599px) {
  .event-row__link {
    grid-template-columns: 64px minmax(0, 1fr);
    gap: 16px;
  }
  .event-row__go { display: none; }
  .event-date__day { font-size: 1.75rem; }
}

/* ---- list section headings and empty state ------------------------------ */

.events-heading {
  margin: 0 0 8px;
  font-family: var(--font-ui);
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.events-empty {
  margin: 0 0 var(--space-2);
  padding: 26px 0;
  font-family: var(--font-display);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--muted);
  border-top: 1px solid var(--line);
}

.events-more {
  margin-top: var(--space-2);
  text-align: right;
}

/* ---- single event page --------------------------------------------------- */

.event-page { padding-top: calc(var(--header-h) + var(--space-2)); }
@media (max-width: 959px) {
  .event-page { padding-top: calc(var(--header-h-mobile) + var(--space-2)); }
}

.event-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  font-family: var(--font-ui);
  font-size: var(--fs-nav);
  font-weight: var(--fw-bold);
  color: var(--muted);
}
.event-back:hover { color: var(--ink); }

.event-hero {
  display: grid;
  gap: clamp(1.5rem, 1rem + 3vw, 3.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  align-items: start;
  margin-top: var(--space-2);
}

.event-hero__figure { margin: 0; }
.event-hero__figure img { width: 100%; height: auto; border: 1px solid var(--line); }

.event-title {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  font-weight: var(--fw-bold);
  line-height: var(--lh-display);
  text-transform: uppercase;
  color: var(--ink);
}

.event-when {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: 1.125rem;
  line-height: var(--lh-tight);
  color: var(--black);
}

.event-passed {
  display: inline-block;
  margin: 0 0 18px;
  padding: 6px 14px;
  background: var(--grey-soft);
  border: 1px solid var(--line);
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.event-details {
  margin: 0 0 var(--space-2);
  padding: 0;
  border-top: 1px solid var(--line);
}
.event-details div {
  display: grid;
  grid-template-columns: minmax(120px, 26%) minmax(0, 1fr);
  gap: 12px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.event-details dt {
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.event-details dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--black);
}
/* Vertical padding on an inline element grows the hit area without changing
   the line box, so these clear the WCAG 2.2 24x24 minimum (20px → 28px) while
   still wrapping normally — which matters, because some organiser URLs are
   long and a non-wrapping target would push the page sideways at 360px. */
.event-details a {
  padding-block: 4px;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.event-details a:hover { color: var(--muted); }

.event-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: var(--space-2);
}

/* Previous / next, mirroring the original's « … » navigation. */
.event-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: var(--space-1);
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}
.event-nav a {
  display: block;
  min-height: 44px;
  font-family: var(--font-display);
  color: var(--ink);
}
.event-nav span {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.event-nav a:hover { color: var(--muted); }
.event-nav__next { text-align: right; }

/* ---- components/footer.css ---- */
/* ---- footer --------------------------------------------------------------
   Measured: two bands, both #141618. The upper one carries the contact
   columns at 144px top and bottom padding; the lower one carries the 333px
   logo badge and the copyright line.

   The original showed different footers on different pages — the homepage had
   Management + "Call For Details", every other page had Management + Booking.
   Booking enquiries were missing from the one page most visitors land on.
   One footer now, carrying all three. See .scrape/DEFECTS.md B5.
   -------------------------------------------------------------------------- */

.site-footer {
  background: var(--footer);
  color: var(--white);
}

/* The shared .label token is tuned for light backgrounds; on this band it needs
   the lighter grey to clear 4.5:1. See tokens.css. */
.site-footer .label { color: var(--muted-on-dark); }

.footer-main {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: var(--space-2);
  padding: var(--space-4) 0;
}

.footer-col p { margin: 0 0 10px; }
.footer-col p:last-child { margin-bottom: 0; }

.footer-col a {
  display: inline-block;
  padding-block: 4px;          /* 28px hit area; see utilities.css */
  color: var(--white);
}
.footer-col a:hover { opacity: 0.7; }

.footer-social {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The original's footer social links were 18 x 18 — under the WCAG 2.2
   minimum of 24 x 24, and with no accessible name, so a screen reader
   announced four empty links. 44 x 44 with aria-labels. */
.footer-social .social-link {
  width: 44px;
  height: 44px;
  color: var(--white);
}

.footer-bottom {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--line-dark);
  text-align: center;
}

.footer-logo {
  display: inline-block;
  margin-bottom: var(--space-2);
}
/* Measured at 333 x 333 on the original. */
.footer-logo img {
  width: min(333px, 62vw);
  height: auto;
}

.footer-copy {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  color: var(--white);
}

/* ---- back to top ---------------------------------------------------------
   Measured on the original: 32 x 32, position: fixed, right 22px, bottom 22px,
   hidden until scrolled. The box is enlarged to 44 x 44 here so it clears the
   WCAG 2.2 minimum; the chevron inside stays the size it was.
   -------------------------------------------------------------------------- */

.to-top {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 900;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 0;
  background: rgba(20, 22, 24, 0.72);
  color: var(--white);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--transition), visibility var(--transition), background var(--transition);
}
.to-top.is-visible { opacity: 1; visibility: visible; }
.to-top:hover { background: var(--footer); }

/* ---- utilities.css ---- */
/* ---- utilities, motion, print ------------------------------------------- */

/* Any phone or mail link anywhere is a tap target, including one written
   directly into page content. Enforce the WCAG 2.2 minimum globally so a bare
   `tel:` can never ship too small to hit — the original's footer links were
   18 x 18.

   Vertical padding on an INLINE element, not inline-flex: an inline-flex box
   will not wrap its text, so a long address becomes one unbreakable run.
   `info@spencervillefair.ca` did exactly that and pushed one event page 3px
   sideways at 360px. Padding grows the hit area to 28px and leaves wrapping
   alone. */
a[href^="tel:"],
a[href^="mailto:"] {
  padding-block: 4px;
}

/* Scroll reveal. Hidden only when JS is present — with JS off, or if the
   observer never fires, content stays visible. 50-reveal.js also has a timed
   failsafe. An unreadable page is never an acceptable failure mode. */
.js .reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
.js .reveal.is-visible { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .js .reveal { opacity: 1; transform: none; }
}

@media print {
  .site-header,
  .site-footer,
  .nav-backdrop,
  .cursor-dot,
  .to-top,
  .lightbox,
  .skip-link { display: none; }

  body { color: #000; background: #fff; }
  .hero, .music { min-height: 0; }
  .hero__bg, .music__figure { display: none; }
  .section { padding: 1rem 0; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.75em; }
}

/* Two small layout helpers, so no page needs an inline style attribute —
   the CSP has no 'unsafe-inline' on style-src, because the fonts are
   self-hosted and nothing else needed it. */
.measure { max-width: 46rem; margin-top: 1.5rem; }

.link-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 2.5rem;
  margin-top: 2rem;
}
