@font-face {
    font-family: 'Cheltenham Book';
    src: url("./assets/embedded/cheltenham-book.ttf") format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Bleu Femee';
    src: url("./assets/embedded/bleu-femee.ttf") format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'MadInk';
    src: url("./assets/embedded/madink.ttf") format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Cactus Jack';
    src: url("./assets/embedded/cactus-jack.ttf") format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Bootzy TM';
    src: url("./assets/embedded/bootzy-tm.ttf") format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Post DCU';
    src: url("./assets/embedded/post-dcu.otf") format('opentype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Helvetica Neue Light';
    src: url("./assets/embedded/helvetica-neue-light.ttf") format('truetype');
    font-weight: 300;
    font-style: normal;
    font-display: swap;
  }
  /* Recoleta Alt — one family, 7 weights (no italic face was supplied, so
     font-style:italic on it synthesizes an oblique rather than using a
     real italic design) */
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-thin.ttf") format('truetype');
    font-weight: 100;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-light.ttf") format('truetype');
    font-weight: 300;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-regular.ttf") format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-medium.ttf") format('truetype');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-semi-bold.ttf") format('truetype');
    font-weight: 600;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-bold.ttf") format('truetype');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'Recoleta Alt';
    src: url("./assets/embedded/recoleta-alt-black.ttf") format('truetype');
    font-weight: 900;
    font-style: normal;
    font-display: swap;
  }
  /* ZT Bros Oskon 90s — the free weight range only goes up to Regular (no
     bold/black face was supplied), each with its own real italic design
     (not synthesized) */
  @font-face {
    font-family: 'ZT Bros Oskon 90s';
    src: url("./assets/embedded/zt-bros-oskon-90s-extralight.otf") format('opentype');
    font-weight: 200;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'ZT Bros Oskon 90s';
    src: url("./assets/embedded/zt-bros-oskon-90s-extlightitalic.otf") format('opentype');
    font-weight: 200;
    font-style: italic;
    font-display: swap;
  }
  @font-face {
    font-family: 'ZT Bros Oskon 90s';
    src: url("./assets/embedded/zt-bros-oskon-90s-light.otf") format('opentype');
    font-weight: 300;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'ZT Bros Oskon 90s';
    src: url("./assets/embedded/zt-bros-oskon-90s-lightitalic.otf") format('opentype');
    font-weight: 300;
    font-style: italic;
    font-display: swap;
  }
  @font-face {
    font-family: 'ZT Bros Oskon 90s';
    src: url("./assets/embedded/zt-bros-oskon-90s-regular.otf") format('opentype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: 'ZT Bros Oskon 90s';
    src: url("./assets/embedded/zt-bros-oskon-90s-italic.otf") format('opentype');
    font-weight: 400;
    font-style: italic;
    font-display: swap;
  }
  :root{
    --bg: #0d0d0d;
    --ink: #f5f5f0;
    --accent: #ff4e00;
    --grey: #a3a39c;
    --font-display: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    --font-body: 'Inter', sans-serif;
    --font-mono: 'JetBrains Mono', monospace;
    --bio-font: 'Cheltenham Book', Georgia, serif;
    --bio-size: 17px;
    --bio-tracking: 0em;
    --bio-weight: 400;
    --bio-align: center;
  }
  * { box-sizing: border-box; }
  /* overflow-x:hidden needs to be on BOTH html and body — body alone only
     clips its own rendering, but a transform-scaled descendant (e.g. the
     launchpad's own dive-zoom, .is-diving below) can still widen the
     document's layout viewport past the visual one without it on html
     too, which makes mobile browsers zoom the *whole page* out to
     compensate — visually cramping everything on screen, not just the
     element that actually overflowed */
  html { scroll-behavior: smooth; overflow-x: hidden; }
  body { margin: 0; background: #f1ede3; color: #0d0d0d; font-family: var(--font-body); overflow-x: hidden; cursor: crosshair; }
  /* native cursor only hides once the JS glitch-HUD cursor actually
     confirms it's running — keeps a real fallback cursor for
     reduced-motion users or if the script fails for any reason */
  body.glitch-cursor-active { cursor: none; }
  /* buttons/links have their own default cursor:pointer from the browser
     that otherwise overrides the inherited cursor:none above — this
     forces the native cursor off everywhere, so our custom glyph is the
     only cursor visible anywhere on the page */
  body.glitch-cursor-active * { cursor: none; }
  /* subtle grain texture over the whole page — fixed so it doesn't scroll
     with content, pointer-events:none so it never blocks clicks, very low
     opacity so it reads as texture rather than visible static */
  body::after {
    content: '';
    position: fixed; top: -20%; left: -20%; width: 140%; height: 140%; z-index: 9999; pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%22220%22%20height%3D%22220%22%3E%0A%3Cfilter%20id%3D%22n%22%3E%0A%3CfeTurbulence%20type%3D%22fractalNoise%22%20baseFrequency%3D%220.85%22%20numOctaves%3D%222%22%20stitchTiles%3D%22stitch%22%20result%3D%22noise%22/%3E%0A%3CfeColorMatrix%20in%3D%22noise%22%20type%3D%22matrix%22%20values%3D%220%200%200%200%200%20%200%200%200%200%200%20%200%200%200%200%200%20%200%200%200%200.9%200%22/%3E%0A%3C/filter%3E%0A%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20filter%3D%22url%28%23n%29%22/%3E%0A%3C/svg%3E");
    background-size: 220px 220px;
    opacity: 0.14;
    mix-blend-mode: multiply;
    /* jitters the whole noise layer around — kept oversized (140%) so the
       edges never scroll into view during the animation */
    animation: grainMove 0.9s steps(8) infinite;
  }
  @keyframes grainMove {
    0%   { transform: translate(0%, 0%); }
    12%  { transform: translate(-3%, 4%); }
    25%  { transform: translate(4%, -2%); }
    37%  { transform: translate(-2%, -4%); }
    50%  { transform: translate(3%, 3%); }
    62%  { transform: translate(-4%, 1%); }
    75%  { transform: translate(2%, -3%); }
    87%  { transform: translate(-1%, 4%); }
    100% { transform: translate(0%, 0%); }
  }
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  }
  /* mix-blend-mode can't blend correctly against cross-origin iframe
     content (a browser compositing limitation, not something CSS can work
     around) — rather than the grain reading as a much more visible whitish
     film specifically over an open video, just don't render it there */
  body.lightbox-open::after { opacity: 0; }
  body.modal-open { overflow: hidden; }
  /* TouchDesigner-style cursor particle trail — sits above everything
     (including the grain overlay) so it's always visible while
     navigating, but never intercepts clicks */
  #cursorTrail {
    position: fixed; inset: 0; width: 100vw; height: 100vh;
    z-index: 10000; pointer-events: none;
  }
  a { color: inherit; }
  ::selection { background: var(--ink); color: var(--accent); }

  /* ---------- HERO ---------- */
  .slider {
    position: relative;
    height: 92vh; min-height: 540px; width: 100%;
    overflow: hidden;
    background: var(--bg);
  }
  /* brands.html's hero: a plain fade-in rather than the artists page's oval
     "TV startup" wipe (that one's scoped to .artists-page, so this doesn't
     touch it). main.js adds .tv-startup to every page's hero once it's
     ready to reveal — on a direct/refreshed load that's immediately; via
     the homepage hand-off it's once the overlay has fully faded away. */
  body:not(.artists-page) .slider { opacity: 0; transition: opacity 0.9s ease; }
  body:not(.artists-page) .slider.tv-startup { opacity: 1; }
  @media (prefers-reduced-motion: reduce) {
    body:not(.artists-page) .slider { opacity: 1; transition: none; }
  }
  .slide {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--font-mono); font-size: 11px; color: rgba(255,255,255,0.5);
    text-transform: uppercase; letter-spacing: 0.06em;
    transition: opacity 0.45s ease;
    overflow: hidden;
    /* lets the iframe below size itself off the slide's own on-screen
       box (cqw/cqh) instead of the viewport — the hero is rarely 16:9
       itself (92vh tall, full width), so sizing against the viewport
       would still leave black letterbox bars on wide/short screens */
    container-type: size;
  }
  .slide iframe {
    position: absolute; top: 50%; left: 50%;
    /* scaled up + centered so YouTube's title strip and corner watermark —
       which sit right at the iframe's own edges regardless of controls=0 —
       get pushed outside .slide's clipped viewport instead of showing.
       Flat fallback for browsers without container query units. */
    width: 130%; height: 130%;
    transform: translate(-50%, -50%);
    pointer-events: none; border: 0;
  }
  /* true "cover" fit: keeps the iframe at the video's own real aspect
     ratio (--v-ratio, set in JS from oEmbed — falls back to 16:9 if that
     lookup ever fails) while forcing it to always fully cover the slide,
     whatever the slide's own shape — this is what actually removes the
     black letterbox bars. Using the real ratio (not a flat 16:9 assumption)
     matters because a non-16:9 source (4:3, ultra-wide, vertical) would
     otherwise still show letterboxing baked in by YouTube/Vimeo's own
     player, which is cross-origin and impossible to reach in and fix.
     --overscan then scales the whole thing up a bit further beyond the
     minimum needed to cover — YouTube/Vimeo draw their own title text and
     logo watermark right at the iframe's own edges regardless of any
     controls/branding params, and this pushes those edges (and everything
     drawn against them) outside .slide's clipped view. Without it, sizing
     exactly to the minimum cover size leaves the iframe's edges flush with
     the visible area, so that chrome is visible again — the flat 130%
     fallback above never had this problem since it was already oversized. */
  @supports (width: 1cqw) {
    .slide iframe {
      --ratio: var(--v-ratio, 1.7778);
      --overscan: 1.18;
      width: calc(100cqw * var(--overscan)); height: calc(100cqh * var(--overscan));
      min-width: calc(100cqh * var(--ratio) * var(--overscan));
      min-height: calc(100cqw / var(--ratio) * var(--overscan));
    }
  }
  .slide video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }

  .media-error {
    position: absolute; inset: 0; z-index: 2;
    display: flex; align-items: center; justify-content: center; text-align: center;
    padding: 40px; font-family: var(--font-mono); font-size: 12px; color: var(--ink);
    background: #1a1a1a;
  }


  /* a standalone wrapper (.page-top) instead of the header — these three
     just need to render at the literal top of the page regardless of
     which section ends up first (e.g. artists.html leads with the
     pad-section, ahead of the header these used to be positioned
     relative to), NOT stay pinned while scrolling — position:fixed was
     tried for this and made them follow the viewport permanently, which
     wasn't wanted. .page-top sits first in the body with no height of
     its own, so these absolutely-positioned children land at the true
     top of the page and then scroll away normally with everything else. */
  /* flow-root: an empty box otherwise lets a following sibling's
     margin-top collapse straight through it (and even out to body),
     dragging .page-top's own absolutely-positioned children down along
     with whatever comes after — this keeps .page-top pinned at the true
     top regardless of any margin-top on the header that follows it */
  .page-top { position: relative; z-index: 40; display: flow-root; }
  .mark { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); z-index: 30; pointer-events: auto; text-align: center; text-decoration: none; display: block; opacity: 0.92; transition: opacity .2s ease; }
  .mark:hover, .mark:focus-visible {
    opacity: 1;
    cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34"><text x="0" y="26" font-size="26">👆🏻</text></svg>') 6 2, pointer;
  }
  .mark img { height: clamp(22px, 3.8vw, 40px); width: auto; display: block; }

  .top-right-nav {
    position: absolute; bottom: 32px; right: 28px; z-index: 35;
    display: flex; align-items: center; gap: 18px;
  }

  .who-link {
    position: absolute; top: 20px; left: 28px; z-index: 35;
    font-family: var(--font-display);
    font-size: 15px; font-weight: 500; letter-spacing: -0.05em; text-transform: uppercase;
    color: var(--ink); text-decoration: none; opacity: 0.85;
    transition: opacity .2s ease;
    /* was an <a>, now a <button> so it doesn't navigate — reset the
       default button chrome back to how the link used to look */
    background: none; border: none; cursor: pointer;
    /* padding + negative margin: enlarges the actual clickable/hoverable
       area well beyond the text itself, without shifting the
       text's visual position at all */
    padding: 10px 14px; margin: -10px -14px;
  }
  .who-link:hover { opacity: 1; }

  .mascot {
    position: absolute; top: 20px; right: 28px; z-index: 35;
    background: none; border: none; cursor: pointer; opacity: 0.85;
    transition: opacity .2s ease;
    padding: 10px; margin: -10px;
  }
  .mascot:hover { opacity: 1; }
  .mascot-icon { display: block; height: 25px; width: auto; filter: invert(1); }

  .mute-toggle {
    width: 38px; height: 38px; font-size: 15px;
    color: var(--ink); background: rgba(245,245,240,0.12);
    border: 1px solid rgba(245,245,240,0.35); border-radius: 50%;
    cursor: pointer; backdrop-filter: blur(6px);
    transition: background .2s ease; display: flex; align-items: center; justify-content: center;
  }
  .mute-toggle:hover { background: rgba(245,245,240,0.22); }

  .edit-toggle {
    position: absolute; bottom: 32px; right: 28px; z-index: 35;
    font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--ink); background: rgba(245,245,240,0.12);
    border: 1px solid rgba(245,245,240,0.35); border-radius: 40px;
    padding: 9px 16px; cursor: pointer; backdrop-filter: blur(6px);
    transition: background .2s ease;
  }
  .edit-toggle:hover { background: rgba(245,245,240,0.22); }
  .text-edit-toggle { bottom: 80px; }

  .caption .n { color: var(--accent); flex-shrink: 0; }

  .strip {
    position: absolute; bottom: 32px; left: 32px; z-index: 30;
    display: flex; gap: 8px; max-width: calc(100vw - 64px); overflow-x: auto; scrollbar-width: none;
    /* double-clicking a thumb (to open the lightbox) is natively a
       "select word" gesture — nothing here stops the browser from actually
       selecting nearby text, which then paints in this site's own
       ::selection colors (a light background, orange-red text) across
       whatever got caught in the selection, including the lightbox close
       button — reads exactly like a stray white film + a highlighted "X" */
    user-select: none;
  }
  .strip::-webkit-scrollbar { display: none; }
  @media (max-width: 480px) {
    .strip { bottom: 20px; left: 16px; gap: 6px; }
    .thumb { width: 40px; height: 40px; flex-shrink: 0; }
  }
  .thumb {
    width: 52px; height: 52px; border-radius: 50%; cursor: pointer; overflow: hidden;
    opacity: 0.55; transition: opacity .2s ease, transform .2s ease;
    /* zoomed in past a plain "cover" fit — YouTube's default thumbnails are
       often letterboxed with black bars baked into the image; scaling past
       100% crops those bars out instead of showing them */
    background-size: 220%; background-position: center; position: relative; flex-shrink: 0;
  }
  .thumb video { width: 100%; height: 100%; object-fit: cover; }
  .thumb:hover { opacity: 0.85; transform: translateY(-2px); }
  .thumb.active { opacity: 1; }

  /* ---------- EDIT PANEL ---------- */
  .panel-backdrop { position: fixed; inset: 0; z-index: 200; background: rgba(13,13,13,0.55); display: none; align-items: center; justify-content: center; padding: 24px; }
  .panel-backdrop.open { display: flex; }

  .lightbox { position: fixed; inset: 0; z-index: 250; background: rgba(0,0,0,0.92); display: none; align-items: center; justify-content: center; padding: 40px; }
  .lightbox.open { display: flex; }
  .lightbox-content { width: min(1000px, 100%); aspect-ratio: 16/9; position: relative; background: #000; overflow: hidden; }
  .lightbox-content video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
  .lightbox-content iframe {
    /* unlike the hero's background loop, this is a deliberate "watch it"
       view — shown as the real embed, uncropped, with native controls
       (see embedUrl's showControls branch), not force-hidden chrome */
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    border: 0;
  }
  .lightbox-close {
    position: absolute; top: 24px; right: 28px; z-index: 260;
    width: 40px; height: 40px; border-radius: 50%; border: 1px solid rgba(245,245,240,0.4);
    background: rgba(245,245,240,0.1); color: var(--ink); font-size: 16px; cursor: pointer;
  }
  .lightbox-close:hover { background: rgba(245,245,240,0.22); }

  /* ---------- "Who?" bio popup ---------- */
  .who-modal {
    position: fixed; inset: 0; z-index: 300;
    background: rgba(13,13,13,0.72);
    display: flex; align-items: center; justify-content: center; padding: 40px;
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity 0.15s ease;
  }
  .who-modal.open { opacity: 1; visibility: visible; pointer-events: auto; }
  .who-modal-close {
    position: fixed; top: 24px; left: 28px; z-index: 310;
    width: 40px; height: 40px; border-radius: 50%; border: 1px solid rgba(13,13,13,0.15);
    background: #f1ede3; color: #0d0d0d; font-size: 16px; cursor: pointer;
  }
  .who-modal-close:hover { background: #e7e2d4; }
  .who-modal-content {
    position: relative;
    background: #f1ede3; color: #0d0d0d; width: min(640px, 100%); max-height: 82vh;
    overflow-y: auto; border-radius: 20px; padding: 128px 44px 44px;
    font-family: 'Neue Montreal', 'Inter', sans-serif;
  }
  .who-eyebrow {
    /* pulled out of flow and tucked into the card's top-left corner —
       higher and further left than the padding box's normal start */
    position: absolute; top: 20px; left: 20px; margin: 0; z-index: 2;
    /* same font stack + tracking as the "WHO?" link in the hero */
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 500; letter-spacing: -0.05em; text-transform: uppercase;
    font-size: clamp(1.6rem, 4vw, 2.2rem); color: #0d0d0d;
  }
  .who-logo-top {
    /* mirrors .who-eyebrow's corner treatment, opposite side */
    position: absolute; top: 20px; right: 20px; margin: 0; z-index: 2;
    height: clamp(44px, 8vw, 58px); width: auto; display: block;
  }
  .who-face {
    display: block; width: 100%; max-width: 380px; aspect-ratio: 16 / 9;
    object-fit: cover; border-radius: 20px; background: #e7e2d4; margin: 0 0 28px;
  }
  .who-modal-content p { font-size: 14px; line-height: 1.7; margin: 0 0 18px 0; color: #2a2a26; }
  .who-modal-content p:last-child { margin-bottom: 0; }
  .who-modal-content ul.who-list { list-style: none; margin: 0 0 18px 0; padding: 0; }
  .who-modal-content ul.who-list li { font-size: 14px; line-height: 1.7; color: #2a2a26; }
  /* homepage's WHO? popup only — mobile, description text a touch smaller */
  @media (max-width: 640px) {
    .home-page .who-modal-content p,
    .home-page .who-modal-content ul.who-list li { font-size: 12px; }
  }
  .who-foot { display: flex; align-items: center; margin: 24px 0 0; }
  .who-mascot { height: 26px; width: auto; display: block; }
  .panel { background: #fff; color: #0d0d0d; width: min(480px, 100%); border-radius: 16px; padding: 28px; max-height: 86vh; overflow-y: auto; }
  .panel h3 { font-family: var(--font-display); letter-spacing: -0.05em; font-size: 20px; margin: 0 0 6px 0; }
  .panel p.hint { font-family: var(--font-mono); font-size: 11px; color: #8a8a85; margin: 0 0 22px 0; line-height: 1.6; }
  .panel-row { margin-bottom: 14px; }
  .panel-row label { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: #8a8a85; margin-bottom: 6px; }
  .remove-link { color: var(--accent); cursor: pointer; text-transform: none; letter-spacing: 0; font-size: 11px; }
  .remove-link:hover { text-decoration: underline; }
  .panel-row input:disabled { opacity: 0.35; }
  .panel-row input[type="text"] { width: 100%; font-family: var(--font-mono); font-size: 13px; padding: 10px 12px; border: 1.5px solid #ececea; border-radius: 8px; outline: none; background: #fafaf8; margin-bottom: 6px; }
  .panel-row input[type="text"]:focus { border-color: var(--accent); }
  .panel-row input[type="file"] { width: 100%; font-family: var(--font-mono); font-size: 11px; color: #8a8a85; }
  .trim-row { display: none; margin: -6px 0 14px 0; padding: 10px; background: #fafaf8; border: 1.5px solid #ececea; border-radius: 8px; }
  .trim-row.active { display: block; }
  .trim-row video { width: 100%; max-height: 140px; border-radius: 6px; margin-bottom: 8px; display: block; background: #000; }
  .trim-row .trim-labels { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 10px; color: #8a8a85; margin-bottom: 4px; }
  .trim-row .trim-sliders { position: relative; height: 20px; }
  .trim-row .trim-sliders input[type="range"] {
    position: absolute; top: 0; left: 0; width: 100%; margin: 0;
    -webkit-appearance: none; appearance: none; background: transparent; pointer-events: none;
  }
  .trim-row .trim-sliders input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none; pointer-events: auto;
    width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; margin-top: 3px;
  }
  .trim-row .trim-sliders input[type="range"]::-moz-range-thumb {
    pointer-events: auto; width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: none;
  }
  .trim-row .trim-track { position: absolute; top: 8px; left: 0; right: 0; height: 4px; background: #ececea; border-radius: 2px; }
  .trim-row .trim-track-fill { position: absolute; top: 0; height: 100%; background: var(--accent); border-radius: 2px; }
  .panel-actions { display: flex; gap: 10px; margin-top: 22px; }
  .panel-btn { font-family: var(--font-mono); font-size: 12px; padding: 11px 18px; border-radius: 40px; border: none; cursor: pointer; text-transform: uppercase; letter-spacing: 0.04em; }
  .panel-btn.apply { background: var(--accent); color: #0d0d0d; }
  .panel-btn.close { background: #f1f1ea; color: #0d0d0d; }
  .panel-status { font-family: var(--font-mono); font-size: 11px; color: #8a8a85; margin-top: 12px; min-height: 16px; }

  .panel-select {
    width: 100%; font-family: var(--font-mono); font-size: 13px; padding: 10px 12px;
    border: 1.5px solid #ececea; border-radius: 8px; outline: none; background: #fafaf8; color: #0d0d0d;
  }
  .panel-row input[type="range"] { width: 100%; accent-color: var(--accent); }
  .segmented { display: flex; gap: 8px; }
  .segmented button {
    flex: 1; font-family: var(--font-mono); font-size: 12px; padding: 9px 10px;
    border: 1.5px solid #ececea; border-radius: 8px; background: #fafaf8; color: #6b6b66; cursor: pointer;
  }
  .segmented button.active { background: var(--accent); border-color: var(--accent); color: #0d0d0d; }

  .text-toolbar {
    display: none; position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
    z-index: 210; background: #0d0d0d; color: #f5f5f0; border-radius: 40px;
    padding: 10px 16px; gap: 14px; align-items: center; flex-wrap: wrap;
    font-family: var(--font-mono); font-size: 11px; max-width: min(720px, 92vw);
    box-shadow: 0 20px 40px -12px rgba(0,0,0,0.4);
  }
  .text-toolbar.open { display: flex; }
  .tb-select {
    font-family: var(--font-mono); font-size: 11px; padding: 6px 8px; border-radius: 6px;
    border: 1px solid rgba(245,245,240,0.3); background: rgba(245,245,240,0.1); color: #fff;
  }
  .tb-field { display: flex; align-items: center; gap: 6px; color: rgba(245,245,240,0.7); }
  .tb-field input[type="range"] { width: 70px; accent-color: var(--accent); }
  .tb-seg { display: flex; gap: 4px; }
  .tb-seg button {
    font-family: var(--font-mono); font-size: 10px; padding: 5px 8px; border-radius: 6px;
    border: 1px solid rgba(245,245,240,0.3); background: rgba(245,245,240,0.1); color: #fff; cursor: pointer;
  }
  .tb-seg button.active { background: var(--accent); border-color: var(--accent); color: #0d0d0d; }
  .tb-btn {
    font-family: var(--font-mono); font-size: 11px; padding: 7px 12px; border-radius: 40px;
    border: none; cursor: pointer; background: rgba(245,245,240,0.14); color: #fff;
  }
  .tb-done { background: var(--accent); color: #0d0d0d; font-weight: 600; }

  .bio-editing [contenteditable="true"] { outline: none; }
  .bio-editing .bio-center p {
    cursor: text; border-radius: 4px; transition: background .15s ease;
  }
  .bio-editing .bio-center p:hover {
    background: rgba(255,78,0,0.08); outline: 1px dashed rgba(255,78,0,0.5); outline-offset: 3px;
  }
  .bio-editing .bio-center p.bio-active-p {
    background: rgba(255,78,0,0.14); outline: 1.5px dashed rgba(255,78,0,0.85); outline-offset: 3px;
  }

  /* ---------- BODY / CONTENT ---------- */
  .body-section { max-width: 1400px; margin: 0 auto; padding: 96px 24px 100px; }
  .bio-center { margin-top: 8px; }
  .bio-center p { font-family: 'Neue Montreal', 'Inter', sans-serif; font-size: 22px; font-weight: 400; letter-spacing: 0em; line-height: 1.6; color: #0d0d0d; margin: 0 auto 10px; max-width: 640px; text-align: center; }

  /* ---------- INTRO TEXT: 3-block editorial masthead (left / center /
     right), rather than one centered line ---------- */
  .intro-editorial {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: 56px; max-width: 900px; margin: 0 auto;
  }
  /* ---------- INTRO TEXT SECTION: connects hero to the rest of the page ---------- */
  .intro-text-section { width: 100%; padding: 110px 24px 32px; }
  .carousel-surround p.bio-lead {
    position: absolute; z-index: 5; pointer-events: none;
    font-family: 'Bootzy TM', var(--font-display); font-weight: 400; text-transform: uppercase;
    letter-spacing: 0.06em; font-size: clamp(15px, 1.8vw, 24px); line-height: 1.15;
    color: #0d0d0d; margin: 0;
  }
  .carousel-surround { position: relative; padding-bottom: 70px; margin-top: 240px; }
  .bio-center p.carousel-hint {
    position: absolute; top: 55%; left: 50%; transform: translate(-50%, -50%);
    z-index: 3; pointer-events: none; width: max-content; max-width: 80%;
    font-family: 'Bootzy TM', var(--font-display); font-size: 11px; letter-spacing: 0.08em;
    text-transform: uppercase; color: #8a8a84; text-align: center; margin: 0;
  }
  .carousel-surround p.surround-tl { top: 24px; left: 24px; text-align: left; }
  .carousel-surround p.surround-tr { top: 24px; right: 24px; text-align: right; }
  .carousel-surround p.surround-bc { bottom: 48px; left: 50%; transform: translateX(-50%); text-align: center; }
  @media (max-width: 980px) {
    .carousel-surround p.bio-lead { display: none; } /* no room to surround the carousel once it narrows — revisit if wanted */
  }

  .bio-logo { margin: 0 0 24px 0 !important; }
  .bio-logo img { height: 54px; width: auto; display: inline-block; filter: invert(1); }
  /* ---------- WORK CAROUSEL (real 3D GLB models revolving in a ring) ---------- */
  .work-carousel {
    position: relative;
    width: 100%; max-width: 1350px;
    height: 520px;
    margin: 0 auto 56px;
    cursor: none;
    touch-action: pan-y;
  }
  .work-carousel.is-dragging { cursor: none; }
  .work-carousel canvas { display: block; width: 100%; height: 100%; }
  .work-carousel-loading {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-family: 'JetBrains Mono', monospace; font-size: 11px;
    letter-spacing: 0.05em; color: #8a8a84;
  }
  @media (max-width: 640px) {
    .work-carousel { height: 260px; }
    /* smaller intro text and much less space before the carousel — the
       240px gap above was sized to leave room for the bio-lead surround
       captions (surround-tl/tr/bc), which are already hidden below 980px,
       so on mobile it was just dead space */
    .bio-center p { font-size: 16px; }
    .bio-center p.bio-craft-line { font-size: 13px; }
    .bio-center p.carousel-hint { font-size: 10px; }
    .carousel-surround { margin-top: 24px; }
  }
  .bio-logo-inline { height: 3em; width: auto; display: inline-block; filter: none; vertical-align: middle; }
  .bio-logo-badge {
    display: inline-flex; align-items: center; justify-content: center;
    margin: 0 0.4em 0 0.15em; vertical-align: middle; position: relative; top: -0.05em;
  }
  .bio b { color: #0d0d0d; font-weight: 600; }
  /* ---------- CLIENTS MARQUEE (brand logos, slow auto-scroll right→left) ---------- */
  /* ---------- SCROLL REVEAL (fade + rise as sections enter view) ---------- */
  .reveal {
    opacity: 0; transform: translateY(28px);
    transition: opacity 2.4s cubic-bezier(.16,.84,.44,1), transform 2.4s cubic-bezier(.16,.84,.44,1);
  }
  .reveal.is-visible { opacity: 1; transform: none; }

  .reveal-stagger > * {
    opacity: 0; transform: translateY(20px);
    transition: opacity 1.8s cubic-bezier(.16,.84,.44,1), transform 1.8s cubic-bezier(.16,.84,.44,1);
  }
  .reveal-stagger.is-visible > * { opacity: 1; transform: none; }
  .reveal-stagger.is-visible > *:nth-child(1) { transition-delay: 0.00s; }
  .reveal-stagger.is-visible > *:nth-child(2) { transition-delay: 0.18s; }
  .reveal-stagger.is-visible > *:nth-child(3) { transition-delay: 0.36s; }
  .reveal-stagger.is-visible > *:nth-child(4) { transition-delay: 0.54s; }
  .reveal-stagger.is-visible > *:nth-child(5) { transition-delay: 0.72s; }
  .reveal-stagger.is-visible > *:nth-child(6) { transition-delay: 0.90s; }
  .reveal-stagger.is-visible > *:nth-child(7) { transition-delay: 1.08s; }
  .reveal-stagger.is-visible > *:nth-child(8) { transition-delay: 1.26s; }

  .reveal-blur {
    opacity: 0; filter: blur(16px); transform: scale(1.04);
    transition: opacity 2.7s cubic-bezier(.16,.84,.44,1), filter 2.7s cubic-bezier(.16,.84,.44,1), transform 2.7s cubic-bezier(.16,.84,.44,1);
  }
  .reveal-blur.is-visible { opacity: 1; filter: blur(0); transform: scale(1); }

  .clients-section { overflow: hidden; padding: 100px 0 50px; max-width: none; width: 100%; }
  .clients-track {
    display: flex; align-items: center; gap: 88px; width: max-content;
    animation: clientsScroll 55s linear infinite;
  }
  @keyframes clientsScroll {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
  }
  .client-logo { flex-shrink: 0; height: 58px; display: flex; align-items: center; justify-content: center; }
  .client-logo img {
    height: 100%; width: auto; object-fit: contain;
    filter: grayscale(1); opacity: 0.55;
    transition: opacity .2s ease, filter .2s ease;
  }
  .client-logo:hover img { filter: grayscale(0); opacity: 1; }

  /* individually calibrated — wide wordmarks (Toyota, Atome, Spotify, Grab)
     read much bigger than compact marks (KFC, Hong Leong) at the same
     height, since their footprint is horizontal rather than vertical */
  .client-logo[data-brand="toyota"] { height: 32px; }
  .client-logo[data-brand="atome"] { height: 34px; }
  .client-logo[data-brand="spotify"] { height: 38px; }
  .client-logo[data-brand="grab"] { height: 42px; }
  .client-logo[data-brand="kfc"] { height: 88px; }
  .client-logo[data-brand="hong-leong-bank"] { height: 82px; }
  .client-logo-fallback {
    display: none;
    font-family: var(--font-display); font-weight: 700; font-size: 19px;
    letter-spacing: -0.02em; text-transform: uppercase; white-space: nowrap;
    color: #0d0d0d; opacity: 0.45; transition: opacity .2s ease;
  }
  .client-logo:hover .client-logo-fallback { opacity: 0.8; }

  /* "Experience" heading, sitting between the bio and the launchpad below
     it — large hand-drawn title with a small direction line under it.
     No scroll-reveal animation from here down the page (title, direction
     line, launchpad, craft note, contact section all render immediately,
     full opacity, no transition) — everything above this point (bio
     intro, client marquee) keeps its own reveal untouched. */
  .section-heading { text-align: center; margin: 130px auto 28px; padding: 0 24px; }
  .section-heading-title {
    /* same face as "Names We've Worked With" (.artist-clients-label) */
    font-family: 'Bootzy TM', var(--font-display); font-weight: 400;
    font-size: clamp(32px, 6vw, 56px); letter-spacing: 0.03em; line-height: 1;
    color: #0d0d0d; margin: 0;
  }
  .section-heading-direction {
    font-family: 'Bootzy TM', var(--font-display); font-size: 11px; letter-spacing: 0.08em;
    text-transform: uppercase; color: #8a8a84; text-align: center; margin: 14px 0 0;
  }
  /* "1" and "8" boxed like they were lifted straight off a keyboard */
  .key-cap {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 1.6em; height: 1.6em; padding: 0 0.2em; margin: 0 0.05em;
    border: 1px solid currentColor; border-radius: 4px;
    font-family: inherit; font-size: inherit; letter-spacing: 0;
    line-height: 1; vertical-align: middle;
  }
  /* the "press 1-8" copy only makes sense with a physical keyboard — swap
     it for the tap-first phrasing on touch-only devices (no hover, coarse
     pointer), rather than guessing from viewport width, since a touch
     tablet can still be plenty wide */
  .section-heading-direction-mobile { display: none; }
  @media (hover: none) and (pointer: coarse) {
    .section-heading-direction-desktop { display: none; }
    .section-heading-direction-mobile { display: block; }
  }
  .pad-section { max-width: 900px; margin: 0 auto 0; padding: 0 24px 40px; }
  .pad-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
  @media (max-width: 560px) { .pad-grid { grid-template-columns: repeat(2, 1fr); } }
  .pad {
    position: relative;
    aspect-ratio: 1 / 1;
    border: none;
    border-radius: 18px;
    background: color-mix(in srgb, var(--pad-color) 14%, #fff);
    color: var(--pad-color);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: clamp(20px, 3vw, 28px);
    cursor: pointer;
    overflow: hidden;
    transition: transform .08s ease, background .15s ease, box-shadow .15s ease;
    box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--pad-color) 30%, transparent);
    -webkit-tap-highlight-color: transparent;
  }
  .pad:hover { background: color-mix(in srgb, var(--pad-color) 24%, #fff); }
  .pad:active,
  .pad.is-active {
    transform: scale(0.92);
    background: var(--pad-color);
    color: #fff;
    box-shadow: 0 0 0 0 transparent, 0 6px 18px color-mix(in srgb, var(--pad-color) 55%, transparent);
  }
  .pad::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 18px;
    background: var(--pad-color);
    opacity: 0;
    transform: scale(0.6);
    pointer-events: none;
  }
  .pad.is-pulsing::after {
    animation: padPulse .5s ease-out;
  }

  /* the real 3D launchpad (scripts/launchpad.mjs) replaces the flat CSS pad
     grid — the stage is the canvas's own box; the 8 real .pad buttons sit
     absolutely positioned on top of it and get moved every frame to track
     8 points on the model's actual rendered top grid, so they read as
     invisible hit-zones on the real buttons rather than a separate UI. */
  .launchpad-stage {
    position: relative; width: 100%;
    /* squarer than before — the camera sits closer to top-down now, so the
       model reads as roughly square rather than a wide letterboxed strip */
    aspect-ratio: 1 / 1; max-width: 640px; margin: 0 auto;
    /* lets the .pad circles below size themselves off the stage's own
       rendered box (cqw) rather than the viewport — see the comment on
       .artists-page .pad for why that distinction actually matters here */
    container-type: inline-size;
  }
  .launchpad-canvas {
    position: absolute; inset: 0; width: 100%; height: 100%; display: block;
    touch-action: none; cursor: grab;
  }
  .launchpad-canvas.is-dragging { cursor: grabbing; }
  /* pointer-events:none is the important part here — without it this div's
     own full-size box (it covers the whole stage, inset:0) swallows every
     pointerdown before it ever reaches the canvas underneath, which is why
     drag-to-rotate silently did nothing anywhere except directly on one of
     the 8 pad circles below (which opt back into pointer-events:auto). */
  .artists-page .pad-grid { position: absolute; inset: 0; display: block; pointer-events: none; }
  .artists-page .pad {
    position: absolute; top: 0; left: 0; transform: translate(-50%, -50%);
    /* sized off the viewport (vw) as a flat fallback for browsers without
       container query units — see the cqw-based override just below for
       why the real site relies on that instead */
    width: clamp(22px, 5vw, 34px); aspect-ratio: 1 / 1;
    border-radius: 50%; pointer-events: auto;
    display: flex; align-items: center; justify-content: center;
    /* idle state: a soft colored ring sitting right on the model's real
       button — a light tint rather than a solid fill, so the rendered 3D
       button still reads through it — with its number shown in that same
       color, so which key is which (and what it'll light up) is obvious
       before anyone presses anything, not just after */
    background: color-mix(in srgb, var(--pad-color) 16%, transparent);
    box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--pad-color) 55%, transparent);
    font-family: var(--font-display); font-weight: 700;
    font-size: clamp(10px, 2.4vw, 13px); color: var(--pad-color);
  }
  /* the 8 pads are re-positioned every frame to track 8 real points on the
     model (scripts/launchpad.mjs), and that on-screen spacing is a stable
     ~5.7% of the stage's own rendered width at any size — it comes from
     the model's fixed camera framing, not from the viewport. Sizing pads
     off vw (above) instead of that meant they tracked viewport growth
     independently of the model, and once the stage hit its own 640px cap
     the pads kept growing past it — overlapping into each other instead
     of stopping. cqw ties pad size to the stage's actual rendered size
     instead, so they stay a fixed, comfortable fraction of that ~5.7%
     spacing at every breakpoint, mobile through desktop. */
  @supports (width: 1cqw) {
    .artists-page .pad {
      width: clamp(12px, 4cqw, 26px);
      font-size: clamp(8px, 1.9cqw, 10px);
    }
  }
  .artists-page .pad:hover { background: color-mix(in srgb, var(--pad-color) 30%, transparent); }
  .artists-page .pad:active { transform: translate(-50%, -50%) scale(0.88); }
  /* .is-on is a toggle, not a transient press — it stays lit until the
     same pad is pressed again ("mute/unmute", not a one-shot pluck) */
  .artists-page .pad.is-on {
    background: var(--pad-color); color: #fff;
    box-shadow: 0 0 16px 3px color-mix(in srgb, var(--pad-color) 70%, transparent);
  }
  .artists-page .pad::after { border-radius: 50%; }
  /* touch-only devices: same colored dot, no number — there's no keyboard
     to match a printed digit to anyway, so the number is just clutter.
     any-pointer (not plain pointer) so this also catches hybrids — a
     touchscreen laptop or an iPad with a keyboard case, where the PRIMARY
     input can still report pointer:fine/hover:hover even while someone's
     actually tapping the screen — matching the same check the launchpad
     zoom itself uses (below), so the two stay consistent. */
  @media (any-pointer: coarse) {
    .artists-page .pad { font-size: 0; color: transparent; }
  }
  @keyframes padPulse {
    0% { opacity: .5; transform: scale(0.6); }
    100% { opacity: 0; transform: scale(1.6); }
  }

  /* touch-only: the first tap anywhere on the launchpad (canvas or a pad)
     dives into a big flat 3x3 grid of the same 8 pads instead of
     registering that tap as a press — see launchpad.mjs, which builds the
     grid and wires each circle to its real <button class="pad">. The
     launchpad itself rushes forward, blurring and fading (.is-diving),
     while a solid backdrop — the page's own background, not a dark scrim —
     fades in behind the grid as it swells into place a beat later, so it
     reads as one continuous push through the model into its own space
     rather than a menu opening on top of the page. Closing reverses all of
     it and does NOT touch any pad's on/off state — whatever's left playing
     shows up back on the model via the row-highlight sync above, same as
     if it'd been pressed there directly. */
  @media (any-pointer: coarse) {
    .launchpad-stage {
      transition: opacity 0.55s cubic-bezier(.4,0,1,1), transform 0.6s cubic-bezier(.4,0,1,1), filter 0.55s ease;
    }
    .launchpad-stage.is-diving {
      opacity: 0; transform: scale(2.4); filter: blur(18px); pointer-events: none;
    }
  }
  .pad-zoom-backdrop {
    position: fixed; inset: 0; z-index: 398;
    background: #f1ede3;
    opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity 0.4s ease, visibility 0s linear 0.4s;
  }
  .pad-zoom-backdrop.is-active {
    opacity: 1; visibility: visible; pointer-events: auto;
    transition: opacity 0.45s ease;
  }
  .pad-zoom-overlay {
    /* sits a bit above dead-center (42% not 50%) so the reminder + grid
       group reads as raised rather than pinned to the exact middle */
    position: fixed; z-index: 400; top: 42%; left: 50%;
    display: flex; flex-direction: column; align-items: center;
    opacity: 0; visibility: hidden; pointer-events: none;
    transform: translate(-50%, -50%) scale(0.4);
    filter: blur(10px);
    transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease, visibility 0s linear 0.3s;
  }
  /* "unmute your device" reminder, sitting above the grid — same Bootzy
     label family as the page's other instructional lines
     (.section-heading-direction, "TAP EACH COLORED PAD…"), just larger
     and more tightly tracked than that 11px/0.4em line */
  .pad-zoom-reminder {
    font-family: 'Bootzy TM', var(--font-display); font-size: 18px; letter-spacing: 0.15em;
    text-transform: uppercase; color: #8a8a84; text-align: center;
    margin: 0 0 26px;
  }
  .pad-zoom-overlay.is-active {
    opacity: 1; visibility: visible; pointer-events: auto;
    transform: translate(-50%, -50%) scale(1);
    filter: blur(0);
    /* starts a beat after the launchpad's own rush-forward begins, so the
       grid reads as "arriving" once that motion has actually gone
       somewhere, not as a simultaneous cross-fade with it */
    transition: opacity 0.5s cubic-bezier(.34,1.56,.64,1) 0.2s,
      transform 0.5s cubic-bezier(.34,1.56,.64,1) 0.2s,
      filter 0.4s ease 0.2s;
  }
  .pad-zoom-grid {
    display: grid; grid-template-columns: repeat(3, 1fr);
    /* gap scales with viewport (not a fixed 24px) specifically so the
       lit-circle glow below has room to breathe instead of merging into
       its neighbours once several pads are on at once */
    gap: clamp(18px, 5.5vw, 34px);
    width: min(80vw, 64vh);
    max-width: 420px;
  }
  .pad-zoom-circle {
    aspect-ratio: 1 / 1; border-radius: 50%; border: none; cursor: pointer;
    background: color-mix(in srgb, var(--pad-color) 16%, transparent);
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--pad-color) 55%, transparent);
    font-size: 0; color: transparent; /* touch-only context, same as the real pads — no keyboard to match a digit to */
    transition: transform .08s ease, background .15s ease;
  }
  .pad-zoom-circle:active { transform: scale(0.9); }
  .pad-zoom-circle.is-on {
    background: var(--pad-color);
    /* smaller glow than before — the old 22px/4px halo reached past the
       gap and visually fused with an adjacent lit circle */
    box-shadow: 0 0 12px 2px color-mix(in srgb, var(--pad-color) 65%, transparent);
  }
  .pad-zoom-close {
    aspect-ratio: 1 / 1; border-radius: 50%; cursor: pointer;
    background: #f1ede3; border: 1px solid rgba(13,13,13,0.15);
    color: #0d0d0d; font-size: clamp(16px, 4vw, 22px);
  }
  /* tablet / larger touch screens: vw alone would blow the grid up huge
     (a wide-but-not-tall viewport), which is what made the fixed gap look
     cramped relative to the circles — cap it and rely on gap's own clamp */
  @media (min-width: 620px) {
    .pad-zoom-grid { width: min(56vw, 58vh); max-width: 480px; }
  }
  @media (min-width: 1000px) {
    .pad-zoom-grid { width: min(36vw, 50vh); max-width: 460px; }
  }

  .contact { text-align: center; padding: 90px 24px 40px; border-top: 1px solid #ececea; }
  .contact h2, .contact-heading { font-family: 'Bootzy TM', var(--font-display); letter-spacing: 0.05em; line-height: 1.1; margin: 0 0 28px 0; color: #0d0d0d; }
  /* "HOLLA AT" and the logo now sized independently — the logo no longer
     scales off the text's font-size, so shrinking one doesn't shrink both */
  .contact-heading { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.1em; }
  .contact-heading .cta-line { display: block; font-size: clamp(1.05rem, 2.1vw, 1.4rem); }
  .contact-heading .cta-logo { height: clamp(3.3rem, 7vw, 4.9rem); width: auto; display: block; }
  .contact-heading-link { text-decoration: none; display: inline-block; transition: opacity .2s ease; }
  .contact-heading-link:hover { opacity: 0.6; }
  /* small emphasis pop played once, when the mascot link scrolls this CTA
     into view — a quick overshoot-and-settle rather than a plain bounce,
     so it reads as a nudge of attention rather than a wobble */
  @keyframes ctaPop {
    0% { transform: scale(1); }
    40% { transform: scale(1.1); }
    65% { transform: scale(0.96); }
    100% { transform: scale(1); }
  }
  .contact-heading-link.cta-pop { animation: ctaPop .5s cubic-bezier(.34,1.56,.64,1); }
  @media (prefers-reduced-motion: reduce) {
    .contact-heading-link.cta-pop { animation: none; }
  }
  .socials { display: flex; align-items: center; gap: 16px; justify-content: center; margin-top: 32px; color: #6b6b66; }
  .socials a { text-decoration: none; display: inline-flex; }
  .socials a:hover { color: #0d0d0d; }
  /* same quiet hint treatment used sitewide for small print — the
     disclaimer note on index.html, "VIEW ALL WORKS", "SELECT AN ASSET TO
     PREVIEW" — Bootzy TM, tracked-out uppercase, muted grey */
  .credits {
    text-align: right; padding: 24px 28px;
    font-family: 'Bootzy TM', var(--font-display); font-size: 11px;
    letter-spacing: 0.08em; text-transform: uppercase; color: #8a8a84;
  }
  @media (max-width: 640px) {
    .credits { font-size: 9px; padding: 16px 20px; }
  }
  /* same font treatment as the bio text (.bio-intro-line) — sits between
     the launchpad and the contact section. The launchpad's own square
     canvas box (.launchpad-stage) has a lot of empty transparent space
     below the rendered model itself (the model doesn't fill its own
     bounding box), so a plain small margin here still reads as a big gap
     below the model visually — pulled up with a negative margin instead,
     tuned to sit close under the model without overlapping the pads. */
  .craft-note {
    text-align: center; margin: -90px auto 0; padding: 0 24px; line-height: 1.7;
    font-family: 'Neue Montreal', 'Inter', sans-serif; font-size: 13px;
    font-weight: 400; color: #4a4a44; position: relative; z-index: 1;
  }
  @media (max-width: 640px) {
    .craft-note { font-size: 11px; margin-top: -130px; }
  }
  @media (max-width: 480px) {
    /* the launchpad's own square canvas box scales down with viewport
       width (it's max-width:640px, not fixed), and the empty space below
       the rendered model shrinks right along with it — the 640px-tier
       pull above overshoots into the model at true phone widths. Given
       some breathing room instead of pulled tight, per feedback that it
       read as too cramped right under the launchpad on mobile. */
    .craft-note { margin-top: 28px; }
  }

  /* ---------- Artists page: oval "old TV" hero, bio-first ---------- */
  body.artists-page { background: #f1ede3; }

  /* logo moves from top-center to top-left now that "Who?" is gone from
     .page-top; mascot moves from top-right to top-center. Both swap to the
     dark logo/icon (no invert) since this page sits on the cream background,
     not the dark hero it was originally positioned/colored for. */
  .artists-page .mark { top: 20px; left: 28px; transform: none; }
  /* left is 50% + 10px, not a plain 50% — .mascot's own hit-area trick
     (padding:10px paired with margin:-10px, base rule above) pulls the
     border box 10px left of wherever `left` points once a negative margin
     is in play, so a plain 50% actually renders the icon 10px off-center.
     This nudges the CSS anchor point that same 10px back the other way to
     cancel it out — confirmed by comparing the mascot's rendered center
     against the viewport's before and after. Without it, the icon lands
     dead-center only while the homepage hand-off overlay's decoy mascot
     (a plain, unpadded img, so it doesn't hit this) is still covering it,
     then visibly hops left by that same 10px the moment the real one
     underneath is revealed. */
  .artists-page .mascot { top: 20px; right: auto; left: calc(50% + 10px); transform: translateX(-50%); }
  .artists-page .mascot-icon { filter: none; }

  /* a gentle in-place float — no horizontal drift, just a slow bob + light
     rotate, looping forever. .mascot's own left/translateX(-50%) above
     is what keeps it centered; the keyframes re-include that -50% every
     step and just add a small translateY on top of it. */
  .artists-page .mascot { animation: artistsMascotFloat 4s ease-in-out infinite; }
  @keyframes artistsMascotFloat {
    0%, 100% { transform: translateX(-50%) translateY(0) rotate(-4deg); }
    50% { transform: translateX(-50%) translateY(-10px) rotate(4deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .artists-page .mascot { animation: none; }
  }

  /* opening statement — its own beat right after the hero, centered, three
     stacked phrases: a bold lead line (the real logo swapped in for the
     wordmark, same badge treatment brands.html uses) into two smaller,
     softer-colored lines — ahead of the gif/scattered bio below. */
  .bio-intro { padding: 100px 28px 20px; text-align: center; }
  .bio-intro-text {
    display: flex; flex-direction: column; align-items: center; gap: 10px;
    margin: 0 auto; max-width: 640px;
    font-family: 'Neue Montreal', 'Inter', sans-serif;
  }
  .bio-intro-line { margin: 0; line-height: 1.3; }
  /* wraps everything past "is the medium." — same flex-column rhythm as
     .bio-intro-text itself, just as its own reveal-stagger group so this
     part specifically fades in on scroll while the first line doesn't */
  .bio-intro-rest { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
  .bio-intro-line-1 {
    display: flex; align-items: center; justify-content: center;
    font-size: 22px; font-weight: 500; letter-spacing: -0.01em; color: #0d0d0d;
    margin-bottom: 16px;
  }
  /* plain fade, once the hero-revealed gate lets this line start (main.js)
     — slower than the generic .reveal (2.4s), since this one shares the
     first scene with the hero and shouldn't rush in right alongside it. */
  .bio-intro-line-1.reveal {
    transition-duration: 3s;
  }
  .bio-intro-line:not(.bio-intro-line-1) {
    font-size: 14px; font-weight: 400; color: #4a4a44;
  }
  /* two separate copies of the "An audio house.. one belief remains:" text
     (see the HTML comment next to them) — only one ever shown, toggled by
     this same breakpoint. Kept as two whole separate reveal-stagger groups
     rather than one shared set of paragraphs with breakpoint-toggled <br>s,
     so each keeps its own clean nth-child stagger timing regardless of
     which one is hidden. */
  .bio-intro-rest-mobile { display: none; }
  @media (max-width: 900px) {
    .bio-intro { padding: 90px 24px 10px; }
    .bio-intro-line-1 { font-size: 18px; }
    /* smaller on mobile — the gap down to the gif below (.bio-feature's own
       top padding) is untouched by this, so the two stay just as far apart */
    .bio-intro-line:not(.bio-intro-line-1) { font-size: 13px; }
    .bio-intro-rest-desktop { display: none; }
    /* more breathing room between the two paragraphs than the 10px default
       (.bio-intro-rest) — they read as two distinct beats now, not just
       consecutive lines of one block */
    .bio-intro-rest-mobile { display: flex; gap: 28px; }
  }

  /* the bio pulled out of the "Who?" popup — gif, then the two text lines,
     stacked and centered. A plain flex column on every width now (this used
     to be a pixel-positioned "scatter" around a since-removed carousel,
     which is why the class names still say "scatter" — nothing left to
     scatter around any more, so a normal stack is simpler and doesn't need
     re-tuning every time a line is added, removed, or changes length). */
  .bio-feature { padding: 40px 28px 60px; }
  .bio-feature-scatter {
    display: flex; flex-direction: column; align-items: center; gap: 32px;
    max-width: 1080px; margin: 0 auto;
  }
  .bio-feature-gif {
    width: min(360px, 38vw); aspect-ratio: 16 / 9; object-fit: cover;
    display: block; border-radius: 20px;
    box-shadow: 0 24px 60px rgba(13,13,13,0.18);
  }
  .bio-scatter {
    margin: 0; max-width: 520px; text-align: center;
    font-family: 'Neue Montreal', 'Inter', sans-serif; font-size: 16px; line-height: 1.7; color: #0d0d0d;
  }
  .bio-scatter-2 {
    /* same treatment as the intro statement's "founded by..." lines */
    font-size: 17px; font-weight: 400; color: #4a4a44;
  }
  @media (max-width: 900px) {
    /* more space above the gif specifically — was 14px, sat right under
       "After a decade.. one belief remains:" with barely a gap */
    .bio-feature { padding: 50px 24px 30px; }
    .bio-feature-gif { width: min(220px, 60vw); }
    .bio-scatter-2 { font-size: 15px; }
  }

  /* ---------- artist credits: names ringed around the mascot ---------- */
  .artist-clients { position: relative; max-width: 640px; margin: 0 auto; padding: 100px 24px 40px; text-align: center; }
  .artist-clients-label-row {
    display: flex; align-items: center; justify-content: center; gap: 18px;
    margin: 0 0 28px;
  }
  .artist-clients-label { margin: 0; text-align: center;
    font-family: 'Bootzy TM', var(--font-display); font-weight: 400; font-size: clamp(24px, 4vw, 36px);
    line-height: 1.05; letter-spacing: 0.03em; color: #0d0d0d;
  }
  .artist-clients-flower { flex: none; }
  .artist-clients-flower--right { transform: scaleX(-1); }
  @media (max-width: 560px) {
    .artist-clients-label-row { gap: 8px; }
    .artist-clients-flower { width: 22px; height: 19px; }
  }
  /* a fixed square stage, mascot dead center, names placed at even angles
     around it (each --i/--n pair below) like a halo it casts — the ring's
     radius (the translateY distance in .artist-clients-circle-name) is
     comfortably clear of the mascot itself, so nothing needs a backdrop to
     mask overlap this time */
  .artist-clients-circle {
    position: relative; width: 340px; height: 340px; max-width: 100%;
    margin: 0 auto;
  }
  .artist-clients-circle-mascot {
    position: absolute; top: 50%; left: 50%; z-index: 2;
    width: 130px; height: auto;
    animation: artistClientsMascotFloat 3.6s ease-in-out infinite;
  }
  @keyframes artistClientsMascotFloat {
    0%, 100% { transform: translate(-50%, -50%) rotate(-4deg); }
    50% { transform: translate(-50%, -52%) rotate(4deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    .artist-clients-circle-mascot { animation: none; }
  }
  .artist-clients-circle-name {
    --n: 15;
    /* .artist-clients-circle-name--long can push a name out further via
       this, if a future reorder ever needs the extra clearance again — at
       the current --i order it isn't needed at either breakpoint (both
       radii below are the tightest value found, by brute-force search
       across every adjacent pair, that still keeps a couple px of clear
       air between every name — going any tighter starts touching) */
    --extra: 0px;
    position: absolute; top: 50%; left: 50%; z-index: 1;
    transform: translate(-50%, -50%) rotate(calc(360deg / var(--n) * var(--i))) translateY(calc(-113px - var(--extra))) rotate(calc(-360deg / var(--n) * var(--i)));
    font-family: 'Neue Montreal', 'Inter', sans-serif; font-weight: 400; font-size: 14px;
    letter-spacing: -0.02em; word-spacing: -2px; color: #6b6b66; white-space: nowrap;
  }
  .artist-clients-circle-name--long { --extra: 0px; }
  @media (max-width: 560px) {
    .artist-clients-circle { width: 250px; height: 250px; }
    .artist-clients-circle-mascot { width: 90px; }
    .artist-clients-circle-name {
      font-size: 11px; word-spacing: -1px;
      transform: translate(-50%, -50%) rotate(calc(360deg / var(--n) * var(--i))) translateY(calc(-89px - var(--extra))) rotate(calc(-360deg / var(--n) * var(--i)));
    }
    .artist-clients-circle-name--long { --extra: 0px; }
  }

  /* hero reshaped into a horizontal oval frame, like an old CRT set —
     the strip/nav move off the curved corners into the flat middle so
     nothing sits under the clipped edge of the oval */
  .artists-page .slider {
    width: min(1040px, 92vw); height: min(56vh, 480px); min-height: 340px;
    margin: 80px auto 60px; border-radius: 50% / 18%;
  }
  /* overflow:hidden + border-radius on .slider alone doesn't clip <video>/
     <iframe> elements in most browsers — they composite outside the
     ancestor's rounded mask. Applying the same radius directly on .slide
     and its video/iframe children is what actually makes the oval stick. */
  .artists-page .slide,
  .artists-page .slide video,
  .artists-page .slide iframe { border-radius: 50% / 18%; }
  .artists-page .strip { left: 50%; bottom: 22px; transform: translateX(-50%); }
  .artists-page .top-right-nav { right: auto; left: 50%; bottom: auto; top: 20px; transform: translateX(-50%); }
  .artists-page .edit-toggle { right: auto; left: 50%; transform: translateX(-50%); }
  .artists-page .text-edit-toggle { bottom: 100px; }

  /* hero "TV startup": starts as a thin vertical sliver at dead-center and
     widens out to both sides, like an old CRT warming up — held collapsed
     by default so there's no flash of the full hero before the animation
     runs; main.js adds .tv-startup once (timed to line up with the
     homepage hand-off overlay's fade-out, if that's how this page loaded). */
  .artists-page .slider { clip-path: inset(0 50% 0 50%); opacity: 0; }
  .artists-page .slider.tv-startup {
    animation: heroTvStartup 1.3s cubic-bezier(.16,.84,.44,1) forwards;
  }
  @keyframes heroTvStartup {
    0% { clip-path: inset(0 50% 0 50%); opacity: 0.4; }
    100% { clip-path: inset(0 0 0 0); opacity: 1; }
  }
  @media (prefers-reduced-motion: reduce) {
    .artists-page .slider { clip-path: none; opacity: 1; animation: none; }
  }

  /* homepage hand-off overlay: sits above everything, starting as the same
     big, centered wordmark the homepage had (a hard cut between the two
     reads as continuous, since they match). See the inline script right
     after <body> for how it's shown, and main.js for the hold, the flight
     down to the real .mark logo's spot, and the fade-out timing. */
  .hero-transition-overlay {
    position: fixed; inset: 0; z-index: 500;
    background: #f1ede3;
    display: flex; align-items: center; justify-content: center;
    pointer-events: none;
    opacity: 1; transition: opacity 0.9s ease;
  }
  .hero-transition-overlay img { height: clamp(70px, 12vw, 190px); width: auto; display: block; }
  /* main.js switches the image to this once it starts flying — frozen to
     its current on-screen spot via explicit top/left/width/height (set
     just before this class is added) rather than the flexbox centering
     above, so those four properties can actually be transitioned. Duration
     here must match main.js's flyMs, which gates when the fade-out (above)
     starts. */
  .hero-transition-overlay img.flying {
    position: fixed; margin: 0;
    transition: top 0.85s cubic-bezier(.4,0,.2,1), left 0.85s cubic-bezier(.4,0,.2,1),
      width 0.85s cubic-bezier(.4,0,.2,1), height 0.85s cubic-bezier(.4,0,.2,1);
  }
  .hero-transition-overlay.fade-out { opacity: 0; }
  @media (prefers-reduced-motion: reduce) {
    .hero-transition-overlay { transition: none; }
    .hero-transition-overlay img.flying { transition: none; }
  }

  /* a decoy mascot living inside the overlay itself (see the HTML comment
     next to it, on both artists.html and brands.html) — already at the
     overlay's own elevated stacking level, so no z-index tug-of-war with
     .page-top's own stacking context (which is what made raising the REAL
     mascot's z-index alone ineffective — a descendant can never climb
     higher than the ceiling its own stacking-context-establishing ancestor
     allows, regardless of its own z-index value). Positioned/sized/filtered
     per page to match that page's real mascot exactly, so the swap to the
     real one underneath (once this whole overlay fades out and is removed)
     is invisible. */
  .hero-transition-overlay .mascot-handoff { position: absolute; height: 25px; width: auto; margin: 0; }
  .artists-page .hero-transition-overlay .mascot-handoff {
    top: 20px; left: 50%; transform: translateX(-50%);
    animation: artistsMascotFloat 4s ease-in-out infinite;
  }
  /* brands.html doesn't move the mascot (same top:20/right:28 corner it
     already has on the homepage) or give it any idle float — just the
     34px→25px shrink, already handled by index.html's clone-fly itself.
     The un-invert specifically waits for THIS overlay's own fade-out
     (same moment the big overlay logo — always dark, the whole hold+fly —
     disappears to reveal the real header logo underneath, which is white
     on brands.html) rather than happening earlier during index.html's
     750ms fly, so the mascot only turns white in step with the logo. */
  body:not(.artists-page) .hero-transition-overlay .mascot-handoff {
    top: 20px; right: 28px; filter: invert(0);
    transition: filter 0.9s ease; /* matches .hero-transition-overlay's own fade-out duration */
  }
  body:not(.artists-page) .hero-transition-overlay.fade-out .mascot-handoff {
    filter: invert(1);
  }
  @media (prefers-reduced-motion: reduce) {
    .hero-transition-overlay .mascot-handoff { animation: none; transition: none; }
  }
