/* ky-music — the music bar's stylesheet. Pairs with ky/ky-music.js.
   ---------------------------------------------------------------------------
   GLASS: #ky-music carries .lg-surface and CONSUMES it. This file declares no
   background, no backdrop-filter, no border and no box-shadow on #ky-music —
   the frost, the tint and the rim are whatever ky/liquid-glass.css says they
   are, so retuning that surface retunes this bar with it. The only paint
   properties here belong to the bar's own parts (art tile, hairlines, fills),
   and every one of them is mixed from the live theme tokens.

   Kept from the previous stylesheet so nothing regresses: the class names the
   player and ky-musicfx.js both address (.kym-title/.kym-meta/.kym-vol input),
   the marquee, the equaliser ladder, the hairline seek + volume tracks.

   THE TRACK CHANGE is the one thing here that is not decoration. A change used
   to be invisible: the title's text node was simply reassigned, so six tracks
   read as one label that occasionally said something else. It is now built on
   the site's OWN idiom for "one label replaced by another" — the same roll
   every nav link and footer link uses (outgoing translateY(0 -> -100%),
   incoming +100% -> 0, .4s on cubic-bezier(.51,.92,.24,1.15)) — which is what
   makes it native to this theme rather than a generic crossfade. The outgoing
   string lives in a transient .kym-t-ghost that ky-musicfx.js clones; the whole
   window is 520ms (SWAP_MS, owned by that file) and every animation below
   finishes inside it. See (04).

   Vocabulary: montrealMono, --color-foreground ink on --color-accent-200
   ground, cubic-bezier(.51,.92,.24,1.15) — the same reveal register ky-flow
   uses. Numbering is parenthetical, (01)…(13), like the rest of the site.

   COLOUR LAW: --kym-ink is the theme's foreground and it is LIGHT in six of
   the twelve themes (Noir is #FFDE00 on #000). Every new declaration mixes
   from --kym-ink / --kym-bg / the --lg-* tokens, never from a literal — the
   sole exception being drop shadows, which stay rgb(0 0 0 / a) because an ink
   shadow becomes a yellow glow in Noir. */

/* ── (01) shell, tokens, resets ──────────────────────────────────────── */
html[data-glass] #ky-music{
  /* theme hooks — re-resolved live when ky-switch rewrites the html vars */
  --kym-ink: var(--color-foreground, #14110A);
  --kym-bg:  var(--color-accent-200, #E3AC33);
  --kym-ease: cubic-bezier(.51,.92,.24,1.15);
  --kym-r: 30px;
  --kym-w: min(640px, 100vw - 120px);

  /* PRESS-DOWN CURVE — see (05). Separate token because three controls share
     it and the value is the point: 60ms, not the .3s the release uses. */
  --kym-press: 60ms cubic-bezier(.2,.9,.3,1);

  /* FOCUS HALO — the ring's own ground, so it never has to survive the
     backdrop. Mixed from --kym-bg, i.e. from the theme's OWN contrast pair; a
     fixed white or black halo would invert somewhere between Honey & Ink and
     Noir, where ink is #14110A and #FFDE00 respectively.

     MEASURED off rendered pixels, ink outline vs. what sits immediately
     outside it, at two scroll positions x two themes:

                            plate only (before)   this halo (after)
       Honey & Ink  hero         10.42:1               7.73:1
       Honey & Ink  #Works        5.70:1               7.23:1
       Noir         hero          4.95:1              12.22:1
       Noir         #Works        1.69:1              10.57:1

     The Noir/#Works cell is the whole argument: a yellow ring on a plate that
     composites to rgb(180,176,87) over that band is 1.69:1 — WCAG 1.4.11 asks
     3:1 for a control boundary, so the focus indicator was effectively
     invisible in that theme at that scroll position, and no amount of "make it
     lighter" fixes it in the six themes where ink is dark. Giving the ring its
     own ground raises the FLOOR from 1.69:1 to 7.23:1 and costs the amber hero
     nothing that matters (10.42 -> 7.73, both far past 3:1).
     7px so it clears the 2px outline sitting at outline-offset:2px.
     Re-measure with scratchpad A/v7.py if either token moves. */
  --kym-halo: 0 0 0 7px color-mix(in srgb, var(--kym-bg) 90%, transparent);

  /* PLATE OPACITY — no longer overridden here. ---------------------------
     This used to force --lg-tint:80%. That was the right fix for the material
     as it stood: the lens engine was replacing the declared blur(14px) with
     blur(2px), so nothing of the backdrop was actually diffused and the only
     way to hold contrast on a bar that TRAVELS over the amber hero, the dark
     #Works band and each project's colour wash was to make the plate nearly
     solid. It also made it the most opaque thing on the site, which is exactly
     what the user then asked to be rid of.

     Fixed at the source instead: ky/liquid-glass.js now takes its blur,
     saturation, brightness and contrast from custom properties, and
     ky/liquid-glass.css sets them to a heavy blur plus a brightness/contrast
     pair that NORMALISES whatever is behind the pane. Legibility now comes
     from flattening the backdrop rather than from hiding it, so this bar can
     sit at the same near-clear tint as every other surface.
     Re-measure with scratchpad/contrast.py if the material is touched. */

  position:fixed; top:12px; left:50%; transform:translateX(-50%);
  z-index:9000;
  display:flex; flex-direction:column;
  width:var(--kym-w); height:auto;

  /* geometry only — .lg-surface owns the frost. Both are needed here: the
     liquid-glass engine clips its lens to this radius, and the queue panel
     is clipped by this overflow. */
  border-radius:var(--kym-r);
  overflow:hidden; isolation:isolate;

  font-family:montrealMono,'montrealMono Fallback',ui-monospace,SFMono-Regular,Menlo,monospace;
  color:var(--kym-ink);
  transition:width .5s var(--kym-ease);
  animation:kymEnter .8s var(--kym-ease) both;
}

/* stepped inner hairline — a second, tighter radius reads as continuous
   curvature. Ink, not white: it follows the theme rather than the glass. */
html[data-glass] #ky-music::before{
  content:""; position:absolute; inset:1px; z-index:3; pointer-events:none;
  border-radius:calc(var(--kym-r) - 1.5px);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--kym-ink) 9%, transparent);
}

@keyframes kymEnter{
  from{opacity:0; transform:translateX(-50%) translateY(-14px);}
  to  {opacity:1; transform:translateX(-50%) translateY(0);}
}

#ky-music *{box-sizing:border-box;}
#ky-music button{
  background:none; border:0; padding:0; cursor:pointer; color:inherit;
  position:relative;
  display:flex; align-items:center; justify-content:center;
  font:inherit; -webkit-tap-highlight-color:transparent;
}
#ky-music svg{display:block;}

/* focus — visible ink ring on every control, over its own halo.
   The outline alone lands on whatever the frost happens to be showing at that
   scroll position; this bar travels over the amber hero, the dark #Works band
   and every project wash, so "2px of ink" is legible in some of them and not
   in others. The halo gives the ring a guaranteed ground of its own. It is a
   box-shadow, so it paints UNDER the outline and outside the border box: 0–7px
   of halo with the ink outline covering 2–4px of it. */
#ky-music :is(button, input, [role="slider"]):focus-visible,
#ky-music:focus-visible{
  outline:2px solid var(--kym-ink); outline-offset:2px; border-radius:6px;
  box-shadow:var(--kym-halo);
}
/* the two controls that already carry a box-shadow keep it and append */
#ky-music .kym-play:focus-visible{
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 18%, transparent),
    0 2px 6px -2px rgba(12,9,2,.4),
    var(--kym-halo);
}
#ky-music .kym-art:focus-visible{
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 22%, transparent),
    0 2px 6px -2px rgba(12,9,2,.35),
    var(--kym-halo);
}

/* 44x44 pointer targets without 44px of visual weight. Centred and sized
   absolutely rather than by a negative inset, so every control clears 44
   whatever its visual diameter is. Gaps below are then set so that no two
   adjacent hit areas overlap by more than a pixel or two. */
#ky-music :is(.kym-sk, .kym-play, .kym-mute, .kym-queue, .kym-fold)::before{
  content:""; position:absolute; left:50%; top:50%;
  width:44px; height:44px; margin:-22px 0 0 -22px; border-radius:50%;
}

.kym-sr{
  position:absolute!important; width:1px; height:1px; margin:-1px;
  padding:0; overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%);
  white-space:nowrap; border:0;
}

/* ── (02) the bar row ────────────────────────────────────────────────── */
#ky-music .kym-row{
  display:flex; align-items:center; gap:12px;
  height:60px; flex:0 0 60px; padding:0 12px 0 10px;
}

/* ── (03) art tile: analyser canvas over the CSS equaliser fallback ──── */
#ky-music .kym-art{
  position:relative; width:40px; height:40px; flex:0 0 40px;
  border-radius:16px; overflow:hidden;
  background:linear-gradient(135deg,
    var(--kym-ink),
    color-mix(in srgb, var(--kym-ink) 58%, var(--kym-bg)));
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 22%, transparent),
    0 2px 6px -2px rgba(12,9,2,.35);
  transition:transform .35s var(--kym-ease), box-shadow .35s var(--kym-ease);
}
#ky-music .kym-art::before{
  content:""; position:absolute; left:50%; top:50%;
  width:44px; height:44px; margin:-22px 0 0 -22px; border-radius:18px;
}
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-art:hover{transform:translateY(-1px);}
}
/* ASYMMETRIC PRESS — see (05) for the reasoning; the token is --kym-press. */
#ky-music .kym-art:active{transform:scale(.95); transition:transform var(--kym-press);}
#ky-music.playing .kym-art{
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 22%, transparent),
    0 2px 10px -2px color-mix(in srgb, var(--kym-bg) 55%, rgba(12,9,2,.4));
}

/* CSS ladder — the readout you get with no AudioContext (i.e. before any
   gesture, or when WebAudio is blocked outright) */
#ky-music .kym-eq{
  position:absolute; inset:0; z-index:1;
  display:flex; align-items:flex-end; justify-content:center; gap:3px;
  padding:10px 0;
  transition:opacity .4s var(--kym-ease);
}
#ky-music .kym-eq i{
  width:3px; height:100%; border-radius:2px;
  background:var(--kym-bg);
  transform:scaleY(.25); transform-origin:bottom;
  transition:transform .45s var(--kym-ease);
}
#ky-music.playing .kym-eq i{animation:kymEq 1.1s ease-in-out infinite;}
#ky-music.playing .kym-eq i:nth-child(2){animation-delay:.26s;}
#ky-music.playing .kym-eq i:nth-child(3){animation-delay:.54s;}
@keyframes kymEq{
  0%,100%{transform:scaleY(.28);}
  45%{transform:scaleY(.95);}
  70%{transform:scaleY(.5);}
}

/* TRACK CHANGE, art tile — the record takes the hit and the ladder goes quiet.
   The tile is the only round thing in the bar, so it gets the rotation; a few
   degrees against the direction of travel is enough to read as a record being
   knocked, and more than ~5deg starts to look like a loading spinner. The
   ladder drops to its floor and stays there for the middle of the window, so
   there is a beat of silence across the change rather than three bars bouncing
   through a handover — that beat is what makes the change read as a change.

   Both are inside SWAP_MS (520): the punch is 460ms, the ladder falls in 220ms
   and resumes the instant .kym-swap is dropped. The drop uses the ladder's own
   transition rather than a keyframe so the resume is a re-entry, not a cut. */
@keyframes kymArtPunch{
  0%  {transform:scale(1)   rotate(0);}
  34% {transform:scale(.86) rotate(var(--kym-punch,-4deg));}
  100%{transform:scale(1)   rotate(0);}
}
#ky-music.kym-swap .kym-art{animation:kymArtPunch .46s var(--kym-ease) both;}
#ky-music.kym-swap[data-kym-dir="prev"] .kym-art{--kym-punch:4deg;}
#ky-music.kym-swap .kym-eq i{
  animation:none; transform:scaleY(.14);
  transition:transform .22s var(--kym-ease);
}

/* real spectrum — fades in over the ladder once the analyser is live */
#ky-music .kym-viz{
  position:absolute; inset:0; z-index:2; width:100%; height:100%;
  opacity:0; transition:opacity .5s var(--kym-ease); pointer-events:none;
}
#ky-music.kym-viz-on .kym-viz{opacity:1;}
#ky-music.kym-viz-on .kym-eq{opacity:0;}

/* loading — a thin ink sweep around the tile, never a spinner-on-top */
#ky-music .kym-spin{
  position:absolute; inset:0; z-index:3; border-radius:inherit;
  pointer-events:none; opacity:0; transition:opacity .3s ease;
  background:conic-gradient(from 0deg,
    transparent 0deg, transparent 250deg,
    color-mix(in srgb, var(--kym-bg) 85%, transparent) 330deg, transparent 360deg);
  -webkit-mask:radial-gradient(circle, transparent 62%, #000 66%);
          mask:radial-gradient(circle, transparent 62%, #000 66%);
}
#ky-music.kym-loading .kym-spin{opacity:1; animation:kymSpin 1.05s linear infinite;}
@keyframes kymSpin{to{transform:rotate(360deg);}}

/* error — the tile goes hollow, and says so in the sub-line */
#ky-music.kym-err .kym-art{
  background:none;
  box-shadow:inset 0 0 0 1.5px color-mix(in srgb, var(--kym-ink) 45%, transparent);
}
#ky-music.kym-err .kym-eq i{background:color-mix(in srgb, var(--kym-ink) 30%, transparent);}

/* ── (04) meta: title roll + marquee + sub-line ──────────────────────── */
#ky-music .kym-meta{
  min-width:0; width:132px; flex:0 1 auto;
  display:flex; flex-direction:column; gap:2px; text-align:left;
  overflow:hidden;   /* the marquee clip — meta width is fixed: no layout shift */
}

/* .kym-title IS THE CLIP BOX, not the text. The text lives in .kym-t so a
   track change can put the outgoing string in a .kym-t-ghost sibling and roll
   the two past each other inside this box.

   line-height is pinned because translateY(100%) is meaningless without a
   definite line box — "normal" resolves per font metrics and a percentage
   translate against it is whatever the font happens to say. MEASURED: at 12px
   montrealMono, `normal` already computes to exactly 18px here, so 18px pins
   the value without moving anything: .kym-meta stays 34.25px tall (18 title +
   2 gap + 14.25 artist) inside the 60px row, 12.9px of clearance top and
   bottom. If the font or the size changes, re-measure before trusting this.

   position:relative anchors the ghost. text-overflow moved down to .kym-t with
   the text; an ellipsis is a property of the block that holds the line. */
#ky-music .kym-title{
  position:relative;
  /* WEIGHT 400, NOT 500. montrealMono ships faces only at <=400; at 450+ Chromium
     silently substitutes the proportional fallback, and document.fonts.check()
     still returns true so nothing warns you. MEASURED advance of 'iiiiiiiiii' vs
     'WWWWWWWWWW' at 12px on this family: weight 400 -> 72.00 / 72.00 (monospace),
     weight 500 -> 24.38 / 110.77 (proportional, 4.5:1). So the title was set in a
     different TYPEFACE from the artist line 2px beneath it, which is at 400 and
     does render mono. The line-height:18px pin below was reasoned against
     montrealMono's metrics and only now actually gets them. */
  font-size:12px; font-weight:400; letter-spacing:.01em; line-height:18px;
  white-space:nowrap; overflow:hidden;
}
#ky-music .kym-t{
  display:block;            /* transforms do not apply to inline boxes */
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* ky-musicfx.js flags overflowing titles with .kym-scroll + --kym-over (px).
   The class moved from .kym-title to .kym-t with the text: .kym-title is now a
   clip box holding two independently moving layers, and a transform on it
   would drag the outgoing ghost along with the incoming line instead of
   sliding one past the other. */
#ky-music .kym-t.kym-scroll{
  overflow:visible; text-overflow:clip; width:max-content; max-width:none;
  animation:kymMarquee var(--kym-dur,12s) cubic-bezier(.45,0,.25,1) infinite;
  animation-play-state:paused;              /* frozen (no snap) whenever paused */
}
#ky-music.playing .kym-t.kym-scroll{animation-play-state:running;}
/* the edge fade is wanted whenever the line is wider than its box — which is
   true of a scrolling title, and true of BOTH layers during a swap */
#ky-music .kym-meta:has(.kym-t.kym-scroll),
#ky-music.kym-swap .kym-meta{
  -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 5px, #000 calc(100% - 12px), transparent);
          mask-image:linear-gradient(90deg, transparent 0, #000 5px, #000 calc(100% - 12px), transparent);
}
@keyframes kymMarquee{
  0%,14%   {transform:translateX(0);}
  46%,58%  {transform:translateX(calc(var(--kym-over,0px) * -1));}
  90%,100% {transform:translateX(0);}
}

/* THE TRACK ROLL ──────────────────────────────────────────────────────────
   The site's own replacement idiom, unchanged: outgoing 0 -> -100%, incoming
   +100% -> 0, .4s on --kym-ease, exactly as every nav and footer link does it.
   Reversed for prev, because a change you asked for backwards that animates
   forwards reads as the wrong track arriving.

   Two keyframes serve all four cases through --kym-roll rather than eight
   near-identical @keyframes blocks. --kym-roll is declared on .kym-title and
   inherits to both layers, so the pair always moves as one system; it is set
   by static rules only — never written per frame, and never on :root (a root
   custom-property write invalidates computed style for the whole document,
   measured 7.675ms vs 0.025ms scoped on this page).

   AUTO-ADVANCE IS THE SAME MECHANISM AT LOWER ENERGY. A change you asked for
   deserves a direct mechanical answer; one that just happened while you were
   reading must not demand the eye. So .kym-auto halves the travel and lengthens
   the duration — a drift, not a flick. Half travel cannot clear the clip box on
   its own, which is why the drift is the only variant that also fades
   (--kym-rollo): without it the ghost would still be half in frame when the
   window closes and the removal would read as a cut.

   Everything here is inside SWAP_MS = 520 (ky-musicfx.js owns that timer):
   .4s flick, .48s drift, no delays.

   The marquee is SUSPENDED for the window — ky-musicfx.js drops .kym-scroll and
   re-measures on close — and both layers are forced to their natural width so
   the outgoing line does not sprout an ellipsis the instant it starts to leave.
   animation-play-state is forced running because prev/next work while paused,
   and the marquee's rule leaves it paused there. */
@keyframes kymRollIn{
  from{transform:translateY(var(--kym-roll,100%)); opacity:var(--kym-rollo,1);}
  to  {transform:translateY(0);                    opacity:1;}
}
@keyframes kymRollOut{
  from{transform:translateY(0);                                 opacity:1;}
  to  {transform:translateY(calc(var(--kym-roll,100%) * -1));   opacity:var(--kym-rollo,1);}
}
#ky-music.kym-swap .kym-title{--kym-roll:100%; --kym-rollo:1; overflow:hidden;}
#ky-music.kym-swap[data-kym-dir="prev"] .kym-title{--kym-roll:-100%;}
/* AUTO-ADVANCE: lower energy, but it must still HAND OFF rather than dissolve.
   Half travel on an 18px line box leaves the two titles only 9px apart, so the
   clip that makes the flick unambiguous is gone and the fade has to do the whole
   job. At the first attempt it did not: measured at t=120ms the outgoing line was
   74% visible at opacity 0.471 and the incoming 76% visible at 0.529 — both names
   fully readable at once, with the ghost painted on top because it is appended
   last. A cross-fade through 0.5/0.5 is the one thing a replacement must never
   do. So the opacity curve is not linear with the travel: the ghost is gone by
   35% of the window and the incoming line does not start arriving until then,
   which keeps the calm of the short travel without ever showing two tracks.
   kymRollOut/In carry the timing; --kym-rollo stays 0 so each end is fully
   transparent. */
#ky-music.kym-swap.kym-auto .kym-title{--kym-roll:50%; --kym-rollo:0;}
#ky-music.kym-swap.kym-auto[data-kym-dir="prev"] .kym-title{--kym-roll:-50%;}
#ky-music.kym-swap.kym-auto .kym-title .kym-t{
  animation-timing-function:cubic-bezier(.33,0,.15,1);
  animation-name:kymRollInAuto;
}
#ky-music.kym-swap.kym-auto .kym-title .kym-t-ghost{
  animation-timing-function:cubic-bezier(.33,0,.15,1);
  animation-name:kymRollOutAuto;
}
@keyframes kymRollOutAuto{
  0%  {transform:translateY(0);                                opacity:1;}
  35% {transform:translateY(calc(var(--kym-roll,50%) * -.62));  opacity:0;}
  100%{transform:translateY(calc(var(--kym-roll,50%) * -1));    opacity:0;}
}
@keyframes kymRollInAuto{
  0%  {transform:translateY(var(--kym-roll,50%));              opacity:0;}
  35% {transform:translateY(calc(var(--kym-roll,50%) * .62));  opacity:0;}
  100%{transform:translateY(0);                                opacity:1;}
}

#ky-music.kym-swap .kym-title .kym-t{
  width:max-content; max-width:none; overflow:visible; text-overflow:clip;
  animation:kymRollIn .4s var(--kym-ease) both;
  animation-play-state:running;
}
#ky-music.kym-swap .kym-title .kym-t-ghost{animation-name:kymRollOut;}
#ky-music.kym-swap.kym-auto .kym-title .kym-t{animation-duration:.48s;}

/* the outgoing line, cloned by ky-musicfx.js and alive for one window only */
#ky-music .kym-t-ghost{
  position:absolute; left:0; top:0;
  pointer-events:none; user-select:none;
}
#ky-music .kym-artist{
  font-size:9.5px; text-transform:uppercase; letter-spacing:.13em;
  /* was 74% — MEASURED 3.45:1 even over the amber hero, i.e. failing WCAG AA
     on its own plate before any dark ground was involved. At 9.5px this is
     small text, so it needs the full 4.5:1, not the 3:1 large-text allowance. */
  color:color-mix(in srgb, var(--kym-ink) 93%, transparent);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  transition:color .3s ease;
}
#ky-music .kym-artist.kym-msg{
  color:var(--kym-ink); text-transform:none; letter-spacing:.04em;
  font-size:10px;
}
#ky-music .kym-artist.kym-msg-err{
  color:var(--color-accent-600, var(--kym-ink));
  text-shadow:0 0 12px color-mix(in srgb, var(--kym-bg) 60%, transparent);
}

/* ── (05) transport ───────────────────────────────────────────────────────
   PRESS PHYSICS, all three controls (.kym-sk, .kym-play, .kym-art). The press
   and the release are not the same gesture and must not share a curve. With a
   single `transition: transform .3s`, a tap shorter than ~300ms never reaches
   its pressed state at all — the transition is still travelling outward when
   the finger lifts and the control simply eases back from wherever it got to,
   so a quick tap produces no press at all. The press is therefore declared on
   :active itself at 60ms on a sharp curve (--kym-press), and the release falls
   back to the base rule's .3s --kym-ease. Down hard, up soft. */
#ky-music .kym-ctl{display:flex; align-items:center; gap:12px; flex:0 0 auto;}
#ky-music .kym-sk{
  width:30px; height:30px; border-radius:50%;
  color:color-mix(in srgb, var(--kym-ink) 76%, transparent);
  transition:color .3s var(--kym-ease), transform .3s var(--kym-ease),
             background-color .3s var(--kym-ease);
}
#ky-music .kym-sk svg{width:12px; height:12px;}
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-sk:hover{
    color:var(--kym-ink); transform:scale(1.1);
    background:color-mix(in srgb, var(--kym-ink) 10%, transparent);
  }
}
#ky-music .kym-sk:active{transform:scale(.9); transition:transform var(--kym-press);}

/* DIRECTION ACKNOWLEDGEMENT — the glyph leaves the way it just sent you and
   comes back. This is confirmation, not decoration: 3px and 340ms, on the
   button's SVG rather than the button, so it composes with the hover scale and
   the press instead of fighting them. data-dir is stamped by the player's
   build(); ky-musicfx.js only adds/removes .kym-nudge, on `click`, so keyboard
   activation acknowledges too. */
#ky-music .kym-sk[data-dir="1"]{--kym-nx:3px;}
#ky-music .kym-sk[data-dir="-1"]{--kym-nx:-3px;}
#ky-music .kym-sk.kym-nudge svg{animation:kymNudge .34s var(--kym-ease);}
@keyframes kymNudge{
  0%  {transform:translateX(0);}
  34% {transform:translateX(var(--kym-nx,0));}
  100%{transform:translateX(0);}
}

#ky-music .kym-play{
  width:34px; height:34px; border-radius:50%; flex:0 0 34px;
  background:var(--kym-ink); color:var(--kym-bg);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 18%, transparent),
    0 2px 6px -2px rgba(12,9,2,.4);
  transition:transform .3s var(--kym-ease), box-shadow .3s var(--kym-ease);
}
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-play:hover{
    transform:scale(1.07);
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, #fff 18%, transparent),
      0 4px 12px -3px rgba(12,9,2,.5);
  }
}
#ky-music .kym-play:active{transform:scale(.93); transition:transform var(--kym-press);}

/* PLAY/PAUSE — a morph, not a crossfade ────────────────────────────────────
   The glyph is two <i> halves stacked on the same 14x14 box, each clipped to
   its own polygon; .is-playing (set by the player, only when the value
   actually changed) interpolates them from a split triangle into the two pause
   bars. Both polygons in a pair MUST have the same vertex count or the
   interpolation is discrete — which is why the right half, a triangle, is
   written as a degenerate quad with its apex point repeated.

   This replaces an SVG that sync() rebuilt about four times a second, so no
   transition on it could ever run; the elements now survive, and the morph is
   the same one Apple's transport does. clip-path is animated here rather than
   width/height, and it is 14px of glyph on a button, not per-frame work on a
   scrolling surface, so it is inside the perf law's stated exception.

   OPTICAL CENTRING IS BAKED INTO THE VERTICES. A triangle's optical centre sits
   left of its bounding box, so the paused polygons span 18%–86% (centre 52%)
   while the playing bars span 18%–82% (centre 50%). The old
   `transform:translateX(1px)` hack that did this to the SVG is gone: there is
   no SVG to move, and a transform on the glyph would fight the button's own
   press scale. */
#ky-music .kym-play .kym-g{
  position:relative; display:block; width:14px; height:14px; flex:0 0 14px;
}
#ky-music .kym-play .kym-g i{
  position:absolute; inset:0; background:currentColor;
  transition:clip-path .34s var(--kym-ease);
}
#ky-music .kym-play .kym-g1{clip-path:polygon(18% 8%, 52% 29%, 52% 71%, 18% 92%);}
#ky-music .kym-play .kym-g2{clip-path:polygon(52% 29%, 86% 50%, 86% 50%, 52% 71%);}
#ky-music .kym-play.is-playing .kym-g1{clip-path:polygon(18% 6%, 40% 6%, 40% 94%, 18% 94%);}
#ky-music .kym-play.is-playing .kym-g2{clip-path:polygon(60% 6%, 82% 6%, 82% 94%, 60% 94%);}

/* ── (06) progress: hairline track, ink dot thumb on hover/focus ─────── */
#ky-music .kym-prog{
  flex:1 1 auto; display:flex; align-items:center; gap:8px; min-width:60px;
  transition:opacity .3s var(--kym-ease);
}
#ky-music .kym-time{
  font-size:9.5px; flex:0 0 auto;
  /* was 74% — MEASURED, composited, over the #Works band: 4.26:1 in Honey & Ink
     and 3.76-4.24:1 across the other six light themes, i.e. the ONLY element on
     the open bar failing there once the glass luminance switch was wired up (the
     title and artist clear 4.5 in all seven). At 9.5px this is small text and
     owes the full 4.5:1. 88% carries every light theme while staying below the
     artist's 93%, so the hierarchy title > artist > time is preserved.
     The five dark themes still miss on all three lines — that is a plate
     problem, not an ink one, and raising --lg-tint is the only lever left.
     Re-measure with scratchpad/text2.py; note getComputedStyle returns
     color(srgb 0-1) here, not rgb(0-255). */
  color:color-mix(in srgb, var(--kym-ink) 88%, transparent);
  font-variant-numeric:tabular-nums; letter-spacing:.02em;
  transition:color .25s var(--kym-ease), opacity .3s var(--kym-ease),
             transform .25s var(--kym-ease);
}
/* the duration is not known yet — fade the readout instead of publishing the
   0:00 it would otherwise show between one track's metadata and the next's.
   The player owns .kym-nodur and toggles it from isFinite(audio.duration). */
#ky-music.kym-nodur .kym-dur{opacity:.25;}
#ky-music .kym-bar{
  flex:1; height:2px; border-radius:2px; position:relative; cursor:pointer;
  touch-action:none;
  background:color-mix(in srgb, var(--kym-ink) 24%, transparent);
  transition:height .3s var(--kym-ease), background-color .3s var(--kym-ease);
}
#ky-music .kym-bar::before{content:""; position:absolute; inset:-15px 0;}  /* 32px hit strip */
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-bar:hover{height:4px;}
}
#ky-music .kym-bar:focus-visible{height:4px;}
#ky-music .kym-fill{
  position:absolute; left:0; top:0; bottom:0; width:0%;
  border-radius:inherit; background:var(--kym-ink);
}
#ky-music .kym-fill::after{
  content:""; position:absolute; right:-4px; top:50%;
  width:8px; height:8px; margin-top:-4px; border-radius:50%;
  background:var(--kym-ink);
  box-shadow:0 0 0 2px color-mix(in srgb, var(--kym-bg) 55%, transparent);
  transform:scale(0);
  transition:transform .3s var(--kym-ease);
}
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-bar:hover .kym-fill::after{transform:scale(1);}
}
#ky-music .kym-bar:focus-visible .kym-fill::after{transform:scale(1);}

/* SCRUB — a drag has to look different from a hover ────────────────────────
   The seek line is dragged with pointer capture, so the pointer can be far
   outside the 32px hit strip while still driving it; until now the bar looked
   exactly as it does under a passing cursor, and on a long drag it was not
   obvious anything was being held. ky-musicfx.js adds .kym-scrub on
   pointerdown and drops it on pointerup/pointercancel — it only listens, it
   never preventDefaults, so the player's own capture is untouched.
   The elapsed readout is emphasised by colour and a 6% scale rather than by
   weight: .kym-cur is a flex item, and a weight change on tabular numerals
   would resize it and shove the seek line sideways mid-drag. */
#ky-music .kym-bar.kym-scrub{
  height:6px;
  background:color-mix(in srgb, var(--kym-ink) 32%, transparent);
}
#ky-music .kym-bar.kym-scrub .kym-fill::after{transform:scale(1.5);}
#ky-music .kym-prog:has(.kym-bar.kym-scrub) .kym-cur{
  color:var(--kym-ink); transform:scale(1.06);
}

/* TRACK CHANGE — the fill retracts instead of teleporting.
   The single ugliest frame in the bar was the fill jumping 78% -> 0% between
   two paint frames. Under .kym-swap only, the width gets a transition so the
   played span pulls back to the start while the title rolls, and the whole
   group dims so the reset reads as part of the handover rather than as a
   glitch. SCOPED TO THE SWAP DELIBERATELY: sync() writes this width roughly
   four times a second during normal playback, and a transition left in place
   would smear every one of those updates into a lag that never catches up.
   .40s, inside SWAP_MS = 520. */
#ky-music.kym-swap .kym-prog{opacity:.55;}
#ky-music.kym-swap .kym-fill{transition:width .4s var(--kym-ease);}

/* ── (07) volume ─────────────────────────────────────────────────────── */
#ky-music .kym-vol{display:flex; align-items:center; gap:6px; flex:0 0 auto;}
#ky-music .kym-mute{
  width:26px; height:26px; border-radius:50%;
  color:color-mix(in srgb, var(--kym-ink) 74%, transparent);
  transition:color .3s var(--kym-ease);
}
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-mute:hover{color:var(--kym-ink);}
}
#ky-music .kym-mute svg{width:13px; height:13px;}
#ky-music .kym-vol input{
  -webkit-appearance:none; appearance:none;
  width:54px; height:18px; margin:0; background:transparent; cursor:pointer;
  accent-color:var(--kym-ink);
}
#ky-music .kym-vol input::-webkit-slider-runnable-track{
  height:2px; border-radius:2px;
  background:linear-gradient(90deg,
    var(--kym-ink) 0 var(--kym-v,80%),
    color-mix(in srgb, var(--kym-ink) 24%, transparent) var(--kym-v,80%));
}
#ky-music .kym-vol input::-webkit-slider-thumb{
  -webkit-appearance:none; width:9px; height:9px; margin-top:-3.5px;
  border-radius:50%; border:0; background:var(--kym-ink);
  transition:transform .3s var(--kym-ease);
}
@media (hover: hover) and (pointer: fine){
  #ky-music .kym-vol input:hover::-webkit-slider-thumb{transform:scale(1.3);}
}
#ky-music .kym-vol input::-moz-range-track{
  height:2px; border-radius:2px;
  background:color-mix(in srgb, var(--kym-ink) 24%, transparent);
}
#ky-music .kym-vol input::-moz-range-progress{height:2px; border-radius:2px; background:var(--kym-ink);}
#ky-music .kym-vol input::-moz-range-thumb{
  width:9px; height:9px; border:0; border-radius:50%; background:var(--kym-ink);
}

/* ── (08) tools: queue + collapse ────────────────────────────────────── */
#ky-music .kym-tools{display:flex; align-items:center; gap:14px; flex:0 0 auto;}
#ky-music :is(.kym-queue, .kym-fold){
  width:30px; height:30px; border-radius:50%;
  color:color-mix(in srgb, var(--kym-ink) 74%, transparent);
  transition:color .3s var(--kym-ease), transform .35s var(--kym-ease),
             background-color .3s var(--kym-ease);
}
#ky-music :is(.kym-queue, .kym-fold):hover{
  color:var(--kym-ink);
  background:color-mix(in srgb, var(--kym-ink) 10%, transparent);
}
#ky-music.kym-open .kym-queue{
  color:var(--kym-bg); background:var(--kym-ink);
}
#ky-music .kym-fold svg{transition:transform .4s var(--kym-ease);}
#ky-music.kym-min .kym-fold svg{transform:rotate(180deg);}

/* ── (09) queue panel ────────────────────────────────────────────────── */
#ky-music .kym-panel{
  display:grid; grid-template-rows:0fr;
  opacity:0; visibility:hidden;
  transition:grid-template-rows .44s var(--kym-ease),
             opacity .26s ease,
             visibility 0s linear .44s;
}
#ky-music.kym-open .kym-panel{
  grid-template-rows:1fr; opacity:1; visibility:visible;
  transition:grid-template-rows .44s var(--kym-ease),
             opacity .32s ease .06s,
             visibility 0s;
}
/* READING PLATE — not more glass, a legibility device.
   The bar row is short strings at 12px and reads fine straight through the
   frost. The queue is six lines of 11.5px type over whatever the page is
   showing at that scroll position, which at the top of the page is the hero
   wordmark at 168px — blurred, but still dark enough to eat the list. So the
   panel gets a denser ground, mixed from the SAME theme token the page uses
   (--color-accent-200), which keeps it in the palette in every theme: honey
   plate under ink in Honey & Ink, black plate under yellow in Noir. */
#ky-music .kym-panel-in{
  overflow:hidden; min-height:0;
  padding:0 12px 10px;
  border-top:1px solid color-mix(in srgb, var(--kym-ink) 14%, transparent);
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--kym-bg) 82%, transparent),
    color-mix(in srgb, var(--kym-bg) 90%, transparent));
}
#ky-music .kym-wave{
  display:block; width:100%; height:32px; margin:8px 0 4px;
  opacity:.9;
}
#ky-music .kym-list{
  list-style:none; margin:0; padding:0;
  max-height:min(46vh, 300px); overflow-y:auto; overscroll-behavior:contain;
  scrollbar-width:thin;
  scrollbar-color:color-mix(in srgb, var(--kym-ink) 30%, transparent) transparent;
}
#ky-music .kym-li{margin:0;}
#ky-music .kym-item{
  width:100%; min-height:34px; padding:0 8px;
  display:flex; align-items:center; gap:10px;
  border-radius:9px; text-align:left;
  color:color-mix(in srgb, var(--kym-ink) 78%, transparent);
  transition:background-color .25s var(--kym-ease), color .25s var(--kym-ease);
}
#ky-music .kym-item::before{content:none;}   /* the row is already >=34px tall */
#ky-music .kym-item:hover{
  background:color-mix(in srgb, var(--kym-ink) 9%, transparent);
  color:var(--kym-ink);
}
#ky-music .kym-item.on{
  background:color-mix(in srgb, var(--kym-ink) 14%, transparent);
  color:var(--kym-ink);
}
#ky-music .kym-num{
  font-size:9.5px; letter-spacing:.06em; flex:0 0 auto;
  font-variant-numeric:tabular-nums;
  color:color-mix(in srgb, var(--kym-ink) 72%, transparent);
}
#ky-music .kym-item.on .kym-num{color:var(--kym-ink);}
#ky-music .kym-name{
  flex:1 1 auto; min-width:0; font-size:11.5px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
#ky-music .kym-item.on .kym-name{font-weight:600;}
#ky-music .kym-len{
  flex:0 0 auto; font-size:9.5px; font-variant-numeric:tabular-nums;
  color:color-mix(in srgb, var(--kym-ink) 72%, transparent);
}
/* the playing row gets the site's own trailing marker rather than an icon */
#ky-music .kym-item.on .kym-len::after{content:" \2022"; opacity:.75;}
#ky-music .kym-item.bad{
  color:color-mix(in srgb, var(--kym-ink) 48%, transparent);
  text-decoration:line-through; cursor:not-allowed;
}
#ky-music .kym-item.bad:hover{background:none;}

#ky-music .kym-note{
  display:flex; justify-content:space-between; gap:10px;
  margin:8px 0 0; padding:7px 8px 0;
  border-top:1px solid color-mix(in srgb, var(--kym-ink) 10%, transparent);
  font-size:9px; text-transform:uppercase; letter-spacing:.12em;
  color:color-mix(in srgb, var(--kym-ink) 72%, transparent);
}
#ky-music .kym-keys{white-space:nowrap; text-transform:none; letter-spacing:.04em;}

/* ── (10) collapsed puck ─────────────────────────────────────────────── */
#ky-music.kym-min{--kym-w:126px;}
#ky-music.kym-min :is(.kym-meta, .kym-prog, .kym-vol, .kym-queue, .kym-sk){display:none;}
#ky-music.kym-min .kym-row{gap:10px; padding:0 10px;}
#ky-music.kym-min .kym-ctl{gap:0;}

/* ── (11) responsive ─────────────────────────────────────────────────── */
@media (max-width:1100px){
  html[data-glass] #ky-music{--kym-w:min(520px, 100vw - 100px);}
  html[data-glass] #ky-music .kym-vol input{width:44px;}
  html[data-glass] #ky-music .kym-meta{width:104px;}
}

/* bottom dock — the panel opens ABOVE the row (column-reverse) so the
   transport stays where the thumb already is */
@media (max-width:680px){
  html[data-glass] #ky-music{
    top:auto; bottom:calc(12px + env(safe-area-inset-bottom, 0px));
    --kym-w:min(430px, 100vw - 24px);
    --kym-r:27px;
    flex-direction:column-reverse;
    animation-name:kymEnterDock;
  }
  html[data-glass] #ky-music .kym-row{height:54px; flex-basis:54px; gap:10px; padding:0 10px 0 8px;}
  html[data-glass] #ky-music .kym-vol{display:none;}
  html[data-glass] #ky-music .kym-meta{width:auto; flex:1 1 auto; min-width:52px;}
  html[data-glass] #ky-music .kym-prog{flex:0 1 118px; min-width:66px;}
  html[data-glass] #ky-music .kym-art{width:36px; height:36px; flex:0 0 36px; border-radius:11px;}
  html[data-glass] #ky-music .kym-ctl{gap:10px;}
  html[data-glass] #ky-music .kym-panel-in{
    border-top:0;
    border-bottom:1px solid color-mix(in srgb, var(--kym-ink) 14%, transparent);
    padding:10px 12px 8px;
  }
  html[data-glass] #ky-music .kym-note{padding-top:6px; margin-top:6px;}
  html[data-glass] #ky-music .kym-keys{display:none;}
  html[data-glass] #ky-music .kym-item{min-height:44px;}   /* touch target */
  html[data-glass] #ky-music.kym-min{--kym-w:118px;}
}
@keyframes kymEnterDock{
  from{opacity:0; transform:translateX(-50%) translateY(16px);}
  to  {opacity:1; transform:translateX(-50%) translateY(0);}
}
@media (max-width:480px){
  html[data-glass] #ky-music .kym-time{display:none;}
  html[data-glass] #ky-music .kym-prog{flex:0 1 92px; min-width:54px;}
  html[data-glass] #ky-music .kym-ctl{gap:8px;}
  html[data-glass] #ky-music .kym-tools{gap:4px;}
}

/* the bar must never be taller than the viewport it docks in */
@media (max-height:560px){
  html[data-glass] #ky-music .kym-wave{display:none;}
  html[data-glass] #ky-music .kym-item{min-height:30px;}
}

/* ── (12) a11y ───────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce){
  html[data-glass] #ky-music{animation:none; transition:none;}
  #ky-music .kym-t.kym-scroll{
    animation:none; width:auto; overflow:hidden; text-overflow:ellipsis;
  }
  #ky-music.playing .kym-eq i{animation:none; transform:scaleY(.6);}
  #ky-music.kym-loading .kym-spin{animation:none; opacity:.55;}
  #ky-music .kym-panel,
  #ky-music.kym-open .kym-panel{transition:none;}
  #ky-music .kym-art, #ky-music .kym-sk, #ky-music .kym-play,
  #ky-music .kym-bar, #ky-music .kym-fill::after, #ky-music .kym-eq,
  #ky-music .kym-eq i, #ky-music .kym-viz, #ky-music .kym-item,
  #ky-music .kym-queue, #ky-music .kym-fold, #ky-music .kym-fold svg,
  #ky-music .kym-mute, #ky-music .kym-artist, #ky-music .kym-prog,
  #ky-music .kym-time, #ky-music .kym-play .kym-g i,
  #ky-music .kym-vol input::-webkit-slider-thumb{transition:none;}

  /* THE TRACK CHANGE BECOMES AN INSTANT SWAP. ky-musicfx.js already declines
     to clone a ghost under this query, so nothing normally reaches these
     rules — they are the backstop for the case where the query flips after a
     swap has started, which would otherwise leave a ghost frozen mid-travel
     on top of the live title with `both` fill holding it there. Belt and
     braces, and cheap: a display:none on an element that will be removed in
     under half a second. */
  #ky-music .kym-t-ghost{display:none;}
  #ky-music.kym-swap .kym-title .kym-t,
  #ky-music.kym-swap .kym-art,
  #ky-music .kym-sk.kym-nudge svg{animation:none;}
  #ky-music.kym-swap .kym-fill{transition:none;}
  #ky-music.kym-swap .kym-eq i{transition:none;}
  /* the glyph changes state rather than morphing through it */
  #ky-music .kym-play .kym-g i{transition:none;}

  /* no analyser motion either — the ladder stays as a static level read */
  #ky-music.kym-viz-on .kym-viz{opacity:0;}
  #ky-music.kym-viz-on .kym-eq{opacity:1;}
}
/* .lg-surface drops its own backdrop-filter under this query; nothing here
   re-adds one, so the bar simply inherits whatever opaque fallback it gets. */

/* ── (13) PLATE TINT: this bar travels, so it needs more than the shared 14% ─
   liquid-glass.css sets --lg-tint:14% and its comment claims that "survives
   travelling over both the amber hero and the dark #Works band". Measured off
   rendered pixels, it does not — not for type this small:

                        amber hero      dark band
     title    (12px)      11.78:1         3.05:1
     artist   (9.5px)     10.18:1         2.60:1

   The bar's ink is pinned dark (--kym-ink -> --color-foreground, rgb(20,17,10))
   while its plate tracks whatever is behind it. Over the amber hero the plate
   composites to rgb(233,203,81) and the dark ink sings; over the dark bands it
   composites to rgb(98,97,90) and the whole bar turns to grey-on-grey. Both
   lines are small text, so both owe 4.5:1, and both miss it for roughly half
   the page's scroll length.

   #ky-music is the ONLY surface with this problem, because it is the only one
   that flies over every ground the site has — the trays and cards each sit on
   a known backdrop. So the fix is scoped to it rather than to --lg-tint
   globally, which would flatten glass that is currently fine.

   Solved against the ARTIST line, not the title: at 9.5px with its ink already
   softened to 93%, it is the worst case on the bar, and a tint that clears the
   title still leaves it short (34% measured 4.94:1 title but only 4.23:1
   artist). 38% carries both — measured 4.65:1 artist, 5.988:1 title on the
   dark band, and the amber hero only gets better.

   This is NOT a return to the old --lg-tint:80% override that the earlier note
   in this file removed for making the bar the most opaque thing on the site —
   at 38% nearly two thirds of the backdrop still comes through. */
html[data-glass] #ky-music{ --lg-tint: 38%; }
