/* ====================================================================
   FONTS — self-hosted (assets/fonts/), not Google Fonts.
   A real PageSpeed Insights run measured the old Google Fonts
   <link rel="stylesheet"> as ~780ms of render-blocking time (a second
   cross-origin connection to fonts.gstatic.com on top of the CSS fetch
   itself). Self-hosting removes both the extra origin and the
   render-blocking stylesheet request.

   font-display: optional (not swap): the project owner explicitly does
   not want any visible font swap/flash, even a brief one — optional
   tells the browser to use the fallback font for a given pageview if the
   real font isn't ready in time, rather than swapping it in mid-render.
   The font is expected to change again later, so this block is kept
   self-contained and easy to replace wholesale rather than fine-tuned.
   Only the Latin-1 (U+0000-00FF) glyphs actually needed for it/en/de
   content were kept — no cyrillic/greek/vietnamese subsets.
   ==================================================================== */
@font-face{
  font-family: 'Inter'; font-style: normal; font-weight: 400 600; font-display: optional;
  src: url('../assets/fonts/inter-var.woff2') format('woff2');
}
@font-face{
  font-family: 'Sora'; font-style: normal; font-weight: 600 700; font-display: optional;
  src: url('../assets/fonts/sora-var.woff2') format('woff2');
}
@font-face{
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: optional;
  src: url('../assets/fonts/ibmplexmono-400.woff2') format('woff2');
}
@font-face{
  font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: optional;
  src: url('../assets/fonts/ibmplexmono-500.woff2') format('woff2');
}

/* ====================================================================
   DESIGN TOKENS
   Palette sampled from assets/logo.png's dominant pixel colors
   (teal #588898, navy #284048, cream #f8e8b8), black-dominant per the
   brief — teal/cream/navy are accents only.
   ==================================================================== */
:root{
  --bg: #0a0a0a;
  --bg-2: #121212;
  --bg-3: #1a1a1a;
  --line: rgba(255,255,255,0.08);

  --text: #f5f2ea;
  --text-muted: #b8b6ad;

  --teal: #588898;
  --teal-soft: #8fb6c2;
  --teal-deep: #3a5f6b;

  --cream: #f8e8b8;
  --cream-soft: #fcf2d4;
  --cream-ink: #241d0f;

  --navy: #284048;
  --navy-soft: #3a5560;

  /* First error/warning color in the palette. Terracotta, not a pure red —
     reads as "warm/premium" rather than "alarm," matches the teal/cream/
     navy palette, and clears WCAG AA (4.5:1) against all three dark
     backgrounds: ~6.4:1 on --bg, ~6.1:1 on --bg-2, ~5.6:1 on --bg-3. Used
     for the required-field asterisk, invalid-input borders, and
     contact-form error text — never as a button fill (no CTA should read
     as an error). */
  --error: #e2725b;

  --radius-sm: 2px;
  --radius-md: 6px;
  --radius-pill: 999px;

  --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px;
  --space-5: 40px; --space-6: 64px; --space-7: 96px;

  --nav-height: 64px;

  /* atan(0.043) ≈ 2.5° — a "few degrees" tilt on the section dividers
     (.section's clip-path, below), expressed in vw rather than px so the
     *angle* stays constant across viewport widths instead of looking
     steep on mobile and barely-there on desktop (angle = atan(tilt /
     renderedWidth), and a full-bleed section's width ≈ 100vw). clamp()'d
     so it doesn't get absurdly large on ultra-wide monitors or
     vanishingly small on narrow phones. */
  --section-tilt: clamp(18px, 4.3vw, 64px);
}

*{ box-sizing: border-box; }
html{ scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior: auto; }
  *{ animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

body{
  margin: 0;
  background: var(--bg);
  color: var(--text-muted);
  font-family: 'Inter', sans-serif;
  /* dvh (dynamic viewport height) after the vh fallback — see the
     .hero rule below for why plain vh causes a visible jump/gap on
     mobile as the browser's address bar shows/hides during scroll. */
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
body.no-scroll{ overflow: hidden; }

h1, h2, h3{ color: var(--text); margin: 0; }
a{ color: var(--teal-soft); }
img{ max-width: 100%; display: block; }

.section-inner{ max-width: 1080px; margin: 0 auto; padding: 0 20px; }
.section-title{
  font-family: 'Sora', sans-serif; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.02em; font-size: clamp(24px, 4vw, 34px); margin-bottom: var(--space-4);
}
.section-subtitle{ font-size: 15px; line-height: 1.6; max-width: 620px; margin: -8px 0 var(--space-4); }
/* Section-internal kicker heading — one step below .section-title, for
   labelling a group *within* a section without reading as a second
   stacked section. Shared by Shows' "Past shows" heading and the
   "Why Choose Us" group inside #section-book. */
.section-subheading{
  margin: var(--space-5) 0 var(--space-3); font-family: 'Sora', sans-serif; font-size: 15px;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;
}

/* ====================================================================
   HERO (design/app.js orchestrates; hero/hero.js drives the video)
   ==================================================================== */
.hero{
  position: relative;
  /* dvh after the vh fallback — plain vh is computed against the
     "large" viewport (address bar collapsed) in Android Chrome/iOS
     Safari, so as the toolbar shows/hides while scrolling near the
     top of the page, a vh-sized .hero doesn't track the real visible
     height, and the mismatch reveals a gap of body's background (or
     causes a visible reflow "jump") right at .hero's lower edge — real
     regression reported 2026-08-21, reproduced on Android Chrome.
     svh (not dvh) tracks that fix now: 2026-08-23 traded dvh for svh
     here specifically because .hero-logo (design/style.css, near
     .intro-section) started straddling this exact boundary as a
     hard-edged badge — dvh recalculates continuously as the toolbar
     itself animates during a scroll gesture, which reflows .hero's
     height in real time and made the badge visibly snap/jump between
     positions (reported on a real device 2026-08-23). svh is static —
     based on the smallest possible viewport (toolbar fully shown) — so
     .hero's height, and therefore the seam the badge sits on, never
     moves mid-scroll. Trade-off: when the toolbar is hidden (actual
     viewport taller than svh), slightly more of #section-intro is
     visible in the same screen alongside the hero — a static framing
     difference, not a jump, and the intended one per web.dev's own
     guidance on this exact dvh trade-off. Browsers without svh support
     simply ignore that line and keep the vh value above it. */
  min-height: 90vh;
  min-height: 90svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: #000;
  /* No overflow:hidden here: .hero-video-wrap already clips the
     background <video> on its own. .site-nav no longer needs .hero's
     overflow behavior now that it's position:fixed (escapes .hero's box
     entirely) rather than position:sticky. */
}
@media (max-width: 640px){
  .hero{ min-height: 68vh; min-height: 68svh; }
}

.hero-media{ position: absolute; inset: 0; z-index: 0; }
.hero-video-wrap{ position: absolute; inset: 0; overflow: hidden; }
.hero-video-wrap video{
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; pointer-events: none;
}
.hero-poster{ position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: #000; transition: opacity 0.25s ease; }
/* Shown until the real Firebase Storage header picture loads (or if it
   never does) — the band's own logo, letterboxed and centered rather
   than stretched full-bleed like the eventual real photo. Only
   object-fit changes here, never the box's own width/height/position —
   a real PageSpeed Insights run measured a CLS of 0.270 from an earlier
   version that shrank the box itself (40%/40%/margin:auto) for the
   placeholder look; the box now stays 100%x100% at all times so the
   swap to the real photo repaints in place instead of visibly resizing. */
.hero-poster.hero-poster-placeholder{ object-fit: contain; }
.hero-scrim{
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.55) 65%, rgba(0,0,0,0.92) 100%);
}
.hero-mute-btn{
  position: absolute; top: calc(var(--nav-height) + var(--space-4)); right: var(--space-4); z-index: 3;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.5); border: 1px solid var(--line); color: var(--text);
  font-size: 17px; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
/* Hamburger toggle, top-left, clear of the mute button pinned top-right.
   Used to also wrap a small brand-mark image (.brand-mini-logo) — removed
   per the owner's request; .hero-logo now lives in #section-intro instead. */
.brand-bar{
  position: absolute; top: calc(var(--nav-height) + var(--space-4)); left: var(--space-4); z-index: 4;
  display: flex; align-items: center; gap: var(--space-3);
}

/* nav bar (desktop) / hamburger drawer (mobile) — design/nav.js */
.nav-toggle{
  width: 44px; height: 44px; border-radius: var(--radius-sm); background: rgba(10,10,10,0.5);
  border: 1px solid var(--line); display: none; flex-direction: column; align-items: center;
  justify-content: center; gap: 5px; cursor: pointer; flex-shrink: 0;
}
.nav-toggle .bar{ width: 20px; height: 2px; background: var(--text); transition: transform 0.2s, opacity 0.2s; }
.nav-toggle.open .bar:nth-child(1){ transform: translateY(7px) rotate(45deg); }
.nav-toggle.open .bar:nth-child(2){ opacity: 0; }
.nav-toggle.open .bar:nth-child(3){ transform: translateY(-7px) rotate(-45deg); }

.nav-overlay{
  position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 5;
  opacity: 0; pointer-events: none; transition: opacity 0.25s;
}
.nav-overlay.show{ opacity: 1; pointer-events: auto; }

/* Fixed to the very top of the viewport on desktop, for the entire page
   scroll — not position:sticky. .site-nav used to sit flush at the very
   bottom of .hero (a bounded ~90vh box) with zero slack beneath it, so
   it would "stick" for ~0px of scroll before scrolling away with the
   hero entirely. position:fixed escapes .hero's box altogether (fixed
   positioning is relative to the viewport, not the DOM parent, since
   .hero has no transform/filter/contain that would change that), so the
   bar now stays pinned for the whole page, and — since fixed elements
   sit at the viewport's top:0 immediately on load — it's also now
   visually at the top of the page from the very first paint, overlaying
   the hero. .hero-mute-btn and .brand-bar are both pushed down by
   --nav-height so neither sits underneath it (see their own rules).
   Mobile keeps the existing off-canvas drawer behavior entirely — the
   max-width:900px block below still fully overrides position/top/
   left/right/width for it. */
.site-nav{
  position: fixed; top: 0; left: 0; width: 100%; z-index: 20;
  display: flex; align-items: center; justify-content: center; gap: var(--space-5);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
  background: var(--bg-2);
}
/* Pushes .lang-switch (and the relocated social-icon cluster that
   follows it in the DOM) to the end of the bar without depending on
   .site-nav's centered justify-content — desktop only, see the mobile
   override below. Must stay before that @media block in source order:
   same-specificity rules resolve by document order, so if this sat
   after the mobile reset instead, this unconditional rule would win on
   mobile too and undo the reset. */
.lang-switch{ margin-left: auto; }
/* flex-wrap + a tighter gap: at 7 nav links (Home/About/Gallery/Book Us/
   Shows/FAQ/Contact — Video and Foto merged into one Gallery link), this
   still doesn't reliably fit one row at moderate desktop widths —
   wrapping to a second row beats overflowing or shrinking text
   unreadably small. */
.nav-links{ list-style: none; display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-3) var(--space-4); margin: 0; padding: 0; }
.nav-link{
  font-family: 'IBM Plex Mono', monospace; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--text-muted); text-decoration: none; padding-bottom: 4px;
  border-bottom: 2px solid transparent; transition: color 0.2s, border-color 0.2s;
}
.nav-link:hover, .nav-link.active{ color: var(--cream); border-color: var(--teal); }
.nav-close{ display: none; }

@media (max-width: 900px){
  .nav-toggle{ display: flex; }
  .nav-close{
    display: flex; position: absolute; top: var(--space-3); right: var(--space-3);
    width: 36px; height: 36px; background: transparent; border: none; color: var(--text);
    font-size: 22px; cursor: pointer; align-items: center; justify-content: center;
  }
  .site-nav{
    position: fixed; top: 0; left: auto; right: 0; height: 100vh; height: 100dvh; width: min(320px, 84vw);
    background: var(--bg-3); flex-direction: column; justify-content: flex-start;
    align-items: flex-start; gap: var(--space-5); padding: 80px var(--space-4) var(--space-4);
    transform: translateX(100%); transition: transform 0.3s ease; z-index: 6;
    border-top: none; border-bottom: none; border-left: 1px solid var(--line); overflow-y: auto;
  }
  .site-nav.open{ transform: translateX(0); }
  /* flex-wrap: nowrap overrides the desktop rule's wrap — in column mode,
     wrapping would spill extra items into a second column instead of just
     scrolling (overflow-y: auto above already handles a short viewport). */
  .nav-links{ flex-direction: column; flex-wrap: nowrap; gap: var(--space-4); }
  /* The lang-switch/social-icons pair stacks below the nav links inside
     the drawer, same as every other drawer child — the desktop-only auto
     margin that pushes them to the end of a horizontal bar doesn't apply
     here. */
  .lang-switch{ margin-left: 0; }
  /* The base rules' top: calc(var(--nav-height) + var(--space-4)) exists
     to clear the desktop .site-nav bar, which is position:fixed at the
     viewport's top:0 and actually occupies that space. On mobile,
     .site-nav is the off-canvas drawer above (translateX(100%) by
     default, out of the flow) — nothing sits at the top of the
     viewport to clear, so pushing these down by --nav-height too just
     moved the mobile "app bar" (the hamburger toggle + brand mark)
     down for no reason. Restored to their original, un-pushed top
     offset here. */
  .hero-mute-btn, .brand-bar{ top: var(--space-4); }
}

/* language toggle — reused inside the nav */
.lang-switch{
  position: relative; display: flex; width: max-content; border: 1px solid var(--line);
  border-radius: var(--radius-pill); overflow: hidden; font-family: 'IBM Plex Mono', monospace;
  font-size: 11px; letter-spacing: 0.05em;
}
.lang-indicator{
  position: absolute; top: 0; bottom: 0; left: 0; width: 33.333%; background: var(--teal);
  border-radius: var(--radius-pill); transform: translateX(0%); transition: transform 0.3s ease; z-index: 0;
}
.lang-switch.lang-en .lang-indicator{ transform: translateX(100%); }
.lang-switch.lang-de .lang-indicator{ transform: translateX(200%); }
.lang-switch a{
  position: relative; z-index: 1; display: inline-block; background: transparent; color: var(--text-muted);
  padding: 7px 14px; cursor: pointer; text-transform: uppercase; text-decoration: none;
}
.lang-switch a.active{ color: var(--cream-ink); font-weight: 600; }
.lang-switch a:not(.active):hover{ color: var(--text); }

/* Intro tagline — its own breathing room right below the hero/video.
   position:relative anchors .hero-logo below, which straddles this
   section's top edge (the seam against the hero). padding-top is widened
   beyond the base var(--space-6) to clear the logo's lower half (see that
   rule's comment for the math) plus a bit of breathing room before the
   tagline title. */
.intro-section{
  position: relative;
  padding: var(--space-6) var(--space-4);
  padding-top: calc(clamp(49px, 7.7vw, 77px) + var(--space-4));
  background: var(--bg); text-align: center;
}
.intro-section .section-inner{ max-width: 640px; margin: 0 auto; padding: 0; }
/* Centered exactly ON the horizontal line between the hero and this
   section, half overlapping each side — per the owner's request. top:0
   is the seam (this section's own top edge, since .intro-section is the
   positioned ancestor); translate(-50%,-50%) centers the image on that
   point using its OWN box (horizontally and vertically), which is robust
   regardless of its actual rendered size. logo-hero.webp is 480x336
   (aspect ratio 0.7), so at width clamp(140px,22vw,220px) its rendered
   height is clamp(98px,15.4vw,154px) — half of that is the
   clamp(49px,7.7vw,77px) figure .intro-section's padding-top compensates
   for above, so the tagline title below never collides with it. */
.hero-logo{
  position: absolute; top: 0; left: 50%; transform: translate(-50%, -50%); z-index: 2;
  width: clamp(140px, 22vw, 220px);
  filter: drop-shadow(0 8px 20px rgba(0,0,0,0.5));
}
.tagline-title{ font-family: 'Sora', sans-serif; font-weight: 600; font-size: 19px; color: var(--text); margin: 0 0 10px; }
.tagline-body{ font-size: 15px; line-height: 1.7; margin: 0; color: var(--text-muted); white-space: pre-line; }

/* ====================================================================
   ABOUT / FOR VENUES & EVENTS / FAQ / TESTIMONIALS — static markup, no
   dedicated JS module (the FAQ's expand/collapse is native
   <details>/<summary>, zero JS). Content owned by design/i18n.js like
   every other static section.
   ==================================================================== */
/* Small kicker labels over each paragraph — gives a skimming visitor
   something to jump their eyes to instead of three back-to-back
   paragraphs with no hierarchy. Shared by About and the "Why Choose Us"
   group inside For Venues & Events — the second real consumer of this
   pattern. Set in Sora (the same face as .section-title) rather than
   uppercase mono — the owner found the original monospace/all-caps
   treatment read as technical/coder-ish rather than the premium tone
   this site targets. */
.section-label{
  font-family: 'Sora', sans-serif; font-weight: 600; font-size: 13px;
  letter-spacing: 0.02em; color: var(--teal-soft); margin: var(--space-3) 0 6px;
}
.section-label:first-of-type{ margin-top: 0; }
.section-body{ font-size: 15px; line-height: 1.7; max-width: 720px; margin: 0 0 var(--space-3); }
.section-body:last-of-type{ margin-bottom: 0; }

.book-usecases{
  list-style: none; margin: 0 0 var(--space-4); padding: 0; max-width: 720px;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-2) var(--space-4);
}
.book-usecases li{ position: relative; padding-left: 18px; font-size: 14px; line-height: 1.5; color: var(--text); }
.book-usecases li::before{
  content: ''; position: absolute; left: 0; top: 0.5em; width: 6px; height: 6px;
  background: var(--teal); border-radius: 2px;
}

/* Same auto-fit grid shape as .book-usecases above, as a visual echo —
   these are supporting detail inside the venues pitch, not a section's
   worth of content, so they sit side by side rather than stacked.
   .book-why-item exists so each label+body stays one grid item (a flat
   grid would split every pair across two rows) — and so .section-label's
   :first-of-type / .section-body's :last-of-type rules keep resolving
   per pair, exactly as they do in #section-about. */
.book-why{
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4); align-items: start; margin: 0;
}
/* Overrides .section-subtitle's -8px top margin, which is tuned for a
   subtitle sitting directly under .section-title. Here the availability
   line closes the section instead, after .book-why — it needs real space
   above it, not negative space. Must stay after .section-subtitle in the
   file: same specificity, source order breaks the tie. */
.book-availability{ margin-top: var(--space-5); }

.faq-list{ display: flex; flex-direction: column; gap: var(--space-2); max-width: 760px; }
.faq-item{
  border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--bg-3);
  padding: var(--space-3) var(--space-4);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.faq-item:hover{ border-color: var(--teal-soft); box-shadow: 0 4px 16px rgba(0,0,0,0.18); }
.faq-item summary{
  cursor: pointer; list-style: none; position: relative; padding-right: 28px;
  font-family: 'Sora', sans-serif; font-weight: 600; font-size: 15px; color: var(--text);
  transition: color 0.2s ease;
}
.faq-item summary:hover{ color: var(--teal-soft); }
.faq-item summary::-webkit-details-marker{ display: none; }
.faq-item summary::after{
  content: '+'; position: absolute; right: 0; top: 0; font-size: 18px; line-height: 1;
  color: var(--teal-soft); transition: transform 0.25s ease, color 0.2s ease;
}
.faq-item[open] summary::after{ transform: rotate(45deg); }
.faq-item summary:hover::after{ color: var(--teal); }
/* The open/close expand-shrink animation itself is JS (faq/faq.js
   animates the closed <details>'s own `height`, via the Web Animations
   API) — CSS alone can't do this: a closed <details>'s non-summary
   content is suppressed by the browser in a way no CSS transition on
   the content itself (grid-template-rows, opacity, max-height, ...) can
   hook into, since the suppression/reveal happens as one discrete,
   unanimatable step, not a gradual property change. faq.js progressively
   enhances the native instant toggle and leaves it untouched when
   .animate() isn't available or prefers-reduced-motion is set. */
.faq-item p{ margin: var(--space-2) 0 0; font-size: 14px; line-height: 1.6; color: var(--text-muted); }
@media (prefers-reduced-motion: reduce){
  .faq-item, .faq-item summary, .faq-item summary::after{ transition: none; }
}

.testimonials-grid{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }

/* ====================================================================
   SECTIONS (Shows / Video / Foto / Contact) — parallax/parallax.js
   wires the .parallax-layer transform; sections alternate background
   for visual rhythm as you scroll.
   ==================================================================== */
/* scroll-margin-top: clicking a nav link no longer leaves the target
   section's heading hidden behind the fixed .site-nav bar.
   clip-path + the matching negative margin-top give every section a
   subtly tilted top edge instead of a flat horizontal seam — a skewY()
   transform on .section itself was considered and rejected: .section
   already needs overflow:hidden for .parallax-layer, and skewing it
   would distort that same clipping box into a parallelogram, cutting
   content at an angle on the left/right edges too (the usual fix,
   oversizing the section and wrapping the whole page in an
   overflow-x:hidden container, is a much bigger retrofit than this
   deserves). The negative margin is what makes the wedge the clip-path
   cuts away reveal the *previous* section's real background rather than
   the page's own base color — later siblings paint over earlier ones by
   default, and clip-path only removes paint from this section's own box,
   so the overlap zone shows through to whatever painted first. Every
   section tilts the same direction (down-left, up-right) — alternating
   the direction per section would read as busy/zig-zaggy rather than the
   subtle, professional effect asked for. */
.section{
  position: relative; padding: var(--space-7) 0; background: var(--bg);
  overflow: hidden; scroll-margin-top: var(--nav-height);
  /* section-tilt/section-tilt.js overrides this per-section on scroll
     (a direct inline clip-path write, not a custom property referenced
     from here — see that file's header comment for why), straightening
     the tilt as the section nears the fixed nav bar. This static value
     is what's visible before that script runs, or when it's disabled. */
  clip-path: polygon(0 var(--section-tilt), 100% 0, 100% 100%, 0 100%);
  margin-top: calc(-1 * var(--section-tilt));
}
.section.alt{ background: var(--bg-2); }
/* For a section whose top boundary must stay flat — either because
   nothing of a different shade sits above it, or (like #section-about,
   right after .intro-section) because the boundary above it is fragile
   for an unrelated reason and shouldn't shift position at all. */
.section-flat-top{ clip-path: none; margin-top: 0; }
.parallax-layer{ transform: translate3d(0, var(--parallax-offset, 0px), 0); will-change: transform; }

/* Shows — shows/shows.js (Firestore-backed: upcoming + past groups) */
/* :not([hidden]) — shows.js sets #shows-list-past's hidden attribute to
   true when there are zero past shows; an unconditional display here
   would override the browser's own [hidden]{display:none} rule and
   defeat that (same bug class already found/fixed on #contact-form-fields
   and .contact-form-submit — see contact/CLAUDE.md). */
.shows-list:not([hidden]){ list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.show-row{
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3);
  background: var(--bg-3); border: 1px solid var(--line); border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4); flex-wrap: wrap;
}
.show-datetime{ display: flex; flex-direction: column; gap: 2px; min-width: 120px; }
.show-date{ font-family: 'IBM Plex Mono', monospace; font-size: 12px; letter-spacing: 0.08em; color: var(--teal-soft); text-transform: uppercase; }
/* white-space: pre-line lets a multi-set show's `time` string (e.g.
   "1. 20:00 - 21:30\n2. 22:30 - 00:00") render as real line breaks —
   same font/size/color as a single-line time, just multi-line capable.
   A plain textContent with embedded "\n" would otherwise collapse to a
   single space, per normal HTML whitespace handling. */
.show-time{ font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--text-muted); white-space: pre-line; }
.show-location{ display: flex; flex-direction: column; gap: 2px; flex: 1 1 220px; }
/* Venue name + its Facebook/Instagram icons share one row (icons right
   after the name — owner's explicit ordering call, 2026-09-05);
   .show-address stays on its own line below, outside this wrapper. */
/* flex-wrap: nowrap keeps the social icons on the same line as the venue
   name no matter how long the name is (a real reported issue on a long
   venue name, 2026-09-05) — min-width: 0 on .show-venue is what actually
   makes that possible: without it, a flex item's default min-width:auto
   refuses to shrink below its text's natural width, forcing the icons
   off to their own line (or overflowing) instead of letting the name
   wrap internally. .show-venue is deliberately flex-grow: 0 (not 1) —
   growing it stretched its box to fill the row's full width on a wide
   card, leaving the icons pinned at the row's far end, visibly
   disconnected from the (much shorter) visible name text — a second
   real reported issue, same day. Flex-start packing (this row's default)
   already keeps the icons snug against the name without any grow at
   all; only trailing empty space is left unused, same as .show-address
   already does. .show-social-links gets flex: 0 0 auto so it never
   shrinks or wraps away either. align-items: center vertically centers
   the icon circles against the name's single line of text (a real
   reported misalignment when this was flex-start, 2026-09-05) — the
   trade-off, accepted for now, is a very long name wrapping to 2 lines
   would center the icons against the whole wrapped block rather than
   its first line specifically. */
.show-venue-row{ display: flex; align-items: center; gap: var(--space-2); flex-wrap: nowrap; }
.show-venue-row .show-venue{ flex: 0 1 auto; min-width: 0; }
.show-venue-row .show-social-links{ flex: 0 0 auto; }
.show-venue{ color: var(--text); font-weight: 500; }
.show-address{ color: var(--text-muted); font-size: 13px; }
/* An upcoming show's address is a Google Maps link + a copy-to-clipboard
   button (past shows keep the plain .show-address span above, untouched
   — directions/copying only matter before the show happens). Link stays
   the same muted color/size as the plain span (an address isn't a
   typical "click me" call to action; only the underline on hover signals
   it), overriding the site's default a{color:teal-soft}. */
.show-address-row{ display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.show-address-row .show-address{ color: var(--text-muted); font-size: 13px; text-decoration: none; }
.show-address-row .show-address:hover{ text-decoration: underline; }
.show-address-copy{
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; background: none; border: none; cursor: pointer;
  color: var(--text-muted); transition: color 0.15s;
}
.show-address-copy .icon{ width: 15px; height: 15px; fill: currentColor; }
.show-address-copy:hover{ color: var(--teal-soft); }
/* Small Facebook/Instagram icon-link pair, reused for both a show's venue
   (inside .show-venue-row) and its organizer (inside .show-organizer,
   below) — sized a little larger than .footer-made-with's compact icon
   treatment (28px vs. 22px) specifically to clear WCAG 2.2 SC 2.5.8's
   24x24px minimum touch-target size (2026-09-05 accessibility pass) —
   22px fell just short of it. Still well under the default 38px
   .contact-icon-small, appropriate for sitting inline next to small row
   text rather than as a standalone social row. */
.show-social-links{ display: flex; gap: var(--space-2); }
.show-social-links .contact-icon-small{ width: 28px; height: 28px; }
.show-social-links .contact-icon-small .icon{ width: 14px; height: 14px; }
/* .show-footer holds the organizer credit (bottom-left) and the ticket
   CTA (bottom-right, when present) on one shared row, appended after
   .show-description in shows.js — its own full-width flex line
   (flex: 1 1 100%) guarantees it always starts a fresh line at the
   bottom of the card, whether or not a description exists above it.
   space-between does the left/right split; with only one of the two
   children present, that child simply sits at the start (left). */
.show-footer{ display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-3); flex: 1 1 100%; }
/* The organizer credit line ("Evento organizzato da ...") plus its own
   socials, on one row — aligned with .show-location's own left edge
   (matches .show-datetime's min-width (120px) plus .show-row's gap
   (var(--space-3)), the same offset .show-location itself sits at).
   Only the organizer's own name (the <strong> buildOrganizerText() wraps
   it in) is bold/full-contrast — the surrounding phrasing stays small and
   muted, same tier as .show-address. */
.show-organizer{ display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--space-2); margin-left: calc(120px + var(--space-3)); }
.show-organizer-text{ color: var(--text-muted); font-size: 11.5px; }
.show-organizer-text strong{ color: var(--text); font-weight: 600; }
/* Left-aligns the description text with .show-location's venue/address
   column (not .show-datetime's date column) — matches .show-datetime's
   min-width (120px) plus .show-row's own gap (var(--space-3)), the same
   offset .show-location itself sits at. Applies inside #past-shows-overlay
   too (as of 2026-09-05) — an earlier pass had reset it flush-left there
   specifically, but the owner asked for the two to match after seeing the
   overlay in a real browser: .show-organizer already used this same
   alignment inside the overlay with no such reset, which read as
   inconsistent next to a flush-left description. */
.show-description{ flex: 1 1 100%; margin-left: calc(120px + var(--space-3)); }
/* The 2-line cap (max-height, not -webkit-line-clamp, which can't be
   transitioned) must stay UNCONDITIONAL, not gated behind .has-toggle:
   shows/shows.js's applyDescriptionTruncations() measures scrollHeight vs
   clientHeight to DECIDE whether a description needs a toggle, and that
   decision only works if the clamp — the thing that makes clientHeight
   report a genuine "2 lines" height instead of the full natural height —
   is already active at measurement time. A short description that fits in
   2 lines is completely unaffected (max-height only caps an upper bound;
   content already inside it renders at its own natural height). */
.show-description-text{
  margin: 0 0 4px; font-size: 13.5px; line-height: 1.5; color: var(--text-muted);
  position: relative; overflow: hidden; max-height: 3em;
  transition: max-height 0.35s ease;
}
/* The bottom fade-to-background overlay, unlike the clamp above, only
   makes sense once we know from that measurement that text is actually
   being cut off — .has-toggle is added exclusively in that case, so this
   stays gated to avoid a stray gradient under short, uncropped text. */
.show-description.has-toggle .show-description-text::after{
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1.2em;
  background: linear-gradient(to bottom, transparent, var(--bg-3));
  transition: opacity 0.25s ease; pointer-events: none;
}
.show-description.expanded .show-description-text{ max-height: 400px; }
.show-description.expanded .show-description-text::after{ opacity: 0; }
.show-description-toggle{
  background: none; border: none; padding: 0; cursor: pointer; color: var(--teal-soft);
  font-family: 'IBM Plex Mono', monospace; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
}
.show-description-toggle:hover{ color: var(--text); text-decoration: underline; }
/* Entry-price tiers — a <ul>, one <li> per tier (e.g. "🎫 10€ solo social
   dance", "🎫 22€ social dance + cena"), each on its own line (owner's
   explicit call, 2026-09-21 — not one run-on line). Same left alignment
   as .show-description/.show-organizer above (flush with
   .show-location's venue/address column), same reset at the narrow
   breakpoint below. Sits between the description and .show-footer in
   shows.js's DOM order. list-style/padding reset since this is a
   presentational list, not a semantic bulleted one — the 🎫 prefix
   already marks each line. */
.show-entry-prices{
  flex: 1 1 100%; margin: 0 0 4px; margin-left: calc(120px + var(--space-3));
  padding: 0; list-style: none; font-size: 13.5px; color: var(--text-muted);
}
.show-entry-prices li + li{ margin-top: 2px; }
.show-ticket-link{
  font-family: 'IBM Plex Mono', monospace; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--cream-ink); background: var(--cream); padding: 8px 16px; border-radius: var(--radius-pill);
  text-decoration: none; white-space: nowrap;
}
.show-ticket-link:hover{ background: var(--cream-soft); }
.shows-empty{ font-size: 14px; font-style: italic; }

/* 0.6 (the original value) dropped --text-muted's already-modest ~10:1
   contrast against this section's near-black background down to ~3.9:1,
   below WCAG AA's 4.5:1 minimum for normal text — a real PageSpeed
   Insights contrast audit flagged exactly this. 0.85 keeps a visible
   "this is a past show" de-emphasis while landing back around ~7:1. */
.show-row-past{ opacity: 0.85; }
.show-past-badge{
  display: inline-block; margin-top: 2px; font-family: 'IBM Plex Mono', monospace; font-size: 10px;
  color: var(--teal-soft); text-transform: uppercase; letter-spacing: 0.05em;
}
/* .show-description/.show-organizer's margin-left (above) only makes
   sense while .show-datetime and .show-location sit side by side — on a
   narrow viewport .show-row's own flex-wrap stacks them into separate
   full-width lines instead (confirmed on a real device, 2026-09-05), and
   that same fixed offset becomes a meaningless indent with no column
   left to align to — text floating in the middle of the card instead of
   a clean flush-left block. Reset both to flush-left below the width
   where this card's two-column layout still holds together. */
@media (max-width: 640px){
  .show-description{ margin-left: 0; }
  .show-organizer{ margin-left: 0; }
  .show-entry-prices{ margin-left: 0; }
}

/* Video — videos/videos.js (click-to-load facade cards) */
.video-grid{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); }
.video-card{
  position: relative; aspect-ratio: 16/9; border-radius: var(--radius-md); overflow: hidden;
  background: var(--bg-3); border: 1px solid var(--line); cursor: pointer;
}
.video-card img{ width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }
.video-card:hover img, .video-card:focus-visible img{ transform: scale(1.06); }
.video-card .video-play-icon{
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 54px; height: 54px; border-radius: var(--radius-pill); background: rgba(10,10,10,0.6);
  border: 2px solid var(--cream); color: var(--cream); display: flex; align-items: center; justify-content: center; font-size: 18px;
  transition: transform 0.25s ease, background 0.25s ease;
}
.video-card:hover .video-play-icon, .video-card:focus-visible .video-play-icon{
  transform: translate(-50%, -50%) scale(1.1); background: rgba(10,10,10,0.8);
}
.video-card .video-title{
  position: absolute; left: 0; right: 0; bottom: 0; padding: var(--space-2) var(--space-3);
  background: linear-gradient(0deg, rgba(0,0,0,0.85), transparent); font-size: 12.5px; color: var(--text);
}
.video-card iframe{ position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* Video event cards — videos/videos.js's buildEventCard(). Rendered in
   #video-grid and reused verbatim by videos/all-videos-overlay.js's own
   grid; never plays a clip itself (activating one always opens
   videos/video-event-overlay.js), so unlike .video-card there's no
   iframe state to style here. */
.video-event-card{
  border-radius: var(--radius-md); overflow: hidden; background: var(--bg-3);
  border: 1px solid var(--line); cursor: pointer;
}
.video-event-thumb{
  position: relative; aspect-ratio: 16/9; overflow: hidden;
}
.video-event-thumb img{ width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }
.video-event-card:hover .video-event-thumb img, .video-event-card:focus-visible .video-event-thumb img{ transform: scale(1.06); }
.video-event-thumb .video-play-icon{
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 54px; height: 54px; border-radius: var(--radius-pill); background: rgba(10,10,10,0.6);
  border: 2px solid var(--cream); color: var(--cream); display: flex; align-items: center; justify-content: center; font-size: 18px;
  transition: transform 0.25s ease, background 0.25s ease;
}
.video-event-card:hover .video-play-icon, .video-event-card:focus-visible .video-play-icon{
  transform: translate(-50%, -50%) scale(1.1); background: rgba(10,10,10,0.8);
}
.video-event-badge{
  position: absolute; top: var(--space-2); right: var(--space-2);
  background: rgba(10,10,10,0.72); color: var(--cream); font-family: 'IBM Plex Mono', monospace;
  font-size: 11px; letter-spacing: 0.03em; padding: 3px 8px; border-radius: var(--radius-pill);
  border: 1px solid var(--line);
}
.video-event-meta{ padding: var(--space-2) var(--space-3) var(--space-3); display: flex; flex-direction: column; gap: 2px; }
.video-event-venue{ font-family: 'Sora', sans-serif; font-weight: 600; font-size: 14px; color: var(--text); }
.video-event-date{
  font-family: 'IBM Plex Mono', monospace; font-size: 11px; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--teal-soft);
}
@media (prefers-reduced-motion: reduce){
  .video-card img, .video-card .video-play-icon,
  .video-event-thumb img, .video-event-thumb .video-play-icon{ transition: none; }
  .video-card:hover img, .video-card:focus-visible img,
  .video-event-card:hover .video-event-thumb img, .video-event-card:focus-visible .video-event-thumb img{ transform: none; }
  .video-card:hover .video-play-icon, .video-card:focus-visible .video-play-icon,
  .video-event-card:hover .video-play-icon, .video-event-card:focus-visible .video-play-icon{
    transform: translate(-50%, -50%); background: rgba(10,10,10,0.6);
  }
}

/* Shared "loading" skeleton + spinner for a freshly-created <img> whose
   real bytes are still downloading (gallery.js's/lightbox.js's own
   wrapMediaLoading()) — replaces the browser's own partial/progressive
   paint of a still-loading photo (real Storage photos are unresized
   originals, so this can be visible for a while on a slow connection)
   with a clean placeholder until the image's load/error event fires.
   Wraps the <img> in a small <span class="media-frame">; sized entirely
   by whichever container it's placed in (.album-card, .lightbox-grid) —
   this block only owns the loading visuals, not layout/dimensions.
   <video> elements are never wrapped in this — see wrapMediaLoading()'s
   own comment for why. */
.media-frame{ position: relative; display: block; }
.media-frame.media-loading{ background: var(--bg-3); }
.media-frame.media-loading img{ opacity: 0; }
.media-frame img{ opacity: 1; transition: opacity 0.3s ease; }
.media-frame.media-loading::after{
  content: ''; position: absolute; top: 50%; left: 50%; z-index: 1;
  width: 28px; height: 28px; margin: -14px 0 0 -14px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,0.25); border-top-color: var(--cream);
  animation: media-spin 0.8s linear infinite;
}
@keyframes media-spin{ to{ transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce){
  .media-frame img{ transition: none; }
  .media-frame.media-loading::after{ animation: none; }
}

/* Foto — gallery/gallery.js (self-hosted photo album grid, Firebase Storage) */
/* :not([hidden]) guard: #all-albums-grid reuses this class and, unlike
   #gallery-grid, gets toggled via el.hidden in JS — an unconditional
   `display` here would silently defeat that (see design/CLAUDE.md's
   note on this exact bug class). */
.gallery-grid:not([hidden]){ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-4); }
.album-card{
  position: relative; aspect-ratio: 1/1; border-radius: var(--radius-md); overflow: hidden;
  border: 1px solid var(--line); background: var(--bg-3); cursor: pointer;
}
.album-card img, .album-card video{ width: 100%; height: 100%; object-fit: cover; transition: transform 0.4s ease; }
.album-card .media-frame{ width: 100%; height: 100%; }
.album-card:hover img, .album-card:hover video,
.album-card:focus-visible img, .album-card:focus-visible video{ transform: scale(1.06); }
.album-card-icon{
  position: absolute; top: var(--space-2); right: var(--space-2); z-index: 1;
  width: 28px; height: 28px; border-radius: var(--radius-pill);
  background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center;
  transition: transform 0.25s ease, background 0.25s ease;
}
.album-card-icon .icon{ width: 14px; height: 14px; fill: var(--cream); }
.album-card:hover .album-card-icon, .album-card:focus-visible .album-card-icon{
  transform: scale(1.1); background: rgba(0,0,0,0.75);
}
.album-card-caption{
  position: absolute; left: 0; right: 0; bottom: 0; padding: var(--space-3);
  background: linear-gradient(0deg, rgba(0,0,0,0.9), transparent);
}
.album-card-title{ font-weight: 600; color: var(--text); font-size: 14px; display: block; }
.album-card-date{
  display: block; font-family: 'IBM Plex Mono', monospace; font-size: 11px;
  color: var(--text-muted); margin-top: 2px;
}
@media (prefers-reduced-motion: reduce){
  .album-card img, .album-card video, .album-card-icon{ transition: none; }
  .album-card:hover img, .album-card:hover video,
  .album-card:focus-visible img, .album-card:focus-visible video{ transform: none; }
}
.gallery-toggle{
  display: block; margin: var(--space-4) auto 0; font-family: 'IBM Plex Mono', monospace;
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--teal-soft);
  background: transparent; border: 1px solid var(--line); border-radius: var(--radius-pill);
  padding: 10px 22px; cursor: pointer;
}
.gallery-toggle:hover{ color: var(--text); border-color: var(--teal-soft); }
.gallery-empty{ font-size: 14px; font-style: italic; color: var(--text-muted); }

/* Photo lightbox — gallery/lightbox.js. Full-screen waterfall viewer for a
   self-hosted album's real photos; a singleton overlay (static shell in
   index.html), same pattern as .site-nav. */
body.lightbox-lock{ overflow: hidden; }
.lightbox{
  position: fixed; inset: 0; z-index: 30; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.lightbox.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.lightbox-inner{
  height: 100%; overflow-y: auto; padding: var(--space-6) var(--space-4) var(--space-5);
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.lightbox.open .lightbox-inner{ transform: scale(1); }
.lightbox-header{ max-width: 1100px; margin: 0 auto var(--space-3); }
.lightbox-title{ font-family: 'Sora', sans-serif; font-size: 18px; color: var(--text); margin: 0; }
.lightbox-subtitle:not([hidden]){
  display: block; font-family: 'IBM Plex Mono', monospace; font-size: 12px;
  color: var(--text-muted); margin-top: 4px;
}
.lightbox-grid{ column-count: 1; column-gap: var(--space-3); max-width: 1100px; margin: 0 auto; }
.lightbox-grid video, .lightbox-grid .media-frame{
  display: block; width: 100%; margin-bottom: var(--space-3); border-radius: var(--radius-md);
  break-inside: avoid; box-shadow: 0 8px 28px rgba(0,0,0,0.4);
  opacity: 0; transform: translateY(18px); animation: lightbox-photo-in 0.55s ease both;
}
/* A photo's own load-triggered opacity fade (the shared .media-frame
   rule above) is separate from this entrance animation, which fires on
   the *wrapper* the moment it's inserted regardless of image load state
   — overflow: hidden clips the image to the wrapper's rounded corners,
   which used to live on the <img> itself before it gained a wrapper. */
.lightbox-grid .media-frame{ overflow: hidden; }
.lightbox-grid .media-frame img{ display: block; width: 100%; margin-bottom: 0; }
@keyframes lightbox-photo-in{ to{ opacity: 1; transform: translateY(0); } }
.lightbox-empty{
  color: var(--text-muted); text-align: center; font-family: 'IBM Plex Mono', monospace;
  font-size: 13px; margin-top: var(--space-6);
}
.lightbox-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 31;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.lightbox-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
@media (min-width: 640px){ .lightbox-grid{ column-count: 2; } }
@media (min-width: 1024px){ .lightbox-grid{ column-count: 3; } }
@media (prefers-reduced-motion: reduce){
  .lightbox, .lightbox-inner, .lightbox-grid video, .lightbox-grid .media-frame, .lightbox-close{ transition: none; animation: none; opacity: 1; transform: none; }
}

/* Fullscreen single-photo viewer — gallery/lightbox.js's openPhotoViewer().
   Desktop-only entry point (see gallery/CLAUDE.md); a singleton overlay
   that can be nested on top of either .lightbox or .all-albums-overlay's
   detail pane. z-index 30, same tier as .lightbox — placed after
   #photo-lightbox in index.html so DOM order breaks the tie in its favor
   when stacked on top of it (see this file's z-index tier-list note).
   display:flex lives on .photo-viewer.open only, not the bare selector —
   see this file's rule on never declaring display unconditionally on a
   selector also toggled via el.hidden. */
body.photo-viewer-lock{ overflow: hidden; }
.photo-viewer{
  position: fixed; inset: 0; z-index: 30; background: rgba(8,8,8,0.96);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.photo-viewer.open{
  display: flex; align-items: center; justify-content: center;
  opacity: 1; visibility: visible; transition: opacity 0.35s ease;
}
.photo-viewer-image{
  max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: var(--radius-md);
  box-shadow: 0 8px 28px rgba(0,0,0,0.5);
  opacity: 1; transition: opacity 0.25s ease;
}
/* .photo-viewer-image's own class toggle (lightbox.js's
   showPhotoViewerAt()/onPhotoViewerImageSettled()) — this markup is
   static, not JS-created like .media-frame's callers, so it gets its
   own small parallel mechanism instead of that wrapper, reusing the
   same @keyframes media-spin for visual consistency. */
.photo-viewer-image.media-loading{ opacity: 0; }
.photo-viewer-spinner:not([hidden]){
  position: fixed; top: 50%; left: 50%; z-index: 30;
  width: 40px; height: 40px; margin: -20px 0 0 -20px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,0.25); border-top-color: var(--cream);
  animation: media-spin 0.8s linear infinite;
}
.photo-viewer-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 31;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.photo-viewer-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
.photo-viewer-nav{
  position: fixed; top: 50%; transform: translateY(-50%); z-index: 31;
  width: 52px; height: 52px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 28px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.photo-viewer-nav:hover{ background: rgba(10,10,10,0.85); }
.photo-viewer-prev{ left: var(--space-4); }
.photo-viewer-next{ right: var(--space-4); }
.photo-viewer-counter{
  position: fixed; bottom: var(--space-4); left: 50%; transform: translateX(-50%); z-index: 31;
  color: var(--text-muted); font-family: 'IBM Plex Mono', monospace; font-size: 13px;
}
.lightbox-grid .photo-viewer-trigger{ cursor: zoom-in; }
@media (prefers-reduced-motion: reduce){
  .photo-viewer{ transition: none; opacity: 1; }
  .photo-viewer-image{ transition: none; }
  .photo-viewer-spinner{ animation: none; }
}

/* All-albums overlay — gallery/all-albums-overlay.js. Reuses .gallery-grid
   (for its initial grid) and .lightbox-grid (for its desktop detail pane)
   rather than duplicating either — see gallery/CLAUDE.md.
   z-index is 29, deliberately one tier BELOW the shared 30 every other
   overlay uses: on mobile, .lightbox (30) opens stacked on top of this
   overlay, and must always win regardless of DOM order or click sequence.
   This overlay's own close button below can safely reuse 30 without
   colliding with .lightbox's real 30 — .all-albums-overlay is its own
   stacking context (position:fixed + z-index), so descendant z-index
   values are only ever compared against each other, never against
   siblings outside it; the whole subtree still paints as one unit at 29.
   Don't "simplify" this into a single shared number. .all-videos-overlay/
   .video-event-overlay below reuse this exact 29/30 split for the same
   reason, this codebase's second nested-overlay pair. */
body.gallery-albums-lock{ overflow: hidden; }
.all-albums-overlay{
  position: fixed; inset: 0; z-index: 29; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.all-albums-overlay.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.all-albums-overlay-inner{
  height: 100%; overflow-y: auto; padding: var(--space-6) var(--space-4) var(--space-5);
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.all-albums-overlay.open .all-albums-overlay-inner{ transform: scale(1); }
.all-albums-overlay-title{ font-family: 'Sora', sans-serif; font-size: 20px; color: var(--text); margin: 0 0 var(--space-4); max-width: 1100px; margin-left: auto; margin-right: auto; }
.all-albums-overlay-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 30;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.all-albums-overlay-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }

.all-albums-grid{ max-width: 1100px; margin: 0 auto; }
/* :not([hidden]) guard — see the matching note on .gallery-grid above;
   #all-albums-split is toggled the same way. */
.all-albums-split:not([hidden]){ display: flex; gap: var(--space-4); max-width: 1100px; margin: 0 auto; }
.all-albums-sidebar{ flex: 0 0 220px; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-2); }
.all-albums-sidebar-item{
  display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2);
  border-radius: var(--radius-md); cursor: pointer; border: 1px solid transparent;
}
.all-albums-sidebar-item:hover, .all-albums-sidebar-item:focus-visible{ background: var(--bg-3); }
.all-albums-sidebar-item img, .all-albums-sidebar-item video{
  width: 48px; height: 48px; object-fit: cover; border-radius: var(--radius-sm); flex-shrink: 0;
}
.all-albums-sidebar-item-title{ font-size: 13px; color: var(--text); }
.all-albums-sidebar-item.selected{ border-color: var(--teal-soft); background: var(--bg-3); }
.all-albums-detail{ flex: 1 1 auto; min-width: 0; }
.all-albums-detail-title{ font-family: 'Sora', sans-serif; font-size: 16px; color: var(--text); margin: 0 0 4px; }
.all-albums-detail-date:not([hidden]){
  display: block; font-family: 'IBM Plex Mono', monospace; font-size: 12px;
  color: var(--text-muted); margin-bottom: var(--space-3);
}

@media (prefers-reduced-motion: reduce){
  .all-albums-overlay, .all-albums-overlay-inner, .all-albums-overlay-close{ transition: none; animation: none; opacity: 1; transform: none; }
}

/* Past shows overlay — shows/past-overlay.js. Year-grouped, collapsible
   accordion of the full past-shows history; a singleton overlay (static
   shell in index.html), same open-class convention as .lightbox — but its
   own dedicated rules, since the content shape (accordion list) differs
   from the lightbox's photo waterfall. */
body.past-overlay-lock{ overflow: hidden; }
.past-overlay{
  position: fixed; inset: 0; z-index: 30; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.past-overlay.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.past-overlay-inner{
  height: 100%; overflow-y: auto; padding: var(--space-6) var(--space-4) var(--space-5);
  max-width: 860px; margin: 0 auto;
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.past-overlay.open .past-overlay-inner{ transform: scale(1); }
.past-overlay-title{
  font-family: 'Sora', sans-serif; font-size: 20px; color: var(--text); margin: 0 0 var(--space-4);
}
.past-overlay-groups{ display: flex; flex-direction: column; gap: var(--space-3); }
.past-year-group{ border: 1px solid var(--line); border-radius: var(--radius-md); overflow: hidden; }
.past-year-toggle{
  display: flex; align-items: center; justify-content: space-between; width: 100%;
  background: var(--bg-3); border: none; padding: var(--space-3) var(--space-4); cursor: pointer;
  font-family: 'IBM Plex Mono', monospace; font-size: 13px; letter-spacing: 0.05em; color: var(--text);
  text-transform: uppercase;
}
.past-year-chevron{ display: inline-block; transition: transform 0.25s ease; }
.past-year-toggle[aria-expanded="true"] .past-year-chevron{ transform: rotate(180deg); }
.past-year-list{
  list-style: none; margin: 0; padding: 0 var(--space-4);
  max-height: 0; overflow: hidden; transition: max-height 0.35s ease, padding 0.35s ease;
}
.past-year-list.expanded{ max-height: 4000px; padding: var(--space-3) var(--space-4); }
.past-overlay-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 31;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.past-overlay-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
@media (prefers-reduced-motion: reduce){
  .past-overlay, .past-overlay-inner, .past-year-chevron, .past-year-list, .past-overlay-close{
    transition: none; animation: none; opacity: 1; transform: none; max-height: none;
  }
}

/* All-videos overlay — videos/all-videos-overlay.js. Reuses .video-grid
   for its grid (same event-card markup as the inline #video-grid) rather
   than duplicating it — see videos/CLAUDE.md.
   z-index is 29, deliberately one tier BELOW the shared 30 every other
   overlay uses, for the exact same reason as .all-albums-overlay above:
   .video-event-overlay (30) opens stacked on top of this overlay when an
   event card is clicked from inside it, and must always win regardless
   of DOM order or click sequence. This overlay's own close button below
   can safely reuse 30 without colliding with .video-event-overlay's real
   30 — .all-videos-overlay is its own stacking context (position:fixed +
   z-index), so descendant z-index values are only ever compared against
   each other, never against siblings outside it; the whole subtree still
   paints as one unit at 29. Don't "simplify" this into a single shared
   number. */
body.all-videos-lock{ overflow: hidden; }
.all-videos-overlay{
  position: fixed; inset: 0; z-index: 29; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.all-videos-overlay.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.all-videos-overlay-inner{
  height: 100%; overflow-y: auto; padding: var(--space-6) var(--space-4) var(--space-5);
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.all-videos-overlay.open .all-videos-overlay-inner{ transform: scale(1); }
.all-videos-overlay-title{ font-family: 'Sora', sans-serif; font-size: 20px; color: var(--text); margin: 0 0 var(--space-4); max-width: 1100px; margin-left: auto; margin-right: auto; }
.all-videos-grid{ max-width: 1100px; margin: 0 auto; }
.all-videos-overlay-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 30;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.all-videos-overlay-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
@media (prefers-reduced-motion: reduce){
  .all-videos-overlay, .all-videos-overlay-inner, .all-videos-overlay-close{ transition: none; animation: none; opacity: 1; transform: none; }
}

/* Single video-event overlay — videos/video-event-overlay.js. Shows one
   event's venue/date as a header plus its clips (reuses .video-grid, the
   same click-to-load facade cards as the inline grid). Opens standalone
   from #video-grid directly, or stacked on top of .all-videos-overlay
   when opened from inside it — see that overlay's z-index comment above;
   this element's shared 30 is what "wins" in the stacked case. */
body.video-event-lock{ overflow: hidden; }
.video-event-overlay{
  position: fixed; inset: 0; z-index: 30; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.video-event-overlay.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.video-event-overlay-inner{
  height: 100%; overflow-y: auto; padding: var(--space-6) var(--space-4) var(--space-5);
  max-width: 1100px; margin: 0 auto;
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.video-event-overlay.open .video-event-overlay-inner{ transform: scale(1); }
.video-event-overlay-header{ margin: 0 0 var(--space-4); }
.video-event-overlay-venue:not([hidden]){ font-family: 'Sora', sans-serif; font-size: 20px; color: var(--text); margin: 0; }
.video-event-overlay-date:not([hidden]){
  display: block; font-family: 'IBM Plex Mono', monospace; font-size: 12px;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--teal-soft); margin-top: 4px;
}
.video-event-overlay-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 31;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.video-event-overlay-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
@media (prefers-reduced-motion: reduce){
  .video-event-overlay, .video-event-overlay-inner, .video-event-overlay-close{ transition: none; animation: none; opacity: 1; transform: none; }
}

/* Shared overlay helpers — design/overlay.js. body.overlay-lock is the
   one scroll-lock class every overlay opened through that helper uses
   (safe as a single shared class, not a per-overlay one, only because at
   most one overlay is ever open at a time — see accessibility/CLAUDE.md).
   .overlay-card/.overlay-card-inner are the { background: true } look —
   an opaque, centered "floating card" instead of content sitting
   directly on the dark veil, now shared by both #contact-modal and
   #credits-overlay (the two card-style overlays) so they read as one
   consistent design system rather than two independently-styled boxes.
   Only the card CHROME lives here (background/radius/shadow/accent) —
   each overlay still owns its own max-width/padding/typography, since a
   form and a list reasonably want different internal spacing. Any
   overlay opting into background:true needs BOTH classes: .overlay-card
   on the outer dialog element (toggled by design/overlay.js) and
   .overlay-card-inner on its own inner content wrapper (a second class
   added in index.html, alongside that overlay's own specific inner
   class). */
body.overlay-lock{ overflow: hidden; }
.overlay-card{ display: flex; align-items: center; justify-content: center; padding: var(--space-4); }
.overlay-card .overlay-card-inner{
  height: auto; max-height: 90vh; max-height: 90dvh; overflow-y: auto;
  background: var(--bg-2); border-radius: 24px; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  border-top: 3px solid var(--teal-soft);
}
/* .overlay-letterhead is the other shared piece: a logo beside a
   title/intro, closed off with a divider — the "Booking Inquiry"
   modal's header shape, applied to #credits-overlay too since the
   owner wanted both card overlays to read as one designed system, not
   independently laid out. Each overlay still owns its own title/intro
   typography (.contact-modal-title vs .credits-overlay-title) — only
   the structural row/logo-size/divider is shared. */
.overlay-letterhead{
  display: flex; align-items: center; gap: var(--space-4);
  padding-bottom: var(--space-4); margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--line);
}
.overlay-letterhead-logo{ display: block; width: 84px; height: auto; flex-shrink: 0; }
.overlay-letterhead-heading{ text-align: left; min-width: 0; }
@media (max-width: 480px){
  /* An 84px logo beside two lines of text is tight below ~480px —
     stack them instead of squeezing the heading into a sliver. */
  .overlay-letterhead{ flex-direction: column; text-align: center; }
  .overlay-letterhead-heading{ text-align: center; }
}

/* Credits overlay — credits/credits.js. A short, rarely-changing list of
   real people/organizations to thank, authored as real static markup in
   index.html (not JS-rendered, for crawlability) — same singleton
   static-shell + open-class convention and z-index tier as .past-overlay,
   reused here since a short scrollable list is the same shape. Laid out
   as a colophon — a fixed-width role label on the left, the name (plus
   plain inline link glyphs, not circular buttons) on the right — the
   conventional print/editorial credits format, rather than an app-style
   card-per-person list.
   Always opens with { background: true } (see credits/credits.js) — the
   base .credits-overlay-inner rule below still includes a harmless
   fallback centering (margin:0 auto) for the (currently unreachable)
   case where .overlay-card hasn't been applied yet. */
.credits-overlay{
  position: fixed; inset: 0; z-index: 30; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.credits-overlay.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.credits-overlay-inner{
  height: 100%; overflow-y: auto; padding: var(--space-7) var(--space-6);
  max-width: 720px; width: 100%; min-height: 420px; margin: 0 auto;
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.credits-overlay.open .credits-overlay-inner{ transform: scale(1); }
/* Title/intro typography only — the row/logo/divider structure comes
   from the shared .overlay-letterhead classes (also used by
   #contact-modal), same reasoning as that modal's own title/intro rules. */
.credits-overlay-title{ font-family: 'Sora', sans-serif; font-size: 22px; color: var(--text); margin: 0 0 4px; }
.credits-overlay-subtitle{ color: var(--text-muted); font-size: 14px; line-height: 1.6; margin: 0; }
.credits-list{ list-style: none; margin: 0; padding: 0; }
.credits-item{
  display: grid; grid-template-columns: 210px 1fr; column-gap: var(--space-4);
  align-items: baseline; padding: var(--space-3) 0; border-bottom: 1px solid var(--line);
}
.credits-item:last-child{ border-bottom: none; }
.credits-role{
  font-family: 'Sora', sans-serif; font-weight: 600; font-size: 13px;
  letter-spacing: 0.02em; color: var(--teal-soft);
}
.credits-name-line{ display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); min-width: 0; }
.credits-name{ font-family: 'Sora', sans-serif; font-weight: 600; color: var(--text); font-size: 15px; }
/* Icon links reuse .contact-icon/.contact-icon-small (the same circular
   pill buttons as the nav's Instagram/YouTube/Facebook icons) rather
   than a Credits-specific glyph style — a deliberate reversal of this
   overlay's earlier "plain inline glyph, not a circular button" look,
   per the project owner's call to keep icon-link styling uniform across
   the whole site. */
@media (max-width: 560px){
  /* A fixed 210px label column doesn't leave enough room for the name
     next to it at phone widths — stack role above name instead. */
  .credits-overlay-inner{ padding: var(--space-5) var(--space-4); min-height: 0; }
  .credits-item{ grid-template-columns: 1fr; row-gap: 4px; }
}
.credits-overlay-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 31;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.credits-overlay-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
@media (prefers-reduced-motion: reduce){
  .credits-overlay, .credits-overlay-inner, .credits-overlay-close{ transition: none; animation: none; opacity: 1; transform: none; }
}

/* FAQ "show all" overlay — faq/faq.js. Same singleton static-shell +
   open-class convention, z-index tier, and { background: true } card
   look as .credits-overlay (reused here for the same reason: a short
   scrollable list of static content, not fetched/rendered data). The
   full FAQ list inside reuses the existing .faq-list/.faq-item classes
   unchanged. */
.faq-overlay{
  position: fixed; inset: 0; z-index: 30; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.faq-overlay.open{ opacity: 1; visibility: visible; transition: opacity 0.35s ease; }
.faq-overlay-inner{
  height: 100%; overflow-y: auto; padding: var(--space-7) var(--space-6);
  max-width: 720px; width: 100%; margin: 0 auto;
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.faq-overlay.open .faq-overlay-inner{ transform: scale(1); }
.faq-overlay-title{ font-family: 'Sora', sans-serif; font-size: 22px; color: var(--text); margin: 0 0 var(--space-4); }
@media (max-width: 560px){
  .faq-overlay-inner{ padding: var(--space-5) var(--space-4); }
}
.faq-overlay-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 31;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.faq-overlay-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }
@media (prefers-reduced-motion: reduce){
  .faq-overlay, .faq-overlay-inner, .faq-overlay-close{ transition: none; animation: none; opacity: 1; transform: none; }
}

/* Contact — static markup, no dedicated JS module */
.contact-links{ display: flex; gap: var(--space-3); flex-wrap: wrap; }
.contact-icon{
  width: 54px; height: 54px; border-radius: var(--radius-pill); border: 1px solid var(--line);
  background: var(--bg-3); color: var(--cream); display: flex; align-items: center; justify-content: center;
  text-decoration: none; transition: background 0.2s, transform 0.15s;
}
/* No focus style existed at all before (2026-09-05 accessibility pass) —
   relying purely on the browser's own default outline, which varies by
   browser/platform and isn't guaranteed to read clearly against --bg-3.
   An explicit :focus-visible ring keeps keyboard-navigation visibility
   consistent everywhere this class is used (nav, footer, Contact,
   Credits, and the new show organizer/venue social icons). */
.contact-icon:focus-visible{ outline: 2px solid var(--teal-soft); outline-offset: 2px; }
.contact-icon:hover{ background: var(--teal-deep); transform: translateY(-2px); }
.contact-icon .icon{ width: 22px; height: 22px; fill: currentColor; }

/* ====================================================================
   FLOATING CONTACT FAB + CONTACT MODAL — contact/floating-cta.js,
   contact/contact-form.js. One shared modal, three triggers (the fixed
   FAB's email mini-button, #contact-email-icon, #book-email-icon).
   ==================================================================== */
.contact-fab{
  /* z-index 32, above .lightbox/.past-overlay (30) and their close
     buttons (31) — otherwise the FAB sits behind those overlays'
     near-opaque backdrop and is invisible while one is open, even
     though its own .is-visible class is still correctly applied. */
  position: fixed; right: var(--space-4); bottom: var(--space-4); z-index: 32;
  display: flex; flex-direction: column; align-items: center; gap: var(--space-3);
  opacity: 0; visibility: hidden; transform: translateY(12px);
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0.3s;
}
.contact-fab.is-visible{ opacity: 1; visibility: visible; transform: none; transition: opacity 0.3s ease, transform 0.3s ease; }
.floating-contact-cta{
  position: relative; width: 56px; height: 56px; border-radius: var(--radius-pill);
  background: var(--teal); color: var(--text); border: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 18px rgba(0,0,0,0.4);
  transition: background 0.2s;
}
.floating-contact-cta:hover{ background: var(--teal-deep); }
.floating-contact-cta .icon{ width: 24px; height: 24px; fill: currentColor; transition: opacity 0.2s ease, transform 0.2s ease; }
.floating-contact-cta .fab-icon-close{
  position: absolute; font-size: 26px; line-height: 1; opacity: 0; transform: scale(0.5) rotate(-45deg);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.contact-fab.open .floating-contact-cta .fab-icon-message{ opacity: 0; transform: scale(0.5) rotate(45deg); }
.contact-fab.open .floating-contact-cta .fab-icon-close{ opacity: 1; transform: none; }

.fab-speed-dial{
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  opacity: 0; visibility: hidden; transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
}
.contact-fab.open .fab-speed-dial{ opacity: 1; visibility: visible; transform: none; transition: opacity 0.2s ease, transform 0.2s ease; }
.fab-mini-btn{
  width: 44px; height: 44px; border-radius: var(--radius-pill); border: 1px solid var(--line);
  background: var(--bg-3); color: var(--cream); display: flex; align-items: center; justify-content: center;
  text-decoration: none; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.35);
  transition: background 0.2s, transform 0.15s;
}
.fab-mini-btn:hover{ background: var(--teal-deep); transform: translateY(-2px); }
.fab-mini-btn .icon{ width: 20px; height: 20px; fill: currentColor; }
@media (prefers-reduced-motion: reduce){
  .contact-fab, .floating-contact-cta .icon, .floating-contact-cta .fab-icon-close, .fab-speed-dial{ transition: none; }
}

.contact-modal{
  /* z-index 33, above .contact-fab (32) — the FAB's own Email button
     is one of this modal's triggers, so opening it (whether from the
     Contact/For Venues sections, or the FAB, possibly with the photo
     lightbox or past-shows overlay still open behind both) must always
     render on top of everything else, including the FAB itself.
     .overlay-card (design/overlay.js's { background: true }) supplies
     the flex-centering/padding and the card chrome (background/radius/
     shadow/accent, shared with #credits-overlay) — this rule only adds
     what's specific to being #contact-modal: position/z-index/veil
     color/transition. */
  position: fixed; inset: 0; z-index: 33; background: rgba(8,8,8,0.94);
  opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.contact-modal.open{
  opacity: 1; visibility: visible; transition: opacity 0.35s ease;
}
/* max-width/padding/scale-in animation are specific to this modal's own
   content (a form wants different breathing room than Credits' list) —
   background/radius/shadow/accent come from the shared .overlay-card-inner
   rule above (design/style.css's "Shared overlay helpers" section). */
.contact-modal-inner{
  width: 100%; max-width: 640px;
  padding: var(--space-5) var(--space-4);
  transform: scale(0.96); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.contact-modal.open .contact-modal-inner{ transform: scale(1); }
/* Structural layout (row/logo-size/divider/mobile stack) comes from the
   shared .overlay-letterhead/.overlay-letterhead-logo/.overlay-letterhead-heading
   classes (design/style.css's "Shared overlay helpers" section) — these
   two rules are just this modal's own title/intro typography. */
.contact-modal-title{ font-family: 'Sora', sans-serif; font-size: 22px; color: var(--text); margin: 0 0 4px; }
.contact-modal-intro{ font-size: 14px; line-height: 1.6; margin: 0; color: var(--text-muted); }
.contact-required-legend{ font-size: 12px; color: var(--text-muted); margin: 0 0 var(--space-4); }
.contact-modal-close{
  position: fixed; top: var(--space-4); right: var(--space-4); z-index: 34;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: rgba(10,10,10,0.6); border: 1px solid var(--line); color: var(--text);
  font-size: 22px; line-height: 1; cursor: pointer; transition: background 0.2s, transform 0.2s;
}
.contact-modal-close:hover{ background: rgba(10,10,10,0.85); transform: scale(1.06); }

.contact-form{ display: flex; flex-direction: column; gap: var(--space-3); }
/* :not([hidden]) — the same class of bug already fixed once on
   .contact-modal: an unconditional `display` here would override the
   browser's own [hidden]{display:none} rule, so showError()/
   showSuccess() setting fields.hidden = true would stop actually
   hiding the fields at all (confirmed via getComputedStyle in a real
   browser — the result screen was rendering stacked underneath the
   still-visible form, not replacing it).
   One column on mobile (plenty of width for a single field already);
   two on desktop, pairing the short fields (name/email, phone/event
   type, event date/venue) so the modal doesn't just get taller and
   taller as fields were added — .contact-field-full opts a field out
   (hear-about, message) to span both columns instead. This is also
   what actually frees up visible room for the message textarea: fewer
   stacked rows above it, not a bigger textarea fighting the same
   cramped space. */
#contact-form-fields:not([hidden]){ display: grid; grid-template-columns: 1fr; gap: var(--space-3); }
#txt-contact-required-legend{ grid-column: 1 / -1; }
.contact-field{ display: flex; flex-direction: column; gap: 4px; }
.contact-field-full{ grid-column: 1 / -1; }
.contact-field label{ font-size: 13px; font-weight: 600; color: var(--text); }
.required-mark{ color: var(--error); font-weight: 700; font-size: 1.1em; }
.contact-field input, .contact-field select, .contact-field textarea{
  font-family: inherit; font-size: 14px; color: var(--text); background: var(--bg-3);
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 10px 12px; width: 100%;
}
.contact-field textarea{ min-height: 110px; resize: vertical; }
/* Same 640px breakpoint already used elsewhere (.hero, .credits-overlay-inner)
   for "is this genuinely a desktop-width viewport, not just a wide phone".
   Widening the modal here is what gives the two-column grid room to
   breathe instead of squeezing two ~260px fields side by side. */
@media (min-width: 640px){
  .contact-modal-inner{ max-width: 720px; }
  #contact-form-fields:not([hidden]){ grid-template-columns: 1fr 1fr; column-gap: var(--space-4); }
  .contact-field textarea{ min-height: 170px; }
}
.contact-field input:focus, .contact-field select:focus, .contact-field textarea:focus{
  outline: none; border-color: var(--teal-soft);
}
.contact-field input[aria-invalid="true"], .contact-field textarea[aria-invalid="true"]{ border-color: var(--error); }
/* The native calendar-picker glyph isn't just a dark-on-dark styling
   problem (which invert+brighten alone used to "fix") — iOS Safari
   doesn't render this pseudo-element at all, so mobile visitors saw no
   icon whatsoever, and even where a browser does draw one (desktop
   Chrome/Safari), its exact position is entirely up to the browser/OS
   and doesn't reliably land at the same spot as the <select>'s hand-
   drawn chevron below. Drawing our own icon as a background-image
   works identically on every platform, at the exact same
   right:12px/center/14x14 spot the select uses.
   appearance:none (same technique already used on .contact-field select
   below) is what actually removes the native control's own icon/arrow —
   hiding just ::-webkit-calendar-picker-indicator's opacity wasn't
   enough on real mobile browsers, which showed our drawn icon *and* a
   separate native arrow of their own side by side (confirmed on a real
   device: fine on desktop, doubled-up on mobile). openDatePicker() in
   contact/contact-form.js (showPicker() on click) is what keeps the
   field clickable now that there's no native icon left to click. */
.contact-field input[type="date"]{
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 36px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23b8b6ad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; background-size: 14px 14px;
}
.contact-field input[type="date"]::-webkit-calendar-picker-indicator{
  opacity: 0; cursor: pointer;
}
/* The native <select> arrow's vertical position is entirely up to the
   browser/OS and doesn't reliably line up with the date field's own
   icon above (confirmed misaligned) — appearance:none + a hand-drawn
   chevron gives it the exact same right:12px/center positioning as
   the calendar icon, instead of trying to nudge a glyph we don't
   control. */
.contact-field select{
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 36px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23b8b6ad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; background-size: 14px 14px;
}
.contact-field-error{ margin: 0; font-size: 12.5px; color: var(--error); }
.contact-honeypot-wrap{ position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.contact-form-status{ margin: 0; font-size: 13.5px; color: var(--error); }
/* Both now live inside #contact-form-fields, which became a two-column
   grid on desktop for the field-pairing above — without this they'd
   land in just the left column instead of spanning/centering across
   the whole form width like before. */
.contact-form-status, #contact-form-submit{ grid-column: 1 / -1; }
/* :not([hidden]) — this class is shared by #contact-form-result-close
   and #contact-form-result-retry, exactly one of which has the hidden
   attribute set at any time (contact-form.js's renderResult()). An
   unconditional display here would override the browser's own
   [hidden]{display:none} rule and show both buttons at once (same bug
   class already found/fixed on #contact-form-fields — see
   contact/CLAUDE.md). Harmless for the plain #contact-form-submit
   button, which is never given the hidden attribute. */
.contact-form-submit:not([hidden]){
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  font-family: 'IBM Plex Mono', monospace; font-size: 13px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--cream-ink); background: var(--cream);
  border: none; padding: 12px 28px; border-radius: var(--radius-pill); cursor: pointer;
  margin-top: var(--space-2);
  transition: background 0.2s, transform 0.15s;
}
.contact-form-submit:hover{ background: var(--cream-soft); transform: translateY(-1px); }
.contact-form-submit:disabled{ opacity: 0.6; cursor: not-allowed; transform: none; }
/* Circle loader shown inside #contact-form-submit while the request is
   in flight (contact-form.js's setLoadingState()) — the button is
   already `disabled` at the same time, which is what actually blocks
   clicks; this is only the visual cue. Colored against the button's
   own light background (--cream), not the page's dark background.
   :not([hidden]) — same bug class as #contact-form-fields/
   .contact-form-submit/.shows-list (see design/CLAUDE.md): an
   unconditional `display` here overrode the browser's own
   [hidden]{display:none}, so the spinner was visible even before the
   button was ever clicked. */
.btn-spinner:not([hidden]){
  display: inline-block; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid rgba(36, 29, 15, 0.35); border-top-color: var(--cream-ink);
  animation: btn-spin 0.7s linear infinite;
}
@keyframes btn-spin{ to{ transform: rotate(360deg); } }
.contact-form-result{ text-align: center; padding: var(--space-4) 0; }
.contact-form-result p{ font-size: 15px; line-height: 1.6; color: var(--text); margin: 0 0 var(--space-3); }
/* Replayed on every showSuccess()/showError() call (contact-form.js's
   renderResult()), including two identical errors in a row — without
   this, a repeat failure snaps straight to text that hasn't visibly
   changed, giving no cue anything happened. */
@keyframes contact-result-flash{ 0%{ opacity: 0.35; } 100%{ opacity: 1; } }
.contact-form-result.flash{ animation: contact-result-flash 0.4s ease; }
@media (prefers-reduced-motion: reduce){
  .contact-modal, .contact-modal-inner{ transition: none; opacity: 1; transform: none; }
  .contact-form-result.flash{ animation: none; }
  .btn-spinner{ animation: none; }
}

/* ====================================================================
   FOOTER — split columns on desktop (brand/copyright left, social +
   legal links right), collapsing to a single centered column on mobile.
   Replaced the earlier everything-centered-and-stacked layout (v1.18.x)
   per the owner's explicit request for a more considered, "real site
   footer" look. Cookie preferences reopen link is Termly's own script
   (index.html's <head>), not a GATH module; see root CLAUDE.md.
   ==================================================================== */
.site-footer{
  padding: var(--space-6) 20px var(--space-5); border-top: 1px solid var(--line);
  font-size: 12.5px; color: var(--text-muted);
}
.footer-row{
  max-width: 960px; margin: 0 auto; display: flex; justify-content: space-between;
  align-items: flex-start; gap: var(--space-6); flex-wrap: wrap;
}
.footer-brand{ text-align: left; }
.footer-wordmark{ font-family: 'Sora', sans-serif; font-weight: 600; color: var(--text); font-size: 15px; margin: 0 0 6px; }
.footer-brand p{ margin: 4px 0; }
.contact-icon-small{ width: 38px; height: 38px; }
.contact-icon-small .icon{ width: 17px; height: 17px; }
.footer-made-with{ display: flex; align-items: center; gap: var(--space-2); margin-top: 2px; }
.footer-made-with .contact-icon-small{ width: 22px; height: 22px; }
.footer-made-with .contact-icon-small .icon{ width: 11px; height: 11px; }
.footer-meta{ display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-3); }
.footer-social{ display: flex; gap: var(--space-2); }
.footer-links{ display: flex; gap: var(--space-4); }
.footer-links a, .footer-link-btn{
  color: var(--text-muted); text-decoration: none; background: transparent; border: none;
  font-size: 12.5px; font-family: inherit; line-height: normal; cursor: pointer;
  margin: 0; padding: 0; -webkit-appearance: none; appearance: none;
}
.footer-links a:hover, .footer-link-btn:hover{ color: var(--teal-soft); }

/* App version tag (design/version.js) — lives inside the footer's normal
   flow, not fixed to the viewport, so it's only visible once a visitor
   actually scrolls to the bottom of the page rather than floating over
   content the whole time they scroll. */
.app-version-tag{
  font-family: 'IBM Plex Mono', monospace; font-size: 10px;
  letter-spacing: 0.03em; color: var(--text-muted); opacity: 0.55;
}
@media (max-width: 640px){
  .footer-row{ flex-direction: column; align-items: center; text-align: center; }
  .footer-brand{ text-align: center; }
  .footer-made-with{ justify-content: center; }
  .footer-meta{ align-items: center; }
}

/* Non-production reminder (design/env-badge.js) — fixed so it's visible
   on every scroll position, on both mobile and desktop; sits above every
   other overlay (nav drawer, lightbox, cookie banner). Padded against
   env(safe-area-inset-*) so it clears a notch/rounded corner on mobile. */
.env-badge{
  position: fixed; top: max(var(--space-2), env(safe-area-inset-top));
  right: max(var(--space-2), env(safe-area-inset-right));
  z-index: 50; pointer-events: none;
  background: #d64545; color: #fff; font-family: 'IBM Plex Mono', monospace;
  font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
  padding: 4px 10px; border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}

/* Generic toast (design/toast.js) — a transient confirmation any module
   can trigger (e.g. shows/shows.js's "Copied!"). z-index: 60, above even
   .env-badge (50, the previous highest tier) and every overlay/modal —
   a toast is meant to be visible no matter what else is open.
   pointer-events: none on the wrapper so it never blocks a click behind
   it; opacity/transform-only transition keeps it out of layout when
   hidden (no display:none, which can't be transitioned). */
.gath-toast{
  position: fixed; left: 50%; bottom: var(--space-5); z-index: 60;
  transform: translate(-50%, 8px); opacity: 0; pointer-events: none;
  background: var(--bg-3); color: var(--text); border: 1px solid var(--line);
  padding: 10px 20px; border-radius: var(--radius-pill); font-size: 14px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
  transition: opacity 0.25s ease, transform 0.25s ease;
  max-width: calc(100vw - var(--space-5) * 2); text-align: center;
}
.gath-toast.show{ opacity: 1; transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce){
  .gath-toast{ transition: none; }
}

/* Cookie consent banner/preferences are now Termly's own script (see
   index.html's <head>) — no local styling needed here anymore. */
