/*
  Design tokens + base styles for Michael Song, Cello.
  This file is linked directly (not bundled) from src/layouts/Layout.astro
  AND from public/admin/index.html, so the Decap CMS preview pane renders
  with the same typography and palette as the live site. Keep it framework-free.
*/

:root {
  /* Palette: warm off-white paper, near-black ink, one muted bronze accent. */
  --color-paper: #faf9f7;
  --color-paper-dim: #f2f0eb;
  --color-ink: #1a1a1a;
  --color-ink-soft: #4a4640;
  --color-line: #ddd6c9;
  --color-accent: #8a7458;
  --color-accent-dark: #6e5b46;

  /* ---- DISPLAY FACE: ONE-LINE TRIAL SWITCH ----------------------------------
     Newsreader is on trial as the display face. To revert, comment the first
     line and uncomment the second — nothing else changes anywhere in the site,
     and both families stay installed and imported in Layout.astro, so flipping
     back and forth is free.

     Why it is on trial: Cormorant is very high contrast with hairline thins and
     a small x-height — superb at 60px, fragile below ~20px, which is the same
     fault that made the About biography read thin. It is also one of the most
     template-worn serifs on the web. Newsreader is drawn for screen reading, so
     it holds up at text sizes, and it carries oldstyle figures, which make dates
     and times read as text rather than as data. */
  --font-serif: "Cormorant Garamond", "Georgia", serif;
  /* Body/nav text: a quieter Garamond than the display serif above, closer
     to the Adobe Garamond voice of the original Squarespace site. Inter is
     kept for small-caps labels, buttons, date chips, and form UI. */
  --font-body: "EB Garamond", "Georgia", serif;
  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --content-max: 720px;
  --content-max-wide: 1200px;

  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2.5rem;
  --space-5: 4rem;
  --space-6: 6rem;
  --space-7: 9rem;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --header-height: 88px;

  /* One radius for every piece of media on the site. Photographs were a mix of
     hard square edges (homepage intro, About portrait), a 3px radius (photo
     grid, schedule thumbs), and a 1px hairline with square corners (press
     photos) — three treatments with no rule behind them, which is why they read
     as unfinished rather than austere. The rule is now: PHOTOGRAPHS get the
     radius and no keyline; VIDEO frames get the radius AND keep the hairline,
     because a near-black rectangle genuinely needs an edge against warm paper.
     Square corners remain the default elsewhere (§2) — this is scoped to media. */
  --radius-media: 3px;
}

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

html {
  scroll-behavior: smooth;
}

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

body {
  margin: 0;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1.0625rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-serif);
  font-weight: 500;
  line-height: 1.2;
  margin: 0 0 var(--space-2);
  color: var(--color-ink);
}

h1 {
  font-size: clamp(2.5rem, 5vw, 4rem);
}

h2 {
  font-size: clamp(1.75rem, 3.2vw, 2.5rem);
}

h3 {
  font-size: clamp(1.35rem, 2.2vw, 1.75rem);
}

p {
  margin: 0 0 var(--space-2);
}

em,
i {
  font-style: italic;
}

.container {
  max-width: var(--content-max-wide);
  margin: 0 auto;
  padding: 0 var(--space-3);
}

.prose {
  max-width: var(--content-max);
  margin: 0 auto;
}

.prose p {
  /* Body face, not the display face: Cormorant's hairlines break up at
     paragraph size (the About biography had the same fault, Brief 4). This is
     also what lets `.label` keep its own face inside a prose container; at
     (0,1,1) `.prose p` out-specified it and set the Contact form note as a
     large bronze headline. */
  font-family: var(--font-body);
  font-size: 1.15rem;
  line-height: 1.7;
}

.prose p.label {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  line-height: 1.5;
}

/* Small-caps section / nav / metadata labels — bronze, so the accent recurs in
   the metadata layer rather than only on hover (uses the darker bronze for AA
   contrast at this small size). */
.label {
  font-family: var(--font-sans);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--color-accent-dark);
}

.section {
  padding: var(--space-6) 0;
}

.section-title {
  text-align: center;
  margin-bottom: var(--space-4);
}

/* A short bronze rule under each section heading gives the page a quiet gold
   rhythm without coloring the headings themselves. */
.section-title::after {
  content: '';
  display: block;
  width: 40px;
  height: 2px;
  margin: var(--space-2) auto 0;
  background: var(--color-accent);
}

/* Understated button: small-caps + hairline border */
.btn {
  display: inline-block;
  padding: 0.85em 1.8em;
  border: 1px solid var(--color-ink);
  font-family: var(--font-sans);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink);
  background: transparent;
  cursor: pointer;
  transition: background 200ms var(--ease-out), color 200ms var(--ease-out);
}

.btn:hover,
.btn:focus-visible {
  /* Fill with bronze rather than ink on hover, so the primary calls-to-action
     carry the accent. Darker bronze keeps the paper label at AA contrast. */
  background: var(--color-accent-dark);
  border-color: var(--color-accent-dark);
  color: var(--color-paper);
}

.link-underline {
  border-bottom: 1px solid var(--color-line);
  transition: border-color 200ms var(--ease-out), color 200ms var(--ease-out);
}

.link-underline:hover,
.link-underline:focus-visible {
  color: var(--color-accent-dark);
  border-color: var(--color-accent);
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}

/* Hidden until keyboard-focused, then floats above the fixed header. */
.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 200;
  padding: 0.6em 1em;
  background: var(--color-ink);
  color: var(--color-paper);
  font-family: var(--font-sans);
  font-size: 0.85rem;
  transform: translateY(-250%);
  transition: transform 150ms var(--ease-out);
}

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

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

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: var(--header-height);
  display: flex;
  align-items: center;
  /* Driven from 0→1 by scroll position in initHeaderScroll(), rather than
     snapped by an .is-scrolled class at an 80px threshold. The old binary
     toggle is what made the bar appear "suddenly", and no transition duration
     fixes a step change in the trigger itself. */
  --header-reveal: 0;
  /* Tone and blur both live on ::before below, not here — see the note there
     for why a gradient background alone could not remove the hard edge. */
  background: transparent;
  border-bottom: none;
  /* Light text in BOTH states now — the bar is bronze when revealed and a hero
     photo when not, so there is no longer a light-background state to switch
     for. The shadow stays for the transparent-over-photo case. */
  color: var(--color-paper);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.45), 0 2px 14px rgba(0, 0, 0, 0.3);
}

/* The bar's tone AND its blur, on one masked layer.
   Why not a gradient background plus backdrop-filter on the header itself (the
   previous attempt): `backdrop-filter` cannot be graduated. It applies at full
   strength across the whole element and then stops dead at the element's edge,
   so however softly the background colour faded, the BLUR still ended in a hard
   horizontal line at 88px — which is the visible split, and why the blur looked
   uniform top to bottom while the tint did not.
   A mask solves it properly: masking this layer fades the blur and the tint
   together, by exactly the same ramp, because they are the same layer. */
.site-header::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: rgb(110, 91, 70);
  backdrop-filter: blur(14px) saturate(1.05);
  -webkit-backdrop-filter: blur(14px) saturate(1.05);
  /* Solid behind the text, then away to nothing well before the lower edge. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 1) 0%,
    rgba(0, 0, 0, 1) 44%,
    rgba(0, 0, 0, 0.72) 66%,
    rgba(0, 0, 0, 0.28) 85%,
    rgba(0, 0, 0, 0) 100%
  );
  mask-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 1) 0%,
    rgba(0, 0, 0, 1) 44%,
    rgba(0, 0, 0, 0.72) 66%,
    rgba(0, 0, 0, 0.28) 85%,
    rgba(0, 0, 0, 0) 100%
  );
  /* One control for the scroll ramp, so tone and blur can never drift apart. */
  opacity: calc(0.92 * max(var(--header-reveal), var(--header-floor, 0)));
}

.site-header .brand em {
  color: #e8dcc9;
}

.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  /* Stretch the whole chain (container → nav → dropdown) to the header's full
     88px so the dropdown panel's `top: 100%` resolves to the BAR's lower edge.
     Without this each link is only ~24px tall and the panel opened 23px up
     inside the header, straddling its bottom edge. Children stay vertically
     centred via align-items above. */
  align-self: stretch;
}

.site-header .brand {
  font-family: var(--font-serif);
  font-size: 1.35rem;
  letter-spacing: 0.02em;
}

.site-header .brand em {
  /* A touch lighter than --color-accent-dark so it still reads against a
     dark hero photo; the scrolled state below swaps it back to the darker,
     higher-contrast tone for the off-white background. */
  color: #d9c9b4;
}

.site-nav {
  display: none;
  align-items: center;
  align-self: stretch;
  gap: var(--space-4);
}

.site-nav a {
  position: relative;
  font-family: var(--font-body);
  font-size: 1rem;
  letter-spacing: 0.01em;
  padding-bottom: 4px;
  transition: color 300ms var(--ease-out);
}

/* Underline grows from the left on hover/focus — smoother and more editorial
   than the old 200ms border-color fade. Scoped to the top-level items; the
   dropdown-panel links (also .site-nav a) get their own treatment below. */
.site-nav > a::after,
.nav-dropdown > a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms var(--ease-out);
}

.site-nav > a:hover,
.site-nav > a:focus-visible,
.nav-dropdown > a:hover,
.nav-dropdown > a:focus-visible {
  /* Paper-bright in both states: the bar is bronze once revealed, so the old
     bronze hover had nothing to contrast against. */
  color: #fffdfa;
}

/* 90ms grow delay so a cursor sweeping across the nav on its way somewhere else
   doesn't start each underline — that half-started-then-restarted animation was
   the double-trigger stutter. Retract stays instant (no delay on the base rule
   above), so leaving still feels immediate. */
.site-nav > a:hover::after,
.site-nav > a:focus-visible::after,
.nav-dropdown > a:hover::after,
.nav-dropdown > a:focus-visible::after {
  transform: scaleX(1);
  transition-delay: 60ms;
}

.nav-dropdown {
  position: relative;
  /* Stretch to the full header height so the panel's `top: 100%` resolves to
     the BAR's lower edge rather than the text baseline — it was starting 29px
     up inside the header and straddling the bar's own bottom edge. */
  align-self: stretch;
  /* Without this, the nested "Media" link stays display:inline (only direct
     flex children get blockified), so its padding/border don't expand its
     box the way the sibling links' do, and align-items:center on .site-nav
     ends up centering it a few pixels off from the others. Making this
     wrapper a flex container too blockifies the link the same way. */
  display: flex;
  align-items: center;
}

/* One cohesive hairline panel that fades + slides down on expand, rather than a
   stack of separate bordered boxes. display can't be transitioned, so the panel
   stays in flow and toggles opacity/visibility/transform instead. */
.nav-dropdown-panel {
  position: absolute;
  /* Flush to the bar's lower edge. The 6px gap made it a detached object
     hovering below the header — the eye reads the BAR edge as what the panel
     hangs from, not the word's baseline, so any gap there reads as a void
     rather than as breathing room. Hanging flush is what makes it read as an
     extension of the nav item rather than a tooltip. */
  top: 100%;
  /* Centred on the trigger. Left-aligning a panel that is wider than the word
     it belongs to made it jut out to one side only — that was the imbalance. */
  left: 50%;
  margin-left: 0;
  display: flex;
  flex-direction: column;
  /* Was as narrow as its content (73px), which made a two-row panel taller than
     it was wide — an awkward vertical slab. A wider minimum gives it a
     horizontal footing under the word. */
  min-width: 148px;
  /* Deeper than paper-dim: paper-dim (#f2f0eb) still read as white against a
     dark hero. Blending the hairline tone into it lands ~#e9e4d8 — clearly a
     warm panel, not a white box — without adding a new palette entry. */
  background: var(--color-paper-dim);
  background: color-mix(in srgb, var(--color-line) 45%, var(--color-paper-dim));
  /* Bronze-tinted hairline (falls back to the plain warm-gray line where
     color-mix isn't supported) plus a 2px bronze top edge, so the panel reads
     as belonging to the hovered nav item rather than as a default box. */
  /* Uniform 1px on all four sides: a 2px top against 1px sides cannot turn the
     corner radius cleanly, which made the top corners look pinched. The bronze
     now reads through the whole outline instead of only the top edge. */
  border: 1px solid var(--color-line);
  border-color: color-mix(in srgb, var(--color-accent) 55%, var(--color-line));
  /* Square corners are the default, not a mandate (CLAUDE.md §2). 2px was
     invisible at this size; 5px is legible as a softened corner while staying
     far from a rounded card. */
  border-radius: 5px;
  /* Two items don't need a room around them: 16px block padding plus 8px per
     row left the panel mostly empty. 4px here and tighter rows below make it
     sit close to its content. */
  padding: 6px 0;
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(-8px);
  /* Closing: quick and delay-free, so dismissing feels immediate. */
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out),
    visibility 0s linear 200ms;
}

.nav-dropdown:hover .nav-dropdown-panel,
.nav-dropdown:focus-within .nav-dropdown-panel {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  /* Hover intent, final: 100ms fired on a sweep, 260/180ms felt like waiting.
     120ms is short enough to feel immediate on a deliberate hover while still
     sitting above the dwell time of a pass-through, and the underline's own
     90ms delay above now absorbs most of the accidental triggers anyway. */
  transition: opacity 250ms var(--ease-out) 90ms,
    transform 250ms var(--ease-out) 90ms, visibility 0s;
}

.nav-dropdown-panel a {
  padding: 7px var(--space-3);
  /* Always on the solid paper panel regardless of header scroll state,
     so this always needs the dark, no-shadow treatment. */
  color: var(--color-ink);
  text-shadow: none;
  white-space: nowrap;
  transition: color 200ms var(--ease-out), background 200ms var(--ease-out);
}

/* The panel is now the dim tone, so the row highlight is a soft bronze wash
   rather than the dim surface it used to use. */
.nav-dropdown-panel a:hover,
.nav-dropdown-panel a:focus-visible {
  color: var(--color-accent-dark);
  background: var(--color-paper);
  background: color-mix(in srgb, var(--color-accent) 12%, var(--color-paper));
}

/* Visible at every width, not just ≥900px. The Instagram link and the language
   toggle were buried in the hamburger drawer, where a visitor who never finds
   the hamburger never finds them — and the language toggle in particular is
   something a French-speaking visitor needs to see immediately, not hunt for.
   Both are small enough to sit in the bar even at 375px. */
.header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

@media (min-width: 900px) {
  .header-actions {
    gap: var(--space-3);
  }
}

.header-instagram {
  display: inline-flex;
  /* text-shadow only affects text, not the SVG icon's stroke/fill, so it
     needs its own drop-shadow for the same over-hero legibility. */
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
  transition: filter 300ms var(--ease-out);
}

.lang-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-family: var(--font-sans);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  font-weight: 500;
  padding-bottom: 4px;
  border-bottom: 1px solid transparent;
  transition: border-color 200ms var(--ease-out), color 200ms var(--ease-out);
}

.lang-toggle:hover,
.lang-toggle:focus-visible {
  /* Paper, like the nav links: the hover colour used to be the darker
     bronze, which is exactly the tone of the revealed bar (1.00:1). */
  color: #fffdfa;
  border-bottom-color: currentColor;
}

/* The global bronze focus ring is invisible on the bronze bar (1.45:1). */
.site-header :focus-visible {
  outline-color: var(--color-paper);
}

.hamburger {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  /* 44px touch target (was 32) — the bars stay 26px wide via the span rule, so
     this only grows the hit area, not the mark. */
  width: 44px;
  height: 44px;
  align-items: flex-end;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  /* Buttons don't inherit `color` from ancestors by default (UA stylesheets
     set their own), so without this the bars would stay dark even when the
     header switches to light text over the hero. */
  color: inherit;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
  transition: filter 300ms var(--ease-out);
}

/* Was height:1px — three hairlines over a photographic hero, which is the site's
   hairline language applied where it doesn't belong: this is the only way to
   reach navigation below 900px, so it has to be findable before it is elegant.
   2px still reads as a fine rule, not a chunky app icon. */
.hamburger span {
  display: block;
  width: 26px;
  height: 2px;
  background: currentColor;
  transition: transform 200ms var(--ease-out), opacity 200ms var(--ease-out);
}

.mobile-drawer {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: var(--color-paper);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  transform: translateY(-100%);
  /* Closed, it is also hidden from keyboard and screen readers (six links
     used to sit in the tab order off screen). visibility waits for the slide
     so the close animation still plays; the script sets `inert` as well. */
  visibility: hidden;
  transition: transform 350ms var(--ease-out), visibility 0s linear 350ms;
}

.mobile-drawer.is-open {
  transform: translateY(0);
  visibility: visible;
  transition: transform 350ms var(--ease-out), visibility 0s;
}

.mobile-drawer a {
  font-family: var(--font-serif);
  font-size: 1.9rem;
}

.mobile-drawer .label {
  margin-top: var(--space-3);
}

/* Full nav from 768 (was 900), so iPad-portrait and up get real navigation
   instead of a hamburger. It fits at 768 with a tighter gap and a slightly
   smaller nav size; the 900px step restores the roomier desktop spacing. */
@media (min-width: 768px) {
  .site-nav {
    display: flex;
    gap: var(--space-3);
  }
  .site-nav a {
    font-size: 0.94rem;
  }
  .hamburger,
  .mobile-drawer {
    display: none;
  }
}

@media (min-width: 900px) {
  .site-nav {
    gap: var(--space-4);
  }
  .site-nav a {
    font-size: 1rem;
  }
}

/* ---------- Hero ---------- */

.hero {
  position: relative;
  min-height: 62vh;
  display: flex;
  align-items: flex-end;
  color: var(--color-paper);
  background-size: cover;
  background-position: center;
  overflow: hidden;
}

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.55) 100%);
}

.hero img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero-content {
  position: relative;
  z-index: 1;
  padding: var(--space-5) var(--space-3);
  width: 100%;
}

.hero-content h1 {
  color: var(--color-paper);
}

/* Interior-page banner (§14, Option A). Taller than before so a 3:2 or 16:9
   source shows more of its frame instead of a compressed strip, capped so it
   never dominates on tall viewports. */
.hero.hero-short {
  /* Reduced ~1/3 from min(58vh, 600px): the banner is decorative and was
     claiming too much of the first screen on every interior page. */
  min-height: min(39vh, 400px);
}

/* A clear vertical interval between the banner and the first content section,
   identical across every interior page, so no page lurches straight from image
   into text. */
.hero-short + .section {
  /* 96px, reduced from the 144px "Option A" interval (§4). With the banner
     shortened the old gap read as a dead chunk between the banner and the
     content. Approved deviation from that recorded decision; applies to every
     interior page so they stay consistent with each other. */
  padding-top: var(--space-6);
}

/* Full-viewport homepage hero. The quote sits in the lower third, over the
   clean stage floor rather than the busy mid-frame (music stands, chairs). */
.hero.hero-full {
  min-height: 100svh;
  align-items: flex-end;
}

.hero.hero-full .hero-content {
  padding-bottom: 22vh;
}

/* Subtle bottom-up scrim (transparent → rgba(0,0,0,0.35) over the lower 40%)
   to separate the quote from the floor without a text shadow. Layered over
   the shared hero overlay. */
.hero.hero-full::after {
  background:
    linear-gradient(to top, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0) 40%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.5) 100%);
}

/* Quote pinned to the site's left axis (§3, Michael's choice). To re-center it
   someday, revert these three rules to `text-align: center` and restore
   `margin: 0 auto` on the pull-quote (the hero-content padding keeps it in the
   lower third either way). */
.hero-quote {
  text-align: left;
}

.hero-quote .pull-quote {
  text-align: left;
  margin-left: 0;
  margin-right: auto;
  color: var(--color-paper);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}

.hero-quote .pull-quote-attribution {
  text-align: left;
  color: var(--color-paper);
  opacity: 0.9;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}

/* Bronze rule beneath the quote, on the same left axis. Base state is the fully
   revealed rule so reduced-motion (and no-JS) visitors get it statically. */
.hero-quote-rule {
  display: block;
  width: clamp(72px, 12vw, 160px);
  height: 2px;
  margin: 0 0 var(--space-2);
  background: var(--color-accent);
  transform-origin: left center;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

/* Entrance (motion allowed only): the quote wipes in left-to-right, the bronze
   rule grows in beneath it on the same axis, then the attribution fades up —
   staggered. animation-fill-mode: both holds each element's start (hidden) state
   through its delay so nothing flashes before its turn. */
@media (prefers-reduced-motion: no-preference) {
  .hero-quote .pull-quote {
    animation: hero-quote-wipe 800ms var(--ease-out) 150ms both;
  }
  .hero-quote-rule {
    animation: hero-rule-grow 650ms var(--ease-out) 700ms both;
  }
  .hero-quote .pull-quote-attribution {
    animation: hero-attr-in 700ms var(--ease-out) 1150ms both;
  }
}

@keyframes hero-quote-wipe {
  from { clip-path: inset(0 100% 0 0); }
  to { clip-path: inset(0 0 0 0); }
}

@keyframes hero-rule-grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

@keyframes hero-attr-in {
  from { opacity: 0; transform: translateX(-10px); }
  to { opacity: 0.9; transform: translateX(0); }
}

/* ---------- Homepage intro (asymmetric, centered gutter) ---------- */

/* Breathing room after the full-height hero. */
.home-intro {
  padding: var(--space-6) 0;
}

/* Mobile: single column, page-edge padding on both blocks. */
.home-intro-body {
  padding: 0 var(--space-3);
}

.home-intro-body p {
  font-family: var(--font-serif);
  font-size: 1.5rem;
  line-height: 1.55;
  margin: 0 0 var(--space-3);
}

.home-intro-media {
  padding: 0 var(--space-3);
  margin-top: var(--space-4);
}

.home-intro-media img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-media);
}

/* Desktop: the two blocks split at the page's vertical centerline, so the empty
   channel between them sits dead-center. The text is pinned to the site's left
   axis (the same content edge .container uses) and ends a half-gutter left of
   center; the landscape photo starts a half-gutter right of center and runs out
   to a page-margin inset on the right. Both inner edges are therefore
   equidistant from the centerline, while the photo stays the larger block —
   widths are deliberately unequal, only the gutter is symmetric. */
@media (min-width: 900px) {
  .home-intro-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: center;
  }
  /* The text is pinned to the site's left axis — but that axis keeps marching
     right as the viewport grows while the text's right edge stays at the
     centerline, so above ~1370px the block was frozen at ~550px while the
     photo grew without limit (1.66x at 1920px, worse beyond). Capping the
     indent at 8vw lets the text fill its half on wide screens; the photo still
     bleeds to the page margin and stays the larger block, at a steady ~1.17x
     rather than running away. Below ~1370px the axis still wins, so the
     moderate/laptop widths are unchanged. */
  .home-intro-body {
    padding-left: clamp(
      var(--space-3),
      calc(50vw - var(--content-max-wide) / 2 + var(--space-3)),
      8vw
    );
    padding-right: calc(var(--space-4) / 2);
  }
  .home-intro-media {
    margin-top: 0;
    padding-left: calc(var(--space-4) / 2);
    padding-right: var(--space-3);
  }
}

@media (max-width: 700px) {
  .home-intro {
    padding: var(--space-5) 0;
  }
}

/* ---------- Homepage video feature ---------- */

/* The clip fills the page measure. It was `min(70vw, 960px)`, which had two
   faults that both showed up as "the video shrinks on a wide window":
     - 70vw is narrower than the container at every width past ~900px, so the
       clip sat inside its own margins with paper either side of it while the
       rest of the page ran to the container edge;
     - a `max-width: 899px` rule set it back to 100%, so crossing 900px made it
       jump from ~836px wide down to 630px — the page got wider and the video
       got smaller.
   One rule, full container width, no cliff. */
.home-video-frame {
  width: 100%;
  margin: 0 auto;
}

/* Past the container width the section was mostly paper: at 1440 a 1102px clip
   sat inside 1200px of container inside a 1440px window, so ~340px of the first
   screen was empty margin and the eye read the brightness before the picture.
   Watch alone runs wider than --content-max-wide, which the design language
   already allows for media ("may bleed past the content margin for balance").
   The clip goes 1102 -> 1256px at 1440 and 1102 -> 1460 at 1920. Only the video
   frame widens — the caption and button stay on the page measure, so the
   section still resolves back to the same axis as everything under it. */
@media (min-width: 1300px) {
  .home-video .home-video-frame {
    /* Wider than its own container, so it needs the negative offset to stay
       centred on the page rather than on the container's content box. */
    width: clamp(1200px, 94vw, 1560px);
    margin-left: calc(50% - clamp(1200px, 94vw, 1560px) / 2);
  }
}

.home-video-more {
  margin-top: var(--space-3);
  text-align: center;
}

/* ---------- Homepage recent engagements (single-column list) ---------- */

/* A calendar-like column centered under its section title: each entry separated
   by a hairline rule so six dates read as a rhythmic list, not a grid. */
/* Full container width (was a 720px centered island, which left the sides empty
   and read as a template default); the rows themselves now carry the width. */
.home-engagements {
  border-top: 1px solid var(--color-line);
}

.home-engagements .event-compact {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-line);
}

.home-engagements-more {
  margin-top: var(--space-4);
  text-align: center;
}

/* ---------- Homepage section rhythm (§ whitespace) ---------- */

/* On wide screens the centered sections (Watch, engagements) otherwise float in
   a field of paper. Alternating full-bleed tonal bands (the paper-dim surface
   the palette reserves for exactly this) plus hairline dividers give the page
   structure and make the wide-viewport reflow read calmer — without any card
   chrome or shadow. */
/* Sections were previously differentiated by tone alone, which failed: every
   one used the same recipe (full-bleed band, centred title, centred content),
   so recolouring one just made a longer slab of the same thing. They are now
   differentiated STRUCTURALLY instead — see .home-video (dark) and
   .home-engagements-section (left axis) below. */
.home-instagram-section {
  background: var(--color-paper-dim);
}

/* Lever 1 (W1) — Watch is separated by FRAMING, not by weight. Two darker
   treatments were tried and both failed for the same reason: a tonal slab, hard
   edged or gradient, adds mass to a page whose whole argument is restraint, and
   the eye reads the seam rather than the video. Here nothing on the page gets
   darker at all — the section stays on paper and the video sits inside a bronze
   keyline with a generous inset, so the frame alone marks it as a screen. It is
   also the only option that adds no new tone to the palette. */
/* No fill. A `paper-dim` wash was tried inside the keyline to give the frame
   more presence and rejected — it reads as a panel behind the video, which is
   card chrome by another name. The presence problem is solved by size instead:
   the clip now fills the container, so the keyline has something substantial to
   sit around. */
/* Corner marks rather than a closed keyline. A continuous border was the same
   KIND of mark as the section rails — a 1px line near an edge — so the two read
   as nested boxes and neither declared itself the subject. Registration marks
   are a different kind of object: they frame without enclosing, and nothing else
   on the site looks like them, so there is nothing left to collide with.

   Two diagonal marks, not four. Four closes the rectangle again by implication;
   two state it and let the eye finish it, which is also the asymmetry §2 asks
   for. Set on ::before/::after so the mat stays a single element and the marks
   never enter the accessibility tree. */
.home-video-mat {
  position: relative;
  padding: var(--space-3);
}

.home-video-mat::before,
.home-video-mat::after {
  content: '';
  position: absolute;
  width: 28px;
  height: 28px;
  border: 0 solid var(--color-accent);
}

.home-video-mat::before {
  top: 0;
  left: 0;
  border-top-width: 2px;
  border-left-width: 2px;
}

.home-video-mat::after {
  bottom: 0;
  right: 0;
  border-bottom-width: 2px;
  border-right-width: 2px;
}

@media (max-width: 700px) {
  .home-video-mat {
    padding: var(--space-2);
  }

  /* The marks scale down with the inset — at 28px against a 16px pad they would
     overrun the video's corners rather than sit outside them. */
  .home-video-mat::before,
  .home-video-mat::after {
    width: 18px;
    height: 18px;
  }
}

/* Lever 2 — Recent engagements sits on the site's left axis while Watch stays
   centred. Structure, not colour, is what tells the two sections apart, and it
   puts §2's "balance, not symmetry" to work: a left-pinned title against a
   full-width programme list. */
.home-engagements-section .section-title {
  text-align: left;
}

.home-engagements-section .section-title::after {
  margin-left: 0;
  margin-right: auto;
}

.home-intro,
.home-video,
.home-engagements-section,
.home-instagram-section {
  border-top: 1px solid var(--color-line);
}

/* Wide screens only: the centered sections keep their content in the 1200px
   container, so past ~1400px the side margins read as plain emptiness. A
   hairline rule just outside each content edge frames the column and makes that
   margin deliberate. Purely decorative, so it's aria-free (::before/::after)
   and simply absent on laptops, tablets, and phones.

   NOT on .home-video. Watch already carries a frame of its own, and measured at
   1440 the section stacked three 1px lines on each side — rail at x=72.5, mat at
   137, video keyline at 162 — with the OUTER and INNER lines both --color-line
   and the bronze sandwiched between them. Three concentric rectangles differing
   only in tone: nothing in the group reads as the subject, and the odd one out
   was the middle, which is backwards. The rails stay where they earn their keep,
   on sections that have no frame of their own. */
@media (min-width: 1400px) {
  .home-engagements-section,
  .home-instagram-section {
    position: relative;
  }

  .home-engagements-section::before,
  .home-engagements-section::after,
  .home-instagram-section::before,
  .home-instagram-section::after {
    content: '';
    position: absolute;
    top: var(--space-4);
    bottom: var(--space-4);
    width: 1px;
    background: var(--color-line);
  }

  .home-engagements-section::before,
  .home-instagram-section::before {
    left: max(var(--space-3), calc(50% - var(--content-max-wide) / 2 - var(--space-4)));
  }

  .home-engagements-section::after,
  .home-instagram-section::after {
    right: max(var(--space-3), calc(50% - var(--content-max-wide) / 2 - var(--space-4)));
  }
}

/* ---------- Pull quote ---------- */

.pull-quote {
  text-align: center;
  font-family: var(--font-serif);
  font-size: clamp(1.75rem, 4vw, 2.75rem);
  font-style: italic;
  max-width: 800px;
  margin: 0 auto var(--space-2);
}

.pull-quote-attribution {
  text-align: center;
  font-family: var(--font-sans);
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
}

/* ---------- Reveal on scroll ---------- */

/* Hidden state is opt-in via .reveal-enabled on <html> (set synchronously by an
   is:inline head script in Layout.astro). No-JS visitors never get the class, so
   content is simply visible; see CLAUDE.md §5. */
.reveal-enabled [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 600ms var(--ease-out), transform 600ms var(--ease-out);
}

.reveal-enabled [data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* `translateY(0)` is still a transform, and any transform creates a stacking
   context — which isolates a descendant's mix-blend-mode from the page behind
   it. That silently defeated the sponsor logos' `multiply` (see §Affiliations):
   the blend was applied and computed correctly, it just had nothing but
   transparency to blend against, so the white JPG grounds stayed white.
   Ending on `none` drops the stacking context once the reveal is done; the
   transition still interpolates, because `none` animates as the identity.
   Scoped rather than applied to every [data-reveal] — the reveal system is
   load-bearing (CLAUDE.md §3) and only these elements need it. */
.reveal-enabled .about-sponsors .affiliation-item.is-visible {
  transform: none;
}

/* Reveal variants. The homepage intro arrives as a choreographed pair rather
   than one block: the text slides in along the same left-to-right axis as the
   hero quote above it, then the photo settles in just behind it. Declared here,
   before the reduced-motion override below, so that override still wins. */
.reveal-enabled [data-reveal='slide-left'] {
  transform: translateX(-28px);
  transition-duration: 700ms;
}

.reveal-enabled [data-reveal='slide-left'].is-visible {
  transform: translateX(0);
}

.reveal-enabled [data-reveal='settle'] {
  transform: scale(0.975);
  transition-duration: 800ms;
  transition-delay: 180ms;
}

.reveal-enabled [data-reveal='settle'].is-visible {
  transform: scale(1);
}

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

/* ---------- Date chip ---------- */

.date-chip {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 76px;
  padding: var(--space-1) 0;
  border: 1px solid var(--color-line);
  flex-shrink: 0;
}

.date-chip .month {
  font-family: var(--font-sans);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink-soft);
}

.date-chip .day {
  font-family: var(--font-serif);
  font-size: 2.1rem;
  line-height: 1.1;
}

.date-chip .year {
  font-family: var(--font-sans);
  font-size: 0.7rem;
  color: var(--color-ink-soft);
}

/* ---------- Event card ---------- */

/* ---------- Schedule listing ---------- */

/* The Schedule page ran in the 720px `prose` container, so on a 1440px screen
   the page whose entire job IS the event list occupied half the width — while
   the homepage's *summary* of the same events ran the full 1200. The listing now
   grows with the viewport and stops at 1280, which is where a row with a 340px
   thumbnail still reads as one object rather than as two things at opposite
   ends of a wide band. Below ~1390 the 92vw term governs, so narrow windows are
   unchanged in feel — the width only arrives when there is width to spend. */
.container--schedule {
  max-width: min(92vw, 1280px);
}

/* Dropping `prose` also dropped `.prose p` (1.2rem/1.7) off the programme
   lines. Restated here at the size `.event-description` always intended, so the
   result is deliberate rather than a side effect of the container change. */
.container--schedule .event-description p {
  font-size: 1.05rem;
  line-height: 1.55;
  margin-bottom: 0.25em;
}

/* The meta row is a flex line at every width so date and time are separate
   objects rather than one string joined by middots. (The venue used to live here
   too; it now shares the title's row — see `.event-head`.)

   margin-bottom carries a full extra line of air before the programme lines.
   0.6em of an 1.1rem line was ~10px against 26px description lines, so the date
   sat almost as close to the repertoire as the repertoire lines sat to each
   other, and the row read as one undifferentiated block of small text. */
.container--schedule .event-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4em var(--space-3);
  margin-bottom: 1.6em;
  /* Metadata moves off Inter onto the body face. Inter's role here is meant to
     be small-caps labels, buttons and chips; a date, a time and a venue name are
     content, and setting content in the UI sans is what made the row read as a
     web interface rather than as a printed programme. */
  font-family: var(--font-body);
}

/* Date and time are the same thing said twice, so they are set identically —
   same face, same size, same ink. They were separated by style (Inter, smaller,
   greyed) while sitting side by side, which read as a mismatch rather than as a
   hierarchy. Only SPACE separates them now: the middot is gone and the time
   carries an extra indent. Tabular figures so times align down the column. */
.container--schedule .event-meta-when,
.container--schedule .event-meta-time {
  /* Stepped back (was 1.1rem, full ink): the list is read by title, and with
     every date in the past the date is the least useful fact in the row
     (Michael, 2026-10-02). */
  font-size: 1rem;
  color: var(--color-ink-soft);
}

.container--schedule .event-meta-time {
  margin-left: var(--space-2);
  font-variant-numeric: tabular-nums;
}

/* Venue keeps the same face but steps back in size and ink, so the right edge is
   anchored without competing with the title. Sentence case, not letterspaced
   caps — caps at this length shouted and cost legibility. */
.container--schedule .event-venue {
  font-size: 1.05rem;
  color: var(--color-ink-soft);
}

.container--schedule .event-card h3 {
  font-size: 1.8rem;
}

/* Programme lines move off the DISPLAY serif onto the body face. Cormorant is
   specified for headings and pull quotes only; running text set in it reads thin
   and fragile, which is the same fault that was corrected on the About
   biography. This is a pre-existing bug, not a consequence of the new scale. */
.container--schedule .event-description {
  font-family: var(--font-body);
}

.container--schedule .view-event-link {
  font-size: 0.95rem;
}

/* Widening the container alone did not fill the space — it only moved the void
   from outside the list into each row. Measured at 1440: the text ended 754px
   into a 1232px row, leaving 39% of every row empty on the right, because the
   strings are simply short. Anchoring the venue to the right edge is what
   actually spans the row, and it is the pattern the homepage programme rows
   already use, so the two views of the same data converge instead of diverging.
   Below 1000px the row is too narrow to hold both ends, so `.event-head` goes
   back to normal block flow and the venue takes its own line under the title. */
@media (min-width: 1000px) {
  .container--schedule .event-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
  }
  .container--schedule .event-head .event-venue {
    margin-left: auto;
    text-align: right;
    /* Long venue strings must not squeeze the title into a narrow column. */
    flex-shrink: 0;
    max-width: 40%;
  }
}

/* "View event" sits at the bottom of the ROW, not at the end of the text. A
   one-line entry is ~110px of text against a 255px thumbnail, so the link used
   to stop level with the middle of the photograph and leave the lower half of
   the row visibly empty. `align-self: stretch` gives the body the row's full
   height (the card aligns items to flex-start, so it does not get it otherwise)
   and `margin-top: auto` pushes the link down to sit level with the bottom of
   the thumbnail. On a tall entry nothing moves — auto margin collapses to 0. */
.container--schedule .event-card .event-body {
  display: flex;
  flex-direction: column;
  align-self: stretch;
}

.container--schedule .event-card .view-event-link {
  /* Shrink to the text: stretched to the 856px row it drew a second hairline
     25px above every row's own rule. Cross-axis only; the bottom pin stays. */
  align-self: flex-start;
  margin-top: auto;
  /* margin-top:auto absorbs the gap the old 0.4em provided, so restate the
     minimum distance from the programme lines for entries tall enough that the
     auto margin resolves to zero. */
  padding-top: 0.6em;
}

/* A schedule's one real boundary is the turn of the year, and 15 events ran
   past it with nothing to mark it. Deliberately a divider and not a heading:
   the year is set at label scale in the margin and a hairline carries across,
   so it registers while scanning without competing with the event titles. */
.schedule-year {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-5) 0 var(--space-2);
  font-family: var(--font-sans);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
}

/* The first group sits directly under the banner interval, which already
   supplies the air — a second full margin there reads as a dropped section. */
.schedule-group:first-child .schedule-year {
  margin-top: 0;
}

.schedule-year::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--color-line);
}

.event-card {
  display: flex;
  gap: clamp(var(--space-3), 2.5vw, var(--space-4));
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-line);
  align-items: flex-start;
}

/* Grows with the row rather than sitting at a fixed 160px. At 160 the thumbnail
   was too small to read as a photograph and too big to ignore; the cap keeps it
   from taking so much of a wide row that the text becomes the accessory. */
.event-card .event-thumb {
  width: clamp(160px, 24vw, 340px);
  aspect-ratio: 4 / 3;
  overflow: hidden;
  flex-shrink: 0;
  border-radius: var(--radius-media);
}

.event-card .event-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 400ms var(--ease-out);
}

.event-card:hover .event-thumb img {
  transform: scale(1.02);
}

.event-card .event-body {
  flex: 1;
  min-width: 0;
}

.event-card h3 {
  margin-bottom: 0.3em;
}

.event-card h3 a {
  transition: color 200ms var(--ease-out);
}

.event-card h3 a:hover,
.event-card h3 a:focus-visible {
  color: var(--color-accent-dark);
}

.event-meta {
  font-family: var(--font-sans);
  font-size: 0.85rem;
  color: var(--color-ink-soft);
  margin-bottom: 0.6em;
}

.event-meta a {
  border-bottom: 1px solid var(--color-line);
}

.event-meta a:hover {
  color: var(--color-accent-dark);
  border-color: var(--color-accent);
}

.event-description {
  font-family: var(--font-serif);
  font-size: 1.05rem;
  color: var(--color-ink);
}

.event-description strong {
  font-weight: 600;
}

.event-description em {
  font-style: italic;
}

.event-card.is-past {
  opacity: 0.85;
}

.event-card.is-past .event-thumb img {
  filter: grayscale(30%);
  opacity: 0.7;
}

.event-card.is-past .event-description,
.event-card.is-past h3 {
  color: var(--color-ink-soft);
}

.view-event-link {
  display: inline-block;
  margin-top: 0.4em;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
}

@media (max-width: 700px) {
  .event-card {
    flex-direction: column;
  }
  .event-card .event-thumb {
    width: 100%;
    aspect-ratio: 3 / 2;
  }
}

/* Compact homepage "Recent engagements" variant — Schedule typography,
   no thumbnail, one repertoire line, in a 2×2 grid. */

/* Compact variant = the homepage programme row (the Schedule page uses the
   `full` variant). Date and venue take fixed side columns so the dates stack
   into a readable rail and the venues align on a common right edge; the work
   and repertoire take the flexible middle. */
.event-compact {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr) 15rem;
  gap: var(--space-3);
  align-items: baseline;
}

.event-compact h3 {
  /* An h3 under the section's h2 (was an h4, which skipped a level); styled
     exactly as the h4 was. */
  font-family: var(--font-serif);
  font-size: 1.35rem;
  font-weight: 500;
  margin: 0;
}

.event-row-date {
  font-family: var(--font-sans);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.event-row-venue {
  /* Body face, sentence case: a venue name is content, not a label (Brief 6
     made the same change on Schedule; the homepage rows were missed). */
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--color-ink-soft);
  text-align: right;
}

.event-compact-program {
  font-family: var(--font-body);
  color: var(--color-ink-soft);
  font-size: 1rem;
  line-height: 1.5;
  margin: 0.35em 0 0;
}

/* Collapse the three tracks to a stack at the same 900px threshold the homepage
   intro uses. (Was 760px, which left iPad-portrait widths on the three-column
   row with a ~257px middle track — titles and repertoire wrapped badly.) */
@media (max-width: 900px) {
  .event-compact {
    grid-template-columns: 1fr;
    gap: 0.35rem;
  }
  .event-row-venue {
    text-align: left;
  }
}

/* ---------- Instagram grid ---------- */

.instagram-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;
  /* The full content width, like the engagements list above it. At 900px the
     grid left ~150px of bare band either side at 1440. */
  max-width: var(--content-max-wide);
  margin: 0 auto;
}

.instagram-grid a {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
}

.instagram-grid img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 400ms var(--ease-out);
}

.instagram-grid a:hover img,
.instagram-grid a:focus-visible img {
  transform: scale(1.04);
}

@media (max-width: 700px) {
  .instagram-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 1px;
  }
}

/* ---------- Photo gallery ---------- */

/* Masonry via CSS columns: photos keep their natural aspect ratio (no crop),
   flowing top-to-bottom then across columns. */
.photo-grid {
  column-count: 2;
  column-gap: var(--space-2);
}

@media (min-width: 768px) {
  .photo-grid {
    column-count: 3;
    column-gap: var(--space-3);
  }
}

.photo-item {
  margin: 0 0 var(--space-2);
  break-inside: avoid;
}

@media (min-width: 768px) {
  .photo-item {
    margin-bottom: var(--space-3);
  }
}

.photo-grid button {
  display: block;
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  overflow: hidden;
  border-radius: var(--radius-media);
}

.photo-grid img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform 400ms var(--ease-out);
}

.photo-grid button:hover img {
  transform: scale(1.02);
}

/* Captions site-wide are set in the body face, not the display face: at
   12-15px Cormorant's hairlines break up on screen (the same reason the About
   biography moved, Brief 4). Approved on a side-by-side, 2026-10-02. */
.photo-credit {
  margin: 0.5em 0 0;
  font-family: var(--font-body);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--color-ink-soft);
  text-align: center;
}

/* Lightbox */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(26, 26, 26, 0.95);
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
}

.lightbox.is-open {
  display: flex;
}

.lightbox img {
  max-width: 90vw;
  max-height: 80vh;
  width: auto;
  height: auto;
  object-fit: contain;
}

.lightbox figcaption {
  color: var(--color-paper);
  text-align: center;
  font-family: var(--font-body);
  font-style: italic;
  margin-top: var(--space-2);
}

.lightbox-close,
.lightbox-prev,
.lightbox-next {
  position: absolute;
  background: none;
  border: none;
  color: var(--color-paper);
  font-size: 2rem;
  cursor: pointer;
  padding: var(--space-2);
  line-height: 1;
}

.lightbox-close {
  top: var(--space-2);
  right: var(--space-2);
}

.lightbox-prev {
  left: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
}

.lightbox-next {
  right: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
}

/* ---------- Video list ---------- */

.video-embed {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--color-ink);
  overflow: hidden;
  /* Warm-gray hairline so the frame reads as an intentional element on the
     cream ground rather than a loose object floating on it. Video keeps its
     keyline where photographs drop theirs — see --radius-media. */
  border: 1px solid var(--color-line);
  border-radius: var(--radius-media);
}

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

.video-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: none;
  cursor: pointer;
  background: linear-gradient(160deg, #2c2c2c, #141414);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Thumbnail comes in via the --thumb custom property rather than a full inline
   background string, so the fallback script can swap just the image (see
   initThumbFallback) without rebuilding the gradient wash. */
.video-poster.has-thumb {
  background-image: linear-gradient(rgba(20, 20, 20, 0.3), rgba(20, 20, 20, 0.5)), var(--thumb);
  background-size: cover;
  background-position: center;
}

.video-poster .play-ring {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  border: 1px solid rgba(250, 249, 247, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-paper);
  transition: transform 200ms var(--ease-out), background 200ms var(--ease-out);
}

.video-poster:hover .play-ring {
  transform: scale(1.06);
  background: rgba(250, 249, 247, 0.1);
}

.video-caption {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 1.1rem;
  margin-top: var(--space-2);
  text-align: center;
}

/* ---------- Video page: hero player + thumbnail selector (§7) ---------- */

.video-hero {
  max-width: 900px;
  margin: 0 auto;
}

.video-thumbs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-4) var(--space-3);
  max-width: 900px;
  margin: var(--space-5) auto 0;
}

@media (max-width: 700px) {
  .video-thumbs {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3) var(--space-2);
  }
}

.video-thumb {
  display: flex;
  flex-direction: column;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.video-thumb-frame {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 9;
  /* Same hairline framing as the hero/embeds (§8). */
  background: var(--color-ink) center / cover no-repeat;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-media);
  overflow: hidden;
}

.video-thumb-frame.has-thumb {
  background-image: var(--thumb);
}

/* Dim wash so the small play mark reads on any thumbnail; lightens on hover. */
.video-thumb-frame::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(20, 20, 20, 0.3);
  transition: background 200ms var(--ease-out);
}

.video-thumb:hover .video-thumb-frame::before,
.video-thumb:focus-visible .video-thumb-frame::before {
  background: rgba(20, 20, 20, 0.12);
}

.video-thumb .play-ring {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(250, 249, 247, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  color: var(--color-paper);
  transition: transform 200ms var(--ease-out);
}

.video-thumb:hover .play-ring {
  transform: scale(1.08);
}

.video-thumb-caption {
  margin-top: var(--space-2);
  font-size: 0.95rem;
}

/* The clip currently loaded in the hero. */
.video-thumb.is-active .video-thumb-frame {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
}

.video-thumb.is-active .video-thumb-frame::before {
  background: rgba(20, 20, 20, 0.12);
}

/* ---------- Press kit ---------- */

.press-section {
  margin-bottom: var(--space-5);
}

.press-section h2 {
  border-bottom: 1px solid var(--color-line);
  padding-bottom: var(--space-1);
  margin-bottom: var(--space-2);
}

.press-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.press-links a {
  font-family: var(--font-serif);
  font-size: 1.15rem;
}

/* Intro line aligned with the section headings below it, not centered. */
.press-lead {
  text-align: left;
  max-width: var(--content-max);
  margin-right: auto;
}

/* §11a: download lists pinned to the site's left axis, with a single
   portrait/performance photo in the right region alongside them — the
   homepage's text-left / image-right balance, applied here. Single column
   below 1000px (photo follows the lists). */
.press-docs {
  max-width: var(--content-max);
}

.press-aside {
  margin: 0 0 var(--space-5);
  max-width: 480px;
}

.press-aside img {
  display: block;
  width: 100%;
  height: auto;
}

.press-aside figcaption {
  margin-top: 0.5em;
  font-family: var(--font-body);
  font-style: italic;
  font-size: 0.85rem;
  color: var(--color-ink-soft);
}

@media (min-width: 1000px) {
  /* The document column is sized to what it holds, not to a share of the page.
     Measured at 1440 before this change: the longest row, "English (short) ·
     Download", was 192px inside a 675px rail, so 72% of every row was empty and
     the hairline under each heading ran three times the width of the links
     beneath it. A rule that promises width the content never uses is what made
     the page read as boxy. */
  .press-grid {
    display: grid;
    grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
    column-gap: 64px;
    align-items: start;
  }
  /* Pinned to the container's right content edge so the photograph, the photo
     grid below it and the page share one right edge. Before, the section rules
     ended at 811, the Photos grid at 857, the photograph at 1289 and the
     container at 1313 — four edges, none aligned. */
  .press-aside {
    margin: 0;
    max-width: 480px;
    justify-self: end;
  }
}

/* Runs the full container. It used to inherit --content-max (720px), which
   stopped it 456px short of the photograph directly above it for no reason the
   layout expressed. */
.press-photos-block {
  max-width: none;
}

/* Downloadable press photos. Masonry columns so each photo shows at its own
   natural aspect ratio (no forced crop) at a restrained thumbnail size; the
   credit + download live in a bar that slides up on hover. */
.press-photo-grid {
  column-count: 3;
  column-gap: var(--space-3);
}

@media (max-width: 700px) {
  .press-photo-grid { column-count: 2; }
}

@media (max-width: 460px) {
  .press-photo-grid { column-count: 1; }
}

.press-photo {
  position: relative;
  break-inside: avoid;
  margin: 0 0 var(--space-3);
  overflow: hidden;
}

.press-photo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-media);
}

/* Translucent bar with credit (left) + download (right) that slides up from the
   bottom of the photo on hover/focus, so the grid reads as clean images at rest
   and becomes interactive on approach. */
.press-photo figcaption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin: 0;
  padding: 0.6em 0.8em;
  background: rgba(26, 26, 26, 0.72);
  transform: translateY(100%);
  opacity: 0;
  transition: transform 260ms var(--ease-out), opacity 200ms var(--ease-out);
}

.press-photo:hover figcaption,
.press-photo:focus-within figcaption {
  transform: translateY(0);
  opacity: 1;
}

/* No hover on touch devices: the bar had no way to appear, so phones and
   tablets never saw a credit or a Download link (and a long-press saves the
   600px derivative, not the press file). There the same bar slides up once
   the photograph is at least half in view (`.is-inview`, set by a small
   observer in PressKitPage), and slides away as it leaves. Desktop hover
   behaviour is unchanged. */
@media (hover: none) {
  .press-photo.is-inview figcaption {
    transform: translateY(0);
    opacity: 1;
  }
}

.press-photo-credit {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 0.8rem;
  line-height: 1.3;
  color: rgba(250, 249, 247, 0.85);
}

/* Small-caps download affordance on the dark bar (paper text, not the bronze
   link-underline used on light surfaces). */
.press-photo-download {
  flex-shrink: 0;
  font-family: var(--font-sans);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--color-paper);
  border-bottom: 1px solid rgba(250, 249, 247, 0.5);
  padding-bottom: 1px;
  transition: border-color 200ms var(--ease-out);
}

.press-photo-download:hover,
.press-photo-download:focus-visible {
  border-bottom-color: var(--color-paper);
}

/* ---------- About redesign variants (§3) ---------- */

/* The biography proper, framed as a discrete reusable asset: hairline rules
   above and below, so an organizer copying it sees where it begins and ends. */
.bio-asset {
  border-top: 1px solid var(--color-line);
  border-bottom: 1px solid var(--color-line);
  padding: var(--space-3) 0;
}

/* Body is EB Garamond, the face §2 designates for body text. It had been set in
   Cormorant Garamond — the DISPLAY face — which is why it read thin and fragile
   at paragraph size however the sizes were tuned. (Option B in the trial.) */
.bio-asset p {
  font-family: var(--font-body);
  font-size: 1.15rem;
  /* Looser leading + a full paragraph-space between blocks: at ~1650 characters
     the bio was reading as one slab. The extra air is what makes it scannable,
     and it buys back the vertical height the right column loses as the columns
     widen (wider measure = fewer lines). Typography only — the approved bio text
     is untouched. */
  line-height: 1.72;
  margin: 0 0 var(--space-3);
  /* Full-strength ink: with the sturdier text face there is no need to lean on
     a colour drop, and the lead is now marked rather than merely darker. */
  color: var(--color-ink);
}

/* Editorial lead. Third pass on the size question: 1.38/1.2rem (15%) read as a
   mistake; 1.5/1.15rem (30%) was clear but shrank the body below comfortable
   reading. So the size step drops back to ~8% and the *weight* carries the
   distinction instead — Cormorant Garamond 600 against the body's 400, plus
   full-strength ink against softer ink. Body returns to 1.2rem. */
/* Trial option D's mark with option B's type: a bronze rule down the lead's
   left edge does the separating, so the size step can stay small (1.3 vs
   1.15rem) and the weight never has to go heavy — Cormorant 500, not 600. */
.bio-asset p:first-child {
  font-family: var(--font-serif);
  font-size: 1.3rem;
  font-weight: 500;
  line-height: 1.6;
  color: var(--color-ink);
  border-left: 2px solid var(--color-accent);
  padding-left: var(--space-3);
}

.bio-asset p:last-child {
  margin-bottom: 0;
}

/* Usage conditions: a caption on the asset, not another body paragraph. */
.bio-conditions {
  margin: var(--space-2) 0 0;
  max-width: 62ch;
  font-family: var(--font-sans);
  font-size: 0.76rem;
  line-height: 1.7;
  letter-spacing: 0.04em;
  color: var(--color-ink-soft);
}

.about-portrait img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-media);
}

/* Press-kit callout beneath the bio (§10) — a restrained labelled link that
   gives the lower right column a purpose without competing with the bio. */
.about-presskit {
  margin-top: var(--space-4);
}

.about-presskit .label {
  display: block;
  margin-bottom: 0.5em;
}

.about-presskit a {
  font-family: var(--font-serif);
  font-size: 1.15rem;
}

/* Variant A — two column at ≥1000px (single column below) */
.about-two-col {
  display: block;
}

/* Single-column: the portrait is capped at 480px and the bio at its reading
   measure, so left-aligning both stranded up to ~250px of empty space down the
   right-hand side — the blocks read as pushed into a corner rather than placed.
   Centring the *blocks* (text stays left-aligned inside them) is the right call
   once there is no second column for them to hold an axis against. This is the
   narrow-viewport exception to §2's "avoid centered-everything": the site axis
   only means something when something else is sitting opposite it. */
.about-two-col .about-portrait--aside {
  max-width: 480px;
  margin: 0 auto var(--space-4);
}

/* Cap the biography measure so it reads well filling the right column and never
   goes tight in the 1024–1199 single-column band. */
.about-main {
  max-width: 66ch;
  margin-left: auto;
  margin-right: auto;
}

/* Sponsors: three-across below 1200, subordinate vertical stack in the aside. */
.about-sponsors {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: var(--space-3) var(--space-4);
  margin: var(--space-4) 0;
}

.about-sponsors .affiliation-item {
  max-width: 150px;
  text-align: center;
}

/* One shared width, height follows each mark's own proportions. Nothing is
   scaled to a shape it doesn't have. */
.about-sponsors .affiliation-item img {
  width: 100%;
  max-width: 100%;
  height: auto;
}

/* Whitespace trim, measured rather than guessed. Sampling the ink rows of each
   file: Pirastro's mark occupies rows 105–373 of 478, so **22% of the file is
   blank margin at the top and 21.8% at the bottom** — the mark is 2.64:1 sitting
   inside a 1.49:1 canvas, which is why it towered over its neighbours and ate
   the left column. QEMC (3.81:1) and Sylva Gelber (2.24:1) are already tight to
   their edges and get no crop at all.
   Cropping Pirastro to 9/4 keeps rows 81–397: the whole mark, plus ~24px of air
   on each side, and not one pixel of the logo itself. Keyed on the filename so
   reordering the sponsors can't misapply it — if the file is ever replaced the
   rule simply stops matching and the logo shows uncropped, which is the safe
   failure. Re-measure if a new logo arrives with its own padding. */
.about-sponsors .affiliation-item img[src*="pirastro"] {
  aspect-ratio: 9 / 4;
  object-fit: cover;
  object-position: center;
}

/* Larger than the default affiliation caption, for legibility beneath the
   bigger logos. */
.about-sponsors .affiliation-item .caption {
  font-size: 0.95rem;
  line-height: 1.5;
}

/* Three 150px logos plus two 40px gaps need 530px of content width, so below
   ~600px viewport the flex-wrap row broke into two-then-one — the same odd
   split that reads as a mistake in the two-column layout. Stack all three
   instead: it is either three across or three down, never 2+1. */
@media (max-width: 599px) {
  .about-sponsors {
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
  }

  .about-sponsors .affiliation-item {
    max-width: 260px;
  }
}

/* Two-column at ≥1000px (was 1200px, which left common laptop/split-screen
   widths on the single-column stack: portrait, bio, then horizontal logos —
   read as broken. 1000px keeps the columns comfortable while engaging the
   intended portrait+logos-left / bio-right layout for most desktop views). */
/* The About spread is the one layout that genuinely wants more than the global
   1200px measure: two real columns of content, not a single centered column. At
   1200 it froze at ~1280px viewport and everything past that became dead side
   margin (360px each side at 1920, 680px at 2560) — the columns, the gap, and
   the portrait all stopped growing at once, which is what made resizing feel
   inert. Growing to 1560 keeps the page scaling through the common desktop
   widths; beyond that, margin is correct rather than wasteful. */
@media (min-width: 1000px) {
  .container--about {
    /* clamp, not min(): a bare min(92vw, …) resolves *below* the global 1200px
       cap on anything under ~1300px wide, which quietly narrowed the page at
       laptop widths. The 1200px floor guarantees this is never worse than
       before; 92vw does the growing; 1560 stops it. */
    max-width: clamp(1200px, 92vw, 1560px);
  }
}

@media (min-width: 1000px) {
  .about-two-col {
    display: grid;
    /* True 50/50 split about the container centerline: portrait (+ logos) fills
       the left half, biography the right. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    grid-template-areas:
      "portrait bio"
      "sponsors bio";
    /* Was a flat 64px at every width, which read tight against a 449px column at
       1024 and stayed exactly as tight at 2560. Scales with the viewport so the
       centre gutter opens up as the blocks grow. */
    column-gap: clamp(4rem, 5vw, 7rem);
    row-gap: var(--space-3);
    align-items: start;
  }
  .about-two-col .about-portrait--aside {
    grid-area: portrait;
    /* Fills the left half (was capped at 480px, which left it small and
       left-of-center), but stops growing at 640px. Past ~1750px viewport the
       column is wide enough that the 2211x3200 portrait renders over 1000px
       tall — taller than the whole biography — so everything below it in the
       left column is pure overhang no matter how small the logos are. The cap
       never engages at 1440 or below (the column is 602px there), so the
       measured desktop layout is untouched. Scaled, never cropped. */
    max-width: min(100%, 640px);
    margin: 0;
  }
  /* A single full-width stack, all three. A 2+1 grid was tried as a way to hold
     the column heights together and rejected on sight — an odd number in a
     two-column grid reads as a mistake, not a layout. The only two acceptable
     arrangements are all three stacked or all three across; the breakpoint
     below picks between them. */
  .about-two-col .about-sponsors {
    grid-area: sponsors;
    flex-direction: column;
    /* Centred in the rail rather than pinned to its left edge, so the block sits
       under the middle of the portrait the way the portrait sits under the
       middle of the rail. Left-aligned, the marks read as hanging off the
       photo's left corner with dead space to their right. Text inside each item
       stays left-aligned. */
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-3);
    margin: var(--space-3) 0 0;
  }
  /* Bio fills the right half and reflows with the window. The column itself is
     uncapped so it tracks the 50/50 split, but the reading measure is capped
     just below the widest column the grid can produce — past ~66ch a serif at
     this size gets hard to track line-to-line. In practice the cap only bites in
     the last few hundred pixels before 1560, where it leaves a small gutter
     rather than an over-long line. */
  .about-two-col .about-main {
    grid-area: bio;
    max-width: none;
  }
  .about-two-col .bio-asset,
  .about-two-col .bio-conditions {
    /* Deliberately a px cap, not `ch`: EB Garamond's zero-glyph is ~8.2px at
       this size, so a "66ch" cap resolves to ~539px — narrower than the column
       the old 1200px container already produced, which would have made the bio
       taller instead of wider. 620px ≈ 75ch here, the top of the comfortable
       range, and only engages past ~1500px viewport. */
    max-width: min(100%, 620px);
  }
  /* Caption under the mark, left-aligned on the portrait's edge. The marks are
     no longer locked to the portrait's width — they don't need to be. With the
     white grounds blended away there is no box edge to line up, so a shared left
     edge and a shared width between the three is enough, and the smaller mark is
     what buys the stack its range (see the breakpoint note below). */
  /* The cap is on the ITEM, not the image, so the caption can never run wider
     than the mark above it. Previously only the image was capped and the item
     kept the full rail — at a 460px column that put a 460px caption line under a
     190px logo, and the caption read as belonging to the column rather than to
     the mark. One width now governs both. */
  .about-sponsors .affiliation-item {
    max-width: clamp(320px, 80%, 440px);
    text-align: left;
  }
  .about-sponsors .affiliation-item img {
    width: 100%;
    margin: 0;
  }
  /* Two separate savings, and the second is the one that shows.
     1. Smaller and tighter: the caption is a credit line, not body copy, and it
        was set at 0.95rem/1.5 — nearly biography size.
     2. The gap to the mark above it. Every logo file carries its own internal
        padding below the ink (Pirastro ~8px at this scale even after the crop),
        so a nominal 0.5em margin read as roughly double that. Now the margin is
        set to sit *under* the file's own padding rather than on top of it, which
        is why it looks like almost nothing in the CSS and correct on screen. */
  .about-sponsors .affiliation-item .caption {
    display: block;
    margin: 0.1em 0 0;
    font-size: 0.82rem;
    line-height: 1.45;
  }
}

/* Three across is the FALLBACK, not the default. The stack holds until its
   overhang past the end of the right column — biography, usage note, and the
   press-kit line, i.e. all of `.about-main` — passes about 100px.

   Mark size costs stack range, but far less than it looks — in this band the
   biography reflows shorter almost as fast as the block grows, so most of the
   width is free. Measured, every logo loaded and the whitespace trim applied
   (LEFT − RIGHT; negative = the biography is longer), item = share of the rail:

     width  col    40% (caption full rail)   60%          80%          100%
     1000   437    190w  348h   −357         262w  −274   349w  −173   437w   −72
     1086   480    190w  348h   −307         288w  −126   384w   +15   480w  +106
     1152   513    205w  346h    −65         308w   +73   410w  +191   513w  +290

     margin passed at ~1320 (40%), ~1160 (60%), ~1105 (80%), ~1085 (100%)

   40% reaches furthest but gets there by letting the caption run the full rail
   under a much narrower mark — a 460px caption line under a 190px logo, which
   reads as the caption belonging to the column rather than to the logo.
   Rejected. From there the cost curve is shallow: 60% → 80% doubles the visual
   size of the mark for ~55px of range, so take the width.

   So: one width governs mark and caption together, at 80% of the rail
   (320–440px), the block centred in the rail, and the switch at 1110 — the last
   width inside the ~100px margin. Going to 100% would only buy another ~20px of
   mark and would lose the inset that makes the centring read at all.

   Past ~1400 nothing balances at all: the portrait alone is 872px against a
   1073px biography, so the whole sponsor budget is ~177px and even the across
   block is 144px plus its margin. Across is simply the least-bad arrangement up
   there, and it is also where the rail is widest and three marks side by side
   read best.

   Moving the switch is a one-line change; the table above is the trade. */
@media (min-width: 1110px) {
  .about-two-col .about-sponsors {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: clamp(1.5rem, 2.5vw, 2.75rem);
    margin: var(--space-4) 0 0;
    /* Same cap as the portrait above, so the row ends on the portrait's right
       edge instead of running past it once the column outgrows 640px. */
    max-width: min(100%, 640px);
  }
  .about-two-col .about-sponsors .affiliation-item {
    /* Equal tracks so the three sit on a shared grid regardless of logo aspect. */
    flex: 1 1 0;
    min-width: 0;
  }
  /* Side by side the three need a shared optical baseline, which the stack does
     not — stacked, each mark simply sits at its own height and any filler would
     be the whitespace we just removed. So the well lives here only: a 20/9 box
     with each mark anchored to its bottom edge, so the logos sit on one line and
     the captions start on one line. `contain` keeps every mark at its natural
     proportions; the box only supplies the alignment. Pirastro overrides to
     `cover` so its whitespace trim survives into the row (20/9 and 9/4 are close
     enough that Pirastro looks the same stacked or across).
     (`height: 50px` did this job before, but it equalised HEIGHT, so Pirastro
     rendered 74px wide inside a 177px track while QEMC filled it — three logos
     at three sizes. Equal width is the more legible shared constraint.) */
  .about-two-col .about-sponsors .affiliation-item img {
    width: 100%;
    /* A little more than the stack needs. Bottom-anchoring in the well puts each
       mark's ink flush to the box edge, so the file padding that cushions the
       stacked version isn't there — QEMC in particular has none of its own. */
    margin: 0 0 0.35em;
    aspect-ratio: 20 / 9;
    object-fit: contain;
    object-position: left bottom;
  }
  .about-two-col .about-sponsors .affiliation-item img[src*="pirastro"] {
    object-fit: cover;
    object-position: center;
  }
}

/* ---------- Affiliations ---------- */

.affiliation-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--space-4);
  margin: var(--space-4) 0;
}

.affiliation-item {
  text-align: center;
  max-width: 220px;
}

.affiliation-item img {
  filter: grayscale(100%);
  opacity: 0.75;
  margin: 0 auto var(--space-1);
  /* Pirastro and Sylva Gelber are JPGs with white backgrounds, so each mark sat
     in a hard-edged white rectangle on the warm paper — the one bit of card
     chrome left on the page, and cropping only made the rectangle smaller.
     Multiply leaves white untouched by definition (white is the identity for
     multiply), so the background dissolves into whatever it sits on and the ink
     stays. QEMC is already a transparent PNG and is unaffected. */
  mix-blend-mode: multiply;
}

.affiliation-item .caption {
  font-size: 0.8rem;
  color: var(--color-ink-soft);
  font-style: italic;
  font-family: var(--font-body);
}

/* ---------- Forms ---------- */

.contact-form {
  max-width: var(--content-max);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.form-row label {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-bottom: 0.4em;
  color: var(--color-ink-soft);
}

.form-row input,
.form-row textarea {
  width: 100%;
  font-family: var(--font-sans);
  font-size: 1rem;
  padding: 0.75em;
  border: 1px solid var(--color-line);
  background: var(--color-paper);
  color: var(--color-ink);
}

.form-row input:focus,
.form-row textarea:focus {
  outline: none;
  border-color: var(--color-accent);
}

.form-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form-success,
.form-error {
  display: none;
  padding: var(--space-2);
  border: 1px solid var(--color-line);
  font-family: var(--font-serif);
  font-size: 1.1rem;
}

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

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

/* Three-part footer on the site's left axis (was a single centered line, which
   left the whole band looking unfinished). Identity + photo credits lead, then
   two short link columns for navigation and press materials. */
.site-footer {
  border-top: 1px solid var(--color-line);
  padding: var(--space-5) 0 var(--space-4);
  font-size: 0.85rem;
  color: var(--color-ink-soft);
}

.site-footer .footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

@media (min-width: 760px) {
  .site-footer .footer-grid {
    /* Identity block takes the wider left half; the two link columns balance it
       on the right rather than all three sharing equal width. */
    grid-template-columns: 2fr 1fr 1fr;
    gap: var(--space-5);
  }
}

.site-footer .brand-name {
  font-family: var(--font-serif);
  font-size: 1.25rem;
  color: var(--color-ink);
  margin-bottom: 0.6em;
}

.site-footer .footer-credits {
  margin: 0;
  max-width: 34ch;
  font-size: 0.78rem;
  line-height: 1.6;
}

.site-footer .footer-col .label {
  display: block;
  margin-bottom: 0.9em;
}

.site-footer .footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55em;
}

.site-footer .footer-col a {
  font-family: var(--font-body);
  font-size: 0.95rem;
  color: var(--color-ink);
  border-bottom: 1px solid transparent;
  transition: color 200ms var(--ease-out), border-color 200ms var(--ease-out);
}

.site-footer .footer-col a:hover,
.site-footer .footer-col a:focus-visible {
  color: var(--color-accent-dark);
  border-bottom-color: var(--color-accent);
}

/* `.footer-note` (a sub-line under the press-kit link) was removed along with the
   copy it carried — Michael didn't write that line and it read as filler. The
   rule is gone rather than left orphaned; the press column is now link-only,
   which matches the Explore column beside it. */

.site-footer .footer-base {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-line);
  font-size: 0.78rem;
}

/* ---------- Utility ---------- */

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

.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;
}

/* ---------- About: phone refinements ---------- */

/* Two problems stacked on a phone: the interior banner (58vh) plus the fixed
   144px interval below it pushed ~630px of banner-and-air above the portrait,
   and the portrait then filled the rest of the first screen on its own — so an
   "About" page opened on a banner and a photo, with no biography in sight.
   Shortening the banner and the interval, and holding the portrait to ~62% of
   the column, gets the opening of the bio above the fold. The portrait is
   scaled, never cropped — the crop is Michael's call, not the layout's. */
/* Applies across the whole SINGLE-COLUMN regime (<1000px), not just phones. At
   768x1024 the old 700px cutoff left iPad portrait on the desktop treatment: a
   594px banner (58% of the view) and a 144px gap above the portrait against
   40px below it. The stacked layout is the regime where these go wrong, so the
   breakpoint should match the layout, not a guess at "phone". */
@media (max-width: 999px) {
  /* .hero.hero-short, not .hero-short — the base rule at the top of the file
     uses the two-class form, so a single-class override here loses on
     specificity and silently does nothing. */
  /* The banner is decorative and not About-specific, so on a phone it should
     not claim half the first screen. */
  .hero.hero-short {
    min-height: min(30vh, 240px);
  }

  /* Equal air above and below the portrait (40px each) so it sits balanced
     between the banner and the biography instead of being pushed down onto the
     text. Was 64px above / 24px below. */
  .hero-short + .section {
    padding-top: var(--space-4);
  }
}

/* Short viewports — landscape phones (844x390) and the like. The max-width:700px
   block above misses these entirely: at 844px wide it kept the desktop 144px
   interval and the full-height banner, so the banner took 58% of the view and
   the portrait (695px) was taller than the whole 390px viewport. The governing
   dimension for "the banner is eating the screen" is HEIGHT, not width.
   Scoped to <1000px so the desktop two-column layout, which is tuned on the
   column-balance measurements, is never affected by a short window. */
@media (max-width: 999px) and (max-height: 620px) {
  .hero.hero-short {
    min-height: min(40vh, 150px);
  }

  /* min-height alone could not shrink it: the banner's own content (64px of
     padding top and bottom plus a 40px title) set a 195px floor. */
  .hero.hero-short .hero-content {
    padding-top: var(--space-3);
    padding-bottom: var(--space-3);
  }

  .hero.hero-short .hero-content h1 {
    font-size: 2rem;
    margin-bottom: 0.25em;
  }

  .hero-short + .section {
    padding-top: var(--space-4);
  }

  /* Landscape phones: bound the portrait band by viewport HEIGHT so it cannot
     fill a 390px-tall window. Capping the block's width (the band keeps its 3:2
     ratio) rather than the image's height, so the crop never changes with the
     window — only the scale does. */
  .about-two-col .about-portrait--aside {
    max-width: min(100%, calc(56vh * 1.5));
    margin-bottom: var(--space-4);
  }
}


/* ---------- About in the single-column regime: M2 + M3 ---------- */

/* M3 — the banner is dropped, and M2 — the portrait becomes a landscape band.
   Rationale: the banner is decorative and not About-specific, so once there is
   only one column it is the thing that should go, not the headshot. Together
   they mean the page opens on its title and the biography rather than on 600px
   of photography.

   Scoped to the whole SINGLE-COLUMN regime (<1000px), not <=700px. The 700px
   cutoff left a dead band from 701 to 999 — iPad portrait, a half-screen
   desktop window, the 737px preview pane — where the banner stayed AND the
   portrait was still a tall 2:2.9 crop, so the entire first screen was two
   photographs stacked with the biography below the fold. That is the same
   failure the phone rules were written to fix, so it gets the same fix: the
   breakpoint matches the layout, not a guess at "phone". The trade is that
   About loses its banner at 999px and regains it at 1000px — deliberate, and it
   lands on the breakpoint where the whole page changes shape anyway.

   Scoped by :has() on the About body wrapper, so the two-column layout — tuned
   on the column-balance measurements — never sees any of it. */
@media (max-width: 999px) {
  /* M3: the banner collapses to a plain title block. The element stays (one
     <h1>, no duplicate heading) — only its imagery and scrim are dropped. */
  .hero.hero-short:has(+ .section--about) {
    min-height: 0;
    background: none;
  }

  .hero.hero-short:has(+ .section--about) img,
  .hero.hero-short:has(+ .section--about)::after {
    display: none;
  }

  .hero.hero-short:has(+ .section--about) .hero-content {
    padding: calc(var(--header-height) + var(--space-4)) var(--space-3) 0;
  }

  .hero.hero-short:has(+ .section--about) .hero-content h1 {
    color: var(--color-ink);
    text-shadow: none;
  }

  /* With no photo behind it, the header's light text would sit on paper and
     vanish. Hold the bar at full reveal on this page so it stays legible from
     scroll position zero. */
  body:has(.section--about) .site-header,
  body:has(.page--plain) .site-header {
    --header-floor: 1;
  }

  /* M2: a 3:2 landscape band, centred. The base .about-portrait--aside cap of
     480px does the sizing — the old rule here raised it to 100% of the column,
     which at 737-999px produced a band 460-630px tall: the "one massive
     headshot" problem again, just in a wider shape. Letting the base cap stand
     keeps the band the same 480x320 from 700px up to 999px, and on a phone the
     cap never engages (the column is ~340px), so the approved phone treatment
     is unchanged. */

  /* This DOES crop — object-position is the adjustable control. At 3:2 the band
     shows the top 46% of the 2211x3200 source, and the top of Michael's head
     sits at 2.2% of it, so anything past ~4% clips it. Was 22%, which cut the
     crown of his head; anchored to the top it is fully in frame. */
  .about-two-col .about-portrait--aside img {
    aspect-ratio: 3 / 2;
    object-fit: cover;
    object-position: center 0%;
  }
}

/* Landscape phones again, but for the title block this time. The M3 rules above
   win on specificity over the short-viewport block (`:has()` makes them a
   compound selector) and they come later in the file, so the 105px of top
   padding that reads as generous air on a tall phone survived onto a 390px-tall
   window and pushed the whole page down. Nothing fits the biography above the
   fold at this height, but trimming the title block and the band gets the
   opening line to within a few pixels of it instead of 85px past. */
@media (max-width: 999px) and (max-height: 620px) {
  .hero.hero-short:has(+ .section--about) .hero-content {
    padding-top: calc(var(--header-height) + var(--space-2));
  }

  .hero.hero-short:has(+ .section--about) .hero-content h1 {
    font-size: 2rem;
    margin-bottom: 0;
  }

  .about-two-col .about-portrait--aside {
    max-width: min(100%, calc(48vh * 1.5));
  }
}


/* ---------- Homepage section rhythm ---------- */

/* The gap between one section's last element and the next section's title was
   209px — two 96px paddings stacking, plus the trailing paragraph margin —
   against only 97px from a section's own top edge to its title. That is what
   made the space below MORE VIDEO and FULL SCHEDULE read as dead air while the
   sections themselves felt tight.
   96px → 64px was still too loose (a 128px between-section gap), so this is the
   second reduction: 40px inside, ~80px between. The hairline rule on each
   section's top edge is doing most of the separating work, so the padding only
   has to keep the content off the line — it does not have to carry the division
   on its own. */
.home-video,
.home-engagements-section,
.home-instagram-section {
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}

/* These are <p> wrappers around a button, so they carried a paragraph's bottom
   margin into the section padding. */
.home-video-more,
.home-engagements-more {
  margin-bottom: 0;
}

/* ---------- Inline Instagram video playback ---------- */

.instagram-video-trigger {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  padding: 0;
  border: 0;
  border-radius: 0;
  appearance: none;
  background: #000;
  color: #fff;
  cursor: pointer;
}

.instagram-video-trigger img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.instagram-video-trigger:hover img,
.instagram-video-trigger:focus-visible img {
  transform: scale(1.04);
}

.instagram-video-trigger.is-active {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.instagram-embed-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: linear-gradient(
    145deg,
    var(--color-paper-dim),
    var(--color-line)
  );
  color: var(--color-ink);
}

.instagram-embed-kicker {
  font-family: var(--font-sans);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-accent-dark);
}

.instagram-embed-title {
  margin-top: 0.15em;
  font-family: var(--font-serif);
  font-size: clamp(1.5rem, 4vw, 2.25rem);
  line-height: 1;
}

.instagram-video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding-left: 3px;
  border: 1px solid rgba(255, 255, 255, 0.85);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.48);
  color: #fff;
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1;
  transform: translate(-50%, -50%);
  transition:
    transform 200ms var(--ease-out),
    background 200ms var(--ease-out);
}

.instagram-video-trigger:hover
  .instagram-video-play,
.instagram-video-trigger:focus-visible
  .instagram-video-play {
  background: rgba(0, 0, 0, 0.68);
  transform:
    translate(-50%, -50%)
    scale(1.06);
}

/* Lightbox for Instagram videos (src/scripts/instagram-video.js). The media
   is sized in script to the largest box of its own shape that fits the
   screen; nothing frames it. The page behind is dimmed with the site's ink:
   this is a transient viewer over the page, not a section of it. */
.ig-lightbox {
  position: fixed;
  z-index: 300;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: 56px;
  background: rgba(26, 26, 26, 0.92);
}

.ig-lightbox-open,
.ig-lightbox-open body {
  overflow: hidden;
}

.ig-lightbox-controls {
  position: fixed;
  top: 8px;
  right: 8px;
  display: flex;
  gap: 4px;
}

.ig-lightbox-icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: rgba(250, 249, 247, 0.75);
  cursor: pointer;
  transition: color 150ms var(--ease-out);
}

.ig-lightbox-icon:hover,
.ig-lightbox-icon:focus-visible {
  color: var(--color-paper);
}

.ig-lightbox-media {
  position: relative;
  flex: none;
  overflow: hidden;
  background: #000;
  border-radius: var(--radius-media);
}

.ig-lightbox-media video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  cursor: pointer;
}

.ig-lightbox-media video:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* Shown only while paused; clicks pass through to the video beneath. */
.ig-lightbox-play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: none;
  place-items: center;
  width: 64px;
  height: 64px;
  padding-left: 4px;
  border: 1px solid rgba(255, 255, 255, 0.85);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  font-family: var(--font-sans);
  font-size: 1.3rem;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.ig-lightbox-media.is-paused .ig-lightbox-play {
  display: grid;
}

/* The reel window: see REEL in src/scripts/instagram-video.js for the
   measurements behind these offsets. The !important rules beat the inline
   styles embed.js puts on its iframe (max-width: 540px, min-width: 326px,
   margin, width). */
.ig-reel-crop .instagram-media {
  position: absolute !important;
  top: var(--frame-top) !important;
  left: var(--frame-left) !important;
  width: var(--frame-w) !important;
  min-width: 0 !important;
  max-width: none !important;
  margin: 0 !important;
}

.ig-lightbox-loading {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  margin: 0;
  font-family: var(--font-sans);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(250, 249, 247, 0.6);
}

/* ---------------------------------------------------------------------------
   Margin rules: text pages, desktop only
   ---------------------------------------------------------------------------
   Two 1px bronze rules, 28px in from the window edge, down the sides of the
   text-based interior pages (About, Schedule, Press Kit, Contact, event
   detail). They replaced the Brief 8 background field, which Michael removed
   on 2026-09-08 after every edge treatment for its sheet was rejected on
   sight; the answer was to go back to full-width paper and give the paper a
   quiet accent instead of a dark surround.

   Decisions, all Michael's (2026-09-08), chosen from rendered alternatives:
   - 1px, not 2px. Bronze, not the warm-gray hairline.
   - Starts 24px BELOW the banner, not on its edge, so it belongs to the paper
     rather than hanging off the photograph.
   - Stops at the footer's top hairline: the pseudo-elements live on the page's
     section, whose bottom edge is the footer's top edge.
   - >= 1000px only. On a phone 28px of inset is most of the margin and the
     rule would run beside text.
   - Photo and Video pages carry no rule: the accent is for text pages.
   Opted in per page with .section--ruled, so a new page decides for itself.
--------------------------------------------------------------------------- */
@media (min-width: 1000px) {
  .section--ruled {
    position: relative;
  }
  .section--ruled::before,
  .section--ruled::after {
    content: "";
    position: absolute;
    top: 24px;
    bottom: 0;
    width: 1px;
    background: var(--color-accent);
    pointer-events: none;
  }
  .section--ruled::before { left: 28px; }
  .section--ruled::after { right: 28px; }
}

/* ---------------------------------------------------------------------------
   Print. The reveal-on-scroll hidden state must never reach paper: printed,
   the Schedule showed one of seventeen events. The fixed header and the
   Instagram band are screen furniture.
   --------------------------------------------------------------------------- */
@media print {
  .reveal-enabled [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }

  .site-header {
    position: static;
  }

  .site-header::before,
  .mobile-drawer,
  .hamburger,
  .home-instagram-section,
  .ig-lightbox,
  .skip-link {
    display: none !important;
  }

  .hero.hero-full {
    min-height: 0;
  }
}
