:root {
  --bg: #0a0a0a;
  --fg: #f5f5f5;
  --accent: #ffffff;
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  /* content card geometry (tunable live via ?tune) */
  --card-inset: 60rem; /* distance of card's inner anchor from centre */
  --card-width: 32rem;
  --card-top: 50%;
}

* {
  box-sizing: border-box;
}

html {
  /* labels can bleed past the edges (wide ring) without a horizontal scrollbar */
  overflow-x: clip;
  /* hide the scrollbar (scrolling still works) */
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* old Edge/IE */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
  display: none; /* Chrome / Safari */
}

html,
body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-weight: 300; /* Inter Light — thin, sitewide */
  /* sitewide lowercase */
  text-transform: lowercase;
  /* Prevent scroll chaining / rubber-banding so the loop-jump feels seamless. */
  overscroll-behavior: none;
  /* all swipes are handled in JS as one-step navigation */
  touch-action: none;
}

/* Body height (the scroll range) is set in JS based on turn length + cycles.
   Keep a sane fallback before JS runs. */
body {
  min-height: 500vh;
}

/* LCP poster image ON TOP for the first paint (so it's an unoccluded LCP
   candidate), then faded out once the canvas draws its first real frame. */
.poster {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 3;
  transition: opacity 0.45s ease;
}
/* Full-viewport fixed canvas. Drawn with cover-fit math in JS, so no CLS. */
#stage {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  display: block;
  z-index: 1; /* above the poster img */
  transition: filter 0.5s ease;
}
/* very slight darkening of the scene behind an active carousel so the covers
   read better — the covers/caption sit above #stage and stay full-bright. */
/* while a carousel is open the page must be truly FROZEN: overflow:hidden on
   body propagates to the viewport, killing the scroll range (and with it iOS
   momentum + rubber-band, which visually drags even fixed elements). JS saves
   scrollY on enter and restores it on exit, so the figure never jumps. */
body.videos-mode,
body.music-mode {
  overflow: hidden;
  overscroll-behavior: none;
}
body.videos-mode #stage,
body.music-mode #stage {
  filter: brightness(0.85);
}

/* Cinematic vignette: above the canvas (z 0), below the front labels. */
.vignette {
  position: fixed;
  inset: 0;
  z-index: 2; /* above the poster (0) and canvas (1) */
  pointer-events: none;
  background: radial-gradient(
    130% 100% at 50% 42%,
    transparent 52%,
    rgba(0, 0, 0, 0.18) 78%,
    rgba(0, 0, 0, 0.4) 100%
  );
}

/* ---- Nav buttons -------------------------------------------------------- */
/* The <nav> is a static wrapper (no stacking context) so each fixed label can
   be raised above or dropped below the canvas (z-index 0) individually. */
.nav {
  opacity: 1;
}
/* rise + fade: the label drifts up a few px while fading in — a quiet editorial
   "settle" that reads instantly. Driven on an INNER span (.spin-btn__t) so it
   never touches the transform/opacity that applyRing writes on .spin-btn every
   tick. Starts the moment "loading" is removed (i.e. as the spin lands), short
   and snappy so About shows promptly. */
.spin-btn__t {
  display: inline-block;
  will-change: transform, opacity;
}
@keyframes riseIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
body:not(.loading) .spin-btn__t {
  animation: riseIn 0.55s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: var(--in-delay, 0s);
}
/* the "bright" label writes itself in letter by letter, left to right —
   echoing the direction of the reverse spin. The outer span stays put; each
   letter carries the rise, staggered by its index (--li set in the HTML). */
.spin-btn__t--letters span {
  display: inline-block;
  will-change: transform, opacity;
}
body:not(.loading) .spin-btn__t--letters {
  animation: none;
}
body:not(.loading) .spin-btn__t--letters span {
  animation: riseIn 0.45s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: calc(var(--in-delay, 0s) + var(--li, 0) * 0.055s);
}
/* the pill itself blooms from a soft blur into focus. filter is the one channel
   JS never drives on .spin-btn (applyRing owns transform/opacity/font-size), and
   it multiplies with the inline opacity — no fighting. "backwards" so the filter
   drops to none once landed (no lingering rasterise cost on scroll ticks). */
@keyframes pillIn {
  from {
    filter: opacity(0) blur(6px);
  }
  to {
    filter: opacity(1) blur(0);
  }
}
body:not(.loading) .spin-btn {
  animation: pillIn 0.9s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
  animation-delay: var(--in-delay, 0s);
}
/* gentle stagger: about (front) leads, the others follow around the orbit */
.spin-btn:nth-child(2) {
  --in-delay: 0.08s;
}
.spin-btn:nth-child(3) {
  --in-delay: 0.16s;
}
.spin-btn:nth-child(4) {
  --in-delay: 0.24s;
}
@media (prefers-reduced-motion: reduce) {
  body:not(.loading) .spin-btn__t,
  body:not(.loading) .spin-btn__t--letters span,
  body:not(.loading) .spin-btn {
    animation: none;
  }
}

.spin-btn {
  position: fixed;
  left: 50%;
  top: 50%;
  display: inline-block;
  white-space: nowrap;
  font-size: 1rem; /* JS overrides per-depth; this is the pre-JS fallback */
  font-weight: 300; /* Inter Light — thin */
  letter-spacing: 0.06em;
  color: var(--accent);
  text-decoration: none;
  /* plain text, legible over the photo — tight dark halo keeps it readable
     even while blurred during the focus-in */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85), 0 2px 16px rgba(0, 0, 0, 0.75);
  /* line-height:1 + padding tuned by VISUAL pixel measurement (8x magnified
     screenshots) so the glyph ink sits dead-centre in the pill. */
  line-height: 1;
  padding: 0.6em 1.05em 0.51em;
  border-radius: 999px;
  /* always-on liquid-glass pill behind every label. The border is nearly
     invisible — the rim light comes from the ::after gradient ring (real glass
     catches light directionally: lit top, faint sides, soft bottom bounce —
     a uniform border is what reads as flat/cheap). */
  border: 1px solid rgba(255, 255, 255, 0.08);
  /* vertical depth in the glass itself, not a flat tint */
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.18),
    rgba(255, 255, 255, 0.07) 48%,
    rgba(255, 255, 255, 0.12)
  );
  backdrop-filter: blur(10px) saturate(180%);
  -webkit-backdrop-filter: blur(10px) saturate(180%);
  box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.22),
    inset 0 -1px 6px rgba(255, 255, 255, 0.08), 0 8px 26px rgba(0, 0, 0, 0.35);
  transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
  /* transform, opacity, z-index are driven by JS each scroll tick */
  will-change: transform, opacity;
}

/* the rim light: a hairline gradient ring riding the border band — lit on the
   top edge, falling away at the sides, with a faint reflected bounce at the
   bottom. This is what makes the glass read as premium instead of outlined. */
.spin-btn::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.6),
    rgba(255, 255, 255, 0.16) 32%,
    rgba(255, 255, 255, 0.05) 58%,
    rgba(255, 255, 255, 0.24) 100%
  );
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  pointer-events: none;
  transition: filter 0.3s ease;
}
.spin-btn:hover::after,
.spin-btn:focus-visible::after {
  filter: brightness(1.35);
}

/* scroll-to-shine on the pills too: a glint sweeps each pill's edge while the
   orbit is in motion (JS drives --shine from the label's ring angle and flags
   .nav with .shining while scrolling); it melts away when the spin rests. */
.spin-btn::before {
  content: "";
  position: absolute;
  inset: -1px; /* ride ON the border band, over the ::after rim light */
  border-radius: inherit;
  padding: 1px;
  z-index: 1; /* glint above the rim */
  /* a soft highlight that GLIDES along the edge (linear, not conic — angular
     light smears on an elongated pill and bunches at the round caps). JS slides
     --shine-p back and forth with the orbit, so there's no wrap jump. */
  background: linear-gradient(
      105deg,
      transparent 38%,
      rgba(255, 255, 255, 0.95) 50%,
      transparent 62%
    )
    no-repeat;
  background-size: 300% 100%;
  background-position: var(--shine-p, 50%) 0;
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.45s ease;
  pointer-events: none;
}
.nav.shining .spin-btn::before {
  opacity: 1;
}

/* hover / focus: a brighter lift on the already-present glass */
.spin-btn:hover,
.spin-btn:focus-visible {
  outline: none;
  color: #fff;
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.26),
    rgba(255, 255, 255, 0.13) 48%,
    rgba(255, 255, 255, 0.18)
  );
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.3),
    inset 0 -1px 6px rgba(255, 255, 255, 0.12), 0 10px 30px rgba(0, 0, 0, 0.4);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
}

/* ---- Content card (quiet, on a side) ------------------------------------ */
/* Transparent full-viewport layer just for click-away-to-close; the figure
   stays visible. The card itself is a small glass panel on the left/right. */
.panel {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  text-transform: none; /* natural case for prose */
}
body.panel-open .panel {
  pointer-events: auto;
}
body.panel-open .nav {
  opacity: 0; /* hide orbit labels while a card is open */
}

.panel__card {
  position: absolute;
  top: var(--card-top, 50%);
  /* Phones: a centred panel over the figure (a side column is too narrow to
     hold readable text). Wider screens override width + position below to seat
     the card inside a side column. --card-tx is the X half of the centring
     transform (it never animates — the card just fades in at its position). */
  left: 50%;
  --card-tx: -50%;
  width: min(86vw, var(--card-width, 24rem));
  box-sizing: border-box;
  padding: clamp(1.4rem, 2.4vw, 2rem);
  color: #f2f2f2;
  background: rgba(12, 12, 12, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 14px;
  backdrop-filter: blur(18px) saturate(125%);
  -webkit-backdrop-filter: blur(18px) saturate(125%);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
  opacity: 0;
  transform: translate(var(--card-tx), -50%); /* fixed — no slide on open */
  transition: opacity 0.4s ease;
}
body.panel-open .panel__card {
  opacity: 1;
}
/* Wider screens: picture three equal columns with the figure in the centre
   one. The card sits at the HORIZONTAL CENTRE of its side column (the 1/6 or
   5/6 mark) and is sized to fit within that column, so the figure stays clear
   in the middle. */
@media (min-width: 48rem) {
  /* Seat the card beside the centred figure. Its INNER edge sits a fixed,
     proportional gap from the viewport centre (--card-gap, in vw, so every
     screen behaves the same); the card then extends outward toward its side.
     Width is capped to always fit between that inner edge and a small outer
     gutter, so it never overflows. */
  .panel__card {
    --card-center: 22vw;
    width: min(calc(33.333vw - 3rem), var(--card-width, 24rem));
  }
  .panel.left .panel__card {
    left: var(--card-center);
    right: auto;
    --card-tx: -50%;
  }
  .panel.right .panel__card {
    left: auto;
    right: var(--card-center);
    --card-tx: 50%;
  }
}

.panel__title {
  margin: 0 0 0.7rem;
  font-weight: 300;
  font-size: 1.05rem;
  letter-spacing: 0.08em;
  text-transform: lowercase;
  color: rgba(255, 255, 255, 0.6);
}
.panel__content {
  display: none;
}
.panel__content.active {
  display: block;
}
.panel__content p {
  font-weight: 300;
  font-size: clamp(0.92rem, 1.15vw, 1rem);
  line-height: 1.45;
  letter-spacing: 0.01em;
  margin: 0 0 0.6em;
}
.panel__content p:last-child {
  margin-bottom: 0;
}
.panel__content a {
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.35);
  transition: border-color 0.2s ease;
}
.panel__content a:hover {
  border-color: #fff;
}
.panel__k {
  display: inline-block;
  width: 6rem;
  opacity: 0.5;
}
/* thin hairline between the bio and the contact details */
.panel__divider {
  border: none;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
  margin: 1.1em 0;
}
.panel__close {
  position: absolute;
  top: 0.55rem;
  right: 0.7rem;
  width: 1.8rem;
  height: 1.8rem;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  color: rgba(242, 242, 242, 0.55);
  font-family: var(--font);
  font-weight: 300;
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease;
}
.panel__close:hover {
  color: #fff;
}

/* ---- Videos: true 3D cylinder carousel over the dimmed figure ------------ */
.vstage {
  position: fixed;
  inset: 0;
  z-index: 30;
  perspective: 1600px;
  perspective-origin: 50% 50%;
  pointer-events: none;
  opacity: 0;
  /* visibility:hidden makes the ENTIRE subtree non-interactive, regardless of
     the inline pointer-events:auto that applyRingState writes on front cards.
     Without it, an invisible front card lingers dead-centre after exit and
     steals the next click (replaying the last video). Delay the hide until the
     fade-out finishes so the exit still animates. */
  visibility: hidden;
  /* the entrance is carried by the JS dolly/deal motion, not a fade — keep the
     opacity transition short so it only softens the first/last frame */
  transition: opacity 0.3s ease, visibility 0s linear 0.3s;
  touch-action: none; /* we own drag gestures */
}
body.videos-mode .vstage {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 0.25s ease;
}
.vring {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  will-change: transform;
  touch-action: none; /* drag-to-spin owns the gesture; never scroll the page */
}
/* Phones: a shorter perspective so the cylinder reads as a deeper, more curved
   arc (side cards angle away harder) rather than a near-flat row. */
@media (max-width: 768px) {
  .vstage {
    perspective: 900px;
  }
}
/* keep the live figure/scene visible behind the cards (matches Music); just
   hide the orbiting labels */
body.videos-mode .nav {
  opacity: 0;
  pointer-events: none;
}

/* a card on the cylinder — liquid glass frame; transform set by JS */
.vthumb {
  position: absolute;
  left: 0;
  top: 0;
  /* smaller cards so more of the ring is visible at once (more neighbours peek
     in beside the focused one) */
  width: clamp(190px, 23vw, 320px);
  aspect-ratio: 16 / 9;
  padding: 0;
  backface-visibility: hidden; /* far side hides => figure shows through */
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 14px;
  background: rgba(22, 22, 26, 0.5);
  overflow: hidden;
  cursor: pointer;
  /* we own drag gestures — a touch starting on a card must not scroll the page
     (which used to spin the figure behind the carousel and stutter) */
  touch-action: none;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5),
    inset 0 1px 2px rgba(255, 255, 255, 0.16); /* blurred — fuses with the border */
  /* NO will-change here: will-change:transform makes Chrome cache a fixed-size
     texture and scale it under the perspective, blurring the magnified front
     card (and its play glyph). Letting it re-rasterise keeps content crisp. */
  transition: border-color 0.35s ease, box-shadow 0.35s ease;
}
.vthumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
}
/* scroll-to-shine: a glint sweeps along the card edges while the ring is in
   motion. Our take on the CSS view-timeline shine (which needs a real
   scrollport): JS drives --shine from each card's ring angle, and the stage
   gets .shining while the rotation is changing — at rest the glint melts away.
   The conic gradient is masked down to a hairline ring around the card. */
.vthumb::before,
.mthumb::before {
  content: "";
  position: absolute;
  inset: -1px; /* ride ON the 1px border — inside it would draw a double hairline */
  border-radius: inherit;
  padding: 1.5px;
  background: conic-gradient(
    from var(--shine, 0deg),
    transparent 0deg 70deg,
    rgba(255, 255, 255, 0.85) 110deg,
    transparent 150deg 360deg
  );
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.45s ease;
  pointer-events: none;
  z-index: 2;
}
.vstage.shining .vthumb::before,
.mring.shining .mthumb::before {
  opacity: 1;
}

/* glossy diagonal sheen — only on the focused card, so side cards stay clean */
.vthumb::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(150deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0) 40%);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
.vthumb.active::after {
  opacity: 0.5;
}
.vthumb.active {
  border-color: rgba(255, 255, 255, 0.32);
  box-shadow: 0 40px 100px rgba(0, 0, 0, 0.62),
    inset 0 1px 2px rgba(255, 255, 255, 0.24); /* blurred — fuses with the border */
}

/* focused video's work name, under the carousel */
.vcap {
  position: fixed;
  left: 50%;
  /* the focused card is vertically centred; its on-screen height scales with
     the card WIDTH (vw-based) + perspective magnification, so track the offset
     to vw — a vh offset collapses to the min on short viewports and overlaps */
  top: calc(50% + clamp(5.4rem, 10vw, 8.8rem));
  transform: translateX(-50%);
  z-index: 44;
  text-align: center;
  font-weight: 300;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: none; /* keep the work name's original case (e.g. naMean) */
  color: rgba(255, 255, 255, 0.96);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.85);
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
body.videos-mode .vcap {
  opacity: 1;
}

.v-exit {
  position: fixed;
  top: clamp(1rem, 3vh, 1.8rem);
  right: clamp(1rem, 3vw, 2rem);
  z-index: 45;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
  width: 2.6rem;
  height: 2.6rem;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  color: rgba(242, 242, 242, 0.6);
  font-family: var(--font);
  font-weight: 300;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease;
}
body.videos-mode .v-exit {
  opacity: 1;
  pointer-events: auto;
}
.v-exit:hover {
  color: #fff;
}

/* inline lightbox player */
.vplayer {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: none;
  place-items: center;
  background: rgba(0, 0, 0, 0.88);
  padding: clamp(1rem, 4vw, 3rem);
}
.vplayer.show {
  display: grid;
}
.vplayer__frame {
  width: min(92vw, 1100px);
  aspect-ratio: 16 / 9;
}
.vplayer__frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: 8px;
}
.vplayer__close {
  position: absolute;
  top: clamp(1rem, 3vh, 1.8rem);
  right: clamp(1rem, 3vw, 2rem);
  width: 2.6rem;
  height: 2.6rem;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  color: rgba(242, 242, 242, 0.7);
  font-family: var(--font);
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
}
.vplayer__close:hover {
  color: #fff;
}

/* ---- Music: covers form a halo ring ------------------------------------- */
/* Covers sit on an oval — facing the viewer at the left/right extremes and
   turning edge-on into thin slivers at the top/bottom centre (the vertical
   seam). The ring is a perspective stage above the still-visible figure. */
.mring {
  position: fixed;
  inset: 0;
  z-index: 30;
  perspective: 1100px;
  perspective-origin: 50% 50%;
  transform-style: preserve-3d; /* depth-sort the covers by real Z */
  pointer-events: none;
  /* we own the gestures on the EMPTY space too — without this a touch there
     starts a native page scroll: pointercancel, no click, tap-to-close dies */
  touch-action: none;
  opacity: 0;
  visibility: hidden;
  /* the entrance is carried by the JS fan-out motion, not a fade — keep the
     opacity transition short so it only softens the first/last frame */
  transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}
body.music-mode .mring {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  /* belt-and-braces for iOS click synthesis on the empty space (tap-to-close):
     a clickable cursor marks the element interactive even without a listener */
  cursor: pointer;
  transition: opacity 0.25s ease;
}
body.music-mode .nav {
  opacity: 0;
  pointer-events: none;
}
/* a square cover on the halo — size / transform / z-index set per frame by JS */
.mthumb {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 150px;
  aspect-ratio: 1 / 1;
  padding: 0; /* kill the default <button> padding so the square cover fills edge-to-edge */
  border-radius: 8px;
  overflow: hidden;
  background: transparent; /* the square cover fills it — no glass tint */
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45);
  cursor: pointer;
  touch-action: none; /* drag-to-spin owns the gesture */
  backface-visibility: hidden;
  will-change: transform;
}
.mthumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
  -webkit-user-drag: none;
  user-select: none;
}
.mthumb.active {
  border-color: rgba(255, 255, 255, 0.42);
  box-shadow: 0 26px 70px rgba(0, 0, 0, 0.6);
}

/* streaming links overlay — revealed on hover (desktop) / on the focused cover
   (touch). Two glass pills: Apple Music + Spotify. */
.mthumb__links {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.32rem;
  border-radius: inherit;
  background: rgba(0, 0, 0, 0.34);
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}
.mthumb:hover .mthumb__links {
  opacity: 1;
  pointer-events: auto;
}
/* touch: links stay hidden until the user taps the focused cover (JS toggles
   .links-open; it's cleared as soon as the cover leaves the front) */
.mthumb.links-open .mthumb__links {
  opacity: 1;
  pointer-events: auto;
}
/* text pill (apple / spotify), stacked, equal width. These words have both
   ascenders and descenders, so symmetric padding centres them. */
.mlink {
  width: 6rem;
  text-align: center;
  font-family: var(--font);
  font-size: 0.64rem;
  font-weight: 300;
  letter-spacing: 0.08em;
  color: #fff;
  text-decoration: none;
  line-height: 1;
  padding: 0.55em 0;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  background: rgba(28, 28, 32, 0.55);
  white-space: nowrap;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.mlink:hover {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.65);
}
/* mobile: the focused cover is ~105px wide — the desktop 6rem pill would span
   it edge to edge. Scale the pills to the cover (same ~60% share as desktop). */
@media (max-width: 768px) {
  .mlink {
    width: 4.2rem;
    font-size: 0.56rem;
  }
  .mthumb__links {
    gap: 0.28rem;
  }
}

/* caption under the focused cover: work name (light) over category (thinner,
   smaller, tracked, dimmed). */
.mcap {
  position: fixed;
  left: 50%;
  /* sit right under the focused cover (covers are vertically centred) */
  top: calc(50% + clamp(6.4rem, 12vh, 9rem));
  transform: translateX(-50%);
  z-index: 44;
  text-align: center;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}
body.music-mode .mcap {
  opacity: 1;
}
.mcap__name {
  display: block;
  font-weight: 300;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: none; /* keep the work name's original case (e.g. naMean) */
  color: rgba(255, 255, 255, 0.96);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.85);
}
.mcap__cat {
  display: block;
  margin-top: 0.15em;
  font-weight: 200;
  font-size: 0.72rem; /* same size as the work name */
  letter-spacing: 0.06em; /* same tracking as the work name */
  color: rgba(255, 255, 255, 0.55);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
}

.m-exit {
  position: fixed;
  top: clamp(1rem, 3vh, 1.8rem);
  right: clamp(1rem, 3vw, 2rem);
  z-index: 45;
  opacity: 0;
  pointer-events: none;
  width: 2.6rem;
  height: 2.6rem;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  color: rgba(242, 242, 242, 0.6);
  font-family: var(--font);
  font-weight: 300;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease;
}
body.music-mode .m-exit {
  opacity: 1;
  pointer-events: auto;
}
.m-exit:hover {
  color: #fff;
}

/* ---- Sitewide footer ---------------------------------------------------- */
.site-footer {
  position: fixed;
  left: 50%;
  bottom: clamp(0.7rem, 2.2vh, 1.2rem);
  transform: translateX(-50%);
  z-index: 46;
  font-size: 0.66rem;
  font-weight: 300;
  letter-spacing: 0.14em;
  color: rgba(255, 255, 255, 0.5);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7);
  pointer-events: none;
  white-space: nowrap;
}
/* hidden while frames decode; fades in gently the moment the reverse-spin
   intro begins (body gets .intro), and is simply ON once landed */
body.loading .site-footer {
  opacity: 0;
  transition: opacity 1.4s ease;
}
body.loading.intro .site-footer {
  opacity: 1;
}

/* ---- Loader ------------------------------------------------------------- */
/* transparent — the figure idle-spins beneath; only a hairline shows */
.loader {
  position: fixed;
  inset: 0;
  z-index: 50;
  pointer-events: none;
  transition: opacity 0.6s ease;
}
.loader.done {
  opacity: 0;
}
.loader__bar {
  position: absolute;
  left: 50%;
  bottom: clamp(2rem, 9vh, 4.5rem);
  transform: translateX(-50%);
  width: min(42vw, 240px);
  height: 1px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
/* indeterminate: a soft glow segment drifts gently across the hairline the
   whole time frames are loading — subtle motion so the first paint isn't silent. */
.loader__bar span {
  display: block;
  height: 100%;
  width: 36%;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.85),
    transparent
  );
  animation: loaderSlide 1.6s ease-in-out infinite;
}
@keyframes loaderSlide {
  0% {
    transform: translateX(-110%);
  }
  100% {
    transform: translateX(285%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .loader__bar span {
    animation: none;
    transform: translateX(92%);
  }
}

/* Keep the orbiting labels hidden until the frames are ready. */
body.loading .spin-btn {
  opacity: 0 !important;
  pointer-events: none !important;
}

/* mobile: covers are smaller, so pull the captions up closer to the carousel */
@media (max-width: 768px) {
  .mcap {
    top: calc(50% + clamp(3.7rem, 9.5vh, 5.2rem));
  }
  .vcap {
    /* card width pins to its min on phones, so the focused card's half-height is
       a near-constant ~78px — clear it with a flat ~6rem (vw stays below it) */
    top: calc(50% + clamp(6rem, 12vw, 7rem));
  }
}
