/*
  Luna Spiegel — shared layout & component styles
  Companion to tokens.css. Loaded on every page.

  Component names below map to the generative visual system in
  docs/tasks/issue-7-initial-site/04-concept-reviewed.md §2.4:
  Pulse Bars, Brass Wash, Screen Dot, Timeline Rule.
*/

/* ============ Reset ============ */

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

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
}

ul,
ol {
  padding: 0;
  margin: 0;
  list-style: none;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 600;
  margin: 0;
}

h1 {
  font-size: var(--size-h1);
  line-height: 1.05;
}

h2 {
  font-size: var(--size-h2);
  line-height: 1.1;
}

h3 {
  font-size: var(--size-h3);
  font-family: var(--font-body);
  font-weight: 600;
  line-height: 1.3;
}

p {
  margin: 0 0 1em;
}

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

/* ============ Utility ============ */

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

.skip-link {
  position: absolute;
  top: 1rem;
  left: 1rem;
  z-index: 1000;
  padding: 0.75rem 1.25rem;
  background: var(--ink);
  color: var(--paper);
  border-radius: 0.25rem;
  text-decoration: none;
  font-weight: 600;
  /* Hide by translating up by the element's own rendered height (plus the
     1rem top offset), not by a fixed guessed offset. This can never leave
     a sliver visible, regardless of padding/font-size/line-height changes
     (issue #99). */
  transform: translateY(calc(-100% - 1rem));
  transition: transform var(--motion-fast) var(--ease-out);
}

.skip-link:focus {
  transform: translateY(0);
}

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

.wrapper {
  max-width: var(--wrapper-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.measure {
  max-width: var(--measure);
}

.mono-label {
  font-family: var(--font-mono);
  font-size: var(--size-mono);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* ---- Spacing utilities ----
   Only the steps with a real consumer are defined. Do NOT pre-generate the
   full ladder: a utility file that ships 60 classes to serve 12 usages
   becomes a second design system competing with the components below.
   Add a class in the same commit as the usage that needs it.
   Rule of thumb: if the same utility lands on the same kind of element three
   times, that is a component — name it, don't keep bolting on margins. */
.mt-6  { margin-top: var(--space-6); }
.mt-7  { margin-top: var(--space-7); }
.mt-8  { margin-top: var(--space-8); }
.mt-10 { margin-top: var(--space-10); }
.mt-12 { margin-top: var(--space-12); }
.mb-6  { margin-bottom: var(--space-6); }

/* `.hero__actions` (below) and `.mt-7` are both single-class selectors
   (equal specificity 0,1,0); `.hero__actions` appears later in the
   cascade and would otherwise silently win, overriding the 28px utility
   with its own 2rem fallback. Pin the intended value explicitly. */
.hero__actions.mt-7 { margin-top: var(--space-7); }

:focus-visible {
  outline: 2px solid var(--rose);
  outline-offset: 3px;
  border-radius: 2px;
}

.section--night :focus-visible,
.lang-gate :focus-visible,
.site-footer :focus-visible,
.sticky-cta :focus-visible {
  outline-color: var(--rose-tint);
}

/* ============ Buttons & links ============ */

.button {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-height: 44px;
  padding: 0.75em 1.5em;
  border-radius: 999px;
  font-weight: 600;
  text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease-out),
    color var(--motion-fast) var(--ease-out),
    transform var(--motion-fast) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  /* Colour cross-fade is kept deliberately: SC 2.3.3 targets motion, and the
     hover colour change is a real affordance cue. Only the (currently unused)
     transform tween is dropped. Restated in full because `transition` is a
     shorthand. */
  .button {
    transition: background-color var(--motion-fast) var(--ease-out),
      color var(--motion-fast) var(--ease-out);
  }
}

.button--primary {
  background: var(--rose);
  color: var(--paper);
}

.button--primary:hover {
  background: var(--ink);
}

.section--night .button--primary {
  background: var(--rose-tint);
  color: var(--night);
}

.section--night .button--primary:hover {
  background: var(--paper);
}

/* REQUIRED FIX (issue #140, Elli's legal/accessibility review — see
   docs/tasks/issue-140-dark-blue-color-schema/05-storyboard-reviewed.md
   §A.2 / §D.2). The rule above sets background: var(--paper) while
   inheriting color: var(--night) from .section--night .button--primary.
   Re-pointing --paper/--night for moonlight turns that into --night text
   on a --paper fill close to 1:1, a hard SC 1.4.3 failure. This override
   keeps the same visual intent (hover goes brighter than the resting
   fill) while giving --night text on --night-ink fill = 16.68:1. Must
   ship with the rest of the theme's CSS — extend this selector, never
   add a parallel rule. */
[data-theme="moonlight"] .section--night .button--primary:hover {
  background: var(--night-ink);
}

.button--ghost {
  background: transparent;
  color: var(--ink);
  border: 1.5px solid currentColor;
}

.section--night .button--ghost {
  color: var(--night-ink);
}

.button--ghost:hover {
  background: var(--ink);
  color: var(--paper);
}

.section--night .button--ghost:hover {
  background: var(--night-ink);
  color: var(--night);
}

.text-link {
  color: var(--brass-deep);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  font-weight: 600;
}

.section--night .text-link {
  color: var(--brass-tint);
}

.text-link:hover {
  color: var(--rose);
}

.section--night .text-link:hover {
  color: var(--rose-tint);
}

.text-link--icon {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
}

.text-link--icon svg {
  flex-shrink: 0;
}

/* Standalone CTA text links — a link that is the whole content of its own
   block and acts as a section's "next step", not a link inside a sentence.
   SC 2.5.8's inline exception does not cover these, so they take the site's
   house 44px minimum (above the 24px WCAG floor); previously ~20-24px.
   `min-height` on a centred inline-flex box grows the hit area symmetrically
   around the text — no padding, no background, no border, so these stay
   visually quiet text links and never start reading as buttons.
   Inline links inside prose (e.g. the concerts link in the homepage music
   paragraph) are deliberately NOT included here. */
.hero__actions .text-link,
.teaser-cta .text-link,
.cta-row .text-link,
.gig__links .text-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Paragraph wrapper for a standalone CTA link that has no other component
   hook. Carries the 1.5rem top margin the previous inline styles set. */
.cta-row {
  margin-top: 1.5rem;
}

/* ============ Header / primary nav ============ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--paper);
  border-bottom: 1px solid var(--hairline);
  transition: padding var(--motion-fast) var(--ease-out);
}

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

.site-header__inner {
  max-width: var(--wrapper-max);
  margin-inline: auto;
  padding: 1rem var(--gutter);
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.site-header.is-condensed .site-header__inner {
  padding-block: 0.5rem;
}

.wordmark {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: var(--ink);
  margin-right: auto;
  min-height: 44px;
}

/* Crescent-moon eighth note — docs/tasks/new-logo-moon-eighth-note/
   01-concept.md. Narrower than the old 34px Slide Lines mark, so the
   width-critical header row gains 14px. */
.wordmark__mark {
  width: 40px;
  height: 48px;
  flex-shrink: 0;
}

.wordmark__text {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.25rem;
}

/* The 40×48 mark (twice its original size) costs 20px of the header row;
   on phones that wrapped "Luna Spiegel" onto two lines. Keep the name on
   one line by tightening the mark–text gap and the text size there. */
@media (max-width: 413.98px) {
  .wordmark {
    gap: 0.25rem;
  }

  .wordmark__text {
    font-size: 1.125rem;
  }
}

/* 320px has no room for a one-line name next to the menu; it wraps there,
   as it did before the larger mark. */
@media (min-width: 360px) and (max-width: 413.98px) {
  .wordmark__text {
    white-space: nowrap;
  }
}

.site-nav {
  display: flex;
  align-items: center;
}

.nav-disclosure {
  position: relative;
}

.nav-disclosure__button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto;          /* was: 44px — the visible "Menu"/"Menü" label needs room */
  min-width: 44px;      /* SC 2.5.8 floor retained */
  min-height: 44px;
  gap: 0.5rem;
  padding-inline: 0.25rem;
  cursor: pointer;
  list-style: none;
}

.nav-disclosure__button::-webkit-details-marker {
  display: none;
}

/* Icon plus word: the label carries no colour of its own via .mono-label,
   so --ink is the only colour applied (C1, 05-storyboard-reviewed.md §1). */
.nav-disclosure__label {
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--ink);
}

/* A visible label overflows the header at 320px unless it tightens with it
   (verified by render, 04-concept-reviewed.md §2.2 / 05-storyboard-reviewed.md
   §1) — without this, "EN" gets pushed off the right edge at 320px. */
@media (max-width: 479px) {
  .site-header__inner {
    gap: var(--space-3);
  }

  .nav-disclosure__button {
    gap: 0.375rem;
  }

  .nav-disclosure__label {
    font-size: 0.75rem;
    letter-spacing: 0.06em;
  }
}

.nav-disclosure__icon {
  position: relative;
  width: 22px;
  height: 2px;
  background: var(--ink);
}

.nav-disclosure__icon::before,
.nav-disclosure__icon::after {
  content: "";
  position: absolute;
  left: 0;
  width: 22px;
  height: 2px;
  background: var(--ink);
  transition: transform var(--motion-fast) var(--ease-out);
}

.nav-disclosure__icon::before {
  top: -7px;
}

.nav-disclosure__icon::after {
  top: 7px;
}

.nav-disclosure[open] .nav-disclosure__icon {
  background: transparent;
}

.nav-disclosure[open] .nav-disclosure__icon::before {
  transform: translateY(7px) rotate(45deg);
}

.nav-disclosure[open] .nav-disclosure__icon::after {
  transform: translateY(-7px) rotate(-45deg);
}

.nav-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  position: absolute;
  right: 0;
  top: calc(100% + 0.5rem);
  background: var(--paper);
  border: 1px solid var(--hairline-strong);
  border-radius: 0.75rem;
  padding: 0.5rem;
  min-width: 220px;
  box-shadow: var(--shadow-pop);
}

.nav-list a {
  display: block;
  padding: 0.75rem 1rem;
  text-decoration: none;
  border-radius: 0.5rem;
  font-weight: 500;
  min-height: 44px;
  display: flex;
  align-items: center;
  /* Defensive: header is always on --paper, never dark, but this removes
     the dependency on inherited color so it can't silently regress. See
     docs/tasks/issue-29-trombone-contrast-lueneburg/04-concept-reviewed.md §2. */
  color: var(--ink);
}

.nav-list a:hover {
  background: var(--paper-rose);
}

.nav-list .nav-cta {
  background: var(--rose);
  color: var(--paper);
  font-weight: 600;
  margin-top: 0.25rem;
  justify-content: center;
}

.nav-list .nav-cta:hover {
  background: var(--ink);
}

/* Current-page indicator (#44): extends the .lang-switch current-state
   pattern from issue #23 (--rose-deep + weight 700 + underline) to the
   primary nav. One rule covers both .nav-list instances (mobile
   disclosure + .nav-list--bar), since they share this base class.
   .is-section-current is the home page's scroll highlight (site.js
   initSectionNav) and deliberately looks the same. */
.nav-list a[aria-current="page"],
.nav-list a.is-section-current {
  color: var(--rose-deep);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Contact page: the current item is the CTA pill itself. Deepen the fill
   instead of stacking the base rule's color/underline on a --rose fill
   (which would be a rose-on-rose contrast mess) — same colour move
   (--rose -> --rose-deep), applied to the fill rather than the text. */
.nav-list .nav-cta[aria-current="page"],
.nav-list .nav-cta.is-section-current {
  background: var(--rose-deep);
  color: var(--paper);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.nav-list .nav-cta[aria-current="page"]:hover,
.nav-list .nav-cta.is-section-current:hover {
  background: var(--ink);
}

/*
  Desktop/tablet nav is a second, always-present <ul class="nav-list
  nav-list--bar"> (a sibling of .nav-disclosure, not nested inside it) —
  a closed native <details> hides its non-summary content in a way that
  cannot be reliably reopened or made keyboard/AT-reachable via CSS alone,
  so the ≥768px nav can't just "reveal" the disclosure's content. It's
  hidden below 768px and swapped in for the disclosure at 768px+, see
  media query below.
*/
.nav-list--bar {
  display: none;
  gap: 0.25rem;
}

.lang-switch {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
}

.lang-switch a {
  padding: 0.4rem 0.3rem;
  text-decoration: none;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.lang-switch a[aria-current="true"] {
  color: var(--rose-deep);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Three languages (DE · EN · ES) in the header row. The extra link cost
   ~30px, which wrapped the wordmark at 375–414px and at 768px. Tighter
   spacing here, and no "·" separators below 768px (the gap alone parts
   the labels there), restores the pre-ES single-line header at every
   width from 375px up. Link centres stay ≥24px apart, so SC 2.5.8 passes
   via its spacing exception. Footer instance untouched. */
.site-header .lang-switch {
  gap: 0.1rem;
}

.site-header .lang-switch a {
  padding-inline: 0.2rem;
}

@media (max-width: 767.98px) {
  .site-header .lang-switch > span[aria-hidden="true"] {
    display: none;
  }
}

@media (min-width: 768px) {
  /* Mobile-only disclosure: fully removed at desktop, replaced by
     .nav-list--bar below. */
  .nav-disclosure {
    display: none;
  }

  .nav-list--bar {
    display: flex;
    flex-direction: row;
    align-items: center;
    position: static;
    border: 0;
    box-shadow: none;
    padding: 0;
    min-width: 0;
    background: transparent;
  }

  .nav-list--bar a {
    padding: 0.5rem 0.9rem;
  }

  .nav-list--bar .nav-cta {
    margin-top: 0;
    padding: 0.5rem 1.25rem;
    border-radius: 999px;
  }
}

/* 768-1023px: the bar's default sizing above (and the wordmark's natural
   width) don't both fit on one row in this tier, which lets the flex
   container's default shrink behaviour compress/wrap the wordmark text
   instead of the nav links overflowing (nav links can't wrap below
   min-content once nowrap is set). Tighten the nav bar's own footprint
   here so the whole row fits without leaning on that shrink, then widen
   back to the roomier defaults above at 1024px+. See issue #33. */
@media (min-width: 768px) and (max-width: 1023.98px) {
  .site-header__inner {
    gap: 0.5rem;
  }

  .nav-list--bar {
    gap: 0.125rem;
  }

  .nav-list--bar a {
    padding: 0.5rem 0.35rem;
    font-size: 0.875rem;
    white-space: nowrap;
  }

  .nav-list--bar .nav-cta {
    padding: 0.5rem 0.75rem;
  }

  /* Tightened for issue #140: the theme switch adds a third control to
     this row. Measured at exactly 768px (05-storyboard-reviewed.md §7.1,
     §A.3): 687.9px used of 691.6px available with these values — one
     line, no wrap, 3.7px of slack. */
  .lang-switch a {
    padding: 0.4rem 0.2rem;
  }

  /* ES added a third language link: measured at 768px it no longer fits
     with the #140 values above, so the header's language links tighten
     once more. */
  .site-header .lang-switch a {
    padding-inline: 0.15rem;
  }
}

/* ============ Mobile disclosure open animation (JS-enhanced) ============ */

.nav-disclosure.js-enhanced .nav-list {
  overflow: hidden;
  max-height: 0;
  padding-block: 0;
  transition: max-height var(--motion-medium) var(--ease-out);
}

.nav-disclosure.js-enhanced[open] .nav-list {
  max-height: 400px;
  padding: 0.5rem;
}

@media (prefers-reduced-motion: reduce) {
  .nav-disclosure.js-enhanced .nav-list {
    transition: none;
  }
}

/* ============ Sticky mobile booking bar ============ */

.sticky-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  padding: var(--sticky-cta-pad-block) var(--gutter);
  padding-bottom: calc(var(--sticky-cta-pad-block) + env(safe-area-inset-bottom, 0px));
  background: var(--night);
  transform: translateY(100%);
  visibility: hidden;
  transition: transform var(--motion-medium) var(--ease-out),
    visibility 0s linear var(--motion-medium);
}

.sticky-cta.is-visible {
  transform: translateY(0);
  visibility: visible;
  transition: transform var(--motion-medium) var(--ease-out),
    visibility 0s linear 0s;
}

@media (prefers-reduced-motion: reduce) {
  /* Removing the tween also removes the base rule's 350ms delay on the
     visibility flip to `hidden`. That delay exists to keep the bar
     rendered until the slide-out finishes; with no slide there is nothing
     to wait for, so it must not be reintroduced here. Without this,
     reduced-motion users would get 350ms of a hidden bar that is still
     hit-testable and focus-reachable (regression guard for issue #86). */
  .sticky-cta,
  .sticky-cta.is-visible {
    transition: none;
  }
}

.sticky-cta .button {
  width: 100%;
  justify-content: center;
}

/* Reserve space at the end of the document equal to the bar's own
   rendered height, so the fixed bar never paints over the footer's
   last row (see docs/tasks/issue-90-sticky-cta-footer-clearance).
   Additive to the footer's own padding-bottom, not a replacement —
   this preserves 3.5rem of visible breathing room above the bar.
   Sibling combinator only matches on pages where .sticky-cta actually
   precedes .site-footer in the DOM; if that DOM relationship ever
   changes, this selector must be revisited. */
.sticky-cta ~ .site-footer {
  padding-bottom: calc(var(--footer-pad-block) + var(--sticky-cta-height));
}

@media (min-width: 768px) {
  .sticky-cta {
    display: none;
  }

  .sticky-cta ~ .site-footer {
    padding-bottom: var(--footer-pad-block);
  }
}

/* ============ Sections & bands ============ */

.section {
  padding-block: var(--section-padding);
}

.section--paper {
  background: var(--paper);
}

.section--paper-brass {
  background: var(--paper-brass);
}

.section--paper-rose {
  background: var(--paper-rose);
}

.section--night {
  background: var(--night);
  color: var(--night-ink);
  position: relative;
  overflow: hidden;
}

.section__inner {
  max-width: var(--wrapper-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

.section__eyebrow {
  display: block;
  margin-bottom: 1rem;
  color: var(--brass-deep);
}

.section--night .section__eyebrow {
  color: var(--brass-tint);
}

.section__lede {
  font-size: var(--size-lede);
  max-width: var(--measure);
  margin-top: 1rem;
}

/* The opening paragraph under a section heading: body size, capped to a
   readable measure. The smaller sibling of .section__lede. Like
   .section__eyebrow / .section__lede, this is a plain single-class selector
   with no required .section ancestor — contact.html uses it inside
   .contact-form-wrap. */
.section__intro {
  max-width: var(--measure);
  margin-top: var(--space-5);
}

/* The quiet next-action link that closes a content section — the small
   sibling of the .conversion-band CTA. Never let a section dead-end. */
.section__more {
  margin-top: var(--space-6);
}

/* ============ Hero ============ */

/* Height-aware vertical rhythm (issue #81): every value below wraps the
   original `vw`-fluid clamp() in `min(vw-term, vh-term)`, so a short
   viewport (tablet landscape, short laptop window) takes space back from
   padding/gaps/type without a new breakpoint. On a viewport tall enough
   for the design's original intent (roughly >=1150px), every `vh` term
   loses to its `vw` sibling and these resolve to exactly the values
   above — tall desktop stays pixel-identical. `rem` floors stay
   load-bearing under zoom; never a bare `vh` value. See
   docs/tasks/issue-81-hero-tablet-viewport-fold/04-concept-reviewed.md
   §4-5. */
.hero {
  --hero-pad-top: clamp(2.5rem, min(7vw, 6.5vh), 5rem);
  --hero-pad-bottom: clamp(3rem, min(9vw, 8vh), 6rem);
  --hero-gap-h1: clamp(0.75rem, min(2.2vw, 2.2vh), 1.5rem);
  --hero-gap-sublede: clamp(0.75rem, min(2vw, 2vh), 1.25rem);
  --hero-gap-actions: clamp(1.25rem, min(3vw, 3vh), 2rem);
  padding-block: var(--hero-pad-top) var(--hero-pad-bottom);
}

/* Two-zone architecture (issue #42): `.hero__layout` is a CSS Grid with
   two real cells — the text zone (`.hero__text`) and the photo zone
   (`.hero__figure`). Both are ordinary grid items in normal flow; neither
   is positioned. Grid items in separate columns/rows cannot overlap, so
   there is no collision-avoidance formula anywhere in this file any
   more — the two zones simply can't occupy the same space, at any
   viewport. One breakpoint governs the whole hero: 1024px (below). See
   docs/tasks/issue-42-hero-rebuild/04-concept-reviewed.md §5. */
.hero__layout {
  display: grid;
  gap: clamp(1.5rem, min(6vw, 4vh), 3.5rem);
  align-items: start;
}

/* Real measure discipline instead of a width derived from leftover
   space: capped at every breakpoint, so the column never goes
   narrow-and-jammed. At the 20px sub-lede size this yields ~60-65
   characters per line. */
.hero__text {
  max-width: 34rem;
}

/* Hero figure — a 4:5 photograph of Luna (issue #135, replaced the
   hand-built trombone/stave/notes SVG). Same grid cell, same two-zone
   architecture as #42; only the occupant changed. */
.hero__figure {
  width: 100%;
  max-width: 34rem; /* was 22rem */
  justify-self: start;
}

/* Fixed box at every width: reserved at layout time, so a slow or missing
   image costs zero CLS. --night, not .photo-frame's default --paper-brass,
   so the reserved box never flashes cream on the dark hero. */
.photo-frame.hero__photo {
  aspect-ratio: 4 / 5;
  background: var(--night);
}

/* Hero portrait (issue #66) — see docs/tasks/issue-66-hero-photo/. */
.hero__intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
}

/* Redundant inside .hero__text — its own vertical rhythm below already
   separates this row from the h1. Reclaims 16px. */
.hero__intro .section__eyebrow {
  margin-bottom: 0;
}

.hero__portrait {
  display: block;
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  object-fit: cover; /* uniform scale: 1:1 source into a 1:1 box */
  box-shadow: 0 0 0 2px var(--night-ink);
}

@media (min-width: 768px) {
  .hero__intro {
    flex-direction: row;
    align-items: center;
    gap: 1rem;
  }
  .hero__portrait {
    width: clamp(64px, 8vh, 72px);
    height: clamp(64px, 8vh, 72px);
  }
  /* Ceiling, then the #81 short-viewport term (issue #153). 34rem let the
     photo grow to the full 34rem text measure on a tall portrait tablet —
     at 820x1180, `46vh` resolves to 542.8px, i.e. the 34rem cap minus a
     pixel, so the photo rendered 542.8x678.5 and pushed the next section
     off screen entirely. In this band the photo is the second beat under
     compact intro copy, not a banner: 22rem/352px (x440 at 4:5) is ~65% of
     the text measure and sits next to the 328px the same device shows in
     landscape. `46vh` is kept and now binds only below a 765px-tall
     viewport — exactly the squat case #81 added it for. No new media
     query: the 1024px rule below still overrides this one, so the
     two-column band is untouched. Measured targets in
     docs/tasks/issue-153-hero-photo-tablet-portrait/01-concept.md §4. */
  .hero__figure {
    max-width: min(22rem, 46vh);
  }
}

@media (min-width: 1280px) {
  .hero__portrait {
    width: clamp(64px, 8vh, 80px);
    height: clamp(64px, 8vh, 80px);
  }
}

/* Vertical rhythm inside the text zone — identical at every breakpoint,
   see docs/tasks/issue-42-hero-rebuild/05-storyboard-reviewed.md §3. */
/* Hero-scoped override of the display size — deliberately does not touch
   the global `--size-h1` token, which other pages depend on. The `9vh`
   term is generous: it only bites once the viewport is genuinely short
   (see 04-concept-reviewed.md §5.2). */
.hero h1 {
  color: var(--night-ink);
  font-size: clamp(2.25rem, min(6vw, 9vh), 4.5rem);
  margin-top: var(--hero-gap-h1, 1.5rem);
}

/* EN headline is longer than DE ("Jazz trombone with a cheeky edge.") and
   wraps to 3 lines at tablet-landscape widths under the shared clamp,
   eating the vertical space the short-viewport fix above reclaims. Locale-
   scoped override, tighter vh cap, so it wraps at 2 lines like DE.
   The 7.5vh term alone only binds at short heights; at mid heights
   (~850-1200px, e.g. 1366x937) it stops being the smallest clamp
   argument and the size climbs back toward the ceiling, re-wrapping to
   3 lines. The ceiling itself is lowered (4.5rem -> 3.5rem) so growth is
   capped everywhere the vh/vw terms don't already bind it lower — this
   holds 2 lines across the full 1024-1440px width band regardless of
   height. See issue #81 Ed round 1 + Frank QA round 1 feedback.
   ES ("Trombón de jazz con un punto descarado.") is longer still and
   takes the same override. */
html[lang="en"] .hero h1,
html[lang="es"] .hero h1 { font-size: clamp(2.25rem, min(6vw, 7.5vh), 3.5rem); }

/* `opacity: .92` deliberately removed — it was a silent contrast
   reducer (SC 1.4.3). Full-opacity `--night-ink` is load-bearing for the
   measured 9.66:1 ratio in 05-storyboard-reviewed.md §5.1 row 2. */
.hero__sublede {
  font-size: var(--size-lede);
  color: var(--night-ink);
  margin-top: var(--hero-gap-sublede, 1.25rem);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem;
  margin-top: var(--hero-gap-actions, 2rem);
}

/* The one hero-specific breakpoint (issue #42 definition of done): below
   it the hero is a single stacked column (text zone, then figure zone,
   both full-width rows); above it, a real two-column grid. If a second
   hero-specific media query becomes necessary, that's a signal the
   two-zone architecture wasn't fully applied. */
@media (min-width: 1024px) {
  .hero__layout {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    gap: clamp(3rem, 5vw, 5rem);
    align-items: center;
  }

  .hero__figure {
    max-width: min(24rem, 40vh);
    justify-self: end;
  }
}

/* ============ Brass Wash ============ */

/* Both radial hotspots' alpha pulled from 0.9/0.85 down to 0.50, and the
   warm hotspot moved from 15% 15% (behind the text) to 78% 18% (behind
   the figure zone, instead of washing out the headline). This is a
   shared rule — the change applies
   site-wide (hero + the 10 other `.conversion-band` instances), per
   Elli's ruling in docs/tasks/issue-42-hero-rebuild/04-concept-reviewed.md
   §5.3/§7.1 and 05-storyboard-reviewed.md §6. Rendered/sampled worst-case
   pixel is now rgb(79, 55, 33) / #4F3721 (relative luminance ≤ 0.046);
   every text/button pairing that uses this background clears WCAG 2.2 AA
   against it — see 05-storyboard-reviewed.md §5.1. */
.brass-wash {
  background: radial-gradient(
      120% 140% at 78% 18%,
      var(--wash-a),
      transparent 62%
    ),
    radial-gradient(120% 140% at 12% 100%, var(--wash-b), transparent 62%),
    linear-gradient(160deg, var(--night) 0%, var(--wash-mid) 55%, var(--night) 100%);
  background-size: 200% 200%;
  animation: brass-drift 26s ease-in-out infinite alternate;
}

@keyframes brass-drift {
  from {
    background-position: 0% 0%, 100% 100%, 0% 0%;
  }
  to {
    background-position: 15% 10%, 85% 90%, 10% 5%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .brass-wash {
    animation: none;
  }
}

/* ============ Screen Dot ============ */

.section--night::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(var(--night-dot) 1px, transparent 1px);
  background-size: 14px 14px;
  opacity: 0.5;
}

/* ============ Pulse Bars ============ */

.pulse-bars {
  display: flex;
  align-items: flex-end;
  gap: clamp(3px, 1vw, 6px);
  height: 64px;
}

/* Size modifier only. The 1.5rem lead-out that accompanies it on about.html
   is spacing, not size, and stays on the .mb-6 utility. */
.pulse-bars--sm { height: 40px; }

.pulse-bars__bar {
  flex: 1;
  min-width: 3px;
  max-width: 10px;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--rose), var(--brass));
  animation: pulse-bar 5s ease-in-out infinite;
  transform-origin: bottom;
}

/* Tint pair applies wherever the bars are drawn on a night surface: the
   .section--night branch covers a bare .pulse-bars run placed directly in a
   night section; the .empty-state branch covers the card itself, which is
   unconditionally dark (background: var(--night) above) regardless of the
   section it sits in. Both branches carry the identical declaration and are
   equal specificity, so when a card sits inside a night section (today: all
   .empty-state instances) they simply agree — no conflict, no visual change. */
.section--night .pulse-bars__bar,
.empty-state .pulse-bars__bar {
  background: linear-gradient(180deg, var(--rose-tint), var(--brass-tint));
}

.pulse-bars__bar:nth-child(1) { height: 40%; animation-delay: 0s; }
.pulse-bars__bar:nth-child(2) { height: 70%; animation-delay: 0.3s; }
.pulse-bars__bar:nth-child(3) { height: 100%; animation-delay: 0.6s; }
.pulse-bars__bar:nth-child(4) { height: 55%; animation-delay: 0.9s; }
.pulse-bars__bar:nth-child(5) { height: 85%; animation-delay: 1.2s; }
.pulse-bars__bar:nth-child(6) { height: 35%; animation-delay: 1.5s; }
.pulse-bars__bar:nth-child(7) { height: 65%; animation-delay: 1.8s; }
.pulse-bars__bar:nth-child(8) { height: 90%; animation-delay: 2.1s; }
.pulse-bars__bar:nth-child(9) { height: 45%; animation-delay: 2.4s; }
.pulse-bars__bar:nth-child(10) { height: 75%; animation-delay: 2.7s; }
.pulse-bars__bar:nth-child(11) { height: 55%; animation-delay: 3s; }
.pulse-bars__bar:nth-child(12) { height: 95%; animation-delay: 3.3s; }

@keyframes pulse-bar {
  0%, 100% {
    transform: scaleY(0.85);
    opacity: 0.75;
  }
  50% {
    transform: scaleY(1);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pulse-bars__bar {
    animation: none;
    transform: scaleY(1);
    opacity: 0.9;
  }
}

/* ============ Fact strip ============ */

.fact-strip {
  position: relative;
  /* Height-aware lead-in (issue #81): replaces the inherited
     `--section-padding` top value (102-128px) with a shorter,
     height-aware one so the strip reads as attached to the hero above
     it rather than floating in a black field. Bottom padding is
     untouched (still `--section-padding` via `.section`), so the
     asymmetry — tight lead-in, normal exit — is deliberate. Homepage-only
     selector; does not affect any other section's rhythm. See
     05-storyboard-reviewed.md §5 (Elli-approved scope addition). */
  padding-top: clamp(2.5rem, min(8vw, 9vh), 5rem);
}

.fact-strip__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.5rem;
  /* Was `2rem` — the house pattern for separating a list from an
     eyebrow/heading above it. This section has neither (it is a labelled
     region with no heading), so the list is the section's only child and
     the margin was separating it from nothing. Orphaned value, removed
     (issue #81). */
  margin-top: 0;
}

.fact-chip {
  border: 1px solid var(--night-rule);
  border-radius: 0.75rem;
  padding: 1.25rem;
  font-family: var(--font-mono);
  font-size: var(--size-mono);
  letter-spacing: 0.01em;
  color: var(--night-ink);
}

/* ============ Cards ============ */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1.5rem;
  margin-top: 2rem;
}

/* Press highlights: six award cards. Wider minimum columns give 3 + 3
   on tablet/desktop instead of 4 + 2. */
.card-grid--awards {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}

.card {
  background: var(--surface-card);
  color: var(--ink);
  border: 1px solid var(--hairline-strong);
  border-radius: 1rem;
  padding: 1.75rem;
}

.section--paper-brass .card,
.section--paper-rose .card {
  background: var(--surface-card);
}

.card--featured {
  border-color: var(--brass);
  border-width: 2px;
}

.card__eyebrow {
  color: var(--brass-deep);
  display: block;
  margin-bottom: 0.5rem;
}

.section--night .card__eyebrow {
  color: var(--brass-tint);
}

.card h3 {
  margin-bottom: 0.5rem;
}

.card--standout {
  grid-column: 1 / -1;
  background: linear-gradient(135deg, var(--paper-brass), var(--paper-rose));
  border: 2px solid var(--brass);
}

/* Issue #106: cap the body copy's reading measure without shrinking the
   full-width emphasis box. See docs/tasks/issue-106-reading-measure-cap/. */
.card--standout p {
  max-width: var(--measure);
}

/* ============ News feed (issue #157) ============
   A single-column, newest-first stack of .card entries. One column is
   load-bearing, not stylistic: a feed's content IS its order, and
   .card-grid's auto-fit columns destroy it (see 01-concept.md §4).
   Width = the 70ch measure plus the card's own 1.75rem padding on each
   side, so text inside a news card runs to exactly the same measure as
   body copy everywhere else on the site. */
.news-feed {
  list-style: none;
  margin: var(--space-8) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
  max-width: calc(var(--measure) + 3.5rem);
}

/* The entry's optional onward link sits tighter than the standalone
   .cta-row default (1.5rem), because it closes a card rather than a
   section. Keeps the 44px min-height .cta-row .text-link already sets. */
.news-item .cta-row {
  margin-top: var(--space-4);
}

/* ============ Reassurance chips ============ */

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

/* Group label above a .chip-row. A bare .mono-label is display:inline, so the
   mt-* utilities are inert on it — this needs its own block box to sit in the rhythm. */
.chip-row__label { display: block; margin-top: var(--space-8); }
.chip-row__label + .chip-row { margin-top: var(--space-2); }

.chip {
  border: 1px solid var(--brass);
  color: var(--brass-deep);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

.section--night .chip {
  color: var(--brass-tint);
}

/* ============ Timeline Rule ============ */

.timeline {
  margin-top: 2.5rem;
  border-left: 2px solid var(--hairline-strong);
  padding-left: 1.75rem;
}

.section--night .timeline {
  border-left-color: rgba(245, 239, 230, 0.2);
}

.timeline__year {
  font-family: var(--font-mono);
  font-size: 1rem;
  color: var(--rose);
  margin: 2.5rem 0 1rem;
  position: relative;
}

.timeline__year:first-child {
  margin-top: 0;
}

.timeline__year::before {
  content: "";
  position: absolute;
  left: -2.3rem;
  top: 0.4em;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--rose);
}

.section--night .timeline__year {
  color: var(--rose-tint);
}

.section--night .timeline__year::before {
  background: var(--rose-tint);
}

/* Issue #106: cap the reading measure at the item level, since the body
   copy is a bare text node with no inner wrapper to target directly.
   This intentionally also shortens the border-bottom hairline below to the
   measure on wide viewports — that's a reviewed, deliberate consequence
   (see docs/tasks/issue-106-reading-measure-cap/), not a bug to "fix" back. */
.timeline__item {
  max-width: var(--measure);
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--hairline);
}

.section--night .timeline__item {
  border-bottom-color: rgba(245, 239, 230, 0.12);
}

.timeline__item:last-child {
  border-bottom: 0;
}

.timeline__date {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  display: block;
  opacity: 0.75;
  margin-bottom: 0.15rem;
}

/* ============ Credit roster (issue #177) ============
   Names-only roster: one person per row, rows flowing into columns, a
   hairline above each row. Deliberately NOT .timeline — that component's
   grammar is "a labelled period, then names", and this list must never
   carry a period or an affiliation (see 04-concept-reviewed.md §5).
   max-width caps the grid at three columns on desktop: wider and the block
   stretches toward the 1280px wrapper and starts reading as a table. */
.credit-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 0 var(--space-8);
  max-width: 56rem;
  margin-top: var(--space-8);
}

.credit-list li {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--hairline-strong);
}

/* 29 single-column rows is a long scroll on a phone — tighten the rhythm one
   step rather than shortening the list. */
@media (max-width: 47.99em) {
  .credit-list li { padding: var(--space-2) 0; }
}

/* ============ Empty state ============ */

.empty-state {
  position: relative;
  border-radius: 1.25rem;
  padding: clamp(2rem, 5vw, 3rem);
  margin-top: 2rem;
  background: var(--night);
  color: var(--night-ink);
  overflow: hidden;
}

/* On a night section the card's edge is the sole shape-defining boundary
   against its own background (3.29:1, clears SC 1.4.11's 3:1 — same
   token/rationale approved in issue #50). At 1.5px rather than 1px so the
   line holds as one shape over the card's ~1183px perimeter at desktop,
   where a hairline softens; still below the 2px weight reserved for
   .card--featured/.card--standout, so this reads as definition, not
   emphasis (issue #92). Colour/contrast unchanged. Scoped to
   .section--night only; .empty-state on paper sections is unaffected.
   Uses box-shadow rather than border-width: in Chromium, fractional
   border-width (e.g. 1.5px) is rounded to the nearest whole device pixel
   before rasterizing on elements with border-radius, so it renders
   pixel-identical to a 1px border here — a real, confirmed no-op. A
   box-shadow ring isn't subject to that rounding and follows the
   border-radius correctly. Do not "simplify" this back to border-width. */
.section--night .empty-state {
  box-shadow: 0 0 0 1.5px var(--night-rule);
}

.empty-state__bars {
  margin-bottom: 1.5rem;
  opacity: 0.85;
}

.empty-state__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  margin-top: 1.5rem;
  align-items: center;
}

/* ============ Last-seen block ============ */
/* "Last seen at": mono eyebrow label over a display-serif venue/date line.
   Used on the homepage live teaser and in the concerts empty state. The
   wrapper owns the block's lead-in so neither child carries outer spacing. */

.last-seen {
  margin-top: var(--space-8);
}

.last-seen__label {           /* always used together with .mono-label */
  margin-bottom: var(--space-1);
  color: var(--brass-deep);
}

/* The dark-surface recolour keys off the actual dark surface, not off an
   ancestor that happens to be dark. Previously this came from
   `.section--night .section__eyebrow`, which only worked on concerts.html
   because the night .empty-state happens to sit inside a .section--night —
   move that empty state to a paper section and the label would silently
   become --brass-deep on --night = 2.61:1, a hard SC 1.4.3 failure. */
.section--night .last-seen__label,
.empty-state .last-seen__label {
  color: var(--brass-tint);
}

/* margin: 0 removes the global `p { margin: 0 0 1em }` bottom margin, which
   here is 22px (1em of the element's own 22px size) and currently collapses
   against a larger following margin (28px on index, 24px on concerts) — so
   it never contributed anything. Verified in the rendered page. */
.last-seen__value {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--size-display-sm);
}

/* ============ Meanwhile Strip (issue #161) ============ */
/* "Here's what exists in the meantime": attaches to the bottom of a block
   that has just told the visitor "there's nothing here yet" and points at
   the existing footer Instagram lockup, promoted into content flow. Two
   instances: compact (homepage Music teaser, no eyebrow) and full (Music
   page recordings empty state, with eyebrow). Inherits everything, adds no
   new token. See docs/tasks/issue-161-instagram-doubles-outside-quote/. */
.meanwhile {
  margin-top: var(--space-8);
  padding-top: var(--space-6);
  max-width: var(--measure);
  border-top: 1px solid var(--ink-rule);
}

/* Border colour keys off the component's own surface, not off a distant
   ancestor — the .last-seen__label lesson just above this block. */
.section--night .meanwhile,
.empty-state .meanwhile {
  border-top-color: var(--night-rule);
}

/* Inside the empty-state card the card's own padding already bounds the
   strip, so the measure cap (meant to stop the rule becoming a full-width
   hairline on a page surface) is released. */
.empty-state .meanwhile {
  max-width: none;
}

.meanwhile__label {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--brass-deep);
}

.section--night .meanwhile__label,
.empty-state .meanwhile__label {
  color: var(--brass-tint);
}

.meanwhile__text {
  margin: 0 0 var(--space-4);
  max-width: var(--measure);
}

.meanwhile__link {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-height: 44px;
}

.meanwhile__handle {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* Conditional second link (one specific reel/post) — only built once Luna
   supplies a real permalink; unused until then. display:flex (rather than
   the link's own inline-flex) so it reliably starts its own line under the
   primary link without depending on available inline width. */
.meanwhile__deeplink {
  display: flex;
  align-items: center;
  gap: 0.5em;
  min-height: 44px;
  margin-top: var(--space-2);
  width: fit-content;
}

/* ============ Spec list ============ */
/* Term/value rows for plainly-stated booking facts (issue #160). Mono term in
   a fixed left column, body value beside it, hairline between rows — a spec
   sheet, not a card grid. Sentence-case terms on purpose: .mono-label
   uppercases, this does not, so a term can't be mistaken for an eyebrow. */
.spec-list { margin-top: var(--space-8); max-width: var(--measure); }
.spec-list__row { padding: var(--space-4) 0; border-top: 1px solid var(--hairline-strong); }
.spec-list__row:last-child { border-bottom: 1px solid var(--hairline-strong); }
.spec-list dt { font-family: var(--font-mono); font-size: var(--size-mono); color: var(--brass-deep); }
.spec-list dd { margin: var(--space-1) 0 0; }

/* Dark-surface colours key off the component, not off an ancestor. */
.section--night .spec-list__row { border-color: var(--night-rule); }
.section--night .spec-list dt { color: var(--brass-tint); }

@media (min-width: 768px) {
  .spec-list__row {
    display: grid;
    grid-template-columns: minmax(11rem, 14rem) 1fr;
    gap: var(--space-6);
    align-items: baseline;
  }
  .spec-list dd { margin-top: 0; }
}

/* ============ Spec claim ============ */
/* One high-emphasis statement under a mono label. Deliberately mirrors the
   metrics of .last-seen (mono label + display-serif value line, the documented
   purpose of --size-display-sm) but is its own component: reusing a class named
   "last seen at" for a capability statement would be a lie in the markup.
   22px regular is NOT WCAG "large text" — hold it to 4.5:1. */
.spec-claim { margin-top: var(--space-10); max-width: var(--measure); }
.spec-claim + .spec-list { margin-top: var(--space-10); }
.spec-claim__label { display: block; margin-bottom: var(--space-1); color: var(--brass-deep); }
.spec-claim__text { margin: 0; font-family: var(--font-display); font-size: var(--size-display-sm); line-height: 1.35; }
.section--night .spec-claim__label { color: var(--brass-tint); }

/* ============ Gig list ============ */
/* Upcoming-date rows (issue #160). Ships ahead of any real date so the first
   one added lands in a defined pattern instead of an improvised one — see
   docs/tasks/issue-160-stage-plot-tech-rider-gap/04-concept-reviewed.md §7.1
   and the reference template commented into concerts.html. Three columns on
   tablet+: date rail | body | single action. Secondary information is
   quieter by size, family and position — never by a dimmed colour (there is
   no night muted token, and inventing one would be a silent SC 1.4.3
   reducer). */
.gig-list { margin-top: var(--space-8); }
.gig { padding: var(--space-6) 0; border-top: 1px solid var(--hairline-strong); }
.gig:last-child { border-bottom: 1px solid var(--hairline-strong); }
.gig__date { margin: 0 0 var(--space-2); font-family: var(--font-mono); font-size: var(--size-mono); color: var(--brass-deep); }
.gig__time { display: block; }
.gig__body { max-width: var(--measure); }
.gig__eyebrow { display: block; margin-bottom: var(--space-1); color: var(--brass-deep); }
.gig__title { margin: 0; font-family: var(--font-display); font-size: var(--size-display-sm); font-weight: 600; line-height: 1.3; }
.gig__address { margin: var(--space-1) 0 0; }
.gig__meta { margin: var(--space-1) 0 0; font-family: var(--font-mono); font-size: var(--size-mono); }
.gig__links { margin: var(--space-2) 0 0; }
.gig__action { margin: var(--space-4) 0 0; }
.gig__door { font-family: var(--font-mono); font-size: var(--size-mono); }

.section--night .gig { border-color: var(--night-rule); }
.section--night .gig__date,
.section--night .gig__eyebrow { color: var(--brass-tint); }

@media (min-width: 768px) {
  .gig {
    display: grid;
    grid-template-columns: minmax(8rem, 9rem) 1fr auto;
    gap: var(--space-6);
    align-items: start;
  }
  .gig__date { margin: 0; }
  .gig__action { margin: 0; justify-self: end; text-align: right; }
}

/* The "Sold out" state reuses .fact-chip (mono, --night-ink text,
   --night-rule border) with one small modifier, because the base chip is
   sized for the homepage fact-strip grid cell (1.25rem padding) and reads as
   a slab inline. Colour, border and font are inherited unchanged — this only
   resizes the box, so the SC 1.4.11 border ratio is unaffected. */
.fact-chip--inline { display: inline-block; padding: var(--space-1) var(--space-3); }

/* Anchor target under the sticky header: land the "for bookers" band's top
   edge just below the header instead of behind it. */
#booking-specs { scroll-margin-top: 5rem; }
#history,
#ensembles { scroll-margin-top: 5rem; }

/* ============ Conversion band ============ */

.conversion-band {
  padding-block: var(--section-padding);
  text-align: left;
}

.conversion-band__inner {
  max-width: var(--wrapper-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

.conversion-band h2 {
  color: var(--night-ink);
  max-width: var(--measure);
}

.conversion-band p {
  color: var(--night-ink);
  font-size: var(--size-lede);
  margin-top: 1rem;
  opacity: 0.92;
  max-width: var(--measure);
}

.conversion-band .button {
  margin-top: 1.75rem;
}

/* ============ Footer ============ */

.site-footer {
  background: var(--night);
  color: var(--night-ink);
  padding-block: var(--footer-pad-block);
}

.site-footer a {
  text-decoration: none;
}

.site-footer__inner {
  max-width: var(--wrapper-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  gap: 2rem;
}

.footer-nav ul,
.footer-legal ul {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}

.footer-nav a,
.footer-legal a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  color: var(--night-ink);
  opacity: 0.85;
}

.footer-nav a:hover,
.footer-legal a:hover {
  opacity: 1;
  text-decoration: underline;
}

/* One item today (@jazz.posaune, issue #149); flex + gap so additional
   handles slot in without a layout change. */
.footer-social {
  display: flex;
  gap: 1rem;
}

/* Mirrors .footer-nav a exactly — same colour, same rest opacity, same
   44px house minimum (SC 2.5.8 floor is 24px; the footer rows are all 44). */
.footer-social a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: var(--night-ink);
  opacity: 0.85;
}

.footer-social a:hover {
  opacity: 1;
}

/* Underline the words, not the icon or the gaps: `text-decoration` on the
   anchor would draw a rule under the SVG too. Needs the .site-footer prefix
   to out-rank `.site-footer a { text-decoration: none; }`. */
.site-footer .footer-social a:hover .footer-social__label,
.site-footer .footer-social a:hover .footer-social__handle,
.site-footer .footer-social a:focus-visible .footer-social__label,
.site-footer .footer-social a:focus-visible .footer-social__handle {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.footer-social__icon {
  flex-shrink: 0;
}

/* The handle reads as data, in the site's mono voice. Differentiated by
   face and size only — never by lowering contrast. */
.footer-social__handle {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

.footer-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: center;
  font-size: 0.9rem;
  opacity: 0.75;
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 2rem;
  /* Was a hard-coded day-theme warm off-white literal (left un-repointed
     by #140); re-pointed through --hairline (issue #147) — the last
     hard-coded colour in the footer, decorative only, not a compliance
     fix. */
  border-top: 1px solid var(--hairline);
}

/* Non-color affordance (#97, SC 1.4.1). In the footer .lang-switch sits
   inline with the plain-text "Hamburg · Lüneburg" span, so the inactive
   language link needs a cue beyond color to read as interactive; the
   header instance is excluded (no adjacent plain text there, and the
   underline would fight its existing current-page signal). Needs the
   .site-footer prefix to out-rank `.site-footer a { text-decoration:
   none; }` above. Applies to both links: the aria-current rule below
   already underlines the current one, so this just extends the same
   treatment to the inactive state without changing it. */
.site-footer .lang-switch a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.site-footer .lang-switch a[aria-current="true"] {
  color: var(--rose-tint);
}

/* Current-page indicator (#44), footer-legal variant: mirrors the rule
   above. Legal pages (Imprint/Privacy) have no primary-nav item, so this
   is the sole "you are here" signal on those pages. Must use --rose-tint,
   not --rose-deep (2.23:1 on --night, fails). opacity: 1 on top of the
   colour/weight/underline cues makes the current item the brightest row,
   too. Needs the .site-footer prefix to out-rank `.site-footer a {
   text-decoration: none; }` above. */
.site-footer .footer-legal a[aria-current="page"] {
  color: var(--rose-tint);
  opacity: 1;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ============ Forms ============ */

.form-grid {
  display: grid;
  gap: 1.5rem;
  margin-top: 2rem;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.form-field label {
  font-weight: 600;
  font-size: 0.9375rem;
}

.form-field .field-hint {
  font-size: 0.875rem;
  opacity: 0.75;
}

.form-field input,
.form-field select,
.form-field textarea {
  font: inherit;
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  border: 1.5px solid var(--ink-rule);
  background: var(--paper);
  color: var(--ink);
  min-height: 44px;
}

.form-field textarea {
  min-height: 140px;
  resize: vertical;
}

.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible {
  outline: 2px solid var(--rose);
  outline-offset: 1px;
  border-color: var(--rose);
}

.form-field.has-error input,
.form-field.has-error select,
.form-field.has-error textarea {
  border-color: var(--rose);
}

.field-error {
  color: var(--rose);
  font-size: 0.875rem;
  font-weight: 600;
  display: none;
}

.form-field.has-error .field-error {
  display: block;
}

.form-field--consent {
  gap: 0.5rem;
}

/*
  The consent checkbox sits directly above the submit button. Checking the
  box moves focus there, which blurs the checkbox and (correctly) clears
  its error — but with `display: none/block` that also collapses/expands
  the error line's height right as a user's next click lands, shifting the
  button out from under the pointer mid-click. Reserve the line's height
  here instead, so toggling visibility doesn't reflow the button.
*/
.form-field--consent .field-error {
  display: block;
  visibility: hidden;
  min-height: 1.25em;
}

.form-field--consent.has-error .field-error {
  visibility: visible;
}

.form-consent {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}

.form-consent input[type="checkbox"] {
  /* 44×44 is the house minimum tap target (see the touch-target note in the
     Responsive base comment below); an explicit width/min-width is required
     because `.form-field input { min-height: 44px }` above overrides `height`
     and would otherwise leave this box 22×44 — issue #95. */
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  flex-shrink: 0;
  margin-top: 0;
  accent-color: var(--rose);
}

.form-consent label {
  font-weight: 400;
  font-size: 0.9375rem;
  line-height: 1.5;
}

.form-error-summary {
  display: none;
  border: 1.5px solid var(--rose);
  background: var(--paper-rose);
  color: var(--ink);
  border-radius: 0.5rem;
  padding: 1rem 1.25rem;
  font-weight: 600;
  margin-bottom: 1.5rem;
  scroll-margin-top: 7rem;
}

.form-error-summary.is-visible {
  display: block;
}

.form-success {
  display: none;
  border: 1.5px solid var(--brass);
  background: var(--paper-brass);
  border-radius: 1rem;
  padding: 2rem;
  scroll-margin-top: 7rem;
}

.form-success.is-visible {
  display: block;
}

/*
  Note: the form itself is hidden via the `hidden` attribute set directly
  by site.js on submit (see initBookingForm), not via CSS here. The
  mailto fallback link deliberately stays visible even after a successful
  in-place submission — see 05-storyboard-reviewed.md, Form patterns:
  "A mailto: fallback is always visible near the form."
*/

.form-fallback {
  margin-top: 1.5rem;
  font-size: 0.9375rem;
}

/* ============ Prose lists ============ */
/* The global `ul, ol { list-style: none }` reset is correct and stays —
   nearly every list on this site is a layout list (nav, footer, chips,
   tags, card grids). This is the opt-in for the exception: a list in
   running prose where the bullets carry meaning. Reach for this instead of
   re-enabling list-style inline, which is what de/datenschutz.html and
   en/privacy.html had to do before this component existed. */

.prose-list {
  list-style: disc;
  padding-inline-start: var(--space-5);   /* 1.25rem marker gutter */
}

/* Booking checklist variant (issue #85): brass markers, airier items, and
   its own lead-in. The base carries no margin-top on purpose — the legal
   pages' lists have none today, and adding one would shift those pages. */
.checklist { margin-top: var(--space-6); }
.checklist > li { margin-block-end: var(--space-2); }
.checklist > li:last-child { margin-block-end: 0; }
.checklist > li::marker { color: var(--brass-deep); }

.contact-form-wrap .checklist + .button { margin-top: var(--space-6); }

.contact-layout {
  display: grid;
  gap: 3rem;
  margin-top: 2.5rem;
}

.contact-panel {
  background: var(--paper-brass);
  border-radius: 1rem;
  padding: 2rem;
  height: fit-content;
}

.contact-panel dt {
  font-weight: 600;
  margin-top: 1.25rem;
}

.contact-panel dt:first-child {
  margin-top: 0;
}

.contact-panel dd {
  margin: 0.25rem 0 0;
}

/*
  Field pairing, issue #33. Reuses the standing 768px tablet breakpoint (see
  the convention comment in "Responsive base" below) rather than introducing
  a new one. Pairing follows 05-storyboard-reviewed.md's table exactly, in
  DOM order: Name+Email, Phone+"reaching out as", "what's this about"+
  "preferred dates", Venue+Budget. The message textarea and "how did you hear"
  fields have no natural pairing partner, so they stay full-width rather than
  being paired with something unrelated.

  [ELLI] must-fix, not just tidy-up: the consent block, the error summary,
  and the submit button are given *explicit* `grid-column: 1 / -1` below
  rather than being left to auto-placement. Without this, the consent
  checkbox/legal text could collapse into a half-width column and sit beside
  an unrelated field, which would read as if the consent text were scoped to
  that field — a GDPR consent-clarity problem, not just a layout regression.
*/
@media (min-width: 768px) {
  .form-grid {
    grid-template-columns: 1fr 1fr;
  }

  /* Row 5: "Tell us about the event" (textarea) — no natural pairing
     partner, stays full width. */
  .form-grid .form-field:has(textarea) {
    grid-column: 1 / -1;
  }

  /* Row 6: "How did you hear about Luna?" — single field, no natural
     partner; left full width rather than paired with something unrelated. */
  .form-grid .form-field:has(#source) {
    grid-column: 1 / -1;
  }

  /* Row 7: consent checkbox block — must never auto-place into a half-width
     column beside an unrelated field. See [ELLI] note above. */
  .form-field--consent {
    grid-column: 1 / -1;
  }

  /* Row 8: submit button — explicit, not relying on auto-placement landing
     it in the grid's last column. */
  .form-grid > button {
    grid-column: 1 / -1;
  }

  /* Sits outside .form-grid as a block-level sibling (see markup), so it is
     already full-width by normal block flow; grid-column here is a
     defensive no-op kept per Elli's explicit requirement in case the error
     summary is ever nested inside the grid. */
  .form-error-summary {
    grid-column: 1 / -1;
  }
}

@media (min-width: 1024px) {
  .contact-layout {
    grid-template-columns: 1.5fr 1fr;
  }

  .contact-layout .contact-form-wrap {
    order: 1;
  }

  .contact-layout .contact-panel {
    order: 2;
  }
}

/* ============ Embeds (click-to-load facade) ============ */

.embed-card {
  background: var(--night);
  color: var(--night-ink);
  border-radius: 1rem;
  padding: 2rem;
  margin-top: 1.5rem;
}

.embed-card__note {
  font-size: 0.875rem;
  opacity: 0.75;
  margin-top: 1rem;
}

/* ============ Scroll reveal (progressive enhancement) ============ */

.reveal {
  opacity: 1;
  transform: none;
}

.js-reveal .reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--motion-medium) var(--ease-out),
    transform var(--motion-medium) var(--ease-out);
}

.js-reveal .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ============ Scroll cue (site.js initScrollCue) ============ */

.scroll-cue {
  position: fixed;
  left: 50%;
  bottom: max(1.25rem, env(safe-area-inset-bottom));
  z-index: 40;
  width: 44px;
  height: 44px;
  margin-left: -22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
  cursor: pointer;
  transition: opacity var(--motion-medium) var(--ease-out),
    visibility 0s linear 0s;
  animation: scroll-cue-bob 2.4s var(--ease-out) infinite;
}

.scroll-cue svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.scroll-cue:hover {
  color: var(--rose-deep);
}

.scroll-cue.is-hidden {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--motion-medium) var(--ease-out),
    visibility 0s linear var(--motion-medium);
}

@keyframes scroll-cue-bob {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(6px); }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-cue {
    animation: none;
  }
}

@media print {
  .scroll-cue {
    display: none;
  }
}

/* ============ Instrument tags ============ */

.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 1.25rem;
}

.tag {
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 400;
}

.tag--primary {
  background: var(--rose);
  color: var(--paper);
}

.tag--secondary {
  border: 1px solid var(--brass);
  color: var(--brass-deep);
}

.section--night .tag--secondary {
  color: var(--brass-tint);
}

/* Two-field spec chip (issue #161): instrument name | proficiency level.
   Hierarchy comes from weight (bold name) and a drawn divider, never from
   dimming the level — muting the qualifier's contrast is the exact SC 1.4.3
   regression that has already shipped and been reverted on this project
   twice. Both fields stay the same size and the same colour. */
.tag__name {
  font-weight: 700;
}

.tag__level {
  position: relative;
  margin-left: 0.55em;
  padding-left: 0.65em;
}

.tag__level::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.15em;
  bottom: 0.15em;
  width: 1px;
  background: currentColor;
  opacity: 0.55;
}

/* Divider needs its own colour on the filled rose pill — currentColor there
   is --paper already, so this is explicit for clarity, not a fix. */
.tag--primary .tag__level::before {
  background: var(--paper);
}

/* ============ Outside Voice (issue #161) ============ */
/* An attributed quote from someone other than the site itself. Component
   only — ships with no visible content on any page in this issue; the
   markup lives as a TODO(content) comment in about.html until a real,
   permissioned quote exists. See
   docs/tasks/issue-161-instagram-doubles-outside-quote/. Marked as a
   quotation by four redundant cues (rule, inset, italic, attribution line)
   so colour alone never carries the meaning. */
.outside-voice {
  margin: var(--space-12) 0 0;
  max-width: var(--measure);
  padding-left: var(--space-6);
  border-left: 3px solid var(--brass);
}

.outside-voice blockquote {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--size-display-sm);
  line-height: 1.45;
}

.outside-voice blockquote p {
  margin: 0;
}

.outside-voice figcaption {
  margin-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--size-note);
  font-style: normal;
  color: var(--ink-muted);
}

/* ============ Photos (issue #38 — Luna's photos) ============

  Companion to docs/tasks/issue-38-luna-photos/05-storyboard-reviewed.md.
  Shared treatment for every real photo on the site: soft `.card`-matching
  radius. (The thin brass→rose rule along the bottom edge was removed at
  the site owner's request.)
  No color grading/duotone/filters are applied to the photographs themselves
  — see concept §2. Motion: these images live inside sections that already
  carry the sitewide `.reveal` class, so they fade in with their section
  automatically; no extra motion hook is needed here.
*/

.photo-frame {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: 1rem;
  background: var(--paper-brass);
}

.photo-frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Homepage — About-teaser photo (luna-06, Saskia Meiß) ---- */

.teaser-layout {
  display: grid;
  gap: 1.75rem;
  margin-top: 1.5rem;
}

/* Mobile: `.teaser-text` disappears from the box tree (its children become
   direct grid items of `.teaser-layout`) so `order` can place the photo
   between the paragraph and the CTA link, matching storyboard §1, without
   requiring that DOM order themselves (copy, photo, cta is also the actual
   DOM order below, kept in sync for no-JS/no-CSS fallback and reader order). */
.teaser-text {
  display: contents;
}

.teaser-text .teaser-copy,
.teaser-text .teaser-cta {
  margin: 0;
}

.teaser-copy {
  order: 1;
}

/* The photo sits in a <figure> with its credit line, so the figure is
   the grid item; the frame inside keeps the 4:5 crop. */
.teaser-figure {
  order: 2;
  max-width: 26rem;
}

.teaser-photo {
  aspect-ratio: 4 / 5;
}

.teaser-cta {
  order: 3;
}

@media (min-width: 768px) {
  .teaser-layout {
    grid-template-columns: 1.4fr 1fr;
    align-items: center;
    gap: 3rem;
  }

  /* Desktop: copy + CTA become one grid cell in normal document flow, so
     the link sits at ordinary paragraph spacing directly under the copy
     instead of each living in its own grid row inflated by the taller
     spanning photo next to them. */
  .teaser-text {
    display: block;
    grid-column: 1;
    grid-row: 1;
  }

  .teaser-text .teaser-copy {
    margin-bottom: 1.25rem;
  }

  .teaser-figure {
    grid-column: 2;
    grid-row: 1;
    order: initial;
    max-width: none;
  }
}

/* ---- About page — hero photo (luna-03) ---- */

.hero-layout {
  display: grid;
  gap: 2rem;
  margin-top: 2rem;
}

.hero-photo {
  aspect-ratio: 4 / 5;
  max-width: 26rem;
}

@media (min-width: 768px) {
  .hero-layout {
    grid-template-columns: 1fr 1.1fr;
    align-items: center;
    gap: 3rem;
    margin-top: 0;
  }

  .hero-photo {
    max-width: none;
  }
}

/* ---- Photo with a photographer credit (about hero, luna-06) ---- */

.photo-figure {
  margin: 0;
}

.press-photo {
  max-width: 24rem;
  margin-top: 1rem;
}

.photo-credit {
  display: block;
  margin-top: 0.5rem;
  /* Normal case, not .mono-label's uppercase: uppercasing turns the "ß"
     in "Saskia Meiß" into "SS", and a credit should spell the name
     exactly. */
  text-transform: none;
}

/* ---- About page — "Beyond the trombone" banner (luna-01) ---- */

.beyond-banner {
  width: 100%;
  height: clamp(220px, 32vw, 420px);
  margin-top: 2rem;
}

.beyond-banner img {
  /* Biased hard toward the top of the frame: at desktop widths the
     container's aspect ratio is far wider than the source photo's 16:9, so
     `object-fit: cover` crops aggressively — this keeps Luna's raised
     hand/fingers in frame (mobile at 390px is unaffected, see storyboard §3). */
  object-position: center 10%;
}

/* ---- Press page — press-kit photo card (luna-02) ---- */

.press-photo-card {
  display: grid;
  gap: 1.25rem;
  margin-top: 1.5rem;
}

.press-photo-card__img {
  width: 100%;
  max-width: 200px;
  aspect-ratio: 3 / 4;
  align-self: start;
}

.press-photo-card__body {
  display: flex;
  align-items: center;
}

@media (min-width: 640px) {
  .press-photo-card {
    grid-template-columns: 200px 1fr;
  }
}

/* ---- Contact page — small circular avatar (luna-02), no gradient rule ---- */

.contact-panel__avatar {
  display: block;
  width: 112px;
  height: 112px;
  border-radius: 50%;
  object-fit: cover;
  margin-bottom: 1.5rem;
}

/* ============ Legal pages ============ */

.legal-content {
  margin-top: 2rem;
  font-size: 0.9375rem;
  line-height: 1.7;
}

.legal-content strong {
  font-weight: 700;
}

/* The privacy policy prints long bare URLs as link text; let them wrap
   instead of pushing the page sideways on phones. */
.legal-content a {
  overflow-wrap: anywhere;
}

/* "Datenschutzerklärung" is wider than a 375px phone at h1 size. */
.legal-content h1 {
  hyphens: auto;
  overflow-wrap: break-word;
  margin-bottom: 1rem;
}

.legal-content h2 {
  margin-top: 2.5rem;
  margin-bottom: 0.75rem;
}

.legal-content h3 {
  margin-top: 1.5rem;
  margin-bottom: 0.5rem;
}

.legal-content .prose-list {
  margin-bottom: 1em;
}

.legal-content .seal {
  margin-top: 2rem;
}

.legal-content .seal img {
  max-width: 100%;
  height: auto;
}

/* Table-of-contents targets land below the sticky header. */
.legal-content [id] {
  scroll-margin-top: 5rem;
}

/* ============ Language gate (root index.html) ============ */

.lang-gate {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--night);
  color: var(--night-ink);
  padding: var(--gutter);
  position: relative;
  overflow: hidden;
}

.lang-gate__inner {
  position: relative;
  z-index: 1;
  text-align: center;
  max-width: 32rem;
}

.lang-gate h1 {
  color: var(--night-ink);
}

.lang-gate__tagline {
  margin-top: 1rem;
  font-size: var(--size-lede);
  opacity: 0.9;
}

.lang-gate__choices {
  display: flex;
  justify-content: center;
  gap: 1.25rem;
  margin-top: 2.5rem;
  flex-wrap: wrap;
}

.lang-gate__choice {
  min-width: 140px;
  padding: 1rem 1.75rem;
  border-radius: 999px;
  border: 2px solid var(--brass-tint);
  color: var(--night-ink);
  text-decoration: none;
  font-family: var(--font-mono);
  letter-spacing: 0.03em;
  font-weight: 700;
}

.lang-gate__choice:hover {
  background: var(--rose-tint);
  color: var(--night);
  border-color: var(--rose-tint);
}

.lang-gate__mark {
  width: 70px;
  height: 84px;
  margin-inline: auto;
}

/* ============ Responsive base ============ */

/*
  Breakpoint convention (issue #33) — read before adding a new @media rule.

  This site uses exactly two breakpoints: 768px (tablet) and 1024px
  (desktop), both as `min-width`. Base (unprefixed) CSS is the phone layout
  — mobile-first throughout. Do not introduce a third breakpoint value
  without updating this comment and getting design sign-off; reuse 768/1024,
  or better, avoid needing a breakpoint at all (see rule of thumb below).

  Rule of thumb for any new component, in this order:
    1. Fluid sizing first — `clamp()` for type/spacing (house pattern via
       `--size-h1`, `--section-padding`, `--gutter`).
    2. Intrinsic/auto-reflowing layout second — `grid-template-columns:
       repeat(auto-fit, minmax(...))` or `flex-wrap: wrap`, so the component
       reflows itself at whatever width without a hand-picked breakpoint
       (house pattern via `.card-grid`, `.fact-strip__list`, `.chip-row`).
    3. Only reach for `min-width: 768px` / `min-width: 1024px` if 1 and 2
       genuinely can't produce the right layout (a fundamentally different
       arrangement is needed, e.g. the nav disclosure→bar swap, the contact
       page's form+panel split, or the form-field pairing below) — the two
       standing values above, never a new one.

  Touch targets: 44×44px is this site's house minimum for anything tappable
  (nav links, buttons, form inputs/selects, the consent checkbox's tap
  area) — deliberately above the WCAG 2.2 SC 2.5.8 floor of 24×24px. Don't
  let a future component regress to the legal minimum when this is the
  established standard.
*/

@media (min-width: 768px) {
  .contact-layout {
    gap: 3rem;
  }
}

/* .hero__grid's old collision-avoidance formulas (issue #29) are gone:
   the hero's photo is no longer a right-anchored absolutely-positioned
   layer, so there is nothing for a text column to collide with any more.
   See docs/tasks/issue-42-hero-rebuild/04-concept-reviewed.md §5 for the
   two-zone CSS Grid architecture that replaced it, and the `.hero__layout`
   rules under "Hero" above. */

/* ============ Theme toggle ("Night mode" switch, issue #140; reverted
   to this two-state control by #166, replacing the #147 three-way
   chooser) ============

   A <button role="switch" aria-checked> with the accessible name "Night
   mode" / "Nachtmodus" (same string, visible label + accessible name — see
   docs/tasks/issue-140-dark-blue-color-schema/06-content-reviewed.md §C.1).
   State is carried by three non-colour cues at once (SC 1.4.1): knob
   position, glyph shape (sun ↔ crescent), and aria-checked. Two DOM copies
   exist per page, mirroring .nav-list / .nav-list--bar — see
   docs/tasks/issue-166-visual-identity-and-rockytrio-clarity/
   05-storyboard-reviewed.md §A.1. assets/js/site.js (initThemeToggle)
   keeps both in sync.

   Carried forward from #147, not reverted: the html.js gate below, so a
   JS-disabled visitor never sees a dead control.
*/

.theme-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 48px;
  height: 44px;
  flex-shrink: 0;
  cursor: pointer;
  color: var(--ink);
  border-radius: 0.5rem;
}

html.js .theme-toggle {
  display: inline-flex;
}

.theme-toggle__track {
  position: relative;
  display: block;
  flex-shrink: 0;
  width: 48px;
  height: 26px;
  border-radius: 999px;
  border: 1.5px solid var(--ink-rule);
}

.theme-toggle__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--ink);
  transition: transform var(--motion-fast) var(--ease-out);
}

.theme-toggle[aria-checked="true"] .theme-toggle__knob {
  transform: translateX(21px);
}

@media (prefers-reduced-motion: reduce) {
  /* SC 2.3.3: the slide is dropped, the state change stays instant. */
  .theme-toggle__knob {
    transition: none;
  }
}

.theme-toggle__glyph {
  position: absolute;
  width: 14px;
  height: 14px;
}

.theme-toggle__glyph--sun circle {
  fill: var(--paper);
}

.theme-toggle__glyph--sun line {
  stroke: var(--paper);
  stroke-width: 2;
  stroke-linecap: round;
}

.theme-toggle__glyph--moon {
  display: none;
}

.theme-toggle__glyph--moon path {
  fill: var(--paper);
}

.theme-toggle[aria-checked="true"] .theme-toggle__glyph--sun {
  display: none;
}

.theme-toggle[aria-checked="true"] .theme-toggle__glyph--moon {
  display: block;
}

/* Desktop/tablet copy: last item in the header utility cluster, after the
   language switch. Hidden below 768px — the mobile disclosure panel row
   (.nav-utility below) carries the control there instead, per Ed's
   measured header-width constraint (05-storyboard-reviewed.md §7.1). */
.theme-toggle--bar,
html.js .theme-toggle--bar {
  display: none;
}

@media (min-width: 768px) {
  html.js .theme-toggle--bar {
    display: inline-flex;
  }
}

/* Mobile copy: final row of the nav disclosure panel, after "Contact".
   role="none" on the <li> keeps the primary <nav> announcing exactly six
   items — the switch is furniture, not a navigation destination. */
.nav-utility {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 44px;
  margin-top: 0.25rem;
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--hairline);
}

.nav-utility__label {
  font-weight: 500;
}

/* ============ Moonlight-only overrides (issue #140) ============

   The only place in the stylesheet where a [data-theme] prefix is allowed
   (04-concept-reviewed.md §6.1) — everything else is covered by the token
   re-point in tokens.css. Two categories:
   1. Inline SVG marks carry stop-color/fill as presentation attributes,
      which these rules outrank, so no HTML file's SVG markup is edited.
   2. A handful of moonlight-only adjustments called out per-template in
      05-storyboard-reviewed.md Part B.
*/

[data-theme="moonlight"] .wordmark__mark stop:first-child,
[data-theme="moonlight"] .lang-gate__mark stop:first-child {
  stop-color: var(--brass-tint);
}

[data-theme="moonlight"] .wordmark__mark stop:last-child,
[data-theme="moonlight"] .lang-gate__mark stop:last-child {
  stop-color: var(--rose-tint);
}

/* Sticky mobile CTA (--night) gains a hairline top border in moonlight so
   it doesn't float against the dark page (05-storyboard-reviewed.md §B.6). */
[data-theme="moonlight"] .sticky-cta {
  border-top: 1px solid var(--hairline);
}

/* About/Press hero and press photos are high-key (bright sky, white
   graffiti) and jump hard off the dark field. No filter, no tint, no
   reduced opacity — just a deliberate edge instead of a raw cut
   (05-storyboard-reviewed.md §B.2 note 2). */
[data-theme="moonlight"] .photo-frame {
  box-shadow: 0 0 0 1px var(--hairline-strong);
}

@media (min-width: 1024px) {
  .card-grid--asym {
    grid-template-columns: repeat(3, 1fr);
  }
}
