/* ==========================================================================
   MooN — official site
   Vanilla CSS. No framework, no build step, no CDN, no runtime dependency.
   Mobile-first: every base rule targets 375px; wider screens only add.

   PALETTE RULE: deep black dominant, aged amber the ONLY accent, off-white
   for supporting text. There is deliberately no second accent colour in
   this file. Gradients appear only as light and shadow — a beam, a scrim,
   a vignette — never as a colour treatment on a surface.
   ========================================================================== */

/* ---------- fonts: self-hosted, two families, nothing fetched remotely --- */
@font-face {
  font-family: "Cinzel";
  src: url("assets/fonts/cinzel-var.woff2") format("woff2-variations"),
       url("assets/fonts/cinzel-var.woff2") format("woff2");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Barlow";
  src: url("assets/fonts/barlow-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Barlow";
  src: url("assets/fonts/barlow-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* ---------- tokens ------------------------------------------------------ */
:root {
  --black:       #0a0908;
  --black-deep:  #060505;

  --amber:       #c8963c;   /* 7.3:1 on --black  — AA for text */
  --amber-lit:   #e3b45f;   /* hover / focus                    */
  --amber-dim:   rgba(200, 150, 60, 0.22);

  --bone:        #ece5d8;   /* 15.1:1 on --black                */
  --muted:       #a89c8d;   /*  7.4:1 on --black — AA for body  */

  --display: "Cinzel", "Times New Roman", Times, serif;
  --text:    "Barlow", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

  --gut: clamp(1rem, 4vw, 2.5rem);
  --nav-h: 3.25rem;

  --ease: cubic-bezier(0.22, 0.68, 0.2, 1);
}

/* ---------- reset ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1rem);
}
body {
  margin: 0;
  background: var(--black);
  color: var(--bone);
  font-family: var(--text);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.68;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
img { max-width: 100%; height: auto; display: block; }
h1, h2, h3, h4 { margin: 0; font-weight: 400; line-height: 1.05; }
p { margin: 0 0 1.15em; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: var(--amber); text-decoration: none; }

/* ---------- a11y -------------------------------------------------------- */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.skip {
  position: absolute; left: 50%; top: 0; transform: translate(-50%, -120%);
  z-index: 100; padding: 0.7rem 1.25rem;
  background: var(--amber); color: var(--black-deep);
  font-family: var(--text); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.8rem;
}
.skip:focus { transform: translate(-50%, 0); }

:where(a, button, [tabindex]):focus-visible {
  outline: 2px solid var(--amber-lit);
  outline-offset: 3px;
}
/* Anchor targets carry tabindex="-1" so the skip link and the nav actually
   move keyboard focus rather than only scrolling. When one of these
   full-width containers takes focus, the ring sits INSIDE it — an outset
   ring around a whole section reads as a rendering fault. */
.section:focus-visible, .nav:focus-visible { outline-offset: -2px; }

/* ---------- analogue grain: one fixed layer, no image file -------------- */
/* z-index 30 puts the grain OVER the fixed nav (z-index 20), not under it.
   Below it, the bar was the one strip of the page without the texture, and
   since the grain lifts black by roughly six levels, the identical #0a0908
   read as two different blacks — a visible seam under the header.
   It stays below the skip link (z-index 100) and never takes the pointer. */
.grain {
  position: fixed; inset: 0; z-index: 30; pointer-events: none;
  opacity: 0.055;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------- layout ------------------------------------------------------ */
.wrap { width: 100%; max-width: 72rem; margin-inline: auto; padding-inline: var(--gut); }
.wrap--narrow { max-width: 40rem; }

.section { padding-block: clamp(4rem, 11vw, 8.5rem); position: relative; }
.section + .section { border-top: 1px solid var(--amber-dim); }

.section__title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(1.9rem, 1.2rem + 3.4vw, 3.4rem);
  letter-spacing: 0.02em;
  color: var(--bone);
  margin-bottom: clamp(2rem, 5vw, 3.5rem);
  text-align: center;
}
.section__title::after {
  content: ""; display: block;
  width: 3.25rem; height: 1px;
  margin: 1.15rem auto 0;
  background: var(--amber);
}

/* ---------- nav --------------------------------------------------------- */
.nav {
  position: fixed; inset: 0 0 auto; z-index: 20;
  height: var(--nav-h);
  background: var(--black);   /* fully opaque: text must not read through it */
  border-bottom: 1px solid var(--amber-dim);
}
.nav__inner {
  height: 100%;
  max-width: 72rem; margin-inline: auto; padding-inline: var(--gut);
  display: flex; align-items: center; justify-content: center; gap: 1.5rem;
}
/* DELIBERADO, decidido pelo George: o wordmark aparece SÓ no desktop.
   No celular a barra leva apenas os quatro links — os cinco elementos não
   cabem lado a lado a 375px, e o nome já está impresso no pôster do hero,
   logo abaixo. Não "consertar" isto fazendo-o aparecer no mobile. */
.nav__brand {
  display: none;
  font-family: var(--display); font-weight: 700;
  font-size: 2rem; letter-spacing: 0.09em;
  line-height: 1;
  color: var(--amber);
}
.nav__list { display: flex; gap: clamp(0.85rem, 3.4vw, 2.1rem); }
.nav__list a {
  font-size: clamp(0.66rem, 0.55rem + 0.5vw, 0.76rem);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  padding-block: 0.85rem;      /* generous tap target on a 375px phone */
  display: block;
  transition: color 200ms var(--ease);
}
.nav__list a:hover { color: var(--bone); }
/* Current section is marked by a rule as well as by hue — colour alone is
   not a sufficient cue (WCAG 1.4.1). */
.nav__list a[aria-current="true"] {
  color: var(--amber);
  box-shadow: inset 0 -1px 0 0 currentColor;
}

@media (min-width: 46em) {
  /* The bar grows with the wordmark. --nav-h drives scroll-padding and the
     scrollspy offset too, so redefining it here keeps those in step. */
  :root { --nav-h: 4.5rem; }
  .nav__inner { justify-content: space-between; }
  .nav__brand { display: block; }
}

/* ---------- hero -------------------------------------------------------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding-top: var(--nav-h);
  overflow: hidden;

  /* Decorative wallpaper: CSS only, never an <img>, so it is not in the
     content flow and screen readers never meet it.

     CHOSEN: _source/wallpaper2.png, of the three candidates. Reasons, in
     the order of the criteria set for the task:
       - none of the three is figurative, so none competes with the cast
         photo; wallpaper2 has the faintest, most dispersed sigils and the
         darkest field, so it competes least;
       - its Latin is the least broken of the three — it is the only one
         with "COMPREHENDERUNT" spelled in full, plus the IOANNES I:V
         citation. (Residual errors remain: "AQVAA", "OCCULTUM VERITAS",
         "OMNIA VNVS". The veil renders them all but invisible, which the
         brief anyway requires of this artwork.)
     All three source files are 1672px wide, short of the 2400px minimum
     asked for, so no 2560px variant exists: upscaling was ruled out. The
     art is also flattened to 62% brightness at build time — see the veil
     note below for the contrast measurements that forced it.
     background-COLOR, not the background shorthand — the shorthand would
     wipe the image out again in the desktop rule below. */
  background-color: var(--black-deep);
  background-image: url("assets/brand/hero-bg-800.jpg");           /* fallback */
  background-image: image-set(
      url("assets/brand/hero-bg-800.webp") type("image/webp"),
      url("assets/brand/hero-bg-800.jpg")  type("image/jpeg"));
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
  /* No background-attachment: fixed — it breaks on iOS and costs scroll perf. */
}
@media (min-width: 1200px) {
  .hero {
    background-image: url("assets/brand/hero-bg-1672.jpg");
    background-image: image-set(
        url("assets/brand/hero-bg-1672.webp") type("image/webp"),
        url("assets/brand/hero-bg-1672.jpg")  type("image/jpeg"));
  }
}

/* Contrast veil between wallpaper and content. Sits BELOW everything
   (z-index 0 against the content's 1) and ignores the pointer, so it can
   neither dim the cast poster nor swallow the CTA click. */
.hero::before {
  content: "";
  position: absolute; inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Opacities raised from the 0.70/0.55/0.95 first draft. Measured against
     the composited wallpaper, the brightest pixel behind the amber
     telemetry line gave only 2.69:1 at those values — below AA. With these,
     and the wallpaper itself flattened to 62% brightness at build time, the
     worst pixel measures 4.58:1 for the amber and 9.98:1 for the body text.
     Per the spec: the veil was strengthened, the text colour never touched. */
  background: linear-gradient(to bottom,
      rgba(10, 9, 8, 0.75) 0%,
      rgba(10, 9, 8, 0.60) 45%,
      rgba(10, 9, 8, 0.95) 100%);
}
.hero > * { position: relative; z-index: 1; }

/* ---------- moonlight pulse ---------------------------------------------
   A slow band of light crossing the hero, bottom to top.

   ::before is already the contrast veil, so this rides on ::after, which
   paints after it and therefore above it — while still sitting below the
   content, which carries z-index 1. That is what keeps the cast poster from
   lighting up with it: the pulse passes BEHIND the photograph.

   The 12.8s cycle is deliberate and must not be rounded: it is how long
   moonlight takes to reach Earth, in seconds. The crossing itself takes
   about 3.3s; the remaining nine are darkness.

   isolation: isolate creates the stacking context the wave layer needs. */
.hero { isolation: isolate; }

/* ==========================================================================
   ENERGY WAVE — an invisible semicircular front expanding from the point
   where the beam lands.

   THE WAVE ITSELF IS NEVER DRAWN. There is no light, no colour, no ring on
   screen. The layer below is a pixel-perfect COPY of the wallpaper, boosted
   in brightness and saturation, revealed only inside a narrow expanding
   arc. Where the mask is opaque you see the brighter copy; everywhere else,
   the original. Because brightness() is multiplicative, black stays black
   (0 x 1.6 = 0) while gold climbs steeply — so only the luminous elements
   react, and the arc is invisible over the dark field.

   Alignment is automatic: this layer paints the same image with the same
   `cover` / `center`, so it tracks the wallpaper at every viewport size.

   z-index -1 puts it above the hero's own background but BELOW the contrast
   veil (::before) and the content. That ordering matters — above the veil,
   the boost would undo the veil's work and wreck the text contrast.
   ========================================================================== */

@property --wave-p {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

.hero {
  /* --- ALL THE KNOBS LIVE HERE --- */
  --wave-cycle: 3s;      /* full cycle: expansion + rest. One wave every 3s. */
  --wave-width: 0.075;   /* front thickness, as a fraction of the max radius */
  --wave-gain: 1.62;     /* how much brighter struck elements get           */
  --wave-sat: 1.34;      /* how much richer their colour gets               */
  --wave-glow: 0.40;     /* strength of the soft bloom layer (0 disables it) */
  --wave-cut: 26px;      /* softness of the horizon cut at the origin       */

  /* Epicentre and max radius are computed in script.js, because compensating
     background-size:cover needs the image's intrinsic size. These are only
     fallbacks for the frames before that runs. */
  --wave-x: 60%;
  --wave-y: 80%;
  --wave-max: 1200px;

  /* One declaration of the artwork, shared by both wave layers. */
  --wave-img: image-set(
      url("assets/brand/hero-bg-800.webp") type("image/webp"),
      url("assets/brand/hero-bg-800.jpg")  type("image/jpeg"));
}
@media (min-width: 1200px) {
  .hero {
    --wave-img: image-set(
        url("assets/brand/hero-bg-1672.webp") type("image/webp"),
        url("assets/brand/hero-bg-1672.jpg")  type("image/jpeg"));
  }
}

.hero > .hero__wave {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;

  --r: calc(var(--wave-p) * var(--wave-max));
  --w: calc(var(--wave-max) * var(--wave-width));

  /* Two masks intersected: the expanding annulus, and a horizon that erases
     everything at or below the origin — which is what makes it a SEMIcircle
     climbing upward instead of a full ring. */
  -webkit-mask-image:
    radial-gradient(circle at var(--wave-x) var(--wave-y),
      transparent            calc(var(--r) - var(--w)),
      rgba(0,0,0,0.20)       calc(var(--r) - var(--w) * 0.66),
      rgba(0,0,0,0.60)       calc(var(--r) - var(--w) * 0.33),
      rgba(0,0,0,1)          var(--r),
      rgba(0,0,0,0.60)       calc(var(--r) + var(--w) * 0.33),
      rgba(0,0,0,0.20)       calc(var(--r) + var(--w) * 0.66),
      transparent            calc(var(--r) + var(--w))),
    linear-gradient(to bottom,
      rgba(0,0,0,1) calc(var(--wave-y) - var(--wave-cut)),
      transparent   var(--wave-y));
          mask-image:
    radial-gradient(circle at var(--wave-x) var(--wave-y),
      transparent            calc(var(--r) - var(--w)),
      rgba(0,0,0,0.20)       calc(var(--r) - var(--w) * 0.66),
      rgba(0,0,0,0.60)       calc(var(--r) - var(--w) * 0.33),
      rgba(0,0,0,1)          var(--r),
      rgba(0,0,0,0.60)       calc(var(--r) + var(--w) * 0.33),
      rgba(0,0,0,0.20)       calc(var(--r) + var(--w) * 0.66),
      transparent            calc(var(--r) + var(--w))),
    linear-gradient(to bottom,
      rgba(0,0,0,1) calc(var(--wave-y) - var(--wave-cut)),
      transparent   var(--wave-y));
  -webkit-mask-composite: source-in;
          mask-composite: intersect;

  opacity: 0;
  animation: moon-wave var(--wave-cycle) linear infinite;
  animation-play-state: paused;    /* runs only while the hero is on screen */
}
.hero.is-onscreen > .hero__wave { animation-play-state: running; }

/* Both children inherit the parent's mask, so each is clipped to the arc. */
.hero__wave::before,
.hero__wave::after {
  content: "";
  position: absolute; inset: 0;
  background-image: var(--wave-img);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}
/* the reaction itself */
.hero__wave::before {
  filter: brightness(var(--wave-gain)) saturate(var(--wave-sat)) contrast(1.04);
}
/* a soft bloom on the brightest pixels only — set --wave-glow: 0 to drop it */
.hero__wave::after {
  filter: brightness(2.1) saturate(1.45) blur(5px);
  opacity: var(--wave-glow);
}

/* Rest, expand, rest. The --wave-p stops are spaced unevenly on purpose:
   the front leaves quickly and eases as it widens, which reads as physical.

   The stops are percentages, so --wave-cycle scales the whole thing: change
   that one value and the expansion and the pause between waves stretch
   together, keeping these proportions.

   At the current 3s cycle: ~0.15s still, ~2.25s expanding, ~0.6s still. */
@keyframes moon-wave {
  0%,  5%  { --wave-p: 0.00; opacity: 0; }
  9%       { --wave-p: 0.10; opacity: 1; }
  23%      { --wave-p: 0.40; opacity: 1; }
  40%      { --wave-p: 0.66; opacity: 1; }
  58%      { --wave-p: 0.85; opacity: 1; }
  71%      { --wave-p: 0.97; opacity: 1; }
  80%      { --wave-p: 1.08; opacity: 0; }
  100%     { --wave-p: 1.08; opacity: 0; }
}

/* The wordmark runs nearly edge to edge inside the poster, so the image is
   NEVER cropped horizontally — any object-fit:cover guillotines the M and
   the N. Full natural width, height follows. */
.hero__media { position: relative; }
/* A single <img> can carry only ONE width/height pair, but this <picture>
   swaps a 2:3 portrait for a 1:1 square at 900px. Without these the browser
   reserves the box using the portrait ratio and then snaps to the square on
   decode — a ~280px jump on first load. The breakpoint below is written in
   px, deliberately, so it always matches the <source media> exactly; an em
   query would drift apart from it if the root font size changed.

   These two lines briefly collapsed into one on 20 Sep 2026, when the new
   cast photo arrived square only and the phone had to wear it. The 2:3
   version of the same photo came a few hours later and both are back. */
.hero__media img { width: 100%; height: auto; aspect-ratio: 2 / 3; }
@media (min-width: 900px) { .hero__media img { aspect-ratio: 1 / 1; } }

/* Darkness scrim so the premise clears AA over the poster. This is shadow,
   not colour — the palette rule stands. */
.hero__media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to bottom,
              rgba(6, 5, 5, 0.15) 0%,
              rgba(6, 5, 5, 0.05) 34%,
              rgba(6, 5, 5, 0.86) 76%,
              var(--black) 100%);
}
/* Text rides up into the poster's scrim, so the premise and the CTA stay
   close to the faces instead of stranded on empty black below them. */
.hero__body {
  position: relative; z-index: 2;
  margin: -16vh auto 0;
  padding: 0 var(--gut) clamp(2.5rem, 8vw, 4.5rem);
  max-width: 34rem;
  text-align: center;
}
.telemetry {
  font-size: 0.68rem; font-weight: 600;
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--amber);
  margin-bottom: 1.1rem;
}
.hero__premise {
  font-size: clamp(0.95rem, 0.9rem + 0.3vw, 1.08rem);
  line-height: 1.7;
  color: var(--bone);
  margin-bottom: 1.9rem;
  text-wrap: pretty;
}
.hero__cta { margin: 0; }

/* the beam that opens the page */
.hero__beam {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  opacity: 0;
  background: radial-gradient(60% 80% at 50% 0%,
              rgba(227, 180, 95, 0.30) 0%,
              rgba(227, 180, 95, 0.08) 42%,
              transparent 72%);
}

/* ---------- the scroll hint (desktop only) -------------------------------
   The hero is a full 100svh, so the CTA can sit at the fold with nothing
   below it to suggest the page continues. The hint appears last, breathes
   slowly, and leaves for good the instant the page moves.

   DESKTOP ONLY, by George's call (19/09/2026). On a phone the hero has no
   footer to put it in: measured at 375x812, the poster plus the text block
   fill 100svh with 9px to spare, so the mark landed on the CTA itself.
   Making room would have meant moving the approved hero. The markup stays
   in the HTML at every width — it is decorative and aria-hidden, so a
   display:none costs nothing and the breakpoint is one line to change.

   Written as `.hero > .hero__scroll` for the same reason `.hero__wave` is:
   `.hero > *` sets position:relative on every child, and a plain class
   would only win that tie by source order. */
.hero > .hero__scroll { display: none; }

@media (min-width: 56.25em) {
  .hero > .hero__scroll {
    position: absolute;
    left: 50%;
    bottom: clamp(1rem, 2.4vh, 2rem);
    transform: translateX(-50%);
    z-index: 2;
    pointer-events: none;         /* never stands between a cursor and a link */
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
  }
}
.hero__scroll-label {
  font-family: var(--text);
  font-size: 0.58rem; font-weight: 600;
  letter-spacing: 0.26em; text-transform: uppercase;
  color: var(--amber);
  opacity: 0.78;
}
/* Two chevrons in CSS, no icon file: a square with two borders, turned 45°.
   The second is smaller and dimmer, so the pair reads as one mark pointing
   down rather than as two arrows. */
.hero__scroll-mark {
  display: block;
  position: relative;
  width: 0.9rem; height: 0.8rem;
  animation: scroll-breathe 3.4s ease-in-out infinite;
}
.hero__scroll-mark::before,
.hero__scroll-mark::after {
  content: "";
  position: absolute; left: 50%;
  width: 0.58rem; height: 0.58rem;
  border-right: 1px solid var(--amber);
  border-bottom: 1px solid var(--amber);
  transform: translateX(-50%) rotate(45deg);
}
.hero__scroll-mark::before { top: 0.02rem; opacity: 0.85; }
.hero__scroll-mark::after  { top: 0.30rem; opacity: 0.42; }

/* Same 3.4s cycle as the cards' hint-breathe, so the two marks on the page
   that mean "there is more here" move at one tempo. */
@keyframes scroll-breathe {
  0%, 100% { transform: translateY(0);   opacity: 0.68; }
  50%      { transform: translateY(4px); opacity: 1; }
}

/* Gone for good, not hidden and waiting: the class is only ever added.

   Everything here is transition, never animation, and that is the whole
   point. The first version faded IN with `animation: fade … both`, and the
   dismissal did nothing visible: a filled animation outranks any declared
   value, so the opacity simply stayed at 1 until the animation was dropped,
   and then it snapped to 0 with no fade at all. Measured, not guessed.

   `.hero >` for the same kind of reason: the entrance rule `.is-lit
   .hero__scroll` carries equal specificity and sits further down the file,
   so without the child combinator it would win the tie. */
.hero > .hero__scroll.is-gone {
  opacity: 0;
  transform: translateX(-50%) translateY(10px);
  /* its own transition, so it never inherits the entrance delay */
  transition: opacity 560ms var(--ease), transform 560ms var(--ease);
}

@media (min-width: 56.25em) {
  .hero {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    align-items: center; gap: clamp(2rem, 5vw, 5rem);
    /* The section itself spans the full viewport so the wallpaper bleeds
       edge to edge; the CONTENT is still held to 78rem by padding. Capping
       the element's width instead would box the background in, leaving
       black bars down both sides. */
    padding-inline: max(var(--gut), calc((100% - 78rem) / 2));
    padding-block: calc(var(--nav-h) + 2rem) 2rem;
    /* background-color only: the shorthand would erase the wallpaper. */
    background-color: var(--black);
  }
  .hero__media::after { display: none; }   /* nothing overlaps it here */
  .hero__body {
    align-self: center;
    padding: 0; margin: 0; text-align: left; max-width: 30rem;
  }
}

/* ---------- buttons ----------------------------------------------------- */
.btn {
  cursor: pointer;              /* .btn is worn by a <button> now, not only by links */
  display: inline-block;
  font-family: var(--text);
  font-size: 0.78rem; font-weight: 600;
  letter-spacing: 0.17em; text-transform: uppercase;
  padding: 0.95rem 1.7rem;
  border: 1px solid var(--amber);
  border-radius: 1px;                 /* corners stay hard */
  transition: background-color 220ms var(--ease), color 220ms var(--ease);
}
.btn--solid { background: var(--amber); color: var(--black-deep); }
.btn--solid:hover { background: var(--amber-lit); border-color: var(--amber-lit); }

/* ---------- the transmission -------------------------------------------- */
.lede {
  font-size: clamp(1.1rem, 1rem + 0.65vw, 1.4rem);
  line-height: 1.6;
  color: var(--bone);
  margin-bottom: clamp(2.5rem, 7vw, 4.5rem);
  text-wrap: pretty;
}
.lede strong { color: var(--amber); font-weight: 400; white-space: nowrap; }

.wrap--narrow p { color: var(--muted); text-wrap: pretty; }
.wrap--narrow p strong { color: var(--bone); font-weight: 600; }

.turn {
  font-family: var(--display);
  font-size: clamp(1.15rem, 0.95rem + 1vw, 1.6rem);
  line-height: 1.35;
  color: var(--bone) !important;
  text-align: center;
  margin-block: clamp(2.2rem, 6vw, 3.4rem);
}
.turn--last strong { color: var(--amber); }

.cast { margin-block: clamp(2rem, 6vw, 3.5rem); }
.cast__row {
  max-width: 72rem; margin-inline: auto;
  padding: clamp(1.75rem, 5vw, 2.75rem) var(--gut);
  display: grid; gap: clamp(1.1rem, 3vw, 2.5rem);
}
.cast__row p { margin: 0; color: var(--muted); text-wrap: pretty; }
/* The name stays INLINE. Making it a block turned "Iaci lived in Salvador"
   into a heading followed by a sentence opening in lowercase, which breaks
   the reading of copy that must appear exactly as approved. */
.cast__row strong {
  font-family: var(--display);
  font-size: 1.2em; font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--amber);
}
.cast__scene { margin: 0; }
/* No filter here any more. The replacement scenes arrive already graded and
   carry their own amber frame; the old saturate/brightness knock-down was
   compensating for untreated stills and now just muddies them. */
.cast__scene img { width: 100%; }

@media (min-width: 50em) {
  .cast__row { grid-template-columns: 1.15fr 1fr; align-items: center; }
  .cast__row:nth-child(even) .cast__scene { order: 2; }
}

/* ---------- the band ---------------------------------------------------- */
/* One column at 375px is not a stylistic choice: at two columns the
   nationality printed inside each PNG stops being readable. */
/* The line that tells the visitor the cards open (26 Sep 2026). It carries
   the gap under the divider by itself, so the title's own margin is trimmed
   here — kept, the two would stack into a hole. Muted body, the colour the
   record specs use for their values: present when the eye passes, silent
   otherwise. */
#band .section__title { margin-bottom: clamp(1.1rem, 2.4vw, 1.6rem); }
.band__cue {
  margin: 0 auto clamp(1.7rem, 3.6vw, 2.5rem);
  max-width: 28rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.5;
}

.band {
  display: grid;
  gap: clamp(1.25rem, 4vw, 2rem);
  grid-template-columns: 1fr;
  max-width: 24rem; margin-inline: auto;
}
.band img { width: 100%; }

/* ==========================================================================
   MEMBER CARDS — one file in the HTML, two behaviours.

   The file markup exists ONCE and is always in the DOM, only hidden by CSS:
   never injected on click, because injected content is neither indexed nor
   announced by screen readers.

   It sits OUTSIDE the <button> deliberately. A button's accessible name is
   its own text content, so a file nested inside would make the whole card
   announce as "button: Stern Guitar Voice Hamburg Germany Timbre dry and
   cutting…". The button holds the portrait and a short hidden label; the
   file is its sibling, wired up with aria-controls.
   ========================================================================== */
.member { position: relative; }
.member__stage { position: relative; }

.member__toggle {
  display: block; width: 100%; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; cursor: pointer; position: relative;
}
.member__toggle img { width: 100%; display: block; }

/* THE OPEN MARK — a compass star, not a UI plus (George, 3 Sep 2026).

   The portraits already carry an eight-pointed star engraved under each
   name. This is that star, small enough that its cardinal arms still read
   as a "+" from across the card. It replaces the circled plus that stood
   here, which the brief rules out by name: no generic line icons (§3).

   ::before draws the four cardinal points; ::after repeats the shape at
   45°, smaller and dimmer, for the diagonals. Together they make the
   eight-pointed star, with the upright arms dominant so the mark keeps
   saying "there is more here" rather than merely decorating the corner.

   The halo breathes on a slow 3.4s cycle to pull the eye to the corner.
   It stops the moment the card opens — it has done its job by then, and a
   disc still pulsing under an open file is just noise.

   Made louder on 26 Sep 2026: visitors were not seeing that the cards open.
   Same mark, nothing added — 1.85rem to 2.4rem, the outline from --amber to
   --amber-lit at 2px, a darker disc under the star, and a dark contact ring
   in the halo. The ring is what makes it hold: the mark sits over the lower
   third of the portrait, which is the lit part in all six, and amber on
   amber was dissolving. Sizes are in rem against a 16px root, so the mark
   is 38px on a phone as well — it never shrinks with the card. */
.member__hint {
  position: absolute; right: 0.65rem; bottom: 0.65rem;
  width: 2.4rem; height: 2.4rem;
  border: 2px solid var(--amber-lit);
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%,
      rgba(227, 180, 95, 0.20) 0%, rgba(10, 9, 8, 0.96) 70%);
  animation: hint-breathe 3.4s ease-in-out infinite;
  transition: transform 300ms var(--ease), border-color 260ms var(--ease);
}
/* One clip-path, used twice: eight vertices, alternating tip and waist. */
.member__hint::before,
.member__hint::after {
  content: ""; position: absolute; inset: 0;
  background: var(--amber-lit);
  clip-path: polygon(50% 6%, 56% 44%, 94% 50%, 56% 56%,
                     50% 94%, 44% 56%,  6% 50%, 44% 44%);
  transition: background-color 260ms var(--ease);
}
.member__hint::after { opacity: 0.62; transform: rotate(45deg) scale(0.6); }

.member__toggle:hover .member__hint,
.member__toggle:focus-visible .member__hint {
  transform: scale(1.12);
  border-color: var(--amber-lit);
}
.member__toggle:hover .member__hint::before,
.member__toggle:focus-visible .member__hint::before { background: var(--amber-lit); }

/* Open: the star turns 45° and the disc inverts — amber ground, dark star.
   On desktop the flipped card hides this, but on mobile the portrait stays
   put above the open panel, and that is where the state has to be legible. */
.member.is-open .member__hint {
  transform: rotate(45deg);
  background: var(--amber);
  border-color: var(--amber-lit);
  animation: none;
  box-shadow: 0 0 0 3px rgba(200, 150, 60, 0.14);
}
.member.is-open .member__hint::before,
.member.is-open .member__hint::after { background: var(--black); }

/* The first shadow never changes: it is the dark contact ring that keeps the
   mark off the lit portrait behind it. Only the two after it breathe. */
@keyframes hint-breathe {
  0%, 100% { box-shadow: 0 0 0 1.5px rgba(10, 9, 8, 0.55),
                         0 0 0 0 rgba(200, 150, 60, 0),
                         0 0 8px 1px rgba(200, 150, 60, 0.16); }
  50%      { box-shadow: 0 0 0 1.5px rgba(10, 9, 8, 0.55),
                         0 0 0 5px rgba(200, 150, 60, 0.12),
                         0 0 16px 3px rgba(200, 150, 60, 0.40); }
}

/* Six marks breathing in lockstep looks like a control panel. Offsetting
   the phase turns them into embers instead. The delays are negative on
   purpose: that starts each one already mid-cycle, so nothing sits dark
   waiting for its turn when the section first scrolls into view. */
.member:nth-child(2) .member__hint { animation-delay: -0.55s; }
.member:nth-child(3) .member__hint { animation-delay: -1.1s; }
.member:nth-child(4) .member__hint { animation-delay: -1.65s; }
.member:nth-child(5) .member__hint { animation-delay: -2.2s; }
.member:nth-child(6) .member__hint { animation-delay: -2.75s; }

/* ---- MOBILE (< 48em): no flip. The panel expands below the portrait and
   pushes the grid down. grid-template-rows animates from the real content
   height, so no max-height has to be guessed — a guessed value stalls the
   easing and breaks the moment a line of text grows. ---- */
.member__file {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 420ms var(--ease);
}
.member.is-open .member__file { grid-template-rows: 1fr; }
.member__clip { overflow: hidden; }

.mfile {
  position: relative;
  border: 1px solid var(--amber-dim); border-top: 0;
  padding: 1.15rem 1.1rem 1.25rem;
  background: var(--black);
  text-align: left;
}

/* Close button. Required, not a convenience: on the desktop flip the front
   face carries backface-visibility:hidden, which takes it OUT of hit
   testing once turned — a click on the middle of a flipped card lands on
   .member__stage and does nothing. Without this the card had no way back.
   It needs pointer-events of its own because the file it sits in is set to
   `none` on desktop, so clicks fall through to the front face. */
.mfile__close {
  position: absolute; top: 0.5rem; right: 0.5rem;
  width: 2rem; height: 2rem;
  padding: 0; border: 1px solid var(--amber); border-radius: 50%;
  background: rgba(10, 9, 8, 0.7);
  cursor: pointer;
  pointer-events: auto;
  transition: background-color 200ms var(--ease), transform 200ms var(--ease);
}
.mfile__close span { position: absolute; inset: 0; }
.mfile__close span::before,
.mfile__close span::after {
  content: ""; position: absolute;
  top: 50%; left: 50%;
  width: 0.85rem; height: 1px;
  background: var(--amber);
  transform-origin: center;
}
.mfile__close span::before { transform: translate(-50%, -50%) rotate(45deg); }
.mfile__close span::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.mfile__close:hover { background: rgba(200, 150, 60, 0.18); transform: rotate(90deg); }
.mfile__name {
  font-family: var(--display); font-weight: 700;
  font-size: 1.3rem; letter-spacing: 0.05em;
  color: var(--bone); margin: 0 0 0.2rem;
  padding-right: 2.4rem;      /* keeps the name clear of the close button */
}
.mfile__role {
  font-family: var(--text); font-weight: 600;
  font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--amber);
}
.mfile__origin {
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 1rem;
}
.mfile__specs { margin: 0; border-top: 1px solid var(--amber-dim); }
.mfile__specs > div {
  padding-block: 0.7rem; border-bottom: 1px solid var(--amber-dim);
}
.mfile__specs dt {
  font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--amber); margin-bottom: 0.22rem;
}
.mfile__specs dd { margin: 0; color: var(--bone); font-size: 0.92rem; line-height: 1.55; }

/* ---- THE SAMPLE — a few seconds of the voice, at the foot of the file.

   The control is the record the sample comes from: the same pressing that
   already serves as the play marker in the tracklists, on the same spin
   animation. Five of the six are VIRTUES; Mizuki's is SIN, and her disc
   says so without a word of copy.

   The label names the SINGER, not the song (George, 5 Sep 2026). What is
   on offer here is the voice — the track it was lifted from is a detail,
   and putting its title in display type made the card advertise the wrong
   thing. Which take each sample is stays recorded in the filenames under
   _source/sample/. Do not put the song back on the card.

   No native <audio controls> anywhere on the page: the browser's own widget
   is a grey rounded box that belongs to no part of this design. There is no
   no-JS fallback either, and that is not an oversight — without JS the card
   never opens at all, so there is nobody left to fall back for.

   One <audio> object is shared by all six, built in script.js. That is what
   guarantees starting a second sample stops the first. ---- */
.mfile__sample {
  margin-top: auto;                /* pinned to the foot of the file */
  padding-top: 1rem;
}
.sample {
  display: flex; align-items: center; gap: 0.8rem;
  width: 100%; padding: 0.3rem 0;
  border: 0; background: none; font: inherit; color: inherit;
  text-align: left; cursor: pointer;
  pointer-events: auto;   /* the file around it is click-through on desktop */
}
.sample__disc {
  position: relative; flex: none;
  width: 2.15rem; height: 2.15rem;
  border-radius: 50%;
  transition: transform 220ms var(--ease);
}
/* The record turns. The badge sitting on it does not, which is why the art
   lives on ::before and the glyph is a real element on top. */
.sample__disc::before {
  content: ""; position: absolute; inset: 0;
  border-radius: 50%;
  background: 50% 50% / contain no-repeat;
  animation: vinyl-spin 3.4s linear infinite;
  animation-play-state: paused;
}
.sample.is-playing .sample__disc::before { animation-play-state: running; }

.sample--sin .sample__disc::before {
  background-image: url("assets/covers/vinyl-sin.png");
  background-image: image-set(url("assets/covers/vinyl-sin.webp") type("image/webp"),
                              url("assets/covers/vinyl-sin.png")  type("image/png"));
}
.sample--virtues .sample__disc::before {
  background-image: url("assets/covers/vinyl-virtues.png");
  background-image: image-set(url("assets/covers/vinyl-virtues.webp") type("image/webp"),
                              url("assets/covers/vinyl-virtues.png")  type("image/png"));
}

.sample__glyph {
  position: absolute; inset: 30%;
  border-radius: 50%;
  background: rgba(10, 9, 8, 0.86);
  box-shadow: 0 0 0 1px rgba(200, 150, 60, 0.55);
}
/* Play becomes pause. Both shapes are four-point polygons on purpose —
   same vertex count, so the triangle morphs into the bar instead of
   snapping to it. */
.sample__glyph::before,
.sample__glyph::after {
  content: ""; position: absolute; inset: 0;
  background: var(--amber);
  transition: clip-path 200ms var(--ease);
}
.sample__glyph::before { clip-path: polygon(37% 24%, 75% 50%, 75% 50%, 37% 76%); }
.sample__glyph::after  { clip-path: polygon(75% 50%, 75% 50%, 75% 50%, 75% 50%); }
.sample.is-playing .sample__glyph::before { clip-path: polygon(33% 26%, 45% 26%, 45% 74%, 33% 74%); }
.sample.is-playing .sample__glyph::after  { clip-path: polygon(55% 26%, 67% 26%, 67% 74%, 55% 74%); }

.sample__text { display: block; min-width: 0; }
.sample__label {
  display: block;
  font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--amber);
}
.sample__voice {
  display: block;
  font-family: var(--display); font-size: 1rem; letter-spacing: 0.03em;
  color: var(--bone);
  transition: color 200ms var(--ease);
}
.sample:hover .sample__disc,
.sample:focus-visible .sample__disc { transform: scale(1.06); }
.sample:hover .sample__voice,
.sample:focus-visible .sample__voice { color: var(--amber-lit); }

/* The elapsed line: one pixel of amber filling a groove of amber-dim. */
.sample__bar {
  display: block; height: 1px; margin-top: 0.55rem;
  background: var(--amber-dim);
}
.sample__fill {
  display: block; height: 100%; width: 0;
  background: var(--amber);
  transition: width 180ms linear;
}

/* ---- DESKTOP (>= 48em): the card turns on the Y axis. Same file element,
   now absolutely placed as the back face. ---- */
@media (min-width: 48em) {
  .member__stage {
    transform-style: preserve-3d;
    transition: transform 520ms var(--ease);
  }
  .member { perspective: 1400px; }
  .member.is-open .member__stage { transform: rotateY(180deg); }

  .member__toggle { backface-visibility: hidden; }

  .member__file {
    position: absolute; inset: 0;
    display: block; grid-template-rows: none;
    transform: rotateY(180deg);
    backface-visibility: hidden;
    /* The file is text, so it lets clicks fall through. Note this does NOT
       make the flipped card closable by clicking it: the front face is
       hidden by backface-visibility and therefore out of hit testing, so
       such a click reaches .member__stage and does nothing. That is what
       .mfile__close exists for, and it re-enables pointer events itself. */
    pointer-events: none;
  }
  .member__clip { overflow: hidden; height: 100%; }
  .mfile {
    height: 100%; border-top: 1px solid var(--amber-dim);
    display: flex; flex-direction: column; justify-content: center;
    padding: 1.1rem 1rem;
  }
  .mfile__specs > div { padding-block: 0.55rem; }
}

/* Reduced motion: no 3D rotation anywhere — a straight swap between the two
   faces, as the brief requires. */
@media (prefers-reduced-motion: reduce) {
  .member__file { transition: none; }
  /* the halo stops breathing and the star holds still; the open state is
     still marked, by the amber inversion rather than by movement */
  /* the halo stops, but the dark contact ring stays: it is contrast, not
     motion, and without it the mark dissolves into the lit portrait */
  .member__hint {
    transition: none; animation: none;
    box-shadow: 0 0 0 1.5px rgba(10, 9, 8, 0.55);
  }
  .member__toggle:hover .member__hint,
  .member__toggle:focus-visible .member__hint { transform: none; }
  .member.is-open .member__hint { transform: none; }
  @media (min-width: 48em) {
    .member__stage { transition: none; transform: none !important; }
    .member__toggle { backface-visibility: visible; }
    .member__file { transform: none; backface-visibility: visible; opacity: 0; pointer-events: none; }
    .member.is-open .member__file { opacity: 1; }
    .member.is-open .member__toggle { visibility: hidden; }
  }
}

@media (min-width: 40em) {
  .band { grid-template-columns: repeat(2, 1fr); max-width: 46rem; }
}
@media (min-width: 62em) {
  .band { grid-template-columns: repeat(3, 1fr); max-width: 72rem; }
}

/* ---------- the records ------------------------------------------------- */
.record { padding-block: clamp(2.5rem, 7vw, 4.5rem); }
.record + .record { border-top: 1px solid var(--amber-dim); }

.record__head { display: grid; gap: clamp(1.75rem, 5vw, 3.5rem); align-items: center; }
/* Mandala art is already circular on near-black: it bleeds, with no card,
   no border, no shadow and no circular crop. */
.record__cover { margin: 0; }
/* NO corner mask any more (removed 30 Aug 2026, with the new artwork).
   The old covers were a circle floating on a near-black starfield, so
   fading the corners let them bleed into the page. These are record
   sleeves: they have their own worn border and a vinyl disc emerging at the
   right edge. Fading the corners would eat both. They are objects now, and
   are shown whole — still with no card, no CSS border and no shadow, since
   the artwork already carries all three. */
.record__cover img { width: 100%; }

.record__eyebrow {
  font-size: 0.7rem; font-weight: 600;
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--amber);
  margin-bottom: 0.75rem;
}
.record__title {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(2.6rem, 1.6rem + 5vw, 5rem);
  letter-spacing: 0.03em;
  color: var(--bone);
  margin-bottom: 1.25rem;
}
/* A title too long for its column. A one-word title cannot wrap, so at the
   shared size CONVICTION (≈7.64em wide in the display face) widened its grid
   column and squeezed the cover: 405px against 508px at 1440, 180px against
   341px at 800, and wider than the screen itself at 320. The size is set
   from the width the word has, not from the viewport alone — 7.8em gives a
   little air — so the column, and the cover beside it, match the other
   records at every width (George, 19 Oct 2026: same cover size, smaller
   letters if needed). Two columns from 50em: the column is ~42.7vw there,
   capped at 508px. */
.record__title--long {
  font-size: min(clamp(2.6rem, 1.6rem + 5vw, 5rem), calc((100vw - 3rem) / 7.8));
}
@media (min-width: 50em) {
  .record__title--long { font-size: min(4rem, 5.3vw); }
}
.record__blurb { color: var(--muted); text-wrap: pretty; }

.record__spec {
  margin: 1.75rem 0;
  border-top: 1px solid var(--amber-dim);
}
.record__spec > div {
  display: grid; gap: 0.1rem;
  padding-block: 0.8rem;
  border-bottom: 1px solid var(--amber-dim);
}
.record__spec dt {
  font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--amber);
}
.record__spec dd { margin: 0; color: var(--muted); font-size: 0.95rem; }

.record__actions { margin: 0; }

/* ---------- GROOVE: a duration where the others carry a singer ----------
   The eleven rows landed with their notes on 20 Sep 2026, and .tracks--plain
   — the variant that dropped the disc column while the list was titles only
   — went with them. GROOVE now wears the same three-column .track as the
   other three records.

   No row shows a vocal credit: the album is instrumental. The slot is not
   held open either, because .track__voice was always inline — absent, it
   takes up nothing. .track__time steps into exactly that position and
   shares its declaration below, so the two read as one typographic idea:
   ENVY · NOVA above, SYNCOPATED · 1:55 here.

   Durations exist nowhere else on the page. The other 32 rows have never
   shown one, and this block does not invent a second treatment for them —
   when their times arrive, this class is already the place they go. */

/* THE VINYL IS ON LOAN.
   George's call (20 Sep 2026): use the HERE LIES pressing until GROOVE's own
   art exists. Note that this is the one place where two records share a
   label — the convention is one pressing per album, colour marking the
   record and never the track. It is on loan, not adopted.

   Since the release (1 Oct 2026) it is an <a> to the track on Spotify, like
   the other 32; the span rules that held it still before release are gone.

   CONVICTION borrows the same pressing, on the same terms (George, 19 Oct
   2026: "igual ao do groove") — three records now share one label. */
.record--groove .track__disc::before,
.record--conviction .track__disc::before {
  background-image: url("assets/covers/vinyl-here-lies.png");
  background-image: image-set(url("assets/covers/vinyl-here-lies.webp") type("image/webp"),
                              url("assets/covers/vinyl-here-lies.png")  type("image/png"));
}


@media (min-width: 50em) {
  .record__head { grid-template-columns: 1fr 1fr; }
  .record__head--flip .record__cover { order: 2; }
  .record__spec > div { grid-template-columns: 9rem 1fr; gap: 1rem; align-items: baseline; }
}


/* ---------- the book ---------------------------------------------------- */
/* MooN ORIGINS (15 Sep 2026). No new component anywhere: the prose wears
   .record__blurb, the two links wear the existing .btn, and the gap and
   vertical centring are the ones .record__head already uses, so the section
   reads as one more record block. */
.book__grid {
  display: grid;
  gap: clamp(1.75rem, 5vw, 3.5rem);
  align-items: center;
  grid-template-areas:
    "text"
    "cover"
    "actions";
}
/* Phone: the wrapper steps out of the way and its two children are placed
   by area, so the order is text, cover, buttons whatever the HTML says. */
.book__meta { display: contents; }
.book__text { grid-area: text; }
.book__actions {
  grid-area: actions;
  margin: 0;
  display: flex; flex-wrap: wrap; gap: 0.9rem;
}
/* One button since 20 Sep 2026, so there is no longer a twin to match. On a
   phone it still fills the column, the way the pair did. The desktop cap is
   with the rest of the wide layout, below. */
.book__btn { flex: 1 1 14rem; text-align: center; }

/* The render carries its own shadow and perspective, so the column gives it
   room instead of a card or a border, and it never touches the edge. */
.book__cover {
  grid-area: cover;
  margin: 0;
  padding-inline: clamp(0.5rem, 3vw, 2rem);
}
.book__cover img { width: 100%; max-width: 24rem; margin-inline: auto; }
/* The language spans wrap a <picture>; the shared bilingual rule would send
   them back to inline, which is the wrong box for a block image. */
html[data-ui="en"] .book__cover [data-lang="en"],
html[data-ui="pt"] .book__cover [data-lang="pt"] { display: block; }

@media (min-width: 50em) {
  /* Same breakpoint and same 1fr 1fr the records use. The wrapper becomes a
     single column item again, which is what puts the button at the end of
     the text column instead of adrift in a row of their own. */
  .book__grid { grid-template-columns: 1fr 1fr; grid-template-areas: "meta cover"; }
  .book__meta { display: block; grid-area: meta; }
  .book__actions { margin-top: clamp(1.5rem, 3vw, 2.25rem); }
  /* Alone in a half-page column, flex-grow would stretch the button across
     the whole text width. It stops at the size of a button. */
  .book__btn { max-width: 20rem; }
}
/* ---------- tracklists -------------------------------------------------- */
/* --disc drives both the marker and the grid column it sits in, so the two
   can never drift apart: change it here and the row follows. */

/* ---------- the tracklist bar and its fold -------------------------------
   Four records are 43 tracks; a fifth would pass 55. Each list folds away
   behind the hairline that already separated it from the spec above — the
   rule becomes the control, so no second button appears next to Lyrics.

   NOTHING IS REMOVED FROM THE DOM. The rows are always there, hidden by a
   collapsed grid row, because they are the best search asset this site has
   and injected text is not indexed. Same reasoning as the lyrics sheets.

   The bar is hidden until script.js adds .is-foldable, for the same reason
   .reveal-ready exists: with no JavaScript the page shows every tracklist,
   open, rather than four buttons that do nothing. */
.tracks__toggle { display: none; }

.record.is-foldable .tracks__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  margin-top: clamp(2.5rem, 6vw, 4rem);   /* the margin .tracks used to own */
  padding: 0.95rem 0;
  background: none;                        /* no fill on hover, by the brief */
  border: 0;
  border-top: 1px solid var(--amber-dim);
  border-bottom: 1px solid var(--amber-dim);
  border-radius: 0;
  cursor: pointer;
  text-align: left;
  color: var(--amber);
  transition: border-color 220ms var(--ease), color 220ms var(--ease);
}
/* Same voice as the spec labels (SOUND, LYRICS): spaced small capitals in
   amber. Not .btn — this one commands the page, it does not open content. */
.tracks__count {
  font-family: var(--text);
  font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.2em; text-transform: uppercase;
}
/* The emphasis on hover is the hairline brightening, plus the chevron: the
   brief rules out a background change, and amber-on-black has nowhere to go
   but lighter. */
.record.is-foldable .tracks__toggle:hover,
.record.is-foldable .tracks__toggle:focus-visible {
  color: var(--amber-lit);
  border-color: var(--amber);
}
.tracks__toggle:hover .tracks__chev { transform: translateY(2px); }
.tracks__toggle[aria-expanded="true"]:hover .tracks__chev { transform: rotate(180deg) translateY(2px); }

/* The chevron is the scroll hint's mark, one line of CSS lighter: a square
   with two borders, turned 45°. No icon file, no second idiom on the page. */
.tracks__chev {
  flex: none;
  position: relative;
  width: 0.62rem; height: 0.62rem;
  transition: transform 300ms var(--ease);
}
.tracks__chev::before {
  content: "";
  position: absolute; left: 50%; top: 0;
  width: 0.52rem; height: 0.52rem;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translate(-50%, -30%) rotate(45deg);
}
.tracks__toggle[aria-expanded="true"] .tracks__chev { transform: rotate(180deg); }

/* THE FOLD. grid-template-rows 1fr -> 0fr, the same mechanism the member
   cards use on mobile: it animates to the content's real height, with no
   magic number and no max-height guessed large enough to be safe (which
   costs a jumpy, mistimed ease every time the guess is wrong). */
.record.is-foldable .tracks__panel {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 420ms var(--ease);
}
.record.is-foldable .tracks__panel > .tracks {
  min-height: 0;        /* without this a grid item refuses to shrink */
  overflow: hidden;
  margin-top: 0;        /* the bar above now carries that margin */
}
.record.is-foldable.is-folded .tracks__panel { grid-template-rows: 0fr; }
/* The bar's own bottom rule closes the block when it is folded, so the
   first row must not draw a second one right under it. */
.record.is-foldable .tracks__panel > .tracks > .track:first-child { border-top: 0; }

@media (prefers-reduced-motion: reduce) {
  .record.is-foldable .tracks__panel { transition: none; }
  .tracks__chev { transition: none; }
  .tracks__toggle:hover .tracks__chev { transform: none; }
  .tracks__toggle[aria-expanded="true"]:hover .tracks__chev { transform: rotate(180deg); }
  .record.is-foldable .tracks__toggle { transition: none; }
}

.tracks {
  --disc: 2rem;
  margin-top: clamp(2.5rem, 6vw, 4rem);
}
.track {
  display: grid;
  grid-template-columns: var(--disc) 1.9rem 1fr;
  gap: 0 0.7rem;
  align-items: start;
  padding-block: 1.15rem;
  border-top: 1px solid var(--amber-dim);
}
.track:last-child { border-bottom: 1px solid var(--amber-dim); }

/* THE DISC MARKER — a real vinyl now, replacing the pure-CSS discs the
   brief originally specified (George, 31 Aug 2026). It is the same pressing
   on both records; only the centre label changes — purple on SIN, gold on
   VIRTUES — so the colour still marks the ALBUM and never the track: all
   ten rows of a record are identical. HERE LIES has its own pressing
   waiting in _source/, for when that tracklist goes up.

   The source art is ~1250px square with the record sitting off-centre in
   the frame. It was re-cropped so the spindle hole lands exactly on 50%
   50% and the record fills the box edge to edge — that is the whole reason
   it can spin without wobbling. If the art is ever re-exported, it has to
   be re-centred on the hole the same way, or the rotation starts to limp.

   The vinyl is also the play control: each one links to its track on
   Spotify, in a new tab. It used to be an aria-hidden <span>; an
   interactive element can never be aria-hidden, so each carries a
   visually-hidden label ("Play Envy on Spotify") — otherwise a screen
   reader would announce twenty nameless links. */
.track__disc {
  position: relative;
  display: block;                 /* it is an <a> now, inline by default */
  width: var(--disc); height: var(--disc);
  border-radius: 50%;
  margin-top: 0.3rem;
  cursor: pointer;
  transition: transform 220ms var(--ease), box-shadow 220ms var(--ease);
}
.track__disc:hover,
.track__disc:focus-visible {
  transform: scale(1.1);
  box-shadow: 0 0 0 3px rgba(200, 150, 60, 0.18);
}

/* The record rides on ::before rather than on the <a> itself, so that the
   hover scale and the rotation live on two different elements and never
   overwrite each other's transform. */
.track__disc::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: 50% 50% / contain no-repeat;
  animation: vinyl-spin 3.4s linear infinite;
  animation-play-state: paused;   /* the platter sits still until pointed at */
}
/* Hover or keyboard focus starts the turntable. Pausing the animation
   instead of removing it means the record stops where it stopped and picks
   up from there, rather than snapping back to zero when the cursor leaves. */
.track__disc:hover::before,
.track__disc:focus-visible::before { animation-play-state: running; }

@keyframes vinyl-spin { to { transform: rotate(360deg); } }

/* WebP with a fallback, as everywhere else on the page — except the
   fallback here has to be PNG, not JPG: the record is a circle on a
   transparent square. The plain url() is what browsers without image-set()
   read; the ones that understand it take the second declaration and fetch
   only the WebP. */
.record--sin .track__disc::before {
  background-image: url("assets/covers/vinyl-sin.png");
  background-image: image-set(url("assets/covers/vinyl-sin.webp") type("image/webp"),
                              url("assets/covers/vinyl-sin.png")  type("image/png"));
}
.record--virtues .track__disc::before {
  background-image: url("assets/covers/vinyl-virtues.png");
  background-image: image-set(url("assets/covers/vinyl-virtues.webp") type("image/webp"),
                              url("assets/covers/vinyl-virtues.png")  type("image/png"));
}
.record--here-lies .track__disc::before {
  background-image: url("assets/covers/vinyl-here-lies.png");
  background-image: image-set(url("assets/covers/vinyl-here-lies.webp") type("image/webp"),
                              url("assets/covers/vinyl-here-lies.png")  type("image/png"));
}

.track__num {
  font-family: var(--display);
  font-size: 0.9rem;
  color: var(--amber);
  padding-top: 0.16rem;
  font-variant-numeric: tabular-nums;
}
.track__text { min-width: 0; }
.track__title {
  font-family: var(--display);
  font-size: clamp(1.15rem, 1rem + 0.7vw, 1.5rem);
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--bone);
  margin-bottom: 0.3rem;
}
/* One declaration, two jobs: the singer's name on the three sung records,
   the running time on the instrumental one. They never appear on the same
   row, and sharing the rule is what makes SYNCOPATED · 1:55 sit exactly
   where ENVY · NOVA sits — same size, same tracking, same baseline shift. */
.track__voice,
.track__time {
  display: inline-block;
  font-family: var(--text);
  font-size: 0.64rem; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--amber);
  margin-left: 0.6rem;
  vertical-align: 0.22em;
}
/* Tabular figures so eleven times line up as a column, not as prose. */
.track__time { font-variant-numeric: tabular-nums; letter-spacing: 0.14em; }
.track__answers {
  font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.4rem;
}
.track__answers em { font-style: normal; color: var(--amber); }
.track__note {
  margin: 0;
  color: var(--muted);
  font-size: 0.95rem;
  max-width: 46rem;
  text-wrap: pretty;
}

/* ---------- here lies — teaser, deliberately quieter -------------------- */
.teaser {
  margin-top: clamp(2.5rem, 7vw, 4.5rem);
  padding-block: clamp(2.5rem, 7vw, 4rem);
  border-top: 1px solid var(--amber-dim);
  text-align: center;
}
.teaser .record__eyebrow { color: var(--muted); }
.teaser__title {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(1.5rem, 1.15rem + 1.6vw, 2.1rem);
  letter-spacing: 0.14em;
  color: var(--muted);
  margin-bottom: 1.1rem;
}
.teaser__text { color: var(--muted); font-size: 0.98rem; }
.teaser__soon {
  margin: 0;
  font-size: 0.68rem; font-weight: 600;
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--amber);
}

/* ---------- the inner sleeve — lyrics ----------------------------------

   An LP insert, in this page's own ink. Deliberately NOT paper: the brief
   rules out a cream ground (§3), so this is the other kind of sleeve rock
   records actually shipped — black card printed in gold. Same palette as
   everything else, no second accent, hard corners.

   BELT AND BRACES. The UA stylesheet already hides a shut <dialog>, but a
   browser too old to implement the element would render it as a plain block
   and spill thirty-two songs into the middle of the page. This rule holds
   whether or not the element is understood. */
dialog.sleeve:not([open]) { display: none; }

/* With a sleeve open the document behind it must not scroll. */
.has-sleeve, .has-sleeve body { overflow: hidden; }

.sleeve {
  inset: 0;
  margin: auto;
  width: min(46rem, 100%);
  height: min(88vh, 100%);
  max-width: 100%; max-height: 100%;
  padding: 0;
  border: 1px solid var(--amber-dim);
  border-radius: 1px;
  background: var(--black);
  color: var(--bone);
  overflow: hidden;
}
.sleeve::backdrop { background: rgba(3, 3, 2, 0.9); }

.sleeve__sheet { display: flex; flex-direction: column; height: 100%; }

/* The head does not scroll away: on a twelve-song sleeve the way out has to
   stay in reach at every depth. */
.sleeve__head {
  flex: none;
  position: relative;
  padding: 1.4rem clamp(1.1rem, 4vw, 2.4rem) 1.1rem;
  border-bottom: 1px solid var(--amber-dim);
  background: var(--black);
}
.sleeve__eyebrow {
  margin: 0 0 0.35rem;
  font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--amber);
}
.sleeve__title {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(1.7rem, 1.3rem + 1.8vw, 2.6rem);
  letter-spacing: 0.05em;
  color: var(--bone);
  padding-right: 3rem;         /* keeps the longest title off the × */
}
/* Same round × as the member files — one gesture, learned once. */
.sleeve__close {
  position: absolute; top: 1.15rem; right: clamp(1.1rem, 4vw, 2.4rem);
  width: 2.2rem; height: 2.2rem;
  padding: 0; border: 1px solid var(--amber); border-radius: 50%;
  background: rgba(10, 9, 8, 0.7);
  cursor: pointer;
  transition: background-color 200ms var(--ease), transform 200ms var(--ease);
}
.sleeve__close span { position: absolute; inset: 0; }
.sleeve__close span::before,
.sleeve__close span::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 0.85rem; height: 1px; background: var(--amber);
}
.sleeve__close span::before { transform: translate(-50%, -50%) rotate(45deg); }
.sleeve__close span::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.sleeve__close:hover { background: rgba(200, 150, 60, 0.18); transform: rotate(90deg); }

/* The sheet scrolls, so it owns a scrollbar. Without this the browser
   paints the light one, a pale strip down the edge of a black insert. */
.sleeve { color-scheme: dark; }

.sleeve__body {
  overflow-y: auto;
  overscroll-behavior: contain;   /* the page behind must not scroll along */
  padding: clamp(1.4rem, 4vw, 2.4rem);
  scroll-behavior: smooth;
}

/* The printed tracklist at the head of the insert, which happens also to be
   the fastest way to the eleventh song on a phone. */
.sleeve__index { margin-bottom: clamp(2rem, 6vw, 3rem); }
.sleeve__index ol {
  list-style: none; margin: 0; padding: 0;
  border-top: 1px solid var(--amber-dim);
}
.sleeve__index a {
  display: flex; gap: 0.9rem; align-items: baseline;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--amber-dim);
  color: var(--muted);
  font-size: 0.82rem; letter-spacing: 0.06em;
  text-decoration: none;
  transition: color 200ms var(--ease);
}
.sleeve__index a:hover,
.sleeve__index a:focus-visible { color: var(--amber-lit); }
.sleeve__n {
  font-family: var(--display); color: var(--amber);
  font-variant-numeric: tabular-nums;
}

.sleeve__songs { display: grid; gap: clamp(2.4rem, 7vw, 3.6rem); }

.song { scroll-margin-top: 0.5rem; }
.song__head {
  font-family: var(--display); font-weight: 700;
  font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);
  letter-spacing: 0.03em;
  color: var(--bone);
  padding-bottom: 0.5rem;
  margin-bottom: 1.1rem;
  border-bottom: 1px solid var(--amber-dim);
}
.song__num {
  color: var(--amber);
  margin-right: 0.5rem;
  font-variant-numeric: tabular-nums;
}
/* Verse, so ragged right and never justified — the hyphenation rule that
   governs the prose elsewhere on the page would break lines mid-word here.
   The measure is held narrow on purpose: a lyric line that wraps reads as
   two lines, and the stanza stops scanning. */
.song p {
  margin: 0 0 1.15rem;
  max-width: 34rem;
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.75;
  text-align: left;
  text-wrap: pretty;
  hyphens: manual;
}
.song p:last-child { margin-bottom: 0; }

.sleeve__foot {
  margin: clamp(2.4rem, 7vw, 3.4rem) 0 0;
  padding-top: 1.1rem;
  border-top: 1px solid var(--amber-dim);
  color: var(--muted);
  font-size: 0.76rem; line-height: 1.6;
}

/* Phone: the insert is the whole screen. A centred sheet with a margin
   around it wastes the only thing a phone is short of. */
@media (max-width: 40em) {
  .sleeve {
    width: 100%; height: 100%;
    border: 0;
  }
}

/* ---------- listen ------------------------------------------------------ */
.listen { display: grid; gap: clamp(2.5rem, 7vw, 4rem); }
.listen__album {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(1.6rem, 1.3rem + 1.4vw, 2.2rem);
  letter-spacing: 0.05em;
  color: var(--bone);
}
.listen__eyebrow {
  font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--amber);
  margin: 0.5rem 0 1.5rem;
}
/* Mark first, name beside it, both packed to the left. The arrow is gone:
   on a wide column it left a corridor of dead space between the label and
   itself. The list is narrowed to the content instead of stretched to the
   grid, so there is no emptiness left to fill.

   The logos are the official monochrome marks, inlined as SVG — no file, no
   request, no dependency — and painted with currentColor so they inherit
   the amber and never introduce a second accent. */
.platforms { border-top: 1px solid var(--amber-dim); max-width: 17rem; }
.platforms a {
  display: flex; align-items: center; gap: 0.95rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--amber-dim);
  font-size: 0.8rem; font-weight: 600;
  letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--bone);
  transition: color 200ms var(--ease);
}
.pf {
  flex: none;
  width: 1.35rem; height: 1.35rem;
  fill: var(--amber);
  color: var(--amber);          /* the drawn vinyl uses stroke: currentColor */
  transition: transform 220ms var(--ease);
}
/* Amazon's mark is the wordmark-plus-smile lockup; at 22px the word "music"
   is an unreadable smear, so only the smile is cropped in — the half that
   actually reads at this size. It is wide and shallow, so it keeps the
   column width and finds its own height. */
.pf--wide { height: auto; }
.platforms a:hover { color: var(--amber); }
.platforms a:hover .pf,
.platforms a:focus-visible .pf { transform: scale(1.12); }

/* auto-fit rather than a fixed count: one column on a phone, two on a
   tablet, three from roughly 1024px — where SIN, VIRTUES and the art
   channel line up side by side without any of them being squeezed. */
@media (min-width: 44em) {
  .listen { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
}

/* The art column mirrors the streaming ones: same 17rem measure, so the
   guitar sits flush with the link rows beside it and the three columns
   read as one row rather than as an image dropped next to two lists.
   The image is the whole target — no caption, no link row underneath: the
   "Art Track" lettering is already inside the artwork. */
.listen__col--art .artlink {
  display: block;
  max-width: 17rem;
}
.listen__col--art .artlink img {
  width: 100%; height: auto;
  transition: transform 260ms var(--ease), filter 260ms var(--ease);
}
.listen__col--art .artlink:hover img,
.listen__col--art .artlink:focus-visible img {
  transform: scale(1.03);
  filter: brightness(1.08);
}

/* ---------- footer ------------------------------------------------------ */
.footer {
  border-top: 1px solid var(--amber-dim);
  padding-block: clamp(3rem, 8vw, 5rem);
  text-align: center;
}
.footer p { color: var(--muted); font-size: 0.86rem; }
.footer__credit { color: var(--bone) !important; margin-bottom: 0.35rem; }
.footer__contact { font-size: 0.86rem; }
.footer__contact a {
  color: var(--amber);
  border-bottom: 1px solid var(--amber-dim);
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}
.footer__contact a:hover { color: var(--amber-lit); border-bottom-color: var(--amber-lit); }
/* The honesty line: quiet, as the brief asks, but never whispered into
   illegibility — it still clears AA on black, and sits inside its own
   measure so it reads as a statement rather than as fine print. */
.footer__notice {
  max-width: 34rem;
  margin: 1.9rem auto 0;
  padding-top: 1.6rem;
  border-top: 1px solid var(--amber-dim);
  font-size: 0.82rem !important;
  line-height: 1.65;
  text-wrap: pretty;
}
.footer__meta {
  font-size: 0.66rem !important; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--amber) !important;
  margin-top: 2.25rem;
}
.footer__copy { font-size: 0.75rem !important; margin: 0; }
/* The band name is set in the display face wherever it appears as a NAME
   rather than as running prose. In Barlow at 12px the capital N reads as
   "nN" to some eyes — the same glyph ambiguity that makes "Iaci" look like
   "laci" — and the one word nobody may misread is the band's own. Cinzel
   is all-caps by design, so the ambiguity cannot occur. */
.brandmark {
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.15em;
  letter-spacing: 0.08em;
  color: var(--bone);
}

/* ==========================================================================
   MOTION
   One orchestrated moment: the page opens in darkness and a beam of light
   reveals the cast. Everything after that is a short fade, nothing more.
   No parallax, no animated counters.
   ========================================================================== */

/* Nothing is hidden until script.js is RUNNING and has proved it can undo
   the hiding: it sets .reveal-ready itself, as its first action. A <head>
   flag would be set even when the script later fails to load, which would
   strand the whole page at opacity 0. If the script never runs, none of
   these rules ever apply and the page simply renders, fully lit. */
.reveal-ready .hero__media { opacity: 0; }
.reveal-ready [data-reveal] { opacity: 0; }
/* The hint is part of the same opening: hidden with everything else, and
   brought up last, after the nav has faded in. */
.reveal-ready .hero__scroll { opacity: 0; }

.is-lit .hero__media {
  animation: beam-reveal 1750ms var(--ease) 60ms both;
}
.is-lit .hero__beam {
  animation: beam-sweep 1900ms var(--ease) 60ms both;
}
.is-lit .hero__body > * {
  animation: rise 900ms var(--ease) both;
}
.is-lit .telemetry      { animation-delay: 900ms; }
.is-lit .hero__premise  { animation-delay: 1040ms; }
.is-lit .hero__cta      { animation-delay: 1180ms; }
.is-lit .nav            { animation: fade 800ms var(--ease) 1300ms both; }
/* The hint comes up last, after the nav. A transition and not `animation:
   fade`, because the dismissal has to be able to take this opacity back
   down later, and a filled animation would outrank it. */
.is-lit .hero__scroll   { opacity: 1; transition: opacity 900ms var(--ease) 1560ms; }

@keyframes beam-reveal {
  0%   { opacity: 0; clip-path: inset(0 47% 0 47%); filter: brightness(0.12) contrast(1.25); transform: scale(1.05); }
  38%  { opacity: 1; }
  100% { opacity: 1; clip-path: inset(0 0 0 0);     filter: brightness(1) contrast(1);       transform: scale(1); }
}
@keyframes beam-sweep {
  0%   { opacity: 0; transform: scaleX(0.12); }
  30%  { opacity: 1; }
  100% { opacity: 0; transform: scaleX(1.5); }
}
@keyframes rise  { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes fade  { from { opacity: 0; } to { opacity: 1; } }

/* scroll-in: short, discreet */
.reveal-ready [data-reveal] { transform: translateY(16px); }
.reveal-ready [data-reveal].is-visible {
  opacity: 1; transform: none;
  transition: opacity 760ms var(--ease), transform 760ms var(--ease);
}

/* ---------- reduced motion: nothing moves, nothing is hidden ------------ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal-ready .hero__media,
  .reveal-ready [data-reveal] { opacity: 1 !important; transform: none !important; }
  .is-lit .hero__media,
  .is-lit .hero__beam,
  .is-lit .hero__body > *,
  .is-lit .nav { animation: none !important; }
  .hero__beam { opacity: 0 !important; }
  /* the scroll hint is simply there from the start, and holds still. Its
     transform is NOT cleared — it is what centres it — so it gets its own
     rule instead of joining the transform:none list above. */
  .reveal-ready .hero__scroll { opacity: 1; }
  .is-lit .hero__scroll { transition: none; }
  .hero__scroll-mark { animation: none !important; }
  .hero > .hero__scroll.is-gone { transform: translateX(-50%); transition: none; }
  /* the energy wave stops completely: wallpaper stays static */
  .hero > .hero__wave { animation: none !important; opacity: 0 !important; }
  .reveal-ready [data-reveal].is-visible { transition: none; }
  .platforms a, .pf, .nav__list a, .btn, .track__disc,
  .listen__col--art .artlink img { transition: none; }
  .track__disc:hover, .track__disc:focus-visible { transform: none; }
  /* the record never spins; it just sits there, label up */
  .track__disc::before { animation: none; }
  /* the sample disc holds still too — the pause glyph carries the state */
  .sample__disc::before { animation: none; }
  .sample:hover .sample__disc, .sample:focus-visible .sample__disc { transform: none; }
  .platforms a:hover .pf, .platforms a:focus-visible .pf,
  .listen__col--art .artlink:hover img,
  .listen__col--art .artlink:focus-visible img { transform: none; filter: none; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; }
}

/* ---------- justified prose --------------------------------------------
   Now applied at EVERY width, phones included (George, 29 Aug 2026).

   The rev2 spec had cut this off below 768px, fearing rivers of white space
   in a narrow column. Hyphenation is what makes it hold: with `hyphens:
   auto` the browser breaks long words instead of stretching the spaces, so
   the narrowest column on the page (the track notes, 258px at 375px wide)
   stays even. Without hyphenation this would genuinely fall apart — the two
   declarations belong together, do not keep one and drop the other.

   Running prose ONLY — never headings, eyebrows, link rows, footer lines,
   member files or short fragments, where justifying one line stretches the
   words grotesquely. Hyphenation needs lang="en" on <html>, which is set.

   Scoped to #transmission on purpose: a bare .wrap--narrow would also match
   the footer and the HERE LIES teaser. */
.lede,
.cast__row p,
#transmission .wrap--narrow p:not(.turn),
.record__blurb,
.track__note {
  text-align: justify;
  text-justify: inter-word;
  -webkit-hyphens: auto;
          hyphens: auto;
}

/* The hero premise is the ONE exception that stays gated at 900px, and for
   a different reason than column width: below that the hero is a single
   centred column, and justifying a centred block strands its last line hard
   left against a centred telemetry line and button. It is the alignment
   that clashes there, not the measure. */
@media (min-width: 56.25em) {
  .hero__premise {
    text-align: justify;
    text-justify: inter-word;
    -webkit-hyphens: auto;
            hyphens: auto;
  }
}

/* ---------- print: the text survives ------------------------------------ */
@media print {
  .nav, .grain, .hero__beam, .skip { display: none !important; }
  body { background: #fff; color: #000; }
  .reveal-ready .hero__media,
  .reveal-ready [data-reveal] { opacity: 1 !important; transform: none !important; }
}

/* ==========================================================================
   ARCHIVE — gallery + image viewer
   ========================================================================== */

.gallery__lede {
  max-width: 34rem; margin: 0 auto clamp(2rem, 5vw, 3rem);
  text-align: center; color: var(--muted); text-wrap: pretty;
}

.gallery { display: grid; gap: clamp(0.6rem, 2vw, 1.1rem); }

/* Every tile is locked to a ratio so the rows line up. The source files do
   NOT share one: the panoramas run from 3:1 to 8:5 and the portraits are
   about 1:2.25. Left at their natural heights the grid came out ragged, so
   each group gets a single ratio and the image fills it.
   The full frame is never lost — the viewer always shows it uncropped. */
.gallery--wide { margin-bottom: clamp(0.6rem, 2vw, 1.1rem); }
@media (min-width: 50em) { .gallery--wide { grid-template-columns: 1fr 1fr; } }
.gallery--wide .shot img { aspect-ratio: 16 / 9; }

.gallery--grid { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 40em) { .gallery--grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 62em) { .gallery--grid { grid-template-columns: repeat(5, 1fr); } }
.gallery--grid .shot img { aspect-ratio: 1 / 2; }

.shot {
  display: block; width: 100%; padding: 0; margin: 0;
  border: 1px solid var(--amber-dim);
  background: var(--black);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 220ms var(--ease);
}
.shot img {
  width: 100%; height: auto; display: block;
  object-fit: cover;
  object-position: center;
  transition: transform 320ms var(--ease), filter 320ms var(--ease);
}
.shot:hover, .shot:focus-visible { border-color: var(--amber); }
.shot:hover img, .shot:focus-visible img { transform: scale(1.04); filter: brightness(1.12); }

/* ---------- viewer ----------
   The dialog wraps the photograph and nothing else. .sleeve sets a fixed
   46rem × 88vh box for the lyric sheets, which left a big empty frame
   around a picture — all of that is undone here so the window is exactly
   the size of the image inside it. */
.shot__view {
  /* fit-content, not auto: .sleeve sets inset:0, and with inset pinning all
     four edges an `auto` width stretches to fill the viewport — which is
     exactly the oversized window this replaces. margin:auto then centres
     the shrunken box. */
  width: fit-content; height: fit-content;
  inset: 0; margin: auto;
  max-width: 100vw; max-height: 100svh;
  padding: 0; border: 0; background: transparent; overflow: visible;
}
.shot__frame {
  position: relative;
  display: block;
  padding: 0;
  line-height: 0;        /* kills the inline-box gap under the image */
  font-size: 0;
}
.shot__full {
  display: block;
  width: auto; height: auto;
  max-width: min(94vw, 1400px);
  max-height: 94svh;
  border: 1px solid var(--amber-dim);
}
.shot__close { position: absolute; top: 0.6rem; right: 0.6rem; z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  .shot img { transition: none; }
  .shot:hover img, .shot:focus-visible img { transform: none; filter: none; }
}

/* ==========================================================================
   BILINGUAL — English / Brazilian Portuguese
   Decision revised by George on 7 Sep 2026; the brief's §2 rule against a
   language switch no longer applies and was updated with it.

   Both languages sit in the HTML. Nothing is injected by script, so each
   version is real content for search engines and screen readers, and the
   page still reads if JavaScript never runs — it simply stays in English,
   which is the default carried on <html data-ui="en">.
   ========================================================================== */
[data-lang] { display: none; }
html[data-ui="en"] [data-lang="en"],
html[data-ui="pt"] [data-lang="pt"] { display: revert; }

/* the switch */
.lang {
  display: flex; align-items: center; gap: 0.3rem;
  flex: none;
}
.lang__btn {
  padding: 0.35rem 0.4rem; border: 0; background: none;
  font-family: var(--text); font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--muted); cursor: pointer;
  transition: color 200ms var(--ease);
}
.lang__btn:hover { color: var(--bone); }
.lang__btn[aria-pressed="true"] {
  color: var(--amber);
  box-shadow: inset 0 -1px 0 0 currentColor;
}
.lang__sep { color: var(--amber-dim); font-size: 0.66rem; }

/* Sits at the top of each inner sleeve, in Portuguese only: the lyrics
   below it are staying in English and a reader who switched language
   deserves to know that before scrolling through them. */
.sleeve__lang {
  margin: 0 0 clamp(1.4rem, 4vw, 2rem);
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--amber-dim);
  color: var(--muted);
  font-size: 0.76rem; line-height: 1.6;
}

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