/* ── hwic.dev shared music player ──────────────────────────────────────────
   Lives on every page, injected by /player.js. Self-contained on purpose: the
   fallbacks in each var() are here because movie.html declares its own reduced
   token set and does not load /style.css. */
.player{
  --pl-ease:var(--ease,cubic-bezier(.2,.9,.25,1));
  --pl-aqua:var(--aqua,#3fd0e0);
  --pl-aqua-dim:var(--aqua-dim,#2aa6c4);
  --pl-line:var(--line,rgba(255,255,255,.08));
  --pl-fg:var(--fg,#eef1f2);
  --pl-muted:var(--muted,#7f8890);
  --pl-faint:var(--faint,#4a5158);
  position:fixed;right:max(16px,env(safe-area-inset-right));bottom:max(16px,env(safe-area-inset-bottom));
  z-index:40;display:flex;align-items:center;gap:10px;padding:8px;border-radius:999px;
  background:rgba(13,15,18,.86);border:1px solid var(--pl-line);
  -webkit-backdrop-filter:blur(14px) saturate(1.4);backdrop-filter:blur(14px) saturate(1.4);
  box-shadow:0 10px 30px rgba(0,0,0,.45),0 0 0 1px rgba(255,255,255,.02) inset;
  opacity:0;transform:translateY(16px) scale(.96);
  transition:opacity .5s var(--pl-ease),transform .5s var(--pl-ease);
  pointer-events:none;max-width:min(360px,calc(100vw - 28px));overflow:hidden;
  -webkit-user-select:none;user-select:none}
.player.show{opacity:1;transform:none;pointer-events:auto}
.player:hover{border-color:rgba(63,208,224,.3)}
.player[hidden]{display:none!important}

.player .pp{width:36px;height:36px;flex:none;border:none;border-radius:50%;cursor:pointer;
  display:grid;place-items:center;background:var(--pl-aqua);color:#04181c;transition:.2s var(--pl-ease);padding:0}
.player .pp:hover{background:#5ce0f0;transform:scale(1.05)}
.player .pp:active{transform:scale(.96)}
.player .pp svg{width:13px;height:13px;fill:currentColor;pointer-events:none}

.player .meta{min-width:0;display:flex;flex-direction:column;gap:3px;flex:1;padding-right:4px}
.player .line{display:flex;align-items:center;gap:8px;min-width:0}
.player .nmwrap{position:relative;flex:1;min-width:0;overflow:hidden;height:16px}
/* a block, not a flex row — scrollWidth on a flex container does not report the
   overflow player.js needs to decide whether the title has to scroll */
.player .nm{position:absolute;left:0;top:0;height:100%;max-width:100%;display:block;
  line-height:16px;font-size:12.5px;color:var(--pl-fg);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.005em}
.player .tm{font-size:10.5px;color:var(--pl-muted);font-variant-numeric:tabular-nums;flex:none;letter-spacing:.02em}
.player .tm b{font-weight:500;color:var(--pl-fg);opacity:.85}
.player.idle .tm{opacity:.6}
.player.playing{border-color:rgba(63,208,224,.22);
  box-shadow:0 10px 30px rgba(0,0,0,.45),0 0 22px -8px rgba(63,208,224,.35),0 0 0 1px rgba(255,255,255,.02) inset}

/* ── track change: the name slides out and the next one rises in, under a single
     aqua sweep across the pill. Cheap, and it makes a silent swap legible. ── */
@keyframes plNmOut{to{opacity:0;transform:translateY(-8px)}}
@keyframes plNmIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes plSweep{0%{transform:translateX(-140%);opacity:0}18%{opacity:1}100%{transform:translateX(240%);opacity:0}}
@keyframes plPulse{0%{box-shadow:0 0 0 0 rgba(63,208,224,.34)}100%{box-shadow:0 0 0 14px rgba(63,208,224,0)}}
.player .nm.out{animation:plNmOut .19s var(--pl-ease) forwards}
.player .nm.in{animation:plNmIn .44s var(--pl-ease)}
.player .sweep{position:absolute;inset:0;border-radius:999px;overflow:hidden;pointer-events:none;z-index:1}
.player .sweep i{position:absolute;top:0;bottom:0;left:0;width:38%;opacity:0;
  background:linear-gradient(90deg,transparent,rgba(63,208,224,.20),transparent)}
.player.changed .sweep i{animation:plSweep .8s var(--pl-ease)}
.player.changed .pp{animation:plPulse .8s var(--pl-ease)}

/* equaliser doubles as the "now playing" tell */
.player .eq{display:flex;align-items:flex-end;gap:2px;height:11px;flex:none;padding-bottom:1px}
.player .eq i{width:2px;background:var(--pl-aqua);border-radius:1px;height:2px;opacity:.45;transition:opacity .25s}
.player.playing .eq i{opacity:1;animation:plEq .95s ease-in-out infinite}
.player.playing .eq i:nth-child(2){animation-delay:.16s;animation-duration:.78s}
.player.playing .eq i:nth-child(3){animation-delay:.33s;animation-duration:1.1s}
@keyframes plEq{0%,100%{height:2px}50%{height:11px}}

/* seek line: 3px of paint, 14px of touch target */
.player .seek{position:relative;height:3px;border-radius:2px;background:rgba(255,255,255,.11);cursor:pointer;
  flex:1;min-width:54px}
.player .seek:before{content:"";position:absolute;left:0;right:0;top:-7px;bottom:-7px}
.player .seek .fill{position:absolute;left:0;top:0;bottom:0;width:0%;border-radius:2px;
  background:linear-gradient(90deg,var(--pl-aqua-dim),var(--pl-aqua));transition:width .12s linear}
.player .seek .knob{position:absolute;top:50%;left:0;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;
  background:var(--pl-aqua);box-shadow:0 0 0 3px rgba(63,208,224,.18);opacity:0;transition:opacity .2s}
.player:hover .seek .knob,.player.seeking .seek .knob{opacity:1}
.player.seeking .seek .fill{transition:none}
/* a reset to 0 must not animate backwards — that reads as a glitch, not a new track */
.player.jump .seek .fill{transition:none}

.player .nx,.player .pv{border:none;background:none;color:var(--pl-faint);width:26px;height:26px;flex:none;padding:0;
  display:grid;place-items:center;cursor:pointer;border-radius:8px;transition:.18s var(--pl-ease)}
.player .nx:hover,.player .pv:hover{color:var(--pl-fg);background:rgba(255,255,255,.06)}
.player .nx svg,.player .pv svg{width:12px;height:12px;fill:currentColor;pointer-events:none}

/* ── volume: always on show. It used to be width:0 until hover, which meant it
     did not exist at all for touch and was invisible to everyone else. ── */
.player .vol{display:flex;align-items:center;gap:5px;flex:none}
.player .mute{border:none;background:none;padding:2px;flex:none;cursor:pointer;
  display:grid;place-items:center;border-radius:6px;transition:background .18s}
.player .mute:hover{background:rgba(255,255,255,.06)}
.player .vol .spk{width:13px;height:13px;display:block;fill:var(--pl-muted);
  transition:fill .2s;pointer-events:none}
.player:hover .vol .spk{fill:var(--pl-fg)}
.player .vol.muted .spk{fill:var(--pl-faint)}
/* Where the platform ignores HTMLMediaElement.volume (iOS), the slider is a lie —
   drop it and leave the mute button, which IS honoured there. */
.player.novol .vol input{display:none}
.player .vol input{width:64px;height:4px;-webkit-appearance:none;appearance:none;
  background:linear-gradient(90deg,var(--pl-aqua) 0 var(--pct,35%),rgba(255,255,255,.16) var(--pct,35%) 100%);
  border-radius:3px;outline:none;margin:0;cursor:pointer}
.player .vol input::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;
  background:#fff;border:2px solid var(--pl-aqua);box-shadow:0 1px 4px rgba(0,0,0,.5);cursor:pointer;transition:transform .15s}
.player .vol input:hover::-webkit-slider-thumb,.player .vol input:active::-webkit-slider-thumb{transform:scale(1.18)}
.player .vol input::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:#fff;
  border:2px solid var(--pl-aqua);box-shadow:0 1px 4px rgba(0,0,0,.5);cursor:pointer}
.player .vol input::-moz-range-track{height:4px;border-radius:3px;background:transparent}

@media(max-width:560px){
  .player{gap:9px;max-width:calc(100vw - 24px)}
  .player .nm{font-size:12px}
  .player .tm{display:none}
  .player .vol input{width:48px}
}
@media(pointer:coarse){
  /* 4px of track is not a touch target. Pad it out to ~24px while the painted bar stays
     the same height, via background-clip. */
  .player .vol input{height:24px;padding:10px 0;margin:-10px 0;background-clip:content-box}
  .player{padding:9px}
  .player .pp{width:40px;height:40px}
  .player .pp svg{width:14px;height:14px}
  .player .nx,.player .pv{width:34px;height:34px}
  .player .nx svg,.player .pv svg{width:13px;height:13px}
  .player .seek{height:4px}
  .player .vol input{height:6px}
}
@media(prefers-reduced-motion:reduce){
  .player{transition:opacity .2s}
  .player.playing .eq i{animation:none;height:7px}
  .player .nm.out,.player .nm.in{animation:none}
  .player.changed .sweep i,.player.changed .pp{animation:none}
}

/* ── keeping out of the page's way ───────────────────────────────────────
   The pill is fixed to the bottom-right corner, which is exactly where a footer
   puts its right-hand text. `has-player` is set by player.js once the widget is
   actually shown, so a page with music off reserves nothing. */
body.has-player:not(.screen){padding-bottom:88px}
@media(max-width:560px){body.has-player:not(.screen){padding-bottom:78px}}
/* Locked full-height pages cannot grow, so the footer yields its optional half instead. */
body.screen.has-player .foot .r{display:none}

/* ── long titles ─────────────────────────────────────────────────────────
   A phone gives the pill about 150px for the name, so "Style - Taylor Swift"
   was simply cut off. Instead of shrinking the text, the name eases across and
   back — but ONLY when it genuinely does not fit, which player.js measures. */
.player .nm.roll{max-width:none;overflow:visible;text-overflow:clip;
  animation:plRoll var(--roll-dur,9s) ease-in-out infinite alternate}
@keyframes plRoll{0%,14%{transform:translateX(0)}86%,100%{transform:translateX(var(--roll-x,0))}}

/* ── swipe away ──────────────────────────────────────────────────────────
   pan-y keeps vertical page scrolling with the page; the horizontal gesture is
   ours. The two sliders opt out again, since they own their own drag. */
.player{touch-action:pan-y}
.player .seek,.player .vol input{touch-action:none}
/* The pill blurs whatever is behind it. While it is moving, that blur is recomputed
   every single frame, which is what made the swipe stutter — so for the duration of the
   gesture it becomes a solid fill (visually near-identical on this dark background) and
   gets promoted to its own layer. */
.player.dragging,.player.dismissing{
  will-change:transform,opacity;
  -webkit-backdrop-filter:none;backdrop-filter:none;background:rgba(13,15,18,.95)}
.player.dragging{transition:none}
.player.dismissing{transition:transform .34s var(--pl-ease),opacity .34s var(--pl-ease)}

/* The way back. Deliberately small — someone who swiped the player away does not
   want a second one in its place, only a way to change their mind. */
.plrestore{position:fixed;right:max(16px,env(safe-area-inset-right));
  bottom:max(16px,env(safe-area-inset-bottom));z-index:39;
  width:38px;height:38px;border-radius:50%;padding:0;cursor:pointer;
  display:grid;place-items:center;
  border:1px solid var(--line,rgba(255,255,255,.08));background:rgba(13,15,18,.86);
  -webkit-backdrop-filter:blur(14px) saturate(1.4);backdrop-filter:blur(14px) saturate(1.4);
  box-shadow:0 8px 24px rgba(0,0,0,.4);
  color:var(--aqua,#3fd0e0);opacity:0;transform:scale(.82);
  transition:opacity .3s cubic-bezier(.2,.9,.25,1),transform .3s cubic-bezier(.2,.9,.25,1),border-color .2s}
.plrestore.show{opacity:1;transform:none}
.plrestore:hover{border-color:rgba(63,208,224,.45)}
.plrestore:active{transform:scale(.94)}
.plrestore svg{width:15px;height:15px;fill:currentColor;pointer-events:none}
.plrestore[hidden]{display:none!important}
@media(pointer:coarse){.plrestore{width:44px;height:44px}.plrestore svg{width:17px;height:17px}}

@media(prefers-reduced-motion:reduce){
  .player .nm.roll{animation:none;max-width:100%;overflow:hidden;text-overflow:ellipsis}
  .plrestore{transition:opacity .2s}
}
