/* ------------------------------------------------------------------
   components.css — reusable pieces.

   The focus ring is the single most important rule in this file. Focus
   is applied by focus.js as a class; it is never :focus, never tab
   order, and never implied by hover.
   ------------------------------------------------------------------ */

/* ---- bidirectional text ------------------------------------------- */

/*
 * Provider text carries dir="auto" so Arabic, Hebrew and Farsi names are
 * shaped and ordered correctly. That is about the *glyphs*, not the layout:
 * an auto-detected RTL string also flips the element's direction, and since
 * text-align defaults to `start`, the whole line jumps to the right edge of
 * its box. In an interface whose panes, navigation and reading order all run
 * left to right, that reads as broken — an Arabic series title stranded on
 * the far right while its own subtitle sits on the left.
 *
 * So: keep dir="auto" for correct bidi rendering, and pin alignment to the
 * surrounding layout. A genuinely right-to-left *interface* is a different
 * job from rendering right-to-left *content*.
 */
[dir="auto"] { text-align: left; }

/* Centred contexts opt back in. */
.person [dir="auto"] { text-align: center; }

/* ---- focus ------------------------------------------------------- */

.focusable {
  position: relative;
  transition: transform var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              background-color var(--t-fast) var(--ease);
  will-change: transform;
}

/* Plain `.is-focused`, deliberately.
   A grid cell is `poster vgrid__cell` and a list row is `ch-row vlist__row`
   — the recycling containers mark the cursor with `is-focused` and never add
   `focusable`, which lives on the container itself. Qualifying this selector
   with `.focusable` therefore drops the ring from every poster and every row,
   i.e. from the two places the cursor spends all its time.
   Components that declare their own box-shadow later in this file (.btn,
   .sort-chip, .setting-row) used to beat this rule at equal specificity and
   showed no focus at all; each now has its own `.x.is-focused` rule below
   rather than this one being made stricter. */
.is-focused {
  z-index: 2;
  transform: scale(var(--focus-scale));
  /* The ring is INSET.
     An outer ring is clipped by every ancestor with overflow != visible,
     and this app has nine of them: the app root, each screen, the screen
     body, both scroll helpers, the list viewport, the debug log and the
     nav at narrow widths. Reserving bleed in all of them is whack-a-mole.
     A ring drawn inside the element's own box cannot be clipped by
     anything, ever. The outer glow is decorative and may clip harmlessly. */
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-focus),
              var(--shadow-focus);
}

/* Full-width rows do not scale: there is nowhere for them to grow into,
   and a row that widens under the cursor reads as a wobble rather than a
   lift. The inset ring plus a background change is the whole affordance. */
.vlist__row.is-focused,
.row-item.is-focused,
.cat-row.is-focused,
.epg-row.is-focused,
.setting-row.is-focused,
.btn.is-focused,
.sort-chip.is-focused {
  transform: none;
}

/* ---- focused buttons, chips and settings rows --------------------------
   These three keep their own depth shadow AND get the ring, and add one
   more cue that costs no geometry: an accent bar seated inside the bottom
   edge. Inset, so like the ring it cannot be clipped by an ancestor, and
   unlike a scale it cannot wobble or overlap its neighbour. */

.btn.is-focused,
.sort-chip.is-focused,
.setting-row.is-focused {
  background: var(--c-surface-3);
  color: var(--c-text);
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-focus),
              inset 0 calc(-1 * var(--focus-accent)) 0 0 var(--c-accent),
              var(--shadow-focus);
}

.btn.is-focused .icon,
.sort-chip.is-focused .icon { color: var(--c-text); }

/* The primary button is already a bright fill, so a white ring on it is
   invisible. It gets the same treatment in reverse. */
.btn--primary.is-focused {
  background: var(--c-action);
  color: var(--c-action-text);
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-action-text),
              inset 0 calc(-1 * var(--focus-accent)) 0 0 var(--c-accent),
              var(--shadow-focus);
}
.btn--primary.is-focused .icon { color: var(--c-action-text); }

/* ---- buttons ----------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  min-height: 54px;
  padding: 0 var(--sp-5);
  background: var(--c-surface-2);
  color: var(--c-text);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  letter-spacing: 0.02em;
  border-radius: var(--r-md);
  white-space: nowrap;
  box-shadow: var(--shadow-1);
}
.btn .icon { flex: 0 0 auto; opacity: 0.85; }

/* The primary action is the brightest neutral, not the loudest colour. */
.btn--primary {
  background: var(--c-action);
  color: var(--c-action-text);
  font-weight: var(--fw-bold);
}
.btn--primary .icon { opacity: 1; }

.btn--ghost {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--c-line);
}
.btn--danger { color: #FF8A8F; }
.btn--block  { display: flex; width: 100%; }
.btn--sm     { min-height: 44px; padding: 0 var(--sp-4); font-size: var(--fs-xs); }
.btn[disabled], .btn.is-disabled { opacity: 0.4; box-shadow: none; }

.icon {
  display: block;
  /* --icon-size is set per glyph by icons.js; the attribute size stands if
     either property is missing. */
  width:  calc(var(--icon-size, 22px) * var(--icon-scale, 1));
  height: calc(var(--icon-size, 22px) * var(--icon-scale, 1));
  flex-shrink: 0;
}

/* ---- nav items --------------------------------------------------- */

/* Icon only. The name is not printed under the glyph any more: eight labels
   stacked down the edge is eight pieces of text competing with the content
   they sit beside, and on a remote you are never hunting — you are stepping
   through a list you already know. The name appears when you land on it, as
   a pill beside the icon, and then gets out of the way. */
.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  /* An explicit height rather than aspect-ratio: whether aspect-ratio holds
     on this panel's compositor is still an open question (see §0.4 of the
     testing checklist), and the navigation is the last thing that should be
     waiting on an answer. */
  height: var(--nav-item-h);
  padding: 0;
  border-radius: var(--r-md);
  color: var(--c-text-faint);
  transition: color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.nav-item .icon { opacity: 0.7; }

/* The floating name.

   Positioned outside the rail, which is why .nav carries overflow:visible.
   pointer-events:none so it can never intercept a click meant for the icon,
   and it is not display:none between showings because an animation cannot
   start on an element that has no box. */
.nav-item__label {
  position: absolute;
  left: 100%;
  top: 50%;
  margin-left: var(--sp-2);
  padding: 6px var(--sp-3);
  border-radius: var(--r-pill);
  background: var(--c-surface-3);
  color: var(--c-text);
  box-shadow: var(--shadow-2);
  font-size: calc(var(--fs-xs) * 0.9);
  font-weight: var(--fw-medium);
  letter-spacing: 0.06em;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-50%) translateX(-8px);
  z-index: 3;
}

/* Shown on arrival, then it leaves. `forwards` holds the final frame, so a
   label that has had its moment stays gone until focus lands here again —
   at which point the class is re-applied and the animation restarts. */
.nav-item.is-focused .nav-item__label {
  animation: nav-label-visit 2400ms var(--ease) forwards;
}

@keyframes nav-label-visit {
  0%   { opacity: 0; transform: translateY(-50%) translateX(-8px); }
  8%   { opacity: 1; transform: translateY(-50%) translateX(0); }
  70%  { opacity: 1; transform: translateY(-50%) translateX(0); }
  100% { opacity: 0; transform: translateY(-50%) translateX(-8px); }
}

/* Someone who has turned motion down should not get a label that slides and
   fades; they get one that simply stays. */
@media (prefers-reduced-motion: reduce) {
  .nav-item.is-focused .nav-item__label {
    animation: none;
    opacity: 1;
    transform: translateY(-50%);
  }
}

.nav-item.is-current {
  color: var(--c-text);
  background: var(--c-surface-2);
  box-shadow: var(--shadow-1);
}
.nav-item.is-current .icon { opacity: 1; }
/* The current section gets the one spot of brand colour in the chrome. */
.nav-item.is-current .icon { color: var(--c-accent); }
/* The ring has to be repeated here, and the reason is the trap described at
   the top of this file rather than a style choice.

   `.nav-item.is-current` above declares `box-shadow: var(--shadow-1)` at
   (0,2,0). The ring lives in `.is-focused` at (0,1,0). So on every screen,
   the one nav item that matches the screen you are on — SERIES on series
   detail, MOVIES on movies — lost its ring completely and showed no focus
   at all. Everywhere else in the nav it worked, which is exactly why it
   survived: the broken item is the one you are least likely to press.

   Declared after `.is-current` so that an item which is both current and
   focused resolves to focused. */
.nav-item.is-focused {
  color: var(--c-text);
  background: var(--c-surface-3);
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-focus),
              inset 0 calc(-1 * var(--focus-accent)) 0 0 var(--c-accent),
              var(--shadow-focus);
}

/* ---- tab chips ---------------------------------------------------
   A horizontal pill for tab strips: the favourites type tabs, the guide's
   category chips and the season picker.

   All three used to borrow `.nav-item`, which was harmless while the nav
   was a horizontal bar of pills and wrong the moment it became a rail of
   icon squares — they turned into 60px squares with their labels floating
   off to one side. They were never navigation; they are their own control,
   so they have their own class. */

.tab-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 52px;
  padding: 0 var(--sp-5);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  letter-spacing: 0.08em;
  color: var(--c-text-faint);
  white-space: nowrap;
  transition: color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.tab-chip .icon { opacity: 0.7; }

.tab-chip.is-current {
  color: var(--c-text);
  background: var(--c-surface-2);
  box-shadow: var(--shadow-1);
}
.tab-chip.is-current .icon { opacity: 1; color: var(--c-accent); }

/* After `.is-current`, and carrying the ring itself — the same trap as the
   rail: a component that declares its own box-shadow at (0,2,0) silently
   beats the (0,1,0) ring and shows no focus at all. */
.tab-chip.is-focused {
  transform: none;
  color: var(--c-text);
  background: var(--c-surface-3);
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-focus),
              inset 0 calc(-1 * var(--focus-accent)) 0 0 var(--c-accent),
              var(--shadow-focus);
}

/* A strip of them. Chips need real air between them or they read as one
   long segmented bar rather than separate choices. */
.tab-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding-bottom: var(--sp-4);
}

/* ---- cards ------------------------------------------------------- */

.card {
  background: var(--c-surface);
  border-radius: var(--r-lg);
  padding: var(--sp-4) var(--sp-5);
  box-shadow: var(--shadow-1);
}
.card--flat { background: var(--c-surface-2); box-shadow: none; }

/* A card heading: small, quiet, with a hairline under it. */
.card > h2 {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-sm) !important;
  letter-spacing: 0.08em;
  color: var(--c-text-dim);
  text-transform: uppercase;
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--c-line);
  margin-bottom: var(--sp-3) !important;
}
.card > h2 .icon { color: var(--c-text-faint); }

/* ---- list rows --------------------------------------------------- */

.row-item {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 84px;
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  background: transparent;
}
.row-item.is-focused { background: var(--c-surface-3); }
.row-item__title { font-size: var(--fs-base); }
.row-item__meta  { font-size: var(--fs-xs); color: var(--c-text-dim); }

/* ---- form fields ------------------------------------------------- */

.field { display: block; margin-bottom: var(--sp-5); }
.field__label {
  display: block;
  font-size: var(--fs-sm);
  color: var(--c-text-dim);
  margin-bottom: var(--sp-2);
  letter-spacing: 0.04em;
}
.field__input {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 76px;
  padding: 0 var(--sp-5);
  background: var(--c-surface-2);
  border-radius: var(--r-md);
  font-size: var(--fs-base);
  font-family: inherit;
  color: var(--c-text);
}
.field__input::placeholder { color: var(--c-text-faint); }

/* ---- badges & pills ---------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-sm);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  background: var(--c-surface-3);
  color: var(--c-text-dim);
}
.badge--ok   { background: rgba(34,197,94,0.18);  color: var(--c-ok); }
.badge--warn { background: rgba(245,158,11,0.18); color: var(--c-warn); }
.badge--bad  { background: rgba(239,68,68,0.18);  color: var(--c-bad); }
.badge--live { background: var(--c-primary); color: #fff; }

/* ---- spinner ----------------------------------------------------- */

.spinner {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-4);
  color: var(--c-text-dim);
  font-size: var(--fs-sm);
}
.spinner__ring {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--c-surface-3);
  border-top-color: var(--c-primary);
  animation: spin 900ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.loading-block {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 240px;
}

/* ---- empty & error states ---------------------------------------- */

.state-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  height: 100%;
  min-height: 300px;
  text-align: center;
  padding: var(--sp-7);
}
.state-block__title { font-size: var(--fs-xl); font-weight: var(--fw-bold); }
.state-block__text  { font-size: var(--fs-sm); color: var(--c-text-dim); max-width: 760px; }
.state-block__actions { display: flex; gap: var(--sp-4); margin-top: var(--sp-4); }

/* ---- toast ------------------------------------------------------- */

.toast-stack {
  position: fixed;
  right: var(--safe-x);
  bottom: var(--safe-y);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-3);
}
.toast {
  min-width: 320px;
  max-width: 720px;
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-md);
  background: var(--c-surface-3);
  color: var(--c-text);
  font-size: var(--fs-sm);
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
  animation: toast-in var(--t-base) var(--ease);
}
.toast--error { background: var(--c-bad); }
.toast--ok    { background: var(--c-ok); color: #06210F; }
.toast.is-leaving { opacity: 0; transition: opacity var(--t-base) var(--ease); }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* ---- dialog ------------------------------------------------------ */

/* ---- trailer ------------------------------------------------------
   The one place this app shows something from another host. It looks like
   the player rather than like a dialog, because that is what it is. */

.trailer-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-scrim);
}

.trailer {
  width: min(1280px, 84vw);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.trailer__frame {
  width: 100%;
  /* 16:9 by height rather than aspect-ratio: whether aspect-ratio holds on
     this panel's compositor is still an open question (checklist 0.4), and
     a trailer that lays out wrong is a poor place to find out. */
  height: calc(min(1280px, 84vw) * 0.5625);
  border: 0;
  border-radius: var(--r-lg);
  background: #000;
  box-shadow: var(--shadow-2);
}

.trailer__bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
}
.trailer__name {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.trailer__hint {
  flex: 0 0 auto;
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  letter-spacing: 0.06em;
}

.trailer__actions { display: flex; justify-content: flex-end; }

.dialog-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-scrim);
}
.dialog {
  width: min(880px, 70vw);
  padding: var(--sp-7);
  border-radius: var(--r-lg);
  background: var(--c-bg-elevated);
  box-shadow: 0 24px 80px rgba(0,0,0,0.7);
  animation: dialog-in var(--t-base) var(--ease);
}
.dialog__title { font-size: var(--fs-xl); font-weight: var(--fw-bold); margin-bottom: var(--sp-3); }
.dialog__text  { font-size: var(--fs-sm); color: var(--c-text-dim); margin-bottom: var(--sp-6); }
.dialog__actions { display: flex; gap: var(--sp-4); }
@keyframes dialog-in {
  from { opacity: 0; transform: scale(0.97); }
  to   { opacity: 1; transform: none; }
}

/* ---- debug console ----------------------------------------------- */

.debug {
  position: fixed;
  inset: auto 0 0 0;
  height: 52vh;
  z-index: var(--z-debug);
  display: flex;
  flex-direction: column;
  background: rgba(4, 7, 10, 0.96);
  border-top: 2px solid var(--c-primary);
  font-family: "SF Mono", Menlo, Consolas, monospace;
  font-size: 15px;
  line-height: 1.4;
}
.debug__bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  border-bottom: 1px solid var(--c-line);
  color: var(--c-text-dim);
  font-size: 14px;
}
.debug__log { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--sp-3) var(--sp-5); }
.debug__line { display: flex; gap: var(--sp-3); padding: 2px 0; word-break: break-word; }
.debug__t   { color: var(--c-text-faint); flex: 0 0 auto; }
.debug__tag { flex: 0 0 96px; }
.debug__msg { flex: 1 1 auto; white-space: pre-wrap; }
.debug__line--error { color: #FCA5A5; }
.debug__line--warn  { color: #FCD34D; }
.debug__line--info  { color: #C9D4E0; }
.debug__line--debug { color: #7E8CA0; }

/* ---- on-screen keyboard ------------------------------------------ */

.kb {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  background: var(--c-surface);
  border-radius: var(--r-lg);
  padding: var(--sp-4);
}

.kb__bar {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  min-height: 34px;
}
.kb__hint   { font-size: var(--fs-xs); color: var(--c-text-faint); letter-spacing: 0.08em; }
.kb__target { font-size: var(--fs-sm); color: var(--c-text); }

.kb__helpers {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.kb__helper {
  padding: 0 var(--sp-4);
  height: 60px;
  border-radius: var(--r-sm);
  background: var(--c-surface-3);
  color: var(--c-text);
  font-size: var(--fs-sm);
  font-family: "SF Mono", Menlo, Consolas, monospace;
  white-space: nowrap;
}

.kb__grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: var(--sp-2);
}

.kb__key {
  height: 76px;
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
  color: var(--c-text);
  font-size: var(--fs-base);
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease);
}
.kb__key--action  { background: var(--c-surface-3); font-size: var(--fs-sm); }
.kb__key--primary { background: var(--c-primary); font-weight: var(--fw-bold); }
.kb__key.is-on    { background: var(--c-primary); }

@media (min-width: 2560px) {
  .kb__key    { height: 128px; }
  .kb__helper { height: 100px; }
}
@media (max-width: 1400px) {
  .kb__key    { height: 58px; }
  .kb__helper { height: 48px; font-size: var(--fs-xs); }
}

/* ---- login ------------------------------------------------------- */

.login {
  display: flex;
  gap: var(--sp-8);
  height: 100%;
  min-height: 0;
  align-items: flex-start;
}
/* max-height is what makes the column scrollable rather than clipped.
   `.login` is align-items: flex-start, so a child sizes to its own content
   and nothing bounds it: the fields, the two buttons, the status area and
   the storage note together run past the bottom of the screen — 95px over
   at normal size, 207px at large — and `.screen__body` is overflow:hidden,
   so the CONNECT button simply was not there. Bounding the column against
   `.login`'s own height gives `.scroll-y` (on the element in login.js)
   something to scroll. The keyboard column keeps its natural height, so it
   stays put while the fields move. */
.login__left  {
  flex: 0 0 640px; display: flex; flex-direction: column; min-width: 0;
  max-height: 100%;
}
.login__right { flex: 1 1 auto; min-width: 0; max-height: 100%; }

.login__brand {
  font-size: var(--fs-3xl);
  font-weight: var(--fw-bold);
  letter-spacing: -0.02em;
  line-height: 1;
  margin-bottom: var(--sp-2);
}
.login__brand b { color: var(--c-primary); }
.login__tag {
  font-size: var(--fs-sm);
  color: var(--c-text-dim);
  margin-bottom: var(--sp-7);
}

.field__row { display: flex; align-items: center; gap: var(--sp-3); }
/* The field is a real <input>, so a physical or Bluetooth keyboard can type
   into it and — the whole point — the clipboard can paste into it. The
   on-screen keyboard writes to the same element. */
.field__row .field__input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: "SF Mono", Menlo, Consolas, monospace;
  text-overflow: ellipsis;
}
.field__input::placeholder { color: var(--c-text-faint); }
.field__input::selection { background: var(--c-primary); color: #fff; }

.field__toggle {
  flex: 0 0 auto;
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  letter-spacing: 0.06em;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--c-surface-3);
}

.field__error {
  font-size: var(--fs-xs);
  color: var(--c-bad);
  margin-top: var(--sp-2);
  min-height: 1.4em;
}

.field.is-active .field__input { box-shadow: inset 0 0 0 2px var(--c-primary); }

.login__status {
  margin-top: var(--sp-4);
  min-height: 90px;
  font-size: var(--fs-sm);
}
.login__status--error { color: var(--c-bad); }
.login__note {
  margin-top: var(--sp-5);
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  line-height: 1.5;
}

@media (max-width: 1400px) {
  .login { gap: var(--sp-5); }
  .login__left { flex-basis: 420px; }
}

/* ---- programme guide --------------------------------------------- */

.epg-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-line);
}
.epg-row.is-now { background: var(--c-surface-2); border-radius: var(--r-sm); }

.epg-row__time  { flex: 0 0 210px; color: var(--c-text-dim); font-size: var(--fs-sm); }
.epg-row__main  { flex: 1 1 auto; min-width: 0; }
.epg-row__title { font-size: var(--fs-base); }
.epg-row__desc  {
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  margin-top: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.epg-row__bar {
  margin-top: var(--sp-2);
  height: 4px;
  border-radius: 2px;
  background: var(--c-surface-3);
  overflow: hidden;
}
.epg-row__bar i {
  display: block;
  height: 100%;
  background: var(--c-primary);
}

@media (max-width: 1400px) {
  .epg-row__time { flex-basis: 150px; }
}

/* ---- virtual list ------------------------------------------------ */

.vlist {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* The region itself never shows the focus ring — the ring goes on the
   row holding the cursor, which is what the user is actually pointing at. */
.vlist.is-focused {
  transform: none;
  box-shadow: none;
}

.vlist__viewport {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  position: relative;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.vlist__viewport::-webkit-scrollbar { width: 0; height: 0; display: none; }

.vlist__spacer { position: relative; width: 100%; }

.vlist__row {
  will-change: transform;
  contain: layout style;
}

.vlist__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 200px;
  color: var(--c-text-faint);
  font-size: var(--fs-sm);
}

/* A list without focus shows NOTHING selected.
   It previously kept a muted cursor so you could see where you would land
   on returning — but on screen that reads as "this row is chosen", when the
   user has not chosen anything. Nothing is highlighted until the list
   actually holds focus. */
.vlist:not(.is-active) .vlist__row.is-focused,
.vlist:not(.is-active) .vlist__row.is-focused:hover {
  box-shadow: none;
  background: transparent;
  transform: none;
}

/* An element that still owns focus while a dialog is over it. */
.is-focused-muted {
  box-shadow: inset 0 0 0 2px var(--c-text-faint);
}

/* ---- channel row ------------------------------------------------- */

.ch-row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  background: transparent;
}
.ch-row__num {
  flex: 0 0 90px;
  color: var(--c-text-faint);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.ch-row__logo {
  flex: 0 0 auto;
  width: 96px;
  height: 54px;
  border-radius: var(--r-sm);
  object-fit: contain;
  background: var(--c-surface-2);
}

/* A film or a series in a row is a POSTER, and a poster is 2:3.

   Favourites rendered every type through the channel row, so a portrait
   poster was squeezed into a 96x54 landscape box: `object-fit: contain`
   kept it from distorting and shrank it to about a third of the height
   instead, which is what "small and shrinked" looks like. Posters get a
   portrait box and `cover`, so the art fills it. */
.ch-row__logo--poster {
  width: 76px;
  height: 114px;
  object-fit: cover;
  background: var(--c-surface-2);
}
.ch-row__body { flex: 1 1 auto; min-width: 0; }   /* min-width:0 as above */
.ch-row__name {
  font-size: var(--fs-base);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ch-row__meta {
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}
.ch-row__fav {
  flex: 0 0 auto;
  width: 44px;
  text-align: center;
  font-size: var(--fs-md);
  color: var(--c-primary);
}

.lazy-img { opacity: 1; transition: opacity var(--t-base) var(--ease); }

@media (min-width: 2560px) {
  .ch-row__logo { width: 168px; height: 94px; }
  .ch-row__num  { flex-basis: 150px; }
}
@media (max-width: 1400px) {
  .ch-row__logo { width: 72px; height: 40px; }
  .ch-row__num  { flex-basis: 64px; }
}

/* ---- live tv ------------------------------------------------------ */

.pane-title {
  flex: 0 0 auto;
  font-size: var(--fs-xs);
  letter-spacing: 0.1em;
  color: var(--c-text-faint);
  padding-bottom: var(--sp-2);
}

.cat-row {
  display: flex;
  align-items: center;
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  color: var(--c-text-dim);
}
.cat-row.is-current { color: var(--c-text); }
.cat-row.is-focused { color: var(--c-text); background: var(--c-surface-3); }
.cat-row__name { width: 100%; overflow-wrap: anywhere; }

/* ---- home rails ---------------------------------------------------- */

.rail { flex: 0 0 auto; margin-bottom: var(--sp-6); }

.rail__title {
  font-size: var(--fs-sm);
  letter-spacing: 0.1em;
  color: var(--c-text-faint);
  margin-bottom: var(--sp-3);
}

.rail__items {
  display: flex;
  gap: var(--sp-4);
  align-items: flex-start;
}

.rail__empty, .rail__loading {
  color: var(--c-text-faint);
  font-size: var(--fs-sm);
  padding: var(--sp-4) 0;
}

.card-tile {
  flex: 0 0 auto;
  width: 200px;
  text-align: left;
  border-radius: var(--r-md);
  background: var(--c-surface);
  padding: var(--sp-2);
  position: relative;
}
.card-tile--wide { width: 220px; }

.card-tile__art {
  width: 100%;
  height: 264px;
  border-radius: var(--r-sm);
  object-fit: cover;
  background: var(--c-surface-2);
  display: block;
}
.card-tile__art--wide { height: 112px; object-fit: contain; }

.card-tile__name {
  font-size: var(--fs-sm);
  margin-top: var(--sp-2);
  overflow-wrap: anywhere;
}
.card-tile__meta { font-size: var(--fs-xs); color: var(--c-text-dim); }

.card-tile__bar {
  position: absolute;
  left: var(--sp-2);
  right: var(--sp-2);
  bottom: 54px;
  height: 4px;
  border-radius: 2px;
  background: rgba(0,0,0,0.6);
  overflow: hidden;
}
.card-tile__bar i { display: block; height: 100%; background: var(--c-primary); }

.cat-tile {
  flex: 0 0 auto;
  width: 300px;
  height: 132px;
  display: flex;
  align-items: flex-end;
  padding: var(--sp-4);
  border-radius: var(--r-md);
  background: var(--c-surface);
  text-align: left;
}
.cat-tile__name {
  /* min-width:0 is the load-bearing line. A flex child defaults to
     min-width:auto and so refuses to shrink below its content, which is
     why long provider names spilled straight past the tile and over the
     focus ring even with a width set. */
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-base);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Each breakpoint re-states the --wide variants. A bare `.card-tile__art`
   here comes later than the base `--wide` rule at equal specificity, so
   without them channel tiles inherited the poster height at 4K and rendered
   as tall empty boxes with the logo as a strip across the middle. */
@media (min-width: 2560px) {
  .card-tile { width: 340px; }
  .card-tile--wide { width: 374px; }
  .card-tile__art { height: 450px; }
  .card-tile__art--wide { height: 190px; }
  .cat-tile { width: 480px; height: 220px; }
}
@media (max-width: 1400px) {
  .card-tile { width: 150px; }
  .card-tile--wide { width: 165px; }
  .card-tile__art { height: 200px; }
  .card-tile__art--wide { height: 84px; }
  .cat-tile { width: 230px; height: 104px; }
}

/* ---- virtual grid -------------------------------------------------- */

.vgrid {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.vgrid.is-focused { transform: none; box-shadow: none; }

.vgrid__viewport {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  position: relative;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.vgrid__viewport::-webkit-scrollbar { width: 0; height: 0; display: none; }

.vgrid__spacer { position: relative; width: 100%; }
.vgrid__cell   { will-change: transform; contain: layout style; }

.vgrid__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 220px;
  color: var(--c-text-faint);
  font-size: var(--fs-sm);
}
.vgrid:not(.is-active) .vgrid__cell.is-focused {
  box-shadow: none;
  transform: none;
}

/* ---- poster cell --------------------------------------------------- */

.poster {
  border-radius: var(--r-md);
  background: var(--c-surface);
  overflow: hidden;
  text-align: left;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--t-fast) var(--ease);
}
.poster.is-focused { box-shadow: var(--shadow-2); }
/* The ring is an overlay, not the card's own inset shadow. An inset shadow
   paints beneath the card's children, and the frame is opaque and flush to
   the card's edges — so the artwork covered the ring on three sides and it
   only showed around the caption. A pseudo-element drawn last sits above
   the artwork and cannot be covered or clipped. */
.poster.is-focused::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-focus);
  pointer-events: none;
}

/* The artwork and everything that sits on top of it. */
.poster__frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  border-radius: var(--r-md) var(--r-md) 0 0;
  background: var(--c-surface-2);
}

.poster__art {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* A scrim under the badges, so a star on a pale poster is still legible.
   Drawn on the frame rather than the image: an <img> cannot carry a
   pseudo-element, and putting it on the card would tint the caption too. */
.poster__frame::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 38%;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  pointer-events: none;
}

/* Badges, at opposite corners. Both are empty most of the time and an
   empty badge must not draw a plate, hence :empty. */
.poster__rating,
.poster__fav {
  position: absolute;
  top: var(--sp-2);
  z-index: 1;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 1;
  padding: 5px 8px;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, 0.62);
  white-space: nowrap;
}
.poster__rating { right: var(--sp-2); color: #FFD35C; }
.poster__fav    { left:  var(--sp-2); color: var(--c-primary); }
.poster__rating:empty,
.poster__fav:empty { display: none; }

/* Progress sits on the artwork now, along its bottom edge, where it reads
   as "how far into this film" rather than as a divider between two blocks. */
.poster__bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4px;
  background: rgba(0, 0, 0, 0.55);
  z-index: 1;
}
.poster__bar i { display: block; height: 100%; background: var(--c-primary); }

.poster__meta {
  flex: 0 0 auto;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
}
.poster__name {
  font-size: var(--fs-sm);
  line-height: 1.25;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}
/* Just the year now. The rating moved to the artwork, because the two sat
   close enough to read as one value. */
.poster__sub {
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  margin-top: 4px;
  letter-spacing: 0.04em;
}

/* ---- detail screen -------------------------------------------------- */

.detail {
  display: flex;
  gap: var(--sp-7);
  height: 100%;
  min-height: 0;
}
.detail__poster { flex: 0 0 340px; }
.detail__art {
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: var(--r-lg);
  background: var(--c-surface-2);
}
.detail__body {
  flex: 1 1 auto;
  min-width: 0;
  overflow-y: auto;
  scrollbar-width: none;

  /* The column had no padding of its own: the title, the plot and the
     season strip all started hard against the poster and ran to the right
     edge of the screen.

     The block padding is the focus bleed rather than a round number,
     because this is a scroll container and a focused season tile carries a
     ring and a scale — without reserved room at the top and bottom edges
     the ring is clipped flat against the container. The negative block
     margin cancels it again, so the column still lines up with the top of
     the poster instead of sitting 12px lower than it. */
  padding: var(--focus-bleed) var(--sp-5);
  margin-block: calc(var(--focus-bleed) * -1);
  scroll-padding: var(--focus-bleed);
}
.detail__body::-webkit-scrollbar { width: 0; display: none; }

.detail__title {
  font-size: var(--fs-2xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  overflow-wrap: anywhere;
}
.detail__facts {
  font-size: var(--fs-sm);
  color: var(--c-text-dim);
  margin-top: var(--sp-3);
}
.detail__progress {
  height: 5px;
  border-radius: 3px;
  background: var(--c-surface-3);
  margin-top: var(--sp-4);
  max-width: 520px;
  overflow: hidden;
}
.detail__progress i { display: block; height: 100%; background: var(--c-primary); }

.detail__plot {
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--c-text);
  margin-top: var(--sp-5);
  max-width: 1000px;
}
.detail__fact {
  display: flex;
  gap: var(--sp-4);
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  margin-top: var(--sp-3);
}
.detail__factLabel { flex: 0 0 120px; color: var(--c-text-faint); letter-spacing: 0.06em; }

.detail__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
}

@media (max-width: 1400px) {
  .detail { gap: var(--sp-5); }
  .detail__poster { flex-basis: 220px; }
}

/* ---- cast & crew ---------------------------------------------------- */

.cast { margin-top: var(--sp-6); }

.cast__title {
  font-size: var(--fs-xs);
  letter-spacing: 0.1em;
  color: var(--c-text-faint);
  margin-bottom: var(--sp-3);
}

.cast-row {
  display: flex;
  gap: var(--sp-4);
  align-items: flex-start;
  padding-bottom: var(--sp-2);
}

.person {
  flex: 0 0 auto;
  /* The tile is the avatar plus its own padding. Deriving it this way is
     what keeps the image square: setting the image to width:100% of a
     padded tile made it 114x130 — an ellipse, not a circle. */
  width: calc(var(--person-size) + var(--sp-2) * 2);
  text-align: center;
  border-radius: var(--r-md);
  padding: var(--sp-2);
}

.person__photo {
  /* Both axes from the same token. Never width:100%. */
  width: var(--person-size);
  height: var(--person-size);
  min-width: var(--person-size);
  border-radius: 50%;
  object-fit: cover;
  background: var(--c-surface-2);
  display: block;
  margin: 0 auto;
}

.person__name {
  font-size: var(--fs-xs);
  margin-top: var(--sp-2);
  line-height: 1.25;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

.person__role {
  font-size: 13px;
  color: var(--c-text-faint);
  margin-top: 2px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

/* Size comes from --person-size; only type needs adjusting here. */
@media (min-width: 2560px) {
  .person__role { font-size: 22px; }
}
@media (max-width: 1400px) {
  .person__role { font-size: 11px; }
}

/* ---- marquee labels -------------------------------------------------- */

.marq {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.marq__t {
  display: inline-block;
  will-change: transform;
}

/* While scrolling, the ellipsis has to go — otherwise the revealed text
   still ends in "…" and the whole point is lost. */
.marq.is-marqueeing { text-overflow: clip; }

/* ---- focused-item title strip ---------------------------------------- */

/* For captions that are clamped to two lines and so cannot scroll, the
   full title of the focused item lives here — always complete, no motion. */
.focused-title {
  flex: 0 0 auto;
  /* A fixed height, not a minimum. The strip sits directly above the grid,
     so any change in its height moves every poster: with min-height, a
     title that wrapped to two lines pushed the grid down and the next
     one-line title pulled it back up — LEFT/RIGHT made the page bounce.
     Two clamped lines plus the padding, always. */
  height: calc(2.1em + var(--sp-3));
  font-size: var(--fs-md);
  line-height: 1.05;
  color: var(--c-text);
  padding-bottom: var(--sp-3);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

/* ---- seasons -------------------------------------------------------- */

.season-row { display: flex; gap: var(--sp-3); align-items: flex-start; }

/* The season tabs sit directly above the episode list, separated by a
   hairline. The gap has to be generous on BOTH sides of that line: the
   current tab is a filled pill, so a tight gap reads as the pill sitting
   on the line rather than above it.
   It also has to scale, because .nav-item doubles to 104px at 4K. --sp-5
   is itself redefined per breakpoint, so naming the token is enough — the
   earlier --sp-3 was simply too small a token, not a missing media query. */
.season-row--compact {
  padding-bottom: var(--sp-5);
  margin-bottom: var(--sp-5);
  border-bottom: 1px solid var(--c-line);
}

.season-tile {
  flex: 0 0 auto;
  width: 170px;
  min-height: 150px;
  padding: var(--sp-4);
  border-radius: var(--r-md);
  background: var(--c-surface);
  text-align: left;
  position: relative;
}
.season-tile__n {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1;
  color: var(--c-text-dim);
}
.season-tile__label {
  font-size: var(--fs-sm);
  margin-top: var(--sp-2);
  overflow-wrap: anywhere;
}
.season-tile__meta { font-size: var(--fs-xs); color: var(--c-text-faint); margin-top: 2px; }
.season-tile__bar {
  position: absolute;
  left: var(--sp-4);
  right: var(--sp-4);
  bottom: var(--sp-3);
  height: 4px;
  border-radius: 2px;
  background: var(--c-surface-3);
  overflow: hidden;
}
.season-tile__bar i { display: block; height: 100%; background: var(--c-primary); }

/* ---- episodes -------------------------------------------------------- */

.season-head { flex: 0 0 auto; margin-bottom: var(--sp-3); }
.season-head__title {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  overflow-wrap: anywhere;
}
.season-head__sub { font-size: var(--fs-sm); color: var(--c-text-dim); margin-top: 4px; }

.episode {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  position: relative;
}
.episode__n {
  flex: 0 0 62px;
  text-align: right;
  font-size: var(--fs-md);
  color: var(--c-text-faint);
  font-variant-numeric: tabular-nums;
}
.episode__still {
  flex: 0 0 auto;
  width: 160px;
  height: 90px;
  border-radius: var(--r-sm);
  object-fit: cover;
  background: var(--c-surface-2);
}
.episode__body { flex: 1 1 auto; min-width: 0; }
.episode__title { font-size: var(--fs-base); }
.episode__meta {
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  margin-top: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.episode__bar {
  position: absolute;
  left: var(--sp-4);
  right: var(--sp-4);
  bottom: 6px;
  height: 3px;
  border-radius: 2px;
  background: var(--c-surface-3);
  overflow: hidden;
}
.episode__bar i { display: block; height: 100%; background: var(--c-primary); }

.episode-aside { padding-left: var(--sp-5); }
.episode-plot {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--c-text-dim);
  overflow-wrap: anywhere;
}

@media (max-width: 1400px) {
  .season-tile { width: 130px; min-height: 120px; }
  .episode__still { width: 112px; height: 63px; }
  .episode__n { flex-basis: 44px; }
}

/* ---- search ---------------------------------------------------------- */

.search { display: flex; gap: var(--sp-7); height: 100%; min-height: 0; }
.search__left  { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.search__right { flex: 0 0 auto; }

.search__term {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  min-height: 1.3em;
  overflow-wrap: anywhere;
}
.search__term.is-empty { color: var(--c-text-faint); }

.search__coverage {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  margin: var(--sp-2) 0 var(--sp-4);
  min-height: 1.4em;
}

@media (max-width: 1400px) {
  .search { gap: var(--sp-5); }
}

.cast__note {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  margin-top: var(--sp-2);
  max-width: 900px;
}

/* ---- sort chip ------------------------------------------------------- */

.sort-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: 44px;
  padding: 0 var(--sp-4);
  border-radius: var(--r-pill);
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--c-line);
  color: var(--c-text-dim);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.sort-chip .icon { color: var(--c-text-faint); }
.sort-chip.is-focused { color: var(--c-text); background: var(--c-surface-3); }

.pane-head {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex: 0 0 auto;
}
.pane-head .pane-title { padding-bottom: 0; flex: 1 1 auto; min-width: 0; }

/* A sort menu has more choices than a confirmation, so let it stack. */
.dialog__actions { flex-wrap: wrap; }

@media (min-width: 2560px) { .sort-chip { height: 88px; } }
@media (max-width: 1400px) { .sort-chip { height: 44px; } }

.episode-aside__title {
  font-size: var(--fs-md);
  line-height: 1.2;
  overflow-wrap: anywhere;
  margin-bottom: var(--sp-2);
}
.episode-aside__meta {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  margin-bottom: var(--sp-4);
}

/* ---- pin pad --------------------------------------------------------- */

.pin__dots { display: flex; gap: var(--sp-4); margin: var(--sp-5) 0 var(--sp-2); }
.pin__dot {
  width: 22px; height: 22px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px var(--c-text-faint);
}
.pin__dot.is-filled { background: var(--c-text); box-shadow: none; }
.pin__dots.is-wrong { animation: pin-shake 360ms ease; }
@keyframes pin-shake {
  0%,100% { transform: translateX(0); }
  25% { transform: translateX(-10px); }
  75% { transform: translateX(10px); }
}

.pin__error { min-height: 1.6em; color: var(--c-bad); font-size: var(--fs-sm); }

.pin__pad {
  display: grid;
  grid-template-columns: repeat(3, 120px);
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.pin__key {
  height: 96px;
  border-radius: var(--r-md);
  background: var(--c-surface-2);
  font-size: var(--fs-lg);
}
.pin__key--action { font-size: var(--fs-sm); color: var(--c-text-dim); }

@media (min-width: 2560px) {
  .pin__pad { grid-template-columns: repeat(3, 200px); }
  .pin__key { height: 160px; }
  .pin__dot { width: 34px; height: 34px; }
}

/* ---- settings rows --------------------------------------------------- */

.setting-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 56px;
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--c-line);
  text-align: left;
  margin-top: var(--sp-2);
}
.setting-row .icon { flex: 0 0 auto; color: var(--c-text-faint); }
.setting-row__label { flex: 0 0 340px; font-size: var(--fs-sm); }
.setting-row .mono  { flex: 1 1 auto; min-width: 0; font-size: var(--fs-sm); }
.setting-row__hint  { flex: 0 0 auto; font-size: var(--fs-xs); color: var(--c-text-faint); }
.setting-row__note  {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  padding: var(--sp-2) var(--sp-4) 0;
  max-width: 1100px;
}

@media (max-width: 1400px) {
  .setting-row__label { flex-basis: 220px; }
  .setting-row__hint  { display: none; }
}

/* ---- wide poster cells (1–2 column layouts) -------------------------- */

/* Artwork beside the text rather than above it. Only the direction changes;
   the cell's markup is identical, so the recycling pool is unaffected. */
.vgrid--wide .poster {
  flex-direction: row;
  align-items: stretch;
  gap: var(--sp-4);
  padding: var(--sp-2);
}
/* The FRAME is the flex item, not the image. The artwork moved inside
   .poster__frame for the badges, and this width stayed on the <img>: the
   frame kept its default `flex: 1 1 auto` and shrank to whatever the text
   left over — 64px at two columns, a sliver of a poster beside a paragraph.
   Fixed and unshrinkable here, so the art is always a real 2:3 poster. */
.vgrid--wide .poster__frame {
  flex: 0 0 auto;
  /* 2:3 against the 210px cell, less the cell's own padding (194px). */
  width: 130px;
  height: 100%;
  border-radius: var(--r-sm);
}
.vgrid--wide .poster__art {
  width: 100%;
  height: 100%;
}
.vgrid--wide .poster__bar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
}
.vgrid--wide .poster__meta {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0;
}
.vgrid--wide .poster__name {
  font-size: var(--fs-base);
  -webkit-line-clamp: 2;
}
.vgrid--wide .poster { position: relative; }

@media (max-width: 1400px) {
  .vgrid--wide .poster__frame { width: 110px; }
}

/* ---- remote key hints ------------------------------------------------ */

.hints {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  flex: 0 0 auto;
  padding-top: var(--sp-3);
}

.hint {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  white-space: nowrap;
}

.hint__dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  flex: 0 0 auto;
}

.hint__cap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 22px;
  padding: 0 6px;
  border-radius: 6px;
  box-shadow: inset 0 0 0 1px var(--c-line);
  color: var(--c-text-dim);
  font-size: 12px;
  letter-spacing: 0.06em;
}
.hint__cap--icon { min-width: 26px; padding: 0 4px; }
.hint__cap .icon { opacity: 0.85; }

.hint--note {
  color: var(--c-text-faint);
  opacity: 0.75;
  font-style: italic;
}
.hint--note .icon { opacity: 0.7; }
.hint--note .hint__label { font-style: italic; }

.hint__label {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  letter-spacing: 0.03em;
}

@media (min-width: 2560px) {
  .hint__dot { width: 18px; height: 18px; }
  .hint__cap { min-width: 56px; height: 36px; font-size: 20px; border-radius: 9px; }
}

/* ---- wide poster cells ------------------------------------------------ */

.sort-chip__glyph { display: inline-flex; align-items: center; }

/* Only ever visible at one or two columns, where there is room for it. */
.poster__detail {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
  margin-top: var(--sp-2);
}

/* Not the accent colour: it is red, and red on a line that reads
   "1h 40m · MKV" looks like a warning about the file rather than a fact
   about the film. */
.poster__facts {
  font-size: var(--fs-xs);
  color: var(--c-text-dim);
  letter-spacing: 0.04em;
}

.poster__plot {
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--c-text-dim);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.poster__people {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The wide cell is tall enough for three lines of plot; the narrow one is
   never shown at all, so the clamp only needs to change with the breakpoint. */
@media (min-width: 1600px) {
  .vgrid--wide .poster__plot { -webkit-line-clamp: 3; }
}

/* In wide mode the sub-line under the name duplicates the facts line. */
.vgrid--wide .poster__sub .poster__year,
.vgrid--wide .poster__sub .poster__rating { display: none; }

.vgrid--wide .poster__meta { justify-content: flex-start; padding: var(--sp-3) 0; overflow: hidden; }
/* The meta is a fixed-height column, so something has to give when the plot
   runs long — and by default the title did, shrinking to half a line and
   cutting its own text through the middle. The title keeps its height; the
   plot below it is already clamped. */
.vgrid--wide .poster__name { -webkit-line-clamp: 1; flex: 0 0 auto; }

/* ---- action button internals ----------------------------------------- */

.btn__glyph { display: inline-flex; align-items: center; flex: 0 0 auto; }
.btn__glyph[hidden] { display: none; }
.btn__label { display: inline-block; }

/* ---- search result artwork -------------------------------------------
   A result row is type-agnostic, so the art box changes shape rather than
   forcing every result into the channel-logo aspect. `contain` for logos
   (which carry their own padding and must not be cropped) and `cover` for
   posters (which are already the right shape and should fill). */

.srch-art {
  flex: 0 0 auto;
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
}
.srch-art--wide   { width: 96px; height: 54px; object-fit: contain; }
/* Exactly 2:3 (56/84), the aspect posters are actually delivered in, so
   `cover` has nothing to crop. At 60x84 it was trimming about 7% off the
   top and bottom of every poster for no reason. */
.srch-art--poster { width: 56px; height: 84px; object-fit: cover; }

@media (min-width: 2560px) {
  .srch-art--wide   { width: 168px; height: 94px; }
  .srch-art--poster { width: 96px; height: 144px; }
}

/* ---- search result rows -----------------------------------------------
   Its own layout rather than .ch-row: a film has no channel number and no
   favourite marker, and reserving columns for both left a dead 90px gutter
   down the left of every result while the title truncated for want of the
   same space. */

.srch-row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: 0 var(--sp-4);
  border-radius: var(--r-md);
  background: transparent;
}

.srch-row__body {
  flex: 1 1 auto;
  min-width: 0;          /* lets the marquee measure its own overflow */
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.srch-row__name {
  font-size: var(--fs-md);
  color: var(--c-text);
  width: 100%;
}

.srch-row__meta {
  font-size: var(--fs-xs);
  color: var(--c-text-faint);
  letter-spacing: 0.03em;
}


/* ---- the player control bar --------------------------------------------
   Sits above the <video> surface, which is a sibling fixed element at
   --z-player. The layer is pointer-transparent; only the things that take
   input turn pointer events back on, because on a TV nothing is clicked and
   a stray hover must not swallow a key.

   The chrome uses `visibility` as well as `opacity` when it is down, and
   that is load-bearing rather than tidy: focus.js excludes a candidate on
   `visibility: hidden` but not on `opacity: 0`, so without it every control
   would still be focusable behind a bar nobody can see, and the arrows
   would move an invisible cursor. */

.pl {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-player) + 1);
  pointer-events: none;
  color: #fff;
}

.pl__chrome {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--safe-y) var(--safe-x);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--t-base) var(--ease), visibility 0s linear var(--t-base);
  /* Legible over any frame without dimming the middle of the picture. */
  background:
    linear-gradient(to bottom, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0) 22%),
    linear-gradient(to top,    rgba(0,0,0,0.88) 0%, rgba(0,0,0,0) 46%);
}
.pl__chrome.is-shown {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--t-base) var(--ease), visibility 0s;
}

.pl__top { display: flex; align-items: flex-start; gap: var(--sp-4); }
.pl__spacer { flex: 1 1 auto; }

.pl__badge {
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 var(--sp-4);
  border-radius: 999px;
  background: var(--c-accent);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.14em;
}
.pl__badge[hidden] { display: none; }

/* A quiet reminder that the picture or the speed is not on its default. */
.pl__pill {
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 var(--sp-4);
  border-radius: 999px;
  background: rgba(0,0,0,0.55);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.28);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
}
.pl__pill[hidden] { display: none; }

.pl__bottom { display: flex; flex-direction: column; gap: var(--sp-2); }

.pl__title {
  font-size: var(--fs-xl);
  font-weight: var(--fw-bold);
  line-height: 1.1;
  text-shadow: 0 2px 12px rgba(0,0,0,0.6);
}

.pl__sub {
  font-size: var(--fs-sm);
  color: rgba(255,255,255,0.78);
}

/* ---- the scrub bar ------------------------------------------------------
   Focusable in its own right. While it holds focus it claims Left/Right,
   which is why it needs to look unmistakably different from a button that
   merely happens to be wide. */

.pl__scrub {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin-top: var(--sp-3);
  pointer-events: auto;
}
.pl__scrub[hidden] { display: none; }

.pl__time { font-size: var(--fs-sm); color: rgba(255,255,255,0.85); min-width: 6ch; }
.pl__time:last-child { text-align: right; }

.pl__bar {
  position: relative;
  flex: 1 1 auto;
  height: 6px;
  border-radius: 999px;
  background: rgba(255,255,255,0.22);
  cursor: default;
}

/* How far the network has got. Behind the fill, never in front of it. */
.pl__buffered {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0;
  border-radius: 999px;
  background: rgba(255,255,255,0.30);
}

.pl__fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0;
  border-radius: 999px;
  background: var(--c-accent);
  transition: width var(--t-fast) linear;
}

.pl__head {
  position: absolute;
  top: 50%;
  left: 0;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0,0,0,0.65);
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease);
}

/* The head only appears once the bar is the thing being used — a dot that
   sat there permanently would read as a control when it is a readout. */
.pl__bar.is-focused .pl__head,
.pl__bar.is-scrubbing .pl__head { opacity: 1; }

.pl__bar.is-focused,
.pl__bar.is-scrubbing {
  height: 10px;
  background: rgba(255,255,255,0.30);
}
.pl__bar.is-scrubbing .pl__fill {
  background: #fff;
  transition: none;             /* the target must track the key, not lag it */
}

/* The target time, and how far it is from where playback actually is. */
.pl__bubble {
  position: absolute;
  bottom: calc(100% + 14px);
  transform: translateX(-50%);
  white-space: nowrap;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-sm);
  background: rgba(0,0,0,0.88);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.22);
  font-size: var(--fs-sm);
}
.pl__bubble[hidden] { display: none; }

/* ---- the control row ----------------------------------------------------- */

.pl__controls {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: var(--sp-4);
  pointer-events: auto;
}

.pl__divider {
  width: 1px;
  height: 26px;
  margin: 0 var(--sp-3);
  background: rgba(255,255,255,0.22);
}

/* Transport buttons are glyph-only and round: they are the controls a
   viewer reaches for without reading, so they get shape instead of a word.
   Kept small — the picture is what is being watched, and a row of big
   buttons over it reads as a toolbar rather than as a light touch. */
.btn--transport {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  /* .btn carries min-height: 54px, and a min-height beats a height — which
     is what stretched these into ovals when they were first drawn. */
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255,255,255,0.13);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.16);
  color: #fff;
  cursor: default;
}

/* The seconds a skip button jumps, drawn inside the arc. */
.btn__sup {
  position: absolute;
  top: 54%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 9px;
  font-weight: var(--fw-bold);
  letter-spacing: 0;
  pointer-events: none;
}

/* The labelled options beside them, quieter than an ordinary action button
   because the picture behind them is the thing being looked at. */
.btn--player {
  flex: 0 0 auto;
  height: 44px;
  min-height: 0;                /* see the note on .btn--transport */
  padding: 0 var(--sp-4);
  gap: var(--sp-2);
  border: 0;
  border-radius: 999px;
  background: rgba(255,255,255,0.13);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.16);
  color: #fff;
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
}
.btn--player .btn__label { letter-spacing: 0.02em; }
.btn--player .icon { opacity: 0.9; }
.btn--quiet { background: rgba(255,255,255,0.07); }

/* Focus over a moving picture cannot rely on a background change alone, so
   it gets a solid plate, a ring and a lift.

   The glyph rule below is not decoration. `.btn.is-focused .icon` up in the
   shared focus block pins every focused button's icon to --c-text, which is
   near-white — at specificity (0,3,0) it beat the (0,2,0) colour set on the
   button itself and painted a white glyph onto a white plate. The focused
   play button was a blank white disc. Matching that specificity is what
   makes the icon follow its button. */
.btn--transport.is-focused,
.btn--player.is-focused {
  background: #fff;
  color: #0a0d12;
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-accent),
              0 4px 16px rgba(0,0,0,0.5);
}
.btn--transport.is-focused .icon,
.btn--player.is-focused .icon { color: #0a0d12; opacity: 1; }

/* Doubled class deliberately.
   responsive-tv.css re-sets `.btn { min-height }` per breakpoint and loads
   after this file, so a plain `.btn--transport { min-height: 0 }` loses to
   it at every tier and the round buttons come out as ovals. (0,2,0) beats
   `.btn` wherever it is written. */
.btn.btn--transport,
.btn.btn--player { min-height: 0; }

/* ---- the menus ----------------------------------------------------------
   One panel at a time, above the control row it covers. Its rows are
   ordinary focusables, so the same spatial navigation drives them; the row
   underneath is marked data-focus-disabled by controls.js while a panel is
   open so focus cannot slip behind it. */

.pl__menu { pointer-events: auto; }
.pl__menu[hidden] { display: none; }

.pl-menu {
  max-width: 720px;
  margin-bottom: var(--sp-5);
  padding: var(--sp-5);
  border-radius: var(--r-md);
  background: rgba(10,12,16,0.94);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.16), var(--shadow-2);
}

.pl-menu__title {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.62);
  margin-bottom: var(--sp-4);
}

.pl-menu__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 46vh;
  overflow-y: auto;
}

.pl-menu__row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: rgba(255,255,255,0.86);
  font-size: var(--fs-base);
  text-align: left;
  cursor: default;
}

.pl-menu__tick {
  flex: 0 0 auto;
  width: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--c-accent);
}

.pl-menu__label { flex: 1 1 auto; }

/* The current value of a row that cycles, and the aside on a row that
   explains itself. Both sit right, both stay quiet. */
.pl-menu__value {
  flex: 0 0 auto;
  color: #fff;
  font-weight: var(--fw-bold);
}
.pl-menu__note-inline {
  flex: 0 0 auto;
  font-size: var(--fs-sm);
  color: rgba(255,255,255,0.52);
}

.pl-menu__row.is-current { color: #fff; }

.pl-menu__row.is-focused {
  background: rgba(255,255,255,0.14);
  color: #fff;
  box-shadow: inset 0 0 0 var(--focus-width) var(--c-accent);
}
.pl-menu__row.is-focused .pl-menu__note-inline { color: rgba(255,255,255,0.75); }

/* The explanation under a menu that has something to explain — why a
   stream carries no subtitles, why there is nothing to choose between. */
.pl-menu__note {
  margin: var(--sp-4) 0 0;
  max-width: 62ch;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: rgba(255,255,255,0.58);
}

/* ---- spinner and failure -------------------------------------------------- */

/* The spinner is centred over the picture and stays put while buffering. */
.pl > .spinner {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  flex-direction: column;
  gap: var(--sp-4);
  text-shadow: 0 2px 10px rgba(0,0,0,0.7);
}

/* The one thing that does take input unconditionally. */
.pl__error {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--safe-y) var(--safe-x);
  background: rgba(8,10,14,0.92);
  pointer-events: auto;
}
.pl__error[hidden] { display: none; }

/* ---- channel switching (Phase 9) ----------------------------------------
   Three affordances, all of which sit OUTSIDE .pl__chrome. The chrome is
   `visibility: hidden` when the bar is down, and every one of these is
   meant to work while it is down — that is the whole point of them. */

.pl__layer { position: absolute; inset: 0; }

/* The banner a channel change puts up. Deliberately small and top-left:
   it answers "what am I on now?" without taking the picture. */
.pl-banner {
  position: absolute;
  left: var(--safe-x);
  top: var(--safe-y);
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  max-width: 54%;
  padding: var(--sp-3) var(--sp-5) var(--sp-3) var(--sp-3);
  border-radius: var(--r-md);
  background: rgba(8,10,14,0.82);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14), var(--shadow-2);
  animation: banner-in var(--t-base) var(--ease);
}
.pl-banner[hidden] { display: none; }

@keyframes banner-in {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

.pl-banner__num {
  flex: 0 0 auto;
  min-width: 68px;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--c-accent);
  color: #fff;
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  text-align: center;
}

.pl-banner__body { display: flex; flex-direction: column; min-width: 0; }
.pl-banner__name {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pl-banner__sub {
  font-size: var(--fs-sm);
  color: rgba(255,255,255,0.7);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pl-banner__sub[hidden] { display: none; }

/* The number being typed. Centre-screen and large, because it is a thing
   in progress and the viewer needs to see it without hunting. */
.pl-entry {
  position: absolute;
  left: 50%;
  top: 22%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  min-width: 190px;
  padding: var(--sp-5) var(--sp-6);
  border-radius: var(--r-lg);
  background: rgba(8,10,14,0.88);
  box-shadow: inset 0 0 0 2px var(--c-accent), var(--shadow-2);
  color: #fff;
}
.pl-entry[hidden] { display: none; }

.pl-entry__value {
  font-size: calc(var(--fs-2xl) * 1.4);
  font-weight: var(--fw-bold);
  letter-spacing: 0.12em;
  line-height: 1;
}
.pl-entry__note {
  font-size: var(--fs-sm);
  color: rgba(255,255,255,0.6);
  min-height: 1.2em;
}

/* The channel list, over the picture rather than instead of it. */
.pl-channels {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 560px;
  max-width: 44vw;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--safe-y) var(--sp-5);
  background: linear-gradient(to right, rgba(8,10,14,0) 0%, rgba(8,10,14,0.94) 11%);
  pointer-events: auto;
  animation: channels-in var(--t-base) var(--ease);
}
.pl-channels[hidden] { display: none; }

@keyframes channels-in {
  from { opacity: 0; transform: translateX(24px); }
  to   { opacity: 1; transform: none; }
}

/* The list itself takes the remaining height; the title and hints do not. */
.pl-channels > .vlist { flex: 1 1 auto; min-height: 0; }

/* Only the title needs to clear the gradient's feather — the rows already
   do, because .ch-row carries padding of its own. Indenting the whole panel
   instead pushed every channel name into the right edge and clipped it. */
.pl-channels > .pane-title { padding-left: 64px; }

/* The channel you are watching stays marked while you browse past it. */
.pl-channels .ch-row.is-current .ch-row__num { color: var(--c-accent); }
.pl-channels .ch-row.is-current { background: rgba(255,255,255,0.06); }
