/* Emma Luo — v2. No build step, no framework, one page.
   One page is deliberate: audio never cuts out because you never navigate. */

/* The parallax offset, declared so that it does not inherit.
   A custom property inherits by default, so writing --par once per scroll frame invalidated
   the computed style of every descendant of whatever it was written to. app.js writes it to
   the element that actually uses it (.sec-bg-in, .hero-inner), so the invalidation has nowhere
   to spread. Typing it also lets the engine keep it as a length instead of re-parsing a string.

   --par-slow is deliberately NOT registered: it is written to .hero and read by .hero::before,
   and a pseudo-element gets it by inheriting from its originating element. Registering it with
   `inherits: false` silently pinned the poster at 0 — which is how this comment got written. */
@property --par {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

:root,
[data-theme="warm-paper"] {
  --theme-name: 'warm-paper';
  --paper: #faf4ea;
  --paper-2: #f2e9d9;
  --paper-3: #ecdfca;
  --ink: #2c2420;
  --soft: #6d5f53;
  --rule: #cdbfa9;
  --accent: #bb4d2b;
  --accent-2: #3d6b60;
  --terracotta: #bb4d2b;
  --gold: #d9982b;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Helvetica, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 5px 6px 0 var(--rule);
  --staff-opacity: .25;
}

[data-theme="cinematic-dark"] {
  --theme-name: 'cinematic-dark';
  --paper: #161210;
  --paper-2: #221c18;
  --paper-3: #2e2621;
  --ink: #f6f1e9;
  --soft: #b0a193;
  --rule: #463b34;
  --accent: #e5a93c;
  /* +0.3% lightness, same hue: .quip sat at 4.49 against --paper-2 */
  --accent-2: #4e917f;
  --terracotta: #d95d39;
  --gold: #f5c256;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.5), 0 0 16px rgba(229, 169, 60, 0.2);
  --staff-opacity: .06;
}


[data-theme="midnight-navy"] {
  --theme-name: 'midnight-navy';
  --paper: #091322;
  --paper-2: #112240;
  --paper-3: #1e3a60;
  --ink: #f8fafc;
  /* lightness only: the footer's --soft on --paper-3 measured 4.48 */
  --soft: #96a5b9;
  --rule: #233554;
  --accent: #38bdf8;
  --accent-2: #34d399;
  --terracotta: #f43f5e;
  --gold: #fbbf24;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(56, 189, 248, 0.25);
  --staff-opacity: .08;
}

[data-theme="vintage-noir"] {
  --theme-name: 'vintage-noir';
  --paper: #121214;
  --paper-2: #1c1c20;
  --paper-3: #26262c;
  --ink: #f0f0f5;
  --soft: #9e9ea6;
  --rule: #3a3a42;
  /* lightness only: --paper on --accent (.flag, .skip, the wordmark's ♪) measured 4.49 */
  --accent: #e73e4a;
  /* lightness only: .quip on --paper-2 measured 3.70 */
  --accent-2: #4e8bb1;
  --terracotta: #e63946;
  --gold: #f4a261;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(230, 57, 70, 0.25);
  --staff-opacity: .05;
}

[data-theme="velvet-plum"] {
  --theme-name: 'velvet-plum';
  --paper: #180e1a;
  --paper-2: #241428;
  --paper-3: #321b38;
  --ink: #fcf7f2;
  --soft: #b89dba;
  --rule: #46284d;
  --accent: #e8a598;
  --accent-2: #b5838d;
  --terracotta: #e07a5f;
  --gold: #f4d35e;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(232, 165, 152, 0.25);
  --staff-opacity: .07;
}

[data-theme="cyber-neon"] {
  --theme-name: 'cyber-neon';
  --paper: #0d0d15;
  --paper-2: #161624;
  --paper-3: #222238;
  --ink: #f5f3ff;
  --soft: #a78bfa;
  --rule: #3730a3;
  --accent: #d946ef;
  --accent-2: #06b6d4;
  --terracotta: #ff2a85;
  --gold: #facc15;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.7), 0 0 18px rgba(217, 70, 239, 0.35);
  --staff-opacity: .08;
}

[data-theme="emerald-forest"] {
  --theme-name: 'emerald-forest';
  --paper: #0c1a14;
  --paper-2: #14281f;
  --paper-3: #1c382b;
  --ink: #f0fdf4;
  --soft: #86efac;
  --rule: #274e3c;
  --accent: #34d399;
  --accent-2: #fbbf24;
  --terracotta: #f87171;
  --gold: #f59e0b;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(52, 211, 153, 0.25);
  --staff-opacity: .07;
}

[data-theme="nordic-frost"] {
  --theme-name: 'nordic-frost';
  --paper: #0f172a;
  --paper-2: #1e293b;
  --paper-3: #334155;
  --ink: #f8fafc;
  /* lightness only: the footer's --soft on --paper-3 measured 4.04 */
  --soft: #a0adc0;
  --rule: #475569;
  --accent: #38bdf8;
  --accent-2: #818cf8;
  --terracotta: #fb7185;
  --gold: #fcd34d;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(56, 189, 248, 0.25);
  --staff-opacity: .07;
}

[data-theme="desert-sunset"] {
  --theme-name: 'desert-sunset';
  --paper: #1c1210;
  --paper-2: #2b1c18;
  --paper-3: #3b2721;
  --ink: #fff7ed;
  --soft: #fdba74;
  --rule: #573429;
  --accent: #f97316;
  --accent-2: #fb7185;
  --terracotta: #ea580c;
  --gold: #facc15;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(249, 115, 22, 0.25);
  --staff-opacity: .07;
}

[data-theme="tokyo-dusk"] {
  --theme-name: 'tokyo-dusk';
  --paper: #11101d;
  --paper-2: #1a182d;
  --paper-3: #252240;
  --ink: #f5f3ff;
  --soft: #c084fc;
  --rule: #4c3b6e;
  --accent: #ff758f;
  --accent-2: #38bdf8;
  --terracotta: #ff4d6d;
  --gold: #ffd166;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(255, 117, 143, 0.25);
  --staff-opacity: .08;
}

[data-theme="espresso-gold"] {
  --theme-name: 'espresso-gold';
  --paper: #19120c;
  --paper-2: #261b12;
  --paper-3: #342519;
  --ink: #faf6f0;
  --soft: #d4c5b3;
  --rule: #4a3625;
  --accent: #ebd6a5;
  --accent-2: #c49a6c;
  --terracotta: #d96b43;
  --gold: #f3e5ab;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(235, 214, 165, 0.25);
  --staff-opacity: .07;
}

[data-theme="monochrome-minimal"] {
  --theme-name: 'monochrome-minimal';
  --paper: #000000;
  --paper-2: #121212;
  --paper-3: #1e1e1e;
  --ink: #ffffff;
  --soft: #a1a1aa;
  --rule: #3f3f46;
  --accent: #ffffff;
  --accent-2: #d4d4d8;
  --terracotta: #ef4444;
  --gold: #fbbf24;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.8), 0 0 16px rgba(255, 255, 255, 0.2);
  --staff-opacity: .05;
}

[data-theme="autumn-cellar"] {
  --theme-name: 'autumn-cellar';
  --paper: #1e1510;
  --paper-2: #2e2018;
  --paper-3: #3e2b20;
  --ink: #fef3c7;
  --soft: #fde68a;
  --rule: #5c3c28;
  /* lightness only: --paper on --accent (.flag, .skip, the wordmark's ♪) measured 4.10 */
  --accent: #c7664e;
  --accent-2: #eab308;
  --terracotta: #c2593f;
  --gold: #f59e0b;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(0, 0, 0, 0.6), 0 0 16px rgba(194, 89, 63, 0.25);
  --staff-opacity: .07;
}

/* stone, candle-smoke and old gilding — a scriptorium at night */
[data-theme="gothic-scriptorium"] {
  --theme-name: 'gothic-scriptorium';
  --paper: #16141b;
  --paper-2: #201d28;
  --paper-3: #2b2635;
  --ink: #ece7f2;
  --soft: #b9b0cb;
  --rule: #443c56;
  --accent: #b08d57;
  /* the selected-tag pill fills with --accent-2 and sets its label in --paper, so on a dark
     ground this has to be the light one: crimson measured 2.60:1 against the ink it carries */
  --accent-2: #d6a9b3;
  --terracotta: #8f3f4d;
  --gold: #d4b06a;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 26px rgba(0, 0, 0, .65), 0 0 18px rgba(176, 141, 87, .22);
  --staff-opacity: .07;
}

/* pressed flowers and buttercream — daylight, not sugar */
[data-theme="cottage-garden"] {
  --theme-name: 'cottage-garden';
  --paper: #fdf6f4;
  --paper-2: #f8ebe8;
  --paper-3: #f2e0e4;
  --ink: #3b2b33;
  --soft: #6d5560;
  --rule: #ddbfc6;
  --accent: #a84c6f;
  /* same rule the other way up: light paper means the pill's fill carries the contrast, and
     the softer sage measured 3.62:1 */
  --accent-2: #3f5733;
  --terracotta: #a84c6f;
  --gold: #d9a441;
  --hand: 'Shantell Sans', 'Comic Sans MS', cursive;
  --body: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif;
  --navh: 3.5rem;
  --pad: max(1.8rem, 3vw);
  --wide: 1020px;
  --sketch: 0;
  --sketch-2: 0;
  --ease: cubic-bezier(.22, .9, .3, 1);
  --lift: 0 10px 24px rgba(88, 52, 66, .18), 0 0 16px rgba(180, 87, 122, .16);
  --staff-opacity: .08;
}

/* --accent as small body copy ("back to top", an award line, a testimonial credit) has to
   clear 4.5:1, and four palettes miss it against --paper-3. This is that one job: same hue,
   same saturation, lightness moved just far enough, and only in the themes that failed.
   --accent itself is untouched — it is also a fill and a border, where 4.5:1 is not the bar. */
:root {
  --accent-text: var(--accent);
}

[data-theme="warm-paper"] {
  --accent-text: #a74526;
}

[data-theme="vintage-noir"] {
  --accent-text: #eb5f6a;
}

[data-theme="cyber-neon"] {
  --accent-text: #da49ef;
}

[data-theme="autumn-cellar"] {
  --accent-text: #d1826f;
}

[data-theme="gothic-scriptorium"] {
  --accent-text: #c98d99;
}

[data-theme="cottage-garden"] {
  --accent-text: #8e3f5e;
}

* {
  box-sizing: border-box;
}

[hidden] {
  display: none !important;
}

/* .now is display:flex, which otherwise beats [hidden] */
html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--navh) + .75rem);
  /* anchors land below the nav */
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 16px/1.6 var(--body);
  overflow-x: hidden;
  /* faint staff-paper texture, warm not clinical */
  background-image: repeating-linear-gradient(to bottom, transparent 0 119px, rgba(205, 191, 169, .25) 119px 120px);
}

h1,
h2,
h3,
.mark,
.kicker,
.flag,
.hand {
  font-family: var(--hand);
  font-weight: 600;
}

a {
  color: var(--accent);
  text-underline-offset: 3px;
}

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

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

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

::selection {
  background: color-mix(in srgb, var(--gold) 45%, transparent);
}

.skip {
  position: absolute;
  left: -999px;
}

.skip:focus {
  left: 1rem;
  top: 1rem;
  z-index: 30;
  background: var(--paper);
  padding: .5rem;
}

/* reading progress — pure CSS where the browser supports scroll timelines */
.readbar {
  position: fixed;
  inset: 0 0 auto;
  height: 3px;
  z-index: 20;
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--accent), var(--gold), var(--accent-2));
  scale: 0 1;
}

@supports (animation-timeline: scroll()) {
  .readbar {
    animation: readbar linear both;
    animation-timeline: scroll(root block);
  }
}

@keyframes readbar {
  to {
    scale: 1 1;
  }
}

/* ---------- splash: the film opens with sound, so it asks first ---------- */
/* the gate holds the page still while it is up: it covers the viewport, so any scroll
   underneath it moves a page nobody can see and lands you somewhere you did not choose */
html.splash-open,
html.splash-open body {
  overflow: hidden;
  overscroll-behavior: none;
}

.splash {
  position: fixed;
  inset: 0;
  z-index: 50;
  overscroll-behavior: contain;
  touch-action: none;
  display: grid;
  place-items: center;
  padding: var(--pad);
  background: var(--paper);
  /* one flat colour — the staff ruling that used to run behind this competed with the type
     it was sitting under, and the door does not need a texture */
  transition: opacity .9s var(--ease), transform 1.1s var(--ease), filter .9s var(--ease);
}

/* Going in. The sheet does not fade so much as pull back off the film: it lifts towards you,
   goes soft, and thins out — while the hero underneath comes up to meet it from slightly
   further away, so for a moment the two are moving through each other. One second, one
   gesture, and nothing to sit through. */
.splash.gone {
  opacity: 0;
  transform: scale(1.14);
  filter: blur(14px);
  pointer-events: none;
}

.splash.gone .splash-in {
  transform: translateY(-1.5rem) scale(.96);
  opacity: 0;
  transition: transform .7s var(--ease), opacity .45s var(--ease);
}

/* Deliberately nothing on .hero-inner or .hero-media here: the parallax pass owns their
   transform (--par) and writes .hero-inner's opacity inline on every scroll frame, so a rule
   pushing the hero about would either be overwritten or would clobber the drift. The sheet
   does all the moving; the film is simply what is behind it. */

.splash-in {
  max-width: 34rem;
  text-align: center;
  animation: riseIn .7s var(--ease) backwards;
}

.splash-kicker {
  font-family: var(--hand);
  color: var(--accent-text);
  margin: 0 0 .3rem;
  letter-spacing: .04em;
}

.splash-in h2 {
  font-size: clamp(1.8rem, 6vw, 3rem);
  margin: 0 0 .8rem;
}

.splash-line {
  color: var(--soft);
  margin: 0 auto 2rem;
  max-width: 34ch;
}

.splash-row {
  display: flex;
  gap: .8rem;
  justify-content: center;
  flex-wrap: wrap;
}

.splash-go {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-family: var(--hand);
  font-size: 1.05rem;
  padding: .55rem 1.2rem;
  border: 2.5px solid var(--ink);
  border-radius: var(--sketch);
  background: var(--accent);
  color: var(--paper);
  transition: transform .2s var(--ease), box-shadow .2s;
}

.splash-go:hover {
  transform: translateY(-2px);
  box-shadow: 5px 6px 0 var(--ink);
}

.splash-go .count {
  display: grid;
  place-items: center;
  min-width: 1.9rem;
  height: 1.9rem;
  border-radius: 50%;
  background: var(--paper);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  animation: tick 1s linear infinite;
}

@keyframes tick {
  0% {
    transform: scale(1.18);
  }

  40% {
    transform: scale(1);
  }
}

.splash-mute {
  font-family: var(--hand);
  background: transparent;
  color: var(--soft);
  border: 1.5px solid var(--rule);
  border-radius: var(--sketch-2);
  padding: .55rem 1.2rem;
  transition: background .2s, color .2s, border-color .2s;
}

.splash-mute:hover {
  background: var(--paper-2);
  color: var(--ink);
  border-color: var(--ink);
}

/* ---------- nav ---------- */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 12;
  display: flex;
  align-items: center;
  gap: 1rem;
  /* fixed, not var(--navh): JS measures this element into --navh, and a circular
     min-height would latch the nav at whatever height it happened to have first */
  min-height: 3.5rem;
  padding: .4rem var(--pad);
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: blur(10px) saturate(1.2);
  border-bottom: 1.5px solid var(--rule);
  flex-wrap: wrap;
  transition: transform .4s var(--ease), opacity .3s;
}

/* The centred wordmark needs enough room for the complete section nav on its left. At 768px
   "contact" physically collided with the mark; below this content-based breakpoint the mark
   stays in normal flex flow, where the real gap is guaranteed instead of inferred. */
@media (min-width: 840px) {
  .nav {
    justify-content: flex-start;
  }

  .nav nav {
    order: 1;
    margin-right: auto;
  }

  .mark {
    order: 2;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
  }

  .theme-select,
  .now {
    order: 3;
    margin-left: auto;
  }
}

/* the hero owns the whole screen — the bar comes back once you've scrolled off it,
   and any time the keyboard needs it */
body.hero-view .nav {
  transform: translateY(-105%);
  opacity: 0;
}

body.hero-view .nav:focus-within {
  transform: none;
  opacity: 1;
}

/* …unless something is playing: the transport is the one control that must never walk
   off screen while you're using it. Its own rule, not another entry in the list above —
   an engine without :has() would throw the whole list away and take :focus-within,
   the keyboard's only way back to the nav, with it. */
body.hero-view .nav:has(.now:not([hidden])) {
  transform: none;
  opacity: 1;
}

.mark {
  font-size: 1.3rem;
  text-decoration: none;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: .3rem;
}

.mark-note {
  color: var(--accent);
  display: inline-block;
  transition: transform .4s var(--ease);
}

.mark:hover .mark-note {
  transform: rotate(-18deg) scale(1.2);
}

.nav nav {
  display: flex;
  gap: .9rem;
  flex-wrap: wrap;
  font-size: .93rem;
}

.nav nav a {
  color: var(--soft);
  text-decoration: none;
  position: relative;
  padding-bottom: 2px;
  transition: color .2s;
}

/* hand-drawn underline that wipes in, instead of a browser underline */
.nav nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--accent);
  border-radius: 2px;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .28s var(--ease);
}

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

.nav nav a:hover::after,
.nav nav a[aria-current]::after {
  transform: scaleX(1);
}

.nav nav a[aria-current] {
  color: var(--ink);
}

.lang-switch {
  font-family: var(--hand);
  font-weight: 600;
  font-size: .85rem;
  padding: .15rem .5rem;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent) !important;
  transition: all .2s var(--ease);
}

.lang-switch:hover {
  background: var(--accent);
  color: var(--paper) !important;
  border-color: var(--accent);
}

.lang-switch::after {
  display: none !important;
}

.theme-select {
  font-family: var(--hand);
  font-size: .82rem;
  padding: .25rem .65rem;
  border-radius: 999px;
  border: 1.5px solid var(--rule);
  background: var(--paper-2);
  color: var(--ink);
  cursor: pointer;
  outline: none;
  transition: border-color .2s, background .2s, color .2s, transform .2s var(--ease);
}

.theme-select:hover {
  border-color: var(--accent);
  background: var(--paper-3);
  transform: translateY(-1px);
}

.now {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-left: auto;
  padding: .3rem .7rem;
  border: 1.5px solid var(--rule);
  border-radius: 999px;
  background: var(--paper-2);
  font-size: .85rem;
  max-width: 100%;
  animation: popIn .35s var(--ease) backwards;
}

@keyframes popIn {
  from {
    opacity: 0;
    transform: translateY(-6px) scale(.94);
  }
}

/* 24px is the floor (WCAG 2.5.8) — this is the play/pause on the docked bottom bar */
.now-btn {
  background: none;
  border: 0;
  padding: 0 .2rem;
  line-height: 1;
  min-width: 24px;
  min-height: 24px;
}

/* a button now, not a label: it takes you to the card that is playing */
.now-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 14ch;
  font-family: var(--hand);
  font-size: inherit;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  text-align: left;
  cursor: pointer;
  text-decoration: underline dotted transparent;
  text-underline-offset: 3px;
  transition: color .2s, text-decoration-color .2s;
}

.now-title:hover,
.now-title:focus-visible {
  color: var(--accent);
  text-decoration-color: currentColor;
}

.now-time {
  color: var(--soft);
  font-variant-numeric: tabular-nums;
  font-size: .75rem;
  white-space: nowrap;
}

.bars {
  display: inline-flex;
  gap: 2px;
  align-items: flex-end;
  height: 12px;
}

.bars i {
  width: 3px;
  height: 4px;
  background: var(--accent);
  border-radius: 1px;
}

.now.live .bars i {
  animation: bounce .6s ease-in-out infinite alternate;
}

.now.live .bars i:nth-child(2) {
  animation-delay: .2s;
}

.now.live .bars i:nth-child(3) {
  animation-delay: .4s;
}

@keyframes bounce {
  to {
    height: 12px;
  }
}

/* seek: a range input, so keyboard scrubbing comes free.
   24px tall for the thumb to be catchable; the track below stays 4px and the engine centres
   it, so the bar looks the same and the 10px of dead air above and below it is target. */
.seek {
  -webkit-appearance: none;
  appearance: none;
  width: 76px;
  height: 24px;
  background: none;
  margin: 0;
}

.seek::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent) calc(var(--p, 0) * 100%), var(--rule) 0);
}

.seek::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--rule);
}

.seek::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
}

.seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -3.5px;
  border-radius: 50%;
  background: var(--accent);
  border: 0;
}

.seek::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}

/* ---------- layout ---------- */
main {
  width: 100%;
  max-width: 100%;
  margin: 0;
  padding: 0;
}

section {
  margin: 0 !important;
}

h2 {
  font-size: clamp(1.6rem, 4vw, 2.3rem);
  margin: 0 0 .2rem;
}

/* a heading draws its own underline as it arrives — the reveal observer sets .in */
main h2 {
  position: relative;
  width: fit-content;
}

main h2::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -.1em;
  height: .09em;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--accent), var(--gold) 65%, transparent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .9s var(--ease) .2s;
}

main h2.in::after {
  transform: scaleX(1);
}

.sub-note {
  color: var(--soft);
  margin: 0 0 1.5rem;
  font-size: .95rem;
}

hr.staff {
  display: none !important;
}

hr.staff::after {
  content: '♪';
  position: absolute;
  left: 50%;
  top: -2px;
  transform: translateX(-50%);
  background: var(--paper);
  padding: 0 .6rem;
  color: var(--accent);
  font-size: 1.6rem;
}

/* the divider draws itself left-to-right as it comes into view */
hr.staff.reveal {
  opacity: 1;
  transform: none;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1s var(--ease);
}

hr.staff.reveal.in {
  clip-path: inset(0 0 0 0);
}

/* scroll reveal — one class, staggered by --i */
.reveal {
  opacity: 0;
  transform: translateY(20px);
}

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

.reveal:not(hr) {
  transition: opacity .65s var(--ease), transform .65s var(--ease);
  transition-delay: calc(var(--i, 0) * 55ms);
}

/* ---------- hero: full-bleed film, type sits on top ---------- */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  height: 100svh;
  margin: 0;
  display: flex;
  align-items: flex-end;
  background: #1b1512;
}

.hero-media,
.hero-scrim,
.grain {
  position: absolute;
  inset: 0;
}

/* the poster holds the frame until the footage is up; blurred so its own lettering
   never competes with the headline it sits under */
/* the overhang is what parallax eats into: the poster drifts by --par-slow as you scroll */
.hero::before {
  content: '';
  position: absolute;
  inset: -14% -12px;
  z-index: -3;
  background: var(--poster) center / cover no-repeat;
  /* .89, not the .55 this was: the poster sits under .hero-scrim, which now carries the flat
     .38 grade the footage used to apply to itself, and .89 × .62 lands back on the .55 the
     poster has always read at. A filter on a still image is rasterised once, so it stays here
     rather than being folded into the scrim like the footage's was. */
  filter: brightness(.89) saturate(.8) blur(6px);
  transform: translate3d(0, var(--par-slow, 0px), 0);
  transition: opacity 1.1s var(--ease), visibility 0s linear 1.1s;
}

/* Once the footage is up it covers the poster completely, but a blurred layer left underneath
   is still a blur the compositor re-runs on every parallax frame. Retire it — and only then,
   so the fade-in has something to fade over. Engines without :has keep the old behaviour,
   which is merely the cost we used to pay everywhere. */
.hero:has(.hero-media.up:not(.youtube-audio))::before {
  opacity: 0;
  visibility: hidden;
}

/* No filter on this element, deliberately. It used to carry `brightness(.62) saturate(.85)` to
   grade the footage down so overlaid type always wins — but this is the element wrapping a live
   <video> or <iframe>, and a filter on a video layer is a filter Chrome may have to re-run for
   every decoded frame rather than promoting the video to an overlay. The grade moved into
   .hero-scrim, which is a static layer painted once.

   Honest note on what that bought, because the measurement is the point of this file's other
   comments too: on a desktop with a real GPU (RX 5600M, D3D11, 120Hz) it bought nothing —
   filter on 87% of a core in the GPU process, filter off 92%, which is noise. It is kept
   because it is the simpler rule and because software rasterisation is the case that was never
   measured, not because a number moved. If a round of docs/perf-loop.md ever shows it neutral
   there too, put the filter back and delete this.

   What the scrim cannot carry is the saturate(.85): it multiplies toward the dark, it does not
   desaturate. The footage reads a shade more colourful than it did. */
.hero-media {
  z-index: -2;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.1s var(--ease);
}

.hero-media.up {
  opacity: 1;
}

/* Cross-origin YouTube UI cannot be masked selectively. For that source, the authored poster
   is the deterministic hero picture and the hidden player supplies only the showreel audio. */
.hero-media.youtube-audio,
.hero-media.youtube-audio.up {
  opacity: 0;
}

.hero-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* an iframe can't object-fit, so size it to cover by hand: 16/9 against both axes */
.hero-media iframe {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100vw;
  height: 56.25vw;
  min-width: 177.78vh;
  min-height: 100%;
  border: 0;
}

.hero-scrim {
  z-index: -1;
  background:
    linear-gradient(to top, rgba(20, 15, 12, .95) 0%, rgba(20, 15, 12, .7) 45%, rgba(20, 15, 12, .4) 100%),
    radial-gradient(120% 80% at 70% 20%, transparent 35%, rgba(20, 15, 12, .55) 100%),
    /* the grade the footage used to wear as filter: brightness(.62). A flat .38 of the same
       dark is that multiply, painted once instead of once per decoded video frame. Listed last
       so it sits directly on the footage, underneath the two gradients that shape it. */
    linear-gradient(rgba(20, 15, 12, .38), rgba(20, 15, 12, .38));
}

/* film grain, so the footage reads as photographed rather than rendered */
.grain {
  z-index: -1;
  pointer-events: none;
  opacity: .3;
  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'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: grain 700ms steps(2) infinite;
}

/* it blends with the footage, so every step of it re-reads the video texture. Off the screen
   that is pure waste (app.js sets .idle), and on a touch device the whole effect is a
   140px noise tile nobody is close enough to read moving. */
.hero.idle .grain {
  animation-play-state: paused;
}

/* A paused embed is not a free embed. Telling the player to stop stops the picture, but the
   frame is still a live cross-origin document with its own layers and its own rAF, and it was
   the entire remaining cost of scrolling the catalogue: measured over a full-page scroll on
   software raster, 54fps with the film on the page and 60fps with it gone — every frame over
   33ms was this. `display: none` takes the frame out of layout, paint and compositing, and
   stops the document inside it rendering, without unloading it: scroll back up and it is the
   same player at the same second, no refetch. */
.hero.idle #heroMedia {
  display: none;
}

@media (hover: none) {
  .grain {
    animation: none;
  }
}

@keyframes grain {
  50% {
    transform: translate3d(-6px, 4px, 0);
  }
}

/* ---------- nothing bobs at a page nobody is touching ----------

   These four are the decorations that never stop: the cue bobbing under the hero, the arrows
   in the jump pill, the pill's own halo, and the little note swaying in the footer. Together
   they are a few hundred pixels of movement — and the reason to stop them is not their size.
   The compositor does not bill per animated pixel; it bills for being awake. One element
   moving anywhere on the page means a frame is produced for the whole page, at whatever the
   display asks for, for as long as the tab is open.

   How much that costs is the machine's business, not ours, and it varies more than seems
   reasonable: measured here (RX 5600M, D3D11, 120Hz) a blank page with a single 20px square
   translating cost a full core in Chrome's GPU process — the same as this entire page with the
   film running. A healthy desktop pays a twentieth of that for the same square. We cannot fix
   the machine from a stylesheet, but we can stop asking a page at rest for 120 frames a second
   of arrows.

   app.js adds .at-rest after a few seconds of no scroll, pointer, key or touch, and drops it on
   the first sign of life — so the cue still bobs for anyone who has just arrived and is deciding
   whether to scroll, which is the whole job of a cue. Paused, not cancelled: it resumes mid-arc
   rather than snapping back to its first frame.

   Two that deliberately stay out of it. The grain answers to the hero being on screen (above) —
   it is the footage's texture, and freezing it under a running film reads as a stall, not as
   rest. And the testimonial strip drifts because a carousel that is not moving looks like a
   carousel that is broken; it stops itself when it scrolls out of view, which is the same
   argument arriving from the other side. */
.at-rest .scroll-cue,
.at-rest .jump-arrow,
.at-rest .jump.show::after,
.at-rest .foot-note {
  animation-play-state: paused;
}

.hero-inner {
  position: relative;
  width: 100%;
  max-width: var(--wide);
  margin: 0 auto;
  padding: 0 var(--pad) 4.5rem;
  color: #faf4ea;
  transform: translate3d(0, var(--par, 0px), 0);
  /* sinks as the film scrolls away */
}

/* staggered entrance, no JS */
.hero-inner>* {
  animation: riseIn .9s var(--ease) backwards;
}

.hero-inner> :nth-child(1) {
  animation-delay: .05s;
}

.hero-inner> :nth-child(2) {
  animation-delay: .16s;
}

.hero-inner> :nth-child(3) {
  animation-delay: .3s;
}

.hero-inner> :nth-child(4) {
  animation-delay: .42s;
}

.hero-inner> :nth-child(5) {
  animation-delay: .54s;
}

@keyframes riseIn {
  from {
    opacity: 0;
    transform: translateY(26px);
    filter: blur(6px);
  }
}

.kicker {
  color: color-mix(in srgb, var(--paper) 78%, transparent);
  letter-spacing: .02em;
  margin: 0;
  font-size: .95rem;
}

h1 {
  font-size: clamp(3rem, 12vw, 6.5rem);
  line-height: .95;
  margin: .2rem 0 .6rem;
}

.hero h1 {
  color: #faf4ea;
  text-shadow: 0 2px 24px rgba(0, 0, 0, .85), 0 4px 40px rgba(0, 0, 0, .6);
}

.hero-line {
  font-size: 1.15rem;
  max-width: 46ch;
  margin: 0;
}

@keyframes ring {
  from {
    opacity: .8;
    transform: scale(1);
  }

  to {
    opacity: 0;
    transform: scale(1.45);
  }
}

/* film controls: the hero is the film, so these are the only buttons on it */
.hero-controls {
  position: absolute;
  right: var(--pad);
  /* the same --transport-h reserve .jump and .totop take: under 600px the bar docks to the
     bottom of the viewport and would otherwise ride over the bottom third of both buttons */
  bottom: calc(max(1.6rem, env(safe-area-inset-bottom)) + var(--transport-h, 0px));
  z-index: 1;
  display: flex;
  gap: .5rem;
  align-items: center;
  opacity: 1;
  visibility: visible;
  transition: opacity .2s var(--ease), visibility 0s linear 0s;
}

/* At a section anchor the last sliver of the 100svh hero is still physically behind the fixed
   mobile nav. Its controls used to show through that translucent nav even though the hero was
   no longer the active view. Keep them with the film, not with its off-screen edge. */
body:not(.hero-view) .hero-controls {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .2s var(--ease), visibility 0s linear .2s;
}

/* No box at all — the mark is the button. Both of these sit on the film itself, and a plate
   drawn round a glyph on a moving image reads as UI parked on the picture. Without the plate
   the glyph has to carry the contrast on its own, so it is bigger and takes a drop shadow
   instead of a background. The 44px hit area stays; only the paint is gone. */
.film-btn {
  min-width: 44px;
  height: 44px;
  padding: 0 .35rem;
  display: grid;
  place-items: center;
  font-family: var(--hand);
  font-size: 1.6rem;
  line-height: 1;
  color: #faf4ea;
  background: none;
  border: 0;
  border-radius: 0;
  filter: drop-shadow(0 2px 10px rgba(0, 0, 0, .85));
  transition: color .2s, transform .2s var(--ease), opacity .2s;
  opacity: .92;
}

.film-btn svg {
  width: 28px;
  height: 28px;
  display: block;
}

.film-btn:hover {
  opacity: 1;
  transform: translateY(-2px) scale(1.08);
}

/* Centred by auto margins rather than translate(-50%): the nudge below owns the transform,
   and the narrow-width rule moves the cue to the corner the film's buttons don't reach. */
.scroll-cue {
  position: absolute;
  inset-inline: 0;
  width: 44px;
  height: 44px;
  margin-inline: auto;
  bottom: calc(1.1rem + var(--transport-h, 0px));
  display: grid;
  place-items: center;
  color: #faf4ea;
  text-decoration: none;
  font-size: 1.4rem;
  opacity: .85;
  text-shadow: 0 2px 12px rgba(0, 0, 0, .8);
  animation: nudge 2.4s ease-in-out infinite;
}

@keyframes nudge {

  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(.4rem);
  }
}

/* ---------- section backdrops ----------
   Each section gets a layer of its own behind the cards — a built-in animated pattern, a
   picture, or a muted loop, chosen per section in content.json. It overhangs the section
   by 22% top and bottom so there is room to drift (app.js sets --par), and it is masked
   away at both edges so no seam ever shows where one section meets the next. */
/* `position: relative` but deliberately no `isolation` and no z-index, so a section is NOT a
   stacking context. It used to be one, and that trapped an open card's z-index: 4 inside the
   section it belongs to — so the hover panel, which hangs below its card by design, lost to the
   next section outright and had its own description cut in half. The layering the isolation
   used to buy is re-established explicitly below: backdrop and glyph at 0, the section's copy
   at 1, and a card that is open or hovered at 4, all resolved against the page. */
section.works,
.about,
.contact {
  position: relative;
  width: 100%;
  padding: 4.5rem var(--pad);
  margin: 0;
  border: none;
  border-bottom: 2.5px solid var(--rule);
  box-sizing: border-box;
}

section.works#film-scores {
  background: var(--paper-2);
  border-top: 2.5px solid var(--rule);
}

section.works#songs {
  background: var(--paper-3);
}

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

section.contact {
  background: var(--paper);
  border-bottom: none;
}

section.works > *:not(.sec-bg),
section.about > *:not(.sec-bg),
section.contact > *:not(.sec-bg) {
  max-width: var(--wide);
  margin-left: auto;
  margin-right: auto;
}

/* The section's copy, lifted over the backdrop and the glyph, which sit at 0. With no
   isolation on the section that is a position in the *page's* stack, which is the point.
   Named rather than "everything except", because the children left out are left out for four
   different reasons: .filters owns a sticky position and z-index 9, .jump owns a fixed one and
   z-index 15 — this rule would overwrite both; .grid must not become a stacking context or an
   open card's z-index: 4 is trapped one level further down, exactly as it was in the section;
   and this selector outweighs `.card:hover`, so a featured card would be pinned at 1 and never
   reach 4. All four are already above .sec-bg without help — they are positioned, and they come
   after it in the markup. */
section.works > :is(.sec-title-row, .sub-note, .none, .more-link),
section.about > :is(h2, .about-grid),
section.contact > .contact-box {
  position: relative;
  z-index: 1;
}

.sec-bg {
  /* full-bleed: stationary mask container centered relative to section */
  position: absolute;
  top: -10%;
  bottom: -10%;
  left: 50%;
  width: 100dvw;
  transform: translateX(-50%);
  /* 0, not -1: the section is no longer a stacking context, and -1 here would put the backdrop
     behind the *page* — behind the section's own paper, i.e. invisible. At 0 it is the floor of
     the section's own stack and the copy above it is lifted to 1. */
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  opacity: var(--bg-opacity, .5);
  mask-image: linear-gradient(to bottom, transparent 0, #000 15%, #000 85%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 15%, #000 85%, transparent 100%);
}

.sec-bg-in {
  position: absolute;
  top: -35%;
  bottom: -35%;
  left: 0;
  right: 0;
  transform: translate3d(0, var(--par, 0px), 0);
  /* no will-change: translate3d already promotes this, and four permanent 1280×2900 layers
     is ~60MB of GPU memory a tablet does not have to spare. */
}

/* A backdrop nobody can see still animates: the timeline runs, the layer re-rasterises, and
   on a slow device that is the whole scroll budget spent on off-screen paint. app.js puts
   .idle on every .sec-bg outside the viewport; everything below it stops until it comes back.
   !important because each art below sets `animation:` as a shorthand from a longer selector,
   which would otherwise reset play-state to running — and this is a switch, not a look. */
/* .idle is still set on an off-screen .sec-bg, but nothing under it animates any more (see
   below) — what it is for now is the <video> backdrop the ⚙ can set, which app.js pause()s,
   because animation-play-state was never going to stop a video. */

.sec-bg[data-src] .sec-bg-in {
  background: var(--bg-image) center / cover no-repeat;
  filter: blur(var(--bg-blur, 0px)) saturate(.85);
}

.sec-bg video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- the backdrops do not animate themselves ----------
   They used to: the aurora drifted over 28 seconds, the staff's notes walked across, the waves
   crawled. All three are gone, and this is the single largest thing on this page.

   `.sec-bg` carries a mask, an opacity and an `overflow: hidden`, and `.sec-bg-in` a transform.
   An animation under that cannot stay on the compositor, so every frame of the wander repainted
   the whole backdrop on the main thread — and a backdrop is 1.7× a section tall, which on a
   phone or a tablet, where the catalogue lays out in one or two columns instead of three, is
   several thousand pixels. Measured on software rasterisation, scrolling the catalogue:

                            animating   still   no backdrop at all
     1280×900 desktop         25–27      49.2         51.2
     768×1024 iPad            22.6       48.5         59.5

   Stopping the animation is 92% of what deleting the backdrop entirely would buy, at every
   width. Removing the mask instead was tried and recovers a fifth of it — the mask is not the
   only reason the compositor declines.

   What is kept is the art and the parallax: the backdrops still move against the page as you
   scroll, because that is `--par` written per frame to a transform, which does composite, and
   it is the motion anyone actually notices. What is lost is a wander nobody watching a page
   for thirty seconds could describe. */

/* aurora — slow warm washes, the palette out of focus.
   The blur used to be a real one: filter: blur(46px) over a layer inset -25% of a section two
   viewports tall. A filtered layer cannot be re-used across a transform, so that was a full
   re-rasterise of ~1600×2800px every frame. A radial gradient is already a blur — widening the
   stops to reach transparent by 100% instead of 70% buys the same softness for nothing. */
.sec-bg[data-art="aurora"] .sec-bg-in::before,
.sec-bg[data-art="aurora"] .sec-bg-in::after {
  content: '';
  position: absolute;
  inset: -25%;
  background:
    radial-gradient(46% 40% at 22% 28%, color-mix(in srgb, var(--accent) 55%, transparent), transparent 100%),
    radial-gradient(42% 38% at 76% 60%, color-mix(in srgb, var(--accent-2) 50%, transparent), transparent 100%),
    radial-gradient(38% 34% at 50% 88%, color-mix(in srgb, var(--gold) 46%, transparent), transparent 100%);
}

/* the second wash is offset and dimmer, so the two read as depth rather than one flat field */
.sec-bg[data-art="aurora"] .sec-bg-in::after {
  transform: translate3d(7%, -5%, 0);
  opacity: .65;
}

/* staff — ruled paper with a line of notes walking across it */
.sec-bg[data-art="staff"] .sec-bg-in::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(to bottom, transparent 0 26px, var(--rule) 26px 27px);
  background-size: 100% 162px;
}

.sec-bg[data-art="staff"] .sec-bg-in::after {
  content: '♪ ♫ ♩ ♬ ♪ ♫ ♩ ♬ ♪ ♫ ♩ ♬ ♪ ♫ ♩ ♬ ♪ ♫ ♩ ♬ ♪ ♫ ♩ ♬';
  position: absolute;
  left: 0;
  top: 40%;
  white-space: nowrap;
  font-size: 2.6rem;
  letter-spacing: 2.6rem;
  color: var(--accent);
  opacity: .55;
}

/* waves — a sea of them, drawn once and repeated */
.sec-bg[data-art="waves"] .sec-bg-in {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='240'%3E%3Cg fill='none' stroke='%23cdbfa9' stroke-width='2'%3E%3Cpath d='M0 60q150 -46 300 0t300 0 300 0 300 0'/%3E%3Cpath d='M0 120q150 -46 300 0t300 0 300 0 300 0'/%3E%3Cpath d='M0 180q150 -46 300 0t300 0 300 0 300 0'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 1200px 240px;
}

/* dots — halftone. The `animation: halftone` this used to carry named a keyframe that was
   never written, so it drifted nowhere; the drift is the parallax on .sec-bg-in. */
.sec-bg[data-art="dots"] .sec-bg-in {
  background-image: radial-gradient(var(--rule) 1.5px, transparent 1.6px);
  background-size: 24px 24px;
}

/* The glyph below is `scale(1.2) rotate(-6deg)` against the content column's right edge, and a
   transformed painted box counts towards its ancestors' scrollable overflow — so every section
   was ~16px wider than the viewport and the whole page could be dragged sideways, leaving a
   strip of nothing down the right edge. body's `overflow-x: hidden` does not stop it: propagated
   to the viewport it still leaves a scrollable region. `clip` on the sections is the axis-only
   version that has no scrollport at all, and unlike `hidden` here it leaves the sticky filter
   bar alone. */
section.works,
section.about,
section.contact {
  overflow-x: clip;
}

/* ---------- section vector symbols: engraved into the backdrop, drawn on entrance ----------
   Dim and behind the work at all times. It used to arrive bright and on top and only sink
   once the section filled 35% of the viewport — a ratio a phone-height section never
   reaches, so on mobile the glyph simply sat over the cards forever. There is one state
   now, and the only thing that animates is the line drawing itself in. */
.sec-symbol-svg {
  position: absolute;
  /* the content column's right edge, not the viewport's: the section is padded by --pad and
     its children are capped at --wide and centred, so that is where the copy actually ends */
  right: calc(var(--pad) + max(0px, (100% - 2 * var(--pad) - var(--wide)) / 2));
  /* and no `top` at all. The glyph is written into the markup after the section's heading and
     its note, so `auto` puts it exactly below them, at every width, without a number to keep
     in step with the copy. Measured against the viewport it sat squarely behind the heading on
     a phone — decoration over the words is the wrong way round. The margin clears the lift the
     tilt below gives the painted box over its layout box. */
  top: auto;
  margin-top: 2rem;
  --glyph: clamp(200px, 28vw, 380px);
  width: var(--glyph);
  height: var(--glyph);
  color: var(--accent);
  pointer-events: none;
  /* the same floor as .sec-bg; it is written into the markup straight after it, so document
     order alone keeps the glyph above the backdrop, and the copy at 1 keeps it under the words */
  z-index: 0;
  /* .18 put 18% of --accent over --paper-2, which took --soft body copy to 4.06:1 in
     warm-paper — the worst of the thirteen. .09 is the step that carries that same copy to
     4.57:1, measured, not guessed: a wash a reader has to fight is not decoration. */
  opacity: .09;
  filter: blur(4px);
  /* from the top edge, not the centre: about the centre the 1.2 scale grew the painted box
     ~47px past its layout box at the widest sizes and put the glyph back over the note it had
     just been moved out from under. Anchored at the top it only ever grows downward, and the
     tilt is the sole thing left overhanging — which the margin above pays for. */
  transform-origin: 50% 0;
  transform: scale(1.2) rotate(-6deg);
}

/* contact's heading and note live inside .contact-box, so there is no flow position after
   them to fall into — this one is anchored to the foot of the section instead, which puts it
   under the box's lower half and clear of the copy at the top of it. */
section.contact .sec-symbol-svg {
  top: auto;
  /* the scale and the tilt hang .26 of the glyph's own size below its layout box, so the
     offset is written in that size — a bare 1rem let the tail paint out under the footer */
  bottom: calc(var(--glyph) * .26 + 1rem);
  margin-top: 0;
}

/* drawn slowly on purpose — a decoration you notice, not a transition you wait for. The
   same duration runs in reverse when the section leaves the viewport. */
.sec-symbol-svg path,
.sec-symbol-svg circle,
.sec-symbol-svg rect,
.sec-symbol-svg polygon {
  transition: stroke-dashoffset 2.8s var(--ease);
}

/* ---------- filter bar: one per section, between the feature and the grid ---------- */
.filters {
  position: sticky;
  /* a hair of daylight under the header, so the strip reads as floating below it rather
     than welded to its underside once you start scrolling */
  top: calc(var(--navh) + .5rem);
  z-index: 9;
  display: flex;
  gap: .5rem .7rem;
  align-items: center;
  flex-wrap: nowrap;
  /* no padding on the left: the strip inside carries a 16px mask inset of its own, and
     paying for both pushed the first pill a third of an inch further in than it needed */
  padding: .4rem .8rem .4rem 0;
  margin: 1rem auto 1.5rem auto;
  max-width: var(--wide);
  /* At rest there is no plate: no fill, no border, no box. The pills sit straight on the
     section, and the section's art shows through the gaps between them the way it should.

     The plate is what *sticking* looks like. Once the strip parks under the header it takes a
     solid fill and a border, because from then on the page is scrolling behind it and the tags
     have to stay readable over whatever passes. Solid rather than frosted — every section runs
     its own moving backdrop and a translucent plate read differently in each one. `inherit`
     takes that section's own paper, so it arrives as a surface belonging to the section rather
     than a rectangle of some other section's colour dropped on top. */
  background-color: transparent;
  border: 1.5px solid transparent;
  /* softer than a panel, harder than the pills it holds — enough curve that the plate reads
     as cut rather than ruled, without turning into another pill */
  border-radius: 10px;
  box-shadow: none;
  transition: background-color .25s var(--ease), border-color .25s var(--ease), box-shadow .25s var(--ease);
}

.filters.is-stuck {
  background-color: inherit;
  border-color: var(--rule);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.14), 0 0 18px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* pills render in a single-row horizontal touch scroll strip */
.pillstrip {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  gap: .4rem;
  flex: 1 1 auto;
  /* exactly the mask's fade width, no more: at rest the first pill starts where the mask
     has finished fading, so nothing is blurred until you actually scroll the strip */
  padding: .25rem 16px;
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  scroll-behavior: smooth;
  cursor: grab;
  mask-image: linear-gradient(to right, transparent 0%, #000 16px, #000 calc(100% - 16px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 16px, #000 calc(100% - 16px), transparent 100%);
}

.pillstrip.dragging {
  cursor: grabbing;
  user-select: none;
  scroll-behavior: auto;
}

.pillstrip::-webkit-scrollbar {
  display: none;
}

.pill {
  flex: none;
  background: transparent;
  border: 1.2px solid var(--rule);
  color: var(--soft);
  border-radius: 999px;
  padding: .12rem .55rem;
  font-size: .72rem;
  white-space: nowrap;
  transition: background .2s, color .2s, border-color .2s, transform .18s var(--ease);
}

/* button.pill, not .pill: a work's own tags are <li> and do nothing, so they get no lift and
   no accent under the cursor either — hover affordance on a dead element is a lie. */
button.pill:hover {
  border-color: var(--accent-2);
  color: var(--accent-2);
  transform: translateY(-1px);
}

.pill.on {
  background: var(--accent-2);
  border-color: var(--accent-2);
  color: var(--paper);
}

button.pill.on:hover {
  transform: translateY(-1px) rotate(-1.5deg);
}

/* the two controls that share the strip's baseline with the pills wear the pill's box: same
   radius, same border, same padding, same type size — and so the same 24px of target, which
   is what "clear ✕" was 3.5px short of. Two buttons side by side that disagree on all four
   read as a mistake, not as a hierarchy. */
.tag-toggle,
.clear {
  flex: none;
  background: transparent;
  border: 1.2px solid var(--rule);
  border-radius: 999px;
  padding: .12rem .55rem;
  min-height: 24px;
  font-size: .72rem;
  white-space: nowrap;
  transition: background .2s, color .2s, border-color .2s, transform .18s var(--ease);
}

.tag-toggle {
  color: var(--soft);
}

.tag-toggle:hover,
.clear:hover {
  transform: translateY(-1px);
}

.tag-toggle:hover {
  color: var(--accent-2);
}

/* --accent-text, not --accent: at the pill's type size on the pill's plate, raw --accent is
   3.59:1 in autumn-cellar. This is the token round 1 added for exactly that. */
.clear {
  color: var(--accent-text);
}

/* ---------- cards ---------- */
/* stretch, not start: every card in a row takes the row's height, so a row is level whatever
   its three cards happen to contain — and it stays level for the rows the filter builds.
   This only works because .info below is an overlay: if the hover panel still pushed the
   card taller, hovering one card would resize its two neighbours with it. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr));
  gap: 1.2rem;
  /* start, not stretch: a stretched row hands every card the row's height, so the moment one
     card grew on hover its two neighbours grew with it. At `start` each card is exactly as
     tall as it is, and levelCards() gives them all the same resting height — so the row is
     level standing still, and only the hovered card moves. */
  align-items: start;
  min-height: 280px;
}

@media (min-width: 768px) {
  .grid {
    /* minmax(0,…), not 1fr: 1fr floors at min-content, so one long card title pinned the
       third column ~25px wider than the other two between 768 and 830px */
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* one transform, assembled from vars: hover owns --ty/--rz, the pointer owns --rx/--ry */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 2px solid var(--ink);
  border-radius: var(--sketch);
  background: color-mix(in srgb, var(--paper-2) calc(var(--card-opacity, 0.68) * 100%), transparent);
  /* No backdrop-filter, and this is the single biggest reason the page scrolls smoothly on a
     tablet. A backdrop-filter is a snapshot of everything painted behind the element plus a
     gaussian blur, redone whenever any of that moves — and what is behind every card here is a
     .sec-bg layer that moves on every scroll frame. Sixteen cards meant sixteen full re-blurs
     per frame for the whole length of a section. What it bought was softening a backdrop that
     is already a low-opacity blurred gradient, underneath paper that is already ~70% opaque:
     the frost was invisible and the cost was the scroll. Same reasoning as the nav dropping its
     frost under 600px (decisions-v2.2 §5) — the thing in front matters more than the blur. */
  overflow: hidden;
  /* the resting height, measured and written by levelCards() so every card in a grid starts
     level. It is a floor, not a cap: the hover panel below grows straight through it. */
  min-height: var(--card-h, 0px);
  transform: perspective(780px) translate3d(0, var(--ty, 0px), 0) rotate(var(--rz, 0deg)) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform .22s var(--ease), box-shadow .22s var(--ease), border-color .22s;
}

.card:nth-child(even) {
  border-radius: var(--sketch-2);
}

/* --rx and --ry only ever come from the pointer tilt, and app.js does not bind that without a
   hover-capable pointer or with reduced motion asked for. What is left there is a perspective
   and two zero rotations — an identity that still puts each of sixteen cards into its own 3D
   rendering context, which on a tablet is layer memory bought for no picture at all. */
@media (hover: none),
(prefers-reduced-motion: reduce) {
  .card {
    transform: translate3d(0, var(--ty, 0px), 0) rotate(var(--rz, 0deg));
  }
}

.card:hover,
.card:focus-within,
.card.open {
  --ty: -4px;
  --rz: -.25deg;
  box-shadow: var(--lift);
  /* its panel hangs into the row below, and a later card in the DOM would otherwise paint
     straight over it */
  z-index: 4;
}

.card.playing {
  box-shadow: 5px 6px 0 var(--accent);
  border-color: var(--accent);
}

.card.pop {
  animation: cardIn .4s var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}

@keyframes cardIn {
  from {
    opacity: 0;
    transform: translateY(14px) scale(.97);
  }
}

.thumb {
  position: relative;
  aspect-ratio: 16/9;
  background: var(--rule);
  overflow: hidden;
  /* the card above stopped clipping, so the artwork rounds its own top corners. `inherit`
     rather than a copy of the numbers: --sketch, the lopsided --sketch-2 every second card
     gets, and the sharp-corners setting all come along for free. It inherits the border-box
     radius where the padding-box one is 2px tighter, which is under a pixel at the arc. */
  border-radius: inherit;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

.thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .6s var(--ease), filter .4s;
}

.card:hover .thumb img {
  transform: scale(1.06);
}

/* light sweep across the artwork on hover */
.thumb::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, .35) 50%, transparent 65%);
  transform: translateX(-60%);
  transition: opacity .3s;
}

.card:hover .thumb::after {
  opacity: 1;
  animation: sweep .9s var(--ease);
}

@keyframes sweep {
  to {
    transform: translateX(60%);
  }
}

.noimg {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  font-size: 2.5rem;
  color: var(--soft);
  background: repeating-linear-gradient(to bottom, var(--paper) 0 9px, var(--rule) 9px 10px);
}

.card:hover .noimg {
  animation: noteHop .8s var(--ease);
}

@keyframes noteHop {
  40% {
    transform: translateY(-6px) rotate(-8deg);
  }
}

/* a square button, not a disc, and a real square — sharp corners at every size, so the
   per-size radius overrides that used to follow this block are gone */
/* the box is barely bigger than the glyph it holds — a mark on the artwork, not a widget
   parked on top of it. The hero's own transport keeps its larger size. */
.play {
  position: absolute;
  right: .5rem;
  bottom: .5rem;
  width: 22px;
  height: 22px;
  border-radius: 0;
  font-size: .74rem;
  line-height: 1;
  border: 1.5px solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  display: grid;
  place-items: center;
  transition: background .2s, color .2s, transform .2s var(--ease);
}

.play:hover {
  background: var(--accent);
  color: var(--paper);
  transform: scale(1.1);
}

.play.on {
  background: var(--accent);
  color: var(--paper);
}

.play.on::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 0;
  border: 1.5px solid var(--accent);
  animation: ring 1.8s ease-out infinite;
}

/* where a work actually lives — only shown when it isn't a plain audio file */
.src-badge {
  position: absolute;
  left: .6rem;
  top: .6rem;
  z-index: 2;
  font-family: var(--hand);
  font-size: .7rem;
  letter-spacing: .04em;
  padding: .08rem .55rem;
  border-radius: 999px;
  color: var(--paper);
  background: color-mix(in srgb, var(--ink) 76%, transparent);
  backdrop-filter: blur(3px);
  transform: rotate(-1.5deg);
  transition: transform .25s var(--ease), background .25s;
}

.src-badge[data-kind="youtube"] {
  background: color-mix(in srgb, #e02f2f 78%, var(--ink));
}

.src-badge[data-kind="vimeo"] {
  background: color-mix(in srgb, #1ab7ea 70%, var(--ink));
}

.src-badge[data-kind="instagram"] {
  background: color-mix(in srgb, #c13584 76%, var(--ink));
}

.src-badge[data-kind="spotify"] {
  background: color-mix(in srgb, #1db954 70%, var(--ink));
}

.src-badge[data-kind="soundcloud"] {
  background: color-mix(in srgb, #ff7700 78%, var(--ink));
}

.card:hover .src-badge {
  transform: rotate(-1.5deg) translateY(-2px) scale(1.06);
}

/* playing indicator on the artwork itself */
.eq {
  position: absolute;
  left: .6rem;
  bottom: .7rem;
  display: none;
  gap: 3px;
  align-items: flex-end;
  height: 16px;
}

.eq i {
  width: 3px;
  height: 5px;
  background: var(--paper);
  border-radius: 1px;
  box-shadow: 0 0 6px rgba(0, 0, 0, .6);
  animation: bounceTall .55s ease-in-out infinite alternate;
}

.eq i:nth-child(2) {
  animation-delay: .18s;
}

.eq i:nth-child(3) {
  animation-delay: .36s;
}

.playing .eq {
  display: flex;
}

@keyframes bounceTall {
  to {
    height: 16px;
  }
}

/* the caption is a stack of small text blocks; a shared rhythm between them is what
   stops a card reading as a receipt. One gap value, one place to change it. */
.cap {
  --cap-gap: .38rem;
  display: flex;
  flex-direction: column;
  gap: var(--cap-gap);
  padding: .7rem .9rem .85rem;
  /* takes the slack the stretched card hands down, so the text stays put at the top and the
     spare height collects at the bottom instead of stretching the gaps */
  flex: 1 1 auto;
}

.card-audio-bar {
  margin-top: .5rem;
  width: 100%;
}

.card-audio-bar audio {
  width: 100%;
  height: 28px;
  border-radius: 8px;
  outline: none;
}

/* a provider's own player, mounted in the card under its tags (work.embed in content.json).
   --embed-h is the provider's fixed compact height; anything else gets 16:9. */
/* video shape unless the provider has a fixed compact player, in which case cardEmbed()
   writes an inline height and that wins — aspect-ratio only sizes a box whose height is auto.
   Lives in the expanded view now, under the tag row. */
.card-embed {
  width: 100%;
  margin-top: .9rem;
  aspect-ratio: 16 / 9;
}

.card-embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: 12px;
  color-scheme: normal;
}

/* the gap above owns the vertical rhythm now — per-element margins would double it.
   The h3 is a grandchild (it sits inside .card-title-row), so it needs its own reset:
   without it the UA's 1em heading margin quietly reappears and the rhythm goes lumpy. */
.cap > :is(p, ul, div) {
  margin: 0;
}

.cap h3 {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.3;
}

/* Title and credit share one line. The row no longer reserves two lines: the stretched grid
   above levels the row, so the row can be exactly as tall as its content needs. */
.card-title-row {
  display: flex;
  justify-content: space-between;
  /* the credit settles on the title's *last* line, not its first. Plain `baseline` pinned
     the two boxes' first lines together, so a credit next to a two-line title started level
     with line one and trailed off below the title entirely. `last baseline` is the whole
     fix; flex-end is the fallback for a browser without it, and lands within a pixel. */
  align-items: flex-end;
  align-items: last baseline;
  gap: .3rem .7rem;
  flex-wrap: nowrap;
  margin: 0;
  width: 100%;
}

/* the title yields all the width, and wraps to as many lines as it needs. No clamp any
   more: the stretched grid levels the row regardless, so a third title line costs nothing —
   and a clamp here was paid for by the credit, which had to wrap to buy the title its
   second line. A wrapped title is fine; a wrapped two-word credit looks like a mistake. */
.card-title-row h3 {
  flex: 1 1 auto;
  min-width: 0;
}

/* One size for every credit, so no two cards set their title row to different heights. */
.card-dir {
  font-family: var(--hand);
  font-size: .7rem;
  line-height: 1.2;
  color: var(--soft);
  /* It never shrinks: the title yields instead and takes another line, which the levelled
     row absorbs for nothing. Shrinking, "dir. Yezy Suh" was being folded onto two lines to
     buy its title a second — a two-word credit on two lines reads as a bug. The cap is
     still there for the two credits that name two directors; those are the only ones that
     wrap, and they wrap onto the title's own lines rather than below them. */
  flex: 0 0 auto;
  min-width: 0;
  max-width: 62%;
  text-align: right;
  margin-left: auto;
}

.title-btn {
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  font: inherit;
  /* a title with no break opportunity in it ("Prologue/Epilogue") held its max-content width,
     overflowed its own heading and printed itself across the director credit */
  overflow-wrap: anywhere;
  transition: color .2s;
}

.title-btn:hover {
  color: var(--accent);
}

.pills {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
  list-style: none;
  margin: .5rem 0 0;
  padding: 0;
}

.meta {
  color: var(--soft);
  font-size: .85rem;
  margin: 0 0 .4rem;
}

.blurb {
  margin: 0 0 .6rem;
  font-size: .93rem;
}

.award {
  color: var(--accent-text);
  font-family: var(--hand);
  font-size: .84rem;
  margin: 0;
  line-height: 1.3;
}

/* The card grows downward on hover, the way it used to — but it grows *out of* its grid
   cell rather than inside it. In flow the panel pushed the card taller, and a stretched
   grid row levels itself to its tallest card, so one hovered card dragged its two
   neighbours up with it. Hung below the card's own box it takes nothing with it: the row
   stays exactly as level as it was, and only the card you are pointing at gets taller.

   It rides 8px up over the card's bottom edge so the two read as one box rather than a
   card with a tray under it, and it carries the card's bottom corners as its own. At rest
   it is a 2px transparent strip — no border colour, no backdrop, nothing drawn. */
/* In flow, inside the card. The card itself gets taller — one box, one border, one shadow,
   growing — rather than a second rectangle appearing under a card that stayed its old size.
   Everything below slides down to make room and slides back up on the way out, because that
   is just the grid reflowing.

   Its two row-mates hold still because the grid is `align-items: start` and every card
   carries a JS-measured resting height (--card-h), so a row is level at rest and a growing
   card is the only thing in it that moves. */
/* No rule above the role/year line. The panel is part of the card now, not a tray bolted to
   it, and a divider is what a tray needs — the gap does the separating. */
.info {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .28s var(--ease);
}

.info-in {
  overflow: hidden;
  padding: 0 .9rem;
  display: flex;
  flex-direction: column;
  gap: .35rem;
}

.info-in > * {
  margin: 0;
}

.card:hover .info,
.card:focus-within .info,
.card.open .info {
  grid-template-rows: 1fr;
}

/* reaching for a card's embedded player focuses the iframe, which is :focus-within on the
   card — the panel must not unfold under a control you are already aiming at. Keyed on the
   iframe itself, not :focus-within: tabbing to the card's own title or tags should still
   open the panel, exactly as it does on every card without a player. */
.card:has(.card-embed:focus-within):not(:hover):not(.open) :is(.info, .info-in) {
  grid-template-rows: 0fr;
  padding-bottom: 0;
}

.card:hover .info-in,
.card:focus-within .info-in,
.card.open .info-in {
  padding-block: .5rem .85rem;
}

.more {
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent);
  font-size: .88rem;
}

.more:hover {
  text-decoration: underline;
}

/* touch: the card says it has more behind it */
.chev {
  display: none;
  position: absolute;
  right: .7rem;
  top: .7rem;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  color: var(--ink);
  font-size: .8rem;
  place-items: center;
  transition: transform .28s var(--ease);
}

@media (hover: none) {
  .chev {
    display: grid;
  }

  .card.open .chev {
    transform: rotate(180deg);
  }
}

/* ---------- touch: taps are taps, zoom stays deliberate ----------
   A card takes two taps here (first opens its panel, second opens the detail view), and
   two taps in quick succession is also the browser's double-tap-to-zoom gesture — so the
   card's own interaction was fighting a page zoom, and every tap carried the legacy 300ms
   click delay on top. `manipulation` retires double-tap zoom and that delay while leaving
   pinch-zoom and scrolling untouched, which is the point: pinch is how a low-vision user
   reads this site (WCAG 1.4.4), so it is never disabled — only the accidental zooms go. */
@media (hover: none) {
  a,
  button,
  select,
  .card,
  .play,
  .pill {
    touch-action: manipulation;
  }

  /* the two drag strips own their axis and must keep it */
  .pillstrip,
  .testimonial-strip {
    touch-action: pan-x;
  }

  /* iOS zooms the whole page when a control smaller than 16px takes focus. The select was
     13.12px, so tapping the theme picker zoomed the site and left it there. */
  select,
  input,
  textarea {
    font-size: max(16px, 1rem);
  }

  /* 24px of tappable nav link is half of what a fingertip needs */
  .nav nav a {
    padding-block: .45rem;
  }
}

/* featured hero card */
.feature {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  align-items: stretch;
  margin-bottom: 2rem;
}

/* the feature's artwork is a left-hand column, not a top band, so the two corners it owns
   are the card's left pair */
.feature .thumb {
  aspect-ratio: auto;
  min-height: 260px;
  border-bottom-left-radius: inherit;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.feature .cap {
  --cap-gap: .7rem;
  padding: 1.5rem 1.5rem 1.6rem;
  align-self: center;
  position: relative;
}

.feature .cap h3 {
  font-size: clamp(1.4rem, 3vw, 2rem);
}

/* the feature's headline is twice the size of a grid card's, so its credit needs to grow
   with it or it reads as a footnote that fell off the title */
.feature .card-dir {
  font-size: 1rem;
  max-width: 50%;
}

.feature .play {
  width: 30px;
  height: 30px;
  font-size: .95rem;
}

.feature:hover {
  --ty: -4px;
  --rz: 0deg;
}

.flag {
  /* a flex item is blockified — without this the pill stretches the caption's full width */
  align-self: flex-start;
  font-size: .78rem;
  color: var(--paper);
  background: var(--accent);
  padding: .1rem .6rem;
  border-radius: 999px;
  margin: 0;
  letter-spacing: .05em;
  transform: rotate(-1.5deg);
}

.none {
  color: var(--soft);
  font-style: italic;
}

.more-link {
  display: block;
  margin: 1.6rem auto 0;
  padding: .45rem 1.4rem;
  font-family: var(--hand);
  background: color-mix(in srgb, var(--paper-2) 75%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 2px solid var(--ink);
  border-radius: var(--sketch);
  transition: background .2s, color .2s, transform .2s var(--ease), box-shadow .2s;
}

.more-link:hover {
  background: var(--accent);
  color: var(--paper);
  transform: translateY(2px);
  box-shadow: var(--lift);
}

/* ---------- down to the next section (subtle floating at bottom) ---------- */
.jump {
  position: fixed;
  bottom: calc(max(1rem, env(safe-area-inset-bottom)) + var(--transport-h, 0px));
  left: 50%;
  transform: translate(-50%, 10px);
  z-index: 15;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  width: fit-content;
  margin: 0;
  padding: .25rem .85rem .25rem .4rem;
  font-family: var(--hand);
  font-size: .85rem;
  color: var(--soft);
  text-decoration: none;
  background: color-mix(in srgb, var(--paper) 60%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--rule);
  border-radius: 999px;
  box-shadow: 0 4px 14px rgba(20, 15, 12, .08);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s var(--ease), transform .3s var(--ease), background .2s, color .2s, border-color .2s;
}

.jump.show {
  /* not .72: at that wash the label and its arrow composited to 2.96:1 in warm-paper and
     failed 4.5 in nine of the thirteen themes. .96 is the first step that carries the worst
     one past the line; the pill keeps its blur and its 60% paper wash for the airiness. */
  opacity: .96;
  pointer-events: auto;
  transform: translate(-50%, 0);
}

/* A soft ring breathing out of the pill, so it reads as an invitation rather than a label
   parked at the bottom of the screen. It lives on a pseudo-element on purpose: .jump owns
   its own transform for the translate(-50%) centring, and an animation on that property
   would fight the positioning. Hovering stops the pulse — you have already noticed it. */
.jump.show::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  border: 1.5px solid var(--accent);
  pointer-events: none;
  animation: jumpPulse 2.6s var(--ease) infinite;
}

.jump.show:hover::after {
  animation: none;
  opacity: 0;
}

@keyframes jumpPulse {
  0% {
    opacity: .55;
    transform: scale(1);
  }

  70%,
  100% {
    opacity: 0;
    transform: scale(1.22);
  }
}

.jump.show:hover {
  opacity: 1;
  color: var(--ink);
  border-color: var(--ink);
  background: color-mix(in srgb, var(--paper) 90%, transparent);
}

.jump-arrow {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: none;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: color-mix(in srgb, var(--paper-2) 80%, transparent);
  color: var(--soft);
  font-size: .75rem;
  animation: nudge-y 2.6s ease-in-out infinite;
}

@keyframes nudge-y {

  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(.18rem);
  }
}

.jump:hover .jump-arrow {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
}

/* ---------- about / contact ---------- */
.about-grid {
  display: flex;
  gap: 2rem;
  align-items: start;
}

.portrait-wrap {
  margin: 0;
  width: 180px;
  height: 180px;
  flex-shrink: 0;
}

.portrait {
  width: 180px;
  height: 180px;
  object-fit: cover;
  aspect-ratio: 1 / 1;
  border: 2.5px solid var(--ink);
  border-radius: var(--sketch);
  transition: transform .4s var(--ease);
}

.portrait-wrap:hover .portrait {
  transform: rotate(-1.2deg) scale(1.01);
}

.portrait-wrap figcaption {
  font-family: var(--hand);
  font-size: .85rem;
  color: var(--soft);
  margin-top: .4rem;
}

.bio p {
  max-width: 62ch;
}

.lede {
  font-size: 1.15rem;
}

blockquote {
  font-family: var(--hand);
  font-size: 1.15rem;
  margin: 1.5rem 0;
  padding-left: 1rem;
  border-left: 3px solid var(--accent);
}

blockquote cite {
  display: block;
  font-size: .85rem;
  color: var(--soft);
  font-style: normal;
}

.quip {
  font-family: var(--hand);
  color: var(--accent-2);
}

.photos {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 1rem;
  margin-top: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
}

.photos img {
  border: 2px solid var(--ink);
  border-radius: var(--sketch);
  width: 100%;
  height: auto;
  transition: transform .4s var(--ease), box-shadow .3s;
}

.photos li:hover img {
  transform: rotate(.8deg) translateY(-3px);
  box-shadow: var(--lift);
}

.photos span {
  font-family: var(--hand);
  font-size: .9rem;
  display: block;
  margin-top: .3rem;
}

.inline-cta {
  font-family: var(--hand);
  font-size: 1.05rem;
  display: inline-block;
  transition: transform .2s var(--ease);
}

.inline-cta:hover {
  transform: translateX(5px);
}

.email {
  font-size: clamp(1.2rem, 3.4vw, 1.9rem);
  font-family: var(--hand);
  display: flex;
  gap: .7rem .9rem;
  /* centre, not baseline: a .8rem pill hung off the baseline of a 2rem link read as an
     afterthought stuck to the end of the address */
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}

.email a {
  text-decoration-thickness: 2px;
  text-underline-offset: .18em;
  text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
  transition: text-decoration-color .2s;
}

.email a:hover {
  text-decoration-color: var(--accent);
}


.copy {
  font-size: .8rem;
  font-family: var(--body);
  border: 1.5px solid var(--rule);
  background: var(--paper-2);
  border-radius: 999px;
  padding: .2rem .7rem;
  transition: background .2s, color .2s;
}

.copy:hover {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
}

/* One box, one axis. It used to run three alignments at once: the h2 sat left because
   `main h2` is width: fit-content with no auto margin, .email sat left because a flex
   container ignores text-align, and only the note and the socials actually centred.
   A flex column with one gap replaces four sets of ad-hoc margins. */
.contact-box {
  background: var(--paper-2);
  border: 2.5px solid var(--rule);
  border-radius: 22px;
  padding: 3.2rem 2rem 3rem;
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.4rem;
  text-align: center;
  box-shadow: var(--lift);
}

.contact-box > * {
  margin: 0;
  max-width: 100%;
}

.contact-box h2 {
  margin-inline: auto;
}

/* the underline wipes out from the middle here, not in from the left — a centred heading
   with a left-anchored rule under it looks like a mistake */
.contact-box h2::after {
  transform-origin: 50% 50%;
}

.contact-box .sub-note {
  max-width: 42ch;
}

.socials {
  list-style: none;
  padding: 0;
  display: flex;
  gap: 1rem;
  font-family: var(--hand);
  justify-content: center;
  flex-wrap: wrap;
  margin-top: .4rem;
}

.socials a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 1.2rem;
  border: 1.8px solid var(--rule);
  border-radius: var(--sketch);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  transition: transform 0.2s var(--ease), border-color 0.2s, box-shadow 0.2s;
}

.socials a:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: 3px 4px 0 var(--rule);
}

footer {
  width: 100%;
  box-sizing: border-box;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: center;
  padding: 2.2rem var(--pad);
  background: var(--paper-3);
  border-top: 3px solid var(--rule);
  margin-top: 4rem;
  color: var(--soft);
  font-size: .9rem;
}

.foot-note {
  display: inline-block;
  color: var(--accent);
  animation: sway 5s ease-in-out infinite;
}

@keyframes sway {
  50% {
    transform: rotate(14deg) translateY(-2px);
  }
}

.totop {
  text-decoration: none;
}

.totop:hover {
  text-decoration: underline wavy;
}

/* Dead centre of the footer. An auto margin cannot do it alone: with space-between and two
   flanking items of unequal width, "between" is not "centred". Giving the copyright and the
   back-to-top link an equal share each pushes the middle to the real centre. */
footer > :first-child,
footer > :last-child {
  flex: 1 1 0;
}

footer > :last-child {
  text-align: right;
}

.prize-btn {
  flex: 0 0 auto;
  font-family: var(--hand);
  font-size: 0.95rem;
  padding: 0.55rem 1.3rem;
  border: 2px solid var(--ink);
  border-radius: var(--sketch);
  background: var(--accent);
  color: var(--paper);
  cursor: pointer;
  transition: transform 0.2s var(--ease), box-shadow 0.2s;
}

.prize-btn:hover {
  transform: translateY(-2px) rotate(-1deg);
  box-shadow: 4px 5px 0 var(--ink);
}

.hamburger-rain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 99999;
  overflow: hidden;
}

.falling-burger {
  position: absolute;
  top: -60px;
  user-select: none;
  animation: fallBurger linear forwards;
}

@keyframes fallBurger {
  0% {
    transform: translateY(0) rotate(0deg);
    opacity: 1;
  }
  85% {
    opacity: 1;
  }
  100% {
    transform: translateY(105vh) rotate(360deg);
    opacity: 0;
  }
}

/* ---------- detail dialog ---------- */
.detail {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(1160px, 95vw);
  max-width: 1160px;
  max-height: 90vh;
  /* the 3rem bottom was room held for a floating prev/next bar; it takes its own room now */
  padding: 1.8rem;
  border: 2.5px solid var(--ink);
  border-radius: 22px;
  background: var(--paper);
  color: var(--ink);
  /* a scroll route at every width. Capped at 90vh, anything taller than the cap used to be
     simply cut — in phone landscape that was the work's audio player, below the fold of a
     box with no way to reach it. */
  overflow: hidden auto;
  overscroll-behavior: contain;
  box-shadow: 0 24px 60px rgba(20, 15, 12, .45);
  opacity: 1;
  transform: none;
  transition: opacity .3s var(--ease), transform .3s var(--ease), overlay .3s allow-discrete, display .3s allow-discrete;
}

#detailBody {
  display: grid;
  /* the text column has a floor: split by ratio alone it was squeezed to a 19-character
     gutter the moment two columns appeared, so the still gives way instead of the prose */
  grid-template-columns: 1.75fr minmax(26rem, 1fr);
  gap: 2.2rem;
  align-items: center;
  overflow: hidden;
  padding-bottom: 1.5rem;
  /* the prev/next bar is the last thing in the flow (see .detail-nav): where the dialog has
     a definite height, this fills what is left so the bar lands on the bottom edge */
  min-height: calc(100% - 4rem);
}

.dlg-col-left {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
}

.dlg-media {
  width: 100%;
  aspect-ratio: 16/9;
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}

/* One column until both of them can hold something readable. At 681 the two-column form
   still fitted, but only by taking the blurb down to 19 characters — a wider device read
   worse than a phone. Below the floor the still stacks and the prose gets the width. */
@media (max-width: 860px) {
  #detailBody {
    grid-template-columns: 1fr;
    align-items: start;
  }
}

.dlg-audio-preview {
  margin: 0;
  padding: 0.55rem 0.8rem;
  background: var(--paper-2);
  border-radius: 12px;
  border: none;
}

.dlg-audio-preview.small {
  padding: 0.45rem 0.7rem;
  border: none;
}

.dlg-audio-label {
  display: block;
  font-family: var(--hand);
  font-size: 0.78rem;
  /* small copy in the brand accent is exactly what --accent-text exists for: raw --accent
     put this line at 3.59:1 in autumn-cellar */
  color: var(--accent-text);
  margin-bottom: 0.25rem;
}

.dlg-audio-preview audio {
  width: 100%;
  /* the height a native <audio controls> actually needs — at 28px every control it carries
     was squeezed into half the box the UA lays them out in */
  height: 54px;
}

.dlg-spotify-wrap {
  margin-top: 1rem;
}

.dlg-spotify-embed {
  width: 100%;
}

.dlg-spotify-embed iframe {
  width: 100%;
  border-radius: 12px;
  border: none;
}

.dlg-spotify-embed.small iframe {
  height: 152px;
  border: none;
}

.thumb.playing-inline {
  position: relative;
}

.embed-inline {
  position: absolute;
  inset: 0;
  z-index: 5;
  background: #000;
}

.embed-inline iframe,
.embed-inline video {
  width: 100%;
  height: 100%;
  border: 0;
}

.close-inline {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 10;
  background: rgba(0, 0, 0, 0.75);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 50%;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  cursor: pointer;
  font-size: 0.8rem;
}

/* Coarse pointers need real 44px geometry, not a tiny control with wishful padding. The
   compact desktop rhythm stays intact; touch hardware gets deliberate, glove-friendly marks. */
@media (hover: none), (pointer: coarse) {
  .play,
  .feature .play,
  .play.big {
    width: 44px;
    height: 44px;
  }

  .filters button.pill,
  .tag-toggle,
  .clear,
  .more,
  .more-link,
  .copy,
  .now-btn,
  .now-title,
  .detail-nav button,
  .cfg-actions button,
  .cfg-lang,
  .prize-btn,
  footer .totop,
  .title-btn {
    min-height: 44px;
  }

  .filters button.pill,
  .tag-toggle,
  .clear {
    padding-block: .55rem;
  }

  .now-btn {
    min-width: 44px;
  }

  .seek {
    height: 44px;
  }

  .nav nav a,
  .email a,
  .socials a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  .close {
    width: 44px;
    height: 44px;
  }
}

@media (max-width: 600px) {
  .filters {
    display: flex;
    flex-wrap: nowrap;
    flex-direction: row;
    align-items: center;
    gap: 0.4rem;
    border-radius: 999px;
    margin: 0.8rem auto 1.2rem auto;
  }

  .pillstrip {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    width: 100%;
    /* keep the inset that clears the edge fade — a bare 0 here put the first pill back
       under the mask on exactly the screens where the strip is most crowded */
    padding: 0.1rem 18px;
    gap: 0.35rem;
  }

  .pillstrip::-webkit-scrollbar {
    display: none;
  }

  .clear {
    margin-top: 0;
    align-self: center;
  }
}

.detail::backdrop {
  background: rgba(44, 36, 32, .55);
  backdrop-filter: blur(4px);
  transition: opacity .3s, overlay .3s allow-discrete, display .3s allow-discrete;
  opacity: 1;
}

.detail:not([open]),
.detail:not([open])::backdrop {
  opacity: 0;
}

.detail:not([open]) {
  transform: translateY(14px) scale(.97);
}

@starting-style {
  .detail[open] {
    opacity: 0;
    transform: translateY(14px) scale(.97);
  }

  .detail[open]::backdrop {
    opacity: 0;
  }
}

/* cap the artwork so the words are never pushed off the bottom of a short window */
/* 16/9 and no height cap: capping it crops the still to a letterbox strip, and losing
   the framing of a film frame is worse than a short scroll inside the dialog */
.dlg-media {
  position: relative;
  width: 100%;
  aspect-ratio: 16/9;
  background: var(--rule);
  overflow: hidden;
}

.dlg-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* the provider's own player, mounted in place of the still once you press play */
.embed {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  background: #000;
  animation: embedIn .45s var(--ease);
}

@keyframes embedIn {
  from {
    opacity: 0;
    transform: scale(1.03);
  }
}

.dlg-media:has(.embed) {
  background: #000;
}

.dlg-media[data-kind="instagram"]:has(.embed) {
  aspect-ratio: 4/5;
  background: var(--paper);
}

.dlg-media[data-kind="spotify"]:has(.embed) {
  aspect-ratio: auto;
  height: 152px;
  background: none;
}

.dlg-media[data-kind="soundcloud"]:has(.embed) {
  aspect-ratio: auto;
  height: 166px;
  background: none;
}

.play.big {
  width: 44px;
  height: 44px;
  font-size: 1.05rem;
  right: 1rem;
  bottom: 1rem;
}

.dlg-body {
  padding: 1.2rem 1.4rem 1.4rem;
}

.dlg-body h3 {
  font-size: clamp(1.5rem, 4vw, 2.2rem);
  margin: 0 0 .2rem;
}

.dlg-body .blurb {
  font-size: 1.02rem;
  /* and a ceiling to go with the floor above: stacked, the column is the whole dialog */
  max-width: 62ch;
}

.close {
  position: absolute;
  right: 1.2rem;
  top: 1.2rem;
  z-index: 20;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2px solid var(--rule);
  background: var(--paper-2);
  color: var(--ink);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: transform .2s var(--ease), background .2s, color .2s, border-color .2s;
}

.close:hover {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
  transform: rotate(90deg) scale(1.08);
}

/* Sticky, not absolute: absolutely positioned inside a scroll container it was anchored to
   the bottom of the *first* screenful and rode up over the content as you scrolled, parking
   on top of the last block. In the flow it reserves its own height and still floats at the
   bottom of the scrollport all the way down. */
.detail-nav {
  position: sticky;
  bottom: 1.2rem;
  left: 1.4rem;
  right: 1.4rem;
  display: flex;
  justify-content: space-between;
  pointer-events: none;
  padding: 0;
  z-index: 15;
}

.detail-nav button {
  pointer-events: auto;
  background: var(--paper-2);
  border: 2px solid var(--rule);
  border-radius: 999px;
  padding: .4rem 1.1rem;
  font-family: var(--hand);
  color: var(--ink);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
  cursor: pointer;
  transition: transform .2s var(--ease), background .2s, color .2s, border-color .2s, box-shadow .2s;
}

.detail-nav button:hover:not(:disabled) {
  background: var(--accent);
  color: var(--paper);
  border-color: var(--accent);
  transform: translateY(-2px);
}

.detail-nav button:disabled {
  opacity: .35;
  cursor: default;
}

/* shared-element morph: card artwork → dialog artwork */

/* ---------- section gear button & config dialog ---------- */
.sec-title-row {
  display: flex;
  align-items: center;
  gap: .6rem;
}

.sec-config-btn {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 50%;
  width: 1.6rem;
  height: 1.6rem;
  display: inline-grid;
  place-items: center;
  font-size: .85rem;
  /* This is the only door to theme, typeface, corners and language, so it does not get to be
     invisible: 3:1 in all thirteen palettes. Opacity was the wrong lever and --soft the wrong
     ink — a full-strength --soft is still only 2.38:1 on nordic-frost's paper, so no opacity
     could have carried it. --ink at .6 is 3.88:1 in the worst palette (warm-paper) and reads
     quieter than the .73 --soft it replaces in most of the others. */
  color: var(--ink);
  opacity: .6;
  cursor: pointer;
  transition: opacity .25s var(--ease), transform .3s var(--ease), background .2s, border-color .2s, color .2s;
  line-height: 1;
}

.sec-title-row:hover .sec-config-btn,
.sec-config-btn:focus,
.sec-config-btn:hover {
  border-color: var(--ink);
}

/* no hover to reveal it with, so it is simply there — and at a thumb's size */
@media (hover: none),
(max-width: 600px) {
  .sec-config-btn {
    opacity: 1;
    width: 44px;
    height: 44px;
    font-size: 1.1rem;
  }
}

/* the work dialog is wide because a 16/9 still is the point of it. This one is a form, and a
   form read across 1040px is not a form — a select holding a ~30-character label was stretched
   ten times wider than the label needed while its two siblings sat at 336px. */
.detail.sec-config {
  overflow-y: auto !important;
  overscroll-behavior: contain;
  width: min(720px, 95vw);
  max-width: 720px;
  max-height: 85vh;
  scrollbar-width: thin;
}

.detail.sec-config::-webkit-scrollbar {
  width: 8px;
}

.detail.sec-config::-webkit-scrollbar-track {
  background: var(--paper-2);
  border-radius: 4px;
}

.detail.sec-config::-webkit-scrollbar-thumb {
  background: var(--rule);
  border-radius: 4px;
}

.detail.sec-config::-webkit-scrollbar-thumb:hover {
  background: var(--soft);
}

.sec-config-body {
  padding: 1.5rem;
}

.sec-config-body h3 {
  margin: 0 0 1rem;
  font-size: 1.6rem;
  color: var(--ink);
}

.sec-config-body form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* the panel holds two unrelated sets of knobs — one for the whole site, one for the section
   whose ⚙ was pressed. A rule and a label each, so nobody changes the theme thinking they
   are changing a section. */
.cfg-head {
  margin: .6rem 0 -.3rem;
  padding-top: 1rem;
  border-top: 1.5px solid var(--rule);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--soft);
}

.cfg-head:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

/* min-width:0, or the flex item floors at the min-content width of its longest <option>
   ("✏️ Shantell Sans — hand-marker (default)", 346px) and hangs off the side of a 320px
   dialog that cannot scroll sideways. Direct groups live in a column, so a 12rem flex basis
   there means 12rem of vertical blank space; only groups inside a horizontal row get a basis. */
.cfg-group {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  flex: 0 0 auto;
  min-width: 0;
}

.cfg-group label {
  font-weight: 600;
  font-size: .85rem;
  color: var(--soft);
}

.cfg-group input[type="text"],
.cfg-group textarea,
.cfg-group select {
  font: inherit;
  padding: .45rem .75rem;
  border: 1.5px solid var(--rule);
  border-radius: var(--sketch);
  background: var(--paper);
  color: var(--ink);
  transition: border-color .2s;
}

/* :focus-visible, and no outline:none — the tint is a garnish, the ring is the affordance.
   Killing it here left keyboard users with a 1px border swap worth 2.25:1 in autumn-cellar. */
.cfg-group input[type="text"]:focus-visible,
.cfg-group textarea:focus-visible,
.cfg-group select:focus-visible {
  border-color: var(--accent);
}

.cfg-group input[type="range"] {
  accent-color: var(--accent);
  cursor: pointer;
  /* drag targets, and the hardest thing to land on a 16px-tall control */
  height: 44px;
}

/* a grid, not a wrapping flex row: wrapped flex items grow to fill the line they land on, so
   a row of three at a width that only fits two left the third stretched to the full measure —
   a lone field twice the width of its neighbours reads as a mistake. auto-fit keeps every
   field in the row the same width however many columns the dialog has room for. */
.cfg-row {
  display: grid;
  /* min(12rem, 100%), not a bare 12rem: a grid track's minimum is a hard floor, and on a 320px
     phone the 12rem column is wider than the dialog it sits in */
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  gap: 1rem;
}

.cfg-row > .cfg-group {
  flex: 1 1 12rem;
}

/* A failed/malformed catalogue is an operational state, not a blank page. The static About and
   Contact sections remain below this compact retry panel. */
.catalogue-error {
  width: min(42rem, calc(100% - 2 * var(--pad)));
  margin: clamp(2rem, 8vh, 6rem) auto;
  padding: clamp(1.25rem, 4vw, 2.4rem);
  border: 1.5px solid var(--rule);
  border-radius: var(--sketch);
  background: color-mix(in srgb, var(--paper-2) 94%, transparent);
  box-shadow: var(--shadow);
  text-align: center;
}

.catalogue-error .kicker {
  color: var(--ink);
}

.catalogue-error h2 {
  margin: .2rem 0 .5rem;
}

.catalogue-error p {
  max-width: 52ch;
  margin-inline: auto;
}

.catalogue-error-actions {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: .75rem 1rem;
  margin-top: 1rem;
}

.catalogue-error-actions button,
.catalogue-error-actions a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: .55rem 1rem;
  border-radius: var(--sketch);
  font: 600 1rem var(--hand);
}

.catalogue-error-actions button {
  border: 2px solid var(--accent);
  background: var(--accent);
  color: var(--paper);
  cursor: pointer;
}

.hero-poster-only .hero-controls {
  display: none;
}

.catalogue-failed .hero {
  display: none;
}

.catalogue-failed .nav {
  transform: none !important;
  opacity: 1 !important;
}

.catalogue-failed main {
  padding-top: calc(var(--navh, 4rem) + 1rem);
}

.catalogue-failed .catalogue-error {
  margin-top: 0;
}

/* the language switch is a link, not a control, but it sits in a row of controls — so it
   borrows their box and keeps the row from looking like it lost a field */
.cfg-lang {
  font: inherit;
  padding: .45rem .75rem;
  border: 1.5px solid var(--rule);
  border-radius: var(--sketch);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  transition: border-color .2s, color .2s;
}

.cfg-lang:hover,
.cfg-lang:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
}

.cfg-actions {
  display: flex;
  gap: .8rem;
  justify-content: flex-end;
  /* three buttons that cannot shrink past min-content: unwrapped, the surplus goes off the
     *start* edge — flex-end pushes the overflow backwards — into the dialog's overflow-x:
     hidden, where the Download button lost its left third at 320 and could not be scrolled
     back to. Wrapping is the only exit a row this narrow has. */
  flex-wrap: wrap;
  margin-top: .5rem;
}

.btn-download,
.btn-apply,
.btn-done {
  font-family: var(--hand);
  padding: .5rem 1.1rem;
  border-radius: var(--sketch);
  border: 2px solid var(--ink);
  transition: transform .2s var(--ease), background .2s, color .2s, box-shadow .2s;
}

.btn-download {
  border-color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 12%, var(--paper));
  color: var(--accent-2);
  margin-right: auto;
}

.btn-download:hover {
  background: var(--accent-2);
  color: var(--paper);
  transform: translateY(-2px);
}

.btn-apply {
  background: var(--accent);
  color: var(--paper);
}

.btn-apply:hover {
  transform: translateY(-2px);
  box-shadow: 3px 4px 0 var(--ink);
}

.btn-done {
  background: transparent;
  color: var(--ink);
}

.btn-done:hover {
  background: var(--paper-2);
}

::view-transition-old(work-media),
::view-transition-new(work-media) {
  animation-duration: .38s;
}

/* ---------- footer ---------- */
footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* wrap, not nowrap: three items held on one row measured 614px inside a 375px phone,
     and body's overflow-x: hidden then quietly ate "back to top" rather than showing a
     scrollbar. Nothing looked broken — the link was simply gone. */
  flex-wrap: wrap;
  gap: 1rem;
  padding: 2rem max(1.8rem, 4vw);
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  margin: 0;
  font-size: .9rem;
  color: var(--soft);
  border-top: 1.5px solid var(--rule);
}

footer > span:first-child {
  white-space: nowrap;
}

footer .prize-btn {
  font-family: var(--hand);
  font-size: .8rem;
  background: color-mix(in srgb, var(--paper-2) 80%, transparent);
  border: 1px dashed var(--rule);
  padding: .25rem .75rem;
  border-radius: 999px;
  color: var(--soft);
  transition: transform .2s, color .2s, border-color .2s;
  white-space: nowrap;
}

footer .prize-btn:hover {
  color: var(--accent);
  border-color: var(--accent);
  transform: translateY(-1px);
}

footer .totop {
  margin-left: auto;
  color: var(--accent-text);
  text-decoration: none;
  font-family: var(--hand);
  white-space: nowrap;
  transition: color .2s, transform .2s;
}

footer .totop:hover {
  color: var(--ink);
  transform: translateY(-2px);
}

/* ---------- testimonial multi-card strip ---------- */
.bio {
  min-width: 0;
}

.testimonial-carousel {
  margin: 1.8rem 0;
  position: relative;
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
}

.testimonial-header {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-bottom: .6rem;
}

.testimonial-heading {
  font-family: var(--hand);
  font-size: .95rem;
  color: var(--soft);
  display: block;
  /* shrinkable, or at 345px it holds its max-content width and shoves the pause button
     past the right edge */
  min-width: 0;
}

/* WCAG 2.2.2: content that moves for more than five seconds needs a way to stop it that
   doesn't depend on owning a mouse. Hover, focus and drag are all things a touch user
   hasn't got, and reduced-motion is a system setting, not a control. */
.testimonial-pause {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1.2px solid var(--rule);
  border-radius: 8px;
  background: color-mix(in srgb, var(--paper-2) 74%, transparent);
  color: var(--soft);
  font-size: .65rem;
  line-height: 1;
  transition: color .2s, border-color .2s, background .2s, transform .2s var(--ease);
}

.testimonial-pause:hover {
  color: var(--accent);
  border-color: var(--accent);
  transform: translateY(-1px);
}

.testimonial-pause[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}

@media (hover: none), (pointer: coarse) {
  .testimonial-pause {
    width: 44px;
    height: 44px;
  }
}

.testimonial-strip {
  display: flex;
  gap: 1.2rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: .4rem .2rem 1.2rem .2rem;
  touch-action: pan-x;
  /* the strip is scrolled a fraction of a pixel per frame by drift() in app.js —
     smooth scrolling would fight every one of those writes */
  scroll-behavior: auto;
  cursor: grab;
  mask-image: linear-gradient(to right, transparent 0%, #000 16px, #000 calc(100% - 16px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 16px, #000 calc(100% - 16px), transparent 100%);
}

.testimonial-strip.dragging {
  cursor: grabbing;
  user-select: none;
  scroll-behavior: auto;
}

.testimonial-strip::-webkit-scrollbar {
  display: none;
}

.testimonial-card {
  flex: 0 0 min(340px, 82vw);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: .8rem;
  padding: 1.2rem 1.4rem;
  background: color-mix(in srgb, var(--paper-2) 90%, transparent);
  border: 1.5px solid var(--rule);
  border-radius: var(--sketch);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
  transition: transform .22s var(--ease), box-shadow .22s var(--ease);
}

.testimonial-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}

.testimonial-quote {
  font-family: var(--hand);
  font-size: 1.02rem;
  font-style: italic;
  margin: 0;
  color: var(--ink);
  line-height: 1.5;
}

.testimonial-author {
  display: flex;
  align-items: center;
  gap: .6rem;
  font-size: .88rem;
  flex-wrap: wrap;
}

.testimonial-link {
  font-weight: 600;
  color: var(--accent-text);
  text-decoration: none;
  transition: color .2s, text-decoration .2s;
}

.testimonial-link:hover {
  text-decoration: underline;
  color: var(--ink);
}

.testimonial-role {
  color: var(--soft);
  font-size: .82rem;
}

/* ---------- toast ---------- */
.toast {
  position: fixed;
  left: 50%;
  /* third tenant of the bottom-centre corner, after the transport and the jump link —
     same --transport-h reserve, and the home-indicator inset it never had */
  bottom: calc(max(1.5rem, env(safe-area-inset-bottom)) + var(--transport-h, 0px));
  transform: translate(-50%, 1.5rem);
  z-index: 40;
  background: var(--ink);
  color: var(--paper);
  padding: .5rem 1rem;
  border-radius: 999px;
  font-size: .9rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s, transform .25s var(--ease);
  max-width: min(90vw, 34rem);
  text-align: center;
}

.toast.show {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ---------- responsive ---------- */
@media (min-width: 1600px) {
  :root {
    --wide: 1320px;
  }

  body {
    font-size: 17px;
  }
}

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

  .feature .thumb {
    aspect-ratio: 16/9;
    min-height: 0;
    /* stacked now, so it is a top band again and owns the top pair of corners */
    border-top-right-radius: inherit;
    border-bottom-left-radius: 0;
  }

  /* .about-grid is a flex row: grid-template-columns was inert on it, so the portrait kept
     its 180px column and the bio was left a 35px ribbon. Stretch, not the base rule's
     `start`, or a column flex sizes the bio to its widest child instead of the section. */
  .about-grid {
    flex-direction: column;
    align-items: stretch;
  }
}

@media (max-width: 767px) {
  /* the wordmark sat 56px off the bottom edge, level with the film buttons and the scroll cue
     — three things competing for the same band. Lifted clear of them, it reads as the title of
     the picture rather than a caption on it. */
  .hero-inner {
    padding-bottom: 7rem;
  }
}

@media (max-width: 599px) {
  /* the film's own buttons are wide enough here to reach past the middle of the screen, so
     the cue takes the corner nothing else wants rather than sharing pixels with #filmPlay */
  .scroll-cue {
    inset-inline: var(--pad) auto;
    margin-inline: 0;
  }

  .nav {
    gap: .5rem;
    /* a backdrop-filter makes an element the containing block for its fixed descendants,
       which pinned the bottom bar below to the *nav's* bottom edge instead of the
       viewport's. The bar matters more here than the frost does, so the bar wins. */
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: color-mix(in srgb, var(--paper) 97%, transparent);
    /* and no tween on the transform either: mid-slide it is a real transform, which is a
       containing block again, and a track started during those 400ms would drag the bar
       back up into the header. Untweened it is only ever exactly none or -105%. */
    transition: opacity .3s;
  }

  .nav nav {
    font-size: .85rem;
    gap: .9rem;
    order: 3;
    width: 100%;
    /* nowrap is the whole point of the overflow below — without it the base rule's
       flex-wrap: wrap won, the links stacked into a second row, and the header stood
       126px tall on a 640px screen instead of scrolling sideways in one */
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: .25rem;
    mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent 100%);
  }

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

  .nav nav a {
    white-space: nowrap;
  }

  /* now playing docks to the thumb, not the top of a 7-link nav */
  .now {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
    border-radius: 0;
    border-width: 1.5px 0 0;
    padding: .5rem max(.75rem, env(safe-area-inset-left)) max(.5rem, env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--paper-2) 96%, transparent);
    backdrop-filter: blur(8px);
    z-index: 13;
  }

  .now-title {
    max-width: none;
    flex: 1;
  }

  .seek {
    flex: none;
    width: 90px;
  }

  body:has(.now:not([hidden])) footer {
    padding-bottom: 4.5rem;
  }

  /* the transport docks to the bottom here, and the floating jump link docks to the same
     corner — both are fixed, so nothing pushes the other out of the way on its own */
  body:has(.now:not([hidden])) {
    --transport-h: 3.4rem;
  }

  /* the wordmark sits 56px off the hero's bottom edge here, and the docked bar takes 54 of
     those: lifted clear of the bar the cue lands on the name itself. There is no band left
     for it, so it steps aside — the same call the landscape block makes when the hero runs
     out of height. Everything else that docks down here is a control; this is a hint. */
  body:has(.now:not([hidden])) .scroll-cue {
    display: none;
  }

  /* full-screen overlay beats a small modal on a small screen — including the settings form,
     whose desktop measure cap must not turn into a 95vw box floating on a phone */
  .detail,
  .detail.sec-config {
    width: 100%;
    max-width: 100%;
    height: 100%;
    max-height: 100%;
    margin: 0;
    border-radius: 0;
    border: 0;
  }

  .detail:not([open]) {
    transform: translateY(24px);
  }

  @starting-style {
    .detail[open] {
      transform: translateY(24px);
    }
  }

  .grid {
    gap: 1.1rem;
  }

  /* the strip is sticky here too, so keep it thin. No left padding — the strip inside carries
     an 18px mask inset of its own and the first pill is meant to sit as far left as it can —
     but the right side still needs the gap the desktop rule gives it, or "clear ✕" ends up
     against the border. */
  .filters {
    padding: .45rem .8rem .45rem 0;
    gap: .35rem .5rem;
  }

  .filters :is(.pill, .clear, .tag-toggle) {
    font-size: .75rem;
    padding: .1rem .55rem;
  }

  section {
    margin: 2.5rem 0;
  }

  hr.staff {
    margin: 2rem 0;
  }

  /* the prize button's label is a whole sentence; at 345px it was still 16px wider than
     the viewport even with the footer wrapping */
  .prize-btn {
    max-width: 100%;
    box-sizing: border-box;
    font-size: .85rem;
    padding: .5rem .9rem;
  }

  /* …and max-width only caps the *button*: the nowrap label ran 50px out of it and off the
     screen, taking the last word of the joke with it. Needs `footer .prize-btn` to outrank
     the nowrap of the same weight above. */
  footer .prize-btn {
    white-space: normal;
  }
}

/* Portrait phone: the player is what an expanded work is *for*, so it takes every pixel of
   width the screen has — out past the dialog's own padding, edge to edge. 16/9 at the full
   viewport is the ceiling in portrait; a taller box only adds letterbox.

   The grid packs to the top with it. `min-height` on #detailBody (what puts the prev/next bar
   on the bottom edge) makes its auto rows stretch to fill that height, which parked ~40px of
   dead paper between the player and the title — `align-content: start` leaves the bar where it
   is and hands that space back to the words.

   Written down here, below the base .dlg-media and .dlg-body: both of those come after the
   dialog block above and would otherwise win on document order. */
@media (max-width: 600px) and (orientation: portrait) {
  /* 100%, not 100vw: the dialog is fixed to the initial containing block, and where a
     classic scrollbar exists 100vw is wider than that — the box then hangs off both edges
     and the prose is cut on the right. */
  .detail {
    width: 100%;
    max-width: 100%;
    padding: 1rem .9rem;
  }

  #detailBody {
    gap: 1rem;
    align-content: start;
  }

  .dlg-media {
    margin: -1rem -.9rem 0;
    width: calc(100% + 1.8rem);
  }

  .dlg-body {
    padding: .2rem .3rem 1rem;
  }
}

/* phone in landscape: the hero has ~350px of height to work with */
@media (orientation: landscape) and (max-height: 560px) {
  /* The page itself must not advertise a second scrollbar behind a modal. Its position is
     preserved by openDetail()/close; only the foreground text column scrolls at this height. */
  html:has(#detail[open]) {
    overflow-y: hidden;
  }

  .hero {
    min-height: 0;
  }

  /* A landscape phone is wider than 600px, so none of the narrow-screen rules above fire —
     yet it is the size with the least height to spare. Left alone the header wrapped to
     134px of a 360px viewport, better than a third of the screen, before any work showed. */
  .nav {
    gap: .4rem .7rem;
    padding: .25rem var(--pad);
    min-height: 0;
  }

  .nav nav {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    font-size: .85rem;
    gap: .8rem;
  }

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

  .mark {
    font-size: 1.05rem;
  }

  .hero-inner {
    padding-bottom: 1rem;
  }

  h1 {
    font-size: clamp(2rem, 8vh, 3.4rem);
    margin: .1rem 0 .3rem;
  }

  .kicker {
    font-size: .8rem;
  }

  .hero-line {
    font-size: .95rem;
    margin-bottom: .7rem;
    max-width: 40ch;
  }

  .hero-controls {
    bottom: .8rem;
  }

  .scroll-cue {
    display: none;
  }

  /* dialog goes side-by-side so the artwork doesn't eat the whole screen.
     The columns live on #detailBody, not on .detail: .detail transitions `display`
     for its open/close animation, and an in-flight transition owns that property. */
  .detail[open]:not(.sec-config) {
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;
    height: 90vh;
    overflow: hidden;
  }

  #detailBody {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
    min-height: 0;
    overflow: hidden;
    padding-bottom: .5rem;
  }

  /* the still keeps its 16/9 here too: sized by `height: 100%` in a stretched grid it was
     sized by the *text* column instead, and a 443px-tall portrait box in a 360px viewport
     is the artwork eating the whole screen, which is the thing this block exists to stop.
     Top of the column rather than centred in it: the column is as tall as the text beside
     it, and centring pushed the still below the fold of a viewport this short. */
  .dlg-col-left {
    justify-content: flex-start;
  }

  .dlg-body {
    min-height: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding: 1rem 1.2rem;
  }

  /* A sticky nav occupies the scrollport without reserving it and overlapped provider players.
     In short landscape it is a real second grid row: always reachable, never over content. */
  .detail-nav {
    position: static;
    inset: auto;
    padding-top: .55rem;
    border-top: 1px solid var(--rule);
    background: var(--paper);
  }
}

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

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }

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

  hr.staff.reveal {
    clip-path: none !important;
  }

  /* these two are drawn *by* a transition, so without one they need their end state */
  main h2::after {
    transform: none !important;
  }

  /* the drift is --par on .sec-bg-in; the transform on .sec-bg is the centring for
     `left: 50%; width: 100dvw`, and blanket-killing it slid every backdrop half a viewport
     right. Kill the motion, keep the layout. */
  .sec-bg {
    transform: translateX(-50%) !important;
  }

  .sec-bg-in {
    transform: none !important;
  }
}

/* nothing that reveals on scroll may be missing from a printed page */
@media print {
  .reveal {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }

  /* nothing you can press belongs on paper: a sheet carrying a prize button, a ⚙, a jump
     pill and "back to top ↑" reads as a screenshot rather than a document. The glyph goes
     with them — 369px of decoration at 9% ink is a printer cartridge, not a page. */
  .nav,
  .filters,
  .now,
  .readbar,
  .scroll-cue,
  .play,
  .more-link,
  .toast,
  .splash,
  .hero-controls,
  .sec-bg,
  .prize-btn,
  .sec-config-btn,
  .jump,
  .totop,
  .testimonial-pause,
  .sec-symbol-svg {
    display: none !important;
  }

  /* same shape as the grid's row cap above: a horizontal scroller prints its first screen
     and drops the rest — 2 of 5 quotes. Let them wrap down the page, and the clones (which
     are decoration for the drift) go, since on paper nothing drifts. */
  .testimonial-strip {
    flex-wrap: wrap;
    overflow: visible;
    mask-image: none;
    -webkit-mask-image: none;
  }

  .testimonial-card[aria-hidden="true"] {
    display: none !important;
  }

  main h2::after {
    display: none;
  }

  .hero {
    height: auto;
    min-height: 0;
    color: var(--ink);
  }

  .hero-media,
  .hero-scrim,
  .grain {
    display: none;
  }

  .hero::before {
    display: none;
  }

  .hero-inner {
    color: var(--ink);
    padding-bottom: 1rem;
  }

  /* the wordmark's own colour outranks .hero's, and print-color-adjust: economy drops the
     dark band behind it — paper on paper, 1.03:1. On paper it gets ink. */
  .hero h1 {
    color: var(--ink);
    text-shadow: none;
  }

  /* the grid's two-row cap is a JS `hidden`, and "show more" is display:none here — printed,
     that is half the catalogue silently missing. Paper has no folds: print all of it. */
  .grid .card[hidden] {
    display: block !important;
  }

  .card {
    break-inside: avoid;
  }

  body {
    background: none;
  }
}

@media (prefers-contrast: more) {
  :root {
    --soft: #4a3f36;
    --rule: #8d7c66;
  }
}
/* ---------- sharp corners ----------
   Square edges throughout. --sketch is 0 everywhere now (baked into every theme block above),
   so the cards and the surfaces sized off it are square automatically. This block catches the
   handful that size their own radius instead of reading --sketch. The small pill controls are
   pointedly excluded — a tag pill, the theme selects, the copy button and the jump link stay
   round, because roundness is what marks them as controls. .filters is exempt for the same
   reason: it holds the pill controls, and a squared-off tray around round pills would mismatch. */
:is(.contact-box, .detail, .dlg-media, .card-embed iframe, .dlg-media .embed, .embed-inline iframe, .embed-inline video, .dlg-spotify-embed iframe),
.detail-nav button {
  border-radius: 0;
}
