/* jeni.rocks TV — a 90s/00s MTV set.

   Two placement variants share one set of internals:
     .tv--panel  sits inside a .sidebar-panel in the home sidebar
     .tv--pip    floats bottom-right and follows the page

   The screen internals (scanlines, chyron, pop-up bubbles, block badge)
   are placement-agnostic — only the shell chrome and sizing differ. All
   colors come from css/variables.css so the set sits in the site palette
   rather than shipping its own. */

.tv {
  --tv-bezel: #17110f;
  --tv-bezel-hi: #2a201d;
  --tv-screen: #050505;
  /* How much of its natural size YouTube's own player is drawn at.
     The iframe is rendered larger than the frame and then scaled back
     down, so YouTube lays out its chrome — title, channel avatar,
     controls, end-screen cards — for a big player and it lands small.
     None of that UI can be styled from outside a cross-origin frame, so
     this is the only lever there is. Lower = smaller chrome; below
     about 0.5 the video itself starts to look soft. */
  --tv-yt-scale: var(--tv-yt-scale-override, 0.62);
  position: relative;
  border-radius: 14px;
  background: linear-gradient(160deg, var(--tv-bezel-hi) 0%, var(--tv-bezel) 42%, #0d0908 100%);
  border: 1px solid rgba(179, 0, 0, 0.16);
  padding: 0.85rem 0.85rem 0.6rem;
  box-shadow:
    0 0 22px rgba(139, 0, 0, 0.13),
    0 10px 26px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

/* ---------- Screen ---------- */

/* 4:3 is both period-correct for a CRT and the native aspect of most
   actual 90s music videos, so pillarboxing is rare. */
.tv__screen {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: 10px / 14px;   /* uneven radii read as slight glass curvature */
  overflow: hidden;
  background: var(--tv-screen);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, 0.9),
    inset 0 0 32px rgba(0, 0, 0, 0.95);
}

/* Stacking inside the screen is explicit because the player appends its
   iframe last, after the glass and the overlay are already in the DOM.
   Without these the video would paint over the chyron. */
.tv__video,
.tv__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
  z-index: 1;
}
.tv__glass   { z-index: 3; }
.tv__overlay { position: absolute; inset: 0; z-index: 4; }

/* Oversize the frame, then scale it back. Net visible size is
   unchanged; YouTube's furniture just comes out smaller. */
.tv__video {
  width: calc(100% / var(--tv-yt-scale));
  height: calc(100% / var(--tv-yt-scale));
  transform: scale(var(--tv-yt-scale));
  transform-origin: top left;
}

/* The overlay is mostly furniture floating over the video, so it must
   not swallow pointer events; the cards inside it opt back in. */
.tv__overlay { pointer-events: none; }
.tv__overlay .tv__card,
.tv__overlay .tv__offair { pointer-events: auto; }

/* Text channels cover the screen entirely, so the video underneath is
   hidden rather than left peeking around the card. */
.tv__screen.tv--carded .tv__poster,
.tv__screen.tv--carded .tv__video { visibility: hidden; }


/* Embed-blocked video: keep the poster, drop the player. The set still
   reads as a television showing something, which is the whole point of
   not skipping these. */
.tv__screen.tv--static .tv__video { visibility: hidden; }
.tv__screen.tv--static .tv__poster { visibility: visible; }

/* Poster-first: a YouTube thumbnail holds the screen until the real
   iframe is upgraded in on idle. Kept slightly dimmed so the chyron and
   pop-ups stay legible over a busy frame. */
.tv__poster { filter: brightness(0.82) saturate(0.9); }

/* Scanlines + vignette, stacked in one overlay so there is only a
   single compositing layer sitting over the video. */
.tv__glass {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0.20) 0px,
      rgba(0, 0, 0, 0.20) 1px,
      transparent 1px,
      transparent 3px
    ),
    radial-gradient(
      ellipse 78% 72% at 50% 46%,
      transparent 55%,
      rgba(0, 0, 0, 0.55) 100%
    );
  mix-blend-mode: multiply;
}

/* Faint moving brightness band — a CRT refresh roll. Slow enough to
   register as texture rather than motion. */
.tv__glass::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 22%;
  background: linear-gradient(
    to bottom,
    transparent,
    rgba(255, 255, 255, 0.035),
    transparent
  );
  animation: tv-roll 7s linear infinite;
}
@keyframes tv-roll {
  from { transform: translateY(-120%); }
  to   { transform: translateY(560%); }
}

/* ---------- Block badge (top-left) ---------- */

.tv__block {
  position: absolute;
  top: 0.55rem;
  left: 0.6rem;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.28rem 0.5rem;
  border-radius: 4px;
  background: rgba(8, 6, 6, 0.62);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  font-family: var(--font-mono);
  font-size: 0.54rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text);
}

.tv__onair {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-red-bright);
  box-shadow: 0 0 6px var(--color-red-bright);
  animation: tv-pulse 2.4s ease-in-out infinite;
  flex-shrink: 0;
}
@keyframes tv-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

/* ---------- Channel number flash ---------- */

/* The number an old set threw into the corner for a second after you
   pressed a button, then took away again. */
.tv__osd {
  position: absolute;
  top: 0.6rem;
  right: 0.7rem;
  z-index: 5;
  padding: 0.3rem 0.55rem;
  border-radius: 4px;
  background: rgba(8, 6, 6, 0.72);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: #fff;
  text-shadow: 0 0 10px rgba(179, 0, 0, 0.85);
  pointer-events: none;
}
.tv__osd[hidden] { display: none; }
.tv__osd.is-on { animation: tv-osd 1.6s ease-out forwards; }

@keyframes tv-osd {
  0%   { opacity: 0; transform: translateY(-4px); }
  12%  { opacity: 1; transform: none; }
  70%  { opacity: 1; }
  100% { opacity: 0; }
}

/* Smaller in the corner, where the screen is a fraction of the size. */
.tv--pip:not(.is-expanded) .tv__osd {
  font-size: 0.62rem;
  padding: 0.22rem 0.4rem;
}

@media (prefers-reduced-motion: reduce) {
  .tv__osd.is-on { animation: none; opacity: 1; }
}

/* ---------- Lower-third chyron ---------- */

/* The single detail that does the most to say "MTV". Slides in for the
   first few seconds of each video, then slides back out. .is-out is
   toggled by js/tv.js on a timer.

   Two lines only, artist over title — album/year/label were dropped so
   adding a video is a paste and nothing else. */
.tv__chyron {
  position: absolute;
  left: 0;
  bottom: 0.9rem;
  max-width: 82%;
  padding: 0.5rem 0.85rem 0.5rem 0.7rem;
  border-left: 3px solid var(--color-red-bright);
  background: linear-gradient(
    to right,
    rgba(8, 6, 6, 0.88) 0%,
    rgba(8, 6, 6, 0.72) 70%,
    rgba(8, 6, 6, 0) 100%
  );
  transition: transform 420ms cubic-bezier(0.16, 1, 0.3, 1), opacity 300ms ease;
}
.tv__chyron.is-out {
  transform: translateX(-104%);
  opacity: 0;
}

/* Tall condensed caps over an italic serif — the shape every music
   channel used for a lower third. Bebas is set large and loose because
   condensed faces need the tracking to stay readable at a glance. */
.tv__chyron-artist {
  font-family: var(--font-broadcast);
  font-size: 1.35rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #fff;
  line-height: 1.05;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.75);
}
.tv__chyron-title {
  font-family: var(--font-title);
  font-size: 0.95rem;
  font-style: italic;
  color: var(--color-body-text);
  line-height: 1.35;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.75);
}

/* ---------- Pop-Up Video bubble ---------- */

/* Lifted wholesale from VH1. Trivia blooping in over the video is the
   cheapest way to make the channel feel authored rather than embedded. */
.tv__popup {
  position: absolute;
  top: 16%;
  right: 6%;
  max-width: 54%;
  padding: 0.55rem 0.7rem;
  border-radius: 12px;
  background: #f4f0e8;
  color: #14100f;
  font-family: var(--font-body);
  font-size: 0.68rem;
  line-height: 1.4;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
  animation: tv-bloop 340ms cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: bottom left;
}
/* Bubble tail */
.tv__popup::after {
  content: '';
  position: absolute;
  left: 14px;
  bottom: -9px;
  border: 5px solid transparent;
  border-top: 9px solid #f4f0e8;
  border-bottom: 0;
}
@keyframes tv-bloop {
  from { transform: scale(0.2); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}

/* ---------- Blocked from embedding ---------- */

.tv__noembed {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1rem;
  /* Darken the poster so the call to action reads cleanly over it. */
  background: radial-gradient(ellipse 90% 80% at 50% 50%, rgba(8, 6, 6, 0.55), rgba(8, 6, 6, 0.85));
  pointer-events: auto;
}
.tv__watch {
  padding: 0.55rem 1rem;
  border: 1px solid rgba(179, 0, 0, 0.6);
  border-radius: 7px;
  background: rgba(8, 6, 6, 0.7);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #fff;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.tv__watch:hover {
  background: rgba(179, 0, 0, 0.35);
  border-color: var(--color-red-bright);
}
.tv__watch:focus-visible {
  outline: 2px solid var(--color-red-bright);
  outline-offset: 3px;
}
.tv__noembed-why {
  font-family: var(--font-mono);
  font-size: 0.52rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-label);
  text-align: center;
}

/* Collapsed in the corner there is no room to explain, just to offer. */
.tv--pip:not(.is-expanded) .tv__noembed-why { display: none; }
.tv--pip:not(.is-expanded) .tv__watch { font-size: 0.5rem; padding: 0.4rem 0.6rem; }

/* ---------- Off air ---------- */

/* Strict scheduling means a block with no active videos has nothing to
   play. Colour bars are a nicer answer than a silent fallback, and they
   keep the promise that the schedule is real. */
.tv__offair {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  text-align: center;
  padding: 1rem;
}
.tv__bars {
  position: absolute;
  inset: 0;
  opacity: 0.5;
  background: linear-gradient(
    to right,
    #c0c0c0 0 14.28%, #c0c020 14.28% 28.56%, #20c0c0 28.56% 42.84%,
    #20c020 42.84% 57.12%, #c020c0 57.12% 71.4%,  #c02020 71.4% 85.68%,
    #2020c0 85.68% 100%
  );
}
.tv__offair-msg {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.9);
}
.tv__offair-next {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.55rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.78);
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.9);
}

/* ---------- Text channels (weather / fortune) ---------- */

.tv__card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.45rem;
  padding: 1.3rem 1.4rem;
  background: radial-gradient(ellipse 80% 70% at 50% 40%, #1a1210 0%, #0a0707 70%);
}

/* Sizes inside a lower third are their own scale: the point is that the
   picture keeps most of the frame, so the report is set tighter than the
   full-screen cards. */
.tv__card--band .tv__card-label { font-size: 0.66rem; letter-spacing: 0.14em; }
.tv__card--band .tv__card-big   { font-size: 1.25rem; line-height: 1; }
.tv__card--band .tv__card-body  { font-size: 0.88rem; line-height: 1.18; }
.tv__card--band .tv__card-facts { font-size: 0.78rem; margin-top: 0.05rem; }

/* Full-bleed broadcast art behind a card. 4:3 like the screen, so it
   fills without cropping. */
.tv__card-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
}

/* With art behind it the card stops being a full-screen panel and
   becomes a lower third: anchored to the bottom, sized to its content,
   and reading out of a gradient rather than a flat fill so the picture
   stays visible above it. */
.tv__card--band {
  inset: auto 0 0 0;
  justify-content: flex-end;
  gap: 0.1rem;
  padding: 1.1rem 0.9rem 0.7rem;
  z-index: 2;
  background: linear-gradient(
    to top,
    rgba(6, 4, 4, 0.97) 0%,
    rgba(6, 4, 4, 0.9) 55%,
    rgba(6, 4, 4, 0.5) 85%,
    rgba(6, 4, 4, 0) 100%
  );
}
/* VT323 runs small and tall for its point size, so everything here is
   set larger than the equivalent in the site's normal faces. */
.tv__card-label {
  font-family: var(--font-crt);
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-date);
  line-height: 1.1;
}
.tv__card-big {
  font-family: var(--font-crt);
  font-size: 1.75rem;
  line-height: 1.05;
  letter-spacing: 0.01em;
  color: var(--color-text);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);
}
.tv__card-body {
  font-family: var(--font-crt);
  font-size: 1.05rem;
  line-height: 1.25;
  color: var(--color-body-text);
}
/* The forecast numbers. Mono and dimmer than the written line, so the
   card reads as a sentence with a readout under it rather than two
   competing paragraphs. */
.tv__card-facts {
  margin-top: 0.1rem;
  font-family: var(--font-crt);
  font-size: 0.92rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-label);
}

/* Click-through crawl: the caret sits after however much text has been
   revealed so far, the way the Stardew TV reveals a line at a time. */
/* Deliberately NOT guarded against the hidden attribute. An author
   display beats the UA rule behind [hidden], so the caret keeps
   blinking once a card is fully revealed — which reads as a CRT cursor
   sitting there waiting, and is wanted. */
.tv__caret {
  display: inline-block;
  width: 0.45em;
  height: 0.95em;
  vertical-align: -0.12em;
  background: var(--color-red-bright);
  animation: tv-caret 900ms steps(2) infinite;
}
@keyframes tv-caret {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0; }
}

/* A card with more than one line reveals them on click, so it has to
   look like something you can click. */
.tv__card.is-clickable { cursor: pointer; }
.tv__card.is-clickable:focus-visible {
  outline: 2px solid var(--color-red-bright);
  outline-offset: -4px;
}

/* Affiliate call to action. The PAID PROMOTION label above it is
   rendered unconditionally by tv-channels.js and is not stylable away. */
.tv__ad-cta {
  align-self: flex-start;
  margin-top: 0.3rem;
  padding: 0.4rem 0.75rem;
  border: 1px solid rgba(179, 0, 0, 0.5);
  border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text);
  pointer-events: auto;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}
.tv__ad-cta:hover {
  background: rgba(179, 0, 0, 0.18);
  border-color: var(--color-red-bright);
}

/* ---------- Panel stub ---------- */

/* Shown in the sidebar panel while the set is popped out, so the panel
   is not just an empty hole and there is an obvious way to undo it. */
/* An author `display` beats the UA rule that powers the hidden
   attribute, so hiding has to be restated here or the stub shows even
   when the set is docked in the panel. */
.tv-stub[hidden] { display: none; }

.tv-stub {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  border: 1px dashed rgba(179, 0, 0, 0.28);
  border-radius: 10px;
}
.tv-stub__text {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text-label);
}
.tv-stub__btn {
  flex-shrink: 0;
  padding: 0.35rem 0.6rem;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 5px;
  background: linear-gradient(to bottom, #221a18, #140f0e);
  color: var(--color-text-secondary);
  font-family: var(--font-mono);
  font-size: 0.55rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}
.tv-stub__btn:hover { color: #fff; border-color: rgba(179, 0, 0, 0.5); }

/* ---------- Bezel controls ---------- */

/* Five channels plus mute, pop-out and the grille no longer fit on one
   row at panel width, so the row wraps. A two-row bezel is if anything
   more television than one. */
.tv__controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.6rem;
  padding: 0 0.15rem;
}

.tv__channel {
  flex: 1 1 auto;
  min-width: 42px;
  padding: 0.34rem 0.2rem;
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 5px;
  background: linear-gradient(to bottom, #221a18, #140f0e);
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 0.52rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
}
/* The number rides ahead of the label, dimmer, so the dial reads as
   numbered without the digits competing with the channel name. */
.tv__chnum {
  margin-right: 0.28em;
  opacity: 0.5;
  font-weight: 400;
}
.tv__channel.is-active .tv__chnum { opacity: 0.8; }

.tv__channel:hover { color: var(--color-text-secondary); }
.tv__channel.is-active {
  color: #fff;
  border-color: rgba(179, 0, 0, 0.55);
  box-shadow: 0 0 10px rgba(179, 0, 0, 0.3), inset 0 0 8px rgba(179, 0, 0, 0.18);
}

/* Speaker grille. Purely decorative, but a rectangle with buttons is not
   a television — the grille is what makes it read as a set. */
/* The level control hides behind the speaker button so the bezel row
   stays short and the grille keeps its place. */
.tv__volwrap { position: relative; flex-shrink: 0; }

.tv__volpop {
  position: absolute;
  bottom: calc(100% + 0.45rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid rgba(179, 0, 0, 0.4);
  border-radius: 8px;
  background: rgba(14, 10, 10, 0.96);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.6);
}
/* An author display beats the UA rule behind the hidden attribute. */
.tv__volpop[hidden] { display: none; }

/* Little pointer down to the speaker. */
.tv__volpop::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: rgba(179, 0, 0, 0.4);
}

.tv__volmute {
  flex-shrink: 0;
  padding: 0.25rem 0.45rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  background: transparent;
  color: var(--color-text-secondary);
  font-family: var(--font-mono);
  font-size: 0.5rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}
.tv__volmute:hover { color: #fff; border-color: rgba(179, 0, 0, 0.6); }

.tv__vol {
  width: 84px;
  height: 20px;
  flex-shrink: 0;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  cursor: pointer;
}
.tv__vol::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(to right, var(--color-red-bright), rgba(255, 255, 255, 0.14));
}
.tv__vol::-moz-range-track {
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(to right, var(--color-red-bright), rgba(255, 255, 255, 0.14));
}
.tv__vol::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -4px;
  border-radius: 50%;
  background: var(--color-text);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.tv__vol::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border: 0;
  border-radius: 50%;
  background: var(--color-text);
}
.tv__vol:focus-visible { outline: 2px solid var(--color-red-bright); outline-offset: 2px; }

/* Back on the bezel for good: with the level control tucked behind the
   speaker, the row is short enough to keep it. Still dropped in the
   collapsed corner view, where there is genuinely no room. */

/* Notes drift off the grille while a music channel is audible. Purely
   CSS — three spans on staggered loops, no timers to leak. */
.tv__speaker {
  position: relative;
  flex-shrink: 0;
}

.tv__note {
  position: absolute;
  bottom: 55%;
  left: 50%;
  width: 11px;
  height: 14px;
  /* Warm off-white with a red glow rather than flat red: the accent red
     on a near-black bezel has almost no contrast at this size. */
  color: #f0e6e6;
  filter: drop-shadow(0 0 4px rgba(179, 0, 0, 0.95));
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  overflow: visible;
}

.tv__speaker.is-live .tv__note {
  animation: tv-note 3.1s ease-out infinite;
}
/* Staggered so they leave the speaker in a trickle rather than a chord,
   and each drifts its own way. */
.tv__speaker.is-live .tv__note:nth-child(1) { animation-delay: 0s;    --drift: -9px; }
.tv__speaker.is-live .tv__note:nth-child(2) { animation-delay: 1.05s; --drift:  7px; }
.tv__speaker.is-live .tv__note:nth-child(3) { animation-delay: 2.1s;  --drift: -3px; }

@keyframes tv-note {
  0%   { opacity: 0;    transform: translate(-50%, 0) rotate(-6deg) scale(0.65); }
  20%  { opacity: 0.9;  }
  65%  { opacity: 0.6;  }
  100% { opacity: 0;    transform: translate(calc(-50% + var(--drift, 0px)), -28px) rotate(12deg) scale(1); }
}

/* No room for them in the corner, and nothing to hear on a text
   channel anyway. */
.tv--pip:not(.is-expanded) .tv__note { display: none; }

@media (prefers-reduced-motion: reduce) {
  .tv__speaker.is-live .tv__note { animation: none; opacity: 0; }
}
.tv__grille {
  flex-shrink: 0;
  width: 34px;
  height: 18px;
  border-radius: 3px;
  /* The lit stripe was at 4% alpha and effectively invisible against
     the bezel, so the grille read as a smudge rather than a speaker. */
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.75) 0 2px,
    rgba(236, 232, 224, 0.16) 2px 4px
  );
  box-shadow:
    inset 0 0 5px rgba(0, 0, 0, 0.9),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

.tv__mute {
  flex-shrink: 0;
  width: 26px;
  height: 24px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 5px;
  background: linear-gradient(to bottom, #221a18, #140f0e);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: color var(--transition-fast), box-shadow var(--transition-fast);
}
.tv__mute svg { width: 13px; height: 13px; }
.tv__mute:hover:not(:disabled) { color: var(--color-text); }

/* Nothing to hear on a text channel, so the control says so by being
   visibly out of action rather than quietly doing nothing. */
.tv__mute:disabled {
  opacity: 0.32;
  cursor: default;
}
/* Unmuted is the "hot" state — it means this set has claimed the page
   audio and the mixtape has been paused. */
.tv__mute.is-live {
  color: #fff;
  box-shadow: 0 0 10px rgba(179, 0, 0, 0.35), inset 0 0 8px rgba(179, 0, 0, 0.2);
}

/* ---------- Variant: sidebar panel ---------- */

/* The panel wrapper already supplies the well background and border, so
   the set drops its own outer red glow to avoid doubling up. */
.sidebar-panel .tv--panel {
  box-shadow:
    0 6px 18px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

/* ---------- Variant: picture-in-picture ---------- */

.tv--pip {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  width: 268px;
  z-index: 60;
  transition: width 320ms cubic-bezier(0.16, 1, 0.3, 1);
}
.tv--pip.is-expanded { width: 520px; }

/* When signed in, the admin/edit cluster is pinned bottom-right at
   z-index 90 and sits on top of the set. Lift the set clear of it.
   :has() keeps this in CSS rather than making the TV watch for an
   element that mounts only after auth resolves. */
body:has(#edit-mode-cluster) .tv--pip { bottom: 4.75rem; }

/* Collapsed, the set is too small to carry the full furniture: the
   pop-up bubble lands on top of the chyron and the channel labels
   truncate. Strip it back to the essentials and restore everything on
   expand. */
.tv--pip:not(.is-expanded) .tv__popup { display: none; }
.tv--pip:not(.is-expanded) .tv__speaker { display: none; }
.tv--pip:not(.is-expanded) .tv__block {
  font-size: 0.46rem;
  padding: 0.2rem 0.36rem;
  gap: 0.28rem;
}
.tv--pip:not(.is-expanded) .tv__channel {
  font-size: 0.46rem;
  letter-spacing: 0.06em;
  padding: 0.3rem 0.1rem;
}
.tv--pip:not(.is-expanded) .tv__chyron {
  bottom: 0.6rem;
  max-width: 92%;
  padding: 0.4rem 0.6rem 0.4rem 0.55rem;
}
.tv--pip:not(.is-expanded) .tv__chyron-artist { font-size: 1rem; }
.tv--pip:not(.is-expanded) .tv__chyron-title  { font-size: 0.76rem; }

.tv__pipbar {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-bottom: 0.5rem;
  padding: 0 0.1rem;
}
.tv__piptitle {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 0.52rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text-label);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tv__pipbtn {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  transition: color var(--transition-fast), background var(--transition-fast);
}
.tv__pipbtn:hover { color: var(--color-text); background: rgba(255, 255, 255, 0.06); }
.tv__pipbtn svg { width: 12px; height: 12px; }

/* ---------- Responsive ---------- */

@media (max-width: 768px) {
  /* On a phone the panel is the full content width, so the padding on
     the way in is what was keeping the screen small. Tightening the
     panel and the bezel gives the picture back about 35px of width
     without changing anything on desktop. */
  .home-tv.sidebar-panel { padding: 1.1rem 0.85rem 0.95rem; }
  .tv--panel { padding: 0.55rem 0.55rem 0.5rem; }

  /* The dial fills the first row on its own because the channel buttons
     grow; only the wrapped row of icons is actually affected, and it
     centres under them. */
  .tv__controls { justify-content: center; }
  .tv__break { display: block; flex-basis: 100%; height: 0; }

  /* Except in the corner, where a forced second row would make an
     already small set taller for no reason. */
  .tv--pip:not(.is-expanded) .tv__break { display: none; }

  /* PiP would cover the bottom dock and eat a third of a phone screen.
     Pin it above the dock and shrink it; expanding goes near-full-width. */
  .tv--pip {
    right: 0.75rem;
    bottom: 4.25rem;
    width: 208px;
    padding: 0.55rem 0.55rem 0.45rem;
  }
  .tv--pip.is-expanded { width: calc(100vw - 1.5rem); }
}

@media (prefers-reduced-motion: reduce) {
  .tv__glass::after,
  .tv__onair,
  .tv__caret,
  .tv__popup { animation: none; }
  .tv__chyron { transition: none; }
}
