/* ===== 00-base.css ================================================= */
/* ==========================================================================
   Syncopation — site styles
   Ported from the original design export. Tokens and measurements are the
   designer's; the JS-driven breakpoints have been rewritten as media queries.
   ========================================================================== */

/* --- Font ---------------------------------------------------------------- */

@font-face {
  font-family: 'Alex Brush';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/alex-brush-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Tokens -------------------------------------------------------------- */
/* Dark is the base; light overrides. site.js stamps data-theme before paint. */

:root {
  --bg: #0F1113;
  --bg2: #191A1C;
  --panel: #2B2B2B;
  --line: rgba(242, 237, 227, .14);
  --fg: #F2EDE3;
  --fg2: rgba(242, 237, 227, .66);
  /* designer had .42 here — 3.7:1 on --bg, below AA. .56 gives 5.7:1 */
  --fg3: rgba(242, 237, 227, .56);
  --accent: #F2EDE3;
  --on-accent: #0F1113;
  --link: #F2EDE3;
  --navbg: rgba(15, 17, 19, .72);

  --maxw: 1024px;
  --maxw-wide: 1100px;
  --gutter: 22px;
  --radius: 22px;
  --radius-card: 18px;
  --gap: 18px;
  --header-h: 48px;
}

html[data-theme="light"] {
  --bg: #FFFFFF;
  --bg2: #F5F5F7;
  --panel: #FFFFFF;
  --line: rgba(29, 29, 31, .12);
  --fg: #1D1D1F;
  --fg2: #6E6E73;
  --fg3: #6E6E73;
  --accent: #8B3A3A;
  --on-accent: #FFFFFF;
  --link: #8B3A3A;
  --navbg: rgba(255, 255, 255, .72);
}

/* --- Reset --------------------------------------------------------------- */

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

/* Any author `display` rule beats the UA stylesheet's [hidden] { display:none },
   so components like .card (display:flex) stay painted when hidden. Restore it. */
[hidden] { display: none !important; }

html { scroll-behavior: smooth; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  -webkit-font-smoothing: antialiased;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
               "Helvetica Neue", Helvetica, sans-serif;
  text-wrap: pretty;
  letter-spacing: -.011em;
  overflow-x: hidden;
}

img { max-width: 100%; }

a {
  color: var(--fg);
  text-decoration: none;
  transition: opacity .2s ease;
}
a:hover { opacity: .72; }

::selection { background: var(--accent); color: var(--on-accent); }

/* Visible focus — absent from the export, required for keyboard use */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--accent);
  color: var(--on-accent);
  padding: 12px 20px;
  border-radius: 0 0 8px 0;
  font-size: 14px;
  font-weight: 500;
}
.skip-link:focus { left: 0; }

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

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

/* --- Layout -------------------------------------------------------------- */

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
.wrap--wide { max-width: var(--maxw-wide); }

.section { padding: clamp(56px, 7vw, 100px) var(--gutter); background: var(--bg); }
.section--alt { background: var(--bg2); border-top: 1px solid var(--line); }
.section--center { text-align: center; }

/* --- Typography ---------------------------------------------------------- */

.wordmark { font-family: 'Alex Brush', cursive; line-height: 1; }

.eyebrow {
  font-size: 21px;
  font-weight: 600;
  color: var(--accent);
  margin-bottom: 12px;
}

h1, .h1 {
  font-size: clamp(46px, 5.8vw, 76px);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -.025em;
  margin: 0 0 18px;
}

h2, .h2 {
  font-size: clamp(34px, 4vw, 50px);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -.022em;
  margin: 0 0 14px;
}

.lead {
  font-size: 21px;
  line-height: 1.4;
  color: var(--fg2);
  margin: 0 auto 22px;
  max-width: 560px;
}

.link-row {
  display: flex;
  gap: 26px;
  justify-content: center;
  flex-wrap: wrap;
  font-size: 17px;
}
.link-row a { color: var(--link); }

/* Long-form body copy. Sits between .lead (21px) and .care p (15px) — without
   it every narrative page reinvents the same paragraph style. */
.prose > p {
  font-size: 19px;
  line-height: 1.55;
  color: var(--fg2);
  margin: 0 0 1.1em;
}
.prose > p:last-child { margin-bottom: 0; }

/* Embedded players (Apple Music, Spotify, YouTube). Any page embedding one
   wants this — the panel background sits correctly in both themes. */
.embed {
  max-width: 660px;
  margin: 0 auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--panel);
}
.embed iframe {
  display: block;
  width: 100%;
  max-width: 100%;
  border: 0;
  background: transparent;
}

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

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 80;
  background: var(--navbg);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--line);
}

.header__bar {
  max-width: var(--maxw);
  margin: 0 auto;
  height: var(--header-h);
  padding: 0 var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* The brand mark. It is an SVG filled with currentColor, so it needs no
   light/dark variant and no swap on the theme toggle: it inherits --fg through
   the link, lands on the theme's exact ink colour rather than pure black or
   white, and picks up the a:hover fade and the focus ring for free.

   --mark-h is the only number to change; the width follows the artwork's own
   874x410 ink box. The margin is an optical correction, not a gap: the word
   sits in the lower half of the mark and the swash above it is mostly air, so
   centring the box centres nothing the eye is looking at. Four pixels of
   bottom margin lifts the painted mark two, which is as far as it can go
   before the top of the swash reaches the edge of a 48px bar. */
.header__brand {
  --mark-h: 38px;
  display: block;
  margin-bottom: 4px;
  flex: none;
}
/* The fallback is not decoration: the footer uses this partial too, and an
   unset --mark-h would make `height` invalid, drop it to auto, and let the
   SVG's own width/height attributes decide the size. */
.brand-mark {
  display: block;
  height: var(--mark-h, 38px);
  width: auto;
  aspect-ratio: 874 / 410;
}

.nav {
  display: flex;
  align-items: center;
  gap: clamp(12px, 2.2vw, 30px);
  font-size: 12px;
}
.nav a { color: var(--fg2); }
.nav a[aria-current="page"] { color: var(--fg); font-weight: 500; }

.header__actions { display: flex; align-items: center; gap: 14px; flex: none; }

.icon-btn {
  width: 44px;
  height: 44px;
  margin: -9px 0;
  border: 0;
  background: transparent;
  color: var(--fg2);
  cursor: pointer;
  font-size: 14px;
  display: grid;
  place-items: center;
  font-family: inherit;
  border-radius: 50%;
}
.icon-btn:hover { color: var(--fg); }

.menu-btn { display: none; color: var(--fg); font-size: 15px; }

.menu {
  display: none;
  flex-direction: column;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  padding: 8px var(--gutter) 22px;
}
.menu[data-open="true"] { display: flex; }
.menu a {
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
  font-size: 19px;
  font-weight: 500;
}
.menu a:last-child { border-bottom: 0; }

.header-spacer { height: var(--header-h); }

/* --- Announcement bar ---------------------------------------------------- */

.announce {
  background: var(--bg2);
  border-bottom: 1px solid var(--line);
  padding: 11px var(--gutter);
  text-align: center;
  font-size: 13px;
  color: var(--fg2);
}
.announce strong { color: var(--fg); font-weight: 500; }
.announce a { color: var(--link); }

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

.hero-media {
  max-width: var(--maxw-wide);
  margin: clamp(36px, 5vw, 60px) auto clamp(40px, 5vw, 64px);
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--bg2);
}
.hero-media img,
.hero-media picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ==========================================================================
   Text overlays on artwork
   --------------------------------------------------------------------------
   The supplied artwork has the Syncopation wordmark and the chapter names
   burned into the pixels. Baked type cannot be cropped for mobile without
   slicing letters, cannot be selected, translated or read by a screen reader,
   and cannot re-flow. These overlays replace it with live text.

   SVG rather than HTML text on purpose: a viewBox makes the type scale in
   exact proportion to the image at every width, so one declaration covers
   every breakpoint with no font-size media queries and no risk of the words
   wrapping or overflowing their plate.

   Hidden while <html data-art="legacy"> so the current artwork does not read
   twice. Flip "cleanArt": true in src/config.json when the clean files land.
   ========================================================================== */

.plate { position: relative; overflow: hidden; }

.plate__overlay {
  position: absolute;
  display: block;
  overflow: visible;
  pointer-events: none;
  /* the artwork can be light or dark; a soft shadow keeps type legible on both */
  filter: drop-shadow(0 1px 10px rgba(15, 17, 19, .34));
}
.plate__overlay text { fill: #F2EDE3; }

/* .plate--clean marks a single plate whose artwork arrived WITHOUT burned-in
   type, ahead of the rest of the set. It opts that one plate out of the legacy
   gate so its overlay and scrim both render, which is what the cleanArt switch
   will eventually do for everything at once. Written as :not() rather than a
   later override so the two rules cannot be reordered into a specificity fight. */
html[data-art="legacy"] .plate:not(.plate--clean) .plate__overlay { display: none; }

/* The Alex Brush wordmark, as used across the banners and heroes. */
.plate__mark {
  left: 6%;
  right: 6%;
  width: 88%;
  bottom: 7%;
}
.plate__mark text {
  font-family: 'Alex Brush', cursive;
  font-size: 88px;          /* viewBox units — scales with the plate */
}

/* Chapter names on the square stills. */
.plate__label {
  left: 6%;
  right: 6%;
  width: 88%;
  bottom: 8%;
}
.plate__label text {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", sans-serif;
  font-size: 118px;         /* viewBox units — matches the burned-in original */
  font-weight: 300;
  letter-spacing: -1px;
}

/* A caption line rather than a mark — e.g. the sentence burned across the
   Our Story closing banner. Set the text uppercase in the markup; SVG does not
   honour text-transform reliably across engines. */
.plate__line {
  left: 8%;
  right: 8%;
  width: 84%;
  bottom: 9%;
}
/* Set as a caption rather than a headline. At the original 46px/2.5px the
   sentence measured 1281 units against a 1200 viewBox — it overflowed its own
   box by 7% and ran nearly the full width of the plate, which is a headline's
   job, not a closing line's. 34px brings it inside the box and the width it
   gives back goes into the tracking instead.

   .14em is the tracking the site already uses on caps elsewhere (share.css),
   and word-spacing is opened past it on purpose: at this tracking the words
   fuse into one ribbon without it. The 300 renders only where SF Pro Display
   is installed — sans-serif has no Light on Windows and it falls back to 400,
   so the treatment has to hold on size and spacing alone, and does. */
.plate__line text {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", sans-serif;
  font-size: 34px;          /* viewBox units */
  font-weight: 300;
  letter-spacing: .14em;
  word-spacing: .18em;
}

/* Scrim so live type stays readable over pale artwork — the Velouté cherry
   and the Hold Space bonsai are both near-white where the label sits. */
.plate__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top, rgba(15, 17, 19, .46), rgba(15, 17, 19, 0) 58%);
}
/* The scrim rides with the overlays: with the type still burned into the pixels
   it would be darkening artwork for nobody. Same .plate--clean opt-out. */
html[data-art="legacy"] .plate:not(.plate--clean) .plate__scrim { display: none; }

/* On a clean plate the scrim has live type sitting on it, so it takes the same
   gradient the chapter tiles use for exactly that job — .ch-card::before in
   home.css, stop for stop. Two-stop ramps were tried at half this weight and
   read as the photo's own lighting rather than as a treatment.
   Raising the base .plate__scrim instead would be wrong: it is shared with the
   still tiles, whose art still carries its own burned-in type.

   ...which stops being true the moment cleanArt flips, so the second selector
   below carries the same ramp to every plate once the overlays are live. That
   is the one thing .plate--clean was doing that the switch did NOT reproduce,
   and it was inverted: measured on 12 August 2026 by flipping the switch, every
   plate carrying live type had the light two-stop ramp, while the only plates
   on this heavy one were the three that carry no overlay at all.

   Measured on the current photography, hiding the ink and sampling the artwork
   inside its box, worst case is the 95th-percentile pixel. On the light ramp
   nine of the ten live captions came in under 4.5:1 — Velouté 1.82:1 and
   Exuberance 2.09:1, both white on a near-white backdrop, held up only by the
   drop-shadow that WCAG gives no credit for. On this ramp all ten clear it,
   the lowest being Velouté at 5.26:1. Predictive because the clean files are
   these same photographs with the burned-in type taken off, so what sits under
   each caption does not change. */
.plate--clean .plate__scrim,
html[data-art="clean"] .plate__scrim {
  background: linear-gradient(to top,
              rgba(15, 17, 19, .88) 0%,
              rgba(15, 17, 19, .42) 42%,
              rgba(15, 17, 19, .06) 78%);
}

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

.btn {
  border: 0;
  border-radius: 980px;
  background: var(--accent);
  color: var(--on-accent);
  padding: 14px 28px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 500;
  font-family: inherit;
}
.btn:hover { opacity: .85; }
.btn--block { width: 100%; padding: 12px 0; font-size: 14px; }
.btn--quiet {
  background: var(--bg2);
  color: var(--fg2);
  border: 1px solid var(--line);
}

/* --- Product cards ------------------------------------------------------- */

.grid { display: grid; gap: var(--gap); }
.grid--shop { grid-template-columns: repeat(3, 1fr); text-align: left; }
.grid--looks { grid-template-columns: repeat(4, 1fr); }
.grid--care { grid-template-columns: repeat(4, 1fr); gap: clamp(22px, 3vw, 40px); }
/* Three-column sibling of .grid--care. Three page conversions each grew their
   own copy of this; one definition serves all. */
.grid--three { grid-template-columns: repeat(3, 1fr); gap: clamp(22px, 3vw, 40px); }

.card {
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
  transition: transform .45s cubic-bezier(.22, .61, .36, 1), box-shadow .45s ease;
}
.card:hover { transform: translateY(-6px); box-shadow: 0 24px 48px rgba(15, 17, 19, .16); }

.card__media {
  position: relative;
  /* 3:4 portrait, matching the -p crops cut from the full-resolution originals.
     Shows the whole garment instead of the design tool's 760x760 torso square. */
  aspect-ratio: 3 / 4;
  overflow: hidden;
  background: var(--bg2);
}
.card__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 30%);
  transition: transform 1s cubic-bezier(.22, .61, .36, 1);
}
.card:hover .card__media img { transform: scale(1.06); }

.card__veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(15, 17, 19, .7), rgba(15, 17, 19, 0) 60%);
  opacity: 0;
  transition: opacity .45s ease;
  pointer-events: none;
}
.card:hover .card__veil { opacity: 1; }

.tag {
  position: absolute;
  border-radius: 980px;
  padding: 5px 11px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
}
.tag--chapter { left: 12px; top: 12px; background: rgba(255, 255, 255, .92); color: #1D1D1F; }
.tag--sold { right: 12px; top: 12px; background: rgba(15, 17, 19, .78); color: #F2EDE3; }

.card__body {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 18px 18px 20px;
  flex: 1;
}
.card__row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: baseline;
}
.card__name { font-size: 16px; font-weight: 600; letter-spacing: -.013em; }
.card__price { font-size: 15px; color: var(--fg2); white-space: nowrap; }
.card__meta { font-size: 13px; color: var(--fg3); }
.card__foot { margin-top: auto; padding-top: 14px; display: flex; }

/* --- Lookbook ------------------------------------------------------------ */

.look {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: 16px;
  background: var(--bg2);
  transition: transform .45s cubic-bezier(.22, .61, .36, 1), box-shadow .45s ease;
}
.look:hover { transform: translateY(-6px); box-shadow: 0 24px 48px rgba(15, 17, 19, .16); }
.look img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 40%);
  transition: transform 1s cubic-bezier(.22, .61, .36, 1);
}
.look:hover img { transform: scale(1.06); }
.look__label {
  position: absolute;
  left: 14px;
  bottom: 14px;
  font-size: 12px;
  font-weight: 500;
  color: #F2EDE3;
}

/* --- Filters ------------------------------------------------------------- */

.filters {
  display: inline-flex;
  background: var(--bg);
  border-radius: 980px;
  padding: 3px;
  margin-bottom: clamp(28px, 3.4vw, 44px);
  justify-content: center;
  /* never wrap — a pill container with a second row reads as broken.
     Scrolls instead if categories are ever added. */
  flex-wrap: nowrap;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
.filters::-webkit-scrollbar { display: none; }
.filters button {
  border: 0;
  border-radius: 980px;
  background: transparent;
  color: var(--fg2);
  /* min-height keeps the tap target usable; the design's 9px padding alone
     produced a ~33px control, under the accessible floor */
  min-height: 38px;
  padding: 9px 22px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 500;
  font-family: inherit;
  white-space: nowrap;
}
.filters button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }

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

.form-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}
.input {
  flex: 1;
  min-width: 230px;
  background: var(--bg2);
  border: 1px solid var(--line);
  border-radius: 980px;
  outline: none;
  padding: 14px 20px;
  color: var(--fg);
  font-size: 15px;
  font-family: inherit;
}
.input::placeholder { color: var(--fg3); }
.input:focus-visible { border-color: var(--accent); }

/* Required-field marker (WCAG 3.3.2 — the `required` attribute alone tells
   assistive tech but shows sighted users nothing). aria-hidden on the glyph
   because `required` already conveys the state to a screen reader; announcing
   "asterisk" as well is just noise. */
.req {
  color: var(--accent);
  margin-left: .18em;
  font-weight: 600;
}
.form-legend {
  font-size: 13px;
  color: var(--fg3);
  margin: 0 0 18px;
}
.form-legend .req { margin-left: 0; margin-right: .3em; }

.form-msg {
  font-size: 13px;
  color: var(--accent);
  margin-top: 14px;
  min-height: 16px;
}
.form-note { font-size: 12px; color: var(--fg3); margin-top: 10px; }

/* --- Dialog -------------------------------------------------------------- */

.dialog {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--panel);
  color: var(--fg);
  padding: 0;
  max-width: 420px;
  width: calc(100% - 44px);
}
.dialog::backdrop { background: rgba(15, 17, 19, .6); backdrop-filter: blur(4px); }
.dialog__inner { padding: 26px 24px 24px; }
.dialog__title { font-size: 21px; font-weight: 600; letter-spacing: -.02em; margin: 0 0 6px; }
.dialog__sub { font-size: 14px; color: var(--fg2); margin: 0 0 18px; }
/* Stack the dialog form — at 420px the input and button can't share a row
   without the button orphaning at half width. */
.dialog .form-row { flex-direction: column; align-items: stretch; }
.dialog .form-row .btn { width: 100%; }

.dialog__close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 36px;
  height: 36px;
  border: 0;
  background: transparent;
  color: var(--fg2);
  font-size: 16px;
  cursor: pointer;
  border-radius: 50%;
}

/* --- Care grid ----------------------------------------------------------- */

/* margin-top reset so this works as a real <h3>, not only as the <div> the
   merch page uses it as. */
.care__title { font-size: 19px; font-weight: 600; margin-top: 0; margin-bottom: 8px; }
.care p { font-size: 15px; line-height: 1.55; color: var(--fg2); margin: 0; }

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

.footer {
  background: var(--bg2);
  border-top: 1px solid var(--line);
  padding: 36px var(--gutter) 44px;
  font-size: 12px;
  color: var(--fg2);
}
.footer__cols {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 26px 40px;
  padding-bottom: 26px;
  border-bottom: 1px solid var(--line);
}
.footer__head { font-size: 12px; font-weight: 600; color: var(--fg); margin-bottom: 12px; }
.footer__list { display: flex; flex-direction: column; gap: 9px; }
.footer__list a { color: var(--fg2); }
.footer__strap,
.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: space-between;
  color: var(--fg3);
}
.footer__strap { padding-top: 20px; }
.footer__legal { padding-top: 14px; align-items: center; gap: 14px; }
/* The closing line: copyright and the build credit, centred under everything
   else. Both are small print, so they sit below the row that carries the two
   things a reader might actually want — the privacy link and the brand mark.

   Centred by justify-content rather than text-align, so the two stay centred
   as a pair on one line and each centres on its own line once the row is too
   narrow to hold both. The row gap is what separates them; a `·` would be left
   hanging at the start of a line the moment they wrap. */
.footer__base {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 4px 16px;
  padding-top: 14px;
  color: var(--fg3);
}
/* The name is the affordance. Everything around it is --fg3, the tone the site
   uses for small print, and a link in that tone is a link nobody reads as one;
   --fg at 500 is the same lift .nav uses to say which item is live. Not
   underlined, because nothing on this site is and the only underline on it
   would read as a mistake rather than as emphasis. */
.footer__credit { flex: none; }
.footer__credit a { color: var(--fg); font-weight: 500; }
/* The disc and the script, locked up as one unit. The script is the same
   partial the header uses, so it is the same artwork at a second size; only
   --mark-h changes. It takes --fg2 rather than the row's --fg3 because that is
   the weight the wordmark carried here when it was type, and the badge beside
   it is solid ink either way.

   The bottom margin is the same optical correction the header makes, sized for
   this height: the word sits at 62% down the artwork's box, so centring the box
   leaves the word low against a circle that centres at 50%. In a centred flex
   row the margin box is what gets centred, so the lift is half the margin. */
.footer__mark {
  --mark-h: 34px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex: none;
}
.footer__mark .brand-mark { color: var(--fg2); margin-bottom: 8px; }
/* Circular crop of the Sync wordmark. The artwork is black on a white field,
   so the disc carries itself on the dark theme but goes invisible against the
   light theme's white page — the ring is what keeps it reading as a badge
   there, and it is the same ring the lettered badge wore before. Both
   dimensions are set, so the width/height attributes build.js writes cannot
   stretch it. */
.footer__badge {
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--fg3);
}

/* ==========================================================================
   Breakpoints — these replace the export's window.innerWidth listener.
   900px = tablet/narrow, 680px = phone, matching the designer's thresholds.
   ========================================================================== */

@media (max-width: 899px) {
  .nav { display: none; }
  .menu-btn { display: grid; }
  .grid--care,
  .grid--three { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 679px) {
  :root {
    --radius: 18px;
    --gap: 12px;
  }
  .eyebrow { font-size: 17px; }
  h1, .h1 { font-size: 40px; }
  h2, .h2 { font-size: 30px; }
  .lead { font-size: 17px; }
  .link-row { font-size: 16px; }

  .hero-media { aspect-ratio: 4 / 5; }

  /* The plate becomes a 4:5 crop here, and the caption has to give the tracking
     back to fit it. The sentence is 42 characters and the plate is ~346px on a
     390px screen, so a single line tops out near 11px however it is set:
     measured, 50px/.04em already runs past the plate edge. The desktop setting
     would render at 8px. Making it genuinely bigger needs two lines, which the
     one-viewBox overlay cannot express per breakpoint — noted, not attempted. */
  .plate__line { left: 5%; right: 5%; width: 90%; }
  .plate__line text {
    font-size: 42px;        /* viewBox units */
    letter-spacing: .06em;
    word-spacing: normal;
  }

  .grid--looks { grid-template-columns: repeat(2, 1fr); }
  .grid--care,
  .grid--three { grid-template-columns: 1fr; }
  .footer__cols { grid-template-columns: repeat(2, 1fr); }

  /* tighter pills so all four categories fit one row on a 390px screen */
  .filters { justify-content: flex-start; }
  .filters button { padding: 9px 16px; }
}

/* Single column for products on phones. Two columns leaves each card ~167px
   wide, at which the chest print stops being legible; one column lets the
   tighter mobile crops read as intended. */
@media (max-width: 559px) {
  .grid--shop { grid-template-columns: 1fr; }
  .card__media { aspect-ratio: 4 / 5; }
}

@media (min-width: 560px) and (max-width: 899px) {
  .grid--shop { grid-template-columns: repeat(2, 1fr); }
}

/* ===== events.css ================================================== */
/* ==========================================================================
   Events — featured date cards, the gig guide table and its status pills.
   Every responsive value the export computed in renderVals() (featCols,
   threeCols, gigCols, gigGap, metaDisplay) is a media query at the foot of
   this file. Nothing here branches in JavaScript.
   ========================================================================== */

/* --- Status pill tokens -------------------------------------------------- */
/* The "Low stock" pill is the one colour the base sheet has no token for, and
   the export switched it in JS on the theme. Declaring it as a custom property
   keeps it out of the markup and lets the theme drive it like everything else. */

:root {
  --pill-low-bg:   rgba(232, 169, 182, .16);
  --pill-low-fg:   #E8A9B6;
  --pill-low-line: rgba(232, 169, 182, .4);

  /* Redaction bars in the veiled guide. A token per theme for the same reason
     the pill above is one: the bars have to read as text on both backgrounds,
     and a literal here would only be right on one of them. */
  --redact: rgba(255, 255, 255, .34);
}

html[data-theme="light"] {
  --pill-low-bg:   rgba(180, 83, 107, .12);
  /* designer had #B4536B — 4.1:1 on the tinted pill, below AA. #A84A61 gives 4.7:1 */
  --pill-low-fg:   #A84A61;
  --pill-low-line: rgba(180, 83, 107, .34);

  --redact: rgba(29, 29, 31, .3);
}

/* --- Grids --------------------------------------------------------------- */

/* Four across since the headphone-concert card joined the three chapter nights.
   Three columns left the fourth alone on its own row, which reads as a mistake
   rather than as a fourth option. At the 1100px .wrap--wide this gives ~250px
   cards, which is why the img `sizes` on that section says 250px and not 340px —
   a stale `sizes` is a silently oversized download, not a visible fault. */
.grid--events { grid-template-columns: repeat(4, 1fr); text-align: left; }

/* --- Filter bar ---------------------------------------------------------- */
/* The gig guide sits on a plain --bg section, so the pill track needs the tint
   the shop's filter bar gets for free from its --alt band. */

.filters--tinted { background: var(--bg2); }

/* --- Featured cards ------------------------------------------------------ */
/* These reuse .card wholesale; only the media ratio and the body type differ.
   The whole card is a link, so the global a:hover fade has to be cancelled —
   at .72 opacity the lift and the veil both read as a rendering glitch. */

.event-card:hover { opacity: 1; }

/* 4:3 keeps the three cards uniform whatever their source ratio, and overrides
   the 4:5 the base sheet gives product cards below 560px. */
.event-card .card__media { aspect-ratio: 4 / 3; }

/* __format is the invite-only page's version of __date: the cards name the kind
   of night rather than a scheduled one, so the slot is the same and only the
   content changed. __date stays declared — it is what these cards go back to
   the day real dates exist. */
.event-card__date,
.event-card__format { font-size: 12px; font-weight: 600; color: var(--accent); }
.event-card__title {
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.2;
  margin: 0;
}
.event-card__venue { font-size: 14px; color: var(--fg2); }
.event-card__blurb { font-size: 14px; line-height: 1.5; color: var(--fg2); margin-top: 6px; }
.event-card__cta   { font-size: 14px; color: var(--link); margin-top: auto; padding-top: 14px; }

/* Availability tag — .tag--sold's opposite corner twin, for dates still on sale */
.tag--avail { right: 12px; top: 12px; background: rgba(255, 255, 255, .92); color: #1D1D1F; }

/* --- Gig guide ----------------------------------------------------------- */

.gig-list {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
  border-top: 1px solid var(--line);
}

.gig {
  display: grid;
  grid-template-columns: 72px 1fr 150px auto;
  gap: 22px;
  align-items: center;
  padding: 22px 16px;
  border-bottom: 1px solid var(--line);
  transition: background .4s ease;
}
.gig:hover { background: var(--bg2); opacity: 1; }

.gig__date  { display: flex; flex-direction: column; gap: 2px; }
.gig__day   { font-size: 26px; font-weight: 600; letter-spacing: -.02em; line-height: 1; }
.gig__month { font-size: 12px; font-weight: 600; color: var(--accent); }

.gig__main  { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.gig__title { font-size: 17px; font-weight: 600; letter-spacing: -.014em; }
.gig__where { font-size: 14px; color: var(--fg2); }

.gig__meta  { display: flex; flex-direction: column; gap: 4px; }
.gig__time  { font-size: 13px; color: var(--fg2); }
.gig__kind  { font-size: 13px; color: var(--fg3); }

.gig__end   { display: flex; align-items: center; gap: 12px; justify-content: flex-end; }
.gig .arrow-slide { font-size: 15px; color: var(--fg3); }

/* --- Status pills -------------------------------------------------------- */
/* Bare .pill is the "Announced" state; the modifiers are the other three. */

.pill {
  border: 1px solid var(--line);
  border-radius: 980px;
  padding: 8px 16px;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  background: transparent;
  color: var(--fg2);
}
.pill--go   { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.pill--low  { background: var(--pill-low-bg); border-color: var(--pill-low-line); color: var(--pill-low-fg); }
.pill--gone { color: var(--fg3); }

/* --- Veiled gig guide ---------------------------------------------------- */
/* The invite-only treatment: a blurred schedule with a readable panel over it.
 *
 * This is a design device and not an access control, and the difference is the
 * whole reason the markup looks the way it does. `filter: blur()` is paint —
 * the DOM underneath is untouched, so anything written there ships to every
 * visitor, every crawler and every screen reader in full. The rows therefore
 * carry no text for the specifics at all: .redact draws bars from empty spans,
 * so there is nothing to reveal by lifting the blur.
 *
 * Consequences worth keeping:
 *   - overflow:hidden on the wrapper. A blur bleeds past its box, and at 390px
 *     that bleed is horizontal page overflow, which test/suite.js fails on.
 *   - the lock panel is a SIBLING of the blurred list, not a child. filter
 *     creates a containing block, so nesting it would blur the panel too.
 *   - no hover on veiled rows. .gig:hover tints the row, which under a blur
 *     reads as a rendering fault rather than as feedback on something inert.
 */

.gig-veil { position: relative; overflow: hidden; }

.gig-list--veiled {
  filter: blur(6px);
  /* Deliberately near-full opacity. At .6 the bars faded into the background
     and the whole block read as a skeleton loader mid-fetch — "this page is
     broken" rather than "this list is not for you". The blur alone carries
     the inertness; the contrast has to stay up for the gate to look chosen. */
  opacity: .92;
  user-select: none;
  pointer-events: none;
}

.gig-list--veiled .gig:hover { background: none; }

/* Bars stand in for the day, month, title, venue and start time. Heights track
   the type they replace so the blurred block keeps a real schedule's rhythm. */
.redact {
  display: block;
  border-radius: 3px;
  background: var(--redact);
}

.redact--day        { width: 34px; height: 21px; }
.redact--month      { width: 27px; height: 10px; }
.redact--title      { width: 62%;  height: 13px; }
.redact--title-long { width: 78%; }
.redact--title-short{ width: 47%; }
.redact--where      { width: 44%;  height: 11px; }
.redact--where-short{ width: 31%; }
.redact--time       { width: 38px; height: 10px; }

/* A card centred over the list, not a scrim across it. The first attempt was a
   radial gradient fading to --bg, which erased the middle rows entirely and
   left the thing looking like a failed render. A panel with an edge reads as a
   deliberate gate, and it lets the schedule stay visible all the way around —
   which is the whole point of showing a veiled list rather than nothing. */
.gig-veil__lock {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: max-content;
  /* Never wider than the wrapper on a phone, gutters included. */
  max-width: min(420px, calc(100% - 32px));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: clamp(22px, 3vw, 30px) clamp(20px, 3vw, 32px);
  text-align: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .18);
}

.gig-veil__title {
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -.015em;
  margin: 0;
}

.gig-veil__note {
  max-width: 380px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--fg2);
  margin: 0;
}

/* --- Empty state --------------------------------------------------------- */

.gig-empty {
  max-width: 420px;
  margin: 0 auto;
  padding: clamp(38px, 5vw, 64px) 0;
  font-size: 17px;
  line-height: 1.45;
  color: var(--fg2);
  text-align: center;
}

/* --- Hover arrow --------------------------------------------------------- */
/* Only hide it where a hover can bring it back. On touch there is no hover
   state, so the arrow simply stays put. */

.arrow-slide { display: inline-block; }

@media (hover: hover) {
  .arrow-slide {
    opacity: 0;
    transform: translateX(-6px);
    transition: opacity .35s ease, transform .45s cubic-bezier(.22, .61, .36, 1);
  }
  .gig:hover .arrow-slide,
  .gig:focus-visible .arrow-slide,
  .event-card:hover .arrow-slide,
  .event-card:focus-visible .arrow-slide { opacity: 1; transform: translateX(0); }
}

/* ==========================================================================
   Breakpoints — 900px and 680px, the same thresholds as the base sheet.
   ========================================================================== */

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

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

  /* The export drops the time/kind column on phones rather than wrapping it,
     leaving date · title · venue · status. Reproduced as designed. */
  .gig { grid-template-columns: 56px 1fr auto; gap: 14px; }
  .gig__meta { display: none; }
}

/* ==========================================================================
   Enquiry form select

   The venue/festival control on #enquire. It shares .input so the pill, the
   border and the focus treatment come from one place, and overrides only what
   a two-option select needs.
   ========================================================================== */

/* .input takes flex:1 with a 230px floor, which is right for an email field
   and wrong here: at the 560px wrap width the select would claim half the row
   and push the button onto a line of its own. Content-sized instead, so the
   row stays select + email + button until .form-row's own wrap takes over on
   a phone. */
.input--select {
  flex: 0 1 auto;
  min-width: 0;
  cursor: pointer;
}

/* The dropdown list is painted by the browser, not by this sheet, so it does
   not read var(--bg2) and would open as a light menu over a dark page. This is
   the one property that tells the UA which way round the control is. Scoped to
   the select rather than :root deliberately: on :root it would also restyle
   every scrollbar and text field on the site, which is a change to look at on
   its own terms, not a side effect of adding one control. Dark is the base
   here because it is the base in 00-base.css. */
.input--select { color-scheme: dark; }
html[data-theme="light"] .input--select { color-scheme: light; }

/* ===== home.css ==================================================== */
/* ==========================================================================
   Syncopation — home page.
   The export computed these from window.innerWidth inside renderVals();
   here they are media queries at the shared 900px / 680px breakpoints.
   Chapter accents arrive as --accent-chapter on each tile, so the palette
   stays in the markup where the data is and never gets duplicated here.
   ========================================================================== */

/* Tiles sit tighter than the shared --gap so the wide chapter cards and the
   grids under them read as one continuous block. */
.chapters,
.covers,
.stills { gap: 16px; }

@media (max-width: 679px) {
  .chapters,
  .covers,
  .stills { gap: 10px; }
}

/* --- Hero ---------------------------------------------------------------- */
/* The hero standfirst runs larger than the shared .lead — it is the only
   paragraph on the page carrying the whole positioning statement. */

.lead--hero {
  font-size: clamp(21px, 2vw, 26px);
  line-height: 1.35;
  max-width: 600px;
}

@media (max-width: 679px) {
  .lead--hero { font-size: 19px; }
}

/* --- Release player ------------------------------------------------------
   Apple Music and Spotify share one frame; the switcher above it is the stock
   .filters pill group, and .embed now lives in 00-base.css — nothing to
   declare here. */

/* --- Chapters ------------------------------------------------------------ */

.chapters {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
}

.ch-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--panel);
  isolation: isolate;
  transition: transform .5s cubic-bezier(.22, .61, .36, 1),
              box-shadow .5s cubic-bezier(.22, .61, .36, 1);
}
.ch-card:hover { transform: translateY(-8px); box-shadow: 0 26px 54px rgba(15, 17, 19, .28); }

/* Three of the seven chapters are features: they run the full grid width. */
.ch-card--feature { grid-column: span 2; aspect-ratio: 21 / 9; }

.ch-card__media { position: absolute; inset: 0; display: block; z-index: 0; }
.ch-card__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
  transition: transform 1.1s cubic-bezier(.22, .61, .36, 1), filter .6s ease;
}
.ch-card:hover .ch-card__media img {
  transform: scale(1.07);
  filter: saturate(1.12) brightness(1.06);
}

/* Two stacked gradients: ::before always keeps the copy legible over whatever
   the artwork does, ::after deepens it on hover. Both decorative, so neither
   needs to exist in the markup. */
.ch-card::before,
.ch-card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
.ch-card::before {
  background: linear-gradient(to top,
              rgba(15, 17, 19, .88) 0%, rgba(15, 17, 19, .42) 42%, rgba(15, 17, 19, .06) 78%);
}
.ch-card::after {
  background: linear-gradient(to top,
              rgba(15, 17, 19, .94) 0%, rgba(15, 17, 19, .55) 52%, rgba(15, 17, 19, .12) 88%);
  opacity: 0;
  transition: opacity .6s ease;
}
.ch-card:hover::after { opacity: 1; }

/* Deliberately NOT positioned. z-index still applies — this is a flex item, and
   flex items with a z-index paint as though they were positioned — but staying
   static keeps .ch-card the nearest positioned ancestor, which is what lets
   .ch-card__link::after below stretch over the whole tile. With
   `position: relative` here the hit area silently collapses to this text block:
   the tile still looks like one target and only the words are clickable. */
.ch-card__text {
  z-index: 2;
  padding: clamp(20px, 2.4vw, 30px);
}

/* Tile copy sits on the dark gradient in both themes, so it is fixed cream
   rather than a token — --fg would invert to near-black in light mode. */
.ch-card__day {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 7px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  color: var(--accent-chapter, #F2EDE3);
}
.ch-card__day::before {
  content: '';
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-chapter, #F2EDE3);
  transition: transform .5s cubic-bezier(.22, .61, .36, 1), box-shadow .5s ease;
}
.ch-card:hover .ch-card__day::before {
  transform: scale(1.7);
  box-shadow: 0 0 0 5px rgba(242, 237, 227, .18);
}

.ch-card__name {
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -.018em;
  line-height: 1.1;
  color: #F2EDE3;
  margin: 0 0 6px;
}
.ch-card--feature .ch-card__name { font-size: clamp(28px, 3vw, 40px); }

.ch-card__sub { font-size: 14px; color: rgba(242, 237, 227, .74); margin: 0 0 10px; }
.ch-card__copy {
  font-size: 14px;
  line-height: 1.45;
  color: rgba(242, 237, 227, .78);
  margin: 0;
  max-width: 420px;
}

@media (max-width: 899px) {
  .chapters { grid-template-columns: 1fr; }
  /* Nothing spans two columns when there is only one, and the feature tiles
     drop back to the standard crop and title size — same as the export. */
  .ch-card--feature { grid-column: auto; aspect-ratio: 4 / 3; }
  .ch-card--feature .ch-card__name { font-size: 24px; }
}

@media (max-width: 679px) {
  .ch-card,
  .ch-card--feature { aspect-ratio: 4 / 5; }
}

/* --- Video tiles ---------------------------------------------------------
   The Visuals grid reuses .ch-card rather than growing a third card class, so
   it inherits the two stacked gradients that keep copy legible over any
   artwork, the hover lift, the dot on the eyebrow, and cream type that does
   not invert in light mode. Only the crop and the type scale differ.

   Titles sit on the artwork, not under it. This is deliberately NOT the
   .plate__* SVG overlay: that has a fixed viewBox for replacing burned-in
   artwork text and would deform a sentence-length video title.

   9:16 portrait is the owner's decision, taken over square and 16:9 — it is
   the shape the channel's Shorts are shot in, and the only non-square grid on
   the site. Filling it from a 16:9 still keeps about a third of the width, so
   the burned-in wordmark clips on landscape uploads; accepted knowingly, and
   fetch-youtube.js already prefers YouTube's original-aspect thumbnail so
   Shorts arrive as true 9:16 rather than a sliver cropped out of a
   pillarboxed frame. */
.ch-card--video { aspect-ratio: 9 / 16; }

/* Beats the 679px rule above, which drops every .ch-card to 4/5 — a video tile
   keeps its portrait crop at every width. Must stay after it: same
   specificity, so source order decides. */
@media (max-width: 679px) {
  .ch-card--video { aspect-ratio: 9 / 16; }
}

/* Chapter names are one or two words at 24px. Video titles are sentences, so
   they come down a step and stop at three lines. */
.ch-card--video .ch-card__name {
  font-size: 17px;
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The title is the link, and its ::after stretches the hit area over the whole
   tile — so the card behaves like one target while the accessible name stays
   the title alone rather than the tile's entire text. */
.ch-card__link {
  color: inherit;
  text-decoration: none;
}
.ch-card__link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
}
.ch-card__link:hover { opacity: 1; }

/* Keyboard focus lands on the link, whose visible box is only the text, so put
   the ring on the tile instead. */
.ch-card:has(.ch-card__link:focus-visible) {
  outline: 2px solid var(--gold, #F2EDE3);
  outline-offset: 3px;
}
.ch-card__link:focus-visible { outline: none; }

/* The tighter portrait crop leaves less room than a 4:3 chapter tile. */
.ch-card--video .ch-card__text { padding: clamp(14px, 1.4vw, 18px); }
.ch-card--video .ch-card__day { font-size: 10px; margin-bottom: 5px; }

/* --- Reel tiles ----------------------------------------------------------
   "Heard it on TikTok" reuses the video tile whole: TikTok is natively 9:16,
   which is the crop .ch-card--video already carries, so the only additions are
   the track pills.

   That "natively 9:16" is true of the video and was NOT true of the poster,
   which is how a bug lived here until 12 August 2026. TikTok serves two
   different cover assets — the full 9:16 frame for some reels, a 3:4 cover it
   has already centre-cropped for others — and object-fit: cover treats them
   very differently in this box: the 9:16 one fits exactly, the 3:4 one fills
   the height and loses a quarter of its width. The reels carry a burned-in
   "Now Playing" overlay, so that landed at two heights and two sizes down the
   grid. The fix is upstream, in fetch-tiktok.js: every poster is now cropped
   through 3:4 and then to 9:16, so the files reaching this rule all share one
   framing. Nothing here compensates for it, and nothing here should — see
   CROP_STAGES in that file before changing this aspect-ratio.

   Track names are one or two words, not sentences, so they do not need the
   three-line clamp a video title does. */
.ch-card--reel .ch-card__name {
  font-size: 19px;
  -webkit-line-clamp: 2;
}

/* The pills are the exception to the stretched card link, and the layering is
   the whole trick.

   .ch-card__text is a flex item carrying z-index: 2, and a flex item with a
   z-index establishes a stacking context even though it is static. Both the
   pills and .ch-card__link::after (z-index: 3) live inside that context, so
   z-index: 4 here beats the overlay locally without changing anything outside
   the tile.

   Positioning THIS element is safe; positioning .ch-card__text is not. The
   link's ::after resolves against the nearest positioned ancestor, and that
   has to stay .ch-card — see the warning above .ch-card__text. The pills are
   not an ancestor of the link, so making them relative moves nothing. */
.reel__pills {
  position: relative;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 9px 0 0;
}

/* 25px tall, which clears the 24px WCAG 2.5.8 target floor. Deliberately not
   the 38px used for the filter buttons (D7): four of these sit inside a 260px
   card, and at 38px they would take more of the tile than the artwork. */
.reel__pill {
  display: inline-flex;
  align-items: center;
  min-height: 25px;
  padding: 0 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
  text-decoration: none;
  /* Fixed cream on a fixed dark scrim, like every other tile label — a token
     would invert to near-black over artwork in light mode. The scrim is opaque
     enough to carry 11px type over the brightest still in the set. */
  color: #F2EDE3;
  background: rgba(15, 17, 19, .72);
  border: 1px solid rgba(242, 237, 227, .38);
  transition: background .25s ease, border-color .25s ease;
}
.reel__pill:hover {
  background: rgba(242, 237, 227, .18);
  border-color: rgba(242, 237, 227, .72);
}
.reel__pill:focus-visible {
  outline: 2px solid var(--gold, #F2EDE3);
  outline-offset: 2px;
}

/* --- Catalogue ----------------------------------------------------------- */

.covers {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  list-style: none;
  margin: clamp(34px, 4vw, 54px) 0 0;
  padding: 0;
  text-align: left;
}

.cover {
  display: block;
  transition: transform .45s cubic-bezier(.22, .61, .36, 1);
}
/* Cancels the global a:hover fade — the card lifts instead of dimming. */
.cover:hover,
.cover:focus-visible { opacity: 1; transform: translateY(-6px); }

.cover__media {
  display: block;
  position: relative;
  overflow: hidden;
  border-radius: 14px;
  background: var(--panel);
}
.cover__media img {
  display: block;
  width: 100%;
  /* height:auto is load-bearing, not tidiness. The build writes width/height
     attributes onto every <img>, and those are presentational hints — with a
     height attribute in play the box has both dimensions already, so
     aspect-ratio never gets to compute one and the square becomes a 252x800
     sliver. Every other component escapes this only because it pins an
     explicit height of its own. */
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  transition: transform 1s cubic-bezier(.22, .61, .36, 1);
}
.cover:hover .cover__media img,
.cover:focus-visible .cover__media img { transform: scale(1.06); }

.cover__veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(15, 17, 19, .72), rgba(15, 17, 19, 0) 62%);
  opacity: 0;
  transition: opacity .45s ease;
}
.cover:hover .cover__veil,
.cover:focus-visible .cover__veil { opacity: 1; }

.cover__play {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  color: #1D1D1F;
  display: grid;
  place-items: center;
  font-size: 17px;
  padding-left: 3px;
  opacity: 0;
  transform: translate(-50%, -50%) scale(.82);
  transition: opacity .4s ease, transform .5s cubic-bezier(.22, .61, .36, 1);
}
.cover:hover .cover__play,
.cover:focus-visible .cover__play { opacity: 1; transform: translate(-50%, -50%) scale(1); }

.cover__name { display: block; font-size: 15px; font-weight: 500; margin-top: 12px; }
.cover__meta { display: block; font-size: 13px; color: var(--fg3); margin-top: 3px; }

@media (max-width: 899px) { .covers { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 679px) { .covers { grid-template-columns: repeat(2, 1fr); } }

/* --- Visuals ------------------------------------------------------------- */

.stills {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  list-style: none;
  margin: 0;
  padding: 0;
}

.still {
  position: relative;
  /* The design specified 4/5, but these sources are square with the chapter
     name burned into the pixels edge to edge — a 4/5 crop cuts the first and
     last letters off ("Hold Space" -> "old Spac"). 1/1 matches the source and
     keeps the names readable. Revert to 4/5 only with re-cut artwork. */
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 16px;
  background: var(--bg2);
}
.still img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
  transition: transform .7s cubic-bezier(.22, .61, .36, 1);
}
.still:hover img { transform: scale(1.045); }

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

/* --- About --------------------------------------------------------------- */

.about-media {
  border-radius: 20px;
  overflow: hidden;
  aspect-ratio: 21 / 9;
  background: var(--bg2);
}
.about-media picture { display: block; width: 100%; height: 100%; }
.about-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 35%);
}

/* 4/5 rather than the 16/9 this carried before it took a caption, matching
   .hero-media at the same breakpoint. Two reasons, and the second is the one
   that forced it: the caption is four lines, and inside a 16/9 box on a 390px
   screen it left roughly 20px of photograph showing, which is a text block with
   an edge of image rather than a plate. The mobile source is also already 4:5
   (1080x1350, same as the hero's), so 16/9 was throwing away most of a file cut
   for this shape. */
@media (max-width: 679px) {
  .about-media { aspect-ratio: 4 / 5; }

  /* And with the taller crop the caption needs its own scrim, holding the
     shared .plate--captioned ramp's density a further 8% up the plate.

     Same failure the .plate--captioned block below was written for, one step
     worse. That ramp is solved for the hero, whose caption reaches 32% up a
     4:5 plate; this caption is longer and reaches 44%, which puts the eyebrow
     and the sub above the dark part. Measured against the rendered pixels
     rather than estimated: they came out at 3.39:1 and 4.30:1, both under the
     4.5:1 floor for text that size.

     The artwork is the other half of it. Every other captioned plate on the
     site sits on mid-tone photography; this one sits on newsprint that is
     pure white where the words go, which is the worst case a scrim can be
     asked to cover, and the reason this override belongs to .about-media
     rather than being pushed up into the shared rule.

     Only the bottom half moves. Her face is above 55% at this crop and keeps
     the density it had.

     Two class names on the plate, not one. .plate--captioned .plate__scrim is
     the same specificity as .about-media .plate__scrim and is declared further
     down this file, so the one-class form loses the tie however it reads: a
     media query adds no weight. Winning on specificity rather than on source
     order also means neither block can be moved and quietly swap them. */
  .about-media.plate--captioned .plate__scrim {
    background: linear-gradient(to top,
                rgba(15, 17, 19, .92) 0%,
                rgba(15, 17, 19, .68) 50%,
                rgba(15, 17, 19, .08) 86%);
  }

  /* The hover deepen composes over the heavier base above, so its stops are
     solved backwards from the composited result the way .plate--captioned's
     are: two translucent layers land at 1-(1-a)(1-b), so reusing that rule's
     values here would reach .98 at the 50% stop and put the newspaper into
     flat black. These match what the shared pair reaches instead.
     Kept for parity with a real hover device at this width; the plate is
     equally reachable by touch, where it never runs. */
  .about-media.plate--captioned .plate__scrim::after {
    background: linear-gradient(to top,
                rgba(15, 17, 19, .62) 0%,
                rgba(15, 17, 19, .34) 50%,
                rgba(15, 17, 19, .10) 86%);
  }
}

/* Four short value statements. Same component as .grid--care, but they stay
   four across at tablet and pair up on phones rather than stacking — three
   words each does not earn a full row. */
.grid--values {
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(24px, 3vw, 44px);
  max-width: 900px;
  margin: clamp(34px, 4vw, 54px) auto 0;
  text-align: left;
}

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

/* --- Captioned plate ------------------------------------------------------
   A .plate (00-base.css) carrying a caption block instead of a wordmark, laid
   out exactly like the chapter tiles above: the media goes absolute behind and
   the caption is a flex item pushed to the bottom. The caption reuses the
   .ch-card__* type styles directly, so there is one set of rules for words over
   artwork rather than two that drift apart.

   Lives here rather than in the base sheet because only the home banner uses it
   (CONVENTIONS.md: never edit 00-base.css). If a second page wants a captioned
   plate, this block is what should move down into the base sheet.

   The caption must stay UNPOSITIONED — .ch-card__link::after stretches to the
   nearest positioned ancestor, and that has to be the plate. Position the
   caption and the click target silently collapses to the width of the words. */

.plate--captioned {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  isolation: isolate;
  /* The hero copy above this plate is centred by .section--center, and that
     inherits straight into the caption. .ch-card__day is display:flex so it
     ignored it while the other three lines did not, which put four lines on
     three different left edges. .chapters escapes the same inheritance the
     same way. */
  text-align: left;
}

/* A plate is a feature-sized surface, so it takes the feature title size rather
   than the 24px meant for a quarter-width tile — including the drop back to
   24px at the width where .ch-card--feature stops spanning. */
.plate--captioned .ch-card__name { font-size: clamp(28px, 3vw, 40px); }
@media (max-width: 899px) {
  .plate--captioned .ch-card__name { font-size: 24px; }
}

/* The tile grows and haloes its dot on hover; the plate does the same so the
   two read as one component. The tile's -8px lift is deliberately not copied:
   a hero is not a card in a grid and has nothing to lift away from. */
.plate--captioned:hover .ch-card__day::before {
  transform: scale(1.7);
  box-shadow: 0 0 0 5px rgba(242, 237, 227, .18);
}

/* Same shape and direction as the tile scrim, holding its density further up.
   The tiles get away with fading out by 78% because their caption is two short
   lines; this one is four and grows with the copy, so the top line — the 11px
   eyebrow, the smallest text on it — was landing above the dark part and
   measuring 3.3:1 against the photo. Tuning the accent colour instead would
   have re-broken the next time the wording changed length.
   Overrides .plate--clean .plate__scrim in the base sheet: equal specificity,
   and this sheet is concatenated after it. */
.plate--captioned .plate__scrim {
  background: linear-gradient(to top,
              rgba(15, 17, 19, .90) 0%,
              rgba(15, 17, 19, .60) 42%,
              rgba(15, 17, 19, .10) 80%);
}
.plate--captioned picture {
  position: absolute;
  inset: 0;
  z-index: 0;
}

/* The same slow push-in the chapter tiles give their artwork — .ch-card__media
   img above, values and easing copied rather than re-picked. The plate is
   overflow:hidden, so the frame crops the scaled image instead of it spilling.
   Safe to copy verbatim here, unlike the hover gradient: a transform composes
   with nothing underneath it, where two translucent layers multiply. */
.plate--captioned picture img {
  transition: transform 1.1s cubic-bezier(.22, .61, .36, 1), filter .6s ease;
}
.plate--captioned:hover picture img {
  transform: scale(1.07);
  filter: saturate(1.12) brightness(1.06);
}

/* Hover deepen, through a second layer rather than by swapping the scrim's own
   background: background-image does not transition, so that would snap. Same
   reason .ch-card stacks two gradients, and the duration is its .6s.

   The VALUES are not the tile's, and copying them would be the mistake here.
   Two translucent layers compose as 1-(1-a)(1-b), so the tile's .55 over its
   own .42 base lands at .74 mid-band — but over the heavier base above it
   reaches .82, which puts her shoulder into flat black on hover. These stops
   are solved backwards from the tile's composited result instead, so the
   banner deepens by the same amount the tiles appear to:
     bottom  1-(1-.90)(1-.70) = .97    tiles: .99
     42%     1-(1-.60)(1-.38) = .75    tiles: .74
     80%     1-(1-.10)(1-.12) = .21    tiles: .17  */
.plate--captioned .plate__scrim::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
              rgba(15, 17, 19, .70) 0%,
              rgba(15, 17, 19, .38) 42%,
              rgba(15, 17, 19, .12) 80%);
  opacity: 0;
  transition: opacity .6s ease;
}
.plate--captioned:hover .plate__scrim::after { opacity: 1; }

/* Focus ring on the PLATE, not on the link, matching .ch-card. It has to be
   this way round: an outline on the stretched link would paint underneath the
   hover layer above it and measured 1.02:1 on the bottom edge — present in the
   DOM, invisible on screen. An element's own outline is not clipped by its own
   overflow:hidden, so this one draws cleanly outside the corner radius.

   Colour comes from --accent, matching the global :focus-visible rule at the
   top of this sheet. NOT var(--gold, #F2EDE3) as .ch-card uses: --gold is
   defined nowhere in src/, so that falls back to cream and measures 1.17:1 on
   the light theme's white background — a ring that exists and cannot be seen.
   Several other rules in this sheet share that bug; they are not this change's
   to fix, but they have the same failure. */
.plate--captioned:has(.ch-card__link:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.plate--captioned .ch-card__link:focus-visible { outline: none; }

/* ===== share.css =================================================== */
/* --- Share page (/links) --------------------------------------------------
   The Linktree replacement. One column, thumb-reachable, and no nav — the page
   has exactly one job, which is to hand someone a destination.

   Everything here is scoped under .share so it cannot reach the rest of the
   site. It borrows the site's tokens rather than inventing colours, so the
   light/dark toggle works with no extra rules. */

/* --- The wash -------------------------------------------------------------
   The current release cover, blurred and scrimmed, so the glass above it has
   something to refract. Two things this buys beyond looks: the page takes its
   colour from whatever record is out, and it needs no new asset — the cover
   already ships, and the card above shows the same file sharp.

   The blur is a balance, not a maximum. Far enough that the text burned into
   most of the site's imagery (T3) cannot be read behind the one wordmark this
   page prints; not so far that the artwork stops being artwork. The first
   attempt sat at 64px, where the brand art was technically present and
   visually absent — a wash of flat colour.

   scale() hides the transparent edge blur pulls in from outside the box. */
.share {
  position: relative;
  isolation: isolate;
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding: clamp(28px, 6vw, 64px) 0;
  overflow: hidden;
  /* Fixed dark, in both themes. Same rule the tile overlays follow: copy sits
     on artwork here, so a token that inverts to near-black in light mode would
     be unreadable exactly when it mattered. */
  background: #0B0C0D;
  color: #F2EDE3;
}

.share::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -2;
  background-image: var(--share-art);
  background-size: cover;
  background-position: center;
  /* 30px, not 64. At 64 the cover stopped being artwork and became a flat
     colour field — the brand art was technically present and visually absent.
     At 30 the shapes read while the burned-in text most of the site's imagery
     carries (T3) still cannot be made out. The scrim below does the contrast
     work, and the measured worst case is checked rather than assumed. */
  filter: blur(30px) saturate(1.5) brightness(.78);
  transform: scale(1.18);
}

/* The scrim. Without it the glass rows sit on whatever the cover happens to be
   bright at, and a pale record would drop the label contrast below AA on some
   rows and not others — the worst kind of failure, because it looks fine in a
   screenshot of the wrong release. */
.share::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(11, 12, 13, .10) 0%, rgba(11, 12, 13, .62) 72%),
    linear-gradient(to bottom, rgba(11, 12, 13, .34), rgba(11, 12, 13, .74));
}

/* The section is the whole page here, so the document behind it has to match —
   otherwise the body's own background shows as a pale strip above and below,
   which is what a viewport taller than the content produces. :has is the whole
   fix; the fallback if it is unsupported is a strip, not a broken page. */
html:has(.share),
body:has(.share) { background: #0B0C0D; }

/* Two slow drifting colour fields, for depth the flat scrim cannot give. Pure
   CSS gradients — no image, no canvas, nothing to load. */
.share__glow {
  position: absolute;
  inset: -20%;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(40% 34% at 20% 24%, rgba(216, 178, 106, .22), transparent 70%),
    radial-gradient(36% 32% at 80% 70%, rgba(120, 156, 190, .18), transparent 70%),
    radial-gradient(30% 26% at 62% 12%, rgba(190, 108, 132, .13), transparent 72%);
  /* 52s, not 28. Slow enough that the movement is never the thing you notice —
     the colour should look like it is sitting still and simply be different a
     minute later. Motion competing with the links is motion working against
     the page. */
  animation: share-drift 52s ease-in-out infinite alternate;
}

/* Film grain. A page this smooth — blurred wash, frosted panels, no texture
   anywhere — reads as synthetic, and a little noise is what stops large soft
   gradients banding on 8-bit displays. Generated by the browser from an inline
   SVG turbulence, so it weighs nothing and loads nothing. */
.share__grain {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: .16;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

@keyframes share-drift {
  from { transform: translate3d(-2%, -1%, 0) scale(1); }
  to   { transform: translate3d(2%, 3%, 0) scale(1.08); }
}

/* Motion here is decoration and nothing else, so it is the first thing to go. */
@media (prefers-reduced-motion: reduce) {
  .share__glow { animation: none; }
}

.share__wrap {
  width: 100%;
  max-width: 480px;          /* the mobile design, unchanged */
  text-align: center;
}

/* The signature is the `logo` partial here too — the same artwork the header
   and the footer carry, at the one size on the site where it is the hero.
   font-size no longer sets type, but it stays the single knob: --mark-h rides
   it in em, so the mobile clamp here and the cqw clamp in the desktop block
   below keep tuning the mark without either of them learning a new unit.

   1.79em is where the artwork's box comes out the width the Alex Brush word
   had at the same font-size — measured 114/152/181px at 30/40/47.6px — so the
   column's rhythm against the bio and the cover is unchanged.

   line-height: 0 removes the strut. The anchor is an inline-block so that
   text-align still places it, centred here and left in the desktop block, and
   an inline-block sits on a baseline: with a strut in the line box, its
   descender hangs a fifth of an em of dead space under the mark. */
.share__mark {
  --mark-h: 1.79em;
  font-size: clamp(30px, 7vw, 40px);
  font-weight: 400;
  line-height: 0;
  margin: 0 0 14px;
}
.share__mark a {
  display: inline-block;
  text-decoration: none;
  color: #F2EDE3;
  /* Lifts the script off a busy wash without a box around it. A filter rather
     than the text-shadow this wore as type, because the mark is a shape now and
     text-shadow does not reach it. Tighter and denser than the 18px blur the
     type carried: the artwork is a hairline, and a wide blur spreads the little
     ink it has until there is nothing under the stroke. */
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, .62));
}

/* Tightened deliberately. Every pixel above the first link is a pixel of
   scrolling between a visitor arriving and a visitor tapping, and this page has
   one job. The bio earns its place — it is the only voice on the page — but at
   14px on two lines rather than 15px on three. */
.share__bio {
  color: rgba(242, 237, 227, .78);
  font-size: 14px;
  line-height: 1.45;
  margin: 0 auto 18px;
  max-width: 38ch;
  text-shadow: 0 1px 12px rgba(0, 0, 0, .5);
}

/* --- Glass ---------------------------------------------------------------
   One definition, used by the release card and every row.

   The translucency is doing two jobs and they pull against each other: enough
   to show the wash moving underneath, opaque enough that 12px text clears AA
   over whatever cover is behind it. .10 white over the scrim above is where
   both hold.

   The top highlight is what stops these reading as flat translucent boxes —
   a 1px inset light line along the upper edge is how real glass catches a
   room. Cheap, and it does more than the blur does. */
.share__glass {
  /* A DARK base with a light sheen on top, not light translucency.
     The difference is not stylistic. The wash behind this is whatever cover is
     current, and the covers are not all dark — SundaySync and Soul Revolution
     are bright. Light translucency over a bright cover produces a pale panel
     under cream text, which is unreadable, and it would arrive by itself the
     day a bright record ships. Anchoring the panel dark means the wash can be
     any colour at all and the text still clears AA.
     The gradient is what keeps it reading as glass rather than a grey box. */
  background:
    linear-gradient(rgba(255, 255, 255, .13), rgba(255, 255, 255, .05)),
    rgba(14, 15, 17, .58);
  border: 1px solid rgba(242, 237, 227, .16);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .18),
    0 8px 30px rgba(0, 0, 0, .28);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  backdrop-filter: blur(16px) saturate(1.5);
}

/* Where backdrop-filter is unavailable the panels would be flat 10% white over
   a busy image, which is precisely the unreadable case. Go opaque instead:
   less pretty, still legible, and legibility is not the part to gamble. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .share__glass {
    background: rgba(20, 21, 23, .82);
    border-color: rgba(242, 237, 227, .22);
  }
}

/* --- Latest release ------------------------------------------------------
   Visually ahead of the list without being a different component: same radius
   and border language, one step more contrast. */
.share__now {
  position: relative;
  overflow: hidden;
  border-radius: 18px;
  padding: clamp(20px, 5vw, 26px) clamp(16px, 4vw, 22px);
  margin-bottom: clamp(20px, 4vw, 28px);
  color: #F2EDE3;
  text-align: center;
}

/* A slow sheen crossing the card — the one animated flourish on the page, and
   it is on the primary action rather than sprinkled over everything. */
/* Animated with transform, NOT left, and the difference is measurable rather
   than theoretical. The first version moved `left` from -60% to 130%: `left`
   is a layout property, so the browser counted every single frame as a layout
   shift and the page scored CLS 0.11 — worse than the 0.1 "good" threshold, on
   a site whose every other page is 0.0000. Nothing was visibly jumping; it was
   the animation itself being measured. Transform is composited and moves
   nothing in layout. */
.share__now::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 45%;
  height: 100%;
  /* Dimmed and slowed: .06 rather than .13, and a 16s cycle whose sweep takes
     4.8s rather than 1.5s. A fast bright sweep reads as a notification — the
     eye is pulled to it and away from the links, which is the opposite of what
     this page is for. At this weight it is ambient: noticeable if you rest on
     the card, ignorable while scanning past it. */
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .06), transparent);
  transform: translateX(-220%) skewX(-18deg);
  animation: share-sheen 16s cubic-bezier(.4, 0, .3, 1) 3s infinite;
  pointer-events: none;
  will-change: transform;
}
@keyframes share-sheen {
  0%        { transform: translateX(-220%) skewX(-18deg); }
  30%, 100% { transform: translateX(320%) skewX(-18deg); }
}
@media (prefers-reduced-motion: reduce) {
  .share__now::after { animation: none; opacity: 0; }
}

/* The cover. Square, because that is what a release cover is, and pinned by
   aspect-ratio with height:auto — the build writes width/height attributes onto
   every img, and a height attribute in play stops aspect-ratio computing one,
   which is how a square becomes a sliver. The rest of the site learned this the
   hard way; the comment in home.css is the record of it.

   The ring and drop shadow lift it off the glass so it reads as a physical
   object sitting on the card rather than a picture printed into it. */
.share__cover {
  display: block;
  width: clamp(112px, 42vw, 172px);
  margin: 0 auto 16px;
  border-radius: 12px;
  overflow: hidden;
  box-shadow:
    0 0 0 1px rgba(242, 237, 227, .18),
    0 14px 38px rgba(0, 0, 0, .5);
}
.share__cover img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
}

/* Mirrors the home page's #music block: .eyebrow, h2, meta, then .link-row.
   Same order and same relative weights, so the release reads identically in
   both places — one step smaller here because it sits inside a card rather
   than owning a full-width section. */
.share__now-eyebrow {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: rgba(242, 237, 227, .66);
  margin: 0 0 7px;
}
.share__now-title {
  font-size: clamp(26px, 7vw, 32px);
  font-weight: 600;
  letter-spacing: -.018em;
  line-height: 1.08;
  margin: 0;
}
.share__now-meta {
  font-size: 13px;
  color: rgba(242, 237, 227, .72);
  margin: 4px 0 0;
}
/* .link-row is the home page's component, reused rather than reinvented — the
   only overrides are the ones the dark glass needs. --link is a theme token
   and would invert to something unreadable here, so the colour is fixed cream
   like every other label sitting on artwork.

   Targets are padded to 44px because on this page these compete with rows that
   are 58px; a bare text link beside them would be the hardest thing to hit and
   the most important. */
.share__now-links {
  margin-top: 16px;
  gap: 8px 20px;
  font-size: 15px;
}
.share__now-links a {
  color: #F2EDE3;
  text-decoration: none;
  padding: 10px 2px;
  border-bottom: 1px solid rgba(242, 237, 227, .34);
  transition: border-color .3s ease, color .3s ease;
}
.share__now-links a:hover {
  color: #FFFFFF;
  border-bottom-color: #F2EDE3;
}
.share__now-links a:focus-visible {
  outline: 2px solid var(--gold, #F2EDE3);
  outline-offset: 3px;
}

/* --- The list ------------------------------------------------------------ */

.share__list {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
}

/* A group heading is a list item, not a heading element: it labels a run of
   links inside one list rather than opening a new section, and role=presentation
   keeps it out of the item count a screen reader announces. */
.share__head {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: rgba(242, 237, 227, .58);
  margin: 22px 0 10px;
  text-shadow: 0 1px 10px rgba(0, 0, 0, .5);
}
.share__list > .share__head:first-child { margin-top: 0; }

.share__link {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 13px 15px;
  margin-bottom: 9px;
  border-radius: 15px;
  text-decoration: none;
  color: #F2EDE3;
  /* 58px, well past the 44px floor. Nearly every visitor arrives on a phone
     from a bio link, so the target size IS the conversion rate here. */
  min-height: 58px;
  transition: transform .35s cubic-bezier(.22, .61, .36, 1),
              border-color .3s ease, background .3s ease;
}
/* The whole row lifts and brightens, not just the text — the affordance has to
   read as "this is a button" at a glance, on a page where every row is one. */
.share__link:hover {
  transform: translateY(-2px);
  border-color: rgba(242, 237, 227, .34);
  background: linear-gradient(rgba(255,255,255,.20), rgba(255,255,255,.10)), rgba(14,15,17,.55);
}

/* The mark, not the word, is what a visitor scans for. Sized to read at arm's
   length and set in currentColor so it follows the theme — brand colours would
   fight the site's palette and make this look like every other link page. */
.share__icon {
  flex: none;
  width: 21px;
  height: 21px;
  fill: currentColor;
  opacity: .82;
  transition: opacity .3s ease, transform .35s cubic-bezier(.22, .61, .36, 1);
}
.share__link:hover .share__icon { opacity: 1; transform: scale(1.08); }

/* Label over note, so the note never competes for the same row and the label
   can stay large. Stacking also means the note simply disappears on narrow
   screens without disturbing anything. */
.share__text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-right: auto;
  min-width: 0;
}
.share__link:focus-visible {
  outline: 2px solid var(--gold, #F2EDE3);
  outline-offset: 3px;
}

/* The label carries the spare space, which puts the note and the chevron
   together at the right edge whether or not a note exists.

   The obvious-looking alternative — margin-left:auto on the note, and a
   :last-of-type rule on the label for rows without one — does not work, and
   silently. All three children are <span>, so :last-of-type matches the
   CHEVRON rather than the label, and the rows with no note render with the
   chevron tucked against the text instead of at the edge. */
.share__label {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.008em;
  line-height: 1.2;
}

/* A hint, not the thing being scanned for. Under the label rather than beside
   it, so it never squeezes the name a visitor is looking for. */
.share__note {
  font-size: 12px;
  line-height: 1.3;
  color: rgba(242, 237, 227, .62);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.share__chev {
  flex: none;
  font-size: 18px;
  line-height: 1;
  color: rgba(242, 237, 227, .55);
  transition: transform .3s ease;
}
.share__link:hover .share__chev { transform: translateX(3px); }

.share__foot {
  margin: clamp(26px, 5vw, 36px) 0 0;
  font-size: 12px;
  color: rgba(242, 237, 227, .58);
}
.share__foot a { color: inherit; }

/* Stacked now, so the note no longer competes for the label's row and can stay
   on the smallest screens. It truncates rather than wraps — a note that grows
   to two lines would push the rows apart and cost a link below the fold. */

/* --- More (disclosure) ---------------------------------------------------
   A <details>, so it needs no JavaScript and announces its own state. The two
   lowest-traffic destinations live here rather than being deleted: they cost
   two full-height rows above the footer, but they are still real places the
   act publishes to. */
.share__more { margin-top: 4px; }

.share__more-btn {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  padding: 0 4px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .01em;
  color: rgba(242, 237, 227, .66);
  transition: color .3s ease;
}
.share__more-btn::-webkit-details-marker { display: none; }
.share__more-btn::after {
  content: '⌄';
  font-size: 15px;
  line-height: 1;
  transform: translateY(-2px);
  transition: transform .3s ease;
}
.share__more[open] .share__more-btn::after { transform: translateY(1px) rotate(180deg); }
.share__more-btn:hover { color: #F2EDE3; }
.share__more-btn:focus-visible {
  outline: 2px solid var(--gold, #F2EDE3);
  outline-offset: 3px;
}
.share__more .share__list { margin-top: 8px; }

/* --- Reels ---------------------------------------------------------------
   The strongest social proof the brand has, at no new asset cost — these eight
   posters already ship for the home page's TikTok section.

   A horizontal scroller rather than a grid: it keeps the page short on a phone
   while showing that there is more than fits, and scroll-snap makes it feel
   deliberate rather than loose. */
.share__reels { margin-top: 26px; }

.share__reels-head {
  margin: 0 0 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.share__reels-head a {
  color: rgba(242, 237, 227, .72);
  text-decoration: none;
  transition: color .3s ease;
}
.share__reels-head a:hover { color: #F2EDE3; }

.share__reel-strip {
  display: flex;
  gap: 10px;
  list-style: none;
  margin: 0;
  padding: 0 0 6px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.share__reel-strip::-webkit-scrollbar { display: none; }

.share__reel-strip > li {
  flex: none;
  width: 104px;
  scroll-snap-align: start;
}

.share__reel-strip a {
  display: block;
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  text-decoration: none;
  /* 9:16, the shape TikTok is shot in and the same crop the home page tiles
     use. Set on the link so the box is reserved before the poster loads. */
  aspect-ratio: 9 / 16;
  box-shadow: 0 0 0 1px rgba(242, 237, 227, .14), 0 8px 22px rgba(0, 0, 0, .34);
  transition: transform .35s cubic-bezier(.22, .61, .36, 1);
}
.share__reel-strip a:hover { transform: translateY(-3px); }
.share__reel-strip a:focus-visible {
  outline: 2px solid var(--gold, #F2EDE3);
  outline-offset: 3px;
}

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

/* The track name over the bottom of the still, on its own gradient — the
   posters are busy and a label without a scrim is unreadable on half of them. */
.share__reel-name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 18px 8px 7px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
  color: #F2EDE3;
  background: linear-gradient(to top, rgba(11, 12, 13, .92), transparent);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (min-width: 900px) {
  .share__reel-strip > li { width: 118px; }
}

/* --- Desktop: two columns -------------------------------------------------
   AT THE END OF THE FILE ON PURPOSE. These rules override base declarations of
   the same specificity, so source order is what decides — placed above them
   the whole block silently loses and the layout stays one column with a wider
   wrapper, which looks like a broken grid rather than a specificity bug.
   Below 900px nothing here applies and the page is exactly the single column
   it has always been — that layout is the one nearly every visitor sees,
   arriving on a phone from a bio link, and it is not what needed fixing.

   At desktop widths a 480px column left roughly 60% of the viewport as empty
   wash, so the people getting the most screen were getting the least page.
   The split puts the release and its cover on the left at a size worth
   looking at, and the destinations on the right where they are all reachable
   without scrolling. */
@media (min-width: 900px) {
  .share { align-items: center; }

  .share__wrap {
    max-width: 1060px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      'lead stack'
      'foot foot';
    align-items: start;
    gap: clamp(32px, 5vw, 64px);
    text-align: left;
  }

  /* container-type makes the column itself the thing sizes are measured
     against. It matters because viewport units were the bug: the wordmark was
     clamp(38px, 3.4vw, 52px), and 3.4vw does not exceed 38px until the window
     is 1118px wide — so from 900 to 1118 it sat frozen on its floor while the
     cover kept growing, and the two drifted out of proportion. cqw tracks the
     column, so everything in here scales together and keeps scaling down as
     the window narrows.

     Where container queries are unsupported the cqw declarations are invalid
     and dropped, and the base font sizes outside this block apply — smaller
     than ideal, never broken. */
  .share__lead {
    grid-area: lead;
    position: sticky;
    top: 40px;
    container-type: inline-size;
  }
  .share__stack { grid-area: stack; }
  .share__foot  { grid-area: foot; text-align: center; }

  /* The column is the card now, so the panel around it is redundant and its
     border would just draw a box inside a box. */
  .share__now {
    background: none;
    border: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    padding: 0;
    text-align: left;
    margin-bottom: 0;
  }
  .share__now::after { display: none; }   /* no sheen without a panel to cross */

  /* The cover fills the column, so its edges line up with the bio, the title
     and the link row beneath it. Before this it stopped at roughly half the
     column and left a dead gutter beside it while everything else ran full
     width — nothing in the block shared an edge, which is what read as broken
     rather than merely small. */
  .share__cover  { margin: 0 0 clamp(14px, 3cqw, 24px); width: 100%; }

  .share__mark   { text-align: left; font-size: clamp(32px, 10cqw, 56px); }
  .share__bio    { margin-left: 0; max-width: 34ch; font-size: clamp(13px, 3.4cqw, 16px); }
  .share__now-links { justify-content: flex-start; }

  /* Was capped at 32px at every desktop width, so by 1200px it was small
     against a 264px cover and a 47px wordmark. */
  .share__now-title  { font-size: clamp(26px, 8.5cqw, 46px); }
  .share__now-meta   { font-size: clamp(12px, 3.2cqw, 15px); }
  .share__now-eyebrow { font-size: clamp(9px, 2.4cqw, 11px); }
  .share__list   { margin-top: 0; }
  .share__list > .share__head:first-child { margin-top: 0; }
}

/* ===== story.css =================================================== */
/* ==========================================================================
   Our story — page styles

   Everything here is the designer's own measurement, lifted out of
   renderVals() in "Syncopation Our Story.dc.html". The phone (<680) and
   narrow (<900) branches in that method are media queries here; the page
   ships no JavaScript.
   ========================================================================== */

:root {
  /* The one colour this page adds. "With you." is set in a rose that appears
     nowhere else on the site, so it stays page-scoped rather than becoming a
     site token — but it still has to answer to the theme. */
  --story-pink: #E8A9B6;
  /* Plate behind the hero so the frame matches the artwork while the image is
     still decoding. Sampled from the wall the LED sign hangs on, which is why
     it moved off the old cream when that artwork was retired. One value, both
     themes. */
  --story-hero-bg: #D5D6D1;
}
html[data-theme="light"] { --story-pink: #B4536B; }

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

.story-hero { background: var(--story-hero-bg); }

/* The wordmark plate is 21:9 artwork. The base sheet already drops .hero-media
   to 4/5 at 679px, which is the ratio the mobile crops are cut to, so this only
   has to speak above the phone breakpoint. */
@media (min-width: 680px) {
  .story-media--wide { aspect-ratio: 21 / 9; }
}

/* "More than music" carries a 1:1 composition instead. It gets the prose
   column's width rather than the full 1100px plate: the source is 800px wide,
   so 1100 would upscale it, and 720 lines the artwork's edges up with the
   paragraph above it. Below 680px the base sheet's 4/5 takes over, which is the
   ratio the -m recut is cut to. The frame is painted the crimson sampled off
   the artwork's own field so it does not flash grey while the image decodes,
   the same trick --story-hero-bg plays for the hero. */
.story-media--square {
  max-width: 720px;
  background: #4C1211;
}
@media (min-width: 680px) {
  .story-media--square { aspect-ratio: 1 / 1; }
}

/* The export put a slow hover zoom on its hero figure (.sc-fig / .sc-img).
   Same gesture the cards use, a touch gentler. */
.story-fig img { transition: transform 1.2s cubic-bezier(.22, .61, .36, 1); }
.story-fig:hover img { transform: scale(1.05); }

/* --- "Where it begins" statement ----------------------------------------- */

.story-statement { display: flex; flex-direction: column; gap: 14px; }

.story-statement__line {
  font-size: clamp(30px, 3.6vw, 44px);
  font-weight: 600;
  line-height: 1.14;
  letter-spacing: -.022em;
  margin: 0;
}
.story-statement__line--quiet { color: var(--fg3); }

.story-statement__script {
  font-size: clamp(60px, 7vw, 96px);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -.011em;
  color: var(--story-pink);
  margin: 6px 0 0;
}

/* --- Long-form prose ------------------------------------------------------ */
/* Direct children only: these sections also hold .care grids, whose own
   15px type must not be swallowed by the body size. */

.story-prose > p {
  font-size: 19px;
  line-height: 1.55;
  color: var(--fg);
  margin: 0 0 18px;
}
.story-prose > p:last-child { margin-bottom: 0; }
.story-prose > p.story-soft { color: var(--fg2); }

.story-quote {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 26px 0;
  padding: 0 0 0 20px;
  border-left: 2px solid var(--accent);
}
.story-quote p {
  font-size: 19px;
  line-height: 1.4;
  color: var(--fg2);
  margin: 0;
}

/* --- "We are because you are" split --------------------------------------- */

.story-split {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(260px, .9fr);
  gap: clamp(28px, 4vw, 64px);
  align-items: start;
}

.story-moments {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.story-moments li { display: flex; }
/* Five moments, two columns — the last one runs full width rather than
   leaving a hole in the grid. */
.story-moments li:last-child { grid-column: span 2; }

/* Each moment is now a link to the record behind it, so the card is the <a>
   rather than the <li>: the whole tile is the hit area and one focus stop, and
   nothing inside it is separately tabbable.

   The panel styling moved off the <li> with it. It cannot stay there and have
   the link fill it — a padded <li> with a padded <a> inside would give the
   border one box and the click target a smaller one, which is how a card ends
   up looking clickable at an edge that does nothing. */
.story-moment {
  display: flex;
  flex-direction: column;
  gap: 14px;
  flex: 1;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 18px;
  color: inherit;
  text-decoration: none;
  transition: transform .45s cubic-bezier(.22, .61, .36, 1),
              border-color .45s ease, box-shadow .45s ease;
}

/* The designer's own type for the moment, unchanged — it is still the line the
   section is written around. */
.story-moment__text {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -.012em;
}

/* The record, set quieter than the moment above it so the card still reads
   moment-first. Pushed to the bottom so the two lines sit level across a row
   whose moments wrap to different heights. */
.story-moment__release {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

.story-moment__art {
  width: 44px;
  height: 44px;          /* square, and it must beat the width/height attributes
                            build.js writes from the 800px source file */
  flex: none;
  border-radius: 8px;
  object-fit: cover;
  background: var(--bg2);
  transition: transform .45s cubic-bezier(.22, .61, .36, 1);
}

.story-moment__names { display: flex; flex-direction: column; min-width: 0; }
.story-moment__album {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -.01em;
  /* Long titles truncate rather than wrap the card taller than its neighbour. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.story-moment__meta { font-size: 12px; color: var(--fg3); margin-top: 2px; }

.story-moment .arrow-slide {
  margin-left: auto;
  padding-left: 6px;
  font-size: 15px;
  color: var(--fg3);
}

.story-moment:hover,
.story-moment:focus-visible {
  border-color: var(--fg3);
  transform: translateY(-4px);
  box-shadow: 0 18px 36px rgba(15, 17, 19, .12);
}
.story-moment:hover .story-moment__art,
.story-moment:focus-visible .story-moment__art { transform: scale(1.06); }

/* .arrow-slide's reveal-on-hover lives in events.css and names .gig and
   .event-card explicitly, so a third component has to opt itself in. Worth
   promoting to the base sheet next time it is touched — see the summary. */
@media (hover: hover) {
  .story-moment:hover .arrow-slide,
  .story-moment:focus-visible .arrow-slide { opacity: 1; transform: translateX(0); }
}

/* --- Script refrains ------------------------------------------------------ */

.story-refrain {
  font-size: clamp(42px, 4.6vw, 58px);
  line-height: 1.1;
  color: var(--accent);
  text-align: center;
  margin: clamp(34px, 4vw, 56px) 0 0;
}

.story-welcome {
  font-size: clamp(56px, 6.4vw, 88px);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -.011em;
  color: var(--accent);
  margin: 0 0 14px;
}

/* .care__title is a <div> in the base sheet; as the <h3> this page needs it
   also has to lose the UA top margin. */
h3.care__title { margin-top: 0; }

/* ==========================================================================
   Breakpoints — 900px and 680px, the designer's own thresholds.
   ========================================================================== */

@media (max-width: 899px) {
  .story-split { grid-template-columns: 1fr; }
}

@media (max-width: 679px) {
  .story-statement__line { font-size: 26px; }
  .story-statement__script { font-size: 52px; }

  .story-prose > p,
  .story-quote p { font-size: 17px; }

  .story-moments { grid-template-columns: 1fr; gap: 10px; }
  .story-moments li:last-child { grid-column: auto; }

  .story-refrain { font-size: 38px; }
  .story-welcome { font-size: 48px; }
}

/* The signature over the closing banner. Positioned like the .plate__mark
   overlay it replaces: centred, riding 7% up from the bottom of the plate, and
   sized as a percentage of the plate so it scales in exact proportion at every
   width, which is the property the SVG viewBox used to give for free.

   38% is what the Alex Brush word actually measured there — its ink came out
   364 units wide in a 1000-unit viewBox — so the banner's rhythm is unchanged
   by the swap.

   height:auto is load-bearing, not tidiness. The logo partial's <svg> carries
   width and height attributes as its no-stylesheet fallback, and an element
   with a CSS width plus a height attribute already has both dimensions, so
   .brand-mark's aspect-ratio never computes and the mark would render 38% wide
   and 38px tall. That exact fault shipped 31 covers as slivers on 4 August.

   The colour is set rather than inherited because the mark is filled with
   currentColor, and #F2EDE3 is the ink .plate__overlay text already uses in the
   base sheet — so the signature matches the chapter labels instead of arriving
   as pure white next to them. */
.plate__sig {
  left: 0;
  right: 0;
  bottom: 7%;
  width: 100%;
  color: #F2EDE3;
}
.plate__sig .brand-mark {
  width: 38%;
  height: auto;
  margin: 0 auto;
}
