/* ══════════════════════════════════════════════════════════════════════
   QVATUBE — Layout
   ══════════════════════════════════════════════════════════════════════ */

/* ── Header ───────────────────────────────────────────────────────────── */
header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  background: linear-gradient(180deg, rgba(10,10,11,.98) 0%, rgba(10,10,11,.94) 75%, rgba(10,10,11,.85) 100%);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  padding: calc(env(safe-area-inset-top, 0px) + var(--safe-top)) 16px 0;
  transition: border-color .25s ease;
}
header.scrolled { border-bottom-color: var(--border-soft); }

/* ── Banner "sin conexión" (ver web/static/js/offline.js) ────────────────
   Vive DENTRO de <header>, como hermano ANTES de .header-top, para que
   simplemente empuje el resto del header hacia abajo en el flujo normal
   (header no es flex, sus hijos se apilan verticalmente) — así no hace
   falta calcular su alto a mano ni tocar el position:fixed del header. Se
   muestra/oculta con el atributo [hidden] nativo — el selector [hidden] de
   acá es NECESARIO con especificidad más alta que `.offline-banner` de
   abajo, si no el `display:flex` de esa regla gana igual y la caja queda
   visible (vacía) aun con el atributo puesto, empujando todo aunque haya
   red. El resto del header (.content, .az-index, ver más abajo) lee el
   alto real del header vía --header-h, que sincroniza el ResizeObserver
   de app.js — así este banner puede aparecer/desaparecer y el contenido
   se reacomoda solo, con una transición corta en vez de saltar de golpe. */
.offline-banner[hidden] { display: none; }
.offline-banner {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 8px 14px; font-size: 12.5px; font-weight: 600; letter-spacing: .1px;
  color: color-mix(in srgb, var(--accent) 45%, var(--text-2));
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent) 11%, var(--surface-2)) 0%,
    color-mix(in srgb, var(--accent) 5%, var(--surface-2)) 100%
  );
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border-soft));
}
/* Ícono en tono de marca con un "respirar" de opacidad — mismo lenguaje
   visual que .live-dot (punto pulsante de "En Vivo" en .cats), para que
   se lea como parte del mismo sistema de estados en vivo/conexión en vez
   de una alerta genérica del navegador. */
.offline-banner svg {
  flex-shrink: 0; color: var(--accent);
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--accent) 40%, transparent));
  animation: offlineBreathe 2.6s ease-in-out infinite;
}
@keyframes offlineBreathe {
  0%, 100% { opacity: .75; }
  50%      { opacity: 1; }
}
.offline-banner strong { color: var(--text); font-weight: 700; }
@media (prefers-reduced-motion: reduce) {
  .offline-banner svg { animation: none; }
}

/* ── Banner "usuarios en línea" (services/stats.py: online_now) ──────────
   Mismo mecanismo [hidden] que .offline-banner de arriba, pero como
   hermano DESPUÉS de .header-top (debajo del buscador), no antes — es
   ambiente, no una alerta, así que no tiene que ser lo primero que se
   vea. --header-h se reajusta solo igual, no importa en qué hijo de
   <header> esté. Bien discreto: una línea angosta, letra chica, nada que
   compita con el buscador. */
.online-banner[hidden] { display: none; }
/* flex-wrap SIEMPRE nowrap — una sola línea a cualquier costo. En vez de
   wrap, el gap y el font-size se encogen de forma fluida con clamp() a
   medida que el viewport es más angosto (mismo look, menos espacio), y
   como último recurso (pantallas muy angostas / textos muy largos) el
   propio banner scrollea horizontal en vez de partirse en 2 renglones;
   scrollbar oculta en todos los motores para que no se note. */
.online-banner {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(6px, 2.4vw, 12px); flex-wrap: nowrap;
  padding: 0 10px 8px; margin-top: -3px;
  font-size: clamp(8.5px, 2.6vw, 10px); font-weight: 600; letter-spacing: .1px;
  color: var(--text-3);
  overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;
}
.online-banner::-webkit-scrollbar { display: none; width: 0; height: 0; }
.online-banner strong { color: var(--text-2); font-weight: 700; }
.online-banner .online-stat {
  display: flex; align-items: center; gap: 5px;
  flex-shrink: 0; white-space: nowrap;
}
.online-banner .online-stat svg { color: var(--accent); flex-shrink: 0; }
/* Punto pulsante — mismo lenguaje visual que .live-dot en la barra de
   categorías, así se lee como parte del mismo sistema de "esto es en
   vivo" en toda la web. */
.online-banner .online-dot {
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex-shrink: 0;
  box-shadow: 0 0 4px color-mix(in srgb, var(--accent) 70%, transparent);
  animation: offlineBreathe 2.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .online-banner .online-dot { animation: none; }
}

.header-top { display: flex; align-items: center; gap: 12px; padding: 14px 0 8px; }
.logo { display: flex; align-items: center; justify-content: flex-start; gap: 2px; flex-shrink: 0; margin-right: auto; margin-left: -8px; }
.logo-q { font-family: var(--f-display); font-size: 24px; color: var(--accent); }
.logo-text { font-family: var(--f-display); font-size: 22px; letter-spacing: 1px; color: var(--text); }

.search-wrap { position: relative; width: 350px; margin-left: auto; }
.search-wrap input {
  width: 100%; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 10px 14px 10px 36px; color: var(--text);
  font-family: var(--f-body); font-size: 14px; outline: none;
  transition: border-color .2s ease, background .2s ease;
}
.search-wrap input:focus { border-color: var(--accent); background: var(--surface-2); }
.search-wrap input::placeholder { color: var(--text-3); }
.search-icon {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  font-size: 14px; color: var(--text-3); pointer-events: none;
}

/* ── Navegación de categorías (barra inferior) ────────────────────────────
   Antes era una fila de píldoras con scroll horizontal e ícono+texto
   siempre visibles. Ahora es solo-ícono, ancho fijo (sin scroll — la
   cantidad de pestañas es chica y estable: Popular + categorías de
   channels_config + Inicio, ver buildTabs() en app.js), con el mismo
   espíritu de referencia que pidió Andrés: la píldora de fondo SE DESLIZA
   entre íconos (.cats-indicator) y el NOMBRE de la pestaña activa aparece
   debajo, centrado, con un fundido (.cats-label) — en vez de texto fijo
   junto a cada ícono todo el tiempo. Sin destellos extra al pasar por
   los íconos (se quitó: animación innecesaria que restaba fluidez).
   Inicio ya no es la primera pestaña: buildTabs() la inserta a mitad de
   camino del listado (pedido explícito de diseño). */
.cats { position: relative; display: flex; align-items: center; justify-content: space-between; padding: 2px 2px 0; }

/* Píldora que se desliza detrás del ícono activo. Ancho fijo (no ocupa
   toda la columna del botón, que es flex:1 solo para repartir el área de
   toque) — se centra bajo el ícono activo vía left, calculado en
   updateCatsIndicator() a partir de offsetLeft/offsetWidth del botón. */
.cats-indicator {
  position: absolute; top: 50%; left: 0; width: 50px; height: 38px; z-index: 0;
  /* Se mueve con transform (compositor, sin layout) — la posición X la fija
     updateCatsIndicator() en --x; antes animaba `left`, que recalcula el
     layout en cada frame y se entrecortaba en móviles modestos. */
  transform: translate3d(var(--x, 0px), -50%, 0);
  will-change: transform;
  background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
  border-radius: 16px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 42%, var(--border-soft));
  transition: transform .42s cubic-bezier(.22,1,.36,1), background .3s ease, box-shadow .3s ease;
}

/* Glow que "nace" de la propia píldora activa: capa aparte detrás de
   .cats-indicator (mismo left/transición, ya que es un pseudo-elemento
   suyo), más grande y difusa, con una respiración sutil de opacidad —
   mismo lenguaje que .offline-banner svg (ver offlineBreathe más arriba)
   para que se sienta parte del mismo sistema visual, no un efecto suelto.
   z-index -1 la deja detrás de la propia píldora sin taparle el borde. */
.cats-indicator::before {
  content: '';
  position: absolute; inset: -10px; border-radius: 22px; z-index: -1;
  background: radial-gradient(circle, color-mix(in srgb, var(--accent) 55%, transparent) 0%, transparent 72%);
  filter: blur(6px);
  animation: catsGlowBreathe 2.6s ease-in-out infinite;
}
@keyframes catsGlowBreathe {
  0%, 100% { opacity: .5; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .cats-indicator::before { animation: none; opacity: .8; }
}

/* Nombre de la pestaña activa, debajo de la fila de íconos — posición
   horizontal centrada bajo el botón activo (con clamp para no salirse de
   los bordes en la primera/última pestaña), fundido al cambiar en vez de
   saltar de golpe. Ver updateCatsIndicator() en app.js. */
.cats-label {
  position: absolute; top: 100%; left: 0; margin-top: 3px;
  transform: translate3d(calc(var(--x, 0px) - 50%), 0, 0); white-space: nowrap; z-index: 1;
  font-family: var(--f-body); font-size: 12px; font-weight: 700;
  color: var(--text); opacity: 0; pointer-events: none;
  transition: opacity .18s ease, transform .42s cubic-bezier(.22,1,.36,1);
}
.cats-label.visible { opacity: 1; }

.cat-btn {
  position: relative; z-index: 1; flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: none; padding: 9px 2px;
  color: var(--text-2); cursor: pointer; transition: color .2s ease;
}
.cat-btn:active .cat-icon { transform: scale(.85); }
.cat-btn.active { color: var(--text); }
.cat-btn.active .cat-icon { filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 65%, transparent)); }

.cat-icon { position: relative; display: flex; align-items: center; justify-content: center; transition: transform .15s ease; }

/* Numerito de "contenido nuevo" (esquina superior derecha del ícono de
   cada pestaña de la barra inferior) — ver initNewContentBadges() en
   app.js. Se apaga solo, tarjeta por tarjeta, a medida que el usuario
   las va viendo (no hace falta entrar a la pestaña ni tocar nada). */
.cat-badge {
  position: absolute; top: -6px; right: -9px; min-width: 15px; height: 15px;
  padding: 0 4px; border-radius: 999px; background: var(--accent);
  color: #fff; font-family: var(--f-body); font-size: 9.5px; font-weight: 800;
  line-height: 15px; text-align: center; box-shadow: 0 0 0 2px var(--bg);
  pointer-events: none;
}

/* Señal distintiva de "En Vivo": punto pulsante con ondas concéntricas.
   Es el único lugar del diseño donde el movimiento es protagonista. */
.live-dot { position: relative; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.live-dot::before, .live-dot::after {
  content: ''; position: absolute; inset: -4px; border-radius: 50%;
  border: 1px solid var(--accent); opacity: 0; animation: liveSignal 2.4s ease-out infinite;
}
.live-dot::after { animation-delay: 1.2s; }
@keyframes liveSignal {
  0%   { transform: scale(.4); opacity: .8; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* ── Barra inferior (contenedor fijo de .cats) ────────────────────────────
   Mismo lenguaje visual que <header> (blur + degradé + hairline), pero
   pegada abajo y con el degradé invertido. z-index 100, igual que header:
   por debajo de CUALQUIER panel/hoja/modal (arrancan en 200, ver
   auth.css/layout.css más abajo), así que al abrirse cualquiera de esos
   la tapa solo automáticamente sin lógica extra. Por encima va el
   mini-player (z-index 150, ver más abajo — a propósito por DEBAJO de
   todo panel/hoja/modal, ver esa nota), que ahora flota justo arriba
   de esta barra en vez de pegarse al borde física de la pantalla — ver
   "bottom: var(--bottom-nav-h...)" en .mini-player. Un ResizeObserver en
   app.js ("Alto real de la barra inferior") mide su alto real y lo
   publica en --bottom-nav-h, igual que --header-h arriba, así .content,
   .az-index y el propio mini-player se acomodan solos si el alto cambia
   (ej. una pantalla más angosta hace que las píldoras salten de línea). */
.bottom-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 100;
  background: linear-gradient(0deg, rgba(10,10,11,.98) 0%, rgba(10,10,11,.94) 75%, rgba(10,10,11,.85) 100%);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-top: 1px solid var(--border-soft);
  /* padding-bottom extra (vs. antes) para dejarle sitio real a .cats-label,
     que ahora vive debajo de la fila de íconos en vez de ir en línea. */
  padding: 8px 12px calc(22px + env(safe-area-inset-bottom, 0px));
}

/* ── Contenido ────────────────────────────────────────────────────────── */
/* Panel propio con su propio scroll (en vez de fluir bajo el header fijo
   y dejar que el documento entero scrollee) — ver la nota en tokens.css
   sobre por qué: es lo que permite ocultar el scrollbar nativo en el
   índice A-Z, cosa que Chrome/Android no deja hacer sobre el scroll de
   la ventana. */
.content {
  position: fixed;
  /* --header-h la mantiene al día app.js (ResizeObserver sobre <header>,
     ver "Alto real del header" en app.js) para que el banner offline
     (ver .offline-banner) pueda aparecer/desaparecer sin solapar ni saltar
     el contenido. El calc de acá es solo el valor de arranque, antes de
     que el observer corra por primera vez. */
  top: var(--header-h, calc(env(safe-area-inset-top, 0px) + var(--safe-top) + 64px));
  left: 0; right: 0; bottom: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  /* El padding inferior deja el hueco real de .bottom-nav (--bottom-nav-h,
     ver arriba) + un respiro, para que la última fila del catálogo no
     quede tapada por la barra fija de categorías. */
  padding: 0 0 calc(var(--bottom-nav-h, 60px) + 20px);
  transition: top .18s ease, padding-bottom .18s ease;
}

.section { margin-top: 30px; }
.section-title {
  display: flex; align-items: baseline; gap: 10px; padding: 0 16px; margin-bottom: 14px;
  font-family: var(--f-display); font-size: 19px; letter-spacing: .5px; color: var(--text);
}
/* Divisor "de señal": una línea fina que corta el título, evocando la
   idea de transmisión/broadcast sin volverse decorativa. */
.section-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.section-count { font-family: var(--f-body); font-size: 11.5px; font-weight: 500; color: var(--text-3); }

.row, .grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; padding: 0 16px;
}
@media (min-width: 620px) { .row, .grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } }

/* ── Separador de letra dentro del catálogo (modo A-Z) ───────────────────
   Mismo lenguaje visual que .section-title (texto + línea que "corta" el
   título, ver arriba) pero más chico y repetible: aparece una vez por
   cada letra del listado (insertado por catalog.js en renderGrid/
   appendToGrid/prependToGrid), nunca desde CSS. grid-column:1/-1 lo hace
   ocupar las 3 columnas de la grilla en vez de caer como una tarjeta más. */
.az-group-divider {
  grid-column: 1 / -1;
  display: flex; align-items: baseline; gap: 10px;
  font-family: var(--f-display); font-size: 15px; letter-spacing: .5px; color: var(--text);
  margin-top: 8px;
}
.az-group-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.az-group-divider:first-child { margin-top: 0; }
@media (min-width: 1000px) { .content { max-width: 1400px; margin: 0 auto; } }

/* ── Cabecera de catálogo: tarjeta única ──────────────────────────────────
   Contenedor compartido para la cabecera de CUALQUIER categoría, con el
   mismo lenguaje visual en todas: fondo en degradé + borde, agrupando ahí
   dentro los controles de esa cabecera. En Música (buildMusicHeaderCard,
   ver app.js) agrupa el interruptor Recientes/A-Z, "Reproducir todo el
   catálogo" y el modo Aleatorio/Lineal. En Películas/Series/Anime
   (buildCatalogHeaderCard) agrupa el botón "Filtrar por Género" y el
   interruptor Recientes/A-Z — antes eran dos/tres cajas sueltas sin
   relación visual entre sí; ahora todas las categorías comparten el mismo
   contenedor, para que el sitio se vea consistente entre pestañas. Los
   elementos anidados (.sort-bar-compact, .filter-btn-full) pierden su
   propio fondo para que se vean integrados, no como píldoras encima de
   la tarjeta. */
.catalog-header-card {
  display: flex; flex-direction: column; gap: 10px;
  margin: 0 16px 14px; padding: 10px 12px; border-radius: var(--r);
  background: linear-gradient(135deg, var(--surface-2), var(--surface));
  border: 1px solid var(--border);
}
.catalog-header-card .sort-bar-compact {
  align-self: stretch; background: rgba(0,0,0,.18); border-color: var(--border);
}
.music-playall-row {
  display: flex; align-items: center; gap: 8px 10px; flex-wrap: wrap;
  padding-top: 10px; border-top: 1px solid var(--border);
}
.music-playall-main {
  flex: 1 1 160px; min-width: 0; display: flex; align-items: center; gap: 10px;
  text-align: left; cursor: pointer;
}
.music-playall-main:active { transform: scale(.98); }
.music-playall-icon {
  flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%;
  background: var(--accent); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(255,61,26,.3); position: relative;
}
.music-playall-icon svg { position: relative; z-index: 1; width: 14px; height: 14px; }
/* Estado "cargando" mientras se trae /api/category-tracks: mismo lenguaje
   visual que .mini-player-btn.play.loading (anillo girando en vez de un
   emoji/spinner de color fijo que no combina con el resto del sitio). */
.music-playall-main.loading .music-playall-icon svg { opacity: 0; }
.music-playall-main.loading .music-playall-icon::after {
  content: ''; position: absolute; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid rgba(0,0,0,.28); border-top-color: rgba(0,0,0,.8);
  animation: spin .7s linear infinite;
}
.music-playall-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.music-playall-title {
  font-size: 12.5px; font-weight: 700; color: var(--text); line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.music-playall-sub { font-size: 10px; color: var(--text-3); line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.music-mode-switch {
  flex: 0 1 auto; display: flex; align-items: center; gap: 2px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 2px;
}
.music-mode-opt {
  display: flex; align-items: center; gap: 5px; padding: 6px 11px; border-radius: 999px;
  font-family: var(--f-body); font-size: 11px; font-weight: 600; color: var(--text-2);
  white-space: nowrap; cursor: pointer; transition: background .15s ease, color .15s ease;
}
.music-mode-opt svg { flex-shrink: 0; width: 12px; height: 12px; }
.music-mode-opt.active { background: var(--accent); color: var(--accent-ink); }
.music-mode-opt:active { transform: scale(.95); }
/* El botón "Reproducir todo el catálogo" (flex:1 1 160px) empuja el
   interruptor a su propia línea en la mayoría de los anchos de pantalla
   reales — antes solo se estiraba a ancho completo por debajo de 380px,
   así que en teléfonos más anchos (393-412px, lo más común) terminaba en
   su propia línea pero angosto, con un hueco vacío al lado. Ahora ocupa
   ancho completo siempre que esté en su propia línea, sin depender de un
   breakpoint puntual. */
.music-mode-switch { flex-basis: 100%; }
.music-mode-opt { flex: 1; justify-content: center; }

/* ── Banner rotativo de Música (cabecera + promo patrocinada) ─────────────
   Ver buildMusicBannerSlot/buildMusicPromoCard en app.js. .music-promo-card
   reutiliza el mismo contenedor .catalog-header-card (mismo margen/radio/
   degradé que la cabecera normal) para que el swap entre ambas no salte de
   tamaño ni de posición — solo cambia lo que hay adentro. */
.music-banner-slot { position: relative; }
.music-promo-card { padding: 10px; cursor: pointer; }
.music-promo-card:active { transform: scale(.98); }
.music-promo-row { display: flex; align-items: center; gap: 12px; }
.music-promo-cover {
  position: relative; flex-shrink: 0; width: 52px; height: 52px;
  border-radius: 10px; overflow: hidden; background: var(--surface);
}
.music-promo-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.music-promo-cover-ph { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--text-3); }
.music-promo-info { flex: 1; min-width: 0; }
.music-promo-sponsor {
  display: inline-block; font-family: var(--f-body); font-size: 9px; font-weight: 700;
  letter-spacing: .4px; text-transform: uppercase; color: var(--accent);
  background: var(--accent-soft); padding: 2px 7px; border-radius: 999px; margin-bottom: 3px;
}
.music-promo-title {
  font-size: 13px; font-weight: 700; color: var(--text); line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.music-promo-artist { font-size: 11px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.music-promo-cta {
  flex-shrink: 0; display: flex; align-items: center; gap: 6px; padding: 8px 13px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink); font-family: var(--f-body); font-size: 11px; font-weight: 700;
  white-space: nowrap;
}
.music-promo-cta svg { flex-shrink: 0; width: 12px; height: 12px; }

/* ── Filtro por género (Películas/Series/Anime) ───────────────────────────
   Vive DENTRO de .catalog-header-card (ver arriba), arriba del
   interruptor Recientes/A-Z (ver buildFilterBar/buildCatalogHeaderCard en
   app.js) — a todo el ancho de la tarjeta, mismo criterio que
   .sort-bar-compact al lado: ocupar el espacio completo en vez de dejar
   un hueco vacío al lado de un botón chico. Al tocarlo abre un modal con
   la lista de géneros disponibles (ver .genre-chip-list más abajo). */
.filter-btn-full {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 100%; background: rgba(0,0,0,.18); border: 1px solid var(--border); border-radius: 999px;
  padding: 9px 40px; font-family: var(--f-body); font-size: 12.5px; font-weight: 600;
  color: var(--text-2); cursor: pointer; transition: all .15s ease;
}
.filter-btn-full:active { transform: scale(.98); }
.filter-btn-full.active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.filter-btn-full-main { display: flex; align-items: center; gap: 7px; min-width: 0; max-width: 100%; }
.filter-btn-full-main span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-btn-clear {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 19px; height: 19px; border-radius: 50%; background: rgba(0,0,0,.22); color: inherit;
}
.filter-btn-clear:active { transform: translateY(-50%) scale(.88); }

/* ── Modal de selección de género ──────────────────────────────────────
   Reusa .modal-overlay / .modal-box (ver player.css), igual que el modal
   de reportar contenido — mismo mecanismo, chips en vez de lista. */
.genre-chip-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; justify-content: center; }
.genre-chip {
  padding: 9px 16px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  font-family: var(--f-body); font-size: 13px; font-weight: 600; cursor: pointer;
}
.genre-chip:active { transform: scale(.95); }
.genre-chip.selected { border-color: var(--accent); background: var(--accent); color: var(--accent-ink); }

/* ── Orden del catálogo: Recientes / A-Z ─────────────────────────────────
   Mismo lenguaje visual que .music-mode-switch (píldora con dos opciones),
   pero genérico porque vive arriba de CUALQUIER categoría, no solo Música. */
.sort-bar {
  display: flex; align-items: center; gap: 2px;
  margin: 0 16px 14px; padding: 2px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
}
.sort-opt {
  display: flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 12px; border-radius: 999px;
  font-family: var(--f-body); font-size: 11.5px; font-weight: 600; color: var(--text-2);
  white-space: nowrap; cursor: pointer; transition: background .15s ease, color .15s ease;
}
.sort-opt.active { background: var(--accent); color: var(--accent-ink); }
.sort-opt:active { transform: scale(.95); }

/* Antes de que existiera .catalog-header-card (ver arriba), esta píldora
   vivía suelta arriba a la izquierda con toda la línea vacía al lado —
   se veía como un elemento perdido en una esquina. Se deja el estilo
   "suelto" (sort-bar:not(.sort-bar-compact)) por compatibilidad, aunque
   hoy toda cabecera de catálogo pasa por .sort-bar-compact dentro de
   .catalog-header-card. Por default ocupa toda la línea (cada opción
   reparte el ancho a la mitad) y en escritorio queda centrada dentro de
   la columna de contenido en vez de pegada al borde. .sort-bar-compact
   (usada dentro de .catalog-header-card) conserva el tamaño ajustado y
   pierde el margen propio, porque ahí el que separa del borde es el
   padding de la tarjeta que la contiene. */
.sort-bar:not(.sort-bar-compact) { width: auto; }
.sort-bar:not(.sort-bar-compact) .sort-opt { flex: 1; }
@media (min-width: 1000px) {
  .sort-bar:not(.sort-bar-compact) { width: fit-content; margin: 0 auto 14px; }
  .sort-bar:not(.sort-bar-compact) .sort-opt { flex: 0 1 auto; }
}

.sort-bar-compact { width: 100%; margin: 0; }
.sort-bar-compact .sort-opt { flex: 1; }

/* ── Índice A-Z (fast-scroll lateral, estilo cajón de apps) ──────────────
   Tira vertical fija pegada al borde derecho: se recorre con el dedo/mouse
   (pointerdown+move sobre .az-index en app.js) para saltar directo a
   cualquier letra sin scrollear página por página. z-index 90: por debajo
   de cualquier overlay/panel (arrancan en 200, ver auth.css/layout.css) y
   se oculta del todo mientras uno está abierto (ver body.panel-open). */
.az-index {
  position: fixed;
  top: calc(var(--header-h, calc(env(safe-area-inset-top, 0px) + var(--safe-top) + 64px)) + 8px);
  bottom: calc(var(--bottom-nav-h, 60px) + 76px); right: env(safe-area-inset-right, 0px); z-index: 90;
  display: flex; flex-direction: column; align-items: center; justify-content: space-between;
  padding: 6px 0; touch-action: none; user-select: none;
  transition: top .18s ease, bottom .18s ease;
}
.az-index-letter {
  font-family: var(--f-body); font-size: 9px; font-weight: 700; line-height: 1;
  color: var(--text-2); padding: 1.5px 2px 1.5px 4px; border-radius: 4px 0 0 4px;
  transition: color .1s ease, background .1s ease, transform .1s ease;
}
.az-index-letter.dim { color: var(--text-3); opacity: .45; }
.az-index-letter.active { color: var(--accent-ink); background: var(--accent); transform: scale(1.25) translateX(-1px); }

/* Burbuja grande que aparece junto al dedo mientras se arrastra el índice,
   mostrando en grande la letra activa (igual que el cajón de apps de
   Android al deslizar sobre la lista lateral A-Z). */
.az-bubble {
  position: fixed; right: 34px; z-index: 91; transform: translateY(-50%) scale(.7);
  width: 52px; height: 52px; border-radius: 50% 50% 4px 50%;
  background: var(--accent); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--f-display); font-size: 24px; letter-spacing: .5px;
  box-shadow: 0 4px 16px rgba(0,0,0,.4);
  opacity: 0; pointer-events: none; transition: opacity .12s ease, transform .12s ease;
}
.az-bubble.show { opacity: 1; transform: translateY(-50%) scale(1); }

body.panel-open .az-index, body.panel-open .az-bubble { display: none; }

/* Hueco entre las carátulas y el índice A-Z: la tira de letras vive pegada
   al borde derecho (position:fixed, ver .az-index arriba) y por defecto la
   grilla usa el mismo padding de siempre (0 16px) — así las carátulas de
   la última columna quedan flotando justo debajo de las letras, pegadas.
   Se le agrega un padding-right extra a la grilla SOLO mientras el índice
   propio está activo (html.az-active, misma clase que ya oculta el
   scrollbar nativo más abajo), reservando el ancho real de la columna de
   letras + un respiro. Como la grilla mobile es de 3 columnas fr, angostar
   el contenedor angosta las 3 tarjetas por igual (efecto "un poco más
   chiquitas" pedido, sin tocar el layout de columnas). */
html.az-active .row,
html.az-active .grid {
  padding-right: 34px;
}

/* Con el índice A-Z propio en pantalla, el scrollbar nativo sobra — quedan
   los dos pegados al mismo borde derecho y se ve como un error de layout.
   Se oculta SOLO mientras el modo A-Z está activo (clase puesta/sacada por
   setupAZIndex/teardownAZIndex en app.js); en el resto del sitio el scroll
   de #content se ve como siempre.
   Importante: apunta a #content (el panel que de verdad scrollea, ver
   .content más arriba), NO a html/body — Chrome para Android ignora por
   completo el ocultamiento por CSS sobre el scroll de la ventana/documento,
   pero sí lo respeta sobre el overflow de un <div> propio. Por eso el
   intento anterior (con html/body) no se veía en el teléfono. */
html.az-active #content {
  scrollbar-width: none;        /* Firefox */
  -ms-overflow-style: none;     /* navegadores legacy basados en Edge/IE */
}
html.az-active #content::-webkit-scrollbar {
  display: none;                /* Chrome / Safari / WebViews basados en Blink-WebKit */
}

@media (min-width: 1000px) {
  /* En pantallas anchas el contenido queda centrado con márgenes grandes
     (ver .content max-width más abajo) — el índice se ancla al borde de
     esa columna en vez de quedar pegado al borde físico de la ventana. */
  .az-index { right: max(0px, calc((100vw - 1400px) / 2 + 2px)); }
  .az-bubble { right: max(34px, calc((100vw - 1400px) / 2 + 36px)); }
}

.recent-row { display: flex; gap: 10px; overflow-x: auto; padding: 0 16px 4px; scrollbar-width: none; }
.recent-row::-webkit-scrollbar { display: none; }
.recent-row .card { width: 112px; flex-shrink: 0; }

/* ── Card ─────────────────────────────────────────────────────────────── */
.card { cursor: pointer; transition: transform .18s cubic-bezier(.3,1,.5,1); }
.card:active { transform: scale(.96); }

.card-media {
  position: relative; width: 100%; padding-top: 150%; border-radius: var(--r);
  overflow: hidden; background: var(--surface); box-shadow: 0 0 0 1px var(--border-soft);
}
/* Música: las carátulas de álbum son ~1:1 (a diferencia de los pósters de
   Películas/Series, que son 2:3) — el backend marca item.square=true solo
   para categorías con grouping="album" (ver core/categories.py). */
.card-media.square { padding-top: 100%; }
.card-poster, .card-ph {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block;
}
/* Las carátulas de las tarjetas aparecen con un fundido al cargar (mkCard
   en catalog.js) en vez de "pegar el salto" cuando llega la imagen. */
.card-poster--fade { opacity: 0; transition: opacity .3s ease; }
.card-poster--fade.is-in { opacity: 1; }
.card-ph {
  background: linear-gradient(150deg, var(--surface-2), var(--bg-2));
  display: flex; align-items: center; justify-content: center; font-size: 30px;
}
/* Tarjetas de Televisión (ver TV_CHANNELS/showTvSection en app.js): las
   carátulas son logos de canal, no pósters 2:3, así que en vez de
   object-fit:cover (que las recortaría) se ponen sobre una placa clara y
   centradas con contain — mismo trato visual que le dan la mayoría de
   apps de streaming a los logos de canales en vivo. */
.card-media--tv { background: #fff; }
.card-tv-logo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; padding: 18%; box-sizing: border-box; display: block;
}
.card-skeleton {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(100deg, var(--surface) 30%, var(--surface-2) 50%, var(--surface) 70%);
  background-size: 250% 100%; animation: shimmer 1.4s ease-in-out infinite;
}
@keyframes shimmer { 0% { background-position: 150% 0; } 100% { background-position: -150% 0; } }

/* Cinta "Nuevo" (esquina superior izquierda de la carátula) — pintada por
   mkCard() en catalog.js solo cuando item.is_new (subido hoy y este
   usuario todavía no la vio, ver web/routes/catalog.py). Se apaga sola
   cuando la tarjeta entra al viewport (IntersectionObserver en catalog.js
   la retira del DOM), no hace falta tocarla. */
.card-badge {
  position: absolute; top: 6px; left: 6px; background: var(--accent); color: var(--accent-ink);
  font-family: var(--f-body); font-size: 9px; font-weight: 700; letter-spacing: .3px;
  padding: 3px 7px; border-radius: 5px; z-index: 2; text-transform: uppercase;
}
.card-sz {
  position: absolute; bottom: 6px; right: 6px; background: rgba(10,10,11,.82); color: var(--text);
  font-family: var(--f-mono); font-size: 8.5px; font-weight: 600; padding: 2px 6px; border-radius: 5px; z-index: 2;
}
.card-eps {
  position: absolute; bottom: 6px; right: 6px; background: rgba(10,10,11,.82); color: var(--text);
  font-family: var(--f-mono); font-size: 8.5px; padding: 2px 6px; border-radius: 5px; z-index: 2;
}
/* Esquina inferior DERECHA de las tarjetas de Televisión (mismo lugar que
   .card-eps/.card-sz en el catálogo normal, ver showTvSection en app.js):
   cantidad de gente con ese canal abierto ahora mismo (services/live_tv_
   viewers.py). Solo se pinta cuando hay al menos 1 (ver renderTvViewerBadge
   en app.js) — un canal sin nadie viendo no necesita un "0" compitiendo
   visualmente con el logo. */
.card-tv-viewers {
  position: absolute; bottom: 6px; right: 6px; background: rgba(10,10,11,.82); color: var(--text);
  font-family: var(--f-mono); font-size: 8.5px; font-weight: 600; padding: 2px 6px; border-radius: 5px; z-index: 2;
  display: flex; align-items: center; gap: 3px;
}
.card-pop {
  position: absolute; bottom: 6px; left: 6px; background: rgba(10,10,11,.82); color: var(--text);
  font-family: var(--f-mono); font-size: 8.5px; font-weight: 600; padding: 2px 6px; border-radius: 5px; z-index: 2;
}
/* Barra de progreso de "Seguir donde quedaste" (ver renderContinueCard en
   app.js) — pegada al borde inferior de la carátula, mismo lenguaje
   visual que YouTube/Netflix: una tira fina que se rellena con el
   avance real, sin número ni porcentaje que compita con card-sz/card-eps
   (que ya ocupan esa esquina en las tarjetas normales del catálogo). */
.card-progress-bar {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: rgba(10,10,11,.55); z-index: 2;
}
.card-progress-bar-fill { height: 100%; background: var(--accent); }
/* Check de "ya visto" — esquina superior derecha (top-left ya lo usa
   .card-badge). Apagado: solo un puntito casi invisible pegado al
   borde, para no ensuciar la carátula ni competir con el póster —
   descubrible al tacto/tap, no por ser un botón grande de entrada.
   "watched-on" se rellena en --accent (rojo de marca) para mantener
   consistencia visual con el resto del sitio en vez del verde genérico
   de "success" que no pegaba con la paleta. */
.card-watch {
  position: absolute; top: 4px; right: 4px; z-index: 3;
  width: 15px; height: 15px; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10,10,11,.35); color: transparent;
  border: 1px solid rgba(245,244,240,.28);
  cursor: pointer; transition: background .15s, border-color .15s, color .15s, transform .12s;
}
.card-watch:active { transform: scale(.85); }
.card-watch.watched-on {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.card-watch:disabled { opacity: .6; cursor: default; }
/* Corazón de favorito — mismo lenguaje visual que .card-watch de
   arriba (puntito discreto, se "llena" al activarse), pero pegado al
   lado izquierdo del check de "ya visto" en vez de la esquina
   top-left (que ya usa .card-badge para la cinta "Nuevo" — con ambos
   presentes a la vez, chocarían). "favorite-on" usa un rosa/rojo propio
   en vez de --accent para no confundirse visualmente con "ya visto". */
.card-favorite {
  position: absolute; top: 4px; right: 22px; z-index: 3;
  width: 15px; height: 15px; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10,10,11,.35); color: transparent;
  border: 1px solid rgba(245,244,240,.28);
  cursor: pointer; transition: background .15s, border-color .15s, color .15s, transform .12s;
}
.card-favorite:active { transform: scale(.85); }
.card-favorite.favorite-on {
  background: #ff4d6d; border-color: #ff4d6d; color: #fff;
}
.card-favorite:disabled { opacity: .6; cursor: default; }
/* "×" para sacar una tarjeta de "Seguir donde quedaste" (ver
   renderContinueCard/removeContinueItem en app.js). A diferencia de
   .card-watch/.card-favorite (puntito apagado que se "llena" al
   activarse), esta siempre se ve — no es un estado que alternar, es una
   acción directa — por eso el ícono tiene color propio desde el
   arranque en vez de nacer transparente. */
.card-remove {
  position: absolute; top: 4px; right: 4px; z-index: 3;
  width: 18px; height: 18px; border-radius: 50%; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10,10,11,.55); color: var(--text-2);
  border: 1px solid rgba(245,244,240,.28);
  cursor: pointer; transition: background .15s, color .15s, transform .12s;
}
.card-remove:active { transform: scale(.85); background: rgba(10,10,11,.8); color: var(--text); }
.card-remove:disabled { opacity: .5; cursor: default; }
.card-title {
  font-size: 12px; margin-top: 7px; line-height: 1.32; color: var(--text-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Resalto de la palabra buscada dentro del título/subtítulo de la
   tarjeta (ver highlightMatch en catalog.js) — mismo rojo de marca que
   el resto de la UI, no el amarillo por defecto de <mark>. */
mark.search-hl {
  background: var(--accent-soft); color: var(--accent);
  border-radius: 3px; padding: 0 1px; font-weight: 700;
}
/* Música: subtítulo de artista debajo del título del álbum, más discreto */
.card-subtitle {
  font-size: 10.5px; margin-top: 2px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Películas/Series/Anime: género(s) resueltos por TMDb, debajo del título
   (ver it["genres"] en web/routes/catalog.py) — en el color de acento
   para diferenciarlo del subtítulo de artista de Música. */
.card-genre { color: var(--accent-2, var(--accent)); }

/* ── Lista de pistas (álbum de Música) ────────────────────────────────── */
/* Reutiliza el mismo lenguaje visual que .ep-item (episodios de series) —
   ver más abajo — así que si cambias esos estilos, revisa que ambos
   sigan viéndose consistentes. */
.album-header { display: flex; gap: 14px; align-items: center; margin-bottom: 16px; }
.album-header-cover-wrap {
  position: relative; width: 84px; height: 84px; border-radius: 10px; flex-shrink: 0;
  box-shadow: 0 0 0 1px var(--border-soft), 0 6px 16px rgba(0,0,0,.35); overflow: hidden;
}
.album-header-cover { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Carátula ausente (pista sin audio.thumbs en Telegram, ver
   web/routes/cover.py): mismo lenguaje visual que .card-ph en vez de
   dejar el hueco vacío que quedaba con visibility:hidden. */
.album-header-cover-ph {
  width: 100%; height: 100%; align-items: center; justify-content: center; font-size: 28px;
  background: linear-gradient(150deg, var(--surface-2), var(--bg-2));
}
.track-list { margin-top: 14px; display: flex; flex-direction: column; gap: 2px; }
.track-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 8px; border-radius: 8px; cursor: pointer;
  transition: background .15s ease;
}
.track-item:hover, .track-item.playing { background: var(--surface); }
.track-item.playing .track-item-title { color: var(--accent); }
.track-item-num {
  width: 20px; text-align: center; font-family: var(--f-mono); font-size: 11px; color: var(--text-3); flex-shrink: 0;
}
.track-item-info { flex: 1; min-width: 0; }
.track-item-title { font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-item-sub { font-size: 10.5px; color: var(--text-3); margin-top: 2px; }
.track-item-actions { display: flex; gap: 6px; flex-shrink: 0; }

/* ── Mini-reproductor de música persistente (estilo Spotify) ─────────────── */
.mini-player {
  /* Flota justo ARRIBA de la barra de categorías (.bottom-nav) en vez de
     pegarse al borde física de la pantalla — mismo criterio que usan
     Spotify/YouTube Music cuando conviven un mini-player con una tab bar
     fija abajo. --bottom-nav-h la publica el ResizeObserver de app.js.
     z-index 150 A PROPÓSITO por debajo de CUALQUIER panel/hoja/modal de
     la app (arrancan en 200 — info/álbum, cuenta, límite, reportar,
     verificación, etc.) — antes estaba en 550, por ENCIMA de esos
     paneles, y como ellos son fixed;bottom:0 y el mini-player flota más
     arriba todavía, terminaba tapando el último tramo de su contenido
     (ver el fix anterior, que le daba padding-bottom extra a cada panel
     para compensar). Poniéndolo por debajo, el panel que se abre
     directamente lo tapa entero mientras está abierto — no hay tapado
     parcial que compensar porque no queda nada del mini-player a la
     vista para tapar. El precio es que mientras un panel está abierto no
     se puede controlar la música desde el mini-player (vuelve a
     aparecer solo, intacto, en cuanto se cierra el panel). */
  position: fixed; left: 0; right: 0; bottom: var(--bottom-nav-h, 60px); z-index: 150;
  display: none; flex-direction: column;
  background: rgba(16,16,18,.97); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--border-soft);
  box-shadow: 0 -8px 24px rgba(0,0,0,.35);
  transition: bottom .18s ease;
}
.mini-player.show { display: flex; }
/* Deja espacio abajo para que el mini-player no tape el último renglón de
   contenido (grid, footer, etc.) mientras está sonando algo — se suma al
   hueco de .bottom-nav que .content ya reserva siempre (ver arriba). Esto
   sigue haciendo falta: .content es la página de fondo, no un panel, y
   sigue viéndose detrás del mini-player cuando no hay ningún panel
   abierto encima. */
body.has-mini-player .content { padding-bottom: calc(var(--bottom-nav-h, 60px) + 84px); }

/* NOTA: los paneles/modales de scroll interno (info de álbum/película,
   cuenta, planes, límite alcanzado, reportar) YA NO necesitan el
   padding-bottom extra que tenían acá para compensar al mini-player —
   con z-index:150 (ver .mini-player arriba) el propio panel lo tapa
   entero apenas se abre, así que no hay nada que destapar haciendo
   scroll. Si en algún momento el mini-player vuelve a subirse por
   encima de los paneles, este es el primer lugar a revisar. */


/* Barra de progreso "de borde a borde" pegada arriba de todo el mini-player
   — es el patrón que usan Spotify/YouTube Music y da lectura de avance de
   un vistazo sin depender del slider angosto de abajo (que en pantallas
   chicas queda oculto, ver .mini-player-progress). */
.mini-player-bar { position: relative; width: 100%; height: 2.5px; background: var(--border-soft); flex-shrink: 0; cursor: pointer; }
.mini-player-bar-fill { position: absolute; inset: 0 auto 0 0; width: 0%; background: var(--accent); transition: width .1s linear; }

/* Manija de acceso a la cola: position:absolute y pegada al borde
   superior del mini-player (top:0, igual que .mini-player-bar, con la
   que se superpone) — así el mini-player NO crece ni un píxel para
   alojarla, queda "flotando" sobre la barrita de progreso. El trazo
   visible (::before) es angosto, pero el botón en sí es más alto para
   dar un área de toque cómoda sin agrandar nada. Se oculta por JS
   (syncQueueBar en app.js) cuando no hay cola (una sola pista). */
.mini-player-queue-handle {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%); z-index: 2;
  width: 46px; height: 14px; padding: 0; margin: 0; background: none; border: none; cursor: pointer;
  display: none; align-items: center; justify-content: center;
}
.mini-player-queue-handle::before {
  content: ''; width: 30px; height: 3.5px; border-radius: 2px; background: var(--border-soft);
  transition: background .15s ease;
}
.mini-player-queue-handle:hover::before, .mini-player-queue-handle:active::before { background: var(--text-3); }

.mini-player-row {
  /* Ya no toca el borde física de la pantalla (flota arriba de
     .bottom-nav, que es quien reserva su propio env(safe-area-inset-bottom)
     — ver .mini-player y .bottom-nav más arriba), así que no necesita su
     propio respiro de safe-area además del que ya tiene la barra de abajo. */
  display: flex; align-items: center; gap: 12px;
  padding: 9px 14px;
}

.mini-player-cover-wrap {
  position: relative; width: 44px; height: 44px; border-radius: 8px; flex-shrink: 0; overflow: hidden;
  box-shadow: 0 0 0 1px var(--border-soft), 0 3px 10px rgba(0,0,0,.4);
}
.mini-player-cover { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Carátula ausente: mismo lenguaje visual que .card-ph — evita el ícono
   de imagen rota del navegador cuando la pista no tiene audio.thumbs. */
.mini-player-cover-ph {
  width: 100%; height: 100%; align-items: center; justify-content: center; font-size: 17px;
  background: linear-gradient(150deg, var(--surface-2), var(--bg-2));
}

.mini-player-info { min-width: 0; flex: 1 1 90px; }
.mini-player-title {
  font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mini-player-artist {
  font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px;
}

.mini-player-controls { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
.mini-player-btn {
  width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--text-2); font-size: 15px; cursor: pointer;
  line-height: 1; border-radius: 50%; transition: background .15s ease, color .15s ease;
}
.mini-player-btn:hover { color: var(--text); background: var(--surface); }
.mini-player-btn:active { transform: scale(.9); }
.mini-player-btn.play {
  width: 36px; height: 36px; color: var(--accent-ink); font-size: 15px;
  background: var(--accent); box-shadow: 0 2px 8px rgba(0,0,0,.3);
}
.mini-player-btn.play:hover { background: var(--accent); filter: brightness(1.08); }
/* Estado "cargando" (mientras se resuelve el HEAD a /watch antes de sonar):
   antes se ponía el emoji ⏳ como texto — además de romper la inmersión
   (sale amarillo en casi todos los sets de emoji, sin forma de recolorearlo
   desde CSS), no combinaba con el resto del sitio. Reusa el mismo lenguaje
   visual que .spinner, en las tintas del propio botón (--accent-ink). */
.mini-player-btn.play.loading { position: relative; }
.mini-player-btn.play.loading::after {
  content: ''; position: absolute; width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid rgba(0,0,0,.28); border-top-color: rgba(0,0,0,.8);
  animation: spin .7s linear infinite;
}

.mini-player-progress { display: none; align-items: center; gap: 7px; flex: 1 1 140px; min-width: 0; }
.mini-player-time { font-family: var(--f-mono); font-size: 10px; color: var(--text-3); flex-shrink: 0; width: 30px; }
.mini-player-time:last-child { text-align: right; }
.mini-player-progress input[type="range"] {
  flex: 1; height: 3px; accent-color: var(--accent); cursor: pointer; -webkit-appearance: none; appearance: none;
  /* El degradado real (tramo sonado en --accent / resto en --border-soft)
     lo pinta updateSeekFill() en app.js en cada cambio de value — este
     background es solo el estado inicial antes del primer timeupdate. */
  background: linear-gradient(to right, var(--accent) 0%, var(--border-soft) 0%);
  border-radius: 2px;
}
.mini-player-progress input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 11px; height: 11px; border-radius: 50%; background: var(--text); cursor: pointer;
}
.mini-player-btn.close { width: 26px; height: 26px; color: var(--text-3); font-size: 12px; flex-shrink: 0; }
.mini-player-btn.expand { width: 26px; height: 26px; color: var(--text-3); flex-shrink: 0; }
/* La barra de progreso completa (con tiempos) solo entra cómoda desde
   ~460px de ancho — en móviles angostos se prioriza cover+título+controles
   y el avance ya se ve en .mini-player-bar (arriba de toda la barra). */
@media (min-width: 460px) { .mini-player-progress { display: flex; } }

/* ── Reproductor a pantalla completa ──────────────────────────────────────
   Se abre desde el botón ⌃ de la mini-barra (o al revés, ⌄ para volver a
   minimizar) — comparte el mismo <audio> y la misma cola que el mini-
   reproductor; ver syncFsPlayer()/fsPlayerShow()/fsPlayerHide() en app.js.
   z-index por encima de TODO (paneles, modales, el propio mini-player, la
   navbar) porque conceptualmente "es" el mini-player, solo que ocupando
   toda la pantalla. */
.fs-player {
  position: fixed; inset: 0; z-index: 900; display: none; flex-direction: column;
  padding: calc(18px + env(safe-area-inset-top)) 22px calc(28px + env(safe-area-inset-bottom));
  background: radial-gradient(circle at 50% 0%, var(--surface-2) 0%, var(--bg) 65%);
}
.fs-player.show { display: flex; }
body.has-fs-player { overflow: hidden; }

.fs-player-topbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.fs-player-topbar-label {
  font-family: var(--f-body); font-size: 11px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-3);
}
.fs-player-icon-btn {
  width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--text-2); cursor: pointer; border-radius: 50%;
  transition: background .15s ease, color .15s ease;
}
.fs-player-icon-btn:hover { color: var(--text); background: var(--surface-2); }
.fs-player-icon-btn:active { transform: scale(.92); }

/* Promo musical patrocinada dentro del reproductor fullscreen — ver
   syncFsPlayerPromo()/buildMusicPromoCard() en app.js. Vacío (sin margen,
   sin altura) cuando no hay promo activa, para no restarle espacio a
   .fs-player-cover-wrap (flex:1) que centra la carátula. */
.fs-player-promo-slot { flex-shrink: 0; }
.fs-player-promo {
  display: block; cursor: pointer; margin-top: 14px;
  padding: 10px 12px; border-radius: var(--r);
  background: linear-gradient(135deg, var(--surface-2), var(--surface));
  border: 1px solid var(--border);
}
.fs-player-promo:active { transform: scale(.98); }

.fs-player-cover-wrap {
  flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; margin: 8px 0;
}
.fs-player-cover, .fs-player-cover-ph {
  width: min(78vw, 360px); height: min(78vw, 360px); border-radius: var(--r-lg);
  box-shadow: 0 20px 60px rgba(0,0,0,.5), 0 0 0 1px var(--border-soft);
}
.fs-player-cover { object-fit: cover; display: block; }
.fs-player-cover-ph {
  display: flex; align-items: center; justify-content: center; color: var(--text-3);
  position: relative; overflow: hidden; isolation: isolate;
  background: radial-gradient(120% 130% at 78% 8%, #2a0c05 0%, var(--bg-2) 46%, var(--bg) 100%);
}
/* Sin carátula: en vez del rectángulo plano de siempre, un fondo tipo
   "lámpara de lava" con la misma paleta cálida (negro + rojo/naranja de
   --accent) que ya usamos en las tarjetas promo — cuatro manchas
   difuminadas que flotan y cambian de forma en bucles largos y
   desincronizados entre sí para que nunca se sienta repetitivo. */
.fs-player-cover-ph-icon { position: relative; z-index: 1; opacity: .55; filter: drop-shadow(0 2px 10px rgba(0,0,0,.5)); }
.lava-lamp { position: absolute; inset: -10%; z-index: 0; }
.lava-blob {
  position: absolute; width: 62%; height: 62%; border-radius: 42% 58% 65% 35% / 45% 45% 55% 55%;
  filter: blur(26px); mix-blend-mode: screen; will-change: transform, border-radius;
}
.lava-blob.b1 {
  top: 4%; left: 6%; background: radial-gradient(circle at 35% 35%, var(--accent) 0%, rgba(255,61,26,0) 72%);
  animation: lavaFloat1 17s ease-in-out infinite;
}
.lava-blob.b2 {
  bottom: 2%; right: 4%; width: 58%; height: 58%;
  background: radial-gradient(circle at 60% 40%, #ff7a45 0%, rgba(255,122,69,0) 74%);
  animation: lavaFloat2 21s ease-in-out infinite; animation-delay: -6s;
}
.lava-blob.b3 {
  bottom: -8%; left: -6%; width: 52%; height: 52%;
  background: radial-gradient(circle at 45% 55%, var(--accent-dim) 0%, rgba(201,47,18,0) 75%);
  animation: lavaFloat3 25s ease-in-out infinite; animation-delay: -11s;
}
.lava-blob.b4 {
  top: -6%; right: 10%; width: 46%; height: 46%;
  background: radial-gradient(circle at 50% 50%, #7a1204 0%, rgba(122,18,4,0) 75%);
  animation: lavaFloat4 19s ease-in-out infinite; animation-delay: -3s;
}
@keyframes lavaFloat1 {
  0%, 100% { transform: translate(0%, 0%) scale(1); border-radius: 42% 58% 65% 35% / 45% 45% 55% 55%; }
  30%      { transform: translate(22%, 14%) scale(1.12); border-radius: 60% 40% 45% 55% / 55% 60% 40% 45%; }
  65%      { transform: translate(8%, 30%) scale(0.92); border-radius: 50% 50% 38% 62% / 40% 55% 45% 60%; }
}
@keyframes lavaFloat2 {
  0%, 100% { transform: translate(0%, 0%) scale(1); border-radius: 55% 45% 40% 60% / 50% 40% 60% 50%; }
  35%      { transform: translate(-18%, -20%) scale(1.15); border-radius: 40% 60% 55% 45% / 60% 45% 55% 40%; }
  70%      { transform: translate(-6%, -8%) scale(0.95); border-radius: 65% 35% 45% 55% / 45% 55% 45% 55%; }
}
@keyframes lavaFloat3 {
  0%, 100% { transform: translate(0%, 0%) scale(1); border-radius: 48% 52% 60% 40% / 55% 45% 55% 45%; }
  40%      { transform: translate(16%, -18%) scale(1.2); border-radius: 60% 40% 42% 58% / 42% 58% 42% 58%; }
  75%      { transform: translate(26%, -4%) scale(0.9); border-radius: 40% 60% 55% 45% / 60% 40% 60% 40%; }
}
@keyframes lavaFloat4 {
  0%, 100% { transform: translate(0%, 0%) scale(1); border-radius: 50% 50% 45% 55% / 45% 55% 50% 50%; }
  50%      { transform: translate(-14%, 22%) scale(1.18); border-radius: 58% 42% 60% 40% / 40% 60% 45% 55%; }
}
@media (prefers-reduced-motion: reduce) {
  .lava-blob { animation: none; }
}

.fs-player-meta { text-align: center; flex-shrink: 0; padding: 0 6px; }
.fs-player-title {
  font-family: var(--f-display); font-size: 26px; letter-spacing: .01em; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fs-player-artist { font-size: 14px; color: var(--text-2); margin-top: 4px; }

.fs-player-progress { flex-shrink: 0; margin: 22px 0 6px; }
.fs-player-bar { display: none; } /* la barra fina va en el mini-player; acá el slider ya cumple ese rol */
.fs-player-progress input[type="range"] {
  width: 100%; height: 4px; accent-color: var(--accent); cursor: pointer;
  -webkit-appearance: none; appearance: none;
  /* Idem mini-player: relleno real vía updateSeekFill() en app.js. */
  background: linear-gradient(to right, var(--accent) 0%, var(--border-soft) 0%);
  border-radius: 2px;
}
.fs-player-progress input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 13px; height: 13px; border-radius: 50%; background: var(--text); cursor: pointer;
}
.fs-player-times {
  display: flex; justify-content: space-between; margin-top: 6px;
  font-family: var(--f-mono); font-size: 11px; color: var(--text-3);
}

.fs-player-controls { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; padding: 4px 4px 0; }
.fs-player-side-btn {
  width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--text); cursor: pointer; border-radius: 50%;
}
.fs-player-side-btn:active { transform: scale(.92); }
.fs-player-play-btn {
  width: 68px; height: 68px; display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--accent-ink); border: none; border-radius: 50%; cursor: pointer;
  box-shadow: 0 6px 20px rgba(255,61,26,.35);
}
.fs-player-play-btn:active { transform: scale(.94); }
.fs-player-play-btn.loading { position: relative; }
.fs-player-play-btn.loading::after {
  content: ''; position: absolute; width: 22px; height: 22px; border-radius: 50%;
  border: 2.5px solid rgba(0,0,0,.28); border-top-color: rgba(0,0,0,.8); animation: spin .7s linear infinite;
}
/* El SVG de play/pausa que se copia acá viene de la mini-barra (14x15px,
   ver syncFsPlayer en app.js) — se reescala acá por CSS en vez de generar
   un ícono aparte, porque los atributos width/height del SVG ceden ante
   estas reglas (son solo su valor por defecto). */
.fs-player-play-btn svg { width: 26px; height: 26px; }
.fs-player-toggle-btn {
  position: relative; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--text-3); cursor: pointer; border-radius: 50%;
  transition: color .15s ease;
}
.fs-player-toggle-btn:active { transform: scale(.92); }
.fs-player-toggle-btn.active { color: var(--accent); }
/* Punto debajo del ícono cuando el modo repetir/aleatorio está activo —
   mismo lenguaje que usan Spotify/Apple Music, más legible en un vistazo
   que solo el cambio de color del ícono. */
.fs-player-toggle-btn.active::after {
  content: ''; position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%);
  width: 4px; height: 4px; border-radius: 50%; background: var(--accent);
}

/* ── Barrita "A continuación" (acceso a la cola) ─────────────────────────
   Antes era una tarjeta sólida (surface-2 + borde) que quedaba como un
   bloque pegado y desentonaba con el fondo degradado del fullscreen
   player (ver .fs-player, radial-gradient). Ahora es transparente — se
   funde con ese mismo fondo — y el único límite visual es un hairline
   arriba (border-top), igual al patrón que ya usan las listas del sitio
   (más inmersivo, menos "caja flotante"). Ver buildQueueBarHtml()/
   openQueueSheet() en app.js. */
.fs-player-queue-bar {
  flex-shrink: 0; margin-top: 14px; width: 100%; display: flex; align-items: center; gap: 10px;
  padding: 14px 6px 4px; background: transparent; border: none; border-top: 1px solid var(--border-soft);
  color: var(--text-2); cursor: pointer; text-align: left; border-radius: 0;
  transition: opacity .15s ease;
}
.fs-player-queue-bar:active { opacity: .6; transform: none; }
.fs-player-queue-bar .icon { flex-shrink: 0; color: var(--text-3); }
.fs-player-queue-bar-text { flex: 1; min-width: 0; }
.fs-player-queue-bar-label {
  font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.fs-player-queue-bar-count { font-size: 11px; color: var(--text-3); margin-top: 1px; }
.fs-player-queue-bar-chevron { flex-shrink: 0; color: var(--text-3); transform: rotate(-90deg); }

/* ── Hoja deslizable de la cola ("A continuación") ───────────────────────
   z-index por encima del fs-player (900): se abre DESDE el fullscreen
   player, así que tiene que quedar por delante de él. */
.queue-sheet {
  position: fixed; inset: 0; z-index: 950; display: none;
}
.queue-sheet.show { display: block; }
.queue-sheet-backdrop {
  position: absolute; inset: 0; background: rgba(6,5,4,.6); opacity: 0; transition: opacity .22s ease;
}
.queue-sheet.show .queue-sheet-backdrop { opacity: 1; }
.queue-sheet-panel {
  position: absolute; left: 0; right: 0; bottom: 0; max-height: 82vh; display: flex; flex-direction: column;
  background: var(--surface); border-radius: 18px 18px 0 0; box-shadow: 0 -20px 60px rgba(0,0,0,.5);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  transform: translateY(100%); transition: transform .28s cubic-bezier(.32,.72,0,1);
}
.queue-sheet.show .queue-sheet-panel { transform: translateY(0); }
.queue-sheet-grabber {
  width: 36px; height: 4px; border-radius: 2px; background: var(--border-soft); margin: 10px auto 2px; flex-shrink: 0;
}
.queue-sheet-header {
  flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
  padding: 10px 10px 10px 20px;
}
.queue-sheet-title { font-family: var(--f-display); font-size: 17px; color: var(--text); }
.queue-sheet-list { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 8px 18px; }

.queue-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 8px; border-radius: var(--r);
  touch-action: pan-y; /* deja que el scroll vertical funcione normal fuera de la manija */
}
.queue-row.now-playing { background: var(--surface-2); }
.queue-row-cover {
  width: 42px; height: 42px; border-radius: 8px; object-fit: cover; flex-shrink: 0; background: var(--surface-2);
}
.queue-row-info { flex: 1; min-width: 0; }
.queue-row-title {
  font-size: 13.5px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.queue-row.now-playing .queue-row-title { color: var(--accent); }
.queue-row-artist {
  font-size: 11.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px;
}
.queue-row-now-badge {
  flex-shrink: 0; width: 16px; height: 16px; color: var(--accent); display: flex; align-items: center; justify-content: center;
}
.queue-row-handle {
  flex-shrink: 0; width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  color: var(--text-3); cursor: grab; touch-action: none; /* la manija SÍ captura el gesto para reordenar */
}
.queue-row-handle:active { cursor: grabbing; }
/* Fila siendo arrastrada: se despega visualmente del resto de la lista
   mientras el usuario la mueve (ver wireQueueReorder en app.js). */
.queue-row.dragging {
  position: relative; z-index: 2; background: var(--surface-2);
  box-shadow: 0 8px 24px rgba(0,0,0,.4); border-radius: var(--r);
}

/* ── Estados vacíos / carga ───────────────────────────────────────────── */
.center-msg { text-align: center; padding: 60px 24px; color: var(--text-2); }
.center-msg .ico { font-size: 40px; margin-bottom: 12px; opacity: .7; }
.center-msg p { font-size: 14px; }

/* Botón "Sugerir <lo que buscaste>" debajo de "Sin resultados" — ver
   renderGrid en catalog.js. Solo aparece cuando la grilla vacía es de
   verdad una búsqueda (con query), nunca en un "Favoritos" vacío. */
.empty-suggest-btn {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 18px;
  padding: 11px 18px; border: none; border-radius: var(--r); cursor: pointer;
  background: var(--accent); color: var(--accent-ink); font-weight: 700; font-size: 13px;
  max-width: 100%;
}
.empty-suggest-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-suggest-btn:active { transform: scale(.98); }
.spinner {
  width: 26px; height: 26px; border: 2.5px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: spin .8s linear infinite; margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }

.bottom-loader { display: none; align-items: center; justify-content: center; padding: 24px 0 32px; }
.bottom-loader.show { display: flex; }
#scrollSentinel { width: 100%; height: 1px; }

/* ── Panel de info (sinopsis, se completa en el próximo paso) ────────── */
.info-overlay {
  /* z-index 350 (no 200 como antes): tiene que quedar SIEMPRE por
     encima de .auth-panel (301, ver auth.css) — antes no importaba
     porque nada dentro de .auth-panel abría este panel de info, pero
     el panel "Favoritos" (ver renderFavoritesView en app.js) vive
     adentro de .auth-panel y sus tarjetas SÍ lo abren (mismo
     openInfoPanel de siempre). Con 200 quedaba tapado por completo
     detrás del panel de Favoritos. */
  position: fixed; inset: 0; background: rgba(6,5,4,.7); z-index: 350;
  opacity: 0; pointer-events: none; transition: opacity .25s ease;
}
.info-overlay.show { opacity: 1; pointer-events: all; }
.info-panel {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 351; background: var(--surface);
  border-radius: var(--r-lg) var(--r-lg) 0 0; max-height: 85vh; overflow-y: auto;
  transform: translateY(100%); transition: transform .35s cubic-bezier(.32,.72,0,1);
  border-top: 1px solid var(--border);
}
.info-panel.show { transform: translateY(0); }

/* ── Botones "Compartir"/"Reportar" (esquina superior derecha del panel de info) ── */
.info-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.info-title-row h3 { flex: 1; min-width: 0; }
.info-title-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; margin-top: 2px; }
.info-report-btn {
  flex-shrink: 0; display: flex; align-items: center; gap: 5px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 16px;
  padding: 6px 11px; font-family: var(--f-body); font-size: 11px; font-weight: 600;
  color: var(--text-2); cursor: pointer;
}
.info-report-btn:active { transform: scale(.95); }

/* ── Panel de reporte (motivos + "otro") ─────────────────────────────────
   Reusa .modal-overlay / .modal-box (ver player.css) para no duplicar el
   fondo oscuro + caja centrada que ya existe para el modal de "límite
   alcanzado" — mismo mecanismo visual, contenido distinto. */
.report-reason-list { display: flex; flex-direction: column; gap: 8px; margin: 16px 0; text-align: left; }
.report-reason-btn {
  width: 100%; text-align: left; padding: 12px 14px; border-radius: var(--r);
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  font-family: var(--f-body); font-size: 13.5px; font-weight: 500; cursor: pointer;
}
.report-reason-btn:active { transform: scale(.98); }
.report-reason-btn.selected { border-color: var(--accent); background: var(--accent-soft); }
.report-detail-wrap { text-align: left; margin-top: 4px; }
.report-detail-input {
  width: 100%; min-height: 78px; margin-top: 6px; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: 11px 13px; color: var(--text); font-family: var(--f-body);
  font-size: 13.5px; resize: vertical; outline: none;
}
.report-detail-input:focus { border-color: var(--accent); }
.report-status-msg { font-size: 12.5px; color: var(--text-2); margin-top: 12px; min-height: 16px; }
.report-status-msg.error { color: var(--danger); }
.report-status-msg.success { color: var(--success); }

/* ── Captura opcional en el panel de "Sugerir contenido" ─────────────────
   Mismo dropzone que .pay-photo-input (ver plans.css), clase propia acá
   porque el panel de sugerencia vive fuera del flujo de pagos — evita
   acoplar este archivo a plans.css por un solo bloque compartido. */
.suggest-photo-input {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  border: 1.5px dashed var(--border); border-radius: var(--r); padding: 20px 14px; cursor: pointer;
  margin: 12px 0 4px; color: var(--text-2); font-size: 12.5px; text-align: center;
}
.suggest-photo-input.has-file { border-color: var(--accent); color: var(--accent); }
.suggest-photo-input input { display: none; }
.suggest-photo-preview { max-width: 100%; max-height: 140px; border-radius: var(--r-sm); margin-bottom: 4px; }

/* ── Selector de temporadas / episodios (series y anime agrupados) ──────── */
.ep-season-tabs { display: flex; gap: 6px; overflow-x: auto; margin: 14px 0 12px; scrollbar-width: none; }
.ep-season-tabs::-webkit-scrollbar { display: none; }
.ep-season-btn {
  flex-shrink: 0; background: var(--surface-2); border: 1px solid var(--border); border-radius: 16px;
  padding: 6px 13px; font-family: var(--f-body); font-size: 12px; font-weight: 600;
  color: var(--text-2); cursor: pointer;
}
.ep-season-btn.active { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* Botón "Descargar temporada completa" — exclusivo plan Ultra (ver
   app.js: renderSeasonDownloadButton). Fondo tenue + borde acento en vez
   del auth-btn plano, para que se lea como un perk premium y no como un
   botón de acción genérico más; el badge redondo a la derecha reemplaza
   "(N episodios)" en el texto, que antes hacía que el botón se rompiera
   en dos líneas y se viera desprolijo. */
.season-download-btn {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 14px; margin: 10px 0 4px; border-radius: var(--r);
  background: var(--accent-soft); border: 1px solid var(--accent);
  color: var(--accent); font-family: var(--f-body); font-weight: 700; font-size: 13px;
  cursor: pointer; text-align: left;
}
.season-download-btn:active { transform: scale(.98); }
.season-download-btn:disabled { opacity: .55; pointer-events: none; }
.season-download-btn .season-download-label { display: flex; align-items: center; gap: 8px; min-width: 0; }
.season-download-btn .season-download-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.season-download-btn .season-download-count {
  flex-shrink: 0; background: var(--accent); color: var(--accent-ink);
  font-size: 10.5px; font-weight: 700; padding: 3px 9px; border-radius: 10px; white-space: nowrap;
}

.ep-item { display: flex; align-items: center; gap: 10px; padding: 11px 0; border-bottom: 1px solid var(--border-soft); }
.ep-item:last-child { border-bottom: none; }
.ep-item-info { flex: 1; min-width: 0; }
.ep-item-title { font-size: 13px; font-weight: 600; }
.ep-item-sub { font-family: var(--f-mono); font-size: 10.5px; color: var(--text-2); margin-top: 2px; }
.ep-item-actions { display: flex; gap: 6px; flex-shrink: 0; }
.ep-item-btn {
  width: 34px; height: 34px; border-radius: 50%; background: var(--surface-2);
  border: 1px solid var(--border); display: flex; align-items: center; justify-content: center;
  font-size: 13px; color: var(--text); cursor: pointer;
}
.ep-item-btn:active { transform: scale(.9); }
.ep-item-btn.play { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* Check de "ya visto" por episodio — mismo lenguaje visual que
   .card-watch (catálogo), pero un poco más chico para caber a la
   izquierda de cada fila de la lista. Mismo --accent (rojo de marca)
   que .card-watch, en vez del verde genérico anterior. */
.ep-watch {
  width: 26px; height: 26px; border-radius: 50%; padding: 0; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1.5px solid var(--border); color: var(--text-3);
  cursor: pointer; transition: background .15s, border-color .15s, color .15s, transform .12s;
}
.ep-watch:active { transform: scale(.88); }
.ep-watch.watched-on {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.ep-watch:disabled { opacity: .6; cursor: default; }

/* ── Reparto principal ────────────────────────────────────────────────── */
.cast-title {
  font-family: var(--f-body); font-size: 11px; font-weight: 700; color: var(--text-2);
  text-transform: uppercase; letter-spacing: .5px; margin: 18px 0 10px;
}
.cast-row { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
.cast-row::-webkit-scrollbar { display: none; }
.cast-item { flex-shrink: 0; width: 64px; text-align: center; }
.cast-photo {
  width: 54px; height: 54px; border-radius: 50%; overflow: hidden; background: var(--surface-2);
  margin: 0 auto 6px; display: flex; align-items: center; justify-content: center;
}
.cast-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cast-name { font-size: 10.5px; font-weight: 600; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cast-char { font-size: 9px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── Tráiler ──────────────────────────────────────────────────────────── */
.trailer-wrap { position: relative; width: 100%; padding-top: 56.25%; border-radius: var(--r); overflow: hidden; background: #000; }
.trailer-wrap iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ══════════════════════════════════════════════════════════════════════
   Modo TV (web/static/js/tvmode.js)
   ──────────────────────────────────────────────────────────────────────
   Pensado para verse desde el sofá con un control remoto: nada de hover
   ni de gestos táctiles, así que cada elemento con foco tiene que ser
   enorme y obvio, y hay que agrandar tarjetas/texto para la distancia de
   lectura de un TV ("10-foot UI"). Se activa solo por User-Agent en TVs
   conocidas, o a mano desde Configuraciones (ver initTvMode/
   setTvModePreference en tvmode.js) — la clase la pone/saca ese módulo
   en <html>, acá solo se define cómo se ve.
   ══════════════════════════════════════════════════════════════════════ */
html.tv-mode { font-size: 18px; }

html.tv-mode .row,
html.tv-mode .grid {
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 22px;
  padding: 0 40px;
}
html.tv-mode .card-title { font-size: 15px; }
html.tv-mode .section-title { padding: 0 40px; font-size: 23px; }
html.tv-mode .content { padding-left: 0; padding-right: 0; }

/* Barra de categorías: más alta y con texto más grande — en un TV se lee
   de lejos, no hace falta apretarla para dejar sitio a los pulgares. */
html.tv-mode .bottom-nav { padding-top: 14px; padding-bottom: calc(28px + env(safe-area-inset-bottom, 0px)); }
html.tv-mode .cat-icon svg { width: 26px; height: 26px; }
html.tv-mode .cats-label { font-size: 13px; }

/* Foco: gigante y con alto contraste, muy por encima del :focus-visible
   discreto de tokens.css (pensado para teclado/mouse en escritorio) —
   con un control remoto no hay cursor que te diga dónde estás parado,
   el anillo de foco ES el cursor. */
html.tv-mode *:focus { outline: none; }
html.tv-mode .card:focus,
html.tv-mode button:focus,
html.tv-mode a:focus,
html.tv-mode input:focus,
html.tv-mode select:focus,
html.tv-mode textarea:focus,
html.tv-mode .switch-toggle:focus,
html.tv-mode [tabindex]:focus {
  outline: 4px solid var(--accent);
  outline-offset: 4px;
  border-radius: 12px;
  transform: scale(1.06);
  transition: transform .12s ease, outline-offset .12s ease;
  position: relative;
  z-index: 5;
}
/* .card ya tiene su propia transición de transform (hover/active en
   escritorio/táctil) — evita que las dos animaciones compitan y el foco
   "tiemble" al entrar. */
html.tv-mode .card:focus { transition: transform .12s ease; }

/* ── Teclado en pantalla del Modo TV (web/static/js/tvmode.js) ───────────
   Un Smart TV no tiene teclado físico ni táctil, y confiar en que el
   navegador/WebView del fabricante muestre su propio teclado virtual al
   enfocar un <input> es poco confiable (sobre todo en WebViews "pelados"
   como un TWA) — así que Modo TV trae el suyo: aparece solo, se navega
   con las mismas flechas que el resto de la interfaz (reutiliza el motor
   de foco espacial de arriba, cada tecla es un <button> más) y escribe
   directo sobre el <input> real disparando un evento 'input' nativo, así
   la búsqueda existente (ver el listener de #searchInput en app.js) no
   necesita saber que esto existe. */
html.tv-mode .tv-kb-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.75); z-index: 960;
  opacity: 0; pointer-events: none; transition: opacity .15s ease;
}
html.tv-mode .tv-kb-overlay.show { opacity: 1; pointer-events: auto; }
html.tv-mode .tv-kb-panel {
  position: fixed; left: 50%; bottom: -100%; transform: translateX(-50%);
  width: min(760px, 94vw); z-index: 961; background: var(--surface);
  border: 1px solid var(--border); border-radius: 16px 16px 0 0;
  padding: 22px 24px calc(28px + env(safe-area-inset-bottom, 0px));
  transition: bottom .2s ease;
}
html.tv-mode .tv-kb-panel.show { bottom: 0; }
html.tv-mode .tv-kb-preview {
  font-family: var(--f-mono); font-size: 19px; color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border-soft); border-radius: 10px; padding: 12px 16px; margin-bottom: 16px;
  min-height: 24px; letter-spacing: .3px; white-space: nowrap; overflow: hidden;
}
html.tv-mode .tv-kb-cursor { opacity: .55; animation: tvKbBlink 1s step-end infinite; }
@keyframes tvKbBlink { 50% { opacity: 0; } }
html.tv-mode .tv-kb-row { display: flex; gap: 8px; margin-bottom: 8px; justify-content: center; }
html.tv-mode .tv-kb-key {
  min-width: 42px; height: 42px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); font-size: 16px; font-family: var(--f-body);
  cursor: pointer; display: flex; align-items: center; justify-content: center;
}
html.tv-mode .tv-kb-key--wide { min-width: 150px; flex: 1; }
html.tv-mode .tv-kb-key--accent { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 700; }
