/**
 * Unity 89.1 — homepage hero stylesheet.
 *
 * Ported from the approved prototype (.superpowers/brainstorm/
 * 11202-1790023625/room-v3.html, the `lay-marquee` stage plus the shared
 * `.v3` rules). Same easings, timings and colours; selectors renamed to the
 * u89-hero__* convention and every rule scoped under `.u89-hero` so nothing
 * can collide with erplayer's stylesheet, which loads site-wide.
 *
 * Keyframe names are namespaced u89* for the same reason.
 */

/* ------------------------------------------------------------------ *
 * MediaElement backstop
 *
 * assets/js/signal.js's shared <audio> element must stay attached to the
 * document (Safari/iOS refuses to play a detached one), which makes it a
 * visible target for the theme's own MediaElement.js auto-skinning --
 * signal.js opts it out of that scan (no-mejs/no-mediaelement classes) and
 * defensively hides any wrapper that appears anyway. This rule is the
 * belt to that braces: whatever catches it, the skin is never visible.
 * Mirrors erplayer's own identical rule for its player
 * (wp-content/plugins/erplayer/inc/frontend/assets/css/erplayer.css:
 * `.erplayer .mejs-container{display:none!important}`).
 * ------------------------------------------------------------------ */

.u89-hero .mejs-container {
  display: none !important;
}

/* ------------------------------------------------------------------ *
 * Root / stage
 * ------------------------------------------------------------------ */

.u89-hero {
  /* --eo: entrances. --es: swaps. Straight from the prototype. */
  --u89-eo: cubic-bezier(.16, 1, .3, 1);
  --u89-es: cubic-bezier(.65, 0, .35, 1);
  --u89-ink: #05050c;
  --u89-orange: #f89401;
  --u89-yellow: #fccd01;
  --u89-green: #3aa845;
  --u89-blue: #1a9ad9;
  --u89-purple: #bb1e82;
  --u89-red: #e0245e;
  --u89-spectrum: linear-gradient(92deg, #f89401, #fccd01, #3aa845, #1a9ad9, #bb1e82, #e0245e, #f89401);

  /* Progress Pride palette — the equalizer bars only.
     Deliberately NOT wired into --u89-spectrum: the edge-light and the
     station-ID backdrop stay on the logo-sampled spectrum above.
     Flag values, except --u89-pp-black; see .u89-hero__eq below. */
  --u89-pp-white:  #ffffff;
  --u89-pp-pink:   #f5a9b8;
  --u89-pp-cyan:   #5bcefa;
  --u89-pp-brown:  #613915;
  --u89-pp-black:  #0e0e14;
  --u89-pp-red:    #e40303;
  --u89-pp-orange: #ff8c00;
  --u89-pp-yellow: #ffed00;
  --u89-pp-green:  #008026;
  --u89-pp-blue:   #004dff;
  --u89-pp-purple: #750787;

  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 100%;
  min-height: 100vh; /* fallback first */
  min-height: 100svh;
  overflow: hidden;
  isolation: isolate;
  background: var(--u89-ink);
  color: #fff;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

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

/* ------------------------------------------------------------------ *
 * Backdrop: two cross-faded layers, veil, grain, animated edge
 * ------------------------------------------------------------------ */

.u89-hero__bg {
  position: absolute;
  inset: -14%;
  z-index: 1;
  opacity: 0;
  background-position: center;
  background-size: cover;
  filter: blur(40px) saturate(1.45);
  transition: opacity 1s var(--u89-es), filter .6s ease;
  animation: u89Breathe 14s ease-in-out infinite alternate;
}

.u89-hero__bg.on { opacity: 1; }

@keyframes u89Breathe {
  from { transform: scale(1.03) translate3d(-1%, 0, 0); }
  to   { transform: scale(1.13) translate3d(1%, -1%, 0); }
}

.u89-hero__veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: radial-gradient(75% 75% at 50% 42%, rgba(5, 5, 12, .5) 0%, rgba(5, 5, 12, .93) 100%);
}

.u89-hero__grain {
  position: absolute;
  inset: 0;
  z-index: 3;
  opacity: .15;
  mix-blend-mode: overlay;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

.u89-hero__edge {
  position: absolute;
  inset: 9px;
  z-index: 9;
  border-radius: 9px;
  pointer-events: none;
  padding: 1.5px;
  background: var(--u89-spectrum) border-box;
  background-size: 300% 100%;
  animation: u89Hue 9s linear infinite;
  opacity: .5;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  transition: opacity .5s ease, filter .5s ease;
}

@keyframes u89Hue { to { background-position: 300% 0; } }

/* `pulse` lands on the root for 1200ms on every track change. */
.u89-hero.pulse .u89-hero__edge {
  opacity: 1;
  filter: drop-shadow(0 0 10px rgba(224, 36, 94, .55));
}

/* ------------------------------------------------------------------ *
 * Live meta — on-air dot + listener count, top right
 * ------------------------------------------------------------------ */

.u89-hero__meta {
  position: absolute;
  top: 22px;
  right: 26px;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.u89-hero__live {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
}

.u89-hero__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--u89-red);
  box-shadow: 0 0 10px var(--u89-red);
  animation: u89Blink 2s ease-in-out infinite;
}

@keyframes u89Blink {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .45; transform: scale(.8); }
}

.u89-hero__listeners {
  color: rgba(255, 255, 255, .55);
  letter-spacing: .1em;
}

/* ------------------------------------------------------------------ *
 * Centre stack — logo, equalizer, sleeve, copy, transport
 * ------------------------------------------------------------------ */

.u89-hero__stack {
  position: relative;
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh; /* fallback first */
  min-height: 100svh;
  padding: 40px 16px 44px;
  text-align: center;
}

/* The composition is capped rather than distributed across the viewport.
   Without this the elements drift apart on a tall monitor and the screen
   stops reading as one designed object. Inert on a phone, where the
   content is already taller than the cap. */
.u89-hero__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-height: 780px;
}

.u89-hero__brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-bottom: 34px;
}

/* The height is !important and the selector carries the element type on
   purpose. On the live site this image was rendering 487px tall -- nearly
   four times the intended size -- because a more specific third-party rule
   (one of six stylesheets served cross-origin, so unreadable from the page)
   sets img height, letting the mark size itself from its intrinsic width.
   No !important is involved on their side; we lose on specificity alone.
   Since the opponent cannot be read, let alone edited, this element states
   its own size and means it. Scoped to our class: nothing else is affected. */
.u89-hero img.u89-hero__logo {
  display: block;
  width: auto !important;
  max-width: 100%;
  height: 124px !important;
  filter: drop-shadow(0 6px 22px rgba(0, 0, 0, .6));
}

.u89-hero__eq {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 20px;
}

.u89-hero__eq i {
  width: 4px;
  border-radius: 2px;
  background: currentColor;
  height: 5px;
  transition: height .12s linear;
  animation: u89Eq 1.05s ease-in-out infinite alternate;
}

/* Once stage.js is driving the bars from the analyser (see init()'s
   onLevels wiring) the decorative keyframes have to get out of the way --
   they animate the same property. Added only while real levels are
   arriving; removed, along with the inline heights, whenever they stop,
   so the keyframes are always the fallback rather than the exception. */
.u89-hero__eq.is-live i { animation: none; }

/* Progress Pride, one bar per stripe -------------------------------- *
 *
 * Eleven stripes, so eleven bars rather than eight: dropping stripes to
 * fit a fixed bar count would mean choosing which communities to leave
 * out, and doubling up would make some stripes read as more present than
 * others. Eleven bars at 4px on a 3px gap is 74px wide -- narrower than
 * the 72px-tall logo above them even at mobile size (the wordmark is
 * ~1.52:1, so ~109px wide there), so the row still reads as sitting
 * under the mark rather than out-spanning it.
 *
 * The order is the flag read left to right: the chevron from its outer
 * edge inward (white, pink, light blue, brown, black) and then the six
 * rainbow stripes top to bottom (red through purple). signal.js's
 * BAND_COUNT is 11 to match -- a bar with no band sits flat.
 *
 * Two stripes need help on a #05050c ground:
 *
 * - BLACK. Pure #000000 is DARKER than the stage behind it, so the
 *   stripe would simply not exist. It is not dropped: it is rendered as
 *   the darkest form in the row (#0e0e14, just off the ground so it has
 *   an edge of its own) and ringed in white, which also makes it the
 *   most sharply defined bar rather than the most easily missed one.
 * - BROWN. #613915 is the flag value, kept exactly, but it lands around
 *   1.7:1 against the stage -- present but dim. It gets the same ringed
 *   treatment at a lower strength, so the two chevron stripes that carry
 *   the BIPOC colours share one deliberate treatment instead of one
 *   looking like an exception.
 *
 * The trans WHITE stripe keeps #ffffff but is given a soft bloom and a
 * dark hairline, so it reads as an illuminated stripe rather than as a
 * stray piece of the white UI type around it.
 * ------------------------------------------------------------------ */

.u89-hero__eq i:nth-child(1)  { color: var(--u89-pp-white);  animation-delay: -.00s; }
.u89-hero__eq i:nth-child(2)  { color: var(--u89-pp-pink);   animation-delay: -.13s; }
.u89-hero__eq i:nth-child(3)  { color: var(--u89-pp-cyan);   animation-delay: -.41s; }
.u89-hero__eq i:nth-child(4)  { color: var(--u89-pp-brown);  animation-delay: -.22s; }
.u89-hero__eq i:nth-child(5)  { color: var(--u89-pp-black);  animation-delay: -.58s; }
.u89-hero__eq i:nth-child(6)  { color: var(--u89-pp-red);    animation-delay: -.31s; }
.u89-hero__eq i:nth-child(7)  { color: var(--u89-pp-orange); animation-delay: -.67s; }
.u89-hero__eq i:nth-child(8)  { color: var(--u89-pp-yellow); animation-delay: -.49s; }
.u89-hero__eq i:nth-child(9)  { color: var(--u89-pp-green);  animation-delay: -.07s; }
.u89-hero__eq i:nth-child(10) { color: var(--u89-pp-blue);   animation-delay: -.55s; }
.u89-hero__eq i:nth-child(11) { color: var(--u89-pp-purple); animation-delay: -.36s; }

/* Outset rings and the bloom: box-shadow only, so nothing here changes
   the 4px x 3px geometry of the row or the inline heights stage.js
   writes. */
.u89-hero__eq i:nth-child(1)  { box-shadow: 0 0 0 1px rgba(5, 5, 12, .9), 0 0 7px rgba(255, 255, 255, .42); }
.u89-hero__eq i:nth-child(4)  { box-shadow: 0 0 0 1px rgba(255, 255, 255, .34); }
.u89-hero__eq i:nth-child(5)  { box-shadow: 0 0 0 1px rgba(255, 255, 255, .58); }

@keyframes u89Eq {
  from { height: 5px; }
  to   { height: 20px; }
}

/* Sleeve --------------------------------------------------------- */

.u89-hero__coverwrap {
  position: relative;
  width: 196px;
  max-width: 60vw;
  aspect-ratio: 1;
  margin-bottom: 20px;
}

.u89-hero__cover {
  position: absolute;
  inset: 0;
  /* Above the up-next peek, which is the whole point of the peek: the
     next sleeve is PARTLY OCCLUDED by this one, as though queued behind
     it. The sleeve logo sits above both at z-index 2. */
  z-index: 1;
  border-radius: 14px;
  overflow: hidden;
  background-position: center;
  background-size: cover;
  background-color: rgba(255, 255, 255, .06);
  box-shadow: 0 34px 80px rgba(0, 0, 0, .62);
  transition: transform .8s var(--u89-eo), opacity .55s ease;
}

.u89-hero__cover.out {
  transform: translate3d(-12%, -7%, 0) rotate(-7deg) scale(.9);
  opacity: 0;
}

.u89-hero__cover.in {
  transform: translate3d(10%, 9%, 0) rotate(5deg) scale(.94);
  opacity: 0;
}

.u89-hero__sheen {
  position: absolute;
  inset: 0;
  transform: translateX(-120%);
  background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, .3) 46%, transparent 62%);
}

.u89-hero.pulse .u89-hero__sheen { animation: u89Sheen 1.1s var(--u89-eo); }

@keyframes u89Sheen { to { transform: translateX(120%); } }

/* The logo standing in for artwork during a station ID / offline. */
.u89-hero__sleeve-logo {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: none;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Geometrically centred is not optically centred here: the wordmark carries
     its weight up top while the dash marks trail thin below, so true centring
     leaves the sleeve looking bottom-heavy. Nudged down a few percent, and
     given more of the square, so the mark reads as filling it. */
  object-position: 50% 57%;
  padding: 7%;
  filter: drop-shadow(0 6px 22px rgba(0, 0, 0, .6));
}

/* Up next -- the queued sleeve ------------------------------------ *
 *
 * A second, smaller sleeve standing behind the current one and offset to
 * the right, as though the next song were queued up in the stack. At rest
 * only a ~12% slice of it clears the current cover: enough to register as
 * "something behind", little enough that the centred composition still
 * reads as centred. Hovering, tapping or tabbing to it slides it further
 * out and a beat upward, and its label fades in underneath.
 *
 * Transform and opacity only, on the same easing as the rest of the
 * piece. Under prefers-reduced-motion the blanket rule at the foot of
 * this file replaces both transitions, so the reveal still happens --
 * as an instant state change, which is the point -- rather than being
 * stuck shut.
 * ------------------------------------------------------------------ */

.u89-hero__peek {
  position: absolute;
  /* 72% tall, vertically centred against the 196px sleeve: (100-72)/2. */
  top: 14%;
  right: 0;
  z-index: 0;
  width: 72%;
  height: 72%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  /* ~21px of a 141px sleeve clears the cover's right edge at rest. */
  transform: translate3d(15%, 0, 0);
  transition: transform .7s var(--u89-eo);
}

.u89-hero__peek[hidden] { display: none; }

.u89-hero__peek-art {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  background-position: center;
  background-size: cover;
  background-color: rgba(255, 255, 255, .06);
  box-shadow: 0 22px 48px rgba(0, 0, 0, .55);
}

/* A next track with no cover of its own (empty art, or StreamEngine's
   default file -- see stage.js's nextHasArt()) gets the same logo-on-
   spectrum treatment the main sleeve gets from is-no-art, rather than a
   blank square or a broken image. */
.u89-hero__peek.is-no-art .u89-hero__peek-art {
  background-image: linear-gradient(120deg, #f89401, #fccd01, #3aa845, #1a9ad9, #bb1e82, #e0245e) !important;
  background-color: transparent;
}

.u89-hero__peek-logo {
  position: absolute;
  inset: 0;
  display: none;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: 50% 57%;
  padding: 9%;
  filter: drop-shadow(0 4px 14px rgba(0, 0, 0, .55));
}

.u89-hero__peek.is-no-art .u89-hero__peek-logo { display: block; }

.u89-hero__peek-label {
  position: absolute;
  top: calc(100% + 10px);
  left: 6%;
  width: 150px;
  max-width: 40vw;
  opacity: 0;
  transform: translate3d(0, 4px, 0);
  transition: opacity .45s ease, transform .7s var(--u89-eo);
  /* Inert at rest: the label's box sits outside the sleeve even while it
     is invisible, and an invisible hit area over the kicker would eat
     clicks meant for the copy below. It becomes live once revealed --
     the label is a DESCENDANT of the button, so hovering it holds the
     button's own :hover even though it is drawn outside the button's
     box, which is what lets a mouse user travel to it to read it
     instead of dismissing it on the way. */
  pointer-events: none;
}

.u89-hero__peek-kicker,
.u89-hero__peek-title,
.u89-hero__peek-artist {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.u89-hero__peek-kicker {
  margin-bottom: 3px;
  font-size: 8px;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--u89-yellow);
}

.u89-hero__peek-title {
  font-size: 12px;
  font-weight: 600;
  color: rgba(255, 255, 255, .94);
}

.u89-hero__peek-artist {
  font-size: 11px;
  font-weight: 400;
  color: rgba(255, 255, 255, .58);
}

.u89-hero__peek-artist[hidden] { display: none; }

/* Revealed. `hover: hover` keeps a phone's emulated sticky hover from
   latching this open behind the JS toggle's back. */
@media (hover: hover) {
  .u89-hero__peek:hover { transform: translate3d(44%, -8%, 0); }
  .u89-hero__peek:hover .u89-hero__peek-label { opacity: 1; transform: none; }
}

.u89-hero__peek:focus-visible,
.u89-hero__peek.is-peeking { transform: translate3d(44%, -8%, 0); }

.u89-hero__peek:focus-visible .u89-hero__peek-label,
.u89-hero__peek.is-peeking .u89-hero__peek-label { opacity: 1; transform: none; }

@media (hover: hover) {
  .u89-hero__peek:hover .u89-hero__peek-label { pointer-events: auto; }
}

.u89-hero__peek:focus-visible .u89-hero__peek-label,
.u89-hero__peek.is-peeking .u89-hero__peek-label { pointer-events: auto; }

.u89-hero__peek:focus { outline: none; }

.u89-hero__peek:focus-visible .u89-hero__peek-art {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

/* Copy ----------------------------------------------------------- */

.u89-hero__kicker {
  font-size: 10px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--u89-yellow);
  margin-bottom: 10px;
}

.u89-hero__kicker a {
  color: #fff;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.u89-hero__kicker a:hover,
.u89-hero__kicker a:focus-visible {
  color: var(--u89-yellow);
}

/* Each title line: a clipping box with the moving text inside it. The
   out/in classes land on the inner element, because stage.js binds the
   data attribute (and therefore the class) to the node it writes text
   into — the prototype put them on the wrapper instead. */
.u89-hero__line { overflow: hidden; max-width: 100%; }

.u89-hero__title {
  display: block;
  font-weight: 820;
  letter-spacing: -.025em;
  line-height: 1.03;
  font-size: clamp(24px, 3.4vw, 42px);
  transition: transform .7s var(--u89-eo), opacity .7s var(--u89-eo);
}

.u89-hero__title.out { transform: translateY(-105%); opacity: 0; }
.u89-hero__title.in  { transform: translateY(105%);  opacity: 0; }

.u89-hero__artist {
  margin-top: 10px;
  font-size: 14px;
  color: rgba(255, 255, 255, .72);
  transition: transform .7s var(--u89-eo) .06s, opacity .7s var(--u89-eo) .06s;
}

.u89-hero__artist.out { transform: translateY(-14px); opacity: 0; }
.u89-hero__artist.in  { transform: translateY(14px);  opacity: 0; }

/* Transport ------------------------------------------------------- */

.u89-hero__controls {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 22px;
  width: min(460px, 100%);
}

.u89-hero__play {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  flex: none;
  border: none;
  cursor: pointer;
  color: #0b0b14;
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: u89Ready 3s ease-out infinite;
  transition: transform .3s var(--u89-eo);
}

.u89-hero__play:hover { transform: scale(1.07); }

@keyframes u89Ready {
  0%   { box-shadow: 0 0 0 0 rgba(255, 255, 255, .3); }
  100% { box-shadow: 0 0 0 22px rgba(255, 255, 255, 0); }
}

.u89-hero__progress { flex: 1; min-width: 0; }

.u89-hero__track {
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .2);
  overflow: hidden;
}

.u89-hero__fill {
  height: 100%;
  width: 38%;
  border-radius: 2px;
  background: linear-gradient(90deg, #1a9ad9, #bb1e82, #e0245e);
  transition: width .9s linear;
}

.u89-hero__times {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: rgba(255, 255, 255, .45);
  margin-top: 7px;
  font-variant-numeric: tabular-nums;
}

.u89-hero__heart {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex: none;
  border: 1px solid rgba(255, 255, 255, .28);
  background: transparent;
  color: #fff;
  cursor: pointer;
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform .3s var(--u89-eo), background .3s ease;
}

.u89-hero__heart:hover {
  transform: scale(1.1);
  background: rgba(255, 255, 255, .12);
}

.u89-hero__likes {
  font-size: 10px;
  letter-spacing: .1em;
  color: rgba(255, 255, 255, .45);
  font-variant-numeric: tabular-nums;
}

.u89-hero__mute {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex: none;
  border: 1px solid rgba(255, 255, 255, .28);
  background: transparent;
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform .3s var(--u89-eo), background .3s ease;
}

.u89-hero__mute:hover {
  transform: scale(1.1);
  background: rgba(255, 255, 255, .12);
}

.u89-hero__mute-icon--off { display: none; }

.u89-hero.is-muted .u89-hero__mute-icon--on { display: none; }
.u89-hero.is-muted .u89-hero__mute-icon--off { display: block; }

/* Volume slider: collapsed to nothing at rest so it never disturbs the
   composition, and only revealed while a visitor is actually interacting
   with the transport row -- by mouse (:hover) or by keyboard
   (:focus-within, so tabbing to the slider itself reveals it). */
.u89-hero__volume {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 0;
  height: 3px;
  min-width: 0;
  margin: 0;
  border-radius: 2px;
  background: rgba(255, 255, 255, .25);
  outline: none;
  cursor: pointer;
  opacity: 0;
  overflow: hidden;
  transition: width .25s var(--u89-eo), opacity .2s ease;
}

/* Scoped to the volume end, not the whole transport row: expanding while
   the pointer travels toward the play button made the slider jump out at
   exactly the wrong moment. */
.u89-hero__volgroup {
  display: flex;
  align-items: center;
  gap: 6px;
}

.u89-hero__volgroup:hover .u89-hero__volume,
.u89-hero__volgroup:focus-within .u89-hero__volume {
  width: 76px;
  opacity: 1;
}

.u89-hero__volume::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .25);
}

.u89-hero__volume::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -4px;
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .4);
}

.u89-hero__volume::-moz-range-track {
  height: 3px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .25);
}

.u89-hero__volume::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border: none;
  border-radius: 50%;
  background: #fff;
  cursor: pointer;
}

/* Touch devices can't :hover to reveal the slider, and a thin drag target
   in the middle of this composition is a poor touch target anyway -- keep
   it collapsed permanently there and let the mute button stand alone. */
@media (hover: none) {
  .u89-hero__volgroup:hover .u89-hero__volume,
  .u89-hero__volgroup:focus-within .u89-hero__volume,
  .u89-hero__volume {
    width: 0;
    opacity: 0;
  }
}

.u89-hero__scroll {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  z-index: 8;
  font-size: 9px;
  letter-spacing: .3em;
  color: rgba(255, 255, 255, .35);
  animation: u89Nudge 2.4s ease-in-out infinite;
}

@keyframes u89Nudge {
  0%, 100% { transform: translate(-50%, 0);   opacity: .35; }
  50%      { transform: translate(-50%, 5px); opacity: .7; }
}

/* ------------------------------------------------------------------ *
 * Screen-reader announcements — visually hidden, still in the a11y tree
 * ------------------------------------------------------------------ */

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

/* ------------------------------------------------------------------ *
 * Docked bar — Task 10 derives the widget's markup from this
 * ------------------------------------------------------------------ */

.u89-hero__dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
  background: rgba(5, 5, 12, .92);
  border-top: 1px solid rgba(255, 255, 255, .1);
  backdrop-filter: blur(12px);
  transform: translateY(110%);
  opacity: 0;
  pointer-events: none;
  transition: transform .5s var(--u89-eo), opacity .35s ease;
}

.u89-hero__dock.is-docked {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}

.u89-hero__dock-art {
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: 6px;
  background-position: center;
  background-size: cover;
  background-color: rgba(255, 255, 255, .08);
}

.u89-hero__dock-copy {
  flex: 1;
  min-width: 0;
  text-align: left;
  line-height: 1.25;
}

.u89-hero__dock-title {
  display: block;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.u89-hero__dock-artist {
  display: block;
  font-size: 11px;
  color: rgba(255, 255, 255, .6);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.u89-hero__dock-play {
  width: 38px;
  height: 38px;
  flex: none;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  color: #0b0b14;
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ------------------------------------------------------------------ *
 * The five states
 * ------------------------------------------------------------------ */

/* Idle — desaturated and still, inviting the first press. */
.u89-hero.is-idle .u89-hero__bg {
  filter: blur(40px) saturate(.35) brightness(.72);
  animation-play-state: paused;
}
.u89-hero.is-idle .u89-hero__cover,
.u89-hero.is-idle .u89-hero__logo {
  filter: saturate(.35) brightness(.72);
}
.u89-hero.is-idle .u89-hero__eq i { animation-play-state: paused; height: 5px; }
.u89-hero.is-idle .u89-hero__edge { opacity: .28; animation-play-state: paused; }

/* Playing — full colour, everything running. */
.u89-hero.is-playing .u89-hero__bg {
  filter: blur(40px) saturate(1.45);
  animation-play-state: running;
}
.u89-hero.is-playing .u89-hero__cover,
.u89-hero.is-playing .u89-hero__logo { filter: none; }
.u89-hero.is-playing .u89-hero__logo { filter: drop-shadow(0 6px 22px rgba(0, 0, 0, .6)); }
.u89-hero.is-playing .u89-hero__eq i { animation-play-state: running; }

/* Station ID — the logo takes the sleeve slot and the backdrop goes
   full spectrum. The brand's best moment. A legal ID never carries real
   art, so this is unconditional. */
.u89-hero.is-station-id .u89-hero__cover {
  /* !important because stage.js writes the artwork URL to the inline
     background-image; during an ID there is no real artwork to show. */
  background-image: linear-gradient(120deg, #f89401, #fccd01, #3aa845, #1a9ad9, #bb1e82, #e0245e) !important;
  background-color: transparent;
}
.u89-hero.is-station-id .u89-hero__sleeve-logo { display: block; }
.u89-hero.is-station-id .u89-hero__bg.on {
  background-image: linear-gradient(120deg, #f89401, #fccd01, #3aa845, #1a9ad9, #bb1e82, #e0245e) !important;
  opacity: 1;
  filter: blur(40px) saturate(1.6);
}
.u89-hero.is-station-id .u89-hero__edge { opacity: 1; }
.u89-hero.is-station-id .u89-hero__kicker { color: #fff; }

/* No usable art — a normal song (or a commercial) whose art is empty or
   the default sleeve. Same logo-in-sleeve treatment as a station ID
   (a blank square is worse), but NONE of the full-spectrum backdrop
   above: the ordinary backdrop, kicker, title and artist stay put, so a
   cover-less song still reads as a normal song. This also covers
   `.is-commercial` for free -- a commercial with no picture of its own
   gets is-no-art from normalize()'s has_art, and one WITH a picture
   (the commercial-image override) does not, so its real image shows
   through undisturbed. */
.u89-hero.is-no-art .u89-hero__cover {
  background-image: linear-gradient(120deg, #f89401, #fccd01, #3aa845, #1a9ad9, #bb1e82, #e0245e) !important;
  background-color: transparent;
}
.u89-hero.is-no-art .u89-hero__sleeve-logo { display: block; }

/* Paused — a transport state, not an event.
 *
 * Deliberately NOT the idle treatment. Idle is arrival: the desaturated,
 * dimmed room that the first press floods with colour. Replaying that on
 * every pause/resume is what made resuming read as a new song starting,
 * which is the complaint this state exists to fix. So: no saturation
 * change to the artwork, no backdrop swap, no cross-fade, no pulse. The
 * artwork's colour is untouched.
 *
 * What is left is the smallest reversible acknowledgement that the
 * transport has stopped -- the sleeve settles back a hair and loses a
 * little light -- plus the equalizer coming to rest, which is the honest
 * thing for it to do when there is no audio to visualise. 200ms each
 * way, on the swap easing, symmetrical in and out.
 *
 * The dip is on .u89-hero__coverwrap, not on .u89-hero__cover: the cover
 * owns transform and its .out/.in swap classes, and a state rule on it
 * would outrank them and break the track-change choreography while
 * paused. The wrapper has no transform of its own. */
.u89-hero__coverwrap {
  transition: transform .2s var(--u89-es), filter .2s var(--u89-es);
}

.u89-hero.is-paused .u89-hero__coverwrap {
  transform: scale(.985);
  filter: brightness(.92);
}

.u89-hero.is-paused .u89-hero__eq i { animation-play-state: paused; height: 5px; }

/* News — the kicker carries the story link and it must be readable. */
.u89-hero.is-news .u89-hero__kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 11px;
  letter-spacing: .18em;
  color: rgba(255, 255, 255, .72);
}
.u89-hero.is-news .u89-hero__kicker a {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .35);
  color: #fff;
  text-decoration: none;
  letter-spacing: .14em;
  pointer-events: auto;
}
.u89-hero.is-news .u89-hero__kicker a:hover,
.u89-hero.is-news .u89-hero__kicker a:focus-visible {
  background: rgba(255, 255, 255, .14);
  border-color: #fff;
}

/* Offline — honest and calm. Logo centred, spectrum idle, no spinner. */
.u89-hero.is-offline .u89-hero__cover {
  background-image: none !important;
  background-color: rgba(255, 255, 255, .04);
  box-shadow: none;
}
.u89-hero.is-offline .u89-hero__sleeve-logo {
  display: block;
  filter: grayscale(1) brightness(.85);
}
.u89-hero.is-offline .u89-hero__bg {
  filter: blur(40px) saturate(0) brightness(.5);
  animation-play-state: paused;
}
.u89-hero.is-offline .u89-hero__eq i { animation-play-state: paused; height: 5px; }
.u89-hero.is-offline .u89-hero__edge { opacity: .2; animation-play-state: paused; }
.u89-hero.is-offline .u89-hero__dot { animation: none; background: rgba(255, 255, 255, .4); box-shadow: none; }
.u89-hero.is-offline .u89-hero__play { animation: none; }
.u89-hero.is-offline .u89-hero__progress,
.u89-hero.is-offline .u89-hero__heart,
.u89-hero.is-offline .u89-hero__likes { display: none; }

/* ------------------------------------------------------------------ *
 * Mobile — full-bleed blur is most expensive here, so halve it
 * ------------------------------------------------------------------ */

@media (max-width: 767px) {
  .u89-hero__bg { filter: blur(18px) saturate(1.45); }
  .u89-hero.is-idle .u89-hero__bg { filter: blur(18px) saturate(.35) brightness(.72); }
  .u89-hero.is-playing .u89-hero__bg { filter: blur(18px) saturate(1.45); }
  .u89-hero.is-station-id .u89-hero__bg.on { filter: blur(18px) saturate(1.6); }
  .u89-hero.is-offline .u89-hero__bg { filter: blur(18px) saturate(0) brightness(.5); }

  .u89-hero__meta { top: 14px; right: 16px; gap: 10px; }
  .u89-hero__stack { padding: 64px 16px 72px; }
  .u89-hero img.u89-hero__logo { height: 72px !important; }
  .u89-hero__brand { margin-bottom: 24px; }
  .u89-hero__coverwrap { width: 128px; }
  /* Smaller sleeve, tighter screen: the same percentages would throw the
     revealed label off the right edge, so the reveal travels less far and
     the label is narrower. */
  .u89-hero__peek { transform: translate3d(17%, 0, 0); }
  @media (hover: hover) {
    .u89-hero__peek:hover { transform: translate3d(34%, -8%, 0); }
  }
  .u89-hero__peek:focus-visible,
  .u89-hero__peek.is-peeking { transform: translate3d(34%, -8%, 0); }
  .u89-hero__peek-label { width: 118px; top: calc(100% + 8px); }
  .u89-hero__peek-title { font-size: 11px; }
  .u89-hero__peek-artist { font-size: 10px; }
  .u89-hero__controls { gap: 10px; }
  .u89-hero__scroll { display: none; }
}

/* ------------------------------------------------------------------ *
 * Desktop — two columns
 *
 * Below 1200px NOTHING in this block applies and the Marquee stack is
 * exactly as it was: one centre axis, reflowing to a phone with no
 * rearranging. Above it the same seven children of .u89-hero__inner are
 * re-placed by GRID -- no DOM change, so the widget render and
 * tests/preview.html stay node-for-node identical and stage.js's bindings
 * are untouched.
 *
 * Why 1200px: the composition needs ~1080px of content plus its gutters
 * before two columns beat one, and 1200 clears every common tablet
 * landscape width (1024, 1080, 1112, 1180, 1194) so no tablet ever lands
 * in the desktop arrangement by accident.
 *
 * LEFT: the sleeve, spanning the full height of the right column and
 * optically centred against it. RIGHT: the brand (logo + equalizer)
 * heading a left-aligned column of kicker, title, artist and transport.
 *
 * The logo heads the RIGHT column rather than spanning both. Spanning
 * would put a centred mark over a left-aligned column, which is the same
 * "centred text in a left-aligned column" accident the brief warns about
 * one level up; heading the column makes the mark the top of a single
 * left-aligned masthead that runs logo -> equalizer -> kicker -> title,
 * and leaves the sleeve as one clean object opposite it. It also grows
 * here rather than shrinking -- see .u89-hero__logo below.
 * ------------------------------------------------------------------ */

@media (min-width: 1200px) {

  /* The sleeve is the module the rest of the desktop composition is
     measured in, so it is a variable rather than a number in six places.
     Fluid, so the piece inhabits a 2000px screen instead of floating in
     the middle third of it, and clamped so it never outgrows the height
     cap or shrinks below the size the peek needs to read. */
  .u89-hero { --u89-sleeve: clamp(320px, 23vw, 440px); }

  .u89-hero__stack { padding: 48px 40px 56px; }

  .u89-hero__inner {
    display: grid;
    /* Six rows: brand, kicker, title line 1, title line 2, artist,
       transport. Explicit, so the sleeve's `1 / -1` span below has a real
       last line to reach. */
    grid-template-rows: repeat(6, auto);
    grid-template-columns: var(--u89-sleeve) minmax(0, 1fr);
    /* Derived, not chosen. The peek's revealed transform is
       translate3d(44%, -8%) on an element 72% of the sleeve's width, so
       it overhangs the sleeve's right edge by .44 x .72 = 31.7% of the
       sleeve. A gap of 36% of the sleeve therefore always clears it, at
       every size in the clamp above (13.8px of daylight at 320px, 19.0px
       at 440px) -- the gap moves, the peek does not. */
    column-gap: calc(var(--u89-sleeve) * .36);
    /* Wide enough to occupy the screen rather than sit in a column down
       the middle of it, but not so wide that the copy strands itself: at
       1500px the right column ran 959px while a typical title only fills
       ~600px of it, which read as a hole rather than as space. */
    max-width: min(1280px, 92vw);
    text-align: left;
  }

  /* The copy has its own measure. Without it the column's width decides the
     line length, and a short title ("Free Me") sits marooned at the left of
     a 900px box. Capping it makes the right side a deliberate block facing
     the sleeve, and pushes the leftover width out into symmetric margins
     where it reads as air instead of omission. */
  .u89-hero__inner > .u89-hero__kicker,
  .u89-hero__inner > .u89-hero__line,
  .u89-hero__inner > .u89-hero__artist,
  .u89-hero__inner > .u89-hero__controls {
    max-width: 620px;
  }

  .u89-hero__coverwrap {
    grid-area: 1 / 1 / -1 / 2;
    align-self: center;
    width: var(--u89-sleeve);
    max-width: 100%;
    margin-bottom: 0;
  }

  .u89-hero__brand,
  .u89-hero__kicker,
  .u89-hero__line,
  .u89-hero__artist,
  .u89-hero__controls { grid-column: 2; }

  .u89-hero__brand { align-items: flex-start; }

  /* The brand anchor, and the owner has twice asked for it to be
     prominent -- on the live page at 2000px the site header's own mark
     was out-weighing it. Fluid from here up, with the current fixed size
     as the FLOOR at the breakpoint, so nothing gets smaller than it is
     today; 220px is the ceiling, which at the wordmark's ~1.52:1 is
     ~334px wide -- comfortably inside the right column and still ~250px
     clear of the 780px height cap once the rest of the column is
     counted. The mobile override at 767px is untouched. */
  .u89-hero img.u89-hero__logo { height: clamp(124px, 10vw, 220px) !important; }

  /* Type scaled to the measure it now has. Without this a 42px title in
     a ~650-900px column is what makes the two columns read as drifted
     apart rather than as one object. */
  .u89-hero__title { font-size: clamp(38px, 3.6vw, 64px); }
  .u89-hero__artist { font-size: 16px; }
  .u89-hero__controls { width: min(560px, 100%); }

  /* The news kicker centres its row in the Marquee stack; in a
     left-aligned column it has to start at the same axis as everything
     above and below it. */
  .u89-hero.is-news .u89-hero__kicker { justify-content: flex-start; }
}

/* ------------------------------------------------------------------ *
 * Advertiser artwork
 *
 * Album art is square, so `cover` fills the sleeve with no loss. An advert's
 * graphic is whatever shape the advertiser supplied, and cropping it can cut
 * off the very thing they are paying to show -- here it sliced both edges off
 * the station wordmark. Ads are therefore fitted whole, on a dark ground.
 * ------------------------------------------------------------------ */

.u89-hero.is-commercial .u89-hero__cover {
  background-size: contain;
  background-repeat: no-repeat;
  background-color: rgba(8, 8, 16, .55);
}

/* ------------------------------------------------------------------ *
 * Play / pause icon
 *
 * Keyed on aria-pressed rather than a root state class, so the button's
 * accessible state and its picture cannot disagree: stage.js sets that
 * attribute on both the hero and docked buttons on every playback change.
 * ------------------------------------------------------------------ */

.u89-hero__play .u89-hero__icon--pause,
.u89-hero__dock-play .u89-hero__icon--pause { display: none; }

.u89-hero__play[aria-pressed="true"] .u89-hero__icon--play,
.u89-hero__dock-play[aria-pressed="true"] .u89-hero__icon--play { display: none; }

.u89-hero__play[aria-pressed="true"] .u89-hero__icon--pause,
.u89-hero__dock-play[aria-pressed="true"] .u89-hero__icon--pause { display: block; }

/* ------------------------------------------------------------------ *
 * Reduced motion
 * ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .u89-hero *,
  .u89-hero *::before,
  .u89-hero *::after {
    animation: none !important;
    transition: opacity .2s ease !important;
  }
}
