/* Taru Ensemble — base styles */

@import url('https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Fraunces:ital,wght@0,500;0,600;1,500&family=Raleway:wght@300;700&display=swap');

/* Registers --sig-len (see the member-signature block) as a real <number>
   instead of an untyped token. Without this, browsers can't always
   guarantee smooth interpolation of a transition whose value depends on
   var(--sig-len) — some just snap straight to the end state instead of
   animating through it, which is what made the signature stroke reveal
   look like it "appeared" instead of drawing progressively. */
@property --sig-len {
  syntax: '<number>';
  inherits: true;
  initial-value: 1500;
}

:root {
  --color-bg: #fafafa;
  --color-bg-alt: #eaedf1;
  /* Sampled directly from the hero photo's top edge pixels, so the mobile/
     tablet hero's letterbox bands (see .intro-hero-stage) blend into the
     photo instead of reading as bars added on top of it. */
  --color-hero-band: #d4c2ba;
  --color-surface: #ffffff;
  --color-text: #1d2126;
  --color-text-soft: #6b7280;
  --color-accent: #d4a032;
  --color-accent-deep: #a97b1f;
  --color-line: #e0e3e8;
  --color-surface-dark: #363b40;
  --color-surface-dark-warm: #3d362e;
  --color-surface-dark-cool: #2e343a;
  --color-surface-dark-light: #4d545b;
  --color-surface-dark-lighter: #6b7278;
  --color-on-dark: #f0ece4;
  --color-on-dark-soft: #a9afb6;
  --color-line-dark: rgba(240, 236, 228, 0.18);
  --shadow-sm: 0 1px 3px rgba(16,20,26,0.06), 0 1px 2px rgba(16,20,26,0.05);
  --shadow-lg: 0 20px 40px rgba(16,20,26,0.14), 0 6px 12px rgba(16,20,26,0.07);
  /* photo frames: shadow cast down and to the right, not centered */
  --shadow-photo: 18px 26px 44px rgba(16,20,26,0.28), 7px 12px 20px rgba(16,20,26,0.16);
  --font-body: 'Arimo', Arial, Helvetica, sans-serif;
  --font-display: 'Fraunces', Georgia, serif;
  --max-width: 1180px;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  cursor: url('../img/cursor-note.svg') 16 16, auto;
}

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

/* Signature effect: photos sit in cool desaturated tone, bloom into full color on hover/focus */
img {
  filter: grayscale(0.9) contrast(1.05);
  transition: filter 0.6s ease;
}
img:hover, img:focus-visible {
  filter: grayscale(0) contrast(1);
}
.no-desaturate { filter: none; }
.no-desaturate:hover, .no-desaturate:focus-visible { filter: none; }

a { color: inherit; text-decoration: none; cursor: url('../img/cursor-note-hover.svg') 16 16, pointer; }

h1, h2, h3, h4 {
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  line-height: 1.15;
  margin: 0 0 0.5em;
  letter-spacing: 0.02em;
}

p { margin: 0 0 1em; text-align: left; }

.wrap {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 24px;
}

.eyebrow {
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--color-text-soft);
  margin-bottom: 0.9em;
}

/* Two small gold dots centered under the label, echoing the "••" under the
   TARU wordmark logo (same dot:gap ratio, ~1:0.4) instead of a line. A
   single ::after draws both: box-shadow paints the second dot 6px to the
   right (4px dot + 2px gap), and margin-left:-5px centers the resulting
   10px-wide pair under the label's own width. */
.eyebrow::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 4px;
  height: 4px;
  margin-left: -5px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 6px 0 0 var(--color-accent);
}

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

/* ---------- Header ---------- */

/* Hidden by default (frees the space at the top of the page); floats back in
   when the cursor moves near the top edge, or while the mobile menu is open. */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: rgba(250, 250, 250, 0.73);
  backdrop-filter: blur(22px) saturate(140%);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  border-bottom: 1px solid rgba(255, 255, 255, 0.5);
  box-shadow: 0 8px 32px rgba(16, 20, 26, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.55);
  transform: translateY(-100%);
  transition: transform 0.25s ease;
}
.site-header.is-visible { transform: translateY(0); }

/* Small persistent affordance hinting the hidden menu is up there — also the
   only way touch devices (no hover) can reveal it at all. */
.menu-hint {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 101;
  width: 40px;
  height: 40px;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 50%;
  background: rgba(250, 250, 250, 0.77);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  box-shadow: var(--shadow-sm);
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  transition: opacity 0.2s ease;
}
.menu-hint span {
  display: block;
  width: 16px;
  height: 2px;
  background: var(--color-accent);
  transition: transform 0.3s ease, opacity 0.2s ease;
}
.menu-hint[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.menu-hint[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.menu-hint[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.site-header.is-visible ~ .menu-hint,
.menu-hint.is-hidden { opacity: 0; pointer-events: none; }

.site-header .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 76px;
}

.logo {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.logo span { color: var(--color-accent-deep); }

.logo--mark {
  display: flex;
  align-items: center;
}

.logo-mark {
  height: 30px;
  width: auto;
  display: block;
  filter: none;
}
.logo-mark:hover, .logo-mark:focus-visible { filter: none; }

/* Header wordmark's own artwork sits inside an SVG viewBox with built-in
   breathing room and a hanging "••" below the staff-line motif, so it
   needs a bigger on-screen size and a small nudge to actually read as
   centered in the 76px bar (see .site-header .wrap) — the dots hang below
   that visual center rather than being centered themselves. */
.site-header .logo-mark {
  height: 58px;
  position: relative;
  left: 6px;
  top: 4px;
}

.logo-mark--footer { height: 64px; }

.main-nav ul {
  list-style: none;
  display: flex;
  gap: 2rem;
  margin: 0;
  padding: 0;
}

.main-nav a {
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 6px 2px;
  border-bottom: 2px solid transparent;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.main-nav a:hover,
.main-nav a[aria-current="page"] {
  border-color: var(--color-accent-deep);
}

.lang-switch {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: 0.5rem;
}

.lang-switch .lang-link {
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  padding: 4px 9px;
  border-radius: 999px;
  border: 1px solid transparent;
  opacity: 0.5;
}

.lang-switch .lang-link:hover { opacity: 0.85; }

.lang-switch .lang-link.is-active {
  opacity: 1;
  border-color: var(--color-line);
  background: var(--color-surface);
}

.nav-toggle {
  display: none;
  background: none;
  border: none;
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
  width: 40px;
  height: 40px;
  padding: 8px;
}

.nav-toggle span {
  display: block;
  height: 2px;
  background: var(--color-text);
  margin: 6px 0;
  transition: transform 0.3s ease, opacity 0.2s ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

/* ---------- Buttons ---------- */

.btn {
  display: inline-block;
  padding: 14px 30px;
  border: 1px solid var(--color-text);
  border-radius: 999px;
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease, box-shadow 0.35s ease;
}

.btn:hover { transform: translateY(-2px); }

.btn-primary {
  background: var(--color-text);
  color: var(--color-bg);
  box-shadow: var(--shadow-lg);
}

.btn-primary:hover { background: var(--color-accent-deep); color: #fff; border-color: var(--color-accent-deep); }

.btn-ghost {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-line);
  box-shadow: var(--shadow-sm);
}

.btn-ghost:hover { background: var(--color-text); color: var(--color-bg); border-color: var(--color-text); }

/* ---------- Intro hero (homepage landing effect) ---------- */

.intro-hero {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* .site-header is position:fixed (overlays, doesn't push layout), so the
     hero should claim the full viewport, not vh-minus-header — subtracting
     76px here left a blank strip of that height under the hero. */
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
  background: var(--color-bg);
  /* Single source of truth for the corner inset, shared by .intro-logo and
     .intro-logo-glow so they can't drift out of sync again. */
  --logo-pad-x: 3vw;
  --logo-pad-y: 3vh;
  /* Used only by .intro-hero--logo-top (experiment: logo in the top-left
     corner instead of bottom-left) — kept separate from --logo-pad-y so
     switching between the two doesn't require renaming anything. */
  --logo-pad-top: 3vh;
}

.intro-bg {
  position: absolute;
  inset: 0;
}

.intro-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 25%;
  filter: none;
}

/* Corner anchor + size are independent of the fragment fly-in animation below —
   moving/resizing the logo here does not touch how the pieces assemble. */
.intro-logo {
  position: absolute;
  z-index: 2;
  left: var(--logo-pad-x);
  bottom: var(--logo-pad-y);
  /* width is deliberately generous/non-binding (see .intro-logo-glow comment
     below for why) — max-height is what actually sizes the visible ink. */
  width: min(1485.12px, 54.8352vw);
  /* Recalculated so the ON-SCREEN ink size matches what it was BEFORE the
     viewBox crop (52.5504vh against the old 1704-tall viewBox), scaled by
     the viewBox height ratio: 52.5504 * (808/1704) = 24.9183vh. Cropping the
     viewBox removed the old ~53% internal transparent margin, so reusing
     the old vh value would have rendered the ink ~2.1x too large. */
  max-height: 24.9183vh;
  height: auto;
  overflow: visible;
  display: block;
}

/* Soft spotlight behind the logo's corner so it reads against a busy photo.
   Padding (10vw/10vh) is symmetric around .intro-logo's own box, so the
   radial gradient below is naturally centered on the logo without needing
   a separate offset. max-height (not width) drives the logo's visible size,
   so the glow's box — width included — is expressed in vh to track it:
   width = max-height * viewBox aspect (1575/808 = 1.9493), i.e. the box the
   ink actually renders into, plus a symmetric vh bleed on every side. */
.intro-logo-glow {
  position: absolute;
  z-index: 1;
  left: calc(var(--logo-pad-x) - 10vh);
  bottom: calc(var(--logo-pad-y) - 10vh);
  width: calc(48.5721vh + 20vh);
  height: calc(24.9183vh + 20vh);
  background: radial-gradient(ellipse at center, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.25) 40%, rgba(255,255,255,0) 72%);
  opacity: 0;
  pointer-events: none;
  animation-name: logoGlowIn;
  animation-duration: 3.5s;
  animation-delay: 0.3s;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
}

/* Envelope matches the fragment animations exactly: 0.3s = frag-top's delay
   (first piece to move), 3.8s = frag-dots' delay+duration (last piece to land). */
@keyframes logoGlowIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ---------- Light-logo variant (experiment) ----------
   Swaps the black ink for a light one and the white spotlight for a dark
   vignette, reusing .intro-logo-glow's existing box/position/animation —
   only its color changes. To revert: remove "intro-hero--logo-light" from
   <section class="intro-hero"> in index.html. Dots (#d4a032) are untouched:
   the selector explicitly excludes .frag-dots. */
.intro-hero--logo-light {
  --logo-light-color: #ffffff;
}
.intro-hero--logo-light .intro-logo .logo-frag:not(.frag-dots) path[fill]:not([fill="none"]) {
  fill: var(--logo-light-color);
}
.intro-hero--logo-light .intro-logo .logo-frag:not(.frag-dots) path[stroke]:not([stroke="none"]) {
  stroke: var(--logo-light-color);
}
.intro-hero--logo-light .intro-logo-glow {
  /* v2: a hard-edged floor-opacity box (visible rectangle) reads as a sticker,
     not a photographic vignette. Fix: make the box 5x the logo's own ink
     footprint, centered on it, and let the gradient actually reach 0 well
     before that box's edge (~70% of the radius) — so there is no longer any
     boundary for the eye to land on; whatever's left just fades into the
     photo/screen edge. The logo's own corner sits at ~28% of this box's
     radius, comfortably inside the still-strong part of the falloff, which
     is what keeps contrast up despite removing the old hard floor. */
  left: calc(var(--logo-pad-x) - 97.1442vh);
  bottom: calc(var(--logo-pad-y) - 49.8366vh);
  width: 242.8605vh;
  height: 124.5915vh;
  background: radial-gradient(ellipse at center,
    rgba(0,0,0,0.68) 0%,
    rgba(0,0,0,0.55) 15%,
    rgba(0,0,0,0.40) 28%,
    rgba(0,0,0,0.24) 40%,
    rgba(0,0,0,0.10) 55%,
    rgba(0,0,0,0) 70%,
    rgba(0,0,0,0) 100%);
  filter: blur(80px);
}

/* ---------- Top-left corner variant (experiment) ----------
   Alternative to the default bottom-left anchor, to compare composition and
   contrast against the plain wall area near the top. Same size — only the
   anchor changes. Toggle by adding/removing "intro-hero--logo-top" on
   <section class="intro-hero"> in index.html; combines cleanly with
   "intro-hero--logo-light" (see the combined rule below for the vignette's
   re-centered position in that case).
   top offset = 76px (site-header's own height, see .site-header .wrap) +
   the usual --logo-pad-top breathing room — guarantees the logo clears the
   header bar completely even when it's pinned open (.site-header.is-visible
   slides the full 76px bar down over the top of the page). */
.intro-hero--logo-top .intro-logo {
  top: calc(76px + var(--logo-pad-top));
  bottom: auto;
}
.intro-hero--logo-top .intro-logo-glow {
  top: calc(76px + var(--logo-pad-top) - 10vh);
  bottom: auto;
}
.intro-hero--logo-top.intro-hero--logo-light .intro-logo-glow {
  /* Re-centers the big 5x vignette box (left/width/height/background/blur
     inherited from .intro-hero--logo-light .intro-logo-glow above) on the
     logo's new top-anchored position instead of the bottom one. */
  top: calc(76px - 46.8366vh);
  bottom: auto;
}

.logo-frag {
  opacity: 0;
  animation-fill-mode: forwards;
  animation-timing-function: linear;
  transform-box: view-box;
}

.frag-top      { animation-name: flyTop; animation-duration: 2.5s; animation-delay: 0.3s; }
.frag-left-lines  { animation-name: flyLeft; animation-duration: 2.35s; animation-delay: 0.65s; }
.frag-right-lines { animation-name: flyRight; animation-duration: 2.75s; animation-delay: 0.35s; }
.frag-ensemble { animation-name: flyEnsemble; animation-duration: 2.9s; animation-delay: 0.5s; }
.frag-aru      { animation-name: flyCenter; animation-duration: 2.55s; animation-delay: 0.95s; }
.frag-dots     { animation-name: flyDots; animation-duration: 1.7s; animation-delay: 2.1s; }

@keyframes flyTop {
  0% { opacity: 0.00; transform: translate(6.00%, -170.00%) rotate(-4.00deg); }
  10% { opacity: 0.39; transform: translate(-2.33%, -146.05%) rotate(-3.37deg); }
  20% { opacity: 0.77; transform: translate(-7.81%, -121.74%) rotate(-2.76deg); }
  30% { opacity: 1.00; transform: translate(-10.61%, -97.78%) rotate(-2.18deg); }
  40% { opacity: 1.00; transform: translate(-11.10%, -74.92%) rotate(-1.65deg); }
  50% { opacity: 1.00; transform: translate(-9.84%, -53.93%) rotate(-1.17deg); }
  60% { opacity: 1.00; transform: translate(-7.51%, -35.56%) rotate(-0.76deg); }
  70% { opacity: 1.00; transform: translate(-4.78%, -20.47%) rotate(-0.44deg); }
  80% { opacity: 1.00; transform: translate(-2.31%, -9.25%) rotate(-0.20deg); }
  90% { opacity: 1.00; transform: translate(-0.61%, -2.34%) rotate(-0.05deg); }
  100% { opacity: 1.00; transform: translate(0.00%, 0.00%) rotate(-0.00deg); }
}
@keyframes flyLeft {
  0% { opacity: 0.00; transform: translate(-140.00%, 10.00%) rotate(-6.00deg); }
  10% { opacity: 0.39; transform: translate(-124.70%, -4.76%) rotate(-5.06deg); }
  20% { opacity: 0.77; transform: translate(-107.41%, -14.44%) rotate(-4.15deg); }
  30% { opacity: 1.00; transform: translate(-88.84%, -19.33%) rotate(-3.28deg); }
  40% { opacity: 1.00; transform: translate(-69.82%, -20.11%) rotate(-2.47deg); }
  50% { opacity: 1.00; transform: translate(-51.36%, -17.78%) rotate(-1.76deg); }
  60% { opacity: 1.00; transform: translate(-34.46%, -13.54%) rotate(-1.15deg); }
  70% { opacity: 1.00; transform: translate(-20.11%, -8.62%) rotate(-0.65deg); }
  80% { opacity: 1.00; transform: translate(-9.18%, -4.17%) rotate(-0.29deg); }
  90% { opacity: 1.00; transform: translate(-2.33%, -1.09%) rotate(-0.07deg); }
  100% { opacity: 1.00; transform: translate(0.00%, 0.00%) rotate(-0.00deg); }
}
@keyframes flyRight {
  0% { opacity: 0.00; transform: translate(140.00%, -8.00%) rotate(6.00deg); }
  10% { opacity: 0.39; transform: translate(124.70%, 5.39%) rotate(5.06deg); }
  20% { opacity: 0.77; transform: translate(107.41%, 14.12%) rotate(4.15deg); }
  30% { opacity: 1.00; transform: translate(88.84%, 18.44%) rotate(3.28deg); }
  40% { opacity: 1.00; transform: translate(69.82%, 18.99%) rotate(2.47deg); }
  50% { opacity: 1.00; transform: translate(51.36%, 16.71%) rotate(1.76deg); }
  60% { opacity: 1.00; transform: translate(34.46%, 12.69%) rotate(1.15deg); }
  70% { opacity: 1.00; transform: translate(20.11%, 8.06%) rotate(0.65deg); }
  80% { opacity: 1.00; transform: translate(9.18%, 3.89%) rotate(0.29deg); }
  90% { opacity: 1.00; transform: translate(2.33%, 1.02%) rotate(0.07deg); }
  100% { opacity: 1.00; transform: translate(0.00%, 0.00%) rotate(0.00deg); }
}
@keyframes flyCenter {
  0% { opacity: 0.00; transform: translate(0.00%, 80.00%) scale(0.850); }
  10% { opacity: 0.39; transform: translate(-5.28%, 65.37%) scale(0.873); }
  20% { opacity: 0.77; transform: translate(-8.54%, 51.86%) scale(0.896); }
  30% { opacity: 1.00; transform: translate(-9.92%, 39.71%) scale(0.918); }
  40% { opacity: 1.00; transform: translate(-9.69%, 29.10%) scale(0.938); }
  50% { opacity: 1.00; transform: translate(-8.28%, 20.12%) scale(0.956); }
  60% { opacity: 1.00; transform: translate(-6.18%, 12.81%) scale(0.971); }
  70% { opacity: 1.00; transform: translate(-3.88%, 7.17%) scale(0.984); }
  80% { opacity: 1.00; transform: translate(-1.86%, 3.17%) scale(0.993); }
  90% { opacity: 1.00; transform: translate(-0.49%, 0.79%) scale(0.998); }
  100% { opacity: 1.00; transform: translate(0.00%, 0.00%) scale(1.000); }
}
@keyframes flyEnsemble {
  0% { opacity: 0.00; transform: translate(-100.00%, 100.00%) rotate(-3.00deg); }
  10% { opacity: 0.39; transform: translate(-87.52%, 75.91%) rotate(-2.53deg); }
  20% { opacity: 0.77; transform: translate(-74.22%, 55.43%) rotate(-2.07deg); }
  30% { opacity: 1.00; transform: translate(-60.55%, 38.74%) rotate(-1.64deg); }
  40% { opacity: 1.00; transform: translate(-47.04%, 25.71%) rotate(-1.24deg); }
  50% { opacity: 1.00; transform: translate(-34.26%, 16.03%) rotate(-0.88deg); }
  60% { opacity: 1.00; transform: translate(-22.81%, 9.21%) rotate(-0.57deg); }
  70% { opacity: 1.00; transform: translate(-13.23%, 4.68%) rotate(-0.33deg); }
  80% { opacity: 1.00; transform: translate(-6.01%, 1.92%) rotate(-0.15deg); }
  90% { opacity: 1.00; transform: translate(-1.52%, 0.45%) rotate(-0.04deg); }
  100% { opacity: 1.00; transform: translate(0.00%, 0.00%) rotate(-0.00deg); }
}
@keyframes flyDots {
  0% { opacity: 0.00; transform: translate(0.00%, 70.00%) scale(0.400); }
  10% { opacity: 0.39; transform: translate(3.69%, 55.62%) scale(0.494); }
  20% { opacity: 0.77; transform: translate(5.98%, 42.82%) scale(0.585); }
  30% { opacity: 1.00; transform: translate(6.94%, 31.78%) scale(0.672); }
  40% { opacity: 1.00; transform: translate(6.78%, 22.56%) scale(0.753); }
  50% { opacity: 1.00; transform: translate(5.80%, 15.12%) scale(0.824); }
  60% { opacity: 1.00; transform: translate(4.33%, 9.35%) scale(0.885); }
  70% { opacity: 1.00; transform: translate(2.72%, 5.10%) scale(0.935); }
  80% { opacity: 1.00; transform: translate(1.30%, 2.22%) scale(0.971); }
  90% { opacity: 1.00; transform: translate(0.34%, 0.55%) scale(0.993); }
  100% { opacity: 1.00; transform: translate(0.00%, 0.00%) scale(1.000); }
}

@media (prefers-reduced-motion: reduce) {
  .logo-frag { animation: none; opacity: 1; transform: none; }
  .intro-logo-glow { animation: none; opacity: 1; }
}

/* ---------- Photo frame (shared) ---------- */

.hero-photo {
  position: relative;
  border-radius: 28px;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background: var(--color-bg-alt);
  box-shadow: var(--shadow-photo);
}

.hero-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 20%;
}

/* ---------- Page hero (interior pages) ---------- */

.page-hero {
  padding: 76px 0 44px;
  border-bottom: 1px solid var(--color-line);
  background-image: url('../img/deco-pattern.svg');
  background-repeat: repeat;
  background-size: 150px 180px;
}

/* No decorative pattern (used on the Calendar page) */
.page-hero--plain { background-image: none; }

/* "T" de Taru en mosaico, para el banner de Über Taru */
.page-hero--t-pattern {
  background-image: url('../img/deco-pattern-t.svg');
  background-repeat: repeat;
  background-size: 260px 260px;
}

/* ---------- "Encabezado estilo 1": staff lines + gold notes + white band ----------
   Reusable page-hero modifier: pentagram lines and scattered gold "notes" behind
   the title, on a band slightly lighter than the page background. Pair with
   .page-hero--plain to cancel the default tiled pattern. To reuse on another
   page: add both classes to that page's .page-hero, and put .tight on the
   section right after it if you want the tightened gap this was tuned for. */
.page-hero--staff {
  position: relative;
  overflow: hidden;
  padding-bottom: 44px;
  border-bottom: none;
  background-color: var(--color-surface);
}
.page-hero--staff + section.tight {
  padding-top: 32px;
}
.page-hero--staff .wrap {
  position: relative;
  z-index: 1;
}

.page-hero--staff::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 108px;
  height: 60px;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(212,160,50,0.25) 0,
    rgba(212,160,50,0.25) 1px,
    transparent 1px,
    transparent 14px
  );
  pointer-events: none;
}

.page-hero--staff::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 108px;
  height: 60px;
  background-image:
    radial-gradient(circle 5px at 58% 15%, rgba(212,160,50,0.65) 0%, rgba(212,160,50,0.65) 96%, transparent 100%),
    radial-gradient(circle 7px at 64% 55%, rgba(212,160,50,0.55) 0%, rgba(212,160,50,0.55) 96%, transparent 100%),
    radial-gradient(circle 4px at 69% 30%, rgba(212,160,50,0.7) 0%, rgba(212,160,50,0.7) 96%, transparent 100%),
    radial-gradient(circle 6px at 74% 80%, rgba(212,160,50,0.5) 0%, rgba(212,160,50,0.5) 96%, transparent 100%),
    radial-gradient(circle 5px at 80% 45%, rgba(212,160,50,0.6) 0%, rgba(212,160,50,0.6) 96%, transparent 100%);
  pointer-events: none;
}

.page-hero h1 {
  font-size: clamp(1.9rem, 6vw, 3.4rem);
  overflow-wrap: break-word;
}
.page-hero .lede { max-width: 640px; color: var(--color-text-soft); font-size: 1.05rem; }

/* Jump nav under a page-hero title, e.g. "Videos / Galerie" on Medien —
   scrolls to same-page anchors (see scroll-margin-top on the mosaic-content
   sections below, so the fixed header doesn't cover the target on landing). */
.jump-nav {
  display: flex;
  gap: 28px;
  margin-top: 20px;
}
.jump-nav a {
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text-soft);
  text-decoration: none;
  padding-bottom: 4px;
  border-bottom: 1px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.jump-nav a:hover,
.jump-nav a:focus-visible {
  color: var(--color-accent-deep);
  border-color: var(--color-accent-deep);
}

/* ---------- Sections ---------- */

section { padding: 88px 0; }
section.tight { padding: 56px 0; }
section.alt { background: var(--color-bg-alt); }

.section-head { max-width: 680px; margin-bottom: 3rem; }
.section-head h2 { font-size: clamp(1.9rem, 4vw, 2.6rem); }

/* ---------- Homepage Kontakt teaser ----------
   Reuses the same tenue pentagram+notes tile already applied to
   .page-hero on the interior pages (deco-pattern.svg), so this section
   reads as part of the same visual system instead of a flat, textureless
   block. Also surfaces a lower-friction "just email us" link and the same
   social icons used on the full kontakt.html page, so the CTA isn't the
   only way in. */
.kontakt-teaser {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-alt) url('../img/deco-pattern.svg') repeat;
  background-size: 150px 180px;
}

/* Giant "T" from the logo (same deco-t.svg used as the Über-Taru watermark),
   centered faintly behind the CTA — echoes the brand mark without competing
   with the text. */
.kontakt-teaser__t {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(1200px, 120vw);
  height: auto;
  opacity: 0.1;
  pointer-events: none;
  z-index: 0;
}

.kontakt-teaser > .wrap { position: relative; z-index: 1; }

.kontakt-teaser__head {
  text-align: center;
  margin-left: auto;
  margin-right: auto;
}

.kontakt-teaser__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px 24px;
  margin-top: 32px;
}

/* Gold by default, flips to black on hover/focus/active — reversed from
   the site-wide .btn-primary (dark by default) and scoped to this section
   only so the Konzerte "Alle Termine" button elsewhere on the homepage
   keeps its usual styling. */
.kontakt-teaser .btn-primary {
  background: var(--color-accent);
  color: var(--color-text);
  border-color: var(--color-accent);
}
.kontakt-teaser .btn-primary:hover,
.kontakt-teaser .btn-primary:focus-visible,
.kontakt-teaser .btn-primary:active {
  background: var(--color-text);
  color: var(--color-bg);
  border-color: var(--color-text);
}

.kontakt-teaser__social {
  justify-content: center;
  margin-top: 28px;
}

/* "Vox Mundi" intro: block centered on the page (instead of sharing the
   row with a side photo), text keeps its normal left/justify alignment. */
.section-head--centered {
  margin-left: auto;
  margin-right: auto;
}

/* "Vox Mundi" photo banner between the intro text and the programme
   cards — rounded corners, no drop shadow (unlike the site's other
   photo frames, which use --shadow-photo). */
.konzept-photo {
  max-width: 960px;
  margin: 0 auto 48px;
  border-radius: 14px;
  overflow: hidden;
  aspect-ratio: 16 / 9;
}
.konzept-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.prose-wide { max-width: 820px; margin: 0 auto; }
/* Pins the intro text block (and the T watermark riding along with it,
   since it's positioned relative to elements inside .prose-wide) to the
   right side instead of centering it — margin-left:auto with no
   margin-right, so it's a pure position change: max-width/size untouched.
   Desktop only: on narrower screens .prose-wide already fills the
   available width, so there'd be nowhere to shift it. Same 1360px
   threshold as .prose-frame-photo below — the two only make sense
   together (see that rule's comment for why 1360). */
@media (min-width: 1360px) {
  .prose-wide { margin-left: auto; margin-right: 0; }
}
.read-more-toggle { display: none; }

/* Giant "T" from the logo as a watermark behind the intro text. The anchor
   sits right before the first paragraph, and the image's own top edge (not
   its center) is pinned to it — so the crossbar stays at the top of the
   text at any screen size, instead of drifting with the image's height. */
.prose-frame {
  position: relative;
  overflow: hidden;
  /* Flat fallback in the same sampled beige as the desktop gradient below
     (#d6cabe-#9c8a79). Mobile has no room for that gradient's left/right
     sweep, but without any background at all it fell through to the page's
     plain #fafafa — visibly a different, colder color than desktop's warm
     backdrop. This keeps every viewport in the same beige family; on
     desktop the ::before gradient is opaque and sits on top of it. */
  background-color: #b6a89c;
}
/* Extends the photo's beige studio backdrop to fill the whole section,
   without touching the people's size/position in .prose-frame-photo.
   A single cover-scaled copy can't do both at once: scaled to cover the
   full ~1400px-wide section, the same photo's people would end up at a
   completely different apparent size than in the narrow 320px frame.
   So this is a second, separate copy — full-bleed, mostly showing plain
   backdrop color since a portrait photo stretched that wide only shows a
   thin horizontal slice of it — sitting behind (z-index 0) the existing
   sharp copy, which stays exactly as it is on top (z-index 1, unchanged).
   All viewports: it's a plain full-bleed background layer (doesn't depend
   on .prose-frame-photo's width-fragile positioning math), so there's no
   reason it needs a breakpoint the way the side-by-side photo frame does
   — EXCEPT it turned out that reasoning was wrong: on mobile, .prose-
   frame stacks text-then-photo in one tall flex column (see the
   max-width:860px block below), and this same full-bleed layer was
   painting behind that ENTIRE tall container — text included, not just
   "around the photo" the way it does in the desktop side-by-side layout
   where there's genuine empty space to fill. That's why the text became
   unreadable on mobile: this is desktop-only now, both layers. Mobile's
   own stacked photo (cover, its own box) already looks fine without an
   extended backdrop behind it. */
@media (min-width: 861px) {
  /* Flat sampled color, not the blurred photo copy — after five-plus
     rounds of "still too dark near the text" complaints despite cutting
     the shadow layer's own opacity every time, disabling that layer
     entirely (temporarily, for comparison) barely changed the screenshot
     at all: the actual dark patch was coming from the blurred photo
     itself (about-duo.jpg's black clothing bleeds into a dark blob under
     120px of blur, landing roughly bottom-right), not from the shadow
     gradient stacked on top of it. This flat-color version has no such
     patch — the photo's real backdrop tone is a near-uniform warm beige,
     #b6a89c-#c5bab0, sampled directly, so there's nothing in the image
     data left to go dark under a blur. */
  .prose-frame::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    /* Linear, not radial — a straightforward light-left/dark-right sweep
       rather than fading outward from a point near the photo. Range
       widened well past the originally-sampled #c5bab0-#b6a89c (a very
       subtle ~15-unit spread per channel) for a clearly visible gradient
       instead of a near-flat tint. */
    background: linear-gradient(to right, #d6cabe 0%, #9c8a79 100%);
  }
}

/* Photo frame pinned near the true left screen edge — independent of
   .wrap's centered column, same way the text block on the right is now
   also positioned outside of straightforward centering. True desktop
   only (see the 1360px threshold below): there's no room for a side
   photo once the intro text goes full-width on narrower screens, so
   it's hidden there rather than left to fall into normal flow as an
   unstyled full-width image. */
.prose-frame-photo { display: none; }
@media (min-width: 1360px) {
  .prose-frame-photo {
    display: block;
    position: absolute;
    /* Centered in the leftover space between the screen edge and the text
       column's own left edge, not pinned near the edge with a fixed pad.
       That space's left bound is always 0; its right bound (the text's
       left edge) is (100vw + --max-width)/2 - .wrap's padding - .prose-
       wide's max-width = 50vw + 590px - 24px - 820px = 50vw - 254px — but
       only once --max-width (1180px) actually binds, i.e. viewport
       >=1180px; below that .wrap stops centering and the formula drifts
       further from reality the narrower it gets. Photo center = that
       bound / 2 = 25vw - 127px; left edge is center minus half the
       photo's own 380px width = 25vw - 317px. That leftover space only
       exceeds 380px once viewport is roughly >=1270px (at exactly 1180px
       it's already short, at 1024px — a common tablet landscape width —
       it's negative, pushing the photo off-screen to the left and
       clipping it instead of placing it beside the text), which is why
       this whole rule now waits for 1360px: that leaves a real margin
       past the 1270px point where the numbers only just work, since
       even there clearance from the text is razor-thin (440px was tried
       earlier and overlapped outright). Widened from 360px anyway since
       cover was cropping too tight at the sides; if 1360px-wide windows
       turn out to visibly clip, the width needs to come back down. */
    left: calc(25vw - 317px);
    /* Full section height, edge to edge — .prose-frame sits directly
       between the page-hero banner above and the grey Vox Mundi section
       (.alt) below, so top:0/bottom:0 makes the photo start exactly where
       the banner ends and run down to meet the grey section, instead of
       stopping short to match the text block's height. */
    top: 0;
    bottom: 0;
    width: 380px;
    border-radius: 0;
    overflow: hidden;
    box-shadow: none;
    z-index: 1;
  }
  .prose-frame-photo img {
    width: 100%;
    height: 100%;
    /* Back to cover (not contain): the photo should fill the frame edge
       to edge, top to bottom, matching how it looked before — contain's
       letterboxing meant the visible photo no longer touched the frame's
       own top/bottom edges. The frame itself is wider now (see width,
       below) specifically to keep this from cropping too tight at the
       sides again. */
    object-fit: cover;
    object-position: center 20%;
  }
}

/* Mobile and tablet (including tablet landscape, up to 1359px): photo as
   a normal block, stacked after the text (not mixed with/behind it —
   there's no room for the desktop side-by-side layout below 1360px, see
   .prose-frame-photo's min-width:1360px rule above for why that specific
   number). flex + order visually reorders photo-after-text without
   touching the DOM, which stays photo-first/text-second (needed for the
   desktop layout's left/right positioning). */
@media (max-width: 1359px) {
  .prose-frame {
    display: flex;
    flex-direction: column;
  }
  .prose-frame .wrap { order: 1; }
  .prose-frame-photo {
    display: block;
    position: static;
    order: 2;
    margin: 24px 24px 0;
    aspect-ratio: 4 / 3;
    overflow: hidden;
  }
  .prose-frame-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 20%;
  }
}

.prose-frame-anchor {
  position: relative;
  height: 0;
}
.prose-frame-t {
  position: absolute;
  top: -19px;
  left: 50%;
  transform: translate(-50%, 0);
  width: min(1800px, 160vw);
  height: auto;
  opacity: 0.12;
  filter: none;
  pointer-events: none;
  z-index: 0;
}
@media (min-width: 861px) {
  .prose-frame-t { top: -34px; }
}
.prose-frame .wrap { position: relative; z-index: 1; }
.prose-frame .prose-wide > p,
.prose-frame .prose-wide > .read-more-toggle,
.prose-frame .prose-wide > .prose-more {
  position: relative;
  z-index: 1;
}

.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 56px;
  align-items: center;
}

.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}

.grid-quotes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
}

.quote-card {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.4rem;
  line-height: 1.4;
  margin: 0;
  color: var(--color-on-dark);
  background: var(--color-surface-dark);
  border-radius: 0;
  border-left: 3px solid var(--color-accent);
  padding: 34px 34px 30px;
  box-shadow: none;
}

.quote-card::before {
  content: '\201C';
  display: block;
  font-style: normal;
  font-size: 3.4rem;
  line-height: 1;
  color: var(--color-accent);
  margin-bottom: 0.05em;
}

/* "Das Ensemble" image + text row: a single static (non-sticky) image,
   framed like the "Vox Mundi" / Saison 2026/27 photo, living entirely
   inside the Das Ensemble row. "Kammermusik für alle" is a plain sibling
   section below — NOT part of this grid — so it can never overlap the
   image no matter how tall the photo is relative to the Ensemble text;
   it just starts wherever the row above it ends, in normal document flow.

   Markup order is Ensemble text -> image, which is also the mobile
   stacking order once the grid collapses to a plain block stack below
   860px (Kammermusik follows right after as a normal sibling, completing
   the Ensemble-text -> image -> Kammermusik-text order). Desktop uses
   grid-template-areas to place the image in its own left-hand column
   next to the Ensemble text, without changing DOM order. */
/* Gold dots straddling this section's own top/bottom edges: centered
   exactly on the boundary (0px / 100%) so the background clips off the
   half that falls outside the box, reading as if the circle is half-hidden
   behind the section above/below. Kept on the box edges rather than a
   page-height percentage (see body.page-home) so they stay put regardless
   of viewport or content changes elsewhere on the page. */
.ensemble-kammermusik-wrapper {
  padding: 0;
  background-image:
    radial-gradient(circle 50px at 14% 0px, rgba(212,160,50,0.26) 0%, rgba(212,160,50,0.26) 96%, transparent 100%),
    radial-gradient(circle 30px at 82% 0px, rgba(212,160,50,0.20) 0%, rgba(212,160,50,0.20) 96%, transparent 100%);
  background-repeat: no-repeat;
}

/* Same "half-hidden" cut, on this row's own bottom edge — which is exactly
   where the Kontakt teaser begins right after it in the markup — so this
   dot appears to disappear behind that section. */
.ensemble-split {
  background-image: radial-gradient(circle 44px at 88% 100%, rgba(212,160,50,0.22) 0%, rgba(212,160,50,0.22) 96%, transparent 100%);
  background-repeat: no-repeat;
}

@media (min-width: 861px) {
  .ensemble-split {
    --split-image-height: 512px; /* ADJUST HERE: photo's own height. 80% of the previous
       640px (a deliberate 20% shrink) — keep --split-media-col-width below
       scaled by the same factor whenever this changes, so the frame's
       aspect ratio (and thus its cropping) stays the same as before. */
    --split-media-v-padding: 64px; /* ADJUST HERE: vertical breathing room above and below
       the photo — bumped up from 48px now that the photo itself is
       smaller, so the row doesn't just shrink by the same amount and end
       up feeling cramped; this is also effectively the section's own
       top/bottom padding, since the row's height derives from it. */
    --split-inward-shift: 1cm; /* ADJUST HERE: nudges the photo right and the text box
       left by this same amount, toward the section's center — both draw
       from this single value (via --split-gutter and
       --split-media-col-width below) so they always move together and
       stay symmetric. Increase to pull them closer to center; 0 restores
       the previous flush-at-the-gutter positions. */
    --split-gutter: calc(24px + var(--split-inward-shift)); /* ADJUST HERE: outer gutter on
       BOTH ends of this row — the photo's left inset from the viewport
       edge and the text block's right inset from the viewport edge draw
       from this same value, so the section stays symmetric (matching
       gaps on its left and right extremes) no matter what it's changed
       to. Unrelated to --split-col-gap below (that's the gap BETWEEN the
       two columns, this is the gap at the row's own outer edges). */
    --split-col-gap: 24px; /* Gap between the photo and text columns — floor value for
       861-1023px (tablet/narrow-desktop), overridden wider at 1024px and
       1400px below. Scaled down from 32/48/64 to 24/40/56 to match the
       smaller photo — a big gap next to a smaller photo read as too
       distant. Plain padding-left on the text column (see
       .ensemble-split__ensemble), not a grid column-gap. */
    --split-media-col-width: calc(307px + var(--split-inward-shift)); /* Photo column's
       width, FIXED (not a % of the row) — 283px rendered photo (80% of
       the previous 354px) + this row's own --split-gutter inset (itself
       24px + --split-inward-shift). Growing this by exactly
       --split-inward-shift, the same amount --split-gutter grows by,
       cancels out: the photo's own rendered WIDTH stays 283px, only its
       position shifts right. A fixed px column also means capping the
       row's own width below (max-width + margin: auto, for centering)
       can never resize the photo — only the text column (1fr) absorbs
       whatever space that capping removes or the row not being capped
       adds. Overridden at 1024px and 1400px below with the equivalent
       80%-scaled widths for those tiers. */
    display: grid;
    grid-template-columns: var(--split-media-col-width) 1fr;
    grid-template-areas: "media ensemble";
    /* Row auto-sizes to the taller of the two columns anyway (the photo
       plus its padding, per the brief), but min-height makes that
       explicit and guarantees it even if the Ensemble text ever gets
       long enough to matter. */
    min-height: calc(var(--split-image-height) + (var(--split-media-v-padding) * 2));
    /* Same content max-width as .wrap everywhere else on the site,
       centered the same way, so the photo+text pair reads as centered on
       the page instead of pinned toward the left on wide screens. Safe
       to do now that the media column is a fixed px (see above) — this
       only ever changes how much room the TEXT column (1fr) gets, never
       the photo's own size. */
    max-width: var(--max-width);
    margin: 0 auto;
  }

  @media (min-width: 1024px) {
    .ensemble-split {
      --split-col-gap: 40px;
      --split-media-col-width: calc(435px + var(--split-inward-shift)); /* 411px photo
         (80% of the previous 514px, its "before" width at ~1280px
         viewport) + 24px gutter, plus the shared inward shift. */
    }
  }
  @media (min-width: 1400px) {
    .ensemble-split {
      --split-col-gap: 56px;
      --split-media-col-width: calc(542px + var(--split-inward-shift)); /* 518px photo
         (80% of the previous 648px, its "before" width at ~1600px
         viewport and beyond, frozen here rather than continuing to grow
         past 1400px) + 24px gutter, plus the shared inward shift. */
    }
  }

  .ensemble-split__ensemble {
    grid-area: ensemble;
    /* Row height comes from the photo column (see above), which is
       almost always taller than the Ensemble text — center that text
       vertically within it instead of leaving it pinned to the top with
       empty space below. Horizontally: align-items: flex-end pins the
       text box's RIGHT edge to sit --split-gutter from this row's own
       right edge — the same inset the photo has from the row's left edge
       (see .ensemble-split__media below) — so the two outer edges read
       as symmetric. The trade-off: the photo-to-text GAP is no longer
       pinned to exactly --split-col-gap (it's whatever's left over once
       the fixed-width text box is pushed flush right, which is often
       more than that on wide screens) — if the two ever need to both be
       exact at once, the text box would have to stop being a fixed
       width. padding-left doubles as a FLOOR on the gap either way:
       flex-end can only push the box further from the photo than that,
       never closer, since the box can't overflow past its own padding
       box. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-end;
    padding: 0 var(--split-gutter) 0 var(--split-col-gap);
  }

  .ensemble-split__ensemble .wrap {
    max-width: 400px; /* ADJUST HERE: fixed text box width (matches the h2's widest
       line, "Fünf Musiker*innen," ~395px at this font size) — kept as-is
       per the brief (photo and text box both stay their current size;
       --split-col-gap above is the only thing that moves). */
    margin: 0;
    padding: 0;
  }

  .ensemble-split__media {
    grid-area: media;
    /* align-self defaults to stretch, so this box's height matches the
       row's min-height (photo + top/bottom padding) — the flex centering
       below only matters if the Ensemble text column ever grows taller
       than that and stretches the row further. */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--split-media-v-padding) 0 var(--split-media-v-padding) var(--split-gutter); /* left
       inset from the viewport edge — kept equal to .ensemble-split__ensemble's
       right padding via the shared --split-gutter var, see .ensemble-split
       above, so the row is symmetric. Unrelated to the top/bottom padding,
       which is its own value (--split-media-v-padding), and unrelated to
       --split-col-gap (the column-to-column gap lives on the text side
       only, so the photo column's own width is never touched by it). */
  }

  /* Same rounded-corner card as the "Vox Mundi" / Saison 2026/27 photo
     (.hero-photo, defined above under "Photo frame (shared)") — reused
     as-is for border-radius/background, then several bits overridden:
     aspect-ratio (that recipe defaults to 4/5; here the height comes from
     --split-image-height instead), box-shadow (~90% lighter than the
     shared --shadow-photo — see below), and object-position (that
     recipe's 20%-from-top bias was tuned for the Vox Mundi portrait
     specifically, not this photo). Specificity is bumped to
     .ensemble-split__frame.hero-photo throughout (here and in the mobile
     reset below) so these overrides reliably beat the site's other
     .hero-photo media-query rules regardless of source order. */
  .ensemble-split__frame.hero-photo {
    width: 100%;
    height: var(--split-image-height);
    aspect-ratio: auto;
    /* --shadow-photo at ~10% of its normal opacity (0.28 -> 0.028,
       0.16 -> 0.016) rather than touching that shared variable, which
       other .hero-photo photos on the site still use at full strength. */
    box-shadow: 18px 26px 44px rgba(16,20,26,0.028), 7px 12px 20px rgba(16,20,26,0.016);
  }

  .ensemble-split__frame img { object-position: center center; }
}

@media (max-width: 860px) {
  /* .hero-photo's rounded corners (border-radius: 28px, from its base
     rule under "Photo frame (shared)") are kept here deliberately, so
     the photo looks the same — soft corners — on every device. Only
     aspect-ratio and the shadow/background are overridden: aspect-ratio
     because the site's own "@media (max-width: 860px) { .hero-photo {
     aspect-ratio: 4/3 } }" rule further down this file would otherwise
     win that tie (same specificity, comes later) and squash the photo's
     height; shadow/background because this photo sits full-width in the
     flow here rather than floating like the Vox Mundi card. Compound
     selector (.ensemble-split__frame.hero-photo, not just
     .ensemble-split__frame) is what gives these the specificity to beat
     that other rule. */
  .ensemble-split__frame.hero-photo {
    box-shadow: none;
    background: none;
    aspect-ratio: auto;
  }

  .ensemble-split__ensemble {
    padding-bottom: 40px; /* ADJUST HERE: shrinks the default section { padding: 88px 0 }
       bottom gap between the "Alle Mitglieder kennenlernen" button and
       the photo right below it, to match the 56px gap the "Konzept
       entdecken" button has above the Vox Mundi / Saison 2026/27 image
       in its section (.grid-2's gap, unaffected by this — that section
       stacks to one column here but keeps its 56px grid gap either way).
       padding-top is left at the default 88px; only asked to close up
       the bottom. */
  }

  .ensemble-split__media {
    padding: 0 24px 48px; /* ADJUST HERE: 24px keeps the photo off the screen's left/
       right edges (same gutter .wrap uses everywhere else on the site).
       48px on the bottom stops the photo from butting straight up
       against Kammermusik für alle's --color-bg-alt background right
       below it — there's already an equivalent gap above the photo for
       free, from Das Ensemble's own section { padding: 88px 0 }, so only
       the bottom needed a value added here. */
  }

  .ensemble-split__media img {
    width: 100%;
    height: 50vh;
    height: 50dvh;
    object-fit: cover;
    object-position: center center;
  }
}

/* "Kommende Konzerte" section: full-bleed muted video background.
   aspect-ratio keeps the section's own box the same shape as the source
   footage (16:9), so object-fit: contain has nothing to letterbox at
   desktop/tablet-landscape widths. Below the 1024px breakpoint the
   stacked concert-card list is taller than a 16:9 box can hold without
   clipping it (overflow: hidden + aspect-ratio would otherwise lock the
   box to the ratio's height and cut the content off), so aspect-ratio is
   dropped there in favor of a content-driven height with a min-height
   floor — the section keeps that same height. object-fit stays
   "contain" at every breakpoint (rather than switching to "cover") so
   the video never crops or zooms in on the narrower/taller box: it
   letterboxes instead, like a filmstrip — dark bar, video at its own
   16:9 size, dark bar. A small transform: scale() on the video itself
   in the 1024px/600px queries (below) enlarges that filmstrip a little
   past its exact contain size — trimming the letterbox bars down and
   mildly cropping the video's left/right edges (kept inside the box by
   this section's own overflow: hidden) — bigger on phones than on
   tablets, since the section is at its narrowest/tallest there. The
   frame + overlay + glass concert-cards stay full-bleed across the
   entire section at every breakpoint, same as desktop. */
.kommende-konzerte-video {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Compound selector so this beats "section.alt { background: var(--color-bg-alt) }"
   (line ~686) on specificity — same class count, but that rule also carries an
   element selector, so plain ".kommende-konzerte-video" alone always lost to it
   regardless of source order. That bug was invisible before: at desktop/tablet-
   landscape the video's own aspect-ratio matched the box exactly (nothing to show
   through), and below 1024px object-fit: cover filled the box edge to edge. Now
   that object-fit: contain letterboxes below 1024px, the letterbox bars must
   actually be this dark colour, not the light --color-bg-alt "alt section" grey. */
section.alt.kommende-konzerte-video {
  background: var(--color-surface-dark-cool);
}
.kommende-konzerte-video-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}
.kommende-konzerte-video-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center center;
}
.kommende-konzerte-video-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(120deg, rgba(18, 20, 23, 0.75), rgba(18, 20, 23, 0.55));
  z-index: 1;
}
.kommende-konzerte-video-content {
  position: relative;
  z-index: 2;
}
.kommende-konzerte-video .eyebrow { color: var(--color-on-dark-soft); }
.kommende-konzerte-video h2 { color: var(--color-on-dark); }

/* Glass effect for the concert-card list here only — everywhere else
   (konzerte.html, the WIP homepage drafts) .concert-card keeps its solid
   background, since there's no video behind it to show through. Barely
   any blur, so the video stays sharp/readable through the card instead
   of turning into a frosted/sandy smear — the glass look comes mostly
   from the low-opacity tint plus a thin bright edge highlight. */
.kommende-konzerte-video .concert-card {
  background: rgba(54, 59, 64, 0.22);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  border-top: 1px solid rgba(255, 255, 255, 0.18);
  border-right: 1px solid rgba(255, 255, 255, 0.08);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

@media (max-width: 1024px) {
  .kommende-konzerte-video {
    aspect-ratio: auto;
  }
  /* Letterbox bars only, not the video itself: --color-surface-dark-cool
     (used at every breakpoint, incl. desktop where it's never actually
     visible) reads as dark grey rather than black. Overridden here to a
     near-black — the same rgb(18,20,23) already used elsewhere in this
     section for the desktop overlay tint — so the bars above/below the
     letterboxed video read as black/very dark grey, per request. This
     only repaints the empty space around the video; the video's own
     size, position and colours are untouched. */
  section.alt.kommende-konzerte-video {
    background: #121417;
  }
  .kommende-konzerte-video-bg {
    /* Nudges the letterboxed filmstrip bigger than its exact "contain"
       size (see comment above .kommende-konzerte-video). Center-scaling
       trims the dark bars a little and lets the video bleed slightly
       past the box's left/right edges, clipped by overflow: hidden on
       .kommende-konzerte-video — a small, deliberate crop, not a full
       switch back to cover. */
    transform: scale(1.18);
  }
  .kommende-konzerte-video-content {
    padding: 56px 0;
  }
}

@media (max-width: 600px) {
  .kommende-konzerte-video {
    min-height: 70vh;
  }
  .kommende-konzerte-video-bg {
    /* Bigger still on phones specifically (vs. 1.18 for tablet) — the
       section is at its tallest/narrowest here, so the plain "contain"
       filmstrip would be at its thinnest without this. */
    transform: scale(1.45);
  }
  .kommende-konzerte-video-content {
    padding: 40px 0;
  }
}

/* ---------- Cards ---------- */

.card {
  background: var(--color-surface);
  border-radius: 12px;
  padding: 32px;
  box-shadow: var(--shadow-sm);
}

.program-card {
  background: var(--color-surface-dark-cool);
  color: var(--color-on-dark);
  border-radius: 14px;
  border-left: 3px solid var(--color-accent);
  padding: 28px 26px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  box-shadow: none;
  transition: transform 0.4s ease;
  /* All four programme cards share one height (tallest content —
     Programm IV's longer works list — sets the floor) so the two
     grid-2 rows line up instead of each row sizing to its own pair. */
  min-height: 560px;
}

.program-card:hover {
  transform: translateY(-6px);
}

.program-card .num {
  font-family: var(--font-display);
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  color: var(--color-accent);
}

.program-card h3 { font-size: 1.35rem; margin-bottom: 0; color: var(--color-on-dark); }

.program-card .works {
  font-size: 0.88rem;
  color: var(--color-on-dark-soft);
  border-top: 1px dashed var(--color-line-dark);
  padding-top: 12px;
  white-space: pre-line;
}

@media (max-width: 860px) {
  .program-card { min-height: 0; }
}

/* ---------- Members ---------- */

/* Flexbox instead of .grid-3's plain grid so an incomplete last row (e.g.
   5 members = a trailing row of 2) centers instead of hugging the left. */
.member-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.member-grid .member-card {
  flex: 0 1 calc((100% - 64px) / 3);
}

/* Cascade: each card carries its own .reveal (instead of one .reveal on
   the whole grid), so they all cross the IntersectionObserver threshold
   together but fade in one after another via increasing delay. */
.member-grid .member-card:nth-child(1) { transition-delay: 0s; }
.member-grid .member-card:nth-child(2) { transition-delay: 0.08s; }
.member-grid .member-card:nth-child(3) { transition-delay: 0.16s; }
.member-grid .member-card:nth-child(4) { transition-delay: 0.24s; }
.member-grid .member-card:nth-child(5) { transition-delay: 0.32s; }

@media (max-width: 860px) {
  .member-grid .member-card { flex: 1 1 100%; }
}

/* Landscape phones (861-1023px, see the nav breakpoint comment further
   down for why that's the real width range every rotated phone falls
   into): 3 columns squeezes each .member-avatar down to ~271px — a full
   23% narrower than either the 1-column mobile-portrait photo (~342px)
   or the 3-column desktop photo (~356px), which is the size range each
   member's hand-signature was actually sized against (measured live:
   Fraynni's is 29% of avatar width, Joonas's 50%, etc. — those
   percentages read as intentionally sized, not oversized, only at
   avatar widths in that ~340-360px range). Below it, the same
   percentage signature reads as visibly oversized relative to the
   now-small photo — not a proportion bug in the CSS math (the ratio is
   identical at every width), just that a fixed fraction of a photo
   needs to shrink, not stay constant, once the photo itself is this
   much smaller, to keep reading as balanced. Dropping to 2 columns
   here restores each avatar to ~423px — bigger than the reference
   range, if anything, comfortably fixing it. */
@media (min-width: 861px) and (max-width: 1023px) {
  .member-grid .member-card { flex: 0 1 calc((100% - 32px) / 2); }
}

.member-card {
  text-align: center;
}

.member-avatar {
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 18px;
  background: var(--color-bg-alt);
  overflow: hidden;
  margin-bottom: 20px;
  /* --shadow-photo at 20% of its normal opacity (0.28 -> 0.056,
     0.16 -> 0.032) — same offsets/blur, just 80% lighter. */
  box-shadow: 18px 26px 44px rgba(16,20,26,0.056), 7px 12px 20px rgba(16,20,26,0.032);
  perspective: 1400px;
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
  transition: transform 0.35s ease, box-shadow 0.35s ease;
}

.member-avatar:hover,
.member-avatar:focus-visible {
  transform: translateY(-6px);
  box-shadow: 28px 40px 60px rgba(16,20,26,0.068), 12px 20px 30px rgba(16,20,26,0.04);
}

.member-avatar:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
}

/* Click-to-flip photo, same "tornado" mechanic as the Konzerte program
   cards and Kontakt cards (shared .flip-card JS in main.js) — front face
   is the portrait, back face reveals the bio. Only the photo itself
   flips; name/role below stay put so you keep the caption while reading. */
.member-avatar.flip-card { max-width: none; margin-bottom: 0; }

.member-avatar .flip-inner { width: 100%; height: 100%; }

.member-avatar .flip-front,
.member-avatar .flip-back {
  padding: 0;
  box-shadow: none;
  border-radius: 0;
  min-height: 0;
}

.member-avatar .flip-front {
  position: relative;
  border-left: none;
  background: var(--color-bg-alt);
}

.member-avatar .flip-front img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 15%;
  display: block;
}

/* Opt out of the site-wide grayscale-bloom-to-color hover effect (see
   the "Signature effect" rule on the base img selector) for member
   photos specifically — these stay in full color always. The extra
   .member-avatar .flip-front classes give this higher specificity
   than the base img:hover/:focus-visible rule, so it wins regardless
   of source order, in both the resting and hovered states. */
.member-avatar .flip-front img,
.member-avatar .flip-front img:hover,
.member-avatar .flip-front img:focus-visible {
  filter: none;
}

.member-flip-hint {
  position: absolute;
  right: 14px;
  bottom: 14px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(20, 20, 22, 0.55);
  color: var(--color-on-dark);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.member-avatar .flip-back {
  background: var(--color-surface-dark-cool);
  color: var(--color-on-dark);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 22px;
  gap: 14px;
}

.member-avatar .flip-back .bio-placeholder {
  font-size: 0.88rem;
  font-style: italic;
  color: var(--color-on-dark-soft);
  border: 1px dashed var(--color-line-dark);
  border-radius: 8px;
  padding: 14px 16px;
  background: transparent;
  box-shadow: none;
}

/* Real bio copy (once written) replaces .bio-placeholder with this
   instead — .member-avatar is a fixed aspect-ratio: 4/5 box with
   overflow: hidden, too short to fit a full multi-paragraph bio at
   once, so this scrolls internally rather than clipping or shrinking
   the text illegibly. flex: 1 1 auto + min-height: 0 is what lets it
   shrink to the space actually left in the flip-back column (after the
   "← Zurück" hint) and scroll within that, rather than overflowing past
   the card's edge. Left-aligned (flip-back itself is centered, fine for
   the placeholder/hint, but not for multi-sentence prose). */
.member-avatar .flip-back .member-bio {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  overflow-y: auto;
  text-align: left;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--color-on-dark-soft);
}

.member-avatar .flip-back .member-bio p { margin: 0 0 0.85em; }
.member-avatar .flip-back .member-bio p:last-child { margin-bottom: 0; }

.member-avatar .flip-back .flip-hint {
  margin-top: 0;
  color: var(--color-on-dark-soft);
}

.member-card h3 { margin-bottom: 0.15em; font-size: 1.2rem; }
.member-card .role { color: var(--color-text-soft); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 14px; }

/* Experimental: name "written" as a gold signature over the photo
   itself, live, on hover — not a persistent caption. Each member's
   signature is a real hand-drawn SVG (assets/img/Firma <name>
   vertical.svg, supplied by the user, traced as-is in its own source
   coordinate space) broken into its individual ink strokes, one
   <path> per stroke rather than one combined path for the whole
   name. A single combined path reveals in overall arc-length order,
   which isn't the same thing as reading order once a name's strokes
   loop and overlap in x — early attempts had letters further right
   filling in before an earlier letter had finished. Each source file
   has its strokes in whatever order the design tool exported them
   (not necessarily reading order), so before wiring each one up every
   path gets rendered in isolation and in combination to work out the
   real left-to-right/top-to-bottom sequence by eye — see the git log
   for this file's history for that process on each member's signature.
   Once the order's known, each <path> gets an explicit inline
   transition-delay (and, since stroke complexity varies a lot within
   a name, its own transition-duration too) so stroke N can't begin
   until stroke N-1's delay + duration has elapsed. --sig-len (per
   stroke, via the getTotalLength() loop in main.js) is what
   dasharray/dashoffset are set to, so each stroke's own hidden/drawn
   endpoints are exact regardless of its length. The pen dot moves
   along the full combined path via offset-path/-distance at the same
   total duration as every stroke end to end, so it arrives right as
   the last one finishes. Plain :hover/:focus-visible + transition (no
   keyframes) throughout, so leaving the photo reverses everything for
   free. --sig-len has a fallback so it degrades gracefully if JS is
   off (dash math just won't be pixel-exact for the reveal). */
.member-signature {
  position: absolute;
  overflow: visible;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

/* .signature-shown: once a signature has been drawn by hovering it
   the first time (desktop), it stays drawn even after the cursor
   moves away — main.js adds this class on the first hover/focus and
   never removes it (hover-capable devices only; touch's
   scroll-driven .signature-active show/hide further down is
   untouched). Not media-gated since it should apply wherever the
   class ends up. */
.member-avatar:hover .member-signature,
.member-avatar:focus-visible .member-signature,
.member-avatar.signature-shown .member-signature {
  opacity: 1;
}

/* Touch/no-hover devices (tablet, mobile) can't hover, so the signature
   instead draws itself as each photo scrolls through view — main.js
   toggles .signature-active via IntersectionObserver, touch-only (see
   the "Member signature scroll trigger" block). Scoped to
   (hover: none) so it can never affect the desktop cursor-driven
   version above, even if the class were somehow present there. */
@media (hover: none) {
  .member-avatar.signature-active .member-signature {
    opacity: 1;
  }
}

/* Fraynni: from assets/img/Firma Fraynni vertical 1.svg — replaces an
   earlier version traced from the same-named "vertical.svg" (which
   itself had replaced an even earlier version traced from the Meddon
   font). 13 strokes, "Fraynni" over "Rui", upright like the other
   members' real signatures (no added rotation). Sits top-right, over
   the plain beige backdrop instead of her body/violin (her new photo
   has open background there but not in the top-left quadrant every
   other member's signature uses), smaller than before to stay clear
   of the violin scroll lower down, and in near-black ink instead of
   gold since it's sitting on a light background, not a dark one. */
.member-signature--fraynni {
  top: 18px;
  left: auto;
  right: 24px;
  width: 96px;
  max-width: 29%;
  height: auto;
}

.member-signature--fraynni .signature-stroke {
  stroke: var(--color-text);
  stroke-width: 3;
}

.member-signature--fraynni .signature-stroke--filled {
  fill: var(--color-text);
}

.member-signature--fraynni .signature-pen {
  fill: var(--color-text);
}

/* Fiore: a real hand-drawn signature (assets/img/Firma Fiore vertical.svg,
   supplied by the user, not generated) traced as-is — same coordinate
   space as the source file, just recolored and animated. Sits upright,
   no added rotation, since the cursive already has its own natural
   upward slant. Positioned top-right over the plain beige backdrop and
   sized to fit it without touching her hair/body, and inked in
   near-black at the same stroke weight as Fraynni's, matching that
   treatment — her photo's open background is on the right side, not
   the top-left every other member's signature uses. top/right are
   pulled in tighter than Fraynni's (whose photo has more clear space)
   to leave the signature as large as possible while staying clear of
   her hair, which curls closer to this corner and, starting around
   eye height, caps how wide the box can be before grazing it — sized
   past that cap here by explicit request, so its lower-left corner
   grazes a few loose strands. */
.member-signature--fiore {
  top: 47px;
  left: auto;
  right: -15px;
  width: 142px;
  max-width: 40%;
  height: auto;
}

.member-signature--fiore .signature-stroke {
  stroke: var(--color-text);
  stroke-width: 3;
}

.member-signature--fiore .signature-stroke--filled {
  fill: var(--color-text);
}

/* Every stroke in Fraynni's signature is a filled letter-shape, so its
   fill (not just the outline) is what reads as "ink weight" — a 3px
   stroke there is already bold because it's outlining a solid shape.
   The "avaro" of Favaro, though, is traced as two open, unfilled
   pen-line strokes with no fill to give them bulk, so the same 3px
   reads visibly thinner. Bumped up so the apparent thickness — not
   just the property value — matches the rest of the signature. */
.member-signature--fiore .signature-stroke--bold {
  stroke-width: 6;
}

.member-signature--fiore .signature-pen {
  fill: var(--color-text);
}

/* Aleck: same idea, from assets/img/Firma Aleck vertical.svg — just
   "Aleck" over "Carratta", no extra flourish strokes to sequence. */
.member-signature--aleck {
  top: 16px;
  left: 16px;
  width: 136px;
  max-width: 42%;
  height: auto;
  transform: rotate(-8deg);
}

.member-signature--aleck .signature-stroke {
  stroke: var(--color-text);
  stroke-width: 1.5;
}

.member-signature--aleck .signature-stroke--filled {
  fill: var(--color-text);
}

.member-signature--aleck .signature-pen {
  fill: var(--color-text);
}

/* Tablet portrait only (641-860px — narrower and this is a phone, where
   the signature already reads fine at its normal size; ≥861px and
   .member-grid is back to multiple columns, giving a photo no wider
   than the size this signature was tuned against — see the "Member
   grid, landscape phones" comment in the responsive section). In this
   range .member-grid is still a single column, so the photo (and Aleck's
   face in it) renders much larger than at any other width, and the
   signature's own top-left corner — sized/positioned to read well on
   the smaller desktop/phone version of this photo — starts overlapping
   his face — since it's the one signature this was reported on, not the
   others. Went through a few wrong turns before landing here: `left`
   pushed negative (-1mm/-2mm, trying to pull it away from his face)
   just got clipped at the frame's own edge instead, because
   `.member-avatar` is `overflow: hidden` — the signature never actually
   moved further left, it was silently cut off there, which read as
   "touching the edge". `left` has to stay >=0. With that constraint,
   the only lever left to clear his face is size: shrunk to 90px/28%
   (~34% smaller than the base 136px/42%) at a small positive inset
   (16px -> 6px) that stays safely clear of the frame's clip boundary,
   then sized back up 10% (90px/28% -> 99px/31%) once that read as
   smaller than it needed to be — position (6px) unchanged, still clear
   of both his face and the clip boundary at this size. */
@media (min-width: 641px) and (max-width: 860px) {
  .member-signature--aleck {
    width: 99px;
    max-width: 31%;
    left: 6px;
  }
}

/* iPad Pro 12.9" portrait (1024px) and similar — a second blind spot the
   two fixes above don't reach. At >=1024px .member-grid reverts to its
   default 3 columns (no override needed there generally, see the
   861-1023px comment above), but right at the bottom of that range the
   photo is only ~304px — smaller than the ~340-360px this signature was
   tuned against, and on Aleck's specific photo (his face sits further
   left/up than the other members') that's enough on its own for the
   signature to touch his face even at its normal desktop size. Same
   values as the tablet-portrait fix above, same reason (see that
   comment) — `.member-avatar`'s `overflow: hidden` clips any negative
   `left` at the frame's edge instead of actually moving the signature,
   so size (90px/28%) is the only lever, with `left` at a small positive
   inset (6px) that stays clear of that same clip boundary. Upper bound
   (1140px) is where 3-column avatars grow back past ~360px and the
   default size reads fine again — verified live at 1024px (reported
   touching), 1024px with this override (no longer touching), and at
   1150px+ with no override (already fine, confirmed no regression). */
@media (min-width: 1024px) and (max-width: 1140px) {
  .member-signature--aleck {
    width: 90px;
    max-width: 28%;
    left: 6px;
  }
}

/* Alessandro: from assets/img/Firma Alessandro Vertical.svg — 9 paths,
   "Alessandro" broken into 4 fragments (Ale/ssa/n/dro) and "D'Amico"
   into 5 (D/apostrophe/Ami/dot on the i/co), reordered from source
   file order into reading order after checking each fragment and
   combination in isolation (same process as Fiore's signature).
   Inked in near-black at the same stroke weight as Fraynni's/Fiore's
   instead of the gold accent, and sized down 40%. Moved to the top-left
   corner, over the plain beige backdrop clear of his face/shoulder —
   the original position sat mostly over his black shirt, where black
   ink has poor contrast. */
.member-signature--alessandro {
  top: 32px;
  left: 5px;
  width: 132px;
  max-width: 39%;
  height: auto;
}

.member-signature--alessandro .signature-stroke {
  stroke: var(--color-text);
  stroke-width: 3;
}

.member-signature--alessandro .signature-stroke--filled {
  fill: var(--color-text);
}

.member-signature--alessandro .signature-pen {
  fill: var(--color-text);
}

/* Joonas: from assets/img/Firma Joonas vertical.svg — 17 real strokes
   (4 more in the source file were zero-length stray points and got
   skipped entirely). "Joonas" is 3 clean pieces; "Pitkänen" splits
   into 14, including the umlaut's two dots as separate tiny strokes
   from the "a" they sit above. */
.member-signature--joonas {
  top: 16px;
  left: 16px;
  width: 168px;
  max-width: 50%;
  height: auto;
}

.member-signature--joonas .signature-stroke {
  stroke: var(--color-text);
  stroke-width: 0;
}

.member-signature--joonas .signature-stroke--filled {
  fill: var(--color-text);
}

.member-signature--joonas .signature-pen {
  fill: var(--color-text);
}

.signature-stroke {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--sig-len, 200);
  stroke-dashoffset: var(--sig-len, 200);
  transition-property: stroke-dashoffset, fill-opacity;
  transition-duration: 0.32s, 0.18s;
  transition-timing-function: linear, ease;
  /* transition-delay (and, for Fiore, transition-duration too, since its
     strokes vary a lot more in length/complexity than one glyph to the
     next) is set inline per <path> in ensemble.html. */
}

/* Modifier for strokes that are solid ink shapes (Fraynni's letters,
   Fiore's main pen strokes) rather than thin decorative flourishes —
   fill fades in once the outline finishes drawing. Fiore's two small
   trailing loop strokes deliberately don't get this: they're open,
   single-width pen lines in the source SVG, not closed filled shapes,
   so filling them would render a stray sliver instead of a clean line. */
.signature-stroke--filled {
  fill: var(--color-accent);
  fill-opacity: 0;
}

.member-avatar:hover .signature-stroke,
.member-avatar:focus-visible .signature-stroke,
.member-avatar.signature-shown .signature-stroke {
  stroke-dashoffset: 0;
}

.member-avatar:hover .signature-stroke--filled,
.member-avatar:focus-visible .signature-stroke--filled,
.member-avatar.signature-shown .signature-stroke--filled {
  fill-opacity: 1;
}

@media (hover: none) {
  .member-avatar.signature-active .signature-stroke {
    stroke-dashoffset: 0;
  }

  .member-avatar.signature-active .signature-stroke--filled {
    fill-opacity: 1;
  }
}

.signature-pen {
  fill: var(--color-accent);
  offset-distance: 0%;
  opacity: 0;
  transition-property: offset-distance, opacity;
  /* 3.2s default = Fraynni's 10 letters * 0.32s each, so the pen arrives
     right as the last letter's stroke finishes. Fiore's pen overrides
     this inline to match her strokes' own total duration. */
  transition-duration: 3.2s, 0.15s;
  transition-timing-function: linear, ease;
}

.member-avatar:hover .signature-pen,
.member-avatar:focus-visible .signature-pen,
.member-avatar.signature-shown .signature-pen {
  offset-distance: 100%;
  opacity: 1;
}

@media (hover: none) {
  .member-avatar.signature-active .signature-pen {
    offset-distance: 100%;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .member-signature,
  .signature-stroke,
  .signature-pen {
    transition: none;
  }
}

/* Homepage: same gold-dot decoration as the Konzerte page, scattered at
   varied sizes across the "Das Ensemble" row, the one spot on this page
   (besides that row's own edges, see .ensemble-kammermusik-wrapper /
   .ensemble-split below) where the body background actually shows through
   — opaque sections elsewhere would just hide them. Kept fairly visible
   (not as faint as the Konzerte set) per request. */
body.page-home {
  background-image:
    radial-gradient(circle 14px at 90% 62%, rgba(212,160,50,0.32) 0%, rgba(212,160,50,0.32) 96%, transparent 100%),
    radial-gradient(circle 55px at 4% 72%, rgba(212,160,50,0.16) 0%, rgba(212,160,50,0.16) 96%, transparent 100%);
  background-repeat: no-repeat;
}

/* ---------- Calendar ---------- */

/* Faint gold dots echoing the logo's two circles: a small "twin dot" pair as
   a direct callback, plus scattered singles at varied sizes/depths */
body.page-konzerte {
  background-image:
    radial-gradient(circle 19px at 5% 8%, rgba(212,160,50,0.30) 0%, rgba(212,160,50,0.30) 96%, transparent 100%),
    radial-gradient(circle 14px at 8% 9.5%, rgba(212,160,50,0.22) 0%, rgba(212,160,50,0.22) 96%, transparent 100%),
    radial-gradient(circle 156px at 96% 96%, rgba(212,160,50,0.06) 0%, rgba(212,160,50,0.06) 96%, transparent 100%),
    radial-gradient(circle 60px at 90% 16%, rgba(212,160,50,0.12) 0%, rgba(212,160,50,0.12) 96%, transparent 100%),
    radial-gradient(circle 320px at 97% 52%, rgba(212,160,50,0.17) 0%, rgba(212,160,50,0.17) 96%, transparent 100%),
    radial-gradient(circle 46px at 7% 80%, rgba(212,160,50,0.16) 0%, rgba(212,160,50,0.16) 96%, transparent 100%),
    radial-gradient(circle 29px at 50% 4%, rgba(212,160,50,0.08) 0%, rgba(212,160,50,0.08) 96%, transparent 100%),
    radial-gradient(circle 24px at 66% 93%, rgba(212,160,50,0.20) 0%, rgba(212,160,50,0.20) 96%, transparent 100%);
  background-repeat: no-repeat;
}

.concert-month h3,
.concert-feature-cards h3 {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 1rem;
  color: var(--color-text-soft);
  border-bottom: 1px solid var(--color-line);
  padding-bottom: 10px;
  margin-bottom: 0;
  font-family: 'Raleway', var(--font-body);
  font-weight: 700;
}

.concert-month-cards { flex: 1; min-width: 0; }
.concert-feature-cards .concert-month-cards { margin-top: 20px; }

/* Konzerte hero layout: each month-group is a two-column row — a fixed-
   width cards column plus a photo that fills the rest of the row's
   width and bleeds most of the way to that side's true viewport edge
   (same formula as .prose-frame-photo's left-edge bleed elsewhere in
   this stylesheet: .wrap centers with max-width: 1180px/var(--max-width)
   and 24px padding, so once that binds, the gap from .wrap's inner edge
   to the viewport edge is 50vw - 566px; capped to leave 24px of
   clearance instead of going flush, folded into the 590px constant).
   Oktober/November and April/Mai/Juni keep cards on the left and bleed
   the photo right; Januar/Februar mirror it — photo on the left
   (bleeding left instead), cards on the right, normal DOM order (photo
   first) placing them correctly without flex-direction: row-reverse. */
.concert-feature {
  position: relative;
}

.concert-feature-split {
  display: flex;
  align-items: flex-start;
  gap: 28px;
  margin-top: 32px;
}

.concert-feature-split .concert-feature-cards { flex: 0 0 min(620px, 58%); min-width: 0; }

/* .concert-row's .place column is a 1fr grid track, which defaults to
   min-width: auto — it refuses to shrink below its longest unbreakable
   word. This layout's cards are narrower (620px, capped by the split
   next to the photo) than .concert-row gets elsewhere on the site, so a
   long one-word place name like "Begegnungszentrum" would force the
   whole row wider than its .flip-card container instead of wrapping.
   min-width: 0 lets the column shrink and the text wrap instead. */
.concert-feature-cards .concert-row .place { min-width: 0; }

/* Each month-group's cards share one min-height (the tallest card in
   that group, by programme length) so all cards in the group line up:
   Oktober/November's tallest is 211px, Januar/Februar's 258px,
   April/Mai/Juni's 281px — all measured live at 1280px width. */
.concert-feature-split:not(.concert-feature-split--mirror):not(.concert-feature-split--alt) .concert-feature-cards .flip-inner { min-height: 211px; }
.concert-feature-split--mirror .concert-feature-cards .flip-inner { min-height: 258px; }
.concert-feature-split--alt .concert-feature-cards .flip-inner { min-height: 281px; }

.concert-feature-photo {
  flex: 1;
  min-width: 0;
  height: 533px;
  margin-right: min(0px, calc(590px - 50vw));
  border-radius: 16px;
  background: #beada0;
  overflow: hidden;
}

.concert-feature-photo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
}

/* Oktober/November: margin-top of 344px centers the 533px frame against
   .concert-feature-cards' full extent — Oktober's heading through
   November's last card (1222px live at 1280px width) — not just the
   cards themselves, so the heading text is inside the centered span too. */
.concert-feature-split:not(.concert-feature-split--mirror):not(.concert-feature-split--alt) .concert-feature-photo {
  margin-top: 344px;
}

/* Same "lupa" zoom as April/Mai/Juni below — enlarges the photo 20%
   without enlarging the frame (object-fit: contain sizes it first,
   then the frame's overflow: hidden clips the excess). translateX
   nudges the zoomed photo left within that same clipped frame. */
.concert-feature-split:not(.concert-feature-split--mirror):not(.concert-feature-split--alt) .concert-feature-photo img {
  transform: scale(1.2) translateX(-2%);
}

/* Januar/Februar: konzerte-duo.jpg is a portrait shot, 1024x1433 — the
   533px fixed height (tuned for landscape photos) would leave thick
   empty vertical bars down both sides once contain fit it into that
   near-square box, so this group gets its own aspect-ratio instead,
   filling the frame edge to edge. margin-top of 490px centers the
   resulting 747px-tall frame against .concert-feature-cards' full
   extent (1727px live at 1280px width). */
.concert-feature-split--mirror .concert-feature-photo {
  height: auto;
  aspect-ratio: 1024 / 1433;
  margin-right: 0;
  margin-left: min(0px, calc(590px - 50vw));
  margin-top: 490px;
}

/* April/Mai/Juni: konzerte-quartet.jpg is landscape (1024x731) like
   November's photo, so it keeps the base 533px height — only margin-top
   differs, to center that frame against this (taller) group's full
   .concert-feature-cards extent. transform: scale(1.2) on the img is a
   deliberate "lupa" zoom — enlarges the photo without enlarging the
   frame, since object-fit: contain sizes it first and the frame's own
   overflow: hidden then clips the excess instead of growing to fit it. */
.concert-feature-split--alt .concert-feature-photo {
  margin-top: 994px;
}

.concert-feature-split--alt .concert-feature-photo img {
  transform: scale(1.2);
}

/* April/Mai/Juni only: three DOM children (April+Mai's cards, the
   photo, then Juni's cards as a second .concert-feature-cards) instead
   of the usual two, so this group overrides the base flex row for a
   grid — grid-template-areas lets the photo slot between April/Mai and
   Juni once the tablet/mobile breakpoint stacks to one column (see
   below), while staying a single full-height right-hand column here on
   desktop exactly as before the split. Row-gap 32px between the two
   card blocks reproduces the 32px margin-top Juni's <h3> used to carry
   back when both months shared one box. */
.concert-feature-split--alt {
  display: grid;
  grid-template-columns: min(620px, 58%) 1fr;
  grid-template-areas: "cardsA photo" "cardsB photo";
  gap: 32px 28px;
  align-items: start;
}
.concert-feature-split--alt .concert-feature-cards { grid-area: cardsA; }
.concert-feature-split--alt .concert-feature-cards--split-b { grid-area: cardsB; }
.concert-feature-split--alt .concert-feature-photo { grid-area: photo; }

.concert-card {
  background: var(--color-surface-dark);
  color: var(--color-on-dark);
  border-radius: 0;
  border-left: 3px solid var(--color-accent);
  padding: 20px 24px;
  margin-bottom: 12px;
  box-shadow: none;
}

.concert-card:last-child { margin-bottom: 0; }

.concert-row {
  display: grid;
  grid-template-columns: 110px 160px 1fr auto;
  gap: 20px;
  align-items: center;
}

.concert-row .date {
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  letter-spacing: 0.02em;
  font-size: 1.15rem;
  color: var(--color-on-dark);
}
.concert-row .time { color: var(--color-on-dark-soft); font-size: 0.92rem; }
.concert-row .place strong { display: block; color: var(--color-on-dark); }
.concert-row .place span { color: var(--color-on-dark-soft); font-size: 0.88rem; }
.concert-row .tag {
  justify-self: end;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-on-dark-soft);
  border: 1px solid var(--color-line-dark);
  padding: 5px 10px;
  white-space: nowrap;
  box-shadow: none;
}

/* Flip cards: click to reveal the program on the reverse ("tornado" effect) */

.flip-card {
  perspective: 1400px;
  max-width: 760px;
  margin: 0 auto 12px;
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
}

.flip-card:last-child { margin-bottom: 0; }

.flip-card:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
  border-radius: 10px;
}

.flip-inner {
  display: grid;
  transform-style: preserve-3d;
  transition: transform 0.6s cubic-bezier(.4, .2, .2, 1);
}

.flip-card.is-flipped .flip-inner { transform: rotateY(180deg); }

.flip-front,
.flip-back {
  grid-area: 1 / 1;
  min-height: 100%;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border-radius: 10px;
  box-shadow: var(--shadow-sm);
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Dark grey glass — opaque enough (0.85) to read as a solid card on its
   own, since these no longer sit on a dedicated dark panel; the thin
   bright top/side edges are what still sell "glass" rather than flat
   grey. Applied to every concert box on this page for one consistent
   look across all three month-groups. */
.flip-front {
  background: rgba(24, 24, 26, 0.85);
  color: var(--color-on-dark);
  border-left: 3px solid var(--color-accent);
  border-top: 1px solid rgba(255, 255, 255, 0.28);
  border-right: 1px solid rgba(255, 255, 255, 0.12);
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

.flip-back {
  background: var(--color-surface);
  color: var(--color-text-soft);
  transform: rotateY(180deg);
  display: grid;
  grid-template-columns: 1fr auto;
  align-content: space-between;
  column-gap: 14px;
}

.flip-back .concert-program { grid-column: 1; }
.flip-back .concert-musicians { grid-column: 2; }
.flip-back .flip-hint { grid-column: 1 / -1; }

.flip-hint {
  display: block;
  margin-top: 12px;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-soft);
  opacity: 0.65;
}

.flip-front .flip-hint {
  color: var(--color-accent);
  opacity: 0.9;
}

.concert-program {
  font-size: 0.86rem;
  color: var(--color-text-soft);
  line-height: 1.7;
}

.concert-musicians {
  font-size: 0.86rem;
  color: var(--color-text-soft);
  line-height: 1.7;
  text-align: left;
  white-space: nowrap;
  padding-left: 14px;
  border-left: 1px dashed var(--color-line);
}

@media (prefers-reduced-motion: reduce) {
  .flip-inner { transition: none; }
}

.calendar-note {
  margin-top: 40px;
  font-size: 0.9rem;
  color: var(--color-on-dark-soft);
  background: var(--color-surface-dark);
  border-left: 3px solid var(--color-accent);
  border-radius: 0;
  padding: 18px 20px;
  box-shadow: none;
}

/* ---------- Media ---------- */

/* Stacked video list, one large video per row with its title underneath —
   reads as a program of pieces rather than a flat grid of clips. On wide
   screens each row becomes a two-column video+title pair, alternating
   sides row to row (see the nth-child rule below) instead of stacking. */
.video-list {
  display: flex;
  flex-direction: column;
  gap: 56px;
  max-width: 860px;
  margin: 0 auto;
}

.video-item .video-frame { margin-bottom: 18px; }

.video-caption h3 {
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  font-size: clamp(1.1rem, 2.2vw, 1.4rem);
  color: var(--color-on-dark);
  margin: 0;
}

@media (min-width: 900px) {
  .video-list { max-width: 1100px; }

  .video-item {
    display: flex;
    align-items: center;
    gap: 48px;
  }

  .video-item:nth-child(even) { flex-direction: row-reverse; }

  .video-item .video-frame {
    flex: 1 1 58%;
    min-width: 0;
    margin-bottom: 0;
  }

  .video-caption { flex: 1 1 42%; }
}

.video-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  border-radius: 14px;
  overflow: hidden;
  box-shadow: var(--shadow-photo);
}

.video-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Masonry mural: a grid with a tiny fixed row unit, and JS (see main.js)
   gives each figure a grid-row-end span matched to its own image's
   rendered height, so tiles of very different aspect ratios still pack
   edge-to-edge without gaps. .gallery-featured tiles additionally span 2
   grid columns, so full-ensemble shots read as the mural's focal points
   rather than getting the same weight as a single portrait. */
.photo-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 8px;
  gap: 12px;
}

@media (min-width: 640px) {
  .photo-gallery {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 1000px) {
  .photo-gallery {
    grid-template-columns: repeat(4, 1fr);
  }
}

.photo-gallery figure {
  margin: 0;
  overflow: hidden;
}

.photo-gallery .gallery-featured {
  grid-column: span 2;
}

/* Spans 2 columns like .gallery-featured, but the image itself stays at
   single-column width, centered in the middle of that space — for a tile
   that's left with a wide leftover gap next to it (grid-auto-flow isn't
   dense) but shouldn't visually grow, just stop sitting flush left. */
.photo-gallery .gallery-centered {
  grid-column: span 2;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

.photo-gallery .gallery-centered img {
  width: calc(50% - 6px);
}

.photo-gallery img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 14px;
  box-shadow: var(--shadow-photo);
  transition: transform 0.3s ease, box-shadow 0.3s ease, filter 0.6s ease;
}

.photo-gallery img:hover {
  transform: scale(1.015);
}

/* Touch/no-hover devices: main.js adds .is-colored as each photo scrolls
   into view, since these devices never trigger the img:hover bloom above. */
.photo-gallery img.is-colored {
  filter: grayscale(0) contrast(1);
}

/* ---------- Medien: interactive gold-dot mosaic background ----------
   Starts right below the page-hero and runs behind the video/gallery
   sections. The canvas paints a porcelain-tile grid of dots (see
   assets/js/main.js) that glow gold near the cursor and fade back to
   black, like a trail lighting up as it passes over the wall. */
.mosaic-zone {
  position: relative;
  overflow: hidden;
  background: #2a2a2a;
}

.mosaic-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

.mosaic-content {
  position: relative;
  z-index: 1;
  color: var(--color-on-dark);
}

.mosaic-content .eyebrow { color: var(--color-on-dark-soft); }
.mosaic-content h2 { color: var(--color-on-dark); }

/* So the jump-nav anchors (#video, #gallery) land below the fixed header
   instead of flush against the top of the viewport. */
#video, #gallery { scroll-margin-top: 90px; }

/* ---------- Contact ---------- */

.contact-block {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 56px;
}


/* Kontakt hero lede: force it onto a single line on wider screens; back to
   normal wrapping below the point where it would overflow the viewport. */
body.page-kontakt .page-hero .lede {
  max-width: none;
  white-space: nowrap;
}

@media (max-width: 800px) {
  body.page-kontakt .page-hero .lede { white-space: normal; }
}

/* Kontakt: three vertical frosted-glass buttons (Kontakt / Social Media /
   Freundeskreis), right-aligned under the hero. The sofa group photo sits
   behind them, with a dark tint (same diagonal cool→warm hues as the old
   flat gradient) layered on top so the frosted-glass buttons still read
   clearly against it. */
.kontakt-actions {
  padding: 64px 0 88px;
  background-color: var(--color-surface-dark);
  background-image:
    url('../img/kontakt-hero-sofa.jpg'),
    linear-gradient(135deg, rgba(46,52,58,0.95), rgba(54,59,64,0.85) 55%, rgba(61,54,46,0.95));
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain, cover;
}

/* Row that holds the button column and the (usually empty) form area beside
   it. Order is used instead of DOM order so the toggle buttons still come
   before the panels they control in markup/tab order, while appearing to
   their right visually — form on the left, buttons on the right.
   min-height is the .kontakt-form's own natural height (measured with the
   Kontakt panel open — the taller of the two forms), so the section is
   always that tall whether or not a form is open, instead of growing when
   one is revealed. align-items: stretch then lets the button column match
   that height so its buttons can spread out to fill it (see below). */
.kontakt-actions-row {
  display: flex;
  align-items: stretch;
  gap: 32px;
  min-height: 460px;
}

.kontakt-btn-col {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 40px;
  width: min(290px, 100%);
  flex-shrink: 0;
  order: 2;
}

.kontakt-form-stack {
  flex: 1;
  min-width: 0;
  order: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Same glass formula as .kommende-konzerte-video .concert-card (low-opacity
   dark tint + barely-there blur + thin bright top edge) — kept rounded here
   (unlike that square-cornered card) since these are standalone buttons.
   Grid (not flex) so the icon can pin to the left column while the text
   centers in the remaining space, instead of both hugging the left edge. */
.kontakt-glass-btn {
  display: grid;
  grid-template-columns: 40px 1fr;
  align-items: center;
  gap: 14px;
  padding: 16px 18px;
  border-radius: 16px;
  background: rgba(24, 26, 29, 0.38);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-top-color: rgba(255, 255, 255, 0.18);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
  color: #fff;
  text-decoration: none;
  transition: transform 0.25s ease, background 0.25s ease, border-color 0.25s ease;
}

.kontakt-glass-btn:hover,
.kontakt-glass-btn:focus-visible {
  transform: translateY(-3px);
  background: rgba(24, 26, 29, 0.5);
  border-color: rgba(212, 160, 50, 0.6);
}

.kontakt-btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(212, 160, 50, 0.22);
  color: var(--color-accent);
  font-size: 1.2rem;
  font-weight: 700;
}

.kontakt-btn-icon svg { width: 20px; height: 20px; }

.kontakt-btn-text { display: flex; flex-direction: column; gap: 2px; text-align: center; }
.kontakt-btn-text strong {
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  font-size: 1.3rem;
  letter-spacing: 0.02em;
}
.kontakt-btn-text small { font-size: 0.82rem; color: rgba(255, 255, 255, 0.7); }

@media (max-width: 640px) {
  .kontakt-actions-row { flex-direction: column; gap: 0; min-height: 0; }
  .kontakt-btn-col { width: 100%; order: 0; justify-content: flex-start; gap: 20px; }
  .kontakt-form-stack { width: 100%; order: 0; }

  /* Mobile-only layout: buttons sit on the section's own plain dark
     surface (no photo behind them — on a narrow screen the photo read as
     clutter behind the buttons rather than a backdrop). The photo/tint
     background moves from .kontakt-actions to .kontakt-form-stack
     instead, so it shows as its own block right below the buttons, and
     whichever glass panel opens still appears "over the photo" on top of
     that same block — .kontakt-form-stack's `justify-content: center`
     (base rule above) centers it there exactly like it does on desktop.
     min-height is a decorative floor for when no panel is open; it grows
     to fit the panel's own height once one expands. */
  .kontakt-actions { background-image: none; }
  .kontakt-form-stack {
    margin-top: 32px;
    min-height: 300px;
    border-radius: 16px;
    background-image:
      url('../img/kontakt-hero-sofa.jpg'),
      linear-gradient(135deg, rgba(46,52,58,0.95), rgba(54,59,64,0.85) 55%, rgba(61,54,46,0.95));
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain, cover;
  }
}

/* Kontakt / Freundeskreis inline forms — revealed by clicking the matching
   glass button above. Reuses the .project-card-body-wrap grid-template-rows
   collapse trick (see Archiv project cards) so the panel expands/collapses
   smoothly with no JS height calculation, and the same glass formula as
   .kontakt-glass-btn for visual consistency with the buttons that open it. */
.kontakt-form-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s ease;
}

.kontakt-form-wrap.is-open { grid-template-rows: 1fr; }

.kontakt-form-body { overflow: hidden; min-height: 0; }

/* Social Media panel: same .kontakt-form-wrap collapse mechanics as the
   Kontakt/Freundeskreis forms, but with two plain link rows instead of a
   form. Same dark, mostly-opaque tint + light blur as .kontakt-form (see
   below) — needs to stay legible over the now-untinted, full-brightness
   photo background. */
.kontakt-social-panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 28px;
  border-radius: 16px;
  background: rgba(20, 22, 25, 0.6);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-top-color: rgba(255, 255, 255, 0.18);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}

.kontakt-social-link {
  display: grid;
  grid-template-columns: 40px 1fr;
  align-items: center;
  gap: 14px;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.05);
  color: #fff;
  text-decoration: none;
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  font-size: 1.15rem;
  transition: background 0.2s ease, border-color 0.2s ease;
}

.kontakt-social-link:hover,
.kontakt-social-link:focus-visible {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(212, 160, 50, 0.5);
}

.kontakt-form {
  padding: 28px;
  border-radius: 16px;
  background: rgba(20, 22, 25, 0.6);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-top-color: rgba(255, 255, 255, 0.18);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
  color: #fff;
}

/* Short line above the Kontakt form's fields, pointing to the mailto:
   fallback for anyone who'd rather not fill in the form. */
.kontakt-form-intro {
  max-width: 46ch;
  margin: 0 0 20px;
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  font-size: 0.95rem;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.75);
  text-align: left;
}
.kontakt-form-intro a {
  color: var(--color-accent);
  text-decoration: underline;
}
.kontakt-form-intro a:hover,
.kontakt-form-intro a:focus-visible { color: #fff; }

.kontakt-form-row { margin-bottom: 18px; }

@media (min-width: 641px) {
  .kontakt-form-row--two { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
}

.kontakt-form label {
  display: block;
  font-family: 'Raleway', var(--font-body);
  font-weight: 300;
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  margin-bottom: 8px;
  color: rgba(255, 255, 255, 0.85);
}

.kontakt-form input:not([type="checkbox"]),
.kontakt-form textarea,
.kontakt-form select {
  width: 100%;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
  font-family: var(--font-body);
  font-size: 0.95rem;
  transition: border-color 0.2s ease, background 0.2s ease;
}

.kontakt-form input::placeholder,
.kontakt-form textarea::placeholder { color: rgba(255, 255, 255, 0.4); }

.kontakt-form input:focus,
.kontakt-form textarea:focus,
.kontakt-form select:focus {
  outline: none;
  border-color: var(--color-accent);
  background: rgba(255, 255, 255, 0.1);
}

.kontakt-form textarea { resize: vertical; min-height: 110px; }

.kontakt-form select option { color: var(--color-text); }

/* Honeypot: visually hidden but still reachable in the DOM (not display:none
   on the input itself), so it keeps working as a bot trap rather than a
   field screen readers announce. */
.kontakt-form-honeypot {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.kontakt-form-submit {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 13px 30px;
  border-radius: 999px;
  border: 1px solid var(--color-accent);
  background: var(--color-accent);
  color: #1d2126;
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease, opacity 0.2s ease;
}

.kontakt-form-submit:hover { background: var(--color-accent-deep); border-color: var(--color-accent-deep); transform: translateY(-2px); }
.kontakt-form-submit:disabled { opacity: 0.6; cursor: default; transform: none; }

.kontakt-form-status {
  margin: 16px 0 0;
  font-size: 0.9rem;
  display: none;
}
.kontakt-form-status.is-visible { display: block; }
.kontakt-form-status.is-success { color: #9fd6ab; }
.kontakt-form-status.is-error { color: #e8a3a3; }

/* Shared teaser icon style (also used by the homepage Kontakt teaser). */
.kontakt-social-icons { display: flex; gap: 14px; }

.kontakt-social-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: var(--color-surface);
  color: var(--color-accent-deep);
  box-shadow: var(--shadow-sm);
  transition: transform 0.2s ease, color 0.2s ease;
}
.kontakt-social-icon svg { width: 20px; height: 20px; }
.kontakt-social-icon:hover { color: var(--color-text); transform: translateY(-2px); }

/* ---------- Footer ---------- */

.site-footer {
  background: var(--color-text);
  color: var(--color-on-dark-soft);
  padding: 56px 0 28px;
  border-top: 3px solid var(--color-accent);
}

.footer-top {
  display: flex;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--color-line-dark);
  padding-bottom: 32px;
  margin-bottom: 24px;
}

.footer-top .logo { color: var(--color-on-dark); }
.footer-links ul { list-style: none; padding: 0; margin: 0; display: flex; gap: 24px; flex-wrap: wrap; }
.footer-links a { color: var(--color-on-dark-soft); }
.footer-links a:hover { color: var(--color-accent); }

.footer-social { display: flex; align-items: center; gap: 14px; }

.footer-social-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: var(--color-on-dark);
  transition: transform 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.footer-social-icon svg { width: 20px; height: 20px; }
.footer-social-icon:hover { color: var(--color-accent); background: rgba(255, 255, 255, 0.14); transform: translateY(-2px); }

/* left icon (Instagram) sized 20% larger than its neighbor */
.footer-social-icon--lg { width: 50px; height: 50px; }
.footer-social-icon--lg svg { width: 24px; height: 24px; }

.footer-bottom {
  display: flex;
  justify-content: space-between;
  font-size: 0.82rem;
  color: var(--color-on-dark-soft);
  flex-wrap: wrap;
  gap: 10px;
}

/* ---------- Archiv: project cards (click to expand) ---------- */

.project-list { display: flex; flex-direction: column; gap: 24px; }

.project-card {
  background: var(--color-surface);
  border: 1px solid var(--color-line);
  border-radius: 12px;
  box-shadow: var(--shadow-sm);
  padding: 32px;
}

.project-card-head {
  display: flex;
  gap: 32px;
  align-items: flex-start;
}

.project-photo-carousel {
  position: relative;
  flex: 0 0 200px;
  aspect-ratio: 4 / 3;
  border-radius: 10px;
  overflow: hidden;
}

.carousel-slides { position: relative; width: 100%; height: 100%; }

.carousel-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.9s ease;
}

.carousel-slide.is-active { opacity: 1; }

/* Archiv carousel photos stay in full color, opting out of the site-wide
   grayscale-bloom-to-color hover effect (see .no-desaturate above) — with
   slides auto-rotating and no hover target, desaturated ones would never
   bloom into color. */
.carousel-slide { filter: none; }

.project-photo-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 16px;
  border: 1px dashed var(--color-line);
  border-radius: 10px;
  background: var(--color-bg-alt);
  color: #949aa3;
  font-style: italic;
  font-size: 0.85rem;
}

.carousel-dots {
  position: absolute;
  z-index: 1;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 6px;
}

.carousel-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
}

.carousel-dot.is-active { background: #fff; }

@media (prefers-reduced-motion: reduce) {
  .carousel-slide { transition: none; }
}

.project-card-intro { flex: 1; min-width: 0; }
.project-card-intro .eyebrow { margin-bottom: 0.5em; }
.project-card-intro h3 { font-size: clamp(1.35rem, 2.6vw, 1.7rem); margin-bottom: 0.5em; }
.project-card-intro p { color: var(--color-text-soft); }

.project-card-toggle {
  margin-top: 14px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 22px;
  background: var(--color-surface);
  border: 1px solid var(--color-line);
  border-radius: 999px;
  color: var(--color-text);
  font-family: inherit;
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  box-shadow: var(--shadow-sm);
  cursor: url('../img/cursor-note-hover.svg') 16 16, pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.project-card-toggle:hover {
  background: var(--color-text);
  color: var(--color-bg);
  border-color: var(--color-text);
}

.project-card-toggle .label-close { display: none; }
.project-card.is-open .project-card-toggle .label-open { display: none; }
.project-card.is-open .project-card-toggle .label-close { display: inline; }

.project-card-body-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s ease;
}

.project-card.is-open .project-card-body-wrap { grid-template-rows: 1fr; }

.project-card-body {
  overflow: hidden;
  min-height: 0;
}

.project-program {
  padding-top: 28px;
  margin-top: 28px;
  border-top: 1px solid var(--color-line);
}

.project-card-body .project-program:first-child {
  margin-top: 28px;
}

.project-program h4 {
  font-family: 'Raleway', var(--font-body);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  margin-bottom: 0.8em;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.program-number {
  font-family: var(--font-display);
  font-style: italic;
  color: var(--color-accent-deep);
}

.program-tag {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-soft);
  border: 1px solid var(--color-line);
  border-radius: 999px;
  padding: 3px 10px;
}

.program-works {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
}
.program-works li { margin-bottom: 6px; color: var(--color-text); }
.program-works li:last-child { margin-bottom: 0; }

.program-musicians {
  font-size: 0.88rem;
  color: var(--color-text-soft);
  margin: 0;
}

/* Each entry is its own <li> (date + venue), not one long "·"-joined
   string, specifically so a plain text wrap can't split a date from its
   own venue — that read as broken/misaligned once a card's column got
   narrow enough that the full "26.11.2025 Museum Allschwil · 28.11.2025
   …" line no longer fit: the browser would wrap wherever it ran out of
   room, sometimes mid-entry, leaving a date on one line and its venue
   alone on the next. flex-wrap here wraps between whole entries instead
   — each <li> stays intact (white-space: nowrap) and the separating "·"
   is glued to the entry before it via ::after, so it travels with that
   entry rather than starting a line by itself. */
.program-dates {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  row-gap: 4px;
  margin: 0 0 6px;
  padding: 0;
  font-size: 0.88rem;
  color: var(--color-text-soft);
}
.program-dates li { white-space: nowrap; }
.program-dates li:not(:last-child)::after {
  content: "·";
  margin: 0 0.5em;
}

.project-note {
  padding-top: 28px;
  margin-top: 28px;
  border-top: 1px solid var(--color-line);
  color: var(--color-text-soft);
  font-size: 0.92rem;
}

/* ---------- Archiv: repertoire by ensemble type ---------- */

.repertoire-groups {
  display: grid;
  /* Was repeat(3, 1fr) — a 3-column ~366px-wide entry left many "Composer
     — Werktitel, Op. X Nr. Y" lines wrapping with just the trailing
     opus/number reference stranded alone on its own short last line (a
     typographic widow). Measured directly in a live DOM pass (each <li>'s
     wrapped-line rects, flagging any whose last line was under 30% of the
     first line's width): 3 columns produced 16 such widows across the
     repertoire lists; 2 columns' ~570px-wide entries dropped that to 1
     (an exceptionally long work title that would run past two lines at
     almost any column width). Two columns costs more vertical space for
     the longer instrumentation groups, but that was the clear win here —
     shrinking the font instead (tried down to 0.82rem) only got the widow
     count from 16 to 11, at the cost of readability. */
  grid-template-columns: repeat(2, 1fr);
  gap: 28px 40px;
}

.repertoire-group h4 {
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-accent-deep);
  border-bottom: 1px solid var(--color-line);
  padding-bottom: 10px;
  margin-bottom: 14px;
}

.repertoire-group ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.repertoire-group li {
  margin-bottom: 10px;
  color: var(--color-text-soft);
  font-size: 0.92rem;
  line-height: 1.5;
  /* "Composer — Werktitel" entries wrap at every column width (the grid
     goes down to 1 of 3 columns on narrower screens, see the breakpoints
     below), and a plain wrap left a lone short word stranded as its own
     last line surprisingly often. text-wrap:pretty asks the browser to
     spend a bit more layout effort balancing line lengths specifically to
     avoid that — pure progressive enhancement, browsers that don't
     support it just keep today's plain wrapping, so there's no fallback
     to write. */
  text-wrap: pretty;
}
.repertoire-group li:last-child { margin-bottom: 0; }

/* ---------- Reveal animation ---------- */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- Responsive ---------- */

/* Hero cropping isn't just a narrow-phone problem: a portrait tablet (e.g.
   iPad Pro 12.9", 1024px wide) is well above the 860px breakpoint below but
   just as tall/narrow as a phone, so the landscape hero photo still needs
   to cover a near-full-height box and crops members out of frame the same
   way. Trigger the compact hero treatment by orientation too, not width
   alone, so it isn't missed on a device that's wide but still portrait.
   Deliberately NOT widened to also catch landscape phones (861-1023px,
   landscape) the way the nav breakpoint below is: those viewports are
   wide but short, so switching to this rule's aspect-ratio box (sized off
   WIDTH) would make the hero taller than the actual (short) viewport and
   force a scroll on load. min-height:100dvh + cover (this rule's default,
   below) already fills a short-wide viewport exactly, cropping only the
   photo's left/right — never the members' heads — which is the crop this
   rule exists to prevent in the first place. */
@media (max-width: 860px), (orientation: portrait) {
  /* .intro-hero-stage wraps .intro-hero (see index.html/en/index.html) purely
     to give the compact, aspect-ratio-sized hero below a full-height stage to
     sit in: it fills the viewport and centers the (unchanged, uncropped)
     photo box inside it, so any leftover space becomes a color band that
     reads as an intentional frame instead of blank page background trailing
     underneath a small photo. Doesn't touch .intro-hero's own sizing logic
     or the logo's positioning inside it — nothing about the crop-avoidance
     rule below changes. */
  .intro-hero-stage {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--color-hero-band);
  }

  /* Below 860px the hero photo (landscape, ~1.4:1) can't cover a near-
     full-height portrait viewport without cropping members out of frame.
     Switch to the photo's own aspect ratio so the full group stays visible,
     full-width, instead of forcing 100dvh. */
  .intro-hero {
    min-height: 0;
    width: 100%;
    aspect-ratio: 2000 / 1428;
    /* Sits a little lower than the desktop 3vh inset, closer to the bottom edge. */
    --logo-pad-y: 1.5vh;
  }
  /* Same --logo-pad-x/-y as desktop (inherited from .intro-hero) so the
     corner inset feels equally tight at every size. max-height is the
     driver (38 * 808/1704 = 18.0188vh on desktop; 30% smaller here — at
     full size the logo covered the sitters' faces on narrow/tall crops) —
     width just needs to stay generous enough for height to keep binding
     down to the narrowest phone in range (375px needs >=76vw for the math
     to hold; 95vw covers that with margin without ever becoming the
     actual driver). */
  .intro-logo { width: min(900px, 95vw); max-height: 12.6132vh; }
  .intro-logo-glow { left: calc(var(--logo-pad-x) - 8vh); bottom: calc(var(--logo-pad-y) - 8vh); width: calc(24.5877vh + 16vh); height: calc(12.6132vh + 16vh); }
  /* Top-corner variant, same 76px header-clearance logic as the desktop rule. */
  .intro-hero--logo-top .intro-logo-glow { top: calc(76px + var(--logo-pad-top) - 8vh); bottom: auto; }

  /* The desktop light-logo vignette (.intro-hero--logo-light .intro-logo-glow,
     ~243vh x 125vh with an 80px blur) is sized to read as a broad photographic
     scrim on a wide desktop hero — on a tall/narrow viewport that same box
     covers most of the frame and darkens the whole photo. Replace it here
     with a small, tightly-fitted dark patch that only sits behind the
     (now smaller) logo, same footprint math as the plain glow above. */
  .intro-hero--logo-light .intro-logo-glow {
    left: calc(var(--logo-pad-x) - 6vh);
    bottom: calc(var(--logo-pad-y) - 6vh);
    width: calc(24.5877vh + 12vh);
    height: calc(12.6132vh + 12vh);
    /* "closest-side" (not the default farthest-corner) inscribes the ellipse
       inside the box, touching each edge's midpoint at 100% — so the fade
       actually finishes right at the box boundary in every direction. With
       farthest-corner, the box's shorter (vertical) edge fell well short of
       100%, leaving a visible partial-opacity edge/rectangle above the logo. */
    background: radial-gradient(ellipse closest-side at center,
      rgba(0,0,0,0.5) 0%,
      rgba(0,0,0,0.24) 45%,
      rgba(0,0,0,0.08) 70%,
      rgba(0,0,0,0) 100%);
    filter: blur(20px);
  }
}

/* Tablets (portrait) only — phones (<=600px) keep the vh-based sizing
   above untouched. That vh basis reads too small here because tablets are
   much closer to square than phones: the same "logo height in vh" ends up
   a far smaller share of the (much wider) screen. Re-express max-height as
   a vw-derived value instead of a flat vh one so the logo scales with
   screen width on these wider viewports — same corner position
   (--logo-pad-x/-y untouched), same glow bleed/blur/gradient, same fly-in
   effect; only the size baseline changes. 42vw / 1.9493 (viewBox aspect,
   1575/808) keeps the glow math below exact: the width term simplifies to
   42vw + bleed since (42vw / 1.9493) * 1.9493 = 42vw. */
@media (min-width: 601px) and (orientation: portrait) {
  .intro-logo { max-height: calc(42vw / 1.9493); }
  .intro-logo-glow { width: calc(42vw + 16vh); height: calc(42vw / 1.9493 + 16vh); }
  .intro-hero--logo-light .intro-logo-glow {
    width: calc(42vw + 12vh);
    height: calc(42vw / 1.9493 + 12vh);
  }
}

/* Phones only (not tablets, which stay caught by the 860px/portrait rule
   above without this). */
@media (max-width: 600px) {
  /* Zoom the hero photo itself 15%. transform-origin matches .intro-bg
     img's object-position (center 25%) so the crop is biased the same
     way, keeping the group centered rather than pushing them down.
     .intro-bg needs its own overflow:hidden to crop that zoomed overflow:
     .intro-hero (the shared ancestor) is overflow:visible below so the
     logo can sit past its bottom edge, and that would otherwise also
     un-clip the zoomed photo, letting it bleed down into the logo — which
     is exactly the overlap this rule fixes. */
  .intro-bg {
    overflow: hidden;
  }
  .intro-bg img {
    transform: scale(1.15);
    transform-origin: 50% 25%;
  }

  /* EXPERIMENT: logo moved out of the photo entirely, into the color band
     below it — big and centered — instead of overlapping the photo's
     bottom-left corner. .intro-hero needs overflow:visible so the logo (a
     child of it, not of .intro-bg) can render past its bottom edge; the
     photo itself never relied on overflow:hidden to crop (aspect-ratio
     already matches the source photo, see the shared rule above), so
     nothing about the photo's own rendering changes. Shifting .intro-hero
     up by (gap + logo height) re-centers the whole photo+logo group in the
     stage instead of just the photo — this is the "image a bit higher"
     the layout needs to leave room for the logo below it while keeping
     equal color-band margins top and bottom. (Flex centering absorbs half
     of a negative margin back into the free-space split, so the margin
     has to be the FULL gap+height, not half of it, for the visible shift
     to come out symmetric — verified against getBoundingClientRect, not
     just derived on paper.) */
  .intro-hero {
    overflow: visible;
    /* 24.75dvh (10% down from the 27.5vh "+25%" trial) is the height
       target; at the 22vh baseline the logo was already ~93% of screen
       width (fixed aspect ratio, so height and width scale together), so
       the plain vh value would overflow past both edges on narrower
       phones. Capping via the width side (86.4vw, converted to an
       equivalent height — also scaled down 10% in step with the height
       target) means both constraints shrink together, so the render
       stays 10% smaller everywhere regardless of which one binds.
       dvh, not vh: plain "vh" is pinned to the browser's LARGE viewport
       (toolbar hidden) on phones, while .intro-hero-stage's own height
       (min-height:100dvh) tracks the SMALLER dynamic one once the address
       bar is showing — that mismatch is what made the gap read as
       overlap on a real phone even though it measured fine here against a
       fixed-size headless viewport. dvh keeps this math on the same
       basis as the stage it's measured against. */
    --mobile-logo-h: min(22.275dvh, calc(77.76vw * 808 / 1575));
    --mobile-logo-gap: 2.5dvh;
    margin-top: calc(-1 * (var(--mobile-logo-gap) + var(--mobile-logo-h)));
  }
  .intro-logo {
    /* The corner layout leaves width "generous/non-binding" on purpose
       (see the base rule) — any slack just extends further into the photo
       and is never seen. Centered, that same slack becomes asymmetric
       overflow past one edge on narrow phones. Setting width and height
       explicitly (both derived from --mobile-logo-h) removes the slack so
       centering is exact — an aspect-ratio+auto-width version of this was
       tried first, but for this absolutely-positioned SVG the browser
       sized it off the wrong dimension and rendered ~40% too small. */
    width: calc(var(--mobile-logo-h) * 1575 / 808);
    height: var(--mobile-logo-h);
    max-height: var(--mobile-logo-h);
    left: 50%;
    bottom: calc(-1 * (var(--mobile-logo-gap) + var(--mobile-logo-h)));
    transform: translateX(-50%);
  }
  /* The glow/vignette was tuned to spotlight the logo against the photo
     behind it — with the logo now sitting on the flat color band, there's
     no photo there to spotlight. Ink stays the white --logo-light-color
     variant (with its gold dots) to match the tablet/desktop hero. */
  .intro-logo-glow { display: none; }
}

@media (max-width: 860px) {
  .footer-top {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  .footer-links ul { justify-content: center; text-align: center; }
  .footer-bottom {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .prose-more { display: none; }
  .prose-more.is-expanded { display: block; }
  .read-more-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 4px 0 1.2em;
    padding: 10px 22px;
    background: var(--color-surface);
    border: 1px solid var(--color-line);
    border-radius: 999px;
    color: var(--color-text);
    font-family: inherit;
    font-weight: 700;
    font-size: 0.78rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
  }
  .read-more-toggle:hover {
    background: var(--color-text);
    color: var(--color-bg);
    border-color: var(--color-text);
    transform: translateY(-2px);
  }
  .read-more-toggle.is-expanded { display: none; }

  .grid-2 { grid-template-columns: 1fr; }
  .grid-3 { grid-template-columns: 1fr; }
  .contact-block { grid-template-columns: 1fr; }
  .hero-photo { aspect-ratio: 4 / 3; }

  .concert-card,
  .flip-front,
  .flip-back { padding: 16px 18px; }
  .concert-row {
    grid-template-columns: 1fr 1fr;
    row-gap: 6px;
  }
  .concert-row .place { grid-column: 1 / -1; order: 3; }
  .concert-row .tag { grid-column: 1 / -1; order: 4; justify-self: start; }

  .flip-back {
    grid-template-columns: 1fr;
    row-gap: 14px;
  }
  .flip-back .concert-musicians {
    grid-column: 1;
    white-space: normal;
    padding-left: 0;
    padding-top: 14px;
    border-left: none;
    border-top: 1px dashed var(--color-line);
  }

  .project-card { padding: 22px; }
  .project-card-head { flex-direction: column; gap: 20px; }
  .project-photo-carousel { flex-basis: auto; width: 100%; aspect-ratio: 16 / 9; }

  .repertoire-groups { grid-template-columns: 1fr; }
}

/* Nav bar overflow, not just a narrow-phone problem: the horizontal
   .main-nav (all six links + DE/EN switch) only fits comfortably from
   ~1024px wide — below that (down through the 861-1023px range) "Über
   Taru" wraps onto a second line and collides with "Ensemble" next to
   it. Every phone is under 1024px wide even rotated to landscape (the
   widest current phones land around 926-932px), so without this the
   hamburger menu — correct and working below 860px — disappears the
   moment a large phone is turned sideways, right when there's the LEAST
   vertical room to spare for a wrapped nav row. Kept as its own block
   (rather than folded into the 860px one above) because nothing else in
   that block needs the wider range: grid/footer/card layouts already
   read fine at 861-1023px, only the nav bar's own fixed link list
   doesn't. */
@media (max-width: 1023px) {
  .main-nav {
    position: fixed;
    top: 76px;
    left: 0;
    right: 0;
    height: calc(100vh - 76px);
    background: rgba(250, 250, 250, 0.86);
    backdrop-filter: blur(26px) saturate(140%);
    -webkit-backdrop-filter: blur(26px) saturate(140%);
    border-left: 1px solid rgba(255, 255, 255, 0.5);
    box-shadow: -8px 0 32px rgba(16, 20, 26, 0.14);
    transform: translateX(100%);
    opacity: 0;
    transition: transform 0.35s ease, opacity 0.35s ease;
    overflow-y: auto;
  }
  .main-nav.is-open { transform: translateX(0); opacity: 1; }
  .main-nav ul {
    flex-direction: column;
    gap: 0;
    padding: 24px;
  }
  .main-nav a {
    display: block;
    padding: 16px 0;
    border-bottom: 1px solid var(--color-line);
  }
  .nav-toggle { display: block; }

  .main-nav .lang-switch {
    padding: 16px 0;
    margin-left: 0;
  }
  .main-nav .lang-switch a {
    display: inline-block;
    padding: 4px 9px;
    border-bottom: none;
  }
}

/* Konzerte hero layout, tablet range: the two-column cards+photo split
   is tuned for the ~1180px desktop content width (--max-width) — below
   that, .wrap's fluid width (viewport minus padding) squeezes the
   cards column too narrow for its date/time/place grid to read well,
   so tablets get the same stacked layout as mobile instead of a
   cramped side-by-side one. `order` puts cards before their photo in
   every group, including --mirror, whose DOM has the photo first for
   desktop's alternating left/right layout — stacked, that would show
   photo, cards instead of the cards, photo rhythm the other groups
   use. .flip-card's own `margin: 0 auto` (see its base rule) centers
   each card once its 760px cap is narrower than this wide a column.
   The default group's photo margin-top override below has to repeat
   the full :not()/:not() selector — its unconditional desktop rule
   (margin-top: 344px, centering the frame against the two-column row)
   outweighs a plain .concert-feature-photo override on specificity
   alone, media query or not. */
@media (max-width: 1200px) {
  .concert-feature-split { flex-direction: column; }
  .concert-feature-split .concert-feature-cards { flex-basis: auto; width: 100%; order: 1; }
  .concert-feature-photo {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    margin-right: 24px;
    order: 2;
  }
  /* Oktober/November's source photo is 1024×731 — forcing it into the
     generic 16:9 frame above (a taller, narrower box than the photo's
     own ~1.4:1 shape) made `cover` crop real height off the top/bottom
     to fill the extra width. Matching the frame to the photo's actual
     shape, same fix as Januar/Februar's portrait override just below,
     means `cover` has nothing left to crop. */
  .concert-feature-split:not(.concert-feature-split--mirror):not(.concert-feature-split--alt) .concert-feature-photo {
    aspect-ratio: 1024 / 731;
    margin-top: 24px;
  }
  .concert-feature-split--mirror .concert-feature-photo {
    aspect-ratio: 1024 / 1433;
    margin-left: 0;
    margin-top: 24px;
  }
  .concert-feature-split--alt {
    grid-template-columns: 1fr;
    grid-template-areas: "cardsA" "photo" "cardsB";
    gap: 0;
  }
  /* April/Mai/Juni's source photo is 1024×1024 (square) — same crop
     problem and same fix as the plain group above, just its own ratio. */
  .concert-feature-split--alt .concert-feature-photo { aspect-ratio: 1024 / 1024; margin-top: 24px; }
  .concert-feature-split--alt .concert-feature-cards--split-b { margin-top: 24px; }

  /* Desktop's scale(1.2) "lupa" zoom (plus the first group's translateX
     nudge) was tuned for the desktop frame's own near-square shape —
     it doesn't track this breakpoint's differently-shaped frame above,
     so it's reset to none here. Frame and photo now share the same
     aspect ratio (see above), so `cover` and `contain` are equivalent;
     `cover` is kept so a fractional rounding difference between the
     two never leaves a sliver of the frame's tan background showing. */
  .concert-feature-split:not(.concert-feature-split--mirror):not(.concert-feature-split--alt) .concert-feature-photo img,
  .concert-feature-split--alt .concert-feature-photo img {
    object-fit: cover;
    transform: none;
  }

  /* .flip-inner's per-group min-height (211/258/281px, set unconditionally
     above) exists to match every card in a group to that group's PHOTO
     height in the desktop side-by-side layout — once the split stacks to
     one column here (photo now sits below the cards, not beside them),
     that height has nothing left to match. Left in place, it forced
     every card to the tallest card's desktop height regardless of its
     own content, so a card with a single date/time/place row stretched
     to match a much longer sibling, leaving a large empty gap on its
     front face and pushing the photo further down than its own content
     required. Repeats the exact selectors (not just their specificity)
     so this reliably outranks the desktop rule regardless of source
     order. */
  .concert-feature-split:not(.concert-feature-split--mirror):not(.concert-feature-split--alt) .concert-feature-cards .flip-inner,
  .concert-feature-split--mirror .concert-feature-cards .flip-inner,
  .concert-feature-split--alt .concert-feature-cards .flip-inner {
    min-height: 0;
  }
}

/* Very small screens: force the footer nav into two fixed rows of three
   links instead of an unpredictable wrap. */
@media (max-width: 480px) {
  .footer-links ul {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px 12px;
  }
  .footer-links ul li { text-align: center; }
}

/* No longer needs its own tablet override — the default (see
   .repertoire-groups above) is now repeat(2, 1fr) too, since 2 columns
   turned out to be the better desktop width regardless of screen size. */
