/* ==========================================================
   Amedeo Cappelletti — Portfolio
   Shared stylesheet (homepage + section pages)
   Palette: cream #f0ebe0 / ink #111110 / dark #0d0f14
   Fonts: Cormorant Garamond (display) + Inter (body, light)
   ========================================================== */

:root {
  --cream: #f0ebe0;
  --ink: #111110;
  --dark: #0d0f14;
  --dark-text: #f0ebe0;
  --line: rgba(17, 17, 16, 0.15);
  --line-dark: rgba(240, 235, 224, 0.15);
  --accent: #c2643a;
  --max-w: 1600px;
  --gutter: clamp(20px, 4vw, 64px);
}

* {
  box-sizing: border-box;
}

/* Visually hidden but present for screen readers and search engines —
   used for SEO/accessibility text (e.g. page H1s on full-bleed pages)
   that would otherwise clutter the visual design. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: "Inter", sans-serif;
  font-weight: 300;
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4,
.display {
  font-family: "Cormorant Garamond", serif;
  font-weight: 400;
  margin: 0;
  letter-spacing: 0.01em;
}

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

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

.eyebrow,
.label {
  font-family: "Inter", sans-serif;
  font-weight: 400;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* ---------- left vertical nav rail ---------- */
/* No background panel — legibility comes from colour + a soft dark halo
   on the text itself, so the rail stays fully transparent over the photo. */

:root {
  --rail-w: 220px;
}

.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--rail-w);
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 56px;
  padding: 36px 28px;
  color: var(--ink);
  box-sizing: border-box;
  transition: opacity 0.4s ease;
}

/* Faded out for as long as the work grid (homepage "page 2") is the
   section in view — see the IntersectionObserver in index.html. */
.site-nav.nav-hidden {
  opacity: 0;
  pointer-events: none;
}

/* A soft, permanent dark scrim behind the nav rail, for pages where nav
   sits directly over full-bleed media of unpredictable brightness (e.g.
   a run of full-screen video pages) rather than one specific photo that's
   already been tuned for it. Sits below the nav text (z-index), above
   the media itself. */
.nav-scrim {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: calc(var(--rail-w) + 120px);
  z-index: 40;
  pointer-events: none;
  background: linear-gradient(
    to right,
    rgba(13, 15, 20, 0.55) 0%,
    rgba(13, 15, 20, 0.4) 45%,
    transparent 100%
  );
}

.site-nav a {
  color: inherit;
}

.site-nav .nav-name {
  display: inline-block;
  font-family: "Cormorant Garamond", serif;
  font-size: 22px;
  line-height: 1.2;
  letter-spacing: 0.02em;
  transform-origin: left center;
  transition: transform 0.2s ease;
}

.site-nav .nav-name:hover {
  transform: scale(1.06);
}

.site-nav .nav-links {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

/* "Homepage" only makes sense in the mobile dropdown, where the site
   name in the bar isn't always read as a home link — on desktop the
   name itself already does that job, so this entry would just be a
   redundant extra item in the left rail. Selector matches
   ".site-nav .nav-links a"'s specificity (which sets display:inline-block
   further down) — a bare ".nav-home-mobile-only" was losing to it. */
.site-nav .nav-links a.nav-home-mobile-only {
  display: none;
}

.site-nav .nav-links a {
  position: relative;
  display: inline-block;
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.75;
  transform-origin: left center;
  transition: opacity 0.2s ease, transform 0.2s ease;
  /* Padding + matching negative margin grows the actual tappable area well
     past the visible text (12px type, tight 22px stacking) without
     changing the rail's visual spacing or the hover-scale's anchor point —
     the box is bigger, but the layout footprint (and gap between links)
     stays exactly as drawn. */
  padding: 12px 6px;
  margin: -12px -6px;
}

.site-nav .nav-links a:hover,
.site-nav .nav-links a.active {
  opacity: 1;
}

.site-nav .nav-links a:hover {
  transform: scale(1.08);
}

/* :hover never fires on touch (tap has no persistent pointer), so taps on
   a phone/tablet showed zero feedback even though the rules above exist —
   :active picks up the tap-down moment instead. */
.site-nav .nav-links a:active {
  transform: scale(0.95);
  opacity: 1;
}

.site-nav .nav-name:active {
  transform: scale(0.97);
}

/* Current page gets a warm highlight rather than relying on opacity alone —
   works the same whether the rail is sitting on the light (cream) pages or
   the dark (photo/video) ones, since it's a fixed accent, not inherited. */
.site-nav .nav-links a.active {
  color: var(--accent);
}

/* ---------- entrance motion ---------- */
/* A quiet fade + rise on load — nothing bouncy, just enough to make the
   page feel considered rather than static. */

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.site-nav .nav-name {
  /* "backwards" (not "both") — holds the pre-animation state during the
     delay so there's no flash, but releases transform back to the normal
     cascade once the intro finishes, so :hover/:active scale can apply.
     "both" was pinning transform: translateY(0) forever and silently
     blocking every hover/tap effect on these links. */
  animation: fade-up 0.7s ease backwards;
}

.site-nav .nav-links a {
  animation: fade-up 0.6s ease backwards;
  animation-delay: calc(0.08s * var(--i, 0) + 0.1s);
}

.hero-caption .hero-name {
  animation: fade-up 0.9s ease 0.3s both;
}

.hero-caption .hero-tagline {
  animation: fade-up 0.8s ease 0.55s both;
}

@media (prefers-reduced-motion: reduce) {
  .site-nav .nav-name,
  .site-nav .nav-links a,
  .hero-caption .hero-name,
  .hero-caption .hero-tagline {
    animation: none;
  }
}

/* On the homepage the rail sits over a photo with no backing panel and no
   shadow/glow — plain flat dark grey, chosen because the photo behind the
   rail runs light overall (cream wall, warm hair), so a dark neutral reads
   clearly without needing any effect. */
.site-nav.home {
  color: #3c3c3c;
}

/* Some hero-bg variants (dark artwork, dark performance shots) run dark in
   the margins instead — add this class on the nav for those pages and the
   links switch to the same light cream as the caption text. */
.site-nav.home.on-dark {
  color: var(--dark-text);
}

/* Every direct section of the page shifts right to clear the rail,
   except the full-bleed hero photo and homepage work grid, which run
   edge to edge instead of leaving a reserved gutter. */
body > *:not(.site-nav):not(.hero):not(.work-grid) {
  padding-left: calc(var(--rail-w) + var(--gutter));
}

.nav-toggle {
  display: none;
}

@media (max-width: 860px) {
  .site-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: auto;
    width: auto;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 0;
    padding: 16px var(--gutter);
    background: var(--cream);
  }

  /* The rail is see-through by design on desktop (nothing ever scrolls
     behind it — content is pushed clear via padding-left instead). The
     mobile top bar has no such reservation, so without an opaque
     background the fixed "Amedeo Cappelletti" name and links just float
     directly over whatever page content scrolls up underneath them.
     Pages that intentionally run a full-bleed dark photo/video right up
     to the top (via .site-nav.home) keep the see-through look; this
     background is skipped for those specifically. */
  .site-nav.home {
    background: none;
  }

  /* The rail collapses to a single top bar under 860px — previously the
     link list just vanished here with no replacement, leaving phone/
     tablet visitors with a logo and zero way to reach any other page.
     Now a hamburger button opens the same links as a full-screen panel. */
  .nav-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin-left: auto;
    padding: 0;
    background: none;
    border: none;
    color: inherit;
    cursor: pointer;
    z-index: 60;
  }

  .nav-toggle svg {
    width: 22px;
    height: 22px;
  }

  .nav-toggle .bar-top,
  .nav-toggle .bar-bot {
    transition: transform 0.2s ease;
    transform-origin: center;
  }

  .nav-toggle .bar-mid {
    transition: opacity 0.15s ease;
  }

  .nav-toggle.is-open .bar-top {
    transform: translateY(5px) rotate(45deg);
  }

  .nav-toggle.is-open .bar-bot {
    transform: translateY(-5px) rotate(-45deg);
  }

  .nav-toggle.is-open .bar-mid {
    opacity: 0;
  }

  .site-nav .nav-links {
    display: flex;
    position: fixed;
    inset: 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 30px;
    margin: 0;
    background: var(--cream);
    z-index: 55;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
  }

  .site-nav .nav-links.is-open {
    opacity: 1;
    pointer-events: auto;
  }

  .site-nav .nav-links a {
    font-size: 16px;
  }

  .nav-home-mobile-only {
    display: inline-block;
  }

  body.nav-open-lock {
    overflow: hidden;
  }

  body > *:not(.site-nav):not(.hero):not(.work-grid) {
    padding-left: var(--gutter);
  }
}

/* ---------- scroll-snapped "pages" (homepage only) ---------- */
/* One wheel/trackpad scroll carries the whole way from the hero to the
   work grid and locks there — rather than a slow, partial scroll that
   leaves the grid half-cut-off until the person scrolls again. Scoped to
   the homepage body class so interior pages (which scroll normally) are
   unaffected. */
body.snap-home {
  scroll-snap-type: y mandatory;
}

body.snap-home .hero,
body.snap-home .work-grid,
body.snap-home .home-words {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* ---------- hero (homepage) ---------- */

.hero {
  position: relative;
  height: 100vh;
  min-height: 560px;
  width: 100%;
  overflow: hidden;
  background: #e7e1d3;
}

/* Optional fill behind the portrait — sits in the margins either side of
   the contained photo (and full-bleed behind it) instead of a flat colour.
   Default treatment is for photographic fills (moderate blur + darken);
   the line-drawing variants override this inline with a much lighter touch
   so the drawings stay crisp and readable, not just a fuzzy backdrop. */
.hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(0.62) saturate(0.85) blur(3px);
  z-index: 0;
}

/* Wrapper sized to the portrait's real aspect ratio (rather than the
   hero's), so it exactly matches what object-fit: contain would have
   rendered — but because the wrapper's own edges now line up with the
   photo's actual edges, a mask on those edges lands in the right place. */
.hero-portrait-wrap {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  height: 100%;
  aspect-ratio: 2228 / 2550;
  z-index: 1;
  overflow: hidden;
}

.hero-portrait-wrap img.portrait {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Optional soft feather on the portrait's own left/right edges, so the
   seam against a hero-bg fill dissolves gradually instead of cutting hard
   from sharp photo to blurred fill. Opt in per page with .feather-edges.
   Multiple stops (rather than one hard step) give an eased falloff instead
   of a linear ramp, which reads as noticeably softer. */
.hero-portrait-wrap.feather-edges {
  mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.4) 10%,
    black 30%,
    black 70%,
    rgba(0, 0, 0, 0.4) 90%,
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.4) 10%,
    black 30%,
    black 70%,
    rgba(0, 0, 0, 0.4) 90%,
    transparent 100%
  );
}

/* A more extreme version for busier fills, where an even wider/softer
   dissolve helps disguise the seam further. */
.hero-portrait-wrap.feather-wide {
  mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.25) 20%,
    black 45%,
    black 55%,
    rgba(0, 0, 0, 0.25) 80%,
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.25) 20%,
    black 45%,
    black 55%,
    rgba(0, 0, 0, 0.25) 80%,
    transparent 100%
  );
}

/* Keeps the bulk of the photo (the middle ~54%) completely untouched and
   crisp — clarity of the original picture is preserved — and only fades
   the outer band on each side, using many small steps so the fade itself
   reads as perfectly smooth rather than banded, reaching full transparency
   exactly at the true edge. */
.hero-portrait-wrap.feather-clear {
  mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.06) 3%,
    rgba(0, 0, 0, 0.14) 6%,
    rgba(0, 0, 0, 0.24) 9%,
    rgba(0, 0, 0, 0.36) 12%,
    rgba(0, 0, 0, 0.5) 15%,
    rgba(0, 0, 0, 0.65) 18%,
    rgba(0, 0, 0, 0.8) 20%,
    rgba(0, 0, 0, 0.92) 22%,
    black 23%,
    black 77%,
    rgba(0, 0, 0, 0.92) 78%,
    rgba(0, 0, 0, 0.8) 80%,
    rgba(0, 0, 0, 0.65) 82%,
    rgba(0, 0, 0, 0.5) 85%,
    rgba(0, 0, 0, 0.36) 88%,
    rgba(0, 0, 0, 0.24) 91%,
    rgba(0, 0, 0, 0.14) 94%,
    rgba(0, 0, 0, 0.06) 97%,
    transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.06) 3%,
    rgba(0, 0, 0, 0.14) 6%,
    rgba(0, 0, 0, 0.24) 9%,
    rgba(0, 0, 0, 0.36) 12%,
    rgba(0, 0, 0, 0.5) 15%,
    rgba(0, 0, 0, 0.65) 18%,
    rgba(0, 0, 0, 0.8) 20%,
    rgba(0, 0, 0, 0.92) 22%,
    black 23%,
    black 77%,
    rgba(0, 0, 0, 0.92) 78%,
    rgba(0, 0, 0, 0.8) 80%,
    rgba(0, 0, 0, 0.65) 82%,
    rgba(0, 0, 0, 0.5) 85%,
    rgba(0, 0, 0, 0.36) 88%,
    rgba(0, 0, 0, 0.24) 91%,
    rgba(0, 0, 0, 0.14) 94%,
    rgba(0, 0, 0, 0.06) 97%,
    transparent 100%
  );
}

/* Continues the fade past the portrait's own edge, out into the flat
   cream margins — a very gradual grey tint (same grey as the nav text)
   that only shows up right at the far left/right of the viewport, so the
   outer corners settle instead of stopping dead at a flat colour. */
.hero-edge-tint {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to right,
    rgba(60, 60, 60, 0.1) 0%,
    rgba(60, 60, 60, 0.1) 10%,
    rgba(60, 60, 60, 0.075) 12%,
    rgba(60, 60, 60, 0.055) 14%,
    rgba(60, 60, 60, 0.038) 16%,
    rgba(60, 60, 60, 0.025) 18%,
    rgba(60, 60, 60, 0.013) 20%,
    rgba(60, 60, 60, 0.006) 22%,
    transparent 25%,
    transparent 75%,
    rgba(60, 60, 60, 0.006) 78%,
    rgba(60, 60, 60, 0.013) 80%,
    rgba(60, 60, 60, 0.025) 82%,
    rgba(60, 60, 60, 0.038) 84%,
    rgba(60, 60, 60, 0.055) 86%,
    rgba(60, 60, 60, 0.075) 88%,
    rgba(60, 60, 60, 0.1) 90%,
    rgba(60, 60, 60, 0.1) 100%
  );
}

/* Faint overall vignette — unifies tone across the sharp/blurred seam so
   no hard brightness line reads even at the edge of the feather. */
.hero-vignette {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    ellipse at center,
    transparent 35%,
    rgba(13, 15, 20, 0.22) 100%
  );
}

/* Legibility now comes from a soft shadow on the letterforms themselves
   rather than a panel/scrim behind them — no visible shape, just enough
   lift to read clearly over the photo. */

/* Crossfade slideshow — 6 layers, slow continuous loop */
.hero-layer {
  opacity: 0;
  filter: grayscale(12%) contrast(1.04);
  animation: hero-crossfade 48s infinite;
}

@keyframes hero-crossfade {
  0%      { opacity: 1; }
  11.67%  { opacity: 1; }
  16.67%  { opacity: 0; }
  95%     { opacity: 0; }
  100%    { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-layer {
    animation: none;
    opacity: 0;
  }
  .hero-layer:first-child {
    opacity: 1;
  }
}

/* Site-wide reduced-motion safety net. Rather than hand-tracking every
   transition/animation added over time (hover zooms, the photo/video
   slider's sliding transform, the video-feature card-swap crossfade,
   the new nav/contact hover scale, etc.), this blanket rule squashes all
   of them to effectively instant for anyone whose OS has "reduce motion"
   turned on — including inline transitions set from JS, since a stylesheet
   !important still wins over a plain inline style. Nothing disappears or
   breaks, it just stops moving; end states (hover colour, the swapped
   card, the new slide) still land correctly, just without the motion in
   between. The two specific blocks above stay as-is since they also needed
   to pin an explicit end state, not just kill timing. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Bottom gradient removed — no overlay text at the bottom of the hero anymore,
   so the photo now runs full strength with no darkening besides the top scrim. */

/* A full-width grey band across the bottom of the hero — the photo behind
   the caption swung from near-black shirt to near-white panelling in the
   same few inches, which is why no flat text colour ever held up letter
   by letter. Giving the whole strip a consistent backdrop instead solves
   it once, for everything that sits on it (name, tagline, contact). Soft
   top edge rather than a hard line, so it still reads as a blend. */
.hero-bottom-band {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 30%;
  min-height: 190px;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(42, 42, 40, 0.9) 0%,
    rgba(42, 42, 40, 0.87) 12%,
    rgba(42, 42, 40, 0.8) 24%,
    rgba(42, 42, 40, 0.68) 36%,
    rgba(42, 42, 40, 0.54) 47%,
    rgba(42, 42, 40, 0.4) 57%,
    rgba(42, 42, 40, 0.28) 67%,
    rgba(42, 42, 40, 0.18) 76%,
    rgba(42, 42, 40, 0.1) 84%,
    rgba(42, 42, 40, 0.05) 91%,
    rgba(42, 42, 40, 0.02) 96%,
    transparent 100%
  );
}

.hero-caption {
  position: absolute;
  left: 50%;
  bottom: clamp(20px, 3vw, 40px);
  transform: translateX(-50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
}

.hero-caption .hero-name {
  font-size: clamp(34px, 5.2vw, 72px);
  line-height: 1;
  color: #f0ebe0;
}

.hero-caption .hero-tagline {
  font-family: "Inter", sans-serif;
  font-size: clamp(11px, 2.6vw, 13px);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(240, 235, 224, 0.8);
  white-space: nowrap;
}

/* Small photo credit, back to sitting on its own in the bottom-right
   corner of the hero — contact links moved to About/Contact instead. */
.hero-credit {
  position: absolute;
  right: var(--gutter);
  bottom: 24px;
  z-index: 2;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.05em;
  color: rgba(240, 235, 224, 0.45);
}

/* On narrow phones the centered caption block (name + tagline) can run
   wide enough to reach the same corner as the bottom-right photo credit,
   overlapping text. Below this width, drop the credit under the caption
   instead of beside it. */
@media (max-width: 480px) {
  .hero-credit {
    right: auto;
    left: 50%;
    bottom: 6px;
    transform: translateX(-50%);
  }
}

/* ---------- page hero (section pages, no photo) ---------- */

.page-hero {
  padding: clamp(88px, 11vw, 128px) var(--gutter) clamp(28px, 3.4vw, 44px);
  background: var(--cream);
  border-bottom: 1px solid var(--line);
}

.page-hero .eyebrow {
  color: rgba(17, 17, 16, 0.55);
  margin-bottom: 12px;
}

.page-hero h1 {
  font-size: clamp(38px, 5.5vw, 68px);
  line-height: 0.98;
}

.page-hero p {
  max-width: 560px;
  margin-top: 14px;
  font-size: 16px;
  color: rgba(17, 17, 16, 0.75);
}

/* ---------- video feature layout (Game Audio page) ---------- */
/* Everything visible on one screen, no scrolling: the Reel bigger and
   highlighted up top, the other 3 projects smaller in a row underneath —
   all of it clickable straight away, nav in its normal rail on the left
   (not overlaid, since nothing here is a full-bleed backdrop anymore). */

body.video-pages {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.video-main {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: clamp(24px, 3.4vw, 40px) var(--gutter) clamp(24px, 3.4vw, 40px);
}

/* A short line of context above the grid — these pages otherwise drop a
   visitor straight into thumbnails with no write-up at all, a noticeable
   gap coming from Music/Installations, which both open with real prose.
   Kept to one short sentence and not flex-growing, since the fixed-height
   desktop layout (body.video-pages) has no scroll to spare: .video-main's
   flex children (.video-featured, .video-row) simply shrink to give this
   room instead. */
.video-page-intro {
  flex: none;
  font-family: "Inter", sans-serif;
  font-weight: 300;
  font-size: 14px;
  line-height: 1.55;
  color: rgba(17, 17, 16, 0.75);
  max-width: 60ch;
}

.video-featured {
  flex: 2 1 0;
  min-height: 0;
  position: relative;
  overflow: hidden;
  background: var(--ink);
}

.video-featured .play-btn {
  width: 46px;
  height: 46px;
  left: 22px;
  bottom: 22px;
}

.video-featured .play-btn span {
  border-top-width: 8px;
  border-bottom-width: 8px;
  border-left-width: 13px;
  margin-left: 3px;
}

.video-featured .video-thumb:hover .play-btn {
  transform: scale(1.1);
}

.video-row {
  flex: 1 1 0;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

/* Some pages don't have 3 items to fill the row yet — this widens the
   remaining cells instead of leaving an empty gap. */
.video-row.cols-2 {
  grid-template-columns: repeat(2, 1fr);
}

.video-cell {
  position: relative;
  overflow: hidden;
  background: var(--ink);
}

.video-featured .video-thumb,
.video-cell .video-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.video-featured .video-thumb img,
.video-cell .video-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Title + play button now both live in the middle of the frame, so the
   legibility treatment moves from a bottom-edge blend to a centred
   vignette — darkens the middle of the video (where the text/button sit)
   while leaving the corners clear so the thumbnail still reads. */
.video-thumb::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    ellipse 70% 60% at center,
    rgba(13, 15, 20, 0.15) 0%,
    rgba(13, 15, 20, 0.105) 40%,
    rgba(13, 15, 20, 0.05) 65%,
    transparent 100%
  );
}

/* Title + description stay hidden at rest on pointer/hover-capable
   screens (mouse) and fade in on hover — keeps the grid clean but gives
   every piece somewhere to be contextualized. Touch devices have no
   hover, so the caption just stays visible there (see media query
   below) rather than being permanently hidden behind an interaction
   nobody on that device can trigger. */
.video-caption {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2;
  max-width: 88%;
  color: var(--dark-text);
  pointer-events: none;
  text-align: center;
  text-shadow: 0 2px 20px rgba(0, 0, 0, 0.9), 0 1px 6px rgba(0, 0, 0, 0.9);
  transition: opacity 0.35s ease;
}

.video-featured .video-caption {
  font-size: clamp(33px, 3.6vw, 51px);
  transform: translate(-50%, -50%);
}

.video-caption-title {
  display: block;
}

.video-caption-sub {
  display: block;
  margin-top: 10px;
  font-family: "Inter", sans-serif;
  font-weight: 400;
  font-size: 13px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(240, 235, 224, 0.7);
}

.video-caption-desc {
  display: block;
  margin-top: 8px;
  font-family: "Inter", sans-serif;
  font-weight: 300;
  letter-spacing: 0.01em;
  color: rgba(240, 235, 224, 0.78);
}

.video-featured .video-caption-desc {
  font-size: clamp(14px, 1.1vw, 17px);
  max-width: 46ch;
  margin-left: auto;
  margin-right: auto;
}

.video-cell .video-caption {
  font-size: clamp(17px, 1.5vw, 21px);
  line-height: 1.25;
  transform: translate(-50%, -50%);
}

.video-cell .video-caption-desc {
  font-size: 12px;
  margin-top: 5px;
}

@media (hover: hover) and (pointer: fine) {
  .video-caption {
    opacity: 0;
  }

  .video-thumb:hover .video-caption,
  .video-thumb:focus-within .video-caption {
    opacity: 1;
  }
}

.video-thumb.is-playing::before,
.video-thumb.is-playing .video-caption {
  display: none;
}

/* Stop/close control shown once a thumbnail has swapped for a live
   iframe — previously the only way back to the thumbnail was a full page
   reload. Same pill language as the play button/back button elsewhere. */
.video-close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid rgba(240, 235, 224, 0.6);
  background: rgba(13, 15, 20, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.3s ease, border-color 0.3s ease;
}

.video-close:hover,
.video-close:focus-visible {
  background: rgba(13, 15, 20, 0.8);
  border-color: #f0ebe0;
}

.video-close span {
  position: relative;
  width: 14px;
  height: 14px;
  display: block;
}

.video-close span::before,
.video-close span::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 2px;
  background: #f0ebe0;
}

.video-close span::before {
  transform: rotate(45deg);
}

.video-close span::after {
  transform: rotate(-45deg);
}

/* Keyboard focus ring for the newly-focusable row cards and carousel
   wrap — deliberate accent ring instead of the default browser outline,
   so it reads as part of the design rather than a stray blue box. */
.video-cell:focus-visible,
.video-row-wrap:focus-visible,
.video-featured .video-thumb:focus-visible,
.video-featured .play-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Small external-link pair (e.g. project website / SoundCloud) that can
   sit in the corner of a featured card opposite the play button —
   pointer-events re-enabled since the caption above it is click-through. */
.video-links {
  position: absolute;
  right: 14px;
  bottom: 14px;
  left: 14px;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  pointer-events: auto;
}

.video-links a {
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dark-text);
  background: rgba(13, 15, 20, 0.5);
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(240, 235, 224, 0.4);
  transition: background 0.3s ease, border-color 0.3s ease;
}

.video-links a:hover {
  background: rgba(13, 15, 20, 0.7);
  border-color: #f0ebe0;
}

/* Smaller cells need smaller, tighter pills so a pair of links doesn't
   overrun the card. */
.video-cell .video-links {
  right: 8px;
  bottom: 8px;
  gap: 5px;
}

.video-cell .video-links a {
  font-size: 10px;
  padding: 4px 8px;
}

@media (max-width: 860px) {
  body.video-pages {
    height: auto;
    overflow: visible;
    display: block;
  }

  .video-main {
    display: block;
    /* The fixed mobile nav bar (~76px: 16px padding top/bottom either
       side of the 44px hamburger button) has no background of its own,
       so content needs enough top clearance to sit below it rather than
       under it — otherwise the dark nav-name text and toggle icon land
       directly on the featured video thumbnail with near-zero contrast. */
    padding-top: clamp(84px, 9vw, 96px);
  }

  .video-featured {
    aspect-ratio: 16 / 9;
    margin-bottom: 10px;
  }

  /* .video-main's gap is a flex-only feature — on mobile it switches to
     display:block, so the intro paragraph needs its own explicit spacing
     below it instead. */
  .video-page-intro {
    margin-bottom: 14px;
  }

  /* On mobile the featured card collapses to the exact same 16:9 shape
     as every other card (see above) — but its caption was still using
     the desktop-scale clamp(), whose vw-based minimum doesn't shrink
     enough for a narrow phone screen. That let long titles like "Michael
     Naify" wrap to two lines and push the description text down far
     enough to overlap the pill links sitting at the bottom of the same
     card. Bring it in line with .video-cell's already-correct scale so
     every card on a page reads at a consistent weight, not "one huge,
     rest small". */
  /* Match .video-cell's exact clamp() + line-height (not just a similar
     size) so the featured card and every following card render pixel-
     identical on mobile — the desktop-only size bump for the featured
     card belongs on desktop, where there's room for a real hierarchy;
     on a single mobile column, one card looking bigger than the rest
     just reads as inconsistent. */
  .video-featured .video-caption {
    font-size: clamp(17px, 1.5vw, 21px);
    line-height: 1.25;
  }

  .video-featured .video-caption-desc {
    font-size: 12px;
    max-width: 34ch;
    /* Safety net so a longer caption (e.g. Origins' full sentence) can
       never run long enough to reach the pill links sitting at the
       bottom of the same card, regardless of how much text it has —
       truncates cleanly instead of overlapping. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }

  /* The pill links (Website, SoundCloud, Photos, Videos...) kept their
     desktop size while the captions and everything around them scaled
     down for mobile, so they started reading as oversized. Shrink them
     too — moderately, not by the same ratio as the display type, since
     unlike a heading these are tap targets and need to stay usable. */
  .video-links a {
    font-size: 10px;
    padding: 5px 10px;
  }

  .video-cell .video-links a {
    font-size: 9px;
    padding: 3px 7px;
  }

  .video-row,
  .video-row.cols-2 {
    grid-template-columns: 1fr;
    gap: 10px;
  }

  .video-cell {
    aspect-ratio: 16 / 9;
  }

  /* The scrollable carousel (Installations, Music & Concerts) got its
     height from percentages cascading down a flex parent — that chain
     breaks once .video-main goes to display:block above, so pin these to
     auto and let the cells' own aspect-ratio (set just above) define the
     real height instead of an ambiguous 100%. */
  .video-row-wrap {
    flex: none;
  }

  .video-row-scroll,
  .video-row-track {
    height: auto;
  }

}

/* ---------- horizontally scrollable row (more cards than fit at once) ---------- */
/* Same visual cells as .video-row, but the track can hold more items
   than are visible — arrows (or arrow keys) shift it sideways instead
   of everything needing to fit or wrap. */
.video-row-wrap {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
}

.video-row-scroll {
  overflow: hidden;
  height: 100%;
}

.video-row-track {
  display: flex;
  gap: 10px;
  height: 100%;
  transition: transform 0.45s ease;
}

.row-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(13, 15, 20, 0.55);
  border: 1px solid rgba(240, 235, 224, 0.4);
  color: var(--dark-text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.3s ease, border-color 0.3s ease;
}

/* This must come after the base .video-row-track/.row-nav rules above —
   same class, same specificity, so source order decides the winner. On
   mobile the horizontal swipe-carousel (Installations, Music & Concerts)
   switches to the same one-column, scroll-down pattern as the plain-grid
   pages (Game Audio, Film & Documentary): sideways swiping behind small
   arrow buttons is a worse touch pattern than just scrolling, and this
   keeps all four pages consistent. JS skips setting inline
   flex-basis/transform below this breakpoint, so plain block stacking
   takes over here. */
@media (max-width: 860px) {
  .video-row-track {
    display: block;
  }

  .video-row-track .video-cell {
    margin-bottom: 10px;
  }

  .video-row-track .video-cell:last-child {
    margin-bottom: 0;
  }

  .row-nav {
    display: none;
  }
}

.row-nav svg {
  width: 16px;
  height: 16px;
  stroke: var(--dark-text);
}

.row-nav:hover {
  background: rgba(13, 15, 20, 0.85);
  border-color: #f0ebe0;
}

.row-nav.prev {
  left: 8px;
}

.row-nav.next {
  right: 8px;
}

/* ---------- work grid (homepage 2x2) ---------- */

.work-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  height: 100vh;
  min-height: 560px;
  background: var(--dark);
}

.work-cell {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line-dark);
  display: flex;
  align-items: flex-end;
  padding: var(--gutter);
}

.work-cell img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.55;
  transition: opacity 0.4s ease, transform 0.6s ease;
}

/* Music & Concerts' desktop photo is portrait — contain keeps the
   performer in frame on desktop's near-square cell. Reset to the
   default cover below (see the 720px block), since mobile swaps in a
   landscape photo there instead (via <picture>) that should fill the
   tile edge-to-edge like the other three. */
.work-cell-img-contain {
  object-fit: contain;
}

/* Restricted to devices that actually have hover (a real mouse/trackpad).
   Touch devices fire a synthetic :hover on tap right as the link
   navigates away, which read as the image flashing/swapping for an
   instant, most noticeable on the Game Audio and Film & Documentary
   tiles. Real pointer devices are unaffected. */
@media (hover: hover) and (pointer: fine) {
  .work-cell:hover img {
    opacity: 0.85;
    transform: scale(1.04);
  }
}

.work-cell::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(13, 15, 20, 0.75), transparent 55%);
  z-index: 1;
}

.work-cell-label {
  position: relative;
  z-index: 2;
  color: var(--dark-text);
  font-size: clamp(26px, 3.6vw, 48px);
}

@media (max-width: 720px) {
  /* 100vh on mobile is measured against the browser's LARGEST possible
     viewport (chrome hidden) — but on load the address bar is usually
     still showing, so the actual visible area is shorter than 100vh.
     That pushed the bottom-anchored name/tagline (and the credit) below
     the fold, needing a scroll just to read the full landing page.
     100svh is measured against the smallest guaranteed-visible viewport
     instead, so the whole hero — and everything anchored to its bottom —
     always fits without scrolling. Browsers without svh support just
     keep the 100vh from the base rule above (graceful fallback, not a
     regression). */
  .hero {
    height: 100svh;
  }

  .hero-credit {
    font-size: 9px;
  }

  /* Snapping 4 stacked cells into one screen would squash each to ~25vh —
     too cramped to read. Let the grid scroll naturally on small screens
     instead, same as it did before. */
  body.snap-home {
    scroll-snap-type: none;
  }

  .work-grid {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    height: auto;
    min-height: 0;
  }

  .work-cell {
    aspect-ratio: 4 / 3;
  }

  .work-cell-img-contain {
    object-fit: cover;
  }
}

/* ---------- interactive word field (homepage, between the hero and the
   work grid) ---------- */
/* A second full-height "page" in the same hero -> work-grid snap sequence,
   reusing the exact same word-field system as the Home/About page (same
   about-words.js, same classes) so visitors run into the interactive piece
   while scrolling down from the landing page, rather than only finding it
   via a nav click. padding-left is intentionally left unset here — the
   shared body-child rule above already clears the fixed left nav on every
   non-hero/work-grid section, so setting it again here would just fight
   that. */
.home-words {
  /* The word-note popup (.word-popup, position:absolute) is appended as
     a sibling of #wordsField — i.e. a direct child of this section — and
     needs a positioned ancestor to anchor to. Without this, it had no
     containing block short of the viewport itself and landed somewhere
     off-position instead of inside the section: the "no text box
     toggles" bug. .words-col on the Home/About page already had this;
     this section just never got it when it was added. */
  position: relative;
  height: 100vh;
  min-height: 560px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding-top: clamp(48px, 6vw, 72px);
  padding-right: var(--gutter);
  padding-bottom: clamp(36px, 4vw, 56px);
}

@media (max-width: 720px) {
  /* Matches the hero/work-grid mobile treatment just above: no forced
     full-screen snap on small screens, just a normal section you scroll
     past at your own pace. The word field falls back to its own
     min-height (240px, set on .words-field itself). */
  .home-words {
    height: auto;
    min-height: 0;
    overflow: visible;
    /* The desktop padding-top above (48px) was tuned for the left-hand
       nav rail, which takes up no vertical space at the top of the page.
       Below this breakpoint the nav collapses into a fixed, opaque top
       bar instead (~76px tall) — 48px isn't enough to clear it, so the
       "Click a word to play" hint and the mute/volume controls were
       rendering right underneath it: present and functional, just
       invisible and unclickable, sitting behind the bar. */
    padding-top: clamp(84px, 14vw, 100px);
  }
}

/* ---------- footer ---------- */

.site-footer {
  background: var(--dark);
  color: var(--dark-text);
  padding: clamp(28px, 4vw, 44px) var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.site-footer .label {
  color: rgba(240, 235, 224, 0.55);
}

/* Cream variant — for pages where the page itself is the cream/off-white
   tone (About) rather than photo/video pages, so the footer blends into
   the page instead of ending it with a hard black bar. */
.site-footer.light {
  background: var(--cream);
  color: var(--ink);
  border-top: 1px solid var(--line);
}

.site-footer.light .label {
  color: rgba(17, 17, 16, 0.55);
}

/* ---------- project grid (section pages) ---------- */

.project-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.project-card {
  background: var(--cream);
  padding: clamp(20px, 2.6vw, 36px);
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-height: 360px;
}

.project-card .thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--ink);
}

/* ---------- "page-fit" — whole page locked to one screen ---------- */
/* Opt in per-page (body.page-fit) when a section has few enough items
   that they all belong on screen at once with no scrolling — nav stays
   fixed/overlaid as usual, and the header, grid, and footer share the
   remaining height between them, with the grid taking whatever's left. */
body.page-fit {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

body.page-fit .page-hero {
  flex: 0 0 auto;
}

body.page-fit .project-grid {
  flex: 1 1 auto;
  min-height: 0;
  grid-template-columns: repeat(4, 1fr);
}

body.page-fit .project-card {
  min-height: 0;
  padding: clamp(16px, 1.6vw, 28px);
  gap: 14px;
}

body.page-fit .project-card .thumb {
  flex: 1 1 auto;
  aspect-ratio: auto;
}

body.page-fit .project-card h3 {
  flex: 0 0 auto;
  font-size: clamp(16px, 1.3vw, 20px);
}

body.page-fit .site-footer {
  flex: 0 0 auto;
  padding: clamp(14px, 1.8vw, 22px) var(--gutter);
}

/* About page uses the same one-screen lock — hero/copy/footer share the
   remaining height instead of each taking its full normal size, so the
   whole page (bio + contact) sits in view with no scrolling. */
body.page-fit .page-hero {
  padding: clamp(64px, 7vw, 92px) var(--gutter) clamp(16px, 2vw, 24px);
}

body.page-fit .page-hero h1 {
  font-size: clamp(30px, 3.6vw, 46px);
}

body.page-fit .page-hero p {
  margin-top: 8px;
  font-size: 14px;
}

body.page-fit .about-layout {
  flex: 1 1 auto;
  min-height: 0;
}

body.page-fit .about-copy {
  padding: clamp(18px, 2.6vw, 32px);
  gap: 12px;
  overflow-y: auto;
}

body.page-fit .about-copy p {
  font-size: 14.5px;
  line-height: 1.5;
}

body.page-fit .about-field {
  gap: 3px;
}

body.page-fit .about-field .eyebrow {
  font-size: 10px;
}

@media (max-width: 980px) {
  body.page-fit {
    height: auto;
    overflow: visible;
    display: block;
  }

  body.page-fit .project-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  body.page-fit .project-card .thumb {
    aspect-ratio: 4 / 3;
  }

  /* .contact-center relies on its flex parent's height to vertically
     center itself — that parent (body.page-fit) drops to display:block
     just above, so the centering trick stops giving it any top clearance.
     With nothing else pushing it down, it sits flush under the fixed
     mobile nav bar. Give it the same clearance video-pages content gets. */
  body.page-fit .contact-center {
    padding-top: clamp(84px, 9vw, 100px);
  }
}

.project-card .thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Click-to-play Vimeo thumbnails — plain image + centred play button
   until clicked, then swapped for a real iframe (see game-audio.html).
   Thumbs without a data-vimeo-id (no video yet — still photo-only) get
   the same sizing/vignette/caption treatment but none of the "this is
   clickable" affordances below. */
.video-thumb img {
  transition: transform 0.5s ease, opacity 0.3s ease;
}

.video-thumb[data-vimeo-id] {
  cursor: pointer;
}

.video-thumb[data-vimeo-id]:hover img {
  transform: scale(1.04);
  opacity: 0.85;
}

.video-thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(13, 15, 20, 0.18);
  transition: background 0.3s ease;
  pointer-events: none;
}

.video-thumb:hover::after,
.video-thumb:focus-within::after {
  background: rgba(13, 15, 20, 0.45);
}

.play-btn {
  position: absolute;
  left: 14px;
  bottom: 14px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid rgba(240, 235, 224, 0.7);
  background: rgba(13, 15, 20, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
  z-index: 1;
}

.play-btn span {
  width: 0;
  height: 0;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  border-left: 10px solid #f0ebe0;
  margin-left: 2px;
}

.video-thumb:hover .play-btn {
  transform: scale(1.1);
  background: rgba(13, 15, 20, 0.55);
  border-color: #f0ebe0;
}

.video-thumb.is-playing::after,
.video-thumb.is-playing .play-btn {
  display: none;
}

.project-card h3 {
  font-size: clamp(22px, 2.2vw, 28px);
}

.project-card .meta {
  color: rgba(17, 17, 16, 0.55);
}

.project-card .desc {
  font-size: 15.5px;
  color: rgba(17, 17, 16, 0.75);
}

@media (max-width: 980px) {
  .project-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 620px) {
  .project-grid {
    grid-template-columns: 1fr;
  }
}

/* ---------- full-bleed photo viewer (individual project galleries) ---------- */
/* Each photo runs almost full-screen, stacked and scrollable if there's
   more than one — a persistent "back" pill stays fixed in the corner so
   people always have a way out to the project card they came from. */
body.photo-viewer {
  background: var(--ink);
}

.photo-back {
  position: fixed;
  top: 20px;
  left: 20px;
  z-index: 10;
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dark-text);
  background: rgba(13, 15, 20, 0.55);
  padding: 10px 16px;
  border-radius: 999px;
  border: 1px solid rgba(240, 235, 224, 0.4);
  text-decoration: none;
  transition: background 0.3s ease, border-color 0.3s ease;
}

.photo-back:hover {
  background: rgba(13, 15, 20, 0.8);
  border-color: #f0ebe0;
}

.photo-stage {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

.photo-track {
  display: flex;
  height: 100%;
  transition: transform 0.45s ease;
}

.photo-track img {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  object-fit: contain;
  cursor: pointer;
}

/* A plain photo can carry its own caption (e.g. an upcoming-date
   placeholder) without the hover-reveal treatment used on the job
   pages — the text just sits low on the frame, always visible. */
.photo-track .caption-slide {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
}

.photo-track .caption-slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  cursor: pointer;
}

.photo-track .caption-slide .slide-caption {
  position: absolute;
  left: 50%;
  bottom: 64px;
  transform: translateX(-50%);
  text-align: center;
  max-width: 80%;
  font-family: "Cormorant Garamond", serif;
  font-style: italic;
  font-weight: 400;
  font-size: clamp(16px, 2vw, 22px);
  color: #f0ebe0;
  text-shadow: 0 2px 16px rgba(13, 15, 20, 0.8);
  pointer-events: none;
}

/* A video can sit in the same horizontal slider as the photos — reuses
   the click-to-play video-thumb pattern from the job pages, just sized
   to fill the full-bleed slide instead of a grid cell. */
.photo-track .video-slide {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  background: var(--ink);
}

.photo-track .video-slide .video-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.photo-track .video-slide .video-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.photo-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 10;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(13, 15, 20, 0.45);
  border: 1px solid rgba(240, 235, 224, 0.4);
  color: var(--dark-text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.3s ease, border-color 0.3s ease;
}

.photo-nav svg {
  width: 16px;
  height: 16px;
  stroke: var(--dark-text);
}

.photo-nav:hover {
  background: rgba(13, 15, 20, 0.75);
  border-color: #f0ebe0;
}

.photo-nav.prev {
  left: 20px;
}

.photo-nav.next {
  right: 20px;
}

.photo-counter {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.1em;
  color: rgba(240, 235, 224, 0.6);
  background: rgba(13, 15, 20, 0.45);
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(240, 235, 224, 0.3);
}

/* Stacked directly under the back pill, top-left, rather than floating
   over the vertical center of the frame — keeps it clear of the photo
   itself no matter what's in the shot. */
.photo-info {
  position: fixed;
  left: 20px;
  top: 66px;
  z-index: 10;
  max-width: 190px;
  font-family: "Inter", sans-serif;
  font-size: 12px;
  line-height: 1.6;
  letter-spacing: 0.01em;
  color: rgba(240, 235, 224, 0.75);
  text-shadow: 0 1px 8px rgba(13, 15, 20, 0.9), 0 1px 2px rgba(13, 15, 20, 0.9);
  pointer-events: none;
}

.photo-info p + p {
  margin-top: 14px;
}

.photo-info a {
  color: #f0ebe0;
  pointer-events: auto;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.photo-info a:hover {
  color: rgba(240, 235, 224, 0.75);
}

@media (max-width: 720px) {
  .photo-info {
    display: none;
  }
}

.photo-credit {
  position: fixed;
  bottom: 20px;
  left: 20px;
  z-index: 10;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: rgba(240, 235, 224, 0.55);
  background: rgba(13, 15, 20, 0.4);
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid rgba(240, 235, 224, 0.25);
  pointer-events: none;
}

/* A slide can hold a SoundCloud embed instead of a photo or video —
   cover art up top, the player pinned near the bottom of the frame. */
.photo-track .audio-slide {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  background: var(--ink);
}

.audio-slide img {
  max-width: 60vw;
  max-height: 52vh;
  object-fit: contain;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}

.audio-slide .audio-caption {
  text-align: center;
  font-family: "Cormorant Garamond", serif;
  font-style: italic;
  font-size: clamp(18px, 2.2vw, 24px);
  color: #f0ebe0;
}

.audio-slide iframe {
  width: 90vw;
  max-width: 640px;
  height: 166px;
  border: 0;
}

@media (max-width: 720px) {
  .audio-slide img {
    max-width: 78vw;
    max-height: 36vh;
  }

  .audio-slide iframe {
    height: 140px;
  }
}

.photo-empty {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Inter", sans-serif;
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(240, 235, 224, 0.45);
}

@media (max-width: 720px) {
  /* Left at the same 44px as desktop rather than shrinking further —
     that's already close to the minimum comfortable tap size, and mobile
     is exactly where precision is worst, not where it should get smaller. */
  .photo-nav {
    width: 44px;
    height: 44px;
  }
}

/* ---------- about/contact page — centered contact list ---------- */

.contact-center {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Right padding matches the rail's reserved left padding (rail width +
     gutter, not just the gutter) so the panel centers on the true page
     center instead of drifting ~110px right of it to compensate for a
     rail that isn't mirrored on the right. */
  padding: 20px calc(var(--rail-w) + var(--gutter));
}

@media (max-width: 860px) {
  /* The rail becomes a top bar below 860px, so there's no left-side
     reservation to balance anymore — back to a plain equal gutter. */
  .contact-center {
    padding-left: var(--gutter);
    padding-right: var(--gutter);
  }
}

/* Solid panel holding the text — same flat --cream used on the homepage's
   press/contact panel and the About page, so all three read as the exact
   same surface colour rather than three slightly different washes.
   Rebuilt as an asymmetric, left-aligned "spec sheet" — indexed rows
   separated by hairlines — rather than a centered stack, so it reads as
   a small designed system instead of a link-in-bio card. */
.contact-panel {
  position: relative;
  z-index: 1;
  /* A couple of percent darker than the page's own cream — just enough
     of a tonal step that the panel reads as a surface with an edge,
     rather than text floating with nothing to anchor it. Still no
     border/shadow, so it stays quiet rather than becoming a "card". */
  background: #e9e3d4;
  padding: clamp(40px, 6vw, 64px) clamp(44px, 7vw, 80px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  min-width: min(380px, 80vw);
}

.contact-group {
  display: flex;
  align-items: flex-start;
  gap: 20px;
  text-align: left;
  padding: 22px 0;
  border-bottom: 1px solid var(--line);
}

.contact-group:first-child {
  padding-top: 0;
}

.contact-group:last-child {
  padding-bottom: 0;
  border-bottom: none;
}

.contact-index {
  flex: 0 0 auto;
  padding-top: 3px;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: rgba(17, 17, 16, 0.4);
}

.contact-group-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
}

.contact-group-title {
  font-family: "Inter", sans-serif;
  font-weight: 400;
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(17, 17, 16, 0.5);
}

.contact-center a {
  display: inline-block;
  font-family: "Cormorant Garamond", serif;
  font-size: clamp(17px, 1.6vw, 20px);
  color: var(--ink);
  opacity: 0.85;
  transform-origin: left center;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.contact-center a:hover {
  opacity: 1;
  transform: scale(1.1);
}

/* ---------- about page ---------- */

.about-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
}

.about-image {
  position: relative;
  background: var(--cream);
  overflow: hidden;
}

.about-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Same feathered-edge dissolve as the homepage portrait (.feather-clear)
     — the middle stays fully crisp, and only the outer band eases away to
     transparent, revealing the --cream fill behind it so the photo blends
     into its surroundings instead of cutting off with a hard edge. Radial
     rather than left/right-only since this box needs all four sides to
     dissolve, not just the sides. */
  mask-image: radial-gradient(
    ellipse 72% 72% at center,
    black 58%,
    rgba(0, 0, 0, 0.9) 66%,
    rgba(0, 0, 0, 0.7) 74%,
    rgba(0, 0, 0, 0.45) 82%,
    rgba(0, 0, 0, 0.2) 91%,
    transparent 100%
  );
  -webkit-mask-image: radial-gradient(
    ellipse 72% 72% at center,
    black 58%,
    rgba(0, 0, 0, 0.9) 66%,
    rgba(0, 0, 0, 0.7) 74%,
    rgba(0, 0, 0, 0.45) 82%,
    rgba(0, 0, 0, 0.2) 91%,
    transparent 100%
  );
}

/* Small, unobtrusive artist credit — dark-on-cream variant of the same
   pattern used for photo credits elsewhere on the site (which are
   light-on-dark, for photo/video pages). */
.img-credit {
  position: absolute;
  right: 16px;
  bottom: 14px;
  z-index: 2;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.05em;
  color: rgba(17, 17, 16, 0.45);
}

.about-copy {
  background: var(--cream);
  padding: clamp(32px, 5vw, 80px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 22px;
}

.about-copy p {
  max-width: 520px;
  font-size: 17.5px;
  color: rgba(17, 17, 16, 0.8);
}

/* Each paragraph gets a small mono field label above it (Overview,
   Practice, Installations, Releases) — turns one unbroken block of prose
   into something closer to a technical dossier, without touching the
   actual sentences. */
.about-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.about-field .eyebrow {
  color: rgba(17, 17, 16, 0.4);
}

@media (max-width: 860px) {
  .about-layout {
    grid-template-columns: 1fr;
  }
}

/* ---------- about page, draft 2 — compact profile + field grid ---------- */
/* Alternative to the big 50/50 image/text split: a small photo paired
   with the intro line, then the remaining fields laid out as a row of
   columns underneath — closer to the "spec sheet" language used on
   Contact than to a poster diptych. */

.about-alt {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(28px, 4vw, 48px);
  padding: 0 clamp(32px, 5vw, 80px);
  overflow-y: auto;
}

.about-alt-intro {
  display: flex;
  align-items: center;
  gap: clamp(24px, 3vw, 40px);
}

.about-alt-photo {
  position: relative;
  flex: 0 0 auto;
  width: clamp(110px, 13vw, 180px);
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--cream);
}

.about-alt-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.about-alt-intro-text {
  flex: 1 1 auto;
}

.about-alt-intro-text .eyebrow {
  color: rgba(17, 17, 16, 0.45);
  margin-bottom: 8px;
}

.about-alt-intro-text p {
  max-width: 62ch;
  font-size: clamp(17px, 1.8vw, 20px);
  color: rgba(17, 17, 16, 0.8);
}

.about-alt-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--line);
}

.about-alt-grid .about-field {
  padding: 22px clamp(16px, 2vw, 28px) 4px;
  border-left: 1px solid var(--line);
  gap: 8px;
}

.about-alt-grid .about-field:first-child {
  border-left: none;
  padding-left: 0;
}

.about-alt-grid p {
  font-size: 14.5px;
  line-height: 1.55;
  color: rgba(17, 17, 16, 0.75);
}

@media (max-width: 860px) {
  .about-alt-intro {
    flex-direction: column;
    align-items: flex-start;
  }

  .about-alt-grid {
    grid-template-columns: 1fr;
    border-top: none;
  }

  .about-alt-grid .about-field {
    border-left: none;
    padding: 16px 0;
    border-top: 1px solid var(--line);
  }

  .about-alt-grid .about-field:first-child {
    border-top: none;
  }
}

/* ---------- about page, draft 3 — short intro + current/upcoming list ---------- */
/* Unlike the page-fit drafts, this one scrolls normally — a running list
   of projects will outgrow a single screen over time, so it shouldn't be
   locked to one. Each entry reuses the exact fade-up used on the nav
   links (same keyframes, same --i stagger) rather than introducing a new
   motion language just for this page. */

.about-news {
  padding: clamp(64px, 8vw, 100px) var(--gutter) clamp(80px, 10vw, 120px);
  max-width: 880px;
}

.about-news-intro {
  margin-bottom: clamp(48px, 7vw, 80px);
}

.about-news-intro .eyebrow {
  color: rgba(17, 17, 16, 0.45);
  margin-bottom: 12px;
}

.about-news-intro p {
  max-width: 68ch;
  font-size: clamp(18px, 2vw, 22px);
  line-height: 1.55;
  color: rgba(17, 17, 16, 0.85);
}

.news-list-title {
  color: rgba(17, 17, 16, 0.45);
  margin-bottom: 4px;
}

.news-item {
  display: flex;
  gap: clamp(20px, 3vw, 40px);
  padding: 20px 0;
  border-bottom: 1px solid var(--line);
  animation: fade-up 0.6s ease backwards;
  animation-delay: calc(0.07s * var(--i, 0) + 0.1s);
}

.news-item:first-of-type {
  border-top: 1px solid var(--line);
  margin-top: 18px;
}

.news-date {
  flex: 0 0 auto;
  width: 130px;
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(17, 17, 16, 0.55);
  padding-top: 2px;
}

.news-title {
  font-family: "Cormorant Garamond", serif;
  font-size: clamp(19px, 2vw, 23px);
  color: var(--ink);
}

.news-title a {
  border-bottom: 1px solid var(--line);
  transition: border-color 0.2s ease, opacity 0.2s ease;
}

.news-title a:hover {
  border-color: var(--ink);
}

.news-loc {
  margin-top: 3px;
  font-size: 14px;
  color: rgba(17, 17, 16, 0.6);
}

/* A few entries have more than one place to point to (e.g. an
   institution, a virtual walkthrough, a collaborator's own site) —
   small text links in a row rather than overloading the single title
   link. */
.news-links {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin-top: 6px;
}

.news-links a {
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.03em;
  color: rgba(17, 17, 16, 0.55);
  border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.news-links a:hover {
  color: var(--ink);
  border-color: var(--ink);
}

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

@media (max-width: 640px) {
  .news-item {
    flex-direction: column;
    gap: 4px;
  }

  .news-date {
    width: auto;
  }
}

/* ---------- standalone News page ---------- */
/* The same news list/item styling as before, just no longer sharing a
   page with the word field — a plain scrolling page (no page-fit lock)
   since the list is meant to keep growing over time. */
.news-page {
  padding: clamp(64px, 8vw, 100px) var(--gutter) clamp(80px, 12vw, 160px);
}

/* One-screen fit, same idea as the old about-page-fit variant: the page
   itself is locked to the viewport (see body.page-fit), and .news-col
   gets its own internal scroll (via the existing body.page-fit .news-col
   rule further below) so a growing list never breaks the one-screen
   layout — it just scrolls within its own box instead of the page
   scrolling. Reverts to a normal scrolling page on small screens, same
   breakpoint as body.page-fit's own mobile override. */
body.page-fit .news-page {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding-top: clamp(48px, 6vw, 72px);
  padding-bottom: clamp(28px, 3.4vw, 44px);
}

/* max-width + auto margins live on the list itself, not .news-page —
   .news-page spans the full column beside the nav rail, and this centers
   the (narrower, more readable) list within that column instead of it
   hugging the rail with a lopsided gap on the right. */
.news-col {
  max-width: 760px;
  margin: 0 auto;
}

.news-col .eyebrow {
  color: rgba(17, 17, 16, 0.45);
  margin-bottom: 18px;
}

/* ---------- about page, draft 4 — News (left) / About (right) split ---------- */
/* Two columns sharing one scrolling page: the running news list on the
   left, the existing photo + labeled bio fields on the right — but the
   photo+fields now stack vertically in their own column instead of
   spanning the old half-page image/text split. */

.news-about-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(40px, 5vw, 80px);
  padding: clamp(64px, 8vw, 100px) var(--gutter) clamp(80px, 10vw, 120px);
  align-items: start;
}

.news-about-col .eyebrow {
  color: rgba(17, 17, 16, 0.45);
  margin-bottom: 18px;
}

/* Was :first-of-type, but that matches the first <div> among siblings
   regardless of class — once the "News & Upcoming" eyebrow label (also a
   div) was added before the list, it became the true first-of-type div
   and silently stole this rule from the first news item. Target the
   item right after the eyebrow instead, so it's robust either way. */
.news-col .eyebrow + .news-item {
  border-top: 1px solid var(--line);
  margin-top: 4px;
}

.about-vert-photo {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  background: var(--cream);
  overflow: hidden;
  margin-bottom: clamp(28px, 4vw, 44px);
}

.about-vert-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  mask-image: radial-gradient(
    ellipse 72% 72% at center,
    black 58%,
    rgba(0, 0, 0, 0.9) 66%,
    rgba(0, 0, 0, 0.7) 74%,
    rgba(0, 0, 0, 0.45) 82%,
    rgba(0, 0, 0, 0.2) 91%,
    transparent 100%
  );
  -webkit-mask-image: radial-gradient(
    ellipse 72% 72% at center,
    black 58%,
    rgba(0, 0, 0, 0.9) 66%,
    rgba(0, 0, 0, 0.7) 74%,
    rgba(0, 0, 0, 0.45) 82%,
    rgba(0, 0, 0, 0.2) 91%,
    transparent 100%
  );
}

.about-col p {
  font-size: 16px;
  max-width: 48ch;
  color: rgba(17, 17, 16, 0.8);
}

@media (max-width: 900px) {
  .news-about-split {
    grid-template-columns: 1fr;
  }

  /* Visually swap the stacking order only — News stays first in the HTML
     (so reading/tab order is unaffected) but the words field, being the
     actual "About" content, shows up first on a phone instead of sitting
     below a full screen of news before anyone scrolls to it. */
  .news-col {
    order: 2;
  }

  .words-col {
    order: 1;
  }
}

/* page-fit variant — no photo, About condensed to a single short blurb,
   so it can sit beside the news dates on one screen. The page itself is
   locked (no scroll); the news column keeps its own internal scroll
   instead, so the list can keep growing over time without breaking the
   one-screen layout. */
body.page-fit .news-about-split {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  align-items: stretch;
  padding-top: clamp(48px, 6vw, 72px);
  padding-bottom: clamp(28px, 3.4vw, 44px);
}

body.page-fit .about-col {
  align-self: start;
}

body.page-fit .news-col {
  height: 100%;
  overflow-y: auto;
  /* Default OS scrollbar renders as a thick gray bar right in the gap
     between the two columns — replace it with a thin, quiet line that
     matches the rest of the site instead. */
  scrollbar-width: thin;
  scrollbar-color: rgba(17, 17, 16, 0.2) transparent;
}

body.page-fit .news-col::-webkit-scrollbar {
  width: 4px;
}

body.page-fit .news-col::-webkit-scrollbar-track {
  background: transparent;
}

body.page-fit .news-col::-webkit-scrollbar-thumb {
  background: rgba(17, 17, 16, 0.2);
  border-radius: 999px;
}

body.page-fit .news-col::-webkit-scrollbar-thumb:hover {
  background: rgba(17, 17, 16, 0.35);
}

body.page-fit .about-col p {
  font-size: 15px;
  line-height: 1.55;
}

@media (max-width: 900px) {
  body.page-fit {
    height: auto;
    overflow: visible;
    display: block;
  }

  body.page-fit .news-col {
    height: auto;
    overflow-y: visible;
  }
}

/* ---------- about page, draft 5 — News (left) / floating word field (right) ---------- */
/* Same News column as draft 4, but the About text is replaced with loose
   single words/phrases that drift and bounce off each other and the
   container walls (see js/about-words.js). The full bio stays in the DOM
   as visually-hidden text for screen readers and search engines — the
   word field itself is aria-hidden and purely decorative. */

.words-col {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}

.words-field {
  position: relative;
  flex: 1 1 auto;
  min-height: 240px;
  overflow: hidden;
}

.word-particle {
  position: absolute;
  top: 0;
  left: 0;
  font-family: "Cormorant Garamond", serif;
  font-style: italic;
  font-weight: 400;
  color: rgba(17, 17, 16, 0.72);
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  will-change: transform;
}

.word-particle--big {
  color: rgba(17, 17, 16, 0.85);
}

/* Set (see js/about-words.js) on every word, one or the other — sound
   words bold and upright, text-only note words left in the field's
   original italic. Feedback was that visitors clicked a silent word
   expecting sound (the header hint promised "play") and read the
   resulting quiet as broken rather than as "this one's a note." A
   typographic split (not a new color or icon) tells the two apart before
   a first click, not just after one, while the field stays visually
   quiet until a word is actually active. */
.word-particle--sound {
  font-style: normal;
  font-weight: 700;
}

/* Color is the signal that actually reads at a glance on floating, moving
   text — italic alone turned out too subtle to notice before clicking.
   Note words are faded back to ~55% (matching .release-credits' quiet
   sub-caption look) so they visually recede as "secondary" against the
   full-strength ink of the sound words, on top of the italic/weight
   difference rather than instead of it. */
.word-particle--note {
  font-style: italic;
  font-weight: 400;
  color: rgba(17, 17, 16, 0.55);
}

/* italy/berlin on desktop: a sound layer AND a note together, so neither
   the plain sound nor the plain note style alone would be honest — bold
   italic keeps it legible as its own third category rather than reading
   as a plain sound word that happens to also (surprisingly) show text.
   Desktop only: on mobile these two behave as plain sound toggles (no
   note ever opens there, see isMobile in about-words.js), so they're
   classed as .word-particle--sound instead on that breakpoint. */
.word-particle--hybrid {
  font-style: italic;
  font-weight: 700;
}

/* Every word in the field doubles as a sound-layer toggle — override the
   base .word-particle's pointer-events: none (needed so a merely-visual
   word never intercepts clicks meant for the page behind it) so words can
   actually be clicked at all. Hovering hints that they're interactive,
   the same idea as a link changing on approach. */
.word-particle--playable {
  pointer-events: auto;
  cursor: pointer;
  transition: color 0.2s ease;
}

.word-particle--playable:hover {
  color: var(--ink);
}

/* Shown while a word's sound layer is switched on — same warm accent the
   left nav uses to mark the current page, so the same color means "this
   is active" consistently across the site rather than introducing a
   second visual language just for the word field. */
.word-particle--palette-active,
.word-particle--playable:hover.word-particle--palette-active {
  color: var(--accent);
}

/* Shown while a word's text note is open — same accent as the audio
   words' active state, so "this word did something" reads consistently
   whether what it did was start a sound or open a note. */
.word-particle--text-active,
.word-particle--playable:hover.word-particle--text-active {
  color: var(--accent);
}

/* The note itself — a single reusable box (see js/about-words.js),
   positioned in JS via left/top rather than the field's own transform
   system, and floated just above whichever word opened it. A soft
   shadow rather than a heavier card treatment, since it's sitting on the
   same cream as the page behind it and needs a little separation to read
   as "floating" rather than just another line of text. */
.word-popup {
  position: absolute;
  display: none;
  /* A real width, not max-width — with only "left" set (no "right"), an
     absolutely-positioned box with an auto/max-width shrinks to whatever
     space is left between "left" and its containing block's right edge,
     which made the box (and therefore its height, since text reflows)
     balloon unpredictably whenever a word floated near the right side.
     A fixed width sidesteps that entirely; min() keeps it from
     overflowing narrow viewports on its own. */
  width: min(240px, calc(100vw - 20px));
  padding: 10px 13px;
  background: var(--cream);
  border: 1px solid var(--line);
  box-shadow: 0 6px 20px rgba(17, 17, 16, 0.08);
  font-family: "Inter", sans-serif;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink);
  pointer-events: none;
  z-index: 6;
}

/* The popup itself is pointer-events:none (clicks are meant to pass
   through to the field behind it) — this re-enables interaction just for
   the "let's work together" note's Contact link, the one case where the
   popup needs to be clickable. */
.word-popup-link {
  display: inline-block;
  margin-top: 4px;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  pointer-events: auto;
  cursor: pointer;
}

/* A quiet invitation on "play" specifically, before anyone's clicked
   anything yet. Bold on its own via CSS; the actual pulse (a gentle grow
   and shrink, the same visual language as hovering any other word) is
   driven from the JS animation loop instead of a CSS animation — that
   loop already rewrites transform/scale on every frame (see tick() in
   about-words.js), so a CSS transform animation here would just get
   overwritten and never actually show. Respects prefers-reduced-motion
   the same way the rest of that loop already does (see reduceMotion in
   about-words.js), so no separate override is needed here. */
.word-particle--invite {
  font-weight: 700;
}

/* Top row of the words column — the "click a word to play" hint plus the
   mute toggle and volume slider share one line, same position as the News
   column's "News & Upcoming" label, so the two columns read as a matched
   pair rather than the sound controls feeling bolted on somewhere else. */
.words-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
}

.words-header .eyebrow {
  margin-bottom: 0;
}

/* Only the word "play" inside the hint stays highlighted — same accent
   color as a clicked/active word (.word-particle--palette-active,
   .word-particle--text-active below) — and doubles as a shortcut that
   activates the real "play" particle in the field (see about-words.js). */
.eyebrow-play {
  color: var(--ink);
  font-weight: 700;
  cursor: pointer;
}

/* The hint's other half ("...or read") — italic, matching the note words'
   own styling (.word-particle--note above), so the hint's own typography
   previews the same bold/italic split the words themselves use. Sized
   down too, the same quieter feel as the note text itself (.word-popup)
   next to everything else on the page. */
.eyebrow-read {
  font-style: italic;
  font-size: 10px;
  color: rgba(17, 17, 16, 0.55);
  text-transform: none;
}

/* Mobile never opens a note (see isMobile in about-words.js) — the field
   there is 100% sound, so promising "or read" would be inaccurate. Below
   720px the hint quietly drops back to just "Click a word to play". */
@media (max-width: 720px) {
  .eyebrow-read-wrap {
    display: none;
  }
}

.words-header-left {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* Reset loops / Randomize — set in the same italic serif as the small
   floating words themselves (not the eyebrow's uppercase Inter), so they
   read as belonging to the word field rather than as more hint text. */
.words-action {
  padding: 0;
  border: none;
  background: none;
  font-family: "Cormorant Garamond", serif;
  font-style: italic;
  font-weight: 400;
  font-size: 15px;
  color: rgba(17, 17, 16, 0.55);
  cursor: pointer;
  transition: color 0.2s ease;
}

.words-action:hover,
.words-action:focus-visible {
  color: var(--accent);
}

.words-controls-left {
  display: flex;
  align-items: center;
  gap: 9px;
}

.sound-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  background: none;
  color: rgba(17, 17, 16, 0.45);
  cursor: pointer;
  transition: color 0.2s ease;
}

.sound-toggle:hover,
.sound-toggle:focus-visible {
  color: var(--ink);
}


.sound-toggle .sound-mute-x {
  display: none;
}

.sound-toggle--muted .sound-wave {
  display: none;
}

.sound-toggle--muted .sound-mute-x {
  display: inline;
}

.sound-volume {
  -webkit-appearance: none;
  appearance: none;
  width: 56px;
  height: 2px;
  background: rgba(17, 17, 16, 0.22);
  border-radius: 999px;
  outline: none;
  cursor: pointer;
}

.sound-volume::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: rgba(17, 17, 16, 0.55);
  cursor: pointer;
  transition: background 0.2s ease;
}

.sound-volume:hover::-webkit-slider-thumb,
.sound-volume:focus-visible::-webkit-slider-thumb {
  background: var(--accent);
}

.sound-volume::-moz-range-thumb {
  width: 9px;
  height: 9px;
  border: none;
  border-radius: 50%;
  background: rgba(17, 17, 16, 0.55);
  cursor: pointer;
}

.sound-volume::-moz-range-track {
  height: 2px;
  background: rgba(17, 17, 16, 0.22);
  border-radius: 999px;
}

@media (max-width: 900px) {
  .words-col {
    height: auto;
  }

  .words-field {
    height: 340px;
  }

  /* The header row has more in it now (hint + two small actions + mute +
     volume) than fits one line comfortably at phone widths — let it wrap
     to a second line rather than squeezing or overflowing. */
  .words-header {
    flex-wrap: wrap;
    row-gap: 8px;
  }

  .words-header-left {
    flex-wrap: wrap;
    row-gap: 6px;
  }
}

/* ---------- release stack (Music & Concerts) ---------- */
/* Vertical scroll of releases/performances, each its own block: one big
   image or video up top, then a text column underneath with a meta line,
   a short write-up, an inline SoundCloud/Bandcamp player where one
   exists, and a small row of links reserved for things that are
   genuinely separate from the listening experience itself (a label site,
   a distributor, a photo set). Replaces the old featured-card + carousel
   layout, which hid everything but the top item behind hover/clicks. */
body.release-page-body {
  height: auto;
  overflow: visible;
  display: block;
}

.release-list {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: clamp(64px, 8vw, 100px) var(--gutter) clamp(80px, 10vw, 140px);
}

/* Sticky jump-nav across the top of a release list (Installations, Music
   & Concerts) — lets a visitor halfway down a long page reach any other
   section in one click/tap instead of scrolling back up past everything
   they've already seen. Sits inside .release-list (not the page body)
   specifically so it inherits the same left padding as every article
   below it for free, and the same mobile top-clearance the list already
   reserves for the fixed nav bar. */
.section-nav {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  gap: clamp(16px, 2.4vw, 28px);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding: 14px 0;
  margin-bottom: clamp(28px, 3.4vw, 44px);
  background: var(--cream);
  border-bottom: 1px solid var(--line);
}

.section-nav::-webkit-scrollbar {
  display: none;
}

.section-nav a {
  flex: none;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(17, 17, 16, 0.5);
  white-space: nowrap;
  padding-bottom: 3px;
  border-bottom: 1px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.section-nav a:hover {
  color: var(--ink);
}

/* Set by js/section-nav.js as whichever article is currently in view. */
.section-nav a.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

@media (max-width: 860px) {
  /* Matches the fixed mobile top bar's own height (16px padding either
     side of the 44px hamburger button) so the sticky strip stops just
     below it instead of sliding underneath. */
  .section-nav {
    top: 76px;
  }
}

.release-list-header {
  margin-bottom: 8px;
}

.release-list-header .eyebrow {
  color: rgba(17, 17, 16, 0.45);
}

/* Italy Segreta's still carries "Artistic Direction by Italy Segreta"
   baked into the frame — rather than let both that text and our own logo
   compete on the same photo, a solid plate blots the baked-in line out
   entirely, with the curator's own wordmark (cut to transparent,
   recolored white) and the release date stacked centered on top of it
   instead. Sized/positioned by eye against this specific still, so it'll
   need re-checking if the photo ever changes. */
.release-media-cover {
  position: absolute;
  left: 51%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 41%;
  min-width: 210px;
  padding: clamp(6px, 0.8vw, 10px) clamp(10px, 1.4vw, 16px);
  background: var(--ink);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}

.release-logo {
  display: block;
  height: clamp(11px, 1.5vw, 16px);
  width: auto;
}

.release-media-overlay-caption {
  font-family: "Inter", sans-serif;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff;
  margin: 0;
  white-space: nowrap;
}

.release {
  padding: clamp(40px, 5vw, 64px) 0;
  border-bottom: 1px solid var(--line);
  animation: fade-up 0.6s ease backwards;
  animation-delay: calc(0.06s * var(--i, 0) + 0.1s);
}

.release:first-of-type {
  border-top: 1px solid var(--line);
  margin-top: 18px;
}

.release-media {
  position: relative;
  overflow: hidden;
  background: var(--ink);
  aspect-ratio: 16 / 9;
}

/* Opt-in for a header photo shot portrait rather than landscape (e.g.
   Live Performance's Silent Green shot) — the shared 16:9 default is a
   landscape shape, so cover would crop a portrait photo down to a thin
   sliver of itself. Applies at every width, not just desktop: mobile
   headers fall back to this same 16:9 default too, and a portrait photo
   is exactly as wrong-shaped for it there as it is on desktop. */
.release--media-portrait > .release-media {
  aspect-ratio: 4 / 5;
}

/* A header video's 16:9 box crops fairly hard once the same shape gets
   squeezed down to a phone's width — there just isn't much height left to
   work with, and object-fit: cover eats into the frame more than it does
   on a wider desktop box. Going square on mobile only gives the video
   noticeably more vertical room and cuts a lot less out of the picture.
   Desktop keeps the wider 16:9 banner shape untouched. */
@media (max-width: 720px) {
  .release--hero .release-media[data-vimeo-id],
  .release--hero .release-media[data-youtube-id] {
    aspect-ratio: 1 / 1;
  }
}

/* A tiny attribution note pinned into the video's own corner (e.g. Reel's
   "footage belongs to the credited studios") rather than sitting in the
   text flow below — keeps it visually tied to the video it's crediting
   instead of reading as part of the write-up. Text-shadow instead of a
   background plate since the video thumbnail underneath varies in
   brightness shot to shot. */
.release-media-credit {
  position: absolute;
  right: clamp(10px, 1.4vw, 16px);
  bottom: clamp(8px, 1.2vw, 14px);
  margin: 0;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: rgba(240, 235, 224, 0.85);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
  pointer-events: none;
}

/* A commissioning partner's wordmark watermarked into the corner of their
   own header photo/video (e.g. Italy Segreta) rather than replacing the
   entry's own title treatment. Small and quiet, drop-shadowed so it holds
   up over any photo brightness, never intercepting clicks meant for the
   media itself. */
.release-media img.release-media-logo {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  height: clamp(20px, 2.4vw, 30px);
  width: auto;
  object-fit: contain;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.7));
  pointer-events: none;
}

/* The YouTube/Vimeo Player SDK (see playThumb in release-video.js) injects
   its own iframe into the .host div at play time, sized to the platform's
   own pixel default (e.g. Vimeo's 640x360) rather than the box it landed
   in. Without this, every embed past the first one on a page renders as a
   small fixed-size player pinned to the top-left corner of the black
   .release-media box instead of filling it. */
.release-media iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Tapping a video on mobile promotes its .release-media box to a fixed,
   full-viewport overlay (see playThumb's mobile-fullscreen branch in
   release-video.js) instead of playing inline in its normal banner-shaped
   box. The box itself drops the 16:9 crop that works fine on desktop but
   was cutting into the picture on a phone; the iframe/host inside keeps
   its native 16:9 shape and is centered with letterboxing rather than
   stretched, so nothing gets cropped. Body scroll is locked the same way
   the photo lightbox does (see body.lightbox-locked) so the page behind
   it can't scroll while a video covers the screen. Desktop is untouched
   — its rectangular banner treatment is unaffected by any of this. */
@media (max-width: 720px) {
  .release-media.is-mobile-fullscreen {
    position: fixed !important;
    inset: 0 !important;
    z-index: 1000;
    width: 100vw !important;
    height: 100dvh !important;
    aspect-ratio: auto !important;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .release-media.is-mobile-fullscreen .video-host,
  .release-media.is-mobile-fullscreen iframe {
    position: relative !important;
    inset: auto !important;
    width: 100% !important;
    height: auto !important;
    aspect-ratio: 16 / 9;
    max-height: 100dvh;
  }
}

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

/* Opt-in for hero art that isn't a landscape photo (e.g. square cover
   art) — cover would zoom hard into a square image to fill the same wide
   box a photo uses, cropping most of it away. Contain shows the whole
   thing letterboxed on cream instead. */
.release-media.contain {
  background: var(--cream);
}

.release-media.contain img {
  object-fit: contain;
}

/* Every video on Game Audio (Reel, Keres's two, FMOD, Stealth) shares
   this same box now — full width, contain rather than cropped, all at
   the same size so nothing on the page reads as more or less important
   than the rest. !important since FMOD/Stealth/Reel are .release--hero
   (2.3/1 by default, more specific than a single class) and need to
   match this ratio instead of their own hero default. */
.release-media--video-fit {
  aspect-ratio: 2 / 1 !important;
  background: var(--ink);
}

.release-media--video-fit img {
  object-fit: contain !important;
}

/* Video entries reuse .video-thumb (vignette), .play-btn and .video-close
   from the video-pages styles above — those rules are self-contained
   (they don't require a .video-featured/.video-cell ancestor), so
   stacking the class here on .release-media gets the same look for free. */
.release-media.video-thumb {
  cursor: pointer;
}

/* This page's write-ups already say what everything is in plain text
   right below it, so the hover-reveal title/vignette that the shared
   .video-thumb styles use elsewhere (featured cards with no caption of
   their own) is just redundant here — switch it off for this page only,
   the play button itself is enough of a hover affordance. */
.release-media.video-thumb::before,
.release-media.video-thumb .video-caption {
  display: none;
}

.release-body {
  margin: clamp(20px, 2.6vw, 28px) 0 0;
}

/* Reusable text+media pairing, similar in spirit to .release--split but
   usable more than once inside a single article (e.g. Keres A-RPG's two
   text+pictures rows) rather than being tied to the article's own
   top-level grid. Stacks on mobile like every other split layout; add
   .reverse to swap which side the media sits on. Equal columns and text
   vertically centered against the pictures beside it. */
.release-subrow {
  margin-top: clamp(28px, 3.6vw, 48px);
}

.release-subrow-content {
  margin-top: clamp(16px, 2vw, 24px);
}

/* The picture side stacks its photos vertically rather than reusing
   .photo-masonry's own 2-up grid — halved again by the subrow's own
   column, a 2-up grid inside that would leave each photo quarter-width. */
.release-subrow-media .photo-masonry {
  grid-template-columns: 1fr;
}

@media (min-width: 860px) {
  .release-subrow {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: clamp(28px, 3.6vw, 56px);
    align-items: center;
  }

  .release-subrow-content {
    margin-top: 0;
  }

  /* Keres's Cleave/Frost Nova pair (--big) — stacked in a column roughly
     half the article's width, each photo would otherwise render notably
     smaller than the standalone attenuation screenshots later on the same
     page, which get the article's full width to themselves. Skewing the
     columns here closes most of that gap. */
  .release-subrow-media--big {
    grid-column: span 1;
  }

  .release-subrow:has(.release-subrow-media--big) {
    grid-template-columns: 0.8fr 1.2fr;
  }

  /* Keres's attenuation screenshots are much taller per unit width than
     the Cleave/Frost Nova pair (portrait-ish Unreal panels vs. wide
     Ableton timelines), so left uncropped at a full grid-column width
     they still render much bigger than that other pair even without
     --big. Capping the box itself shrinks them to match without cropping
     any of the screenshot out. */
  .release-subrow-media--small {
    max-width: 55%;
    margin-left: auto;
  }

  /* Game Testing and Feedback's screenshot is a plain 16:9 gameplay shot,
     not a tall Unreal panel like Attenuation's, so the same 55% cap reads
     needlessly small next to its paragraph. A slightly larger cap just for
     this one instance. */
  #game-testing-feedback .release-subrow-media--small {
    max-width: 66%;
    margin-left: 0;
  }

  /* Reverse swaps which side the pictures sit on via order — the DOM
     keeps text first either way (reading order/no-JS/screen readers),
     only the visual side flips. Pictures need the LOWER order value to
     actually land in the first (left) column; text otherwise defaults to
     order 0 same as pictures, and ties resolve to DOM order (text first,
     i.e. no visible swap at all) — which is exactly the bug this fixes. */
  .release-subrow.reverse .release-subrow-media {
    order: 1;
  }

  .release-subrow.reverse .release-subrow-content {
    order: 2;
  }
}

/* Text stays capped at a readable width even though .release-body itself
   now runs full width (so embeds/photos below can go just as big as the
   hero image up top instead of being boxed into the text column). */
.release-meta,
.release-title,
.release-text {
  max-width: 640px;
}

/* Opt-out for a couple of write-up paragraphs in game-audio.html's Keres
   section that read better running the full width of .release-body rather
   than boxed into the standard 640px text column. Doubled-up selector
   (.release-text.release-text--wide) so this beats the later, more
   specific-by-cascade-order `.release-text { max-width: 62ch; }` rule
   further down this file. */
.release-text.release-text--wide {
  max-width: none;
}

.release-meta {
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(17, 17, 16, 0.5);
  margin-bottom: 8px;
}

.release-meta a {
  color: inherit;
  border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.release-meta a:hover {
  color: var(--ink);
  border-color: var(--ink);
}

.release-title {
  font-size: clamp(26px, 3vw, 38px);
  line-height: 1.15;
  color: var(--ink);
  margin-bottom: 14px;
}

.release-text {
  font-family: "Inter", sans-serif;
  font-weight: 300;
  font-size: 15px;
  line-height: 1.65;
  color: rgba(17, 17, 16, 0.8);
  max-width: 62ch;
  /* No margin left implicit: without this, a <p> falls back to the
     browser's own default (~1em, relative to ITS font-size, on both top
     and bottom), which is invisible when a release-text happens to be
     last/first in a column but stacks unpredictably with a neighboring
     element's own explicit margin the moment the surrounding layout
     changes (this is exactly what inflated the gap around Reel's credits
     line above). Spacing above one of these is instead handled by the
     single shared rule below, same as every other content block. */
  margin: 0;
}

/* One shared vertical rhythm for every kind of content block inside a
   release's write-up (paragraphs, photo pairs, embeds, quotes, subrows,
   credits...) so a heading-to-text gap, a text-to-picture gap and a
   picture-to-picture gap all read as the same size, rather than each
   component quietly carrying its own slightly different margin (or, for
   a few like .photo-masonry and .release-quote, no defined gap to
   whatever follows them at all). !important because this is meant to be
   the one rule governing this spacing everywhere, not something any
   individual component's own margin-top should be able to quietly
   override again later. */
.release-intro > * + *,
.release-body > * + *,
.release-statement > * + *,
.release-subrow-content > * + * {
  margin-top: clamp(20px, 2.4vw, 28px) !important;
}

/* A .release-title used inside a .release-embed (Keres's "SFX variety
   matters" video, which gets its own title beside the video rather than
   only a plain caption) still needs the standard gap above it — unlike
   .release-embed-caption, which stays deliberately tight against the
   video it's captioning, a full title here reads as its own content
   block and should match every other heading-after-media gap. */
.release-embed > .release-title {
  margin-top: clamp(20px, 2.4vw, 28px) !important;
}

/* Two paragraphs back to back are a continuous train of thought rather
   than a change of content type (text to picture, heading to text) — a
   tighter gap here reads as "still talking," while the shared rule above
   still governs the bigger gap on either side of this pair. Higher
   specificity than the shared rule above so it wins despite both being
   !important. */
.release-text + .release-text {
  margin-top: 14px !important;
}

/* A handful of one-off radio/guest-mix entries condensed under a single
   "Radio & Guest Mixes" heading rather than each getting a full .release
   article — every mix still gets its own sub-heading, write-up and
   embed, just without repeating the section eyebrow and heading. */
.release-mix {
  margin-top: clamp(28px, 3.6vw, 40px);
}

.release-mix:first-of-type {
  margin-top: clamp(20px, 2.6vw, 28px);
}

.release-mix-title {
  font-size: clamp(19px, 2vw, 23px);
  color: var(--ink);
  margin-bottom: 8px;
}

/* A self-hosted track (game jam SFX, no SoundCloud/Bandcamp release to
   embed instead) — same width and placement as the .release-embed
   iframes it sits alongside, just the browser's own native player rather
   than a third-party one. */
.release-audio {
  display: block;
  width: 100%;
  max-width: 640px;
  height: 36px;
}

/* Split layout (desktop only, opt-in via .release--split) — hero image
   pinned to one side, meta/title/write-up beside it instead of stacked
   underneath. Modeled on pablodiserens.studio's project pages: the image
   runs at a sane, fixed proportion of the viewport instead of full width,
   which is also what fixes the "big images lose quality" problem, since
   they're no longer stretched to var(--max-w). Everything after the
   write-up (embeds, photo sets, credits, links) drops to its own
   full-width row below both columns, same as before. Mobile/tablet is
   unaffected: media, intro and body just stack in document order. */
.release-intro {
  margin: clamp(20px, 2.6vw, 28px) 0 0;
}

@media (min-width: 960px) {
  .release--split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(32px, 4vw, 64px);
    align-items: start;
  }

  .release--split > .release-media {
    grid-column: 1;
    grid-row: 1;
    aspect-ratio: 4 / 5;
  }

  .release--split .release-intro {
    grid-column: 2;
    grid-row: 1;
    margin-top: 0;
    align-self: start;
  }

  .release--split .release-intro .release-meta,
  .release--split .release-intro .release-title,
  .release--split .release-intro .release-text {
    max-width: none;
  }

  .release--split .release-body {
    grid-column: 1 / -1;
    grid-row: 2;
    margin-top: clamp(32px, 4vw, 56px);
  }

  /* Alternate which side the image sits on per entry via this second
     modifier, the way a real editorial layout would avoid repeating the
     same image-left rhythm down the whole page. */
  .release--split.release--media-right .release-media {
    grid-column: 2;
  }

  .release--split.release--media-right .release-intro {
    grid-column: 1;
  }

  /* Opt-in per entry (e.g. Live Performance): meta/title/text centered
     against the full height of the header photo beside it, instead of
     pinned to its top edge. */
  .release--split.release--intro-center .release-intro {
    align-self: center;
  }
}

/* Hero layout (desktop only, opt-in via .release--hero) — the live
   site's Kind Stranger treatment: full-width photo on top (same as the
   plain, unsplit .release-media default), then the write-up and the
   embed sitting side by side underneath as two columns, instead of
   either stacking or sitting beside the image. Mobile/tablet: media,
   text and embed just stack in document order, same as any other
   .release. */
/* Kind Stranger's cover photo is a near-square shot of the record itself,
   not a wide scene — the shared .release-media default (aspect-ratio
   16/9, object-fit: cover) crops hard into it. The live site avoids that
   with object-fit: contain (the whole photo, letterboxed on ink) instead
   of cropping to fill; matched here exactly. */
.release--hero > .release-media {
  aspect-ratio: 2.3 / 1;
  background: var(--ink);
}

.release--hero > .release-media img {
  object-fit: contain;
}

.release-columns .release-intro {
  margin-top: 0;
}

.release-columns {
  margin-top: clamp(20px, 2.6vw, 28px);
}

@media (min-width: 960px) {
  .release--hero .release-columns {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(32px, 4vw, 64px);
    align-items: start;
  }

  .release--hero .release-columns .release-intro .release-meta,
  .release--hero .release-columns .release-intro .release-title,
  .release--hero .release-columns .release-intro .release-text {
    max-width: none;
  }

  /* Origins (no .release-columns split — everything runs full width so the
     write-up and its embed aren't squeezed into a narrow half-column). */
  .release--hero > .release-intro .release-meta,
  .release--hero > .release-intro .release-title,
  .release--hero > .release-intro .release-text {
    max-width: none;
  }

  .release--hero .release-columns > .release-embed {
    margin-top: 0;
  }
}

/* Repeating text/image row — alternates a short paragraph with a single
   supporting photo (or a small pair, via the --pair modifier) instead of
   stacking all the write-up above a photo grid. Desktop-only two-column
   split; mobile just stacks paragraph then photo like everything else on
   the page. Source order already puts text first, so the default grid
   placement already lands text on the left, photo on the right — no
   explicit column assignment needed. */
.text-image-row {
  margin-top: clamp(20px, 2.6vw, 28px);
}

.text-image-row .release-text {
  margin-top: 0;
}

.text-image-row-media {
  margin-top: clamp(14px, 2vw, 20px);
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(10px, 1.4vw, 16px);
}

.text-image-row-media.pair {
  grid-template-columns: repeat(2, 1fr);
}

/* Direct child only — a plain photo sits right inside this wrapper, but a
   video-thumb (e.g. the 4D Sound video swapped into one of these rows)
   nests its own <img> a level deeper inside .release-media, which already
   has its own fill/cover sizing that this rule would otherwise clobber
   (same specificity, later in the cascade). */
.text-image-row-media > img {
  width: 100%;
  height: auto;
  display: block;
}

@media (min-width: 960px) {
  .text-image-row {
    display: grid;
    grid-template-columns: minmax(160px, 1fr) minmax(0, 2fr);
    column-gap: clamp(32px, 4vw, 64px);
    align-items: center;
  }

  .text-image-row-media {
    margin-top: 0;
  }

  /* Alternates the zigzag: text moves to the right column, photo to the
     left, by reordering within the grid rather than touching source
     order (which stays text-first for reading order/screen readers
     regardless of visual side). The column widths also need to swap
     along with the order — otherwise the reordered photo lands in the
     narrow track sized for text instead of the wide one, which is what
     was producing the two-different-sizes bug (small photo on the
     reversed row, big photo on the normal one, both meant to match). */
  .text-image-row.reverse {
    grid-template-columns: minmax(0, 2fr) minmax(160px, 1fr);
  }

  .text-image-row.reverse .release-text {
    order: 2;
  }

  .text-image-row.reverse .text-image-row-media {
    order: 1;
  }
}

/* A row can be desktop-only (split text/image pairing) with a mobile-only
   equivalent shown instead below the breakpoint — used when the desktop
   version splits one paragraph into two alternating rows but mobile
   wants the original single-paragraph-plus-photo-pair treatment instead
   of two full-bleed stacked photos. */
.desktop-only {
  display: none;
}

.mobile-only {
  display: block;
}

@media (min-width: 960px) {
  .desktop-only {
    display: block;
  }

  .text-image-row.desktop-only {
    display: grid;
  }

  .mobile-only {
    display: none;
  }
}

.release-embed {
  margin-top: clamp(18px, 2.4vw, 26px);
}

.release-embed iframe,
.release-embed video {
  display: block;
  width: 100%;
  border: 0;
  background: var(--ink);
}

/* A short pull-quote from press/coverage, sitting right under an embed
   it's backing up (e.g. a label review under that release's player). */
.release-quote {
  font-family: "Cormorant Garamond", serif;
  font-style: italic;
  font-size: 16px;
  line-height: 1.5;
  color: rgba(17, 17, 16, 0.75);
  margin-top: 14px;
  margin-bottom: 0;
}

/* Kind Stranger's press quote sits as a third item in the text/embed
   two-column row (see .release--hero .release-columns) — left at the
   grid's default auto-placement it would just drop into column 1 under
   the write-up. Spanning both columns instead runs it the full width of
   the row, below both the text and the embed evenly, rather than
   looking boxed into one side. */
.release-columns > .release-quote {
  grid-column: 1 / -1;
  margin-top: clamp(20px, 2.6vw, 28px);
}

/* A small linked "book" teaser (e.g. Origins' companion book) — cover
   thumbnail beside a caption, the whole thing opens the PDF. Spans the
   full width of a two-column .release-columns row the same way
   .release-quote does. */
.release-columns > .release-book {
  grid-column: 1 / -1;
  margin-top: clamp(20px, 2.6vw, 28px);
}

.release-book-link {
  display: flex;
  align-items: center;
  gap: 16px;
  text-decoration: none;
  color: inherit;
}

.release-book-link img {
  width: 64px;
  height: auto;
  flex-shrink: 0;
  display: block;
  background: var(--ink);
}

.release-book-caption {
  display: block;
  font-family: "Inter", sans-serif;
  font-size: 13px;
  letter-spacing: 0.02em;
  line-height: 1.5;
  color: rgba(17, 17, 16, 0.8);
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.release-book-meta {
  display: block;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(17, 17, 16, 0.5);
  margin-top: 2px;
}

.release-book-link:hover .release-book-caption {
  color: var(--ink);
  border-color: var(--ink);
}

/* Big, full-width closing banner for a companion book (e.g. Origins) —
   sits after .release-body as its own block, same width as .release-media,
   so it reads as a clear "there's a whole book here" invitation rather
   than a small inline link. */
.release-book-full {
  display: block;
  margin-top: clamp(32px, 4vw, 56px);
  text-decoration: none;
  color: inherit;
}

.release-book-full-media {
  position: relative;
  aspect-ratio: 2.4 / 1;
  background: var(--ink);
  overflow: hidden;
}

.release-book-full-media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transition: transform 0.5s ease, opacity 0.3s ease;
}

.release-book-full:hover .release-book-full-media img {
  transform: scale(1.03);
  opacity: 0.9;
}

/* Caption sits right on top of the cover image (bottom-left, on a dark
   scrim) instead of below it, so "there's a whole book here, click to
   open it" reads immediately rather than as a small line of text
   underneath a picture. */
.release-book-full-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 16px;
  padding: clamp(20px, 3vw, 36px) clamp(20px, 3vw, 36px) clamp(16px, 2.4vw, 26px);
  background: linear-gradient(to top, rgba(17, 17, 16, 0.85) 0%, rgba(17, 17, 16, 0.55) 55%, rgba(17, 17, 16, 0) 100%);
}

.release-book-full-title {
  font-size: clamp(24px, 3vw, 36px);
  color: var(--cream);
  border-bottom: 1px solid rgba(240, 235, 224, 0.55);
  transition: border-color 0.2s ease;
}

.release-book-full:hover .release-book-full-title {
  border-color: var(--cream);
}

.release-book-full-meta {
  font-family: "Inter", sans-serif;
  font-size: 12.5px;
  letter-spacing: 0.03em;
  color: rgba(240, 235, 224, 0.75);
}

.release-quote a {
  color: inherit;
  font-style: normal;
  border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.release-quote a:hover {
  color: var(--ink);
  border-color: var(--ink);
}

/* Full-width photo stack — for a small set of large, consistently-shaped
   photos from one shoot (e.g. the Funkhaus session stills on Kind
   Stranger: uniform landscape, high resolution), each one gets its own
   roughly one-screen-tall box via contain + cream letterboxing, so
   scrolling moves through them one at a time at real size. This is the
   wrong fit for a mixed batch of photos with very different shapes/sizes
   (see .photo-masonry below) — the box being one fixed shape means an
   odd-shaped or low-resolution photo either gets cropped hard or shown
   tiny inside a lot of empty letterboxing. */
.release-photos {
  display: flex;
  flex-direction: column;
  gap: clamp(18px, 2.4vw, 26px);
}

.release-photos .release-media {
  aspect-ratio: auto;
  height: 90vh;
  max-height: 900px;
  background: var(--cream);
}

.release-photos .release-media img {
  height: 100%;
  object-fit: contain;
}

.release-photo {
  margin: 0;
}

/* A single photo standing on its own, full width to match the other
   full-width elements in the same entry (the closing video embed,
   photo-masonry pairs) — used for Monom's "palette cleanser" rhythm
   (photo, then a left/right pair, then a right/left pair, then the
   video). */
.release-photo-centered {
  margin: clamp(20px, 2.6vw, 28px) 0 0;
}

/* Grid layout for a mixed batch of photos (different source resolutions
   and aspect ratios, e.g. the Live Performance gallery) — each photo
   runs at its own natural shape and size instead of being forced into
   one shared box, so nothing gets cropped, stretched, or padded with
   empty letterboxing. Previously a CSS multi-column ("masonry") layout,
   but the browser's column-balancing heuristic doesn't account for very
   different photo heights (two tall portraits + two short landscapes)
   and was leaving a large empty gap under the shorter column. A plain
   two-column grid is fully deterministic instead — no balancing, no
   gap — at the cost of only being right for a batch that (like this one)
   pairs up evenly by two. */
.photo-masonry {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(14px, 2vw, 22px);
  align-items: center;
}

.photo-masonry .release-media {
  aspect-ratio: auto;
  height: auto;
  background: none;
}

.photo-masonry .release-media img {
  width: 100%;
  height: auto;
  display: block;
  object-fit: initial;
}

@media (max-width: 640px) {
  .photo-masonry {
    grid-template-columns: 1fr;
  }
}

/* Next-show label, set directly on the photo rather than as a caption
   line underneath it. Previously read bottom-to-top along the left edge
   (a gallery-wall-label treatment) — legible as a design flourish but
   genuinely hard to actually read once the text got as long as "Next
   live performance: 14 November 2026, Sihlfeld Friedhof, Zürich —
   Aurora Edition Festival". Plain horizontal text now, wrapping onto a
   second line rather than forcing a single very tall vertical run. A
   cream chip behind the black text still keeps it legible regardless of
   what's directly behind it in the photo. */
.next-show-tag {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  background: var(--cream);
  color: var(--ink);
  padding: 10px 14px;
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.5;
  text-align: center;
  max-width: min(320px, calc(100% - 28px));
}

.release-embed-caption {
  font-family: "Inter", sans-serif;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(17, 17, 16, 0.45);
  margin-top: 6px;
  margin-bottom: 0;
}

/* Origins' artist statement (Michael Naify's own words, quoted in full)
   set in italic throughout, text and photo captions alike, so it reads
   visibly as a quoted statement rather than Amedeo's own write-up. */
.release-statement .release-text,
.release-statement .release-embed-caption {
  font-style: italic;
}

/* A timestamp inside a caption (e.g. "Highlight at 5:05") is a real
   click-to-seek link (see [data-seek-target] in release-video.js) —
   underlined so it reads as interactive rather than blending into the
   plain caption text around it. */
.release-embed-caption a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.release-embed-caption a:hover {
  color: var(--ink);
}

/* Piranha's sound mood board — reproduces the feel of the brief Hannah
   shared (typewritten notes, then a script title, then an italic serif
   narrative), rather than using the site's usual Inter/Cormorant pairing
   plain. Close, freely-licensed stand-ins for her original fonts: Courier
   Prime for the typewriter block, Tangerine for the ornate script title
   (a common substitute for Zapfino-style scripts), and the site's own
   Cormorant Garamond italic for the narrative — already loaded, no need
   for a fourth family, and its italic is a good match for the serif
   storytelling voice. */
.mood-board {
  margin-top: clamp(20px, 2.6vw, 28px);
  max-width: 62ch;
}

.mood-board-mono {
  font-family: "Courier Prime", monospace;
  font-style: italic;
  font-size: 13.5px;
  line-height: 1.6;
  color: rgba(17, 17, 16, 0.75);
}

.mood-board-title {
  font-family: "Tangerine", cursive;
  font-weight: 700;
  font-size: clamp(36px, 4.6vw, 52px);
  line-height: 1.1;
  color: var(--ink);
  margin: 20px 0 18px;
}

.mood-board-story {
  font-family: "Cormorant Garamond", serif;
  font-style: italic;
  font-size: 19px;
  line-height: 1.5;
  color: rgba(17, 17, 16, 0.85);
}

.mood-board-story + .mood-board-story {
  margin-top: 16px;
}

/* Closing colophon line (label, funders, mastering/video/vocal credits) —
   smaller and quieter than the write-up above it, same idea as liner
   notes on the record itself. */
.release-credits {
  font-family: "Inter", sans-serif;
  font-size: 12.5px;
  line-height: 1.6;
  color: rgba(17, 17, 16, 0.55);
  max-width: 640px;
  margin-top: clamp(18px, 2.4vw, 26px);
  margin-bottom: 0;
}

.release-credits a {
  color: inherit;
  border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.release-credits a:hover {
  color: var(--ink);
  border-color: var(--ink);
}

/* Italy Segreta's own wordmark standing in for the h2 text itself,
   ink-colored so it reads like typography rather than a badge overlaid on
   the photo (see the removed .release-media-cover treatment). Matched to
   .release-title's own font-size clamp so it reads at the same size as
   every other entry's plain-text heading. */
.release-title-logo {
  display: block;
  height: clamp(26px, 3vw, 38px);
  width: auto;
}

/* Inline links inside a write-up paragraph (e.g. crediting a curator by
   name) — same quiet underline as .release-credits above rather than
   full text-decoration:none inherited from the global reset, so a link
   like this doesn't disappear into the surrounding prose. */
.release-text a {
  color: inherit;
  border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.release-text a:hover {
  color: var(--ink);
  border-color: var(--ink);
}

.release-links {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: clamp(16px, 2vw, 22px);
}

.release-links a {
  font-family: "Inter", sans-serif;
  font-size: 12px;
  letter-spacing: 0.03em;
  color: rgba(17, 17, 16, 0.55);
  border-bottom: 1px solid var(--line);
  transition: color 0.2s ease, border-color 0.2s ease;
}

.release-links a:hover {
  color: var(--ink);
  border-color: var(--ink);
}

/* Click-to-enlarge overlay for still photos (see js/photo-lightbox.js).
   Video thumbnails already open the video itself on click, so this only
   ever wires up on plain images. */
.lightbox-trigger {
  cursor: zoom-in;
}

body.lightbox-locked {
  overflow: hidden;
}

.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(20px, 5vw, 64px);
  background: rgba(17, 17, 16, 0.92);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s ease;
}

.lightbox-overlay.is-open {
  opacity: 1;
  visibility: visible;
}

.lightbox-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
}

.lightbox-close {
  position: absolute;
  top: clamp(14px, 2.2vw, 26px);
  right: clamp(14px, 2.2vw, 26px);
  width: 40px;
  height: 40px;
  border: none;
  background: none;
  color: #fff;
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.75;
  transition: opacity 0.2s ease;
}

.lightbox-close:hover {
  opacity: 1;
}

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

@media (max-width: 720px) {
  /* Same fixed mobile nav-bar clearance used on the video-pages layout
     (see body.video-pages .video-main above) — the bar has no background
     of its own, so content needs room to sit below it. */
  .release-list {
    padding-top: clamp(84px, 9vw, 96px);
  }

  .release-media {
    aspect-ratio: 4 / 3;
  }

  .release-embed {
    max-width: 100%;
  }
}

/* ---------- placeholder note (visible only to editor, remove when filled) ---------- */

.todo-note {
  font-family: "Inter", sans-serif;
  font-size: 12px;
  color: #b5763f;
  border: 1px dashed #b5763f;
  padding: 6px 10px;
  display: inline-block;
  margin-top: 10px;
}
