/* Bus Driver Playlist
   Layers, bottom to top: image stack, warmth wash, grain, vignette, UI. */

/* ---------- fonts (self-hosted, see fonts/NOTICE.md) ---------- */

/* Latin. Variable, so one file covers every weight. Devanagari text falls
   through to Noto below via unicode-range. */
@font-face{
  font-family:'Host Grotesk';
  src:url('fonts/hostgrotesk.woff2') format('woff2');
  font-weight:100 900;
  font-display:swap;
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}

@font-face{
  font-family:'Noto Sans Devanagari';
  src:url('fonts/noto-devanagari.woff2') format('woff2');
  font-weight:100 900;
  font-display:swap;
  unicode-range:U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9,
                U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
@font-face{
  font-family:'Noto Sans Devanagari';
  src:url('fonts/noto-latin.woff2') format('woff2');
  font-weight:100 900;
  font-display:swap;
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}

:root{
  --ink:      #14100c;
  --cream:    #f4e6cd;
  --amber:    #e8a33d;
  --dim:      rgba(244,230,205,.62);

  --dwell:    7s;     /* must match DWELL_MS in app.js */
  --fade:     1200ms; /* must match FADE_MS  in app.js */

  /* Album art. The iframe is *built* at --disc-native (YouTube's documented
     200px minimum, so their player renders normally) and then visually scaled
     down to --disc. Keep --disc-scale equal to --disc / --disc-native. */
  --disc:        80px;
  --disc-native: 200px;
  --disc-scale:  .4;

  --pad:      clamp(14px, 3.2vw, 34px);
  --safe-b:   env(safe-area-inset-bottom, 0px);

  /* Devanagari first; the rest are the OS Hindi faces, for the moment
     before the webfont lands and if it ever fails to. */
  --font:     'Host Grotesk', 'Noto Sans Devanagari', 'Nirmala UI',
              'Kohinoor Devanagari', 'Mangal', ui-sans-serif, system-ui, sans-serif;

  /* Player palette, from the Figma spec. */
  --glass-fill:   rgba(217,217,217,.20);   /* #D9D9D9 @ 20% */
  --glass-stroke: #B5B5B5;
  --accent:       #FFB591;
}

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

html,body{ height:100%; }

body{
  margin:0;
  min-height:100dvh;
  background:var(--ink);
  color:var(--cream);
  font-family:var(--font);
  overflow:hidden;
  -webkit-font-smoothing:antialiased;
}

/* ---------- image stage ---------- */

.stage{ position:fixed; inset:0; z-index:0; }
.slides{ position:absolute; inset:0; }

.slide{
  position:absolute; inset:0;
  opacity:0;
  transition:opacity var(--fade) ease-in-out;
  will-change:opacity;
}
.slide.is-active{ opacity:1; }

.slide picture{ display:block; width:100%; height:100%; }

.slide img{
  width:100%; height:100%;
  object-fit:cover;          /* centre-crop; 9:16 sources drop in here later */
  object-position:center;
  display:block;
}

/* Slow drift. Masks the 40s loop and adds life to a still frame. */
.slide.is-active img{
  animation:kenburns calc(var(--dwell) + var(--fade)) ease-out both;
}
@keyframes kenburns{
  from{ transform:scale(1.005); }
  to  { transform:scale(1.055); }
}

/* Unifies four images that don't share a palette. */
.warmth{
  position:absolute; inset:0;
  background:linear-gradient(180deg, rgba(232,163,61,.10), rgba(20,16,12,.16) 55%, rgba(20,16,12,.50));
  mix-blend-mode:multiply;
  pointer-events:none;
}

.grain{
  position:absolute; inset:0;
  pointer-events:none;
  opacity:.16;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.vignette{
  position:absolute; inset:0;
  pointer-events:none;
  background:radial-gradient(120% 80% at 50% 40%, transparent 38%, rgba(20,16,12,.76) 100%);
}

/* ---------- masthead ---------- */

.masthead{
  position:fixed; z-index:30;
  top:0; left:0; right:0;
  /* The artwork is narrow enough to sit beside the right-hand controls, so
     it only needs a little clearance here. Phones centre the topbar, so
     they keep the full drop — see the ≤640px block. */
  padding:calc(var(--pad) - .3rem) var(--pad) 2.2rem;
  text-align:center;
  pointer-events:none;

  /* Fades out downward. The illustrations run bright behind the masthead
     (sunlit windscreens, pale skies), and white text alone was fighting them. */
  background:linear-gradient(180deg,
    rgba(20,16,12,.80) 0%,
    rgba(20,16,12,.62) 32%,
    rgba(20,16,12,.30) 66%,
    transparent 100%);
}

.masthead__title{
  margin:0;
  color:#fff;
  font-weight:800;
  font-size:clamp(2.54rem, 9.1vw, 5.46rem);
  line-height:1.16;
  letter-spacing:.005em;
  text-shadow:0 2px 4px rgba(0,0,0,.5), 0 6px 34px rgba(0,0,0,.62);
  text-wrap:balance;
}

/* Title artwork. Alpha is baked into the asset from the brightest channel,
   so the black ground is genuinely gone. A blend mode cannot be used here:
   .masthead is position:fixed with a z-index, which forms a stacking
   context, so `screen` would blend against the masthead's own gradient
   instead of the photograph — and render as a black slab. */
.masthead__title img{
  display:block;
  width:min(34vw, 420px);
  height:auto;
  margin-inline:auto;
  filter:drop-shadow(0 6px 18px rgba(0,0,0,.55));
}

.masthead__sub{
  /* Negative: the artwork carries its own glow margin below the letters,
     so a positive value here reads as a much larger gap than it is. */
  margin:-.85rem 0 0;
  color:rgba(255,255,255,.9);
  font-weight:600;
  font-size:clamp(1.05rem, 3.1vw, 1.42rem);
  line-height:1.4;
  text-shadow:0 2px 4px rgba(0,0,0,.5), 0 4px 22px rgba(0,0,0,.55);
}

/* ---------- playing HUD ---------- */

.hud{
  position:fixed; inset:0; z-index:10;
  display:flex; flex-direction:column;
  align-items:center; justify-content:flex-end;
  gap:.85rem;
  padding:var(--pad);
  padding-bottom:calc(var(--pad) + var(--safe-b));
  pointer-events:none;
}
.hud[hidden]{ display:none; }
.hud > *{ pointer-events:auto; }

/* player ---------------------------------------------------------- */

/* Liquid glass. Figma's effect is a real refraction shader; CSS has no such
   thing, so this reproduces what the browser can actually do — frost, a
   fill, a fading edge stroke, and inset highlights standing in for depth —
   and approximates the rest. See spec.md §15. */
.player{
  position:relative;
  display:flex; align-items:center;
  padding:10px;
  padding-right:clamp(14px, 3vw, 22px);
  border:0;
  border-radius:999px;
  background:var(--glass-fill);

  /* Frost 8 + refraction, as far as a backdrop filter goes. */
  backdrop-filter:blur(9px) saturate(180%) brightness(1.06);
  -webkit-backdrop-filter:blur(9px) saturate(180%) brightness(1.06);

  /* Depth 28: specular run along the -45° light direction (top-left in,
     bottom-right out), plus the cast shadow. */
  box-shadow:
    inset 1px 1px 1px rgba(255,255,255,.55),
    inset -1px -1px 1px rgba(255,255,255,.14),
    inset 0 14px 28px -18px rgba(255,255,255,.5),
    0 18px 52px rgba(0,0,0,.45);

  width:auto;                /* shrink-wraps the fixed title width below */
  max-width:94vw;
}

/* Gradient stroke: 2px outside, #B5B5B5 opaque → transparent → opaque.
   Drawn as a ring by masking the padding box out of the border box. */
.player::before{
  content:'';
  position:absolute; inset:-2px;
  border-radius:inherit;
  padding:2px;
  background:linear-gradient(100deg,
    rgba(181,181,181,1) 0%,
    rgba(181,181,181,0) 48%,
    rgba(181,181,181,0) 56%,
    rgba(181,181,181,1) 100%);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite:exclude;
  pointer-events:none;
}

/* Dispersion 63: a faint chromatic split at the rim. Subtle on purpose —
   at full strength it reads as a rendering fault rather than glass. */
.player::after{
  content:'';
  position:absolute; inset:-2px;
  border-radius:inherit;
  pointer-events:none;
  box-shadow:
    inset 1px 0 1.5px rgba(120,190,255,.28),
    inset -1px 0 1.5px rgba(255,150,120,.26);
}

/* Record ring. Sits *behind* and slightly proud of the disc, reading as the
   outer edge of the record. Turns at the same rate as the vinyl face so the
   two read as one object. */
.player__art{
  position:relative; flex:0 0 auto;
  width:var(--disc); height:var(--disc);
  display:grid; place-items:center;
}

.player__ring{
  position:absolute;
  width:calc(var(--disc) + 13px);
  height:calc(var(--disc) + 13px);
  border-radius:50%;
  background:
    conic-gradient(from 0deg,
      rgba(255,255,255,.9) 0deg 12deg,
      rgba(255,255,255,.18) 12deg 120deg,
      rgba(255,255,255,.34) 120deg 240deg,
      rgba(255,255,255,.18) 240deg 360deg);
  animation:spin 7s linear infinite;
  animation-play-state:paused;      /* runs only while the music plays */
  pointer-events:none;
}
.player.is-playing .player__ring{ animation-play-state:running; }

@keyframes spin{ to{ transform:rotate(360deg); } }

.player__disc{
  position:relative; z-index:1; flex:0 0 auto;
  width:var(--disc); height:var(--disc);
  border-radius:50%;
  overflow:hidden;
  background:#000;
  box-shadow:
    inset 0 0 0 3px rgba(20,16,12,.92),
    inset 0 0 0 4px rgba(255,255,255,.34),
    0 6px 18px rgba(0,0,0,.55);
}

/* Vinyl face. Covers the iframe: the player is still there and still the
   source of the audio, but what shows is the record. Grooves are drawn as
   fine repeating rings, with the track thumbnail as the centre label. */
.player__vinyl{
  position:absolute; inset:0; z-index:2;
  border-radius:50%;
  display:grid; place-items:center;
  overflow:hidden;
  /* Swallows clicks rather than passing them to the iframe underneath: with
     the video hidden, a click landing on it would pause the song with no
     visible cause. Playback belongs to the buttons. */
  pointer-events:auto;
  background:
    repeating-radial-gradient(circle at 50% 50%,
      rgba(255,255,255,.055) 0 1px,
      rgba(0,0,0,0) 1px 4px),
    radial-gradient(circle at 32% 26%,
      rgba(255,255,255,.16), rgba(255,255,255,0) 58%),
    #0d0b09;
  animation:spin 7s linear infinite;
  animation-play-state:paused;      /* runs only while the music plays */
}
.player.is-playing .player__vinyl{ animation-play-state:running; }

/* Centre label. Sized as a label, not a poster — the grooves stay visible
   around it. mqdefault is 16:9 with no letterbox bars, so cover-cropping it
   into the circle loses only the sides. */
.player__thumb{
  width:56%; height:56%;
  border-radius:50%;
  object-fit:cover;
  opacity:0;
  transition:opacity .45s ease;
  box-shadow:0 0 0 1px rgba(0,0,0,.6), 0 0 0 2px rgba(255,255,255,.14);
}
.player__thumb.is-ready{ opacity:1; }

/* Spindle hole, over the middle of the label. */
.player__spindle{
  position:absolute;
  width:11%; height:11%;
  border-radius:50%;
  background:#14100c;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.22);
}

/* The API swaps #ytplayer for an iframe of the same id. Built 16:9 at the
   native height so YouTube renders a normal player, then scaled down and
   centred to fill the small circle. It sits under the vinyl face above. */
.player__disc #ytplayer,
.player__disc iframe{
  position:absolute; top:50%; left:50%;
  width:calc(var(--disc-native) * 16 / 9);
  height:var(--disc-native);
  transform:translate(-50%,-50%) scale(var(--disc-scale));
  transform-origin:center;
  border:0;
  display:block;
}

/* Sits between disc and controls, vertically centred against the art.
   Fixed width rather than flex-grow: it sets how much title is visible and
   keeps the pill from stretching wider than it needs to. Asymmetric margins
   give 28px from the art and a tighter 16px to the controls. */
.player__meta{
  flex:0 1 auto;
  width:200px;
  min-width:0;
  margin-left:28px;
  margin-right:16px;
  text-align:left;
}

.player__label{
  margin:0 0 .2rem;
  font-size:.62rem;
  font-weight:700;
  letter-spacing:.24em;       /* 24%, per the spec */
  text-transform:uppercase;
  color:var(--accent);
}

/* A fixed one-line window. Titles that fit sit centred; titles that don't
   scroll as a marquee. Both edges are mask-faded rather than clipped, so a
   long title never ends in a hard cut. */
.player__title{
  --fade: 18px;
  --marquee-gap: 40px;      /* must match MARQUEE_GAP_PX in app.js */

  margin:0;
  display:flex;
  justify-content:flex-start;
  overflow:hidden;
  font-size:clamp(1rem, 2.7vw, 1.22rem);
  font-weight:700;
  color:#fff;
  line-height:1.32;
  height:1.32em;            /* the defined space for the title */

  /* Static: fade the right edge only. A left fade would wash out the first
     characters of a left-aligned title that isn't going anywhere. */
  -webkit-mask-image:linear-gradient(90deg,
    #000 0, #000 calc(100% - var(--fade)), transparent 100%);
  mask-image:linear-gradient(90deg,
    #000 0, #000 calc(100% - var(--fade)), transparent 100%);
}

/* Scrolling: fade both edges, so text enters and leaves softly. */
.player__title.is-scrolling{
  -webkit-mask-image:linear-gradient(90deg,
    transparent 0, #000 var(--fade),
    #000 calc(100% - var(--fade)), transparent 100%);
  mask-image:linear-gradient(90deg,
    transparent 0, #000 var(--fade),
    #000 calc(100% - var(--fade)), transparent 100%);
}

.title__track{
  display:flex;
  flex:0 0 auto;                     /* never shrink, or it can't overflow */
  gap:var(--marquee-gap);
  white-space:nowrap;
}
.title__track > span{ flex:0 0 auto; }

.player__title.is-scrolling .title__track{
  animation:marquee var(--marquee-duration, 12s) linear infinite;
  will-change:transform;
}

@keyframes marquee{
  from{ transform:translateX(0); }
  to  { transform:translateX(calc(-1 * var(--marquee-distance, 0px))); }
}

.player__controls{
  display:flex; gap:.35rem; align-items:center;
  flex:0 0 auto;              /* spacing comes from the .player gap */
}

/* seek bar -------------------------------------------------------- */

.seek{
  position:relative;
  margin-top:.5rem;
  height:12px;
  cursor:pointer;
  touch-action:none;          /* let us drag without the page scrolling */
}
.seek:focus-visible{ outline:2px solid var(--amber); outline-offset:3px; border-radius:4px; }

.seek__track{
  position:absolute; inset-inline:0; top:50%;
  height:5px; transform:translateY(-50%);
  border-radius:999px;
  background:rgba(255,255,255,.30);
  overflow:hidden;
}
.seek__fill{
  height:100%; width:0%;
  border-radius:999px;
  background:var(--accent);
}
.seek__knob{
  position:absolute; top:50%; left:0;
  width:10px; height:10px;
  transform:translate(-50%,-50%);
  border-radius:50%;
  background:#fff;
  box-shadow:0 1px 4px rgba(0,0,0,.6);
  opacity:0;
  transition:opacity 160ms;
}
.seek:hover .seek__knob,
.seek:focus-visible .seek__knob,
.seek.is-dragging .seek__knob{ opacity:1; }

.seek__time{
  margin:.32rem 0 0;
  font-size:.66rem;
  font-variant-numeric:tabular-nums;
  color:rgba(255,255,255,.6);
}

/* listen-elsewhere links ------------------------------------------ */

.topbar{
  position:fixed; z-index:31;
  top:calc(var(--pad) + .4rem); right:var(--pad);
  display:flex; align-items:center; gap:.4rem;
}

.links{ display:flex; gap:.4rem; }
.links[hidden]{ display:none; }

/* Site-wide mute: music, horn and ambience together. */
.mute{
  display:grid; place-items:center;
  width:42px; height:42px;
  border:1px solid rgba(255,255,255,.28);
  border-radius:50%;
  background:rgba(20,16,12,.42);
  backdrop-filter:blur(8px);
  color:#fff;
  cursor:pointer;
  transition:background 180ms, border-color 180ms, transform 180ms;
}
.mute:hover{ background:rgba(255,181,145,.24); border-color:rgba(255,181,145,.6); }
.mute:active{ transform:scale(.94); }
.mute:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
.mute svg{ width:19px; height:19px; fill:currentColor; }
.mute.is-muted{ background:rgba(255,181,145,.26); border-color:rgba(255,181,145,.55); }

.links a{
  display:inline-flex; align-items:center; gap:.42rem;
  padding:.42rem .8rem;
  border:1px solid rgba(255,255,255,.28);
  border-radius:999px;
  background:rgba(20,16,12,.42);
  backdrop-filter:blur(8px);
  color:#fff; text-decoration:none;
  font-size:.76rem; font-weight:600;
  text-shadow:0 1px 3px rgba(0,0,0,.5);
  transition:background 180ms, border-color 180ms, transform 180ms;
}
.links a:hover{ background:rgba(232,163,61,.22); border-color:rgba(232,163,61,.6); }
.links a:active{ transform:scale(.96); }
.links svg{ width:15px; height:15px; fill:currentColor; flex:0 0 auto; }

.btn{
  width:42px; height:42px;
  display:grid; place-items:center;
  border:0;
  border-radius:50%;
  background:rgba(255,255,255,.22);
  box-shadow:inset 1px 1px 1px rgba(255,255,255,.4);
  color:#fff;
  cursor:pointer;
  transition:background 180ms, transform 180ms;
}
.btn:hover{ background:rgba(255,255,255,.34); }
.btn:active{ transform:scale(.94); }
.btn:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
.btn svg{ width:18px; height:18px; fill:currentColor; }
.btn--sm{ width:38px; height:38px; }
.btn--sm svg{ width:16px; height:16px; }

/* icon swapping */
.ico-play, .ico-muted{ display:none; }
.btn.is-paused .ico-play{ display:block; }
.btn.is-paused .ico-pause{ display:none; }
.mute.is-muted .ico-muted{ display:block; }
.mute.is-muted .ico-loud { display:none; }

/* Ambience toggle. Frosted fill in both states — only the stroke and the
   dot carry the colour, so it never reads as a solid chip. */
.amb{
  display:inline-flex; align-items:center; gap:.5rem;
  height:42px;
  padding:0 .95rem;
  border-radius:999px;
  border:1.5px solid rgba(74,222,128,.7);
  background:rgba(255,255,255,.10);
  backdrop-filter:blur(10px) saturate(160%);
  -webkit-backdrop-filter:blur(10px) saturate(160%);
  box-shadow:inset 1px 1px 1px rgba(255,255,255,.28);
  color:#fff;
  font-family:inherit; font-size:.78rem; font-weight:600;
  white-space:nowrap;
  cursor:pointer;
  transition:border-color 220ms, background 220ms, transform 180ms;
}
.amb:hover{ background:rgba(255,255,255,.17); }
.amb:active{ transform:scale(.96); }
.amb:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }

.amb__dot{
  width:9px; height:9px; flex:0 0 auto;
  border-radius:50%;
  background:#4ade80;
  box-shadow:0 0 0 3px rgba(74,222,128,.20), 0 0 8px rgba(74,222,128,.65);
  transition:background 220ms, box-shadow 220ms;
}

/* Off: same glass, orange stroke and dot. */
.amb.is-off{ border-color:rgba(255,150,80,.75); }
.amb.is-off .amb__dot{
  background:#ff9650;
  box-shadow:0 0 0 3px rgba(255,150,80,.20), 0 0 8px rgba(255,150,80,.6);
}

/* passengers ------------------------------------------------------ */

.passengers{
  margin:0;
  padding:.4rem .9rem;
  border:1px solid rgba(244,230,205,.14);
  border-radius:999px;
  background:rgba(20,16,12,.46);
  backdrop-filter:blur(6px);
  font-size:clamp(.74rem, 2.2vw, .84rem);
  color:var(--dim);
  white-space:nowrap;
}
.passengers[hidden]{ display:none; }
.passengers b{ color:var(--amber); font-weight:700; }

/* Live indicator. */
.passengers::before{
  content:'';
  display:inline-block;
  width:7px; height:7px;
  margin-right:.5rem;
  border-radius:50%;
  background:#4ade80;
  box-shadow:0 0 0 3px rgba(74,222,128,.16);
  vertical-align:.05em;
  animation:pulse 2.4s ease-in-out infinite;
}

@keyframes pulse{
  0%,100%{ opacity:1;   box-shadow:0 0 0 3px rgba(74,222,128,.16); }
  50%    { opacity:.72; box-shadow:0 0 0 5px rgba(74,222,128,.06); }
}

/* ---------- responsive ---------- */

/* Mobile keeps the same horizontal rounded rectangle — the small disc means
   there is no longer any need to stack it into a card.

   The 28px gaps that suit desktop leave only ~85px of title window on a 390px
   phone (57px at 360px), so every title marquees permanently and reads as
   broken. Trimming the gaps and controls here brings it back to ~120px. To
   force a strict 28px everywhere, delete this block. */
@media (max-width:640px){
  /* Right-aligned pills land on top of the centred title once it wraps, so
     they get their own centred row and the masthead starts below them. */
  .topbar{ left:0; right:0; justify-content:center; top:calc(var(--pad) + .1rem); }
  .links a{ padding:.36rem .7rem; font-size:.72rem; }
  /* Topbar is centred here, so the masthead must start below it. */
  .masthead{ padding-top:calc(var(--pad) + 3.3rem); }
  .masthead__title img{ width:72vw; }

  .player{ width:100%; padding-right:12px; }
  .player__meta{ flex:1 1 auto; width:auto; margin-left:16px; margin-right:12px; }
  .player__controls{ gap:.3rem; }
  .btn{ width:34px; height:34px; }
}

@media (max-height:560px) and (orientation:landscape){
  .masthead__sub{ display:none; }
  .masthead{ padding-bottom:2rem; }
}

/* ---------- accessibility ---------- */

@media (prefers-reduced-motion:reduce){
  .slide{ transition:none; }
  .slide.is-active img{ animation:none; }
  .passengers::before{ animation:none; }
  .player.is-playing .player__ring,
  .player.is-playing .player__vinyl{ animation:none; }
  /* No scrolling. The mask still fades the overflow, so it reads as
     "continues" rather than being chopped off. */
  .player__title.is-scrolling .title__track{ animation:none; }
}
