/* =========================================================
   Tony D — "Ink & Late Night"
   Palette sampled from the Overthinking / HER artwork.
   ========================================================= */

/* =========================================================
   Self-hosted webfonts.

   These were served from fonts.googleapis.com, which is blocked in
   mainland China — a large part of Tony's audience. Loading them from
   our own origin keeps the typography intact there (and removes a
   third-party request everywhere else).

   Latin subsets only: neither family has CJK glyphs, so Chinese text
   falls through to --f-cjk below.
   ========================================================= */

@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/instrument-serif-400-italic-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/instrument-serif-400-italic-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/instrument-serif-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/instrument-serif-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/inter-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(../fonts/inter-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/inter-600-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(../fonts/inter-600-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/inter-700-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/inter-700-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Registered so the browser knows these are numbers, not arbitrary strings.
   Pulse rewrites them every frame while audio is playing, and a typed
   property lets the engine narrow what that invalidates. Browsers without
   @property ignore this and fall back to the var() defaults. */
@property --beat{  syntax:'<number>'; inherits:true; initial-value:0; }
@property --level{ syntax:'<number>'; inherits:true; initial-value:0; }

:root{
  --ink:        #0B0A0C;   /* base, from album cover #1D0C16 */
  --ink-2:      #131114;   /* raised surface */
  --bone:       #EDE6DC;   /* warm off-white text */
  --bone-dim:   #9A938C;
  --magenta:    #FA279F;   /* signature accent (HER artwork) */
  --orange:     #B25A16;   /* warm mid (editorial portrait) */
  --acid:       #05F93B;   /* rare 2nd accent — once per page, max */
  --line:       rgba(237,230,220,.14);

  /* Inter and Instrument Serif carry no CJK glyphs, so Chinese characters
     fall past them to the first family below that has the glyph. Keeping
     the CJK faces in the same stack means one token works on both pages
     and mixed-script lines (想太多 / Overthinking) stay consistent.
     These are system faces — a real CJK webfont is several MB unsubset. */
  --f-sans: 'Inter', system-ui, -apple-system, 'Segoe UI',
            'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
            'Noto Sans CJK SC', sans-serif;
  --f-serif:'Instrument Serif', Georgia,
            'Songti SC', 'Noto Serif CJK SC', 'SimSun', serif;

  --pad: clamp(1.25rem, 4vw, 4.5rem);
  --ease: cubic-bezier(.22,.61,.36,1);

  /* Measured at runtime by HeightVar in main.js; this is the value the
     page uses before that runs, and if it never does. Anything that has to
     clear the sticky nav is expressed against it rather than a hand-tuned
     guess per rule. */
  --nav-h: 4.2rem;

  /* The hero takes the whole first screen below the nav. */
  --hero-h: calc(100svh - var(--nav-h));
}

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

html{ scroll-behavior:smooth; }

body{
  background:var(--ink);
  color:var(--bone);
  font-family:var(--f-sans);
  font-size:16px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

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

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

/* keyboard users shouldn't have to tab the whole nav to reach content */
.skip{
  position:fixed; top:.6rem; left:.6rem; z-index:200;
  transform:translateY(-200%);
  background:var(--bone); color:var(--ink);
  padding:.6rem 1rem; border-radius:8px;
  font-size:.8rem; font-weight:600; text-decoration:none;
}
.skip:focus-visible{ transform:none; }

:where(a,button,summary,[tabindex]):focus-visible{
  outline:2px solid var(--magenta);
  outline-offset:3px;
  border-radius:4px;
}

/* ---------- film grain: the cheapest "designed" cue there is ---------- */
/* A soft light following the pointer across the whole page - the same idea
   as the one on the nav, but far weaker, so the bar stays the brighter of
   the two. Stays fully transparent until JS turns it on, and never turns on
   for reduced motion or on touch. */
.glow{
  position:fixed; inset:0; z-index:9998; pointer-events:none;
  opacity:0; transition:opacity .6s var(--ease);
  /* --beat is published by Pulse while something is playing, and is 0 the
     rest of the time, so this is a still light until the music starts. */
  background:radial-gradient(
             calc(26rem + var(--beat,0) * 9rem) circle
             at var(--px,50%) var(--py,50%),
             rgb(237 230 220 / calc(.055 + var(--beat,0) * .055)),
             rgb(237 230 220 / 0) 70%);
}
.glow.is-on{ opacity:1; }

/* the page warms up while it is playing */
.is-playing .nav::before{ background:rgba(11,10,12,.55); }
.is-playing .nav::after{ opacity:calc(.35 + var(--beat,0) * .65); }

.grain{
  position:fixed; inset:-50%;
  z-index:9999; pointer-events:none;
  opacity:.055;
  background-image:url("grain.svg");
  background-repeat:repeat;
  animation:grain-shift 700ms steps(3) infinite;
}
@keyframes grain-shift{
  0%  { transform:translate(0,0) }
  33% { transform:translate(-2%,1.5%) }
  66% { transform:translate(1.5%,-1%) }
  100%{ transform:translate(0,0) }
}

/* ================= NAV ================= */
/* Sticky rather than fixed, so it takes real space and the page begins
   below it instead of underneath it, and opaque rather than blurred — the
   old backdrop-filter smeared whatever scrolled past and read as a fault
   rather than as an effect. */
.nav{
  position:sticky; top:0; z-index:80;
  display:flex; align-items:center; justify-content:space-between; gap:1rem;
  padding:1.15rem var(--pad);
  background:transparent;
  border-bottom:1px solid var(--line);
}
/* The tint and blur ride on a pseudo-element rather than .nav itself:
   backdrop-filter would make .nav the containing block for the mobile menu,
   which is position:fixed, and strand the panel inside the bar. */
.nav::before{
  content:""; position:absolute; inset:0; z-index:-1;
  background:rgba(11,10,12,.62);
  -webkit-backdrop-filter:blur(16px) saturate(1.25);
          backdrop-filter:blur(16px) saturate(1.25);
}
/* a faint light that follows the cursor along the bar */
.nav::after{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  background:radial-gradient(16rem circle at var(--mx,50%) var(--my,50%),
             rgba(237,230,220,.11), rgba(237,230,220,0) 65%);
  opacity:0; transition:opacity .4s var(--ease);
}
.nav:hover::after,
.nav.is-lit::after{ opacity:1; }   /* .is-lit is how a touch lights it */

/* the two link groups become grid items of .nav so the wordmark can sit
   between them, dead centre, regardless of how wide either group is */
.nav__links{ display:contents; }

/* Each of the six slots takes an equal share of the bar and stands its full
   height, so the target is a sixth of the bar rather than the few small
   words printed in it. The negative margin lets a slot cover the bar's own
   padding without making the bar any taller. */
.nav__mark,
.nav__item,
.nav__pair{
  flex:1 1 0;
  display:flex; align-items:center;
  margin-block:-1.15rem; padding-block:1.15rem;
}
.nav__item{ justify-content:center; }
.nav__pair{ justify-content:flex-end; }
/* Six slots spread evenly across the bar. The wordmark has to live outside
   .nav__links so it stays put when that becomes the mobile panel, so `order`
   rather than DOM position is what lands it in the middle. */
.nav__links > *{ order:0; }
.nav__mark        { order:1; }
.nav__item--music { order:2; }
.nav__item--visuals{ order:3; }
.nav__item--about { order:4; }
.nav__item--inthemaking{ order:5; }
.nav__pair        { order:6; }

/* contact and the language switch read as one item, not two */
.nav__pair{ display:flex; gap:.4rem; align-items:center; }

.nav__mark{
  font-weight:700; letter-spacing:.3em; font-size:1.24rem;
  color:var(--bone); text-decoration:none; white-space:nowrap;
}
.nav__links a,
.nav__pair a{
  color:var(--bone-dim); text-decoration:none;
  font-size:.88rem; letter-spacing:.09em; text-transform:uppercase;
  transition:color .25s var(--ease), text-shadow .25s var(--ease);
  /* "In the Making" is two words longer than anything else in the bar.
     Wrapping it would make the bar taller and take --nav-h with it, so the
     slot is allowed to claim more than its sixth instead. */
  white-space:nowrap;
}
/* everything in the bar answers in the accent, like the section headings */
.nav__links a:hover,
.nav__links a:focus-visible,
.nav__mark:hover,
.nav__mark:focus-visible{ color:var(--magenta); }
.nav__cta{
  color:var(--ink) !important; background:var(--bone);
  padding:.5rem .95rem; border-radius:100px; font-weight:600;
}
.nav__cta:hover{ background:var(--magenta); color:#fff !important; }

/* ---- language switch ---- */
.nav__lang{
  border:1px solid var(--line); border-radius:100px;
  padding:.42rem .85rem;
  letter-spacing:.06em !important; text-transform:none !important;
}
.nav__lang:hover,
.nav__lang:focus-visible{ color:var(--magenta); border-color:var(--magenta); }

/* scroll-spy: the section you're actually in */
.nav__links a.is-active:not(.nav__cta){ color:var(--bone); }
.nav__links a.is-active:not(.nav__cta)::after{
  content:""; display:block; height:1px; margin-top:4px;
  background:var(--magenta);
  transition:box-shadow .25s var(--ease);
}

/* The page you are already on cannot answer a hover by turning the accent
   colour - it is the one item in the bar that is not dim to begin with, and
   it already carries the accent as a rule beneath it. So it lights instead:
   the word goes white behind a magenta halo and the rule under it picks up
   a glow. Same gesture as everything else in the bar, different mechanism,
   because "you are here" should not read the same as "you could go here". */
.nav__links a.is-active:not(.nav__cta):hover,
.nav__links a.is-active:not(.nav__cta):focus-visible{
  color:#fff;
  /* Four stacked shadows rather than one: a tight core that keeps the
     letterforms crisp, then progressively wider and fainter layers to
     carry the light well past the word. A single large blur only smears -
     one shadow cannot be sharp at the edge and soft at distance, so the
     reach has to be built out of separate layers. */
  text-shadow:0 0 4px rgba(250,39,159,1),
              0 0 12px rgba(250,39,159,1),
              0 0 30px rgba(250,39,159,1),
              0 0 66px rgba(250,39,159,.85),
              0 0 130px rgba(250,39,159,.6),
              0 0 220px rgba(250,39,159,.35);
}
.nav__links a.is-active:not(.nav__cta):hover::after,
.nav__links a.is-active:not(.nav__cta):focus-visible::after{
  box-shadow:0 0 12px 2px rgba(250,39,159,1),
             0 0 34px 5px rgba(250,39,159,.9),
             0 0 78px 10px rgba(250,39,159,.6),
             0 0 150px 16px rgba(250,39,159,.32);
}

/* ---- burger: only rendered under 900px (see responsive block) ---- */
.nav__burger{
  display:none;
  width:38px; height:32px; padding:7px 6px;
  background:none; border:0; cursor:pointer;
}
.nav__burger span{
  display:block; height:1.5px; background:var(--bone); border-radius:2px;
  transition:transform .3s var(--ease), opacity .2s var(--ease);
}
.nav__burger span + span{ margin-top:5px; }
.nav__burger[aria-expanded="true"] span:nth-child(1){ transform:translateY(6.5px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:nth-child(2){ opacity:0; }
.nav__burger[aria-expanded="true"] span:nth-child(3){ transform:translateY(-6.5px) rotate(-45deg); }

/* ================= HERO ================= */
.hero{
  position:relative;
  min-height:var(--hero-h);
  display:grid;
  grid-template-columns:1.02fr .98fr;
  align-items:end;
  overflow:hidden;
}

/* portrait lives in its own column so the photo keeps its composition */
.hero__media{
  grid-column:2; grid-row:1;
  position:relative; z-index:0;
  height:var(--hero-h); align-self:start;
  /* feather the left edge into the page — avoids a hard column seam */
  -webkit-mask-image:linear-gradient(to right, transparent 0%, #000 26%);
          mask-image:linear-gradient(to right, transparent 0%, #000 26%);
}
.hero__media img{
  width:100%; height:100%; object-fit:cover;
  object-position:50% 18%;
  filter:grayscale(.22) contrast(1.05) brightness(.9);
  transform:scale(1.02);
  animation:hero-drift 26s ease-in-out infinite alternate;
}
@keyframes hero-drift{ to{ transform:scale(1.07) translateY(-1%); } }

/* ---- the photograph is lit while the music plays ----

   A wash of the accent over the portrait: solid at the right edge, thinning
   away to nothing as it travels left, so the grey studio backdrop reads as
   pink and the dark left side is untouched.

   --wash is the distance that thinning covers, and it is what animates. A
   custom property cannot be transitioned unless it is registered, which is
   what the @property block is for: without it this would snap between the
   two states instead of sweeping. On play it grows leftward; on pause it
   shrinks back toward the right edge and the colour retreats the way it
   arrived. The two durations are the audio's own fades - 1.4s up in
   AudioEngine.start, 1.1s down in stop - so the light goes out exactly as
   the sound does.

   blend mode: `color` takes the hue and keeps the photograph's own
   lighting, which is what actually turns grey into pink. It tints the shirt
   and skin too, like a gel on a studio light. `screen` or `soft-light`
   there instead give a gentler wash that leaves skin tones alone. */
@property --wash{
  syntax:"<percentage>";
  inherits:false;
  initial-value:0%;
}
.hero__media::after{
  content:""; position:absolute; inset:0; z-index:1;
  pointer-events:none;
  mix-blend-mode:color;
  --wash:0%;
  /* Solid for most of its travel and only feathering at the very end, so
     the backdrop is fully covered rather than fading to grey halfway across
     it. The wash is allowed to run past the left edge of the photograph -
     there is nothing to protect out there, because the mask has already
     taken the black panel out along with him. */
  background:linear-gradient(to left,
             rgba(250,39,159,1)   0%,
             rgba(250,39,159,.96) calc(var(--wash) * .45),
             rgba(232,45,190,.78) calc(var(--wash) * .78),
             rgba(210,50,210,0)   var(--wash));

  /* Held to the studio backdrop, so the light falls behind Tony and not on
     him. The mask is a silhouette cut from the photograph itself: the
     backdrop is neutral and mid-bright, while his skin, the guitar and the
     denim are all far more saturated and the white shirt is far brighter,
     so the two separate cleanly on those two measures. Only backdrop that
     reaches the edge of the frame counts, which is what keeps gaps in the
     strings and folds in the shirt from lighting up.

     It is an alpha channel rather than a greyscale image because mask-image
     reads alpha by default - a plain greyscale file would mask nothing.

     The sizing and the drift below mirror .hero__media img exactly. The
     photograph is slowly scaling the whole time, and a mask that did not
     move with it would slide off the shoulders within seconds. */
  -webkit-mask-image:url(../img/hero-backdrop-mask.webp);
          mask-image:url(../img/hero-backdrop-mask.webp);
  -webkit-mask-size:cover;   mask-size:cover;
  -webkit-mask-position:50% 18%; mask-position:50% 18%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  transform:scale(1.02);
  animation:hero-drift 26s ease-in-out infinite alternate;

  transition:--wash 1.1s var(--ease);
}

/* The colour layer can only replace hue - it cannot darken, because that is
   what `color` blending means. So the fade into the black panel is a second
   layer of its own: the same silhouette mask, but painting the page's own
   ink over the backdrop, heaviest at the left. Without it the wash met the
   panel as a hard vertical edge, since the mask stops dead where the panel
   begins. Masked like the wash, so it dims the backdrop and not him. */
.hero__media::before{
  content:""; position:absolute; inset:0; z-index:2;
  pointer-events:none; opacity:0;
  background:linear-gradient(to left,
             rgba(11,10,12,0)   34%,
             rgba(11,10,12,.5)  66%,
             rgba(11,10,12,.92) 88%,
             rgba(11,10,12,1)  100%);
  -webkit-mask-image:url(../img/hero-backdrop-mask.webp);
          mask-image:url(../img/hero-backdrop-mask.webp);
  -webkit-mask-size:cover;   mask-size:cover;
  -webkit-mask-position:50% 18%; mask-position:50% 18%;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  transform:scale(1.02);
  animation:hero-drift 26s ease-in-out infinite alternate;
  transition:opacity 1.1s var(--ease);
}
.is-playing .hero__media::before{
  opacity:1;
  transition:opacity 1.4s var(--ease);
}
.is-playing .hero__media::after{
  --wash:190%;
  /* no transition on opacity, so this tracks the kick frame by frame */
  opacity:calc(.84 + var(--beat,0) * .16);
  transition:--wash 1.4s var(--ease);
}

/* floor fade only — the horizontal blend is handled by the media mask */
.hero__scrim{
  grid-column:2; grid-row:1;
  position:relative; z-index:1; height:var(--hero-h);
  background:linear-gradient(to top, var(--ink) 0%, transparent 32%);
}

.hero__inner{
  grid-column:1; grid-row:1;
  position:relative; z-index:2;
  padding:0 clamp(1rem,2vw,2.5rem) clamp(6.5rem,13vh,9rem) var(--pad);
  max-width:40rem;
}

.hero__eyebrow{
  display:inline-flex; align-items:center; gap:.6rem;
  font-size:.72rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--bone-dim); margin-bottom:1.6rem;
}
.dot{
  width:6px; height:6px; border-radius:50%;
  background:var(--magenta);
  box-shadow:0 0 0 0 rgba(250,39,159,.6);
  animation:pip 2.4s ease-out infinite;
}
@keyframes pip{ 70%,100%{ box-shadow:0 0 0 9px rgba(250,39,159,0); } }

/* ---- ink logo: his own handwriting, wiped on as if being drawn ---- */
.hero__title{ margin:0 0 1.5rem; }
/* resting state is VISIBLE; the keyframes supply the hidden start via
   fill-mode `both`, so a skipped animation can never strand the logo. */
.ink{
  /* the image was trimmed to its first stroke (it used to carry ~17% of
     empty margin on the left), so this is the old 30rem / 74vw scaled by
     444/544 - same size lettering, now flush with the text below. Then
     taken up about a third on top of that, so it is the first thing on the
     page the eye lands on. */
  width:min(33rem, 82vw);
  filter:drop-shadow(0 6px 26px rgba(0,0,0,.55));
  /* 9.5s cycle: draws in over ~1.9s, rests ~6.6s, wipes off, redraws.
     Percentages are scaled to hold the draw itself at ~1.9s — retune them
     alongside the duration if you change it. */
  animation:ink-draw 9.5s var(--ease) .35s infinite both;
  /* Breathes with the low end while something is playing. --beat is 0 the
     rest of the time, so this is a plain scale(1) at rest. The animation
     above drives clip-path only, so there is nothing to fight over here. */
  transform-origin:center;
  transform:scale(calc(1 + var(--beat,0) * .045));
}
/* While something is playing the draw-on cycle stands down and the wordmark
   just stays put, breathing with the beat instead.

   `animation:none` rather than `animation-play-state:paused`, on purpose:
   pausing freezes clip-path wherever the cycle happened to be, and about a
   second of every 9.5 is the wipe-off, so roughly one play in ten would
   strand a half-erased logo for as long as the music ran. Dropping the
   animation falls back to the resting state, which is fully drawn. The cost
   is that pausing restarts the cycle rather than resuming it, so the
   wordmark draws itself on again - which reads as the page waking back up. */
.is-playing .ink{ animation:none; }

/* ---- the song title and his name, taking turns ----
   Both share one box: the title sets its size, the name is laid over it and
   scaled to fit. Each gets a 6s turn of a 12s loop - the same 1.9s stroke as
   above, a shorter hold, the same wipe - and is off for the other one's
   turn; the name starts half the loop late. Nothing in JS - the clock is the animation itself.

   The name's resting state is hidden, the title's is drawn. So anything that
   drops the animation (playing, reduced motion) leaves exactly one wordmark
   up, and it is the song. */
.inks{ position:relative; display:block; width:min(33rem, 82vw); }
.inks .ink{ display:block; width:100%; }
/* Every frame after the first is laid over it, and hidden. This is the
   no-JS resting state as well: one wordmark up, nothing moving. */
.inks .ink--queued{
  position:absolute; inset:0; height:100%;
  object-fit:contain; object-position:left center;
  clip-path:inset(0 0 0 100%);
}

/* ---- the hand-over ----
   This was a pair of keyframe cycles offset by half a loop, which only ever
   knew how to count to two. The number of frames and the length of a turn
   are both content now (see the Handwriting loop card in the admin), so the
   clock moved to InkLoop in main.js and the stylesheet only describes the
   three states it puts a frame in. `inks--loop` is added by that class, not
   by build.py, so none of this applies until the script is actually running.

   Same stroke as the single wordmark above: 1.9s to draw, 0.75s to wipe,
   both left to right, the direction the pen travelled. */
.inks--loop .ink{ animation:none; }
.inks--loop .ink[data-state="wait"]{ clip-path:inset(0 100% 0 0); transition:none; }
.inks--loop .ink[data-state="in"]{
  clip-path:inset(0 -2% 0 0);
  transition:clip-path 1.9s var(--ease);
}
.inks--loop .ink[data-state="out"]{
  clip-path:inset(0 0 0 100%);
  transition:clip-path .75s var(--ease);
}
/* While something is playing the loop stands down and frame one rests, the
   way the single wordmark does. */
.is-playing .inks--loop .ink[data-state="in"]{ transition:none; }
@keyframes ink-draw{
  /* draw on, left to right */
  0%   { clip-path:inset(0 100% 0 0); }
  20%  { clip-path:inset(0 -2% 0 0);  }
  /* hold, fully visible, for the bulk of the cycle */
  89%  { clip-path:inset(0 -2% 0 0);  }
  /* wipe off in the same direction the pen travelled */
  97%  { clip-path:inset(0 0 0 100%); }
  /* both ends are zero-width, so the reset back to the start is invisible */
  100% { clip-path:inset(0 100% 0 0); }
}

.hero__sub{
  font-size:clamp(1rem,1.45vw,1.16rem);
  color:#CFC8C0; max-width:34rem; margin-bottom:1.05rem;
}

/* The tally under the genre line, in the same serif as the line above it.
   The figures run largest to smallest and the sentence lands on the joke.

   Instrument Serif is a single 400 weight, so a <b> in here would be a
   browser-synthesised fake bold and would look it. The figures are lifted
   by colour instead, and the payoff word takes the accent. */
.hero__count{
  font-family:var(--f-serif);
  font-size:clamp(1.2rem,2vw,1.6rem); line-height:1.35;
  max-width:34rem; margin:0 0 1.2rem; color:#CFC8C0;
}
.hero__count b{ font-weight:400; color:var(--bone); }
.hero__count b.key{ font-weight:400; color:var(--magenta); }

.hero__out{
  font-size:.74rem; letter-spacing:.15em; text-transform:uppercase;
  color:var(--bone-dim); margin-bottom:2.2rem;
}
.hero__out strong{ color:var(--bone); font-weight:600; }
.hero__sub em{ font-family:var(--f-serif); font-style:italic; color:var(--bone); }
.hero__sub strong{ color:var(--bone); font-weight:600; }

.hero__actions{ display:flex; gap:.85rem; flex-wrap:wrap; }

.btn{
  display:inline-flex; align-items:center; gap:.65rem;
  padding:.9rem 1.5rem; border-radius:100px;
  font:inherit; font-size:.86rem; font-weight:600; letter-spacing:.04em;
  text-decoration:none; cursor:pointer; border:1px solid transparent;
  transition:transform .25s var(--ease), background .25s var(--ease),
             color .25s var(--ease), border-color .25s var(--ease);
}
.btn:hover{ transform:translateY(-2px); }
.btn--play{
  background:var(--magenta); color:#fff;
  /* The two halves of the record's behaviour are timed from here and only
     here: main.js reads these off the element rather than keeping its own
     copy, so the spin and the morph cannot drift apart. */
  --cd-start:620ms;    /* winding up, and play mark -> note */
  --cd-settle:1150ms;  /* running down, and note -> play mark */
}
.btn--play:hover{ background:#ff45ad; }
.btn--ghost{ background:transparent; color:var(--bone); border-color:var(--line); }
.btn--ghost:hover{ border-color:var(--bone); }

/* a record needs considerably more room than a triangle did to read as one */
.btn__icon{
  width:36px; height:36px; flex:none;
  display:grid; place-items:center; line-height:0;
}
.btn__icon svg{ width:100%; height:100%; grid-area:1/1; display:block; }

/* Both words sit in a one-line window and the pair slides, so the change
   wipes upward instead of blinking. The window is as wide as the longer
   word, so the button never resizes either. The whole label is aria-hidden
   and the button carries an aria-label instead - otherwise a screen reader
   would read straight through both words as "Play Pause". */
.btn__label{ display:block; overflow:hidden; height:1.2em; }
.btn__roll{
  display:block;
  transition:transform .38s var(--ease);
}
.btn__word{ display:block; height:1.2em; line-height:1.2em; white-space:nowrap; }
.btn--play[aria-pressed="true"] .btn__roll{ transform:translateY(-1.2em); }

/* ---- the record on the play button ----

   The disc's rotation is not a CSS animation. It is a Web Animations one
   built in main.js, because animation-play-state can only freeze a spin
   dead, and a record is supposed to coast to a stop - so the playback rate
   is eased down instead, over about a second and a half.

   Everything else here is CSS: the play mark and the note cross-fade and
   scale through each other, so neither shape ever snaps into place. Both
   sit inside the rotating group, which is why the note turns with the
   label it is printed on. */
.cd{ overflow:visible; }
.cd__disc{ transform-box:fill-box; transform-origin:center; }
.cd__glyph{ color:var(--magenta); }
/* The morph is timed to the spin, not to itself. A transition takes its
   duration from the state being moved *to*, so the base rule times the
   note turning back into the play mark and the pressed rule times the
   other direction - which means the triangle finishes arriving on the same
   frame the record stops turning, instead of appearing a second early and
   then riding round with it. */
.cd__play,
.cd__note{
  transform-box:fill-box; transform-origin:center;
  transition:opacity var(--cd-settle) var(--ease),
             transform var(--cd-settle) var(--ease);
}
.btn--play[aria-pressed="true"] .cd__play,
.btn--play[aria-pressed="true"] .cd__note{
  transition-duration:var(--cd-start);
}
.cd__note{ opacity:0; transform:scale(.55) rotate(-25deg); }
.btn--play[aria-pressed="true"] .cd__play{ opacity:0; transform:scale(.55) rotate(25deg); }
.btn--play[aria-pressed="true"] .cd__note{ opacity:1; transform:none; }

/* ---- singles, a year at a time ----

   Every panel is in the same grid cell, so the card is always as tall as
   the busiest year and never jumps as you step between them. That is also
   why panels hide with visibility and not [hidden]: display:none would
   collapse the cell and the height would start moving again. */
.years{ margin-bottom:1.15rem; }
.years__head{
  display:flex; align-items:center; gap:.5rem;
  margin-bottom:.7rem;
}
.years__label{
  font-family:var(--f-serif); font-size:1.2rem; color:var(--bone);
  font-variant-numeric:tabular-nums; min-width:3.2ch;
  transition:color .3s var(--ease), text-shadow .3s var(--ease),
             opacity .26s var(--ease);
}
.years__arrow{
  display:grid; place-items:center;
  width:1.7rem; height:1.7rem; padding:0;
  border:1px solid var(--line); border-radius:50%;
  background:transparent; color:var(--bone-dim); cursor:pointer;
  transition:color .25s var(--ease), border-color .25s var(--ease),
             opacity .25s var(--ease), box-shadow .35s var(--ease);
}
/* Hovering the singles card lights the whole control once and lets it
   fade - the arrows, the year and the titles under it. One pass, not a
   held state: it is there to say the year is something you can move
   through, and a permanent glow would say something else. Suppressed
   while a wrap is running so the two never fight over the same pixels. */
/* A glow on the letters alone was all but invisible: the titles sit on a
   pale pill, which washed a soft magenta text-shadow straight out. So the
   pill itself lights - border and halo, the way the arrows already did -
   and only the year, which has no pill behind it, glows as text. */
/* The arrows carry the brightest of the three. They are the controls - the
   part of this the hint exists to point at - while eleven titles all
   lighting hard just made the card look top-heavy. So the pills are turned
   down to a whisper and the arrows up, and the year sits between them. */
@keyframes years-hint-box{
  0%, 100%{ box-shadow:0 0 0 0 rgba(250,39,159,0);
            border-color:transparent;
            background:rgba(237,230,220,.045); }
  38%     { box-shadow:0 0 7px 0 rgba(250,39,159,.18);
            border-color:rgba(250,39,159,.28);
            background:rgba(250,39,159,.05); }
}
@keyframes years-hint-text{
  0%, 100%{ text-shadow:0 0 0 rgba(250,39,159,0); color:var(--bone); }
  38%     { text-shadow:0 0 10px rgba(250,39,159,.5); color:var(--bone); }
}
@keyframes years-hint-ring{
  0%, 100%{ box-shadow:0 0 0 0 rgba(250,39,159,0);
            border-color:var(--line); color:var(--bone-dim); }
  38%     { box-shadow:0 0 17px 1px rgba(250,39,159,.85);
            border-color:var(--magenta); color:#fff; }
}
.card:hover .years:not(.is-flare):not(.is-out) .years__label{
  animation:years-hint-text 1.15s var(--ease) 1;
}
.card:hover .years:not(.is-flare):not(.is-out) .years__panel.is-on li{
  animation:years-hint-box 1.15s var(--ease) 1;
}
.card:hover .years:not(.is-flare):not(.is-out) .years__arrow:not(:disabled){
  animation:years-hint-ring 1.15s var(--ease) 1;
}
/* a short stagger, so it reads as one pass across the row rather than
   everything blinking at once */
.card:hover .years__panel.is-on li:nth-child(2){ animation-delay:.07s; }
.card:hover .years__panel.is-on li:nth-child(3){ animation-delay:.14s; }

/* ---- reaching either end of the run ----

   Both arrows wrap, and the wrap is announced rather than just happening:
   the arrow, the year and every title under it flare up together, hold for
   a beat, dim out, and the far end of the run fades in behind them. The
   earlier version only tinted the arrow's own border, which is why it was
   easy to miss entirely.

   The flare is a text-shadow rather than a filter so it costs nothing to
   composite, and the swap happens while the block is at zero opacity, so
   the years never appear to change mid-fade. */
.years__arrow.is-cycling{
  color:#fff; border-color:var(--magenta);
  box-shadow:0 0 0 3px rgba(250,39,159,.35), 0 0 20px rgba(250,39,159,.6);
}
.years.is-flare .years__label{
  color:#fff;
  text-shadow:0 0 12px rgba(250,39,159,.9), 0 0 30px rgba(250,39,159,.55);
}
.years.is-flare .years__panel.is-on li{
  color:#fff; opacity:1;
  border-color:var(--magenta);
  background:rgba(250,39,159,.12);
  text-shadow:0 0 10px rgba(250,39,159,.75), 0 0 24px rgba(250,39,159,.4);
}
/* then the whole block dims, and the swap happens behind the dark */
.years.is-out .years__label,
.years.is-out .years__stack{ opacity:0; }
.years__arrow svg{ width:13px; height:13px; }
.years__arrow:hover:not(:disabled),
.years__arrow:focus-visible:not(:disabled){
  color:var(--magenta); border-color:var(--magenta);
}
/* the ends of the run are shown, not hidden - it is the point of the thing */
.years__arrow:disabled{ opacity:.28; cursor:default; }

.years__stack{ display:grid; transition:opacity .26s var(--ease); }
.years__panel{
  grid-area:1/1; margin-bottom:0;
  opacity:0; visibility:hidden;
  transition:opacity .32s var(--ease), visibility .32s;
}
.years__panel.is-on{ opacity:1; visibility:visible; }

/* icon-only, so it stays subordinate to Play; hidden until the queue
   actually has somewhere to skip to */
.btn--skip{
  padding:.9rem; line-height:0;
  background:transparent; color:var(--bone); border-color:var(--line);
}
.btn--skip:hover{ border-color:var(--bone); }
.btn--skip svg{ width:16px; height:16px; fill:currentColor; display:block; }
.btn--skip[hidden]{ display:none; }

/* ---- audio-reactive spectrum ---- */
.hero__wave{
  position:absolute; left:0; right:0; bottom:0; z-index:2;
  width:100%; height:130px;
  pointer-events:none;
}
.hero__note{
  position:absolute; right:var(--pad); bottom:1rem; z-index:3;
  font-size:.68rem; letter-spacing:.08em; color:var(--bone-dim);
  /* a long title must not run under the hero copy on a narrow screen */
  max-width:min(22rem,55vw); text-align:right;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* ================= SECTION ================= */
.section{
  padding:clamp(4.5rem,10vh,8rem) var(--pad);
  scroll-margin-top:calc(var(--nav-h) + 1rem);   /* clear the sticky nav */
}
/* alternating band, so five stacked sections don't read as one slab */
.section--alt{
  background:var(--ink-2);
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
}
.section__head{ margin-bottom:3.2rem; }
/* 3.2rem was tuned under a standfirst that wrapped to two lines. On one line
   it left about a line's worth of dead air above the content, so a head that
   ends in a description closes up. A bare title (Visuals) keeps the full gap,
   and a browser without :has() simply keeps the old spacing. */
.section__head:has(> .section__desc){ margin-bottom:1.75rem; }
.section__title{
  font-family:var(--f-serif); font-weight:400;
  font-size:clamp(2.4rem,5.5vw,3.9rem); line-height:1.02;
  letter-spacing:-.015em; margin-bottom:.85rem;
  /* inline-block so the box hugs the words: a full-width block would answer
     a hover from anywhere along the line, including empty space */
  display:inline-block; transform-origin:left center;
  transition:color .5s var(--ease), text-shadow .5s var(--ease),
             transform .35s var(--ease);
}

/* Lit while something is playing, and lit harder under the pointer. Both
   are scoped to .is-playing, so a paused page behaves exactly as it did -
   Pulse never adds that class under prefers-reduced-motion either, which is
   what keeps all of this off for anyone who has asked for less movement. */
/* A bright, tight core carries most of the colour; the wide layers only
   add reach. Spreading it thin over one large blur is what made the first
   attempt read as a grey smudge rather than as light. */
.is-playing .section__title{
  color:#fff;
  text-shadow:0 0 5px rgba(250,39,159,.95),
              0 0 16px rgba(250,39,159,.85),
              0 0 40px rgba(250,39,159,.6),
              0 0 90px rgba(250,39,159,.35);
}
.is-playing .section__title:hover{
  transform:scale(1.045);
  text-shadow:0 0 5px rgba(250,39,159,1),
              0 0 18px rgba(250,39,159,1),
              0 0 48px rgba(250,39,159,.9),
              0 0 110px rgba(250,39,159,.55);
}
/* Wide enough that a one-sentence standfirst like About's route (Jinan to
   ... Nashville) stays on one line on a desktop window - the old 38rem cap
   on the whole head broke it one word short. When it does have to wrap,
   balance evens the lines instead of stranding the last word. */
.section__desc{ color:var(--bone-dim); max-width:60rem; text-wrap:balance; }

/* ---- cards ---- */
.cards{
  display:grid; gap:1.6rem;
  grid-template-columns:repeat(auto-fit,minmax(min(19rem,100%),1fr));
}
.card{
  position:relative;
  display:flex; flex-direction:column;
  background:rgba(237,230,220,.035);
  border:1px solid var(--line);
  border-radius:14px;
  /* visible, not hidden: the record has to be able to leave the card.
     The sleeve does its own clipping instead — see .card__sleeve. */
  overflow:visible;
  transform-style:preserve-3d;
  transition:border-color .35s var(--ease), background .35s var(--ease);
}
.card:hover{
  border-color:rgba(237,230,220,.3); background:rgba(237,230,220,.06);
  z-index:2;                    /* the disc overlaps the next card, not under it */
}
.card__art{ position:relative; flex:none; aspect-ratio:1/1; background:var(--ink-2);
  border-radius:14px 14px 0 0; }

/* ---- the record: pulled from the sleeve on hover, and spinning ----
   Drawn from gradients rather than an image — one less request on a site
   that has to render inside the GFW, and it scales to any card width. */
.card__sleeve{
  position:absolute; inset:0; z-index:1;
  overflow:hidden;              /* the clipping .card gave up */
  border-radius:inherit;
  transition:opacity .9s var(--ease);
}
.card__disc{
  position:absolute; z-index:0;
  top:7%; bottom:7%; right:7%; aspect-ratio:1/1;
  border-radius:50%;
  /* a crisp lit rim, so the circle reads as a record against a dark card
     rather than dissolving into it */
  box-shadow:0 12px 34px rgba(0,0,0,.6),
             inset 0 0 0 1px rgba(237,230,220,.16);
  transform:translateX(0);        /* tucked fully behind the sleeve at rest */
  transition:transform .55s var(--ease);
}
.card__disc-face{
  position:absolute; inset:0; border-radius:50%;
  /* First layer paints on top: spindle, label, specular sweep, grooves, body.

     The sweep is doing the real work. Only a crescent of this disc ever
     clears the sleeve, and both the grooves and the label are rotationally
     symmetric — spun on their own they look completely still. A bright
     directional streak passing through that crescent is what actually reads
     as a spinning record, so it is deliberately far stronger than a
     realistic sheen would be. */
  background:
    radial-gradient(circle at 50% 50%, var(--ink) 0 2.2%, transparent 2.4%),
    radial-gradient(circle at 50% 50%, var(--magenta) 0 15%, transparent 15.4%),
    conic-gradient(from 0deg,
                   rgba(255,255,255,0)    0deg,
                   rgba(255,255,255,.20) 26deg,
                   rgba(255,255,255,0)   62deg,
                   rgba(255,255,255,0)  170deg,
                   rgba(255,255,255,.13) 200deg,
                   rgba(255,255,255,0)  232deg,
                   rgba(255,255,255,0)  360deg),
    repeating-radial-gradient(circle at 50% 50%,
                   rgba(255,255,255,.06) 0 1px, rgba(0,0,0,0) 1px 4px),
    radial-gradient(circle at 50% 50%, #201d28 0 68%, #100e14 100%);
  /* the spin lives on its own element so it cannot fight the slide-out
     transform on the parent — one transform property, one owner each */
  animation:disc-spin 3.4s linear infinite;
  animation-play-state:paused;
}
@keyframes disc-spin{ to{ transform:rotate(360deg); } }

/* ---- the track list pressed into the record (music page, albums only) ----

   The ring turns far slower than the disc's specular sweep does. That is
   deliberate and it is how a real record behaves: the highlight is a
   reflection sliding over the surface, while the printing underneath is
   part of the disc. It also means the titles are legible - at the sweep's
   3.4s a name would be through the crescent before it could be read, and
   at 22s each one holds still long enough to catch. One full turn brings
   the whole list past the opening. */
.card__ring{
  position:absolute; inset:0; z-index:1;
  animation:disc-spin 22s linear infinite;
  animation-play-state:paused;
}
.card:hover .card__ring{ animation-play-state:running; }
/* the title track, picked out of its own ring */
.card__ring-title{ fill:var(--magenta); }
.card__ring-t{
  fill:rgba(237,230,220,.52);
  font-family:var(--f-sans);
  font-weight:500; letter-spacing:.03em; text-transform:uppercase;
}

/* The album name, near the hub and off to the left: the side of the record
   the sleeve keeps covered. It does not rotate, so it never comes round
   into the crescent - the only way to read it is to hold on the card until
   the sleeve fades. */
/* The album name, near the hub and off to the left - the side of the record
   the sleeve never uncovers. Nothing on the page will reveal it: it does not
   rotate, so it cannot come round into the crescent, and no hover or tap
   lifts the sleeve. It is in the markup for someone reading the markup. */
.card__secret{ position:absolute; inset:0; z-index:0; }
.card__secret text{
  fill:rgba(237,230,220,.26);
  font-family:var(--f-serif); font-size:4.4px; letter-spacing:.08em;
}


/* far enough that a real slice of the record clears the sleeve; the hovered
   card is raised above its neighbour so the overhang reads as depth */
.card:hover .card__disc{ transform:translateX(34%); }
/* paused rather than removed, so the record keeps the angle it stopped at */
.card:hover .card__disc-face{ animation-play-state:running; }
.card__art img{
  width:100%; height:100%; object-fit:cover;
  transition:transform .7s var(--ease), filter .5s var(--ease);
  filter:saturate(.92);
}
.card:hover .card__art img{ transform:scale(1.05); filter:saturate(1.06); }
/* above the sleeve: the sleeve took a z-index so the record could sit
   behind it, which otherwise drops these underneath the artwork.

   The row is what sits over the corner, so a record can carry more than one
   label without them landing on top of each other. */
.card__tags{
  position:absolute; z-index:2; top:.85rem; left:.85rem;
  display:flex; flex-wrap:wrap; gap:.35rem;
  max-width:calc(100% - 1.7rem);
}
.card__badge{
  background:var(--magenta); color:#fff;
  font-size:.62rem; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  padding:.3rem .6rem; border-radius:100px;
  white-space:nowrap;
}
/* uppercase letterspacing pulls Chinese labels apart */
html[lang^="zh"] .card__badge{ letter-spacing:.04em; text-transform:none; font-size:.68rem; }
/* The three cards are grid items and so already share a height; making
   the body a column and pushing the links down with an auto margin lands
   every card's link row on the same line, whatever sits above it. Without
   this the singles card's "All releases" floated well below the albums'
   Spotify and Apple Music, because it has less copy over it. */
.card__body{
  padding:1.35rem 1.4rem 1.5rem;
  display:flex; flex-direction:column; flex:1;
}
.card__links{ margin-top:auto; }
.card__title{
  font-size:1.22rem; font-weight:600; letter-spacing:-.01em; margin-bottom:.35rem;
}
.card__title span{ color:var(--bone-dim); font-weight:400; font-size:.92em; }
.card__meta{
  font-size:.7rem; letter-spacing:.13em; text-transform:uppercase;
  color:var(--bone-dim); margin-bottom:.85rem;
}
.card__copy{ font-size:.9rem; color:#B9B2AB; margin-bottom:1.15rem; }

/* ---- track list: each name lifts on its own, and its neighbours step back
   so the one under the cursor is the only thing in focus ---- */
.card__tracks{
  list-style:none; display:flex; flex-wrap:wrap; gap:.5rem;
  margin-bottom:1.15rem;
}
/* A year holds three titles at most, so they are set larger than they were
   when all eleven had to fit - at the old size a single year looked stranded
   in the middle of the card. */
.card__tracks li{
  font-size:.95rem; color:#B9B2AB;
  padding:.42rem .85rem; border-radius:100px;
  border:1px solid transparent; background:rgba(237,230,220,.045);
  cursor:default;
  transition:transform .28s var(--ease), color .28s var(--ease),
             opacity .28s var(--ease), background .28s var(--ease),
             border-color .28s var(--ease), box-shadow .28s var(--ease),
             text-shadow .28s var(--ease);
}
.card__tracks:hover li{ opacity:.42; }        /* everything recedes... */
.card__tracks li:hover{                        /* ...except this one */
  opacity:1; color:var(--bone);
  transform:translateY(-3px);
  background:rgba(250,39,159,.14);
  border-color:rgba(250,39,159,.5);
  box-shadow:0 6px 16px rgba(250,39,159,.25);
}

.card__links{ display:flex; gap:1.15rem; flex-wrap:wrap; }
.card__links a{
  font-size:.79rem; font-weight:600; color:var(--bone);
  text-decoration:none; padding-bottom:2px;
  border-bottom:1px solid var(--magenta);
  transition:color .28s var(--ease), transform .28s var(--ease),
             opacity .28s var(--ease);
}
/* same idea as the tracks: the hovered link steps forward, the rest back */
.card__links:hover a{ opacity:.45; }
.card__links a:hover{ color:var(--magenta); opacity:1; transform:translateY(-2px); }
.card__links .is-missing{
  color:var(--bone-dim); border-bottom-color:var(--line);
}
.card__links .is-missing::after{ content:" — link needed"; font-weight:400; font-size:.9em; }

/* ================= VIDEOS =================
   Click-to-load facade: a thumbnail + a real YouTube link. main.js swaps
   the frame for an iframe on click, so nothing from youtube.com is
   requested until the visitor asks for it. */
/* standing note above a section — used for "Bilibili not up yet" */
.notice{
  max-width:44rem; margin:0 0 2.6rem;
  padding:1.1rem 1.3rem;
  background:rgba(250,39,159,.06);
  border:1px solid rgba(250,39,159,.28);
  border-radius:12px;
  font-size:.9rem; color:#CFC8C0;
}
.notice__title{
  font-size:.7rem; letter-spacing:.16em; text-transform:uppercase;
  color:var(--magenta); font-weight:600; margin-bottom:.45rem;
}
.notice a{ color:var(--bone); text-decoration:none; border-bottom:1px solid var(--magenta); }
.notice a:hover{ color:var(--magenta); }

.vids{
  display:grid; gap:1.4rem 1.3rem;
  grid-template-columns:repeat(auto-fill,minmax(min(17rem,100%),1fr));
}
.vid{ display:block; text-decoration:none; color:inherit; }
.vid--feature{
  display:block; max-width:64rem; margin:0 auto 3rem;
}
/* On the Visuals page itself the feature is the page, so it runs the whole
   content width instead of sitting in a 64rem column. The homepage keeps the
   narrower one, where it is a taster under the hero. */
#videos.section--lead .vid--feature{ max-width:none; }

.vid__frame{
  position:relative; display:block;
  aspect-ratio:16/9; overflow:hidden;
  border-radius:12px; background:var(--ink);
  border:1px solid var(--line);
  transition:border-color .35s var(--ease);
}
.vid__thumb{
  width:100%; height:100%; object-fit:cover;
  filter:saturate(.86) brightness(.82);
  transition:transform .7s var(--ease), filter .5s var(--ease),
             opacity .45s var(--ease);
}
.vid:hover .vid__frame{ border-color:rgba(237,230,220,.3); }
.vid:hover .vid__thumb{ transform:scale(1.04); filter:saturate(1) brightness(1); }

.vid__play{
  position:absolute; inset:0; margin:auto;
  width:56px; height:56px; border-radius:50%;
  display:grid; place-items:center;
  background:rgba(11,10,12,.55); backdrop-filter:blur(4px);
  border:1px solid rgba(237,230,220,.35);
  transition:background .3s var(--ease), transform .3s var(--ease),
             border-color .3s var(--ease), opacity .35s var(--ease);
}
.vid__play svg{ width:24px; height:24px; fill:var(--bone); margin-left:2px; }
.vid:hover .vid__play{
  background:var(--magenta); border-color:var(--magenta); transform:scale(1.08);
}
.vid:hover .vid__play svg{ fill:#fff; }
.vid--feature .vid__play{ width:78px; height:78px; }
.vid--feature .vid__play svg{ width:32px; height:32px; }

/* the iframe main.js drops in keeps the frame's shape, and sits above the
   poster rather than replacing it - see VideoFacade.embed */
.vid__frame iframe{
  position:absolute; inset:0; z-index:2;
  width:100%; height:100%; border:0; border-radius:12px;
}

/* ---- a played tile grows into its own row ---- */
/* The layout does not change shape: the feature stays on top and the grid
   stays a grid. The tile that was clicked simply claims a full row at the
   feature's own width, and main.js FLIPs the reflow so the siblings slide
   rather than jump. */
.vid--playing{
  grid-column:1 / -1;
  justify-self:center; width:100%; max-width:64rem;
}
.vid--playing .vid__frame{ border-color:rgba(237,230,220,.3); }
/* the poster underneath, on its way out */
.vid__thumb, .vid__play{ will-change:opacity; }
/* a click landing mid-slide would measure against a moving target */
.vids.is-morphing{ pointer-events:none; }

/* A tile with no Bilibili id yet: still an outbound link, but it should
   not pretend to be an inline player, so the play badge comes off. */
.vid--pending .vid__play{ display:none; }
.vid--pending .vid__thumb{ filter:saturate(.6) brightness(.6); }
.vid--pending:hover .vid__thumb{ filter:saturate(.85) brightness(.8); }
.vid__flag{
  position:absolute; left:.7rem; bottom:.7rem;
  background:rgba(11,10,12,.8); border:1px solid var(--line);
  border-radius:100px; padding:.28rem .7rem;
  font-size:.66rem; letter-spacing:.1em; color:var(--bone-dim);
}

.vid__meta{ display:block; padding:.85rem .15rem 0; }
.vid__kicker{
  display:block; font-size:.66rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--magenta); margin-bottom:.4rem;
}
.vid__title{
  display:block; font-weight:600; font-size:.98rem; letter-spacing:-.01em;
}
.vid__title span{ color:var(--bone-dim); font-weight:400; }
.vid--feature .vid__title{ font-size:1.5rem; }
.vid__sub{
  display:block; margin-top:.25rem;
  font-size:.7rem; letter-spacing:.11em; text-transform:uppercase;
  color:var(--bone-dim);
}

.playlists{
  display:flex; flex-wrap:wrap; gap:.7rem;
  margin-top:2.8rem; padding-top:2rem;
  border-top:1px solid var(--line);
}
.playlists a{
  padding:.55rem 1.05rem; border-radius:100px;
  border:1px solid var(--line); color:var(--bone-dim);
  font-size:.76rem; letter-spacing:.09em; text-transform:uppercase;
  text-decoration:none;
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.playlists a:hover{ color:var(--bone); border-color:var(--bone); }

/* ================= ABOUT ================= */
.about{
  display:grid; gap:clamp(2rem,5vw,4rem);
  grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);
  align-items:start;
}
/* the photo column tracks the prose instead of running out early */
.about__media{ position:sticky; top:calc(var(--nav-h) + 1rem); display:grid; gap:1.1rem; }
.about__shot{ margin:0; border-radius:12px; overflow:hidden; border:1px solid var(--line); }
.about__shot img{
  width:100%; aspect-ratio:4/5; object-fit:cover;
  filter:grayscale(.15) contrast(1.03);
}

.about__prose p{ color:#B9B2AB; margin-bottom:1.15rem; max-width:44rem; }
.about__prose strong{ color:var(--bone); font-weight:600; }
.about__prose em{ font-family:var(--f-serif); font-style:italic; color:var(--bone); }

.quote{
  margin:2rem 0; padding:.4rem 0 .4rem 1.5rem;
  border-left:2px solid var(--magenta);
}
.quote p{
  font-family:var(--f-serif); font-style:italic;
  font-size:clamp(1.35rem,2.6vw,1.85rem); line-height:1.25;
  color:var(--bone) !important; margin:0 !important;
}

.about__sub{
  margin:2.4rem 0 1rem;
  font-size:.72rem; letter-spacing:.22em; text-transform:uppercase;
  color:var(--magenta); font-weight:600;
}

.tags{ display:flex; flex-wrap:wrap; gap:.55rem; list-style:none; }
.tags li{
  padding:.4rem .85rem; border-radius:100px;
  border:1px solid var(--line); background:rgba(237,230,220,.035);
  font-size:.78rem; color:#CFC8C0;
}

.facts{ border-top:1px solid var(--line); }
.facts > div{
  display:grid; grid-template-columns:9rem 1fr; gap:1rem;
  padding:.8rem 0; border-bottom:1px solid var(--line);
}
.facts dt{
  font-size:.68rem; letter-spacing:.16em; text-transform:uppercase;
  color:var(--bone-dim); padding-top:.15rem;
}
.facts dd{ font-size:.92rem; color:#CFC8C0; }

/* ================= TIMELINE ================= */
.tl{ list-style:none; max-width:56rem; }
.tl__row{
  display:grid; grid-template-columns:8rem 1fr; gap:2rem;
  padding:2rem 0; border-top:1px solid var(--line);
}
.tl__row:last-child{ border-bottom:1px solid var(--line); }
.tl__year{
  font-family:var(--f-serif); font-weight:400;
  font-size:2.5rem; line-height:1; color:var(--bone-dim);
  position:sticky; top:calc(var(--nav-h) + 1rem); align-self:start;
}
/* the one acid accent on the page — the year it all landed */
.tl__year--now{ color:var(--acid); }

.tl__list{ list-style:none; display:grid; gap:.7rem; }
.tl__list li{
  position:relative; padding-left:1.35rem;
  font-size:.93rem; color:#B9B2AB;
}
.tl__list li::before{
  content:""; position:absolute; left:0; top:.62em;
  width:5px; height:5px; border-radius:50%;
  background:var(--magenta); opacity:.75;
}
.tl__list b{ color:var(--bone); font-weight:600; }

/* ================= IN THE MAKING ================= */

/* ---- the orbit ----
   One flat ring of photos, tipped towards the viewer (--lean) and then
   turned so its long axis runs top-left to bottom-right (--tilt). Scrolling
   turns the ring (--spin, written by Orbit in main.js), so the near side of
   it sweeps down that diagonal. Each card undoes the ring's rotations after
   it has been carried out to the rim, so it always faces the reader instead
   of showing its back half the time.

   --tilt, --lean and --dir are the three the content can set, per ring:
   build.py writes them onto .orbit as an inline style when shared.json says
   so, and the values below are what a ring that has never been adjusted
   uses. The admin's ring dial draws the same three in JavaScript, so keep
   the defaults here and RING_DEFAULTS in admin/preview.js in step.

   Everything sits in one 3D context, cover included, so the browser does the
   depth sorting: cards on the near side pass in front of the cover and the
   far side goes behind it, with no z-index bookkeeping.

   The block is only made tall once the script is running (.is-live). With JS
   off or reduced motion it is a still ring one screen high. */
.orbit{
  /* Radius and card size are held to the screen's height as well as its
     width: the near side of the ring swings low and comes forward, and on a
     short screen it used to run out of the bottom of the stage. */
  --r:min(44vw, 32rem, 42vh);      /* ring radius */
  --tilt:26deg;
  --lean:-20deg;
  /* which way scrolling turns the ring: 1 clockwise, -1 the other way. It
     flips the sign of the angle the script writes rather than the angle
     each card sits at, so the ring is arranged identically at rest and only
     the direction of travel changes. */
  --dir:1;
  --card:clamp(4.5rem, min(11vw, 15vh), 9rem);
  --cover:clamp(8rem, min(18vw, 26vh), 15rem);
  position:relative;
  margin-top:clamp(2rem,6vh,4rem);
  margin-inline:calc(-1 * var(--pad));   /* full bleed: cards fly past the gutter */
}
.orbit.is-live{ height:300vh; }
/* The ring's name, low on the left while it holds the screen. */
.orbit__title{
  position:absolute; left:var(--pad); bottom:clamp(1.2rem,4vh,2.6rem); z-index:2; margin:0;
  font-family:var(--f-serif); font-size:clamp(1.4rem,2.6vw,2.2rem); line-height:1.05;
  color:var(--bone); pointer-events:none;
  text-shadow:0 2px 18px rgba(0,0,0,.6);
  transition:opacity .5s var(--ease), transform .5s var(--ease);
}
.js .orbit.is-live .orbit__title{ opacity:0; transform:translateY(.6rem); }
.js .orbit.is-live .orbit__title.is-on{ opacity:1; transform:none; }

/* ---- a countdown in the middle of a ring ----
   Four numbers on a dark glass card, each over its unit. Countdown in
   main.js fills them in and, at zero, asks the server for the picture. */
.count{
  display:grid; justify-items:center; gap:.6rem;
  width:max-content; max-width:none;
  padding:1.1rem 1.4rem 1.2rem; border-radius:16px;
  background:rgba(11,10,12,.72); border:1px solid rgba(237,230,220,.18);
  backdrop-filter:blur(6px);
}
.count__label{
  margin:0; font-size:.66rem; font-weight:600; letter-spacing:.2em; text-transform:uppercase; color:var(--magenta);
}
html[lang^="zh"] .count__label{ font-size:.8rem; letter-spacing:.08em; }
.count__clock{ display:flex; gap:clamp(.6rem,1.4vw,1.1rem); margin:0; }
.count__unit{ display:grid; justify-items:center; min-width:2.6ch; }
.count__unit b{
  font-family:var(--f-serif); font-weight:400; font-variant-numeric:tabular-nums;
  font-size:clamp(1.6rem,3.2vw,2.6rem); line-height:1; color:var(--bone);
}
.count__unit small{ margin-top:.3rem; font-size:.6rem; letter-spacing:.14em; text-transform:uppercase; color:var(--bone-dim); }
html[lang^="zh"] .count__unit small{ font-size:.72rem; letter-spacing:.04em; }
.count__when{ margin:0; font-size:.8rem; color:var(--bone-dim); }
/* the picture, once it is out, arrives over where the clock was */
.orbit__centre.is-revealed{ animation:reveal 1.2s var(--ease) both; }
@keyframes reveal{ from{ opacity:0; filter:blur(14px); } to{ opacity:1; filter:none; } }
/* the In the Making filter: a ring with no match goes, and so does a card */
.orbit[hidden],
.orbit__card.is-out{ display:none; }
.orbit__stage{
  position:sticky; top:var(--nav-h);
  height:min(80vh, 46rem);
  display:grid; place-items:center;
  perspective:calc(var(--r) * 3);
  /* Sideways only. Clipping top and bottom cut cards off along a hard line
     above the footer as the stage let go at the end. */
  overflow-x:hidden; overflow-x:clip;
}
@supports (overflow-x:clip){ .orbit__stage{ overflow-y:visible; } }
.orbit.is-live .orbit__stage{ height:calc(100vh - var(--nav-h)); height:calc(100svh - var(--nav-h)); }
.orbit__scene{
  --spin:0deg;
  position:relative; width:0; height:0;
  transform-style:preserve-3d;
}
.orbit__centre,
.orbit__card{
  position:absolute; left:0; top:0;
  /* the scene is a zero-width point, so the site-wide img{max-width:100%}
     would shrink every one of these to nothing */
  max-width:none;
  border-radius:6px;
  box-shadow:0 1.2rem 3rem rgba(0,0,0,.55);
}
/* The middle keeps its own proportions - a square cover, a tall poster, a
   wide banner - inside a box about the size the square used to be, centred
   by a translate since its size is not known until it loads. */
.orbit__centre{
  width:auto; height:auto;
  max-width:calc(var(--cover) * 1.35); max-height:var(--cover);
  transform:translate(-50%, -50%);
}
/* the clock is sized by its numbers, not by the picture box */
.orbit__centre.count{ max-width:none; max-height:none; border-radius:16px; }
/* Cards keep their own shape - square covers, tall posters, wide banners -
   fitted inside a box a little wider than it is tall, so a banner reads as
   a banner without dwarfing everything else. Since their height is unknown
   until they load, they are centred by the translate at the head of the
   transform rather than by a margin. Being leftmost it is applied last,
   after the card has been carried out to the rim. */
.orbit__card{
  --a:calc(var(--i) * 360deg / var(--n) + var(--spin) * var(--dir));
  width:auto; height:auto;
  max-width:calc(var(--card) * 1.7); max-height:calc(var(--card) * 1.25);
  transform:
    translate(-50%, -50%)
    rotateZ(var(--tilt)) rotateX(var(--lean))
    rotateY(var(--a)) translateZ(var(--r)) rotateY(calc(-1 * var(--a)))
    rotateX(calc(-1 * var(--lean))) rotateZ(calc(-1 * var(--tilt)));
  /* dimmer on the far side, but never to black - the card straight behind
     the cover would otherwise show as a dark box over its top edge.
     Browsers without CSS trig drop this line and simply show every card at
     full brightness. */
  filter:brightness(calc(.62 + .38 * cos(var(--a))));
}

/* The way back up from the ring, pinned bottom-right while the ring holds
   the screen. Orbit in main.js turns it on; with no script the ring is not
   tall and there is nothing to climb back out of. `visibility` waits for the
   fade so the button cannot be tabbed to while it is invisible. */
.to-top{
  position:fixed; right:clamp(1rem, 3vw, 2rem); bottom:clamp(1rem, 3vw, 2rem);
  z-index:150;
  padding:.75rem 1.15rem;
  border-radius:999px;
  background:var(--bone); color:var(--ink);
  font-size:.74rem; font-weight:600; letter-spacing:.14em; text-transform:uppercase;
  text-decoration:none;
  box-shadow:0 .6rem 1.6rem rgba(0,0,0,.45);
  opacity:0; visibility:hidden; transform:translateY(.6rem);
  transition:opacity .3s var(--ease), transform .3s var(--ease), visibility 0s linear .3s;
}
.to-top.is-on{
  opacity:1; visibility:visible; transform:none;
  transition-delay:0s;
}

/* Up and down arrows at the right edge while a ring holds the screen. Each
   press scrolls to bring the next picture round to the front (Orbit in
   main.js). Shown and hidden on the same terms as the back-to-top pill. */
.orbit-nav{
  position:fixed; right:clamp(.75rem, 2.5vw, 2rem); top:50%; z-index:150;
  display:grid; gap:.6rem;
  opacity:0; visibility:hidden; transform:translate(.6rem, -50%);
  transition:opacity .3s var(--ease), transform .3s var(--ease), visibility 0s linear .3s;
}
.orbit-nav.is-on{
  opacity:1; visibility:visible; transform:translate(0, -50%);
  transition-delay:0s;
}
.orbit-nav__btn{
  width:3rem; height:3rem; border-radius:50%;
  display:grid; place-items:center; padding:0;
  color:var(--bone); cursor:pointer;
  background:rgba(11,10,12,.6); backdrop-filter:blur(6px);
  border:1px solid rgba(237,230,220,.3);
  box-shadow:0 .6rem 1.6rem rgba(0,0,0,.4);
  transition:background .25s var(--ease), border-color .25s var(--ease),
             color .25s var(--ease), transform .25s var(--ease);
}
.orbit-nav__btn svg{
  width:1.3rem; height:1.3rem;
  fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round;
}
.orbit-nav__btn:hover,
.orbit-nav__btn:focus-visible{
  background:var(--magenta); border-color:var(--magenta); color:#fff;
}
.orbit-nav__btn[data-orbit-step="-1"]:hover{ transform:translateY(-2px); }
.orbit-nav__btn[data-orbit-step="1"]:hover{ transform:translateY(2px); }


/* ================= PRESS ================= */
/* Press & Mentions, the cards that close the About page (render_press). */
.press{
  list-style:none; padding:0; margin:0;
  display:grid; gap:1rem;
  grid-template-columns:repeat(auto-fill,minmax(min(20rem,100%),1fr));
}
.press__item a{
  display:grid; gap:.3rem; height:100%;
  padding:1.25rem 1.35rem;
  background:rgba(237,230,220,.035);
  border:1px solid var(--line); border-radius:12px;
  text-decoration:none; color:inherit;
  transition:border-color .3s var(--ease), background .3s var(--ease),
             transform .3s var(--ease);
}
.press__item a:hover{
  border-color:rgba(237,230,220,.3);
  background:rgba(237,230,220,.06);
  transform:translateY(-3px);
}
.press__src{
  font-size:.63rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--magenta);
}
/* the same tracking-and-caps treatment as .contact__who, and for the same
   reason: 微博 at .63rem with wide tracking is a smudge */
html[lang^="zh"] .press__src{ font-size:.78rem; letter-spacing:.08em; }
.press__title{ font-size:1.02rem; font-weight:600; color:var(--bone); }
.press__gloss{ font-size:.82rem; color:var(--bone-dim); }

/* ================= SUBHEADS ================= */
/* A heading inside a section - Now Recording on In the Making. Smaller than
   a section title, with a rule running out to the edge so it reads as a
   divider as well as a label. */
.subhead{
  display:flex; align-items:center; gap:1.1rem;
  margin:clamp(3rem,7vw,5rem) 0 clamp(1.25rem,3vw,2rem);
  font-family:var(--f-serif); font-weight:400;
  font-size:clamp(1.9rem,4vw,2.9rem); line-height:1.05; color:var(--bone);
}
.subhead::after{ content:""; flex:1; height:1px; background:var(--line); }

/* ---- the Visuals filter ----
   A head that carries the switch becomes a row: title on the left, filter
   hard right and sitting on the title's baseline rather than floating in
   the middle of it. Under 700px the two stack, because three buttons and
   a 3.9rem serif title do not share a phone's width. */
.section__head--split{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:1.5rem; flex-wrap:wrap;
}
/* the title's own bottom margin would push the filter off the baseline */
.section__head--split .section__title{ margin-bottom:0; }
.section__headline{ min-width:0; }

/* Filter, then the switch, side by side beside the heading. */
.vtools{ display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; }
.vfind{ display:none; position:relative; }
.vjournal{
  display:none; padding:.6rem 1rem; border:1px solid var(--line); border-radius:999px;
  background:transparent; color:var(--bone-dim); cursor:pointer;
  font:inherit; font-size:.7rem; font-weight:600; letter-spacing:.16em; text-transform:uppercase;
  transition:border-color .25s var(--ease), color .25s var(--ease), background .25s var(--ease);
}
html.js .vjournal{ display:inline-block; }
html[lang^="zh"] .vjournal{ font-size:.8rem; letter-spacing:.04em; text-transform:none; }
.vjournal:hover{ color:var(--bone); border-color:var(--magenta); }
.vjournal:focus-visible{ outline:2px solid var(--magenta); outline-offset:2px; }
.vjournal[aria-pressed="true"]{ background:var(--magenta); border-color:var(--magenta); color:#fff; }
html.js .vfind{ display:block; }
.vfind__btn{
  display:flex; align-items:center; gap:.5rem;
  padding:.8rem 1.15rem; border:1px solid var(--line); border-radius:999px;
  background:rgba(11,10,12,.5); color:var(--bone); cursor:pointer;
  font:inherit; font-size:.72rem; font-weight:600; letter-spacing:.14em; text-transform:uppercase;
  transition:border-color .3s var(--ease);
}
html[lang^="zh"] .vfind__btn{ font-size:.8rem; letter-spacing:.04em; text-transform:none; }
.vfind__btn svg{ width:1rem; height:1rem; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; }
.vfind__btn:hover,
.vfind__btn[aria-expanded="true"]{ border-color:var(--magenta); }
.vfind__btn:focus-visible{ outline:2px solid var(--magenta); outline-offset:2px; }
.vfind__n{
  min-width:1.3rem; height:1.3rem; padding:0 .3rem; border-radius:999px;
  display:grid; place-items:center; background:var(--magenta); color:#fff;
  font-size:.68rem; letter-spacing:0;
}
.vfind__n[hidden]{ display:none; }
.vfind__panel{
  position:absolute; right:0; top:calc(100% + .5rem); z-index:20;
  width:min(24rem, calc(100vw - 2 * var(--pad)));
  padding:1rem; border:1px solid var(--line); border-radius:16px;
  background:rgba(19,17,20,.97); backdrop-filter:blur(10px);
  box-shadow:0 30px 70px rgba(0,0,0,.55);
}
.vfind__panel[hidden]{ display:none; }
.vfind__search{
  width:100%; box-sizing:border-box; margin-bottom:.9rem;
  padding:.65rem .9rem; border:1px solid var(--line); border-radius:10px;
  background:var(--ink, #0B0A0C); color:var(--bone); font:inherit; font-size:.9rem;
}
.vfind__search:focus{ outline:none; border-color:var(--magenta); }
.vfind__group + .vfind__group{ margin-top:.8rem; }
.vfind__name{
  margin:0 0 .45rem; font-size:.64rem; letter-spacing:.18em; text-transform:uppercase; color:var(--bone-dim);
}
html[lang^="zh"] .vfind__name{ font-size:.78rem; letter-spacing:.06em; }
.vfind__chips{ display:flex; flex-wrap:wrap; gap:.4rem; }
.vfind__chip{
  padding:.4rem .8rem; border:1px solid var(--line); border-radius:999px;
  background:transparent; color:var(--bone-dim); cursor:pointer; font:inherit; font-size:.82rem;
  transition:color .25s var(--ease), border-color .25s var(--ease), background .25s var(--ease);
}
.vfind__chip:hover{ color:var(--bone); border-color:rgba(237,230,220,.4); }
.vfind__chip:focus-visible{ outline:2px solid var(--magenta); outline-offset:2px; }
.vfind__chip[aria-pressed="true"]{ background:var(--magenta); border-color:var(--magenta); color:#fff; }
.vfind__clear{
  margin-top:1rem; padding:0; border:0; background:none; cursor:pointer;
  color:var(--bone-dim); font:inherit; font-size:.8rem; text-decoration:underline; text-underline-offset:3px;
}
.vfind__clear:hover{ color:var(--bone); }
.vfind__clear[hidden]{ display:none; }
.vempty{ margin:2rem 0 0; text-align:center; color:var(--bone-dim); }
.vempty[hidden]{ display:none; }

/* Nothing answers a click without JS, so the switch is not shown at all
   until main.js has set html.js - the whole grid is there either way. */
.vfilter{ display:none; }
html.js .vfilter{
  display:flex; gap:.25rem;
  padding:.25rem; border:1px solid var(--line); border-radius:999px;
  background:rgba(11,10,12,.5);
}
.vfilter__btn{
  display:flex; align-items:center; justify-content:center;
  padding:.55rem 1.15rem; border:0; border-radius:999px;
  background:transparent; color:var(--bone-dim); cursor:pointer;
  font:inherit; font-size:.72rem; font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; white-space:nowrap;
  transition:color .3s var(--ease), background .3s var(--ease);
}
/* uppercase letterspacing on Chinese sets the characters adrift */
html[lang^="zh"] .vfilter__btn{ font-size:.8rem; letter-spacing:.04em; text-transform:none; }
.vfilter__btn:hover{ color:var(--bone); }
.vfilter__btn:focus-visible{ outline:2px solid var(--magenta); outline-offset:2px; }
.vfilter__btn[aria-pressed="true"]{ background:var(--magenta); color:#fff; }
/* Wrapping is not enough here: "Visuals" and three short buttons do fit on
   one line at 430px, and what they fit around is the 02 above the title -
   the filter landed on top of it. Under 700px the head is a column and the
   switch becomes a full-width segmented control under the heading. */
@media (max-width:700px){
  .section__head--split{ display:block; }
  .vtools{ margin-top:1.25rem; flex-wrap:nowrap; }
  html.js .vfilter{ display:flex; flex:1; }
  .vfilter__btn{ flex:1; padding:.6rem .5rem; }
  .vfind__btn{ padding:.75rem .9rem; }
  .vfind__btn > span:first-of-type{ position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
  .vfind__panel{ left:0; right:auto; }
}

/* ---- the homepage's lead picture ----
   Stands in for the title video, so it takes the video's box exactly: full
   width of the section and 16:9, cropped to fill. Anything else and the
   homepage would change height depending on which one was chosen. */
.lead-pic{ margin:0; }
.lead-pic img{
  display:block; width:100%; aspect-ratio:16/9; object-fit:cover;
  border-radius:14px; border:1px solid var(--line);
}

/* ---- the picture under the title video ----
   A plain figure, so it takes the section's padding and lines up with the
   video above it and the grid below it by construction rather than by a
   measurement that would have to be kept in step with them. */
.banner{ margin:clamp(1.4rem,3vw,2.4rem) 0 0; }
.banner img{
  display:block; width:100%; height:auto;
  border-radius:12px; border:1px solid var(--line);
}

/* ================= JOURNAL =================
   The newest post sits on top of In the Making as one card, a link to the
   post's own page (journal-<id>.html); the others open from ring pictures.
   Picture on the left, words on the right; on a phone the picture goes on
   top. */
.latest{ margin-top:clamp(1.5rem,4vh,2.5rem); scroll-margin-top:calc(var(--nav-h) + 1.5rem); }
.latest__kicker{
  margin:0 0 .7rem;
  font-size:.7rem; font-weight:600; letter-spacing:.18em; text-transform:uppercase; color:var(--magenta);
}
html[lang^="zh"] .latest__kicker{ font-size:.82rem; letter-spacing:.06em; }
.jcard--latest{ position:relative; grid-template-columns:minmax(0,22rem) minmax(0,1fr); padding:1.2rem; cursor:zoom-in; }
.jcard__open{
  position:absolute; inset:0; z-index:1; border:0; padding:0; margin:0;
  background:transparent; border-radius:inherit; cursor:inherit;
}
.jcard__open:focus-visible{ outline:2px solid var(--magenta); outline-offset:3px; }
.jcard--latest:has(.jcard__open:hover){ border-color:rgba(250,39,159,.6); }
.jcard__titlerow{ display:flex; align-items:center; gap:.8rem; }
.jcard__titlerow .jcard__title{ margin:0; }
.jcard__titlerow + .jdate{ margin-top:.4rem; }

/* A play button at the end of an entry's title, a round one like the
   homepage's. It sits above the card's own button, so it plays rather
   than opening the card. */
.tplay{
  position:relative; z-index:2; flex:none;
  width:2.9rem; height:2.9rem; display:grid; place-items:center;
  border:0; border-radius:50%; background:var(--magenta); color:#fff; cursor:pointer;
  box-shadow:0 6px 20px rgba(250,39,159,.35);
  transition:transform .25s var(--ease), background .25s var(--ease);
}
.tplay:hover{ background:#ff45ad; transform:scale(1.06); }
.tplay:focus-visible{ outline:2px solid var(--bone); outline-offset:3px; }
.tplay svg{ width:1.15rem; height:1.15rem; fill:currentColor; margin-left:.12rem; }
.tplay[aria-pressed="true"] svg{ margin-left:0; }
.lens__titlerow{ display:flex; align-items:center; gap:.9rem; }
.lens__titlerow .lens__title{ margin:0; }
.jcard--latest.jcard--text{ grid-template-columns:minmax(0,1fr); }
.jcard{
  display:grid; grid-template-columns:minmax(0,15rem) minmax(0,1fr); gap:1.4rem;
  align-items:center; padding:1rem; border-radius:14px;
  border:1px solid var(--line); background:var(--ink-2);
  color:inherit; text-decoration:none;
  transition:border-color .35s var(--ease), transform .35s var(--ease);
}
.jcard:hover,
.jcard:focus-visible{ border-color:rgba(250,39,159,.6); transform:translateY(-2px); }
.jcard--latest:hover{ transform:none; }
.jcard:focus-visible{ outline:2px solid var(--magenta); outline-offset:3px; }
.jcard__pic{ display:block; aspect-ratio:4/3; border-radius:9px; overflow:hidden; background:var(--ink-3, #1C1A20); }
.jcard__pic img{ display:block; width:100%; height:100%; object-fit:cover; transition:transform .6s var(--ease); }
.jcard:hover .jcard__pic img{ transform:scale(1.04); }
.jcard__body{ display:block; min-width:0; }
/* a text-only post has no picture to show, so its card is all words */
.jcard--text{ grid-template-columns:minmax(0,1fr); padding:1.3rem 1.5rem; }
.jcard--text .jcard__excerpt{ -webkit-line-clamp:4; max-width:44rem; }
/* The title leads; the kind of post is a small coloured word above it and
   the date a smaller grey one under it. */
.jcard__kind,
.post__kind{
  display:block; margin:0 0 .45rem;
  font-size:.7rem; font-weight:600; letter-spacing:.18em; text-transform:uppercase; color:var(--magenta);
}
html[lang^="zh"] .jcard__kind,
html[lang^="zh"] .post__kind{ font-size:.82rem; letter-spacing:.06em; }
.jcard__title{
  display:block; margin:0 0 .4rem;
  font-family:var(--f-serif); font-size:clamp(1.6rem,2.8vw,2.3rem); line-height:1.08;
}
.jcard--latest .jcard__title{ font-size:clamp(1.9rem,3.6vw,3rem); }
.jdate{ display:block; margin:0 0 .7rem; font-size:.74rem; letter-spacing:.06em; color:var(--bone-dim); }
.post__date{ margin:.6rem 0 0; }
.jcard__excerpt{
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
  color:var(--bone-dim); font-size:.93rem; line-height:1.6;
}
.jcard__read{
  display:inline-block; margin-top:.8rem;
  font-size:.7rem; font-weight:600; letter-spacing:.16em; text-transform:uppercase; color:var(--bone);
}
html[lang^="zh"] .jcard__read{ font-size:.82rem; letter-spacing:.06em; }
.jcard__read span{ color:var(--magenta); }
@media (max-width:640px){
  .jcard,
  .jcard--latest{ grid-template-columns:minmax(0,1fr); gap:1rem; }
  /* wide, so the rings still start within a screen or so of the top */
  .jcard--latest .jcard__pic{ aspect-ratio:16/9; }
}

/* ---- one post ---- */
.post{ max-width:46rem; margin:0 auto; box-sizing:content-box; }
.post__back{ margin:0 0 2rem; font-size:.8rem; letter-spacing:.08em; }
.post__back a{ color:var(--bone-dim); text-decoration:none; }
.post__back a:hover{ color:var(--bone); }
.post__head{ margin-bottom:1.8rem; }
.post__title{
  margin:.4rem 0 0;
  font-family:var(--f-serif); font-weight:400;
  font-size:clamp(2.3rem,5.4vw,3.6rem); line-height:1.04; letter-spacing:-.01em;
}
.post__cover{ margin:0 0 2.2rem; }
.post__cover img{ display:block; width:100%; height:auto; border-radius:14px; border:1px solid var(--line); }
.post__body{ font-size:1.04rem; line-height:1.75; color:#CFC8C0; }
.post__body > p{ margin:0 0 1.2rem; }
.post__body a{ color:var(--bone); text-underline-offset:3px; }
.post__fig{ margin:2rem 0; }
.post__fig img,
.post__fig video{ display:block; width:100%; height:auto; border-radius:12px; border:1px solid var(--line); background:#000; }
/* a tall photo would otherwise be two screens of one picture */
.post__fig img{ width:auto; max-width:100%; max-height:min(80vh, 44rem); margin:0 auto; }
.post__fig .vid--feature{ max-width:none; margin:0; }
.post__fig figcaption,
.post__gallery figcaption{ margin-top:.6rem; font-size:.82rem; color:var(--bone-dim); }
.post__gallery{
  display:grid; gap:.6rem; margin:2rem 0;
  grid-template-columns:repeat(auto-fill, minmax(min(12rem,100%),1fr));
}
.post__gallery img{ display:block; width:100%; aspect-ratio:1; object-fit:cover; border-radius:10px; border:1px solid var(--line); }
.post__gallery figcaption{ grid-column:1 / -1; margin-top:0; }
/* 新歌速递: a new song inside a post - cover, name, a player, where to listen */
.post__song{
  display:grid; grid-template-columns:minmax(0,11rem) minmax(0,1fr); gap:1.4rem; align-items:center;
  margin:2rem 0; padding:1.1rem; border-radius:14px;
  border:1px solid var(--line); background:var(--ink-2);
}
.post__song-art{ display:block; width:100%; aspect-ratio:1; object-fit:cover; border-radius:9px; }
.post__song-body{ min-width:0; }
.post__song-body > p{ margin:0; }
.post__song-kicker{ font-size:.7rem; font-weight:600; letter-spacing:.18em; text-transform:uppercase; color:var(--magenta); }
html[lang^="zh"] .post__song-kicker{ font-size:.82rem; letter-spacing:.06em; }
.post__song-body .post__song-title{ margin:.35rem 0 .25rem; font-family:var(--f-serif); font-size:clamp(1.5rem,3vw,2rem); line-height:1.1; color:var(--bone); }
.post__song-meta{ font-size:.85rem; color:var(--bone-dim); }
.post__song-audio{ display:block; width:100%; margin-top:.9rem; height:2.4rem; }
.post__song-links{ display:flex; flex-wrap:wrap; gap:.5rem; margin-top:.9rem !important; }
.post__song-links a{
  padding:.35rem .8rem; border:1px solid var(--line); border-radius:999px;
  color:var(--bone); font-size:.85rem; text-decoration:none; transition:border-color .25s var(--ease);
}
.post__song-links a:hover{ border-color:var(--magenta); }
.post__song figcaption{ grid-column:1 / -1; font-size:.82rem; color:var(--bone-dim); }
@media (max-width:560px){
  .post__song{ grid-template-columns:minmax(0,1fr); }
  .post__song-art{ max-width:16rem; }
}
.post__pager{
  display:grid; grid-template-columns:1fr 1fr; gap:1rem;
  margin-top:3.2rem; padding-top:1.6rem; border-top:1px solid var(--line);
}
.post__pager a{
  display:block; padding:1rem 1.1rem; border-radius:12px; border:1px solid var(--line);
  color:var(--bone); text-decoration:none; font-family:var(--f-serif); font-size:1.2rem; line-height:1.2;
  transition:border-color .3s var(--ease);
}
.post__pager a:hover{ border-color:var(--magenta); }
.post__pager small{
  display:block; margin-bottom:.35rem; font-family:var(--f-sans, inherit); font-size:.68rem;
  letter-spacing:.16em; text-transform:uppercase; color:var(--bone-dim);
}
html[lang^="zh"] .post__pager small{ font-size:.8rem; letter-spacing:.06em; }
.post__older{ grid-column:2; text-align:right; }
@media (max-width:560px){
  .post__pager{ grid-template-columns:1fr; }
  .post__older{ grid-column:auto; }
}

/* ================= PHOTOS ================= */
/* Rows, each exactly the width of the page (visuals_layout in build.py).
   Every picture carries --ar, its width over height, and takes a share of
   the row's width in proportion to it - flex-grow --ar from a basis of 0 -
   with aspect-ratio turning that width back into a height. Width over ar
   is then the same for every picture in the row: one height, no gaps, and
   nothing can run past the margin, whatever proportions the photos are.

   A stack (.vstack) is one column of pictures in a row. It joins the row
   with the proportions of the whole column, and the pictures inside share
   its height by 1/ar each; the gap between them is taken out of the
   pictures, a crop of a few pixels nobody will see.

   .is-flat lets the authored rows go and flows every picture into fresh
   rows of about the same height - while the filter is on, and always on a
   narrow screen, where an arranged row of four would be four postage
   stamps. The ::after takes up the slack on the last line, which would
   otherwise blow its last few pictures up to the full width. */
.gallery-wrap{ container-type:inline-size; }
.gallery{
  --gap:clamp(.5rem,1.1vw,.95rem);
  --flat-h:clamp(9rem,22cqw,15rem);
  display:flex; flex-direction:column; gap:var(--gap);
}
/* --sum is the row's proportions added up. A row of one tall picture
   would be a screen and a half high at the full width, so a row is never
   taller than most of a screen: it narrows and centres instead. */
.vrow{ display:flex; gap:var(--gap); width:100%; max-width:calc(var(--sum, 99) * 82vh); margin-inline:auto; }
.vrow > .photo,
.vstack{ flex:var(--ar) 1 0; aspect-ratio:var(--ar); min-width:0; }
.vstack{ display:flex; flex-direction:column; gap:var(--gap); }
.vstack > .photo{ flex:calc(1 / var(--ar)) 1 0; min-height:0; }
.photo{
  position:relative; margin:0; overflow:hidden;
  border-radius:12px; border:1px solid var(--line); background:var(--ink-2);
}
/* Featured rows sit at the top of the same grid, a little apart from it;
   the banner comes after them. While a filter is on, the banner steps
   aside, and VisualsFilter.regroup gathers any leftover pictures into
   .vrow--pool rows - which stop short rather than stretch a lone picture. */
.gallery .banner{ margin:0; }
.vrow--featured:not(:has(+ .vrow--featured)){ margin-bottom:calc(var(--gap) * 1.5); }
.gallery.is-filtered .vrow--banner{ display:none; }
.gallery.is-flat{ flex-direction:row; flex-wrap:wrap; }
.gallery.is-flat .vrow,
.gallery.is-flat .vstack{ display:contents; }
.gallery.is-flat .photo{
  flex:var(--ar) 1 calc(var(--ar) * var(--flat-h));
  aspect-ratio:var(--ar); min-width:0; min-height:auto;
}
.gallery.is-flat::after{ content:""; flex:999 1 0; }
@container (max-width:720px){
  .gallery{ flex-direction:row; flex-wrap:wrap; }
  .vrow, .vstack{ display:contents; }
  .gallery .photo{
    flex:var(--ar) 1 calc(var(--ar) * var(--flat-h));
    aspect-ratio:var(--ar); min-width:0; min-height:auto;
  }
  .gallery::after{ content:""; flex:999 1 0; }
}
.photo img{
  width:100%; height:100%; object-fit:cover;
  object-position:var(--fx,50%) var(--fy,50%);
  transform-origin:var(--fx,50%) var(--fy,50%);
  transform:scale(calc(var(--z,1) * var(--hz,1)));
  transition:transform .8s var(--ease), filter .5s var(--ease);
}
.photo__cap{
  position:absolute; inset:auto 0 0 0;
  display:grid; gap:.2rem;
  padding:2.2rem .9rem .8rem;
  background:linear-gradient(to top, rgba(8,7,9,.82), transparent);
  pointer-events:none;
  transition:opacity .35s var(--ease), transform .35s var(--ease);
}
.photo__tag{
  font-size:.6rem; letter-spacing:.18em; text-transform:uppercase; color:var(--magenta);
}
html[lang^="zh"] .photo__tag{ font-size:.72rem; letter-spacing:.06em; }
.photo__title{ font-size:.9rem; font-weight:600; color:var(--bone); line-height:1.3; }
/* With a pointer, the caption waits for it; on touch it is simply there. */
@media (hover:hover){
  .photo__cap{ opacity:0; transform:translateY(.4rem); }
  .photo:hover .photo__cap,
  .photo:focus-within .photo__cap{ opacity:1; transform:none; }
  .photo:hover img{ --hz:1.035; }
}
/* the whole picture is the button, for the ones with more to say */
.photo__open{
  position:absolute; inset:0; z-index:1;
  border:0; padding:0; margin:0; background:transparent;
  cursor:zoom-in; border-radius:inherit;
}
.photo__open:focus-visible{ outline:2px solid var(--magenta); outline-offset:-3px; }
.photo__badge{
  position:absolute; top:.6rem; right:.6rem;
  width:1.9rem; height:1.9rem; border-radius:50%;
  display:grid; place-items:center;
  font-size:1.15rem; line-height:1; color:#fff;
  background:rgba(11,10,12,.55); backdrop-filter:blur(4px);
  border:1px solid rgba(237,230,220,.35);
  transition:background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease);
}
.photo__open:hover .photo__badge,
.photo__open:focus-visible .photo__badge{
  background:var(--magenta); border-color:var(--magenta); transform:rotate(90deg);
}
/* An album says how many pictures it holds instead of "+". The badge
   lights up like the other, but only the icon turns - a number on its
   side is not a number any more. Written after the rule above, which it
   has to beat at the same weight. */
.photo__badge--album{
  width:auto; padding:0 .6rem 0 .5rem; gap:.3rem; border-radius:999px;
  display:flex; align-items:center;
  font-size:.75rem; font-weight:600; letter-spacing:.04em;
}
.photo__badge--album svg{
  width:.95rem; height:.95rem; fill:none; stroke:currentColor; stroke-width:2;
  transition:transform .3s var(--ease);
}
.photo__open:hover .photo__badge--album,
.photo__open:focus-visible .photo__badge--album{ transform:none; }
.photo__open:hover .photo__badge--album svg,
.photo__open:focus-visible .photo__badge--album svg{ transform:rotate(90deg); }

/* ---- filtered out ----
   An attribute rather than a class so it leaves the accessibility tree
   too, with the weight to beat the flex rules above. */
.gallery .photo[hidden],
.gallery .vrow[hidden]{ display:none; }
/* Past the first page until "Load more" - but only once JS is running to
   answer the button; VisualsFilter swaps this for `hidden` on boot. */
.js .gallery > [data-later]{ display:none; }
.more{ margin:clamp(1.6rem,3vw,2.4rem) 0 0; text-align:center; }
.more[hidden]{ display:none; }
.more__btn{
  padding:.8rem 1.8rem; border:1px solid var(--line); border-radius:999px;
  background:rgba(11,10,12,.5); color:var(--bone); cursor:pointer;
  font:inherit; font-size:.74rem; font-weight:600; letter-spacing:.16em; text-transform:uppercase;
  transition:border-color .3s var(--ease), background .3s var(--ease);
}
html[lang^="zh"] .more__btn{ font-size:.85rem; letter-spacing:.06em; text-transform:none; }
.more__btn:hover{ border-color:var(--magenta); background:rgba(250,39,159,.08); }
.more__btn:focus-visible{ outline:2px solid var(--magenta); outline-offset:3px; }
/* The three rules below are written `.js .gallery ...` only to outweigh
   `.js .reveal.is-in`, which sets `transform:none` and a 0.9s transition
   and is further down the file. Scroll-reveal has finished with these
   cells by the time anyone reaches the filter; this is what moves them
   afterwards.

   On the way out: the cell holds its place for the length of the fade and
   is only then pulled from the grid, so the rest does not jump ahead of
   the animation. */
.js .gallery .photo.is-going{
  opacity:0; transform:scale(.965); pointer-events:none;
  transition:opacity .22s var(--ease), transform .22s var(--ease);
}
/* On the way in - a kind that was filtered out and is now back. */
.js .gallery .photo.is-coming{ opacity:0; transform:translateY(14px) scale(.98); }
/* Staying, but moving: the packer has already written the new spans and
   VisualsFilter holds every cell at its old position with a transform, so
   releasing them together slides them into the new arrangement. Only the
   arriving cells carry a --d, so the ones already on screen start at once
   and the new ones come in behind them. */
.js .gallery.is-moving .photo{
  transition:transform .55s var(--ease), opacity .45s var(--ease);
  transition-delay:var(--d,0s);
  will-change:transform;
}
@media (prefers-reduced-motion:reduce){
  .js .gallery .photo.is-going,
  .js .gallery .photo.is-coming{ opacity:1; transform:none; transition:none; }
  .js .gallery.is-moving .photo{ transition:none; }
}

/* ---- videos in the same grid ----
   A video cell is the ordinary tile, stretched to fill its span, with the
   title laid over the bottom of the poster the way a photo's caption is -
   except always showing, since a video with no name is not much use. */
/* max-width:none - a played tile's own cap would leave it narrower than its row */
.photo--video .vid{ position:relative; display:block; height:100%; max-width:none; }
.photo--video .vid__frame{ height:100%; aspect-ratio:auto; border:0; border-radius:0; }
.photo--video .vid__frame iframe{ border-radius:0; }
.photo--video .vid__meta{
  position:absolute; inset:auto 0 0 0; z-index:1;
  padding:2.4rem .9rem .75rem;
  background:linear-gradient(to top, rgba(8,7,9,.85), transparent);
  pointer-events:none;
  transition:opacity .35s var(--ease);
}
.photo--video .vid__title{ font-size:.92rem; }
.photo--video .vid__sub{ font-size:.6rem; margin-top:.15rem; }
.photo--video .vid__play{ width:48px; height:48px; }
.photo--video .vid__flag{ top:.6rem; bottom:auto; }
/* Played, it takes a line of its own across the whole width, at the top
   of its row, and the rest of the row closes up underneath. The caption
   steps off so it cannot sit over the player's own controls. */
.vrow:has(.is-playing),
.vstack:has(.is-playing){ flex-wrap:wrap; }
.vstack:has(.is-playing){ display:contents; }
.vstack:has(.is-playing) > .photo{ flex:var(--ar) 1 0; aspect-ratio:var(--ar); }
.gallery .photo.is-playing{ flex:1 1 100%; aspect-ratio:16 / 9; order:-1; }
.photo.is-playing .vid__meta{ opacity:0; }
@container (max-width:720px){
  .photo--video .vid__meta{ padding:1.6rem .6rem .55rem; }
  .photo--video .vid__title{ font-size:.8rem; }
  .photo--video .vid__flag{ top:.45rem; left:.45rem; padding:.2rem .5rem; font-size:.58rem; }
  .photo--video .vid__play{ width:40px; height:40px; }
}

/* ================= NOW RECORDING ================= */
/* A ring photo with a description is a button: it answers the pointer.
   Only the outline and brightness change - the transform belongs to the
   orbit and cannot take a hover scale on top without fighting it. */
.orbit__card[data-lens],
.orbit__centre[data-lens]{ cursor:zoom-in; }
.orbit__card[data-lens]:hover,
.orbit__card[data-lens]:focus-visible{
  outline:2px solid var(--magenta); outline-offset:4px;
  filter:brightness(1.12);
}

/* ================= LENS ================= */
/* The detail view. main.js flies a copy of the picture from where it sat to
   the middle of the screen, then opens this card outward from its centre
   line while the picture slides into its slot. The dialog itself is a
   transparent full-screen layer; the veil is the dimming. */
.lens{
  position:fixed; inset:0;
  width:100%; height:100%; max-width:none; max-height:none;
  margin:0; padding:clamp(.75rem,3vw,2rem); border:0;
  background:transparent; color:var(--bone);
  overflow:hidden;
}
.lens[open]{ display:grid; place-items:center; }
.lens::backdrop{ background:transparent; }
.lens__veil{
  position:absolute; inset:0;
  background:rgba(6,5,7,.8); backdrop-filter:blur(6px);
}
.lens__card{
  --ar:1;
  position:relative;
  display:grid; align-items:center;
  grid-template-columns:auto minmax(15rem, 24rem);
  gap:clamp(1.25rem,3vw,2.5rem);
  padding:clamp(1rem,2.4vw,1.75rem);
  max-height:100%;
  background:var(--ink-2);
  border:1px solid var(--line); border-radius:18px;
  box-shadow:0 40px 100px rgba(0,0,0,.65);
}
.lens__media{
  aspect-ratio:var(--ar);
  width:min(46vw, 40rem, calc(72vh * var(--ar)), calc(72svh * var(--ar)));
}
.lens__img{
  display:block; width:100%; height:100%; object-fit:cover;
  max-width:none; border-radius:10px;
}
/* The column scrolls, and anything scrolling clips: a round button's ring
   and glow at its left edge were cut off along the picture's side. A
   little room inside, given back with a negative margin, keeps them whole. */
.lens__body{ align-self:center; max-height:72vh; overflow:auto; padding:.4rem .25rem .4rem .6rem; margin:-.4rem 0 -.4rem -.6rem; }
.lens__kicker{
  margin:0 0 .6rem;
  font-size:.66rem; letter-spacing:.2em; text-transform:uppercase; color:var(--magenta);
}
html[lang^="zh"] .lens__kicker{ font-size:.8rem; letter-spacing:.06em; }
.lens__title{
  margin:0 0 1rem;
  font-family:var(--f-serif); font-weight:400;
  font-size:clamp(1.7rem,3.2vw,2.4rem); line-height:1.08;
}
.lens__text{ margin:0 0 .9rem; color:#B9B2AB; font-size:.95rem; line-height:1.65; }
.lens__close{
  position:absolute; top:.7rem; right:.7rem; z-index:2;
  width:2.2rem; height:2.2rem; border-radius:50%;
  border:1px solid var(--line); background:rgba(11,10,12,.6); color:var(--bone-dim);
  font-size:1.3rem; line-height:1; cursor:pointer;
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.lens__close:hover,
.lens__close:focus-visible{ color:var(--bone); border-color:var(--bone); }
/* the copy that flies; it lives in the dialog so it sits above the veil */
.lens__flier{
  position:fixed; z-index:3; margin:0; max-width:none;
  object-fit:cover; border-radius:10px;
  box-shadow:0 30px 80px rgba(0,0,0,.6);
  pointer-events:none; will-change:transform;
  transform-origin:0 0;
}
/* A diary entry (In the Making). Same card, a wider column for the words,
   because a gallery and a row of audio players need the room - the
   client's mock-ups run to about 36rem. */
.lens__card--entry{ grid-template-columns:auto minmax(18rem, 34rem); }
/* Only pictures - an album with no title or words. Same card as any
   other; there is just no empty title line, and the thumbnails start at
   the top of their column, level with the picture, rather than floating
   in the middle of a blank one. */
.lens__title:empty{ display:none; }
.lens__card--photos .lens__body{ align-self:start; padding-top:3rem; }
.lens__card--photos .entry__meta{ display:none; }
.lens__card--entry .lens__media{ width:min(40vw, 36rem, calc(72vh * var(--ar)), calc(72svh * var(--ar))); }
.lens__card--entry .lens__title{ font-size:clamp(1.4rem,2.4vw,1.9rem); margin-bottom:.9rem; }
.lens__media{ position:relative; }
/* another photo in the ring picture's slot fits inside it, uncropped */
.lens__media.is-other .lens__img{ object-fit:contain; background:#000; }
.lens__stage{
  position:absolute; inset:0; width:100%; height:100%;
  border:0; border-radius:10px; background:#000; object-fit:contain;
}
/* stepping through a gallery or an album */
.lens__step{
  position:absolute; top:50%; z-index:2; translate:0 -50%;
  width:2.4rem; height:2.4rem; border-radius:50%;
  display:grid; place-items:center; padding:0; cursor:pointer;
  border:1px solid rgba(237,230,220,.3); background:rgba(11,10,12,.55); color:var(--bone);
  backdrop-filter:blur(4px);
  transition:background .25s var(--ease), border-color .25s var(--ease);
}
.lens__step[hidden]{ display:none; }
.lens__step svg{ width:1.1rem; height:1.1rem; fill:none; stroke:currentColor; stroke-width:2; }
.lens__step--prev{ left:.6rem; }
.lens__step--next{ right:.6rem; }
.lens__step:hover,
.lens__step:focus-visible{ background:var(--magenta); border-color:var(--magenta); }
.lens__count{
  position:absolute; left:50%; bottom:.6rem; z-index:2; translate:-50% 0;
  padding:.2rem .6rem; border-radius:999px;
  background:rgba(11,10,12,.6); color:var(--bone); font-size:.72rem; letter-spacing:.08em;
  pointer-events:none;
}
.lens__count[hidden]{ display:none; }

.entry__date{ margin:0 0 .5rem; line-height:1; }
.entry__date time{ display:flex; align-items:baseline; gap:.6rem; }
.entry__day{ font-family:var(--f-serif); font-size:clamp(1.6rem,2.6vw,2.1rem); color:var(--bone); }
.entry__year{ font-size:.72rem; letter-spacing:.18em; color:var(--bone-dim); }
.entry__meta{
  display:flex; flex-wrap:wrap; align-items:center; gap:.2rem 0;
  margin:0 0 .7rem; font-size:.78rem; color:var(--bone-dim);
}
.entry__meta > span + span::before{
  content:"\00B7"; margin:0 .7rem; opacity:.6;
}
.entry__kind{ color:var(--magenta); }

/* the gallery is a strip: four tiles across, and past four it scrolls
   sideways under two arrows rather than unfolding into more rows */
.entry__strip{ position:relative; margin:0 0 1rem; }
.entry__strip.is-scrolling{ padding:0 2.1rem; }
.entry__gallery{
  --gap:.45rem;
  position:relative;
  display:grid; grid-auto-flow:column; gap:var(--gap);
  grid-auto-columns:calc((100% - 3 * var(--gap)) / 4);
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory; scrollbar-width:none;
  /* room for the current tile's focus ring inside the scroller */
  padding:3px 0;
}
.entry__gallery::-webkit-scrollbar{ display:none; }
.entry__tile{
  position:relative; aspect-ratio:1; padding:0; overflow:hidden;
  border:1px solid var(--line); border-radius:8px; background:#141215;
  cursor:pointer; opacity:.62; scroll-snap-align:start;
  transition:opacity .25s var(--ease), border-color .25s var(--ease);
}
.entry__tile:hover, .entry__tile:focus-visible{ opacity:1; }
.entry__tile:focus-visible{ outline:2px solid var(--magenta); outline-offset:-2px; }
.entry__tile.is-current{ opacity:1; border-color:var(--bone); }
.entry__tile img{ display:block; width:100%; height:100%; object-fit:cover; }
.entry__nav{
  position:absolute; top:50%; translate:0 -50%;
  width:1.8rem; height:1.8rem; border-radius:50%;
  display:grid; place-items:center; padding:0; cursor:pointer;
  border:1px solid rgba(237,230,220,.3); background:transparent; color:var(--bone);
  transition:background .25s var(--ease), border-color .25s var(--ease), opacity .25s var(--ease);
}
.entry__nav svg{ width:.9rem; height:.9rem; fill:none; stroke:currentColor; stroke-width:2; }
.entry__nav--prev{ left:0; }
.entry__nav--next{ right:0; }
.entry__nav:hover:not(:disabled),
.entry__nav:focus-visible{ background:var(--magenta); border-color:var(--magenta); }
.entry__nav:disabled{ opacity:.25; cursor:default; }
.entry__play{
  position:absolute; inset:50% auto auto 50%; transform:translate(-50%,-50%);
  width:2rem; height:2rem; display:grid; place-items:center;
  border-radius:50%; border:1px solid rgba(237,230,220,.5); background:rgba(11,10,12,.45);
}
.entry__play svg{ width:.9rem; height:.9rem; fill:var(--bone); margin-left:2px; }

.entry__audio{
  display:flex; align-items:center; gap:.9rem;
  margin:0 0 .9rem;
}
.entry__audio-btn{
  flex:0 0 auto; width:3rem; height:3rem; display:grid; place-items:center;
  border-radius:50%; border:1px solid rgba(237,230,220,.35); background:transparent;
  cursor:pointer; transition:border-color .25s var(--ease), background .25s var(--ease);
}
.entry__audio-btn:hover, .entry__audio-btn:focus-visible{ border-color:var(--bone); }
.entry__audio.is-playing .entry__audio-btn{ border-color:var(--magenta); background:rgba(250,39,159,.12); }
.entry__audio-btn svg{ width:1rem; height:1rem; fill:var(--bone); }
.entry__audio-body{ flex:1; min-width:0; display:grid; gap:.4rem; }
.entry__audio-name{ font-weight:600; font-size:.92rem; color:var(--bone); }
.entry__audio-bar{
  position:relative; height:3px; border-radius:2px; background:rgba(237,230,220,.16);
  cursor:pointer;
}
/* a taller hit area than the line it draws */
.entry__audio-bar::after{ content:""; position:absolute; inset:-8px 0; }
.entry__audio-fill{ position:absolute; inset:0 auto 0 0; width:0; border-radius:2px; background:var(--magenta); }
.entry__audio-time{ font-size:.72rem; color:var(--bone-dim); font-variant-numeric:tabular-nums; }

.entry__links{ list-style:none; margin:0 0 .9rem; padding:0; display:flex; flex-wrap:wrap; gap:.5rem; }
.entry__links a{
  display:inline-block; padding:.4rem .8rem; border:1px solid var(--line); border-radius:999px;
  color:var(--bone); font-size:.85rem; text-decoration:none;
  transition:border-color .25s var(--ease);
}
.entry__links a:hover, .entry__links a:focus-visible{ border-color:var(--magenta); }

/* a ring picture linked to a journal post: the way through to all of it */
.entry__read{ margin:.4rem 0 .9rem; }
.entry__read a{
  display:inline-block; padding:.55rem 1rem; border-radius:999px;
  background:var(--magenta); color:#fff; text-decoration:none;
  font-size:.72rem; font-weight:600; letter-spacing:.14em; text-transform:uppercase;
}
html[lang^="zh"] .entry__read a{ font-size:.85rem; letter-spacing:.06em; }
.entry__read a:focus-visible{ outline:2px solid var(--bone); outline-offset:2px; }

/* The larger view (the corner button on a diary entry): the card takes
   most of the screen, the picture grows with it, and the words get a wider
   column and a little more size. The picture keeps its own shape, so the
   flight in and out still lands on it exactly. */
.lens__grow{
  position:absolute; top:.7rem; right:3.3rem; z-index:2;
  width:2.2rem; height:2.2rem; border-radius:50%; padding:0;
  display:grid; place-items:center; cursor:pointer;
  border:1px solid var(--line); background:rgba(11,10,12,.6); color:var(--bone-dim);
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.lens__grow[hidden]{ display:none; }
.lens__grow svg{ width:1rem; height:1rem; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
.lens__grow .grow__in{ display:none; }
.lens__card.is-large .lens__grow .grow__out{ display:none; }
.lens__card.is-large .lens__grow .grow__in{ display:inline; }
.lens__grow:hover,
.lens__grow:focus-visible{ color:var(--bone); border-color:var(--bone); }
.lens__card--entry .lens__body > :first-child{ padding-right:4.6rem; }

.lens__card.is-large{ grid-template-columns:auto minmax(20rem, 40rem); }
.lens__card.is-large .lens__media{
  width:min(62vw, 64rem, calc(86vh * var(--ar)), calc(86svh * var(--ar)));
}
.lens__card.is-large .lens__body{ max-height:86vh; max-height:86svh; }
.lens__card.is-large .lens__title{ font-size:clamp(1.6rem,2.8vw,2.4rem); }
.lens__card.is-large .lens__text{ font-size:1.02rem; }
.lens__card.is-large .entry__gallery{ grid-auto-columns:calc((100% - 4 * var(--gap)) / 5); }

/* narrow: picture above, words below, and the card opens top-and-bottom */
@media (max-width:760px){
  .lens__card{ grid-template-columns:minmax(0,1fr); width:100%; }
  .lens__card--entry{ grid-template-columns:minmax(0,1fr); }
  .lens__card--entry .lens__media{ width:min(100%, calc(40vh * var(--ar)), calc(40svh * var(--ar))); }
  .lens__media{
    justify-self:center;
    width:min(100%, calc(46vh * var(--ar)), calc(46svh * var(--ar)));
  }
  .lens__body{ max-height:38vh; max-height:38svh; }
  .lens__title{ padding-right:2rem; }
  .lens__card--entry .lens__body > :first-child{ padding-right:0; }
  /* on a phone "larger" means the picture takes the full width and the
     card the full height, with the words scrolling under it */
  .lens__card.is-large{ grid-template-columns:minmax(0,1fr); height:100%; grid-template-rows:auto minmax(0,1fr); }
  .lens__card.is-large .lens__media{ width:min(100%, calc(56vh * var(--ar)), calc(56svh * var(--ar))); }
  .lens__card.is-large .lens__body{ max-height:none; min-height:0; overflow:auto; }
  .lens__card.is-large .entry__gallery{ grid-auto-columns:calc((100% - 3 * var(--gap)) / 4); }
}

/* ================= MESSAGE WINDOW ================= */
/* The "message Tony" <dialog>. Native modal: the browser handles the top
   layer, focus trapping and Escape; this only has to look like the site. */
.msg{
  width:min(36rem, calc(100vw - 2rem)); max-height:calc(100dvh - 2rem);
  padding:0; margin:auto;
  background:var(--ink-2); color:var(--bone);
  border:1px solid var(--line); border-radius:16px;
  box-shadow:0 30px 80px rgba(0,0,0,.6);
}
.msg::backdrop{ background:rgba(6,5,7,.72); backdrop-filter:blur(4px); }
.msg[open]{ animation:msg-in .35s var(--ease); }
@keyframes msg-in{ from{ opacity:0; transform:translateY(12px); } }
.msg__form{ display:grid; gap:1.1rem; padding:clamp(1.25rem,4vw,2rem); }
.msg__head{ display:flex; align-items:start; justify-content:space-between; gap:1rem; }
.msg__title{
  margin:0; font-family:var(--f-serif); font-weight:400;
  font-size:clamp(1.6rem,4vw,2.1rem); line-height:1.1;
}
.msg__close{
  flex:none; width:2.2rem; height:2.2rem; border-radius:50%;
  border:1px solid var(--line); background:transparent; color:var(--bone-dim);
  font-size:1.3rem; line-height:1; cursor:pointer;
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.msg__close:hover,
.msg__close:focus-visible{ color:var(--bone); border-color:var(--bone); }
.msg__fields{ display:grid; gap:1rem; }
.msg__intro{ margin:0; color:var(--bone-dim); font-size:.95rem; }
.msg__row{ display:grid; gap:1rem; grid-template-columns:1fr 1fr; }
@media (max-width:560px){ .msg__row{ grid-template-columns:1fr; } }
.msg__field{ display:grid; gap:.45rem; }
.msg__label{
  font-size:.68rem; letter-spacing:.16em; text-transform:uppercase;
  color:var(--bone-dim);
}
.msg__label .muted{ letter-spacing:.08em; text-transform:none; opacity:.7; }
html[lang^="zh"] .msg__label{ font-size:.84rem; letter-spacing:.06em; text-transform:none; }
.msg__field input,
.msg__field textarea{
  width:100%; box-sizing:border-box;
  font:inherit; font-size:1rem; color:var(--bone);
  padding:.75rem .9rem; border-radius:10px;
  background:rgba(237,230,220,.04); border:1px solid var(--line);
  transition:border-color .25s var(--ease), background .25s var(--ease);
}
.msg__field textarea{ resize:vertical; min-height:8rem; }
.msg__field input:focus,
.msg__field textarea:focus{
  outline:none; border-color:var(--magenta); background:rgba(237,230,220,.07);
}
.msg__hint{ margin:-.4rem 0 0; font-size:.8rem; color:var(--bone-dim); }
.msg__trap{ position:absolute; left:-9999px; width:1px; height:1px; opacity:0; }
.msg__status{ margin:0; font-size:.92rem; color:var(--bone); }
.msg__status:empty{ display:none; }
.msg__status.is-error{ color:#ff6fbf; }
.msg__actions{ display:flex; justify-content:flex-end; gap:.7rem; flex-wrap:wrap; }
.msg__send:disabled{ opacity:.6; cursor:progress; transform:none; }
/* once it has gone, the fields and the send button step aside for the note */
.msg__form.is-sent .msg__fields,
.msg__form.is-sent .msg__send{ display:none; }

/* ================= CONTACT ================= */
.contact{
  display:grid; gap:clamp(2.5rem,6vw,5rem);
  grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);
  align-items:start;
}
.contact__lead .section__title{ margin-bottom:.85rem; }
/* Sized down towards the width of the line beneath it - the default
   button padding made the pill noticeably wider than "Management — Ms.
   Zhang" and the two left edges lined up while the right ones did not.
   Tuned by eye: the exact match depends on how two different strings
   happen to render, and it is a different pair of strings in Chinese. */
.contact__actions{
  display:flex; flex-wrap:wrap; gap:.7rem; align-items:center;
  margin-top:1.8rem;
}
.contact__msg{ font-size:.82rem; padding:.85rem 1.2rem; }
.contact__mail{
  font-size:.82rem;
  padding:.85rem .95rem;
  word-break:break-all;
}
.contact__who{
  margin-top:.9rem;
  font-size:.7rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--bone-dim);
}
/* Han characters carry far more detail per glyph than Latin caps do, so
   the size that reads cleanly as MANAGEMENT — MS. ZHANG turns 经纪事务 into
   mush. Bigger, and with less of the tracking that was pushing the strokes
   apart - uppercasing does nothing here either way. */
html[lang^="zh"] .contact__who{
  font-size:.86rem; letter-spacing:.06em; text-transform:none;
}

/* Plain 1fr columns gave "Universal Music Publishing Group" exactly as
   much room as "Douyin", so publishing was the only column that wrapped -
   and it wrapped every line. minmax(max-content,1fr) fixes that from the
   other end: no column may be narrower than its own longest entry, and
   whatever is left over is shared out evenly between them. So nothing
   wraps, and the three still reach the right edge together - the block
   sits in its half rather than huddling at the left of it. */
.contact__grid{
  display:grid; gap:2rem 2.75rem;
  grid-template-columns:repeat(3,minmax(max-content,1fr));
  align-items:start;
}
.contact__col li{ white-space:nowrap; }
.contact__col h3{
  font-size:.7rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--magenta); font-weight:600; margin-bottom:1rem;
  padding-bottom:.7rem; border-bottom:1px solid var(--line);
}
.contact__col ul{ list-style:none; display:grid; gap:.65rem; }
.contact__col li{ font-size:.9rem; color:var(--bone-dim); }
.contact__col a{
  color:var(--bone); text-decoration:none;
  transition:color .25s var(--ease);
}
.contact__col a:hover{ color:var(--magenta); }
.muted{ color:var(--bone-dim); }

/* ================= FOOT ================= */
/* the contact block closes every page, under the copyright line */
.section--alt#contact{ border-bottom:0; }

.foot{
  border-top:1px solid var(--line);
  padding:2rem var(--pad);
  font-size:.75rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--bone-dim);
}
.foot p{ display:flex; justify-content:space-between; width:100%; gap:1rem; flex-wrap:wrap; }
.foot a{ color:var(--bone-dim); text-decoration:none; transition:color .25s var(--ease); }
.foot a:hover{ color:var(--bone); }

/* ================= SCROLL REVEAL =================
   Hidden state applies only when JS is running (html.js), so a script
   failure degrades to plain visible content instead of a blank page. */
.js .reveal{ opacity:0; transform:translateY(22px); }
.js .reveal.is-in{
  opacity:1; transform:none;
  transition:opacity .9s var(--ease), transform .9s var(--ease);
}

/* ================= RESPONSIVE ================= */

/* Short viewports (laptops, landscape phones): the hero stack is bottom
   aligned inside its row, so when it outgrows it the stack overflows
   upward and the eyebrow is clipped. Shrink the stack rather than let it
   slide out of view. */
/* ================= PAGE LINKS, RAILS, ABOUT VARIANTS ================= */

/* On the homepage each section heading is the way through to its own page.
   It reads as a heading, not as a link, until you point at it. */
.section__link{
  color:var(--bone); text-decoration:none;
  transition:color .2s var(--ease);
}
.section__link:hover,
.section__link:focus-visible,
.section__link:active{ color:var(--magenta); }

/* The way on from a trimmed section. Set in the display serif at the same
   weight as a timeline year, so it reads as part of the page rather than as
   a footnote you scroll past. */
.section__cta{ margin-top:3.2rem; text-align:center; }
.section__cta a{
  display:inline-block;
  font-family:var(--f-serif); font-weight:400;
  font-size:clamp(1.75rem,3.4vw,2.5rem); line-height:1.1;
  color:var(--bone); text-decoration:none;
  border-bottom:1px solid var(--line); padding-bottom:.3rem;
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.section__cta a::after{ content:" \2192"; }
.section__cta a:hover,
.section__cta a:focus-visible,
.section__cta a:active{ color:var(--magenta); border-color:var(--magenta); }

/* the homepage timeline shows three years and dissolves into the link */
.tl--brief{
  -webkit-mask-image:linear-gradient(to bottom,
    #000 0%, #000 34%, rgba(0,0,0,.78) 62%, rgba(0,0,0,.34) 86%, rgba(0,0,0,.10) 100%);
          mask-image:linear-gradient(to bottom,
    #000 0%, #000 34%, rgba(0,0,0,.78) 62%, rgba(0,0,0,.34) 86%, rgba(0,0,0,.10) 100%);
}
.tl--brief .tl__row:last-child{ border-bottom:0; }

/* the first section on a page with no hero above it still has to clear
   the sticky nav */
.section--lead{ padding-top:calc(var(--nav-h) + clamp(2.5rem,6vh,4.5rem)); }

/* ---- catalogue stat strip on the music page ---- */
/* A <dl>: each figure is a term and its gloss, so a screen reader reads
   "2, albums" rather than four loose numbers. */
/* Heading left, catalogue figures right. Two tiles stretched across the
   whole width read as a mostly empty band, which is what they became once
   the stream counts came out. */
/* Same three columns and the same gap as .cards below, so the figures sit
   exactly over the third sleeve instead of a dozen pixels off it. */
.section__lede{
  display:grid; gap:1.6rem;
  grid-template-columns:repeat(3,minmax(0,1fr));
  align-items:stretch;
  margin-bottom:3.2rem;
}
.section__lede .section__head{ grid-column:1 / 3; }
.section__lede .stats{ grid-column:3; }
.section__lede .section__head{ margin-bottom:0; }
.section__lede .stats{
  margin:0;
  grid-template-columns:repeat(2,minmax(0,1fr));
}
/* the tiles take their height from the heading beside them, so the figures
   centre in whatever that comes to rather than leaving air above and below */
.section__lede .stat{
  display:flex; flex-direction:column; justify-content:center;
}
/* Beside the heading the figures are their own width, so their left edge
   answers to nothing below them. Once the row gets tight, stack instead: full
   width lines both edges up with the cards underneath. */
@media (max-width:1180px){
  .section__lede{ grid-template-columns:minmax(0,1fr); gap:2rem; }
  .section__lede .section__head,
  .section__lede .stats{ grid-column:auto; }
}

.stats{
  display:grid; gap:1px;
  grid-template-columns:repeat(auto-fit,minmax(min(11rem,100%),1fr));
  margin:0 0 2.8rem;
  border:1px solid var(--line); border-radius:10px;
  background:var(--line);          /* the 1px gap shows through as the rule */
  overflow:hidden;
}
.stat{
  background:var(--ink);
  padding:1.35rem 1.1rem;
  text-align:center;
}
.stat__value{
  font-family:var(--f-serif);
  font-size:clamp(1.75rem,4.5vw,2.5rem);
  line-height:1; color:var(--bone); font-weight:600;
  font-variant-numeric:tabular-nums;
}
.stat__label{
  margin:.55rem 0 0;
  font-size:.68rem; letter-spacing:.16em; text-transform:uppercase;
  color:var(--bone-dim);
  /* letter-spacing puts a gap after the LAST letter too, so centring counts
     it and the word lands half a space left of true centre. The figure above
     is tabular and unspaced, so it centres honestly - nudge the label back
     by half the tracking to put them on the same axis. */
  text-indent:.08em;
}
/* A figure that links out still reads as the caption it is - the browser's
   own blue-and-underlined treatment made two of the four look like a
   different kind of thing entirely. It answers on hover instead. */
.stat__label a{
  color:inherit; text-decoration:none;
  border-bottom:1px solid transparent;
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.stat__label a:hover,
.stat__label a:focus-visible{
  color:var(--magenta); border-bottom-color:var(--magenta);
}

/* ---- the listening links, looping under a heading ---- */
.rail__heading{
  /* the cards above need air: this heading was sitting almost on them */
  text-align:center; margin:clamp(3rem,7vw,4.75rem) 0 1.35rem;
  font-size:.72rem; letter-spacing:.22em; text-transform:uppercase;
  color:var(--magenta); font-weight:600;
}
/* Held to the middle half of the page, fading up across its first quarter
   and away across its last - so between a quarter and three eighths of the
   way in it arrives, and between five eighths and three quarters it leaves.

   With only four or five links in the loop, running them the full width put
   the same names on screen twice at once. Half the width shows fewer, and
   the soft ends mean the set drifts in and out instead of visibly restarting
   against a hard edge. The border goes with it: the fade is the edge now. */
.rail--links{
  width:50%; margin-inline:auto; padding:.25rem 0;
  --fade:linear-gradient(to right,
          transparent 0, #000 25%, #000 75%, transparent 100%);
  -webkit-mask-image:var(--fade);
          mask-image:var(--fade);
}
.rail--links .rail__set{ display:flex; align-items:center; gap:.75rem; padding-right:.75rem; }
/* deliberately not a .chip: that one is a two-line press card, and its
   column layout and hover border would both have to be undone here */
.rail__link{
  flex:0 0 auto;
  display:block; white-space:nowrap;
  padding:.5rem 1.05rem; border-radius:100px;
  border:1px solid var(--line); background:rgba(237,230,220,.03);
  color:var(--bone-dim); text-decoration:none;
  font-size:.76rem; letter-spacing:.12em; text-transform:uppercase;
  transition:color .25s var(--ease), border-color .25s var(--ease),
             background .25s var(--ease);
}
.rail__link:hover,
.rail__link:focus-visible{
  color:var(--magenta); border-color:var(--magenta);
  background:rgba(237,230,220,.06);
}

/* ---- rail: the looping press row on the homepage ---- */
.rail{ overflow:hidden; }
.rail__track{ display:flex; width:max-content; will-change:transform; }
.rail__set{ display:inline-flex; gap:1rem; padding-right:1rem; align-items:stretch; }
.chip{
  flex:0 0 auto; display:flex; flex-direction:column; gap:.4rem;
  padding:1rem 1.35rem; text-decoration:none;
  background:rgba(237,230,220,.035);
  border:1px solid var(--line); border-radius:10px;
  transition:border-color .25s var(--ease), background .25s var(--ease);
}
.chip:hover{ border-color:var(--bone); background:rgba(237,230,220,.06); }
.chip__src{
  font-size:.62rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--magenta);
}
.chip__title{ color:var(--bone); font-size:.95rem; white-space:nowrap; }

/* The homepage carries the short profile, not the long bio, so its text
   column is much shorter than the About page's. At the shared .85fr the
   photograph towered over it and lined up with nothing; a fixed column keeps
   it a portrait rather than something that grows with the viewport. */
@media (min-width:1101px){
  .about--brief{
    grid-template-columns:minmax(0,24rem) minmax(0,1fr);
    align-items:stretch;
  }
  /* Now that the column is a fixed 24rem the photograph can fill the height
     of the profile again without the crop going square on a wide screen -
     it only has to stretch from a 4:5 portrait to roughly 3:5. */
  /* `top` is still set from the sticky rule above, and position:relative
     honours it - which quietly pushed the photograph down the height of the
     nav and left it hanging past the last row of the profile. */
  .about--brief .about__media{
    position:relative; top:auto; align-self:stretch;
  }
  .about--brief .about__shot{ position:absolute; inset:0; display:flex; }
  .about--brief .about__shot img{
    aspect-ratio:auto; width:100%; height:100%; object-fit:cover;
  }
  /* the quote leads the column, and its top margin would drop the first line
     below the top edge of the photograph */
  .about--brief .about__prose > :first-child{ margin-top:0; }
}

/* ---- about: one photograph beside the column of text ---- */
/* The photograph keeps its own 4:5 proportions and the two columns start
   together at the top; .about__media stays sticky, so it tracks the prose
   as you read past it. It used to be stretched to finish exactly where the
   text does, which tied the crop to the length of the copy - a tall column
   of facts produced a near-square box and cover() ate everything but his
   face. Nothing here needs a breakpoint: align-items:start is the default
   on .about, and below 1100px the grid is a single column anyway. */
.about--full .about__prose p:last-of-type{ margin-bottom:0; }

/* ---- press page: the photograph, flanked ---- */
/* Without JS this is what you get: every item, in a plain grid, arrows hidden. */
.press-arrow{ display:none; }
.press-stage__media{ margin:0 0 2rem; }

/* PressStage adds .is-live once it has taken over. */
.press-stage.is-live{
  display:grid;
  grid-template-columns:auto minmax(0,1fr) auto minmax(0,1fr) auto;
  grid-template-rows:1fr 1fr;
  gap:1.25rem clamp(1.25rem,3vw,2.5rem);
  align-items:stretch;
}
/* the list itself stops being a box so its items can sit in the stage grid */
.press-stage.is-live .press--stage{ display:contents; }
.press-stage.is-live .press-stage__media{
  grid-column:3; grid-row:1 / span 2;
  width:clamp(17rem,28vw,27rem); margin:0;
  display:flex; flex-direction:column;
}
.press-stage.is-live .press-stage__media img{
  aspect-ratio:auto; height:100%; min-height:0; flex:1 1 auto; object-fit:cover;
}
.press-stage.is-live .press__item[data-slot="0"]{ grid-column:2; grid-row:1; }
.press-stage.is-live .press__item[data-slot="1"]{ grid-column:2; grid-row:2; }
.press-stage.is-live .press__item[data-slot="2"]{ grid-column:4; grid-row:1; }
.press-stage.is-live .press__item[data-slot="3"]{ grid-column:4; grid-row:2; }

.press-stage.is-live .press-arrow{
  display:grid; place-items:center; align-self:center;
  width:2.9rem; height:2.9rem; padding:0;
  background:none; cursor:pointer;
  border:1px solid var(--line); border-radius:100px;
  color:var(--bone-dim);
  transition:color .25s var(--ease), border-color .25s var(--ease),
             background .25s var(--ease);
}
.press-arrow--prev{ grid-column:1; grid-row:1 / span 2; }
.press-arrow--next{ grid-column:5; grid-row:1 / span 2; }
.press-stage.is-live .press-arrow:hover,
.press-stage.is-live .press-arrow:focus-visible{
  color:var(--ink); background:var(--bone); border-color:var(--bone);
}
.press-arrow svg{
  width:20px; height:20px;
  fill:none; stroke:currentColor; stroke-width:1.7;
  stroke-linecap:round; stroke-linejoin:round;
}

@media (max-width:1100px){
  /* no room for a photograph between two columns of cards */
  .press-stage.is-live{
    grid-template-columns:auto minmax(0,1fr) auto;
    grid-template-rows:auto auto;
  }
  .press-stage.is-live .press-stage__media{
    grid-column:1 / -1; grid-row:1; width:100%; margin-bottom:1.5rem;
  }
  .press-stage.is-live .press__item[data-slot="0"]{ grid-column:2; grid-row:2; }
  .press-stage.is-live .press__item[data-slot="1"]{ grid-column:2; grid-row:3; }
  .press-stage.is-live .press__item[data-slot="2"]{ grid-column:2; grid-row:4; }
  .press-stage.is-live .press__item[data-slot="3"]{ grid-column:2; grid-row:5; }
  .press-arrow--prev{ grid-column:1; grid-row:2 / span 4; }
  .press-arrow--next{ grid-column:3; grid-row:2 / span 4; }
}

@media (max-height:780px){
  .ink, .inks{ width:min(25rem,62vw); }
  .hero__eyebrow{ margin-bottom:1.1rem; }
  .hero__title{ margin-bottom:1.1rem; }
  .hero__sub{ margin-bottom:.8rem; }
  .hero__count{ margin-bottom:.9rem; }
  .hero__out{ margin-bottom:1.5rem; }
  .hero__inner{ padding-bottom:clamp(4.5rem,9vh,6.5rem); }
}
@media (max-height:620px){
  .ink, .inks{ width:min(18.5rem,50vw); }
  .hero__sub{ font-size:.95rem; }
  .hero__count{ font-size:1.1rem; }
  .hero__wave{ height:80px; }
}

/* the photo rail has nowhere useful to stick once the grid narrows */
@media (max-width:1100px){
  .about{ grid-template-columns:1fr; }
  .about__media{
    position:static;
    grid-template-columns:repeat(auto-fit,minmax(min(14rem,100%),1fr));
  }
  .contact{ grid-template-columns:1fr; }
}

@media (max-width:900px){
  /* three max-content columns will not fit side by side down here, and
     holding them to one line each would push the page sideways */
  .contact__grid{ grid-template-columns:1fr; }
  .contact__col li{ white-space:normal; }

  /* half of a phone is not enough width for a pill, so the strip goes full
     bleed and only the fades keep their proportions */
  .rail--links{
    width:100%;
    --fade:linear-gradient(to right,
            transparent 0, #000 12%, #000 88%, transparent 100%);
  }
  /* the link row becomes a drop panel driven by the burger */
  .nav__burger{ display:block; }
  /* back to a plain bar: wordmark on one side, burger on the other */
  .nav{ display:flex; justify-content:space-between; }
  /* the six equal slots are a desktop idea only - in the drop panel they
     would stretch each link down the viewport and eat the gaps between */
  .nav__mark,
  .nav__item,
  .nav__pair{
    flex:0 0 auto;
    margin-block:0; padding-block:0;
    justify-content:flex-start;
  }
  .nav__mark{ font-size:.95rem; letter-spacing:.24em; }
  /* the panel stacks in source order; `order` only matters on the desktop row */
  .nav__links > *{ order:0; }
  .nav__pair{ gap:1rem; }
  .nav__links{
    display:flex;
    position:fixed; inset:0 0 auto; top:0;
    flex-direction:column; align-items:flex-start; justify-content:center;
    gap:1.6rem;
    min-height:100svh;
    padding:6rem var(--pad) var(--pad);
    /* fully opaque: the hero must not read through the open menu */
    background:var(--ink);
    transform:translateY(-100%);
    visibility:hidden;
    transition:transform .45s var(--ease), visibility .45s;
  }
  .nav--open .nav__links{ transform:none; visibility:visible; }
  .nav__links a{ font-size:1.6rem; letter-spacing:.04em; text-transform:none; }
  .nav__links a.is-active::after{ display:none; }
  .nav__cta{ margin-top:.5rem; font-size:1rem; padding:.7rem 1.6rem; }
  /* the burger has to sit above its own panel */
  .nav__burger{ position:relative; z-index:2; }

  /* stop the page scrolling behind an open menu */
  body.is-locked{ overflow:hidden; }

  /* stack: photo becomes a full-bleed backdrop behind the copy */
  .hero{ grid-template-columns:1fr; }
  .hero__media,
  .hero__scrim{ grid-column:1; grid-row:1; }
  .hero__media{
    -webkit-mask-image:none;
            mask-image:none;
  }
  .hero__media img{ object-position:56% 12%; filter:grayscale(.22) contrast(1.05) brightness(.72); }
  /* copy sits over the photo here, so the floor has to carry real weight */
  .hero__scrim{
    background:
      linear-gradient(to top,
        var(--ink) 30%,
        rgba(11,10,12,.92) 46%,
        rgba(11,10,12,.55) 66%,
        rgba(11,10,12,.45) 100%);
  }
  .hero__inner{
    grid-column:1; grid-row:1;
    padding:0 var(--pad) clamp(5rem,12vh,7rem);
    max-width:none;
  }
  /* the eyebrow lands on the brightest part of the photo — lift it */
  .hero__eyebrow{ color:#CFC8C0; text-shadow:0 1px 14px rgba(11,10,12,.95); }
  .hero__wave{ height:96px; }

  .nav__mark{ position:relative; z-index:2; }

  /* year rail collapses above its list */
  .tl__row{ grid-template-columns:1fr; gap:1.1rem; }
  .tl__year{ position:static; font-size:2rem; }

  .facts > div{ grid-template-columns:1fr; gap:.25rem; }
  .vid--feature{ margin-bottom:2.2rem; }
}

/* ================= REDUCED MOTION ================= */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-delay:0s !important;          /* else fill-mode `both` strands
                                               delayed elements in their
                                               hidden `from` state */
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    transition-delay:0s !important;
    scroll-behavior:auto !important;
  }
  .ink{ animation:none !important; clip-path:none !important; }
  .ink--name{ display:none; }
  /* a .001ms transition would make the record jump out rather than slide,
     so it simply stays in the sleeve */
  .card__disc{ transform:none !important; }
  .card__disc-face{ animation:none !important; }
  .card__ring{ animation:none !important; }
  .years__panel{ transition:none; }
  .card__tracks li:hover{ transform:none; }
  .card__links a:hover{ transform:none; }
  .js .reveal{ opacity:1; transform:none; }
  .grain{ animation:none; }
}
