/* App-local styles for IPTV.
 *
 * Built from @ecosystem/design components; what lives here is only what is
 * genuinely specific to a channel tableau — the tile grid, the channel logo,
 * and the full-screen player overlay. Per packages/design/README.md this file
 * consumes semantic tokens only: no raw colors, no redefined .ds-* internals,
 * no primitive ramps. The accent block below is the one supported exception.
 */

/* IPTV's accent — a screen-glow blue, distinct from every other app's warm
   or earthy marks, since this is the one app that is literally about a
   screen. Repointing --ds-accent is the supported way an app expresses its
   own identity; nothing else here is colored. */
:root {
  --ds-accent: #1d4ed8;
  --ds-accent-hover: #1e40af;
  --ds-accent-subtle: #eef2ff;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ds-accent: #60a5fa;
    --ds-accent-hover: #93c5fd;
    --ds-accent-subtle: #1e2a4a;
    --ds-on-accent: var(--ds-neutral-900);
  }
}
:root[data-theme="dark"] {
  --ds-accent: #60a5fa;
  --ds-accent-hover: #93c5fd;
  --ds-accent-subtle: #1e2a4a;
  --ds-on-accent: var(--ds-neutral-900);
}

.tv-section + .tv-section {
  border-top: var(--ds-border-width) solid var(--ds-border);
}

.tv-section__title {
  padding: var(--ds-space-4) var(--ds-space-4) 0;
  font-size: var(--ds-text-sm);
  font-weight: var(--ds-weight-semibold);
  color: var(--ds-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.tv-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--ds-space-4);
  padding: var(--ds-space-4);
}

.tv-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-4);
  text-align: center;
  cursor: pointer;
  position: relative;
}

.tv-tile__logo {
  width: 4rem;
  height: 4rem;
  object-fit: contain;
  border-radius: var(--ds-radius-md);
  background: var(--ds-surface-sunken);
}

.tv-tile__logo--letter {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--ds-text-xl);
  font-weight: var(--ds-weight-semibold);
  color: var(--ds-text-muted);
}

.tv-tile__name {
  font-size: var(--ds-text-sm);
  font-weight: var(--ds-weight-medium);
  line-height: var(--ds-leading-tight);
}

.tv-tile__meta {
  font-size: var(--ds-text-xs);
  color: var(--ds-text-muted);
}

.tv-tile__badges {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ds-space-1);
}

.tv-geoblocked-toggle {
  font-size: var(--ds-text-sm);
  color: var(--ds-text-muted);
  white-space: nowrap;
}

/* ── the tableau ───────────────────────────────────────────────────────────
 *
 * Two shapes for the same data. Tiles are for scanning a catalog by logo;
 * rows are for reading a list you built on purpose, which is why a row has
 * somewhere to put what is on now and what is next and a tile does not.
 */

.tv-tile__actions {
  position: absolute;
  top: var(--ds-space-2);
  right: var(--ds-space-2);
  display: flex;
  gap: var(--ds-space-1);
  /* Revealed on hover on a pointer device and always present on a touch one,
     where there is no hover to reveal anything with. */
  opacity: 0;
  transition: opacity var(--ds-duration-fast) var(--ds-ease);
}
.tv-tile:hover .tv-tile__actions,
.tv-tile:focus-within .tv-tile__actions {
  opacity: 1;
}
@media (hover: none) {
  .tv-tile__actions { opacity: 1; }
}
.tv-tile__actions [aria-pressed="true"] { color: var(--ds-danger); }

/* What is on, in either shape. */
.tv-now {
  width: 100%;
  font-size: var(--ds-text-xs);
  display: grid;
  gap: var(--ds-space-1);
}
.tv-now--none { color: var(--ds-text-subtle); font-style: italic; }
.tv-now__title { font-weight: var(--ds-weight-medium); color: var(--ds-text); }
.tv-now__time { color: var(--ds-text-muted); }
.tv-now__bar {
  height: 3px;
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-sunken);
  overflow: hidden;
}
.tv-now__bar > span {
  display: block;
  height: 100%;
  background: var(--ds-accent);
}

.tv-rows {
  display: grid;
  gap: var(--ds-space-2);
  padding: var(--ds-space-4);
}

.tv-row {
  display: grid;
  /* logo | name | now | next | actions. The two schedule columns are the
     point of this format, so they get the room and the identity column is
     the one that yields. */
  grid-template-columns: auto minmax(8rem, 1.2fr) minmax(10rem, 1.6fr) minmax(10rem, 1.6fr) auto;
  align-items: center;
  gap: var(--ds-space-3);
  padding: var(--ds-space-3);
  background: var(--ds-surface);
  border: var(--ds-border-width) solid var(--ds-border);
  border-radius: var(--ds-radius-md);
  cursor: pointer;
}
.tv-row:hover { border-color: var(--ds-accent); }
.tv-row__logo {
  width: 2.5rem;
  height: 2.5rem;
  object-fit: contain;
  border-radius: var(--ds-radius-sm);
  background: var(--ds-surface-sunken);
}
.tv-row__logo--letter {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: var(--ds-weight-semibold);
  color: var(--ds-text-muted);
}
.tv-row__name { font-weight: var(--ds-weight-medium); }
.tv-row__meta,
.tv-row__next { font-size: var(--ds-text-xs); color: var(--ds-text-muted); }
.tv-row__label {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--ds-text-xs);
  color: var(--ds-text-subtle);
}
.tv-row__time { font-variant-numeric: tabular-nums; }
.tv-row__muted { font-style: italic; }
.tv-row__actions { display: flex; gap: var(--ds-space-1); }
.tv-row__actions [aria-pressed="true"] { color: var(--ds-danger); }

/* A five-column row has nowhere to go on a phone, so it becomes two: the
   channel and what is on it, with the upcoming column dropping out entirely
   rather than being crushed into two words and an ellipsis. */
@media (max-width: 48rem) {
  .tv-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      'logo ident actions'
      'logo now   now';
    row-gap: var(--ds-space-2);
  }
  .tv-row__logo { grid-area: logo; align-self: start; }
  .tv-row__ident { grid-area: ident; }
  .tv-row__now { grid-area: now; }
  .tv-row__actions { grid-area: actions; }
  .tv-row__next { display: none; }
}

.tv-view-select { width: auto; }

/* ── the player ────────────────────────────────────────────────────────────
 *
 * One control layer over the picture, drawn by ../public/player.js, never the
 * browser's own. See that file for why.
 */

.tv-player {
  position: fixed;
  inset: 0;
  z-index: var(--ds-z-dialog);
  display: flex;
  align-items: center;
  justify-content: center;
}
.tv-player[hidden] { display: none; }

.tv-player__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ds-neutral-900) 82%, transparent);
  border: none;
}

.tv-player__panel {
  position: relative;
  z-index: 1;
  width: min(92vw, 68rem);
  max-height: 100dvh;
  overflow: auto;
  background: var(--ds-surface);
  border-radius: var(--ds-radius-lg);
  box-shadow: var(--ds-shadow-lg);
  padding: var(--ds-space-3);
}

/* The picture and everything drawn on top of it. Position context for the
   control bar, and the element real fullscreen is requested on. */
.tv-player__stage {
  position: relative;
  background: var(--ds-neutral-900);
  border-radius: var(--ds-radius-md);
  overflow: hidden;
  aspect-ratio: 16 / 9;
  max-width: 100%;
}
.tv-player__video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  background: var(--ds-neutral-900);
}

/* Sound with no picture. Without this the stage is an empty pale rectangle
   and the only clue is a sentence below the fold — which is exactly how the
   MPEG-2 channels read as "the player is broken". */
.tv-player__stage.is-audio-only::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Below the chrome. A pseudo-element is painted after its element's
     positioned children, so with no z-index at all this sat on top of the
     head and the controls — the overlay did not just mark the dead picture,
     it hid every button for getting away from it. `pointer-events: none`
     meant they still worked, which is worse: invisible controls that respond
     to a click nobody can aim. */
  z-index: 1;
  background: repeating-linear-gradient(
    45deg,
    color-mix(in srgb, var(--ds-neutral-900) 92%, transparent),
    color-mix(in srgb, var(--ds-neutral-900) 92%, transparent) 12px,
    color-mix(in srgb, var(--ds-neutral-800) 92%, transparent) 12px,
    color-mix(in srgb, var(--ds-neutral-800) 92%, transparent) 24px
  );
  pointer-events: none;
}

.tv-player__head,
.tv-player__controls {
  position: absolute;
  left: 0;
  right: 0;
  /* Above the audio-only overlay, which is a pseudo-element and therefore
     paints after these unless told otherwise. */
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-3);
  color: var(--ds-neutral-0);
  /* The controls fade with the picture rather than sitting on it permanently:
     a bar that never leaves covers the bottom eighth of every stream. */
  opacity: 0;
  transition: opacity var(--ds-duration-base) var(--ds-ease);
}
.tv-player__head {
  top: 0;
  background: linear-gradient(to bottom, color-mix(in srgb, var(--ds-neutral-900) 78%, transparent), transparent);
}
.tv-player__controls {
  bottom: 0;
  flex-wrap: wrap;
  background: linear-gradient(to top, color-mix(in srgb, var(--ds-neutral-900) 82%, transparent), transparent);
}
/* Shown while somebody is actually here — `is-active`, driven by input in
   ../public/player.js — and whenever anything inside has keyboard focus.
   Deliberately NOT keyed on :hover: a fullscreen stage IS the screen, so the
   pointer is always over it and the bar never left, which is the one place it
   is least wanted. A touch screen has no hover at all, so that rule needed a
   second one forcing the chrome permanently on, and the two together meant the
   controls covered the bottom of every phone-sized picture for its whole
   duration. */
.tv-player__stage.is-active .tv-player__head,
.tv-player__stage.is-active .tv-player__controls,
.tv-player__stage:focus-within .tv-player__head,
.tv-player__stage:focus-within .tv-player__controls {
  opacity: 1;
}

/* The pointer goes too. A cursor parked over a fullscreen picture is the same
   distraction as the bar, and every player hides it for the same reason. */
.tv-player__stage:not(.is-active) { cursor: none; }

/* The channel's own mark, so the picture is identified before the name is
   read — which is how anybody actually recognises a channel. */
.tv-player__logo {
  width: 2rem;
  height: 2rem;
  flex: none;
  object-fit: contain;
  border-radius: var(--ds-radius-sm);
  background: color-mix(in srgb, var(--ds-neutral-0) 12%, transparent);
}
.tv-player__logo[hidden] { display: none; }

.tv-player__heading { min-width: 0; flex: 1; }
.tv-player__where {
  font-size: var(--ds-text-xs);
  opacity: 0.75;
}
.tv-player__where[hidden] { display: none; }
.tv-player__title { font-weight: var(--ds-weight-semibold); }
.tv-player__now { font-size: var(--ds-text-xs); opacity: 0.85; }
.tv-player__spacer { flex: 1; }

.tv-player__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px: the smallest target a thumb reliably hits, and these are used on a
     phone more than anywhere else. */
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: var(--ds-space-1);
  border: none;
  border-radius: var(--ds-radius-sm);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
/* `display` on a class beats the browser's own `[hidden] { display: none }`,
   because an author stylesheet wins over the user-agent one whatever the
   specificity — so every control that sets `display` has to say this too, and
   this file already does it for the logo, the menus, the position and the
   subtitle line. The button row did not, so `hidden` on a control was inert:
   `data-pip` showed on browsers with no picture-in-picture, and the cast button
   showed where nothing can be cast to. Both look like the feature working. */
.tv-player__btn[hidden] { display: none; }
.tv-player__btn:hover:not(:disabled) { background: color-mix(in srgb, var(--ds-neutral-0) 18%, transparent); }
.tv-player__btn:disabled { opacity: 0.4; cursor: default; }
.tv-player__btn[aria-pressed="true"] { color: var(--ds-danger); }
.tv-player__btn.is-on { color: var(--ds-accent); }

.tv-player__volume { display: flex; align-items: center; gap: var(--ds-space-1); }
.tv-player__slider { width: 5rem; accent-color: var(--ds-accent); }
/* A volume slider is the first thing to go when the bar runs out of room —
   the mute button beside it still does the job people actually need. */
@media (max-width: 40rem) {
  .tv-player__slider { display: none; }
}

.tv-player__live {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  padding: var(--ds-space-1) var(--ds-space-2);
  border: none;
  border-radius: var(--ds-radius-full);
  background: color-mix(in srgb, var(--ds-neutral-0) 18%, transparent);
  color: inherit;
  font-size: var(--ds-text-xs);
  font-weight: var(--ds-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
}
.tv-player__live.is-live { background: var(--ds-danger); cursor: default; }

.tv-player__menu-host { position: relative; }
.tv-player__menu {
  position: absolute;
  bottom: calc(100% + var(--ds-space-1));
  right: 0;
  min-width: 11rem;
  max-height: 16rem;
  overflow: auto;
  padding: var(--ds-space-1);
  background: var(--ds-surface);
  color: var(--ds-text);
  border: var(--ds-border-width) solid var(--ds-border);
  border-radius: var(--ds-radius-md);
  box-shadow: var(--ds-shadow-lg);
}
.tv-player__menu[hidden] { display: none; }
.tv-player__menu-item {
  display: block;
  width: 100%;
  padding: var(--ds-space-2);
  border: none;
  border-radius: var(--ds-radius-sm);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.tv-player__menu-item:hover { background: var(--ds-surface-sunken); }
.tv-player__menu-item[aria-checked="true"] { font-weight: var(--ds-weight-semibold); color: var(--ds-accent); }
.tv-player__menu-empty {
  padding: var(--ds-space-2);
  font-size: var(--ds-text-sm);
  color: var(--ds-text-muted);
}

/* Why a stream did not play, under the picture. Tokens only, like every app
   stylesheet here — see packages/design/README.md. */
.tv-player__note {
  margin: var(--ds-space-2) 0 0;
  color: var(--ds-danger);
  font-size: var(--ds-text-sm);
  text-align: center;
}

.tv-player__schedule {
  margin-top: var(--ds-space-3);
  display: grid;
  gap: var(--ds-space-1);
  font-size: var(--ds-text-sm);
}
.tv-player__schedule-row {
  display: grid;
  grid-template-columns: 4rem 1fr;
  gap: var(--ds-space-2);
  color: var(--ds-text-muted);
}
.tv-player__schedule-time { font-variant-numeric: tabular-nums; }

/* ── the phone ─────────────────────────────────────────────────────────────
 *
 * Portrait: the picture takes the full width. A dialog inset inside a 92vw
 * panel inside a 400px screen leaves a video about the size of a postage
 * stamp, which is the one place this app is most likely to be used.
 */
@media (max-width: 40rem) {
  .tv-player__panel {
    width: 100%;
    height: 100dvh;
    max-height: none;
    border-radius: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  .tv-player__stage { border-radius: 0; }
  .tv-player__note,
  .tv-player__schedule { padding: 0 var(--ds-space-4); }
}

/**
 * Landscape: tilt the phone and the picture fills the screen.
 *
 * Done in CSS rather than by calling requestFullscreen() on an orientation
 * change, and that is not a stylistic preference — it is the only thing that
 * works. The Fullscreen API requires a user gesture, and `orientationchange`
 * is not one: Chrome and Firefox reject the call outright, and iOS Safari has
 * no element fullscreen at all. A rule that makes the stage fill the viewport
 * needs no permission, fires on the rotation itself, and reverses the instant
 * the phone comes back upright.
 *
 * Keyed on a short viewport rather than on `orientation: landscape` alone, so
 * a landscape tablet or a desktop window that happens to be wide keeps the
 * ordinary dialog.
 */
@media (orientation: landscape) and (max-height: 32rem) {
  .tv-player__panel {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100dvh;
    max-height: none;
    padding: 0;
    border-radius: 0;
    overflow: hidden;
    background: var(--ds-neutral-900);
  }
  .tv-player__stage {
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    border-radius: 0;
  }
  /* There is no room for anything but the picture and its controls. The note
     still matters, so it moves onto the stage rather than off the screen. */
  .tv-player__schedule { display: none; }
  .tv-player__note {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 4.5rem;
    z-index: 2;
    margin: 0;
    color: var(--ds-neutral-0);
    text-shadow: 0 1px 3px var(--ds-neutral-900);
  }
}

/* Real fullscreen, when the browser gave it to us. Same stage, no dialog
   around it. */
.tv-player__stage:fullscreen {
  width: 100vw;
  height: 100dvh;
  aspect-ratio: auto;
  border-radius: 0;
}

/* The page behind a playing video does not scroll. Without this, a phone
   scrolls the list under the player while somebody reaches for a control. */
body.tv-playing { overflow: hidden; }

/* Which list ch+ and ch- are moving through, under the channel name. Quiet:
   it answers a question you only ask while surfing, and the rest of the time
   it must not compete with the programme title above it. */
.tv-player__position {
  font-size: var(--ds-text-xs);
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}
.tv-player__position[hidden] { display: none; }

/* The general form of the bug fixed above on .tv-player__btn: an author
   stylesheet beats the browser's own `[hidden] { display: none }` whatever the
   specificity, so EVERY element in here that sets `display` silently ignores
   `hidden`. Stating it once for the whole player means the next control added
   cannot reintroduce it — which matters because the failure looks like the
   feature working, and cost two rounds of guessing at a missing button. */
.tv-player [hidden] { display: none !important; }

/* ── paired televisions, in the sidebar ────────────────────────────────────
   Tokens only (CLAUDE.md rule 5): no raw values, no redefining .ds-* internals. */
.tv-device {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2);
  border-radius: var(--ds-radius-sm);
}
.tv-device:hover { background: var(--ds-surface-sunken); }
.tv-device__body { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.tv-device__name { font-size: var(--ds-text-sm); }
.tv-device__meta {
  font-size: var(--ds-text-xs);
  color: var(--ds-text-subtle);
  font-variant-numeric: tabular-nums;
}

/* ── pairing a television ──────────────────────────────────────────────────
   Read on a phone, by somebody standing in front of a TV. One column, one
   field, generous targets — there is no app shell here and nothing to navigate
   to, so the page is the card. Server-rendered by ../src/pairing-pages.js. */
.ds-pair {
  display: flex;
  justify-content: center;
  padding: var(--ds-space-6) var(--ds-space-4);
  background: var(--ds-surface-sunken);
  min-height: 100vh;
}
.ds-pair__card {
  width: 100%;
  max-width: 26rem;
  background: var(--ds-surface);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-6);
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-4);
}
.ds-pair__title { font-size: var(--ds-text-xl); margin: 0; }
.ds-pair__lede { margin: 0; color: var(--ds-text); }
.ds-pair__note { margin: 0; font-size: var(--ds-text-sm); color: var(--ds-text-subtle); }
.ds-pair__error {
  margin: 0;
  padding: var(--ds-space-3);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-danger-subtle);
  color: var(--ds-danger);
}
.ds-pair__form { display: flex; flex-direction: column; gap: var(--ds-space-4); }
/* The code is eight characters read off a screen across a room. Wide tracking
   and tabular figures so a typo is visible before it is submitted. */
.ds-pair__code-input {
  font-size: var(--ds-text-xl);
  letter-spacing: 0.15em;
  text-align: center;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
