/* ==================================================================================================
   SISTEMA DE DISEÑO — el heredado de ODTVPannel, con LA PALETA DE AEON encima
   --------------------------------------------------------------------------------------------------
   ★★★★★ AEON — el rebranding cambió 1.208 textos y no tocó un solo color: el panel seguía siendo
   gris neutro con acento azul índigo, que es la paleta de ODApp. Los tokens de abajo son ahora los
   del apartado 2 de la identidad, y no son una preferencia estética:

     · el fondo NO es gris neutro, es GRAFITO (#14171A) con una pizca de azul. Un gris puro se lee
       como «sin decidir»; y el fondo oscuro no es estética sino función — el ojo juzga el brillo
       por comparación, y un marco claro le roba contraste a lo que se mira.
     · el acento es CIAN (#46C7E8), y es **señal, no decoración**: se reserva para lo que está
       vivo. Si aparece en dos sitios de la misma pantalla compitiendo, deja de significar algo.

   ⚠⚠ Y el cian obliga a un cambio que NO es opcional: `--on-accent` pasa de blanco a grafito.
   Texto blanco sobre #46C7E8 da **1,98:1**, por debajo de cualquier mínimo legible; con grafito
   son **9,21:1**. Es el mismo error que se midió y corrigió en el botón «Reproducir» de la app.
   --------------------------------------------------------------------------------------------------
   (lo de abajo es la nota original de Aeon 52)
   --------------------------------------------------------------------------------------------------
   Los dos paneles ya compartían la paleta; lo que aquí faltaba eran las ESCALAS. Sin ellas cada
   pantalla se inventaba su propio tamaño de letra, su propio hueco y su propia esquina, y eso es lo
   que hacía que este panel y el otro no se parecieran aunque los colores fueran los mismos.
   Espejo de `ODTVPannel/codigo/web/src/tokens.css`.

   ⚠ Las variables viejas (--blue-2, --*-soft, --line-soft, --shadow…) NO se tocan: las usan cientos
   de reglas de este fichero. Las nuevas conviven con ellas.
   ================================================================================================== */
:root {
  color-scheme: dark;
  /* La familia grafito de la identidad: --graf para el fondo, --sup para lo que flota encima. */
  --bg: #14171A;
  --bg-sidebar: #101316;
  --surface: #1E2226;
  --surface-2: #262B30;
  --surface-3: #171B1F;
  --line: rgba(255, 255, 255, 0.09);
  --line-soft: rgba(255, 255, 255, 0.06);
  --text: #d7dade;
  --strong: #F2F3F5;   /* --nieve */
  /* ⚠ NO es el `--humo` (#767D87) de la identidad tal cual, y la diferencia está medida:
     sobre el grafito, #767D87 da **4,33:1** y AA pide 4,5 para texto normal. La paleta anterior
     daba 5,94:1, así que aplicar el token a pelo habría cambiado el color Y empeorado la
     legibilidad de golpe — y este panel usa `--muted` para contenido de verdad, no para adornos.

     #858C96 es el mismo tono, dos pasos más claro: **5,30:1**. Es la misma clase de corrección
     que la identidad ya hace consigo misma con los dos grosores de trazo (10,5 en oscuro y 11,5
     en claro): el valor se ajusta para que la PERCEPCIÓN sea la correcta. */
  --muted: #858C96;
  /* ⚠ Los nombres `--blue*` se conservan a propósito: los usan cientos de reglas de este fichero
     y renombrarlos sería un cambio enorme sin ganar nada. Lo que cambia es el VALOR. */
  --blue: #46C7E8;     /* --cian */
  --blue-2: #6BD4EE;
  --blue-soft: rgba(70, 199, 232, 0.14);
  --green: #3ecf8e;
  --green-soft: rgba(62, 207, 142, 0.13);
  --amber: #f5c266;
  --amber-soft: rgba(245, 194, 102, 0.13);
  --red: #f07178;
  --red-soft: rgba(240, 113, 120, 0.13);
  --purple: #a79bfa;
  --radius-lg: 16px;
  --radius: 12px;
  --radius-sm: 10px;
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
  --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.2);

  /* --- ACENTO ---
     El color de ACCIÓN: botón primario, enlace, estado activo, borde del campo con el foco. Con
     nombre propio para que el día que se quiera cambiar se toque aquí y no en doscientas reglas.

     ⚠⚠ `--on-accent` es GRAFITO, no blanco, y es obligatorio: el cian de la marca es claro
     (luminancia 0,48). Blanco encima da 1,98:1 —ilegible— y grafito da 9,21:1. Si alguna vez se
     vuelve a un acento oscuro, esto vuelve a blanco; mientras el acento sea el cian, no. */
  --accent: var(--blue);
  --accent-hover: var(--blue-2);
  --on-accent: #14171A;

  /* Rojo MACIZO para el botón que ejecuta un destrozo. No es --red: ese está pensado para pintar
     TEXTO sobre fondo oscuro (es claro a propósito) y con letras blancas encima se queda en 2,2:1. */
  --red-solid: #b3202c;
  --on-red: #ffffff;

  /* --- SUPERFICIES SEMÁNTICAS --- derivan del tema, no de la escala de grises. */
  --raised: var(--surface-2); /* lo que flota sobre la página */
  --field-bg: var(--surface-3); /* fondo de input/select */
  --nav-hover: rgba(255, 255, 255, 0.06);
  --nav-active: rgba(255, 255, 255, 0.09);
  --zebra: rgba(255, 255, 255, 0.018);
  --overlay: rgba(0, 0, 0, 0.55);
  /* ★ Aeon 96 — aqui vivia `--focus-ring`, definido y NUNCA usado: el anillo real se dibuja con
     `--focus-width`/`--focus-offset` y `--accent` (ver la seccion FOCO). Un token muerto es una
     divergencia esperando: el dia que alguien lo usara, tendriamos dos focos distintos. */

  /* --- TINTES --- el fondo de un badge/aviso, del color de su significado. */
  --tint-green: rgba(62, 207, 142, 0.14);
  --tint-amber: rgba(245, 194, 102, 0.14);
  --tint-red: rgba(240, 113, 120, 0.14);
  --tint-blue: rgba(91, 124, 250, 0.14);
  --tint-red-border: rgba(240, 113, 120, 0.4);

  /* --- TIPOGRAFÍA ---
     Escala corta a propósito: seis tamaños cubren el panel entero y cada uno tiene un trabajo. */
  --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  /* Monoespaciada para lo que se lee carácter a carácter: referencias, tokens, URLs. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --text-xs: 11px; /* etiquetas de columna, pies */
  --text-sm: 12px; /* texto secundario, badges */
  --text-base: 13px; /* cuerpo: es el tamaño histórico del panel y no se toca */
  --text-md: 15px; /* subtítulos, cifras de ficha */
  --text-lg: 18px; /* título de sección */
  --text-xl: 22px; /* título de página */
  --leading-tight: 1.25;
  --leading: 1.5;
  --weight-normal: 400;
  --weight-medium: 500;
  --weight-strong: 600;

  /* --- ICONOS --- el grosor importa tanto como el tamaño: mezclar 1,5 y 2 en la misma pantalla se
     ve como dos familias de iconos distintas. */
  --icon-sm: 14px;
  --icon: 16px;
  --icon-lg: 20px;
  --icon-stroke: 1.75;

  /* --- ESPACIADO --- base de 4. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;

  /* --- RADIOS ---
     Los controles llevan menos radio que los contenedores: una esquina de 12px en un botón de 28px
     de alto lo convierte en una pastilla. */
  --radius-xs: 6px; /* badge, chip, botón pequeño */
  --radius-control: 8px; /* botón, campo, fila de menú */
  --radius-panel: 14px; /* panel de ventana, diálogo */
  --radius-pill: 999px; /* pastilla: badge de estado, contador */

  /* --- ELEVACIÓN POR CAPAS --- cada capa del armazón tiene la suya. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.18);
  --shadow-menu: 0 6px 20px rgba(0, 0, 0, 0.28);
  --shadow-panel: 0 10px 30px rgba(0, 0, 0, 0.32);
  --shadow-overlay: 0 24px 60px rgba(0, 0, 0, 0.45);

  /* --- MOVIMIENTO ---
     ⚠ EL REPARTO DE VENTANAS NO SE ANIMA (regla C4 del armazón de ODTVPannel): una ventana de
     trabajo que se desliza se siente lenta. Esto es para hover, foco y menús. */
  --motion-fast: 90ms;
  --motion: 140ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);

  /* --- FOCO --- un solo anillo para todo lo interactivo. */
  --focus-width: 2px;
  --focus-offset: 1px;

  /* Alto real de la barra superior. Lo mide el JS (ResizeObserver) porque cambia con el ancho:
     de ello cuelgan el `top` y el alto de la ventana lateral. */
  --appbar-h: 60px;
  /* Ancho de la columna de ventanas auxiliares. Lo fija el motor al abrir y al arrastrar. */
  --ventana-ancho: 425px;
}

/* Quien pida menos movimiento no recibe ninguno. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-fast: 0ms;
    --motion: 0ms;
  }
}

* {
  box-sizing: border-box;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.18);
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.28);
}

html {
  min-width: 320px;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font: var(--text-base) / var(--leading) var(--font-sans);
  letter-spacing: 0;
}

/* Cifras de ancho fijo donde se comparan en vertical: contadores, importes y fechas en tabla. Sin
   esto una columna de números baila de fila en fila y hay que leerla dígito a dígito. */
.data-table,
.metric-card strong,
.diagnostic-grid strong,
.pildora-cuenta {
  font-variant-numeric: tabular-nums;
}

/* UN SOLO ANILLO DE FOCO para todo lo interactivo (regla R1 de ODTVPannel). Tres decisiones:
   - `:focus-visible` y no `:focus`, para que el ratón no deje anillos por detrás;
   - `outline` y no `box-shadow`, porque el outline YA sigue el border-radius del elemento y, a
     diferencia de la sombra, no lo recorta un ancestro con `overflow: hidden` — que es justo lo que
     son la tabla y el cuerpo de la ventana lateral;
   - ancho y separación por token, para que sea el MISMO en el panel, en la ventana y en los menús. */
:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
}

[hidden] {
  display: none !important;
}

button,
input,
textarea,
select {
  font: inherit;
}

button {
  border: 0;
}

h1,
h2,
p {
  margin: 0;
}

h1 {
  color: var(--strong);
  font-size: 20px;
  font-weight: 600;
}

h2 {
  color: var(--strong);
  font-size: 15px;
  font-weight: 600;
}

.login-screen {
  align-items: center;
  background:
    radial-gradient(600px 400px at 50% 20%, rgba(91, 124, 250, 0.08), transparent),
    var(--bg);
  display: grid;
  min-height: 100vh;
  padding: 28px;
}

.login-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 20px;
  box-shadow: var(--shadow);
  display: grid;
  gap: 18px;
  justify-self: center;
  max-width: 400px;
  padding: 36px 32px;
  width: 100%;
}

.login-card img {
  border-radius: 50%;
  box-shadow: var(--shadow-soft);
  height: 56px;
  width: 56px;
}

.login-card h1 {
  font-size: 24px;
}

.login-card p {
  color: var(--muted);
}

/* Escritorio: una sola barra superior [logo | cifras | lupa+usuario+salir] y, debajo, TRES
   columnas: el menú lateral, el área de trabajo y la columna de ventanas auxiliares (vacía y sin
   ancho mientras no haya ninguna abierta). Vertical (<=900px): el menú se va abajo, fijo, con scroll
   horizontal.

   ⭐ 2026-09-12 — El menú vuelve a ser lateral, con el formato de XtreamOD (encargo del dueño). Es
   la rejilla de `XTREAMOD/codigo/web/src/styles.css` —232 px · área · auxiliares—, que es además la
   del armazón de ODTVPannel del que ya salían las ventanas.

   El sitio de cada pieza va DECLARADO (grid-column/grid-row) y no por colocación automática: en el
   armazón cuelgan también los <dialog>, y aunque hoy se abren como modales —y por tanto fuera del
   flujo—, no quiero que añadir uno no modal descoloque la rejilla. */
.shell {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  /* Columna con mínimo 0 para que appbar y workspace no crezcan con su contenido (si no, una tabla
     ancha empuja la rejilla más allá del viewport y da scroll horizontal de página). El menú va fijo a
     232 px, como en XtreamOD. */
  grid-template-columns: 232px minmax(0, 1fr) auto;
  min-height: 100vh;
}

.appbar {
  align-items: center;
  background: var(--bg-sidebar);
  border-bottom: 1px solid var(--line-soft);
  display: flex;
  gap: 14px;
  grid-column: 1 / -1;
  grid-row: 1;
  min-height: 60px;
  padding: 8px 20px;
  position: sticky;
  top: 0;
  z-index: 20;
}

.brand {
  align-items: center;
  display: flex;
  flex: 0 0 auto;
}

.appbar-session {
  align-items: center;
  display: flex;
  flex: 0 0 auto;
  gap: 8px;
  margin-left: auto;
  min-width: 0;
}

/* ⭐ 2026-09-12 — LAS CIFRAS DE ARRIBA, como los contadores de la barra de XtreamOD.
   Van en el hueco entre la marca y la sesión, centradas en ese hueco y no en la ventana: XtreamOD
   las centra en la ventana porque su buscador cambia de ancho al escribir, y aquí la sesión no se
   mueve, así que un `flex` no baila y además no puede montarse encima de la sesión en un portátil. */
.cifras {
  align-items: center;
  color: var(--muted);
  display: flex;
  flex: 1 1 auto;
  font-size: var(--text-sm);
  gap: var(--space-5);
  justify-content: center;
  min-width: 0;
  overflow: hidden;
}

.cifra {
  align-items: baseline;
  display: inline-flex;
  gap: 6px;
  white-space: nowrap;
}

.cifra__valor {
  color: var(--strong);
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-strong);
}

/* Tono sólo para lo que es un aviso de verdad: rojo, ámbar y verde son del sistema. */
.cifra--aviso .cifra__valor {
  color: var(--amber);
}

.cifra--mal .cifra__valor {
  color: var(--red);
}

#session-user {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--strong);
  /* min-width:0 quita el suelo de min-content del flex item; max-width le pone techo para que la
     elipsis entre de verdad (si no, un usuario largo empuja la cabecera y da scroll horizontal). */
  max-width: clamp(120px, 22vw, 260px);
  min-width: 0;
  overflow: hidden;
  padding: 7px 14px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.brand img {
  border-radius: 50%;
  box-shadow: var(--shadow-soft);
  width: 36px;
  height: 36px;
}

/* ⭐★★★★★ 2026-09-12 — EL MENÚ LATERAL, con el formato de XtreamOD (§A de su armazón).
   Pegado bajo la barra de arriba y con su propio scroll: al bajar por una tabla de mil clientes el
   menú sigue ahí. */
.nav-bar {
  background: var(--bg-sidebar);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 2px;
  grid-column: 1;
  grid-row: 2;
  height: calc(100vh - var(--appbar-h));
  overflow-y: auto;
  padding: var(--space-4) var(--space-3);
  position: sticky;
  top: var(--appbar-h);
}

/* Una entrada y la cabecera de un grupo son la MISMA fila: una sola regla para las dos. */
.nav,
.nav-grupo__cabecera {
  align-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--radius-control);
  color: var(--text);
  cursor: pointer;
  display: flex;
  flex: 0 0 auto;
  font-size: var(--text-base);
  gap: 10px;
  min-height: 36px;
  padding: var(--space-2) 10px;
  text-align: left;
  width: 100%;
}

.nav-icon {
  align-items: center;
  color: var(--muted);
  display: inline-flex;
}

.nav-label {
  white-space: nowrap;
}

.nav:hover,
.nav-grupo__cabecera:hover {
  background: var(--nav-hover);
  color: var(--strong);
}

.nav:hover .nav-icon,
.nav-grupo__cabecera:hover .nav-icon {
  color: var(--strong);
}

.nav.active {
  background: var(--nav-active);
  color: var(--strong);
  font-weight: 600;
}

.nav.active .nav-icon {
  color: var(--blue-2);
}

.nav-grupo {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* El galón: gira al desplegarse. Es lo único que distingue un grupo de una entrada suelta. */
.nav-galon {
  color: var(--muted);
  display: inline-block;
  margin-left: auto;
  transition: transform var(--motion-fast) var(--ease);
}

.nav-grupo[data-abierto] .nav-galon {
  transform: rotate(90deg);
}

/* Plegado, el grupo sigue diciendo que ahí dentro está la pantalla abierta. */
.nav-grupo:has(.nav.active) > .nav-grupo__cabecera {
  color: var(--strong);
}

/* Un aviso dentro de un grupo plegado no puede quedarse sin ver. */
.nav-grupo:has(.nav-alert) > .nav-grupo__cabecera .nav-icon {
  color: var(--red);
}

/* Los apartados NACEN del grupo pulsado: sangrados y atados a él con una guía vertical. */
.nav-sub {
  border-left: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 0 var(--space-2) 15px;
  padding-left: var(--space-3);
}

.nav-sub .nav {
  font-size: var(--text-sm);
  min-height: 32px;
  padding: var(--space-1) var(--space-2);
}

/* Un grupo del que no se puede ver nada, fuera: sin esto quedaría una cabecera que despliega vacío.
   ⚠ Lo decide el CSS y no quien esconde la entrada, porque las esconden cinco sitios distintos
   (permiso, rol, lo que el owner no ve, la suscripción, los subagentes) y ninguno sabe de grupos. */
.nav-grupo:not(:has(.nav:not([hidden]))) {
  display: none;
}

@media (min-width: 901px) {
  .nav-grupo:not([data-abierto]) > .nav-sub {
    display: none;
  }

  /* Y uno con UNA sola entrada se pinta plano: un desplegable con un único hijo es un clic de más
     y escribe su nombre dos veces seguidas. Es la regla de XtreamOD, «por número de apartados». */
  .nav-grupo:not(:has(.nav:not([hidden]) ~ .nav:not([hidden]))) > .nav-grupo__cabecera {
    display: none;
  }

  .nav-grupo:not(:has(.nav:not([hidden]) ~ .nav:not([hidden]))) > .nav-sub {
    border: 0;
    display: flex;
    margin: 0;
    padding: 0;
  }

  .nav-grupo:not(:has(.nav:not([hidden]) ~ .nav:not([hidden]))) > .nav-sub .nav {
    font-size: var(--text-base);
    min-height: 36px;
    padding: var(--space-2) 10px;
  }
}

/* ⭐ 2026-09-12 — ENTRE 901 Y 1000 PX, EL MENÚ SE PLIEGA A UNA COLUMNA DE ICONOS, como el de XtreamOD
   (`XTREAMOD/codigo/web/src/styles.css`, `@media (max-width: 1000px)`: 64 px y sólo el icono).
   Por debajo de 900 ya va abajo, así que aquí sólo queda ese tramo.

   Lo encontró la revisión midiendo, no mirando: a 950 px y con la ficha de un cliente abierta, al
   área de trabajo le quedaban **343 px**, menos que la propia ficha (365). Con la columna, 521.

   ⚠ El nombre no se pierde: cada entrada y cada cabecera llevan `title`, que es lo que se lee al
     pasar el ratón. Y los apartados de un grupo siguen sangrados, con la guía, pero con la sangría
     justa para que quepan en 64 px. */
@media (min-width: 901px) and (max-width: 1000px) {
  .shell {
    grid-template-columns: 64px minmax(0, 1fr) auto;
  }

  .nav-bar {
    padding: var(--space-3) 6px;
  }

  .nav-label,
  .nav-galon {
    display: none;
  }

  .nav,
  .nav-grupo__cabecera {
    justify-content: center;
    padding: var(--space-2);
  }

  .nav-sub {
    margin-left: 4px;
    padding-left: 4px;
  }

  .nav-icon svg {
    height: var(--icon-lg);
    width: var(--icon-lg);
  }
}

/* Aviso en el menú (agente con reportes sin atender): icono rojo intermitente. */
.nav.nav-alert .nav-icon {
  color: var(--red);
  animation: nav-alert-blink 1s steps(1, end) infinite;
}

@keyframes nav-alert-blink {
  50% {
    opacity: 0.15;
  }
}

/* Sin parpadeo para quien pide reducir movimiento; el rojo fijo sigue avisando. */
@media (prefers-reduced-motion: reduce) {
  .nav.nav-alert .nav-icon {
    animation: none;
  }
}

/* Tic verde / cruz roja (portada, fondo…). */
.bool-yes,
.bool-no {
  align-items: center;
  display: inline-flex;
}

.bool-yes {
  color: var(--green);
}

.bool-no {
  color: var(--red);
}

/* Aeon 49: resaltar en verde DENTRO de un párrafo. `.bool-yes` no vale aquí: es `inline-flex`
   (está pensada para el tic de una tabla) y un trozo de frase en inline-flex no rompe línea. */
.texto-ok {
  color: var(--green);
}

.bool-yes svg,
.bool-no svg {
  display: block;
}

.workspace {
  grid-column: 2;
  grid-row: 2;
  min-width: 0;
  padding-bottom: 16px;
}

/* ==================================================================================================
   VENTANAS AUXILIARES (zona F del armazón de ODTVPannel) — Aeon 52
   --------------------------------------------------------------------------------------------------
   Reglas que vienen del otro panel y que aquí se respetan:

   · C1 · La ventana nunca navega, se DIVIDE. El sitio se lo quita el área de trabajo, que es su
     vecina de fila; la barra superior no se mueve ni un píxel (por eso la zona empieza bajo ella).
   · C4 · NO SE ANIMA. Medido sobre la grabación de referencia: entre dos fotogramas consecutivos la
     ventana pasa de no existir a estar colocada. Animar esto haría que una ventana de trabajo se
     sienta lenta. Lo que sí se anima es el hover y el foco.
   · C5 · Cada ventana lleva su barra de título con dos controles constantes: ampliar y cerrar.
   · Medidas (§4.1): apertura al 22 % del ancho, mínimo 365 px —en píxeles y no en %, o en un portátil
     de 1280 no cabe una tabla— y máximo el 50 % de la ventana. Las aplica el motor en app.js.
   ================================================================================================== */
.ventanas-zona {
  display: flex;
  grid-column: 3;
  grid-row: 2;
  height: calc(100vh - var(--appbar-h));
  min-width: 0;
  /* Se queda pegada bajo la barra: al bajar por una tabla de mil clientes, la ficha sigue ahí. */
  position: sticky;
  top: var(--appbar-h);
  width: var(--ventana-ancho);
  z-index: 10;
}

/* Separador entre el área de trabajo y la columna. Se arrastra para redimensionar. */
.ventanas__separador {
  background: var(--line);
  cursor: col-resize;
  flex: none;
  touch-action: none;
  width: 5px;
}

.ventanas__separador:hover {
  background: var(--accent);
}

.ventanas {
  background: var(--bg);
  border-left: 1px solid var(--line);
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
}

.ventana {
  background: var(--surface);
  box-shadow: var(--shadow-panel);
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  min-height: 0;
}

.ventana__barra {
  align-items: center;
  border-bottom: 1px solid var(--line);
  display: flex;
  flex: none;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
}

.ventana__titulo {
  color: var(--strong);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ventana__hueco {
  flex: 1;
}

/* Los mandos de la barra van pequeños y sin caja, para no competir con el contenido de la ficha.
   Hay que bajarles el `min-height` a mano: lo heredan de la regla general de botón (36 px), y con él
   la barra se iba a 53 px de alto. */
.ventana__barra .icon-button {
  height: 28px;
  min-height: 28px;
  min-width: 28px;
}

/* El cuerpo scrollea POR DENTRO: una ficha larga en una ventana estrecha no puede estirar la página
   ni empujar la columna. */
.ventana__cuerpo {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: var(--space-4);
}

/* --- La ficha, dentro de la ventana ---------------------------------------------------------
   El <article> de la ficha es un `.panel` porque en la pantalla principal tiene que parecer una
   tarjeta. Dentro de la ventana no: la ventana YA es la tarjeta, y una tarjeta dentro de otra son
   dos bordes y dos fondos para un solo contenido. */
.ventana .panel {
  background: transparent;
  border: 0;
  padding: 0;
}

/* Rejillas de la ficha en una columna de ~425 px: cuatro métricas por fila no caben, y dos columnas
   de formulario tampoco. Se reparten solas por ancho, como el `.grid--stats` de ODTVPannel. */
.ventana .metric-grid {
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}

.ventana .split-grid,
.ventana .dashboard-grid {
  grid-template-columns: minmax(0, 1fr);
}

/* Las métricas pasan a ser pares etiqueta/valor, no tarjetones: en la ficha lo que se lee es el
   dato, y a este ancho un mínimo de 92 px de alto por métrica deja la ventana en un acordeón. */
.ventana .metric-card {
  min-height: 0;
  padding: var(--space-3);
}

.ventana .metric-card strong {
  font-size: var(--text-md);
}

.ventana .panel-heading {
  align-items: flex-start;
  flex-wrap: wrap;
}

/* Dentro de la ventana el aspa de la barra ya cierra: un segundo «Cerrar» debajo sobra. Y «Ver en
   ventana» solo tiene sentido cuando la ficha está FUERA de la ventana. */
.ventana .ficha-cerrar,
.ventana .ficha-a-ventana {
  display: none;
}

/* Fuera de la ventana no hay barra, así que el que sobra es el de volver... salvo que se haya
   pulsado «ampliar»: el motor enciende esta clase en el <body> para ofrecer el camino de vuelta. */
.ficha-a-ventana {
  display: none;
}

body.ficha-en-pantalla .ficha-a-ventana {
  display: inline-flex;
  align-items: center;
}

/* La caja se despliega como capa flotante (absoluta) bajo la barra, para NO desplazar el menú al
   abrirla (la appbar es sticky = contexto de posicionamiento). Sin lupa dentro. */
.search-wrap {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: var(--shadow);
  padding: 0 14px;
  position: absolute;
  right: 16px;
  top: calc(100% + 6px);
  width: min(300px, 60vw);
  z-index: 25;
}

.search-wrap input {
  border: 0;
  background: transparent;
  height: 38px;
  padding: 0;
  width: 100%;
}

/* ⭐⭐ Aeon 97 — **EL ANILLO VA EN LA CÁPSULA, NO EN EL `input`.**
   El anillo común (`:focus-visible`, arriba) se dibujaba sobre el `input`, y **el input no es lo que
   se ve**: la forma de este control es `.search-wrap`, una cápsula de `border-radius: 999px`, y el
   input de dentro tiene 8px. Medido en el navegador con la ventana a 1440px y el foco puesto:

     cápsula ..... 300x40, radio 999px
     input ....... 270x38, radio 8px
     anillo ...... 2px + 1px de separación, sobre el input

   O sea, un rectángulo redondeado que **se salía 2px por arriba y por abajo** de la cápsula y se
   quedaba **12px corto por los lados**. Ya hubo un intento a medias aquí mismo: se apagaban
   `box-shadow` y `border` (la decoración genérica de `input:focus`) pero **no el `outline`**.

   ⚠ `:focus-within` y no `:focus`: quien tiene que llevar el anillo es el CONTENEDOR, y el foco
   real lo tiene su input. Y no hace falta `:focus-visible` aquí —ni el `:has()` que eso obligaría a
   usar— porque el navegador **siempre** considera visible el foco de un campo de texto: la regla del
   anillo común (no pintar anillo por un clic de ratón) no aplica a un `input`. */
.search-wrap input:focus,
.search-wrap input:focus-visible {
  box-shadow: none;
  border: 0;
  outline: none;
}

.search-wrap:focus-within {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
}

.icon-button {
  align-items: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--muted);
  cursor: pointer;
  display: inline-flex;
  height: 34px;
  justify-content: center;
  min-width: 34px;
  padding: 0;
}

.icon-button:hover {
  background: rgba(255, 255, 255, 0.07);
  color: var(--strong);
}

/* Los iconos SVG no capturan el clic: el handler global lee dataset del propio botón. */
.icon-button svg,
.lock-icon svg {
  display: block;
  pointer-events: none;
}

.session-bar {
  padding: 20px 28px 12px;
}

.session-bar p,
.panel-heading p,
.metric-card small,
.connection-status span {
  color: var(--muted);
}

input,
textarea,
select {
  background: var(--field-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  color: var(--text);
  min-height: 36px;
  padding: 8px 12px;
  width: 100%;
  transition: border-color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}

/* Además del anillo común de `:focus-visible`, el borde se tiñe de acento: en un formulario largo
   eso es lo que distingue el campo activo del resto sin tener que buscar el anillo.
   (Aquí había un `outline: none` que se cargaba el anillo de foco de todo el panel.) */
input:focus,
textarea:focus,
select:focus {
  border-color: var(--accent);
}

textarea {
  min-height: 76px;
  resize: vertical;
}

/* ⛔ 2026-09-13 — Un número o una fecha no necesitan media pantalla. «Dominio visible» (un 1),
   «¿Aún estás ahí?» en minutos o el año de los Estrenos salían en cajas de 550 a 1.000 px, y las
   fechas de «Aparece» y «Desaparece», de 550. */
input[type="number"] {
  max-width: 14rem;
}

input[type="date"],
input[type="datetime-local"] {
  max-width: 17rem;
}

/* Y en un campo-y-botón con número, el botón va pegado al campo y no al final de la fila. */
.campo-y-boton:has(input[type="number"]) {
  grid-template-columns: minmax(0, 14rem) auto;
  justify-content: start;
}

label {
  color: var(--muted);
  display: grid;
  gap: 5px;
}

button,
.primary,
.secondary,
.ghost,
.link-button,
.segment {
  border-radius: var(--radius-control);
  cursor: pointer;
  min-height: 36px;
  padding: 8px 16px;
  white-space: nowrap;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease);
}

button,
.primary {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: var(--weight-medium);
}

button:hover,
.primary:hover {
  background: var(--accent-hover);
}

/* El secundario de ODTVPannel es un botón RELLENO en gris, no un contorno hueco: en una fila de
   tabla con tres acciones, tres contornos se leen como un marco y ninguno como un botón. */
.secondary {
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--strong);
  font-weight: var(--weight-medium);
}

.secondary:hover {
  background: var(--surface-3);
}

.ghost:hover {
  background: var(--nav-hover);
}

.ghost {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--strong);
}

.link-button {
  background: transparent;
  color: var(--accent);
  min-height: 30px;
  padding: 5px 8px;
}

.link-button:hover {
  background: var(--nav-hover);
}

/* Botón que EJECUTA un destrozo. Rojo macizo y con el mismo peso que el primario, porque es la
   acción principal de ese diálogo. Ver el porqué de --red-solid en los tokens. */
.danger {
  background: var(--red-solid);
  border: 1px solid var(--red-solid);
  color: var(--on-red);
  font-weight: var(--weight-medium);
}

.danger:hover {
  filter: brightness(1.1);
  background: var(--red-solid);
}

/* El que LLEVA a ese diálogo desde una fila: solo contorno. Si cada fila tuviera un botón rojo
   macizo, la tabla entera parecería una alarma y ninguno destacaría. */
.secondary.danger-plain {
  background: transparent;
  border-color: var(--tint-red-border);
  color: var(--red);
}

.secondary.danger-plain:hover {
  background: var(--tint-red);
}

.status {
  background: var(--green-soft);
  border: 1px solid rgba(62, 207, 142, 0.25);
  border-radius: var(--radius);
  margin: 0 28px 12px;
  padding: 10px 14px;
}

/* ⚠ 2026-09-13 — Los avisos que viven FUERA de las vistas (el de la IP nueva y el de los términos)
   iban de borde a borde del área de trabajo, pegados al menú, mientras todo lo de debajo empieza a
   28 px. Mismo margen que `.status`, que es el otro que vive ahí. */
.workspace > .notice {
  margin: 0 28px 16px;
}

.view {
  display: none;
  padding: 0 28px;
}

.view.active {
  display: grid;
  gap: 16px;
}

.notice {
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  display: flex;
  gap: 10px;
  padding: 12px 16px;
}

.notice.success {
  background: var(--green-soft);
  border-color: rgba(62, 207, 142, 0.25);
}

/* ⚠ 2026-09-13 — El aviso de la IP nueva y el de los términos llevaban `warning` desde que se
   escribieron y la clase no tenía regla: salían igual que cualquier nota. Son avisos, y el ámbar es
   del sistema para eso. Va en el borde y no de fondo, para que no compita con el texto. */
.notice.warning {
  border-color: var(--amber);
  box-shadow: inset 3px 0 0 var(--amber);
}

.notice strong {
  color: var(--strong);
}

/* El botón de un aviso («Entendido», «Los he leído…») va al final de la fila, como el «Fix now» del
   aviso de MaxPlayer: pegado al texto se leía como una palabra más de la frase. */
.notice > button {
  margin-left: auto;
}

/* ⚠ 2026-09-13 — Junto a un botón, la insignia de urgencia se estiraba a su alto (37 px) y «Revisar»
   se leía como otro botón al lado de «Conectar». Una insignia es un rótulo: va a su tamaño. */
.panel-heading .button-row > .badge {
  align-self: center;
}

/* ⛔★★★★★ FASE AEON 7 — los enlaces del aviso de términos. Van juntos y separados del texto para
   que se vea que son DOS documentos: los términos y el descargo de contenido. Uno solo se lee como
   «el legal ese», y el descargo es justamente el que dice lo que AEON no hace. */
.acciones-de-terminos {
  display: inline-flex;
  gap: 12px;
  white-space: nowrap;
}

.acciones-de-terminos a {
  color: var(--cian, #46c7e8);
}

/* En pantalla estrecha el aviso lleva cuatro cosas y no caben en una fila. */
@media (max-width: 720px) {
  #terminos-notice {
    align-items: flex-start;
    flex-wrap: wrap;
  }
}

.notice span {
  color: var(--text);
}

.metric-grid {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

/* Tarjeta de metrica pulsable: es un <button>, hay que deshacer el estilo global de boton. */
.metric-card.clickable {
  background: var(--surface);
  cursor: pointer;
  font: inherit;
  font-weight: 400;
  padding: 16px;
  text-align: left;
  white-space: normal;
}

.metric-card.clickable:hover {
  background: rgba(255, 255, 255, 0.04);
  border-color: var(--accent);
}

.metric-card.tone-green strong {
  color: var(--green);
}

.metric-card.tone-amber strong {
  color: var(--amber);
}

.metric-card.tone-red strong {
  color: var(--red);
}


/* La tarjeta de ODTVPannel es PLANA: borde de un píxel, esquina de 12 y nada de sombra. La sombra
   suave que llevaban aquí sumaba una capa que no existe en el otro panel — y con doce tarjetas en
   pantalla, doce sombras son suciedad, no relieve. */
.metric-card,
.panel,
.domain-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.metric-card,
.panel {
  position: relative;
}

.metric-card {
  display: grid;
  gap: 5px;
  min-height: 92px;
  padding: 16px;
}

.metric-card span {
  color: var(--muted);
}

.metric-card strong {
  color: var(--strong);
  font-size: 22px;
  line-height: 1.15;
  overflow-wrap: anywhere;
}

.panel {
  min-width: 0;
  padding: 18px;
}

.panel-heading {
  align-items: center;
  display: flex;
  gap: 10px;
  justify-content: space-between;
  margin-bottom: 14px;
}

.panel-heading div {
  min-width: 0;
}

/* ⚠ 2026-09-13 — El título de un panel se queda el sitio que sobra, y lo de al lado, el suyo. Un
   desplegable ancho junto al título estrujaba el subtítulo en una columna de 120 px («Mínimo /
   soportado y / forzado…», en «Ajuste avanzado»). Y si no caben, los controles bajan de línea. */
.panel-heading {
  flex-wrap: wrap;
}

.panel-heading > div:first-child {
  flex: 1 1 16rem;
}

/* ⚠ 2026-09-13 — Si el de la izquierda no es para quien mira (los destacados, para el owner), el
   de la derecha ocupa la fila: la actividad del owner iba a media pantalla con la otra mitad vacía. */
.dashboard-grid > .panel[hidden] + .panel:not([hidden]) {
  grid-column: 1 / -1;
}

.dashboard-grid,
.split-grid {
  /* ⚠ 2026-09-13 — Cada cajón a su alto. Estirados, «Canales destacados» medía 552 px para decir
     «Todavía no hay ningún canal medido», y la lista de Avisos, lo que el formulario de al lado. */
  align-items: start;
  display: grid;
  gap: 14px;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

.stack-grid {
  display: grid;
  gap: 14px;
  grid-template-columns: minmax(0, 1fr);
}

.soft-divider {
  border: 0;
  border-top: 1px solid var(--line-soft);
  margin: 4px 0;
}

.schedule-input {
  width: auto;
  min-width: 190px;
  min-height: 32px;
  padding: 4px 8px;
}

.split-grid.wide {
  grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.85fr);
}

#provider .split-grid.wide {
  grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
}

#epg.view.active {
  align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

#epg .split-grid {
  grid-column: 1 / -1;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}


/* Buscador propio de TMDB, arriba a la derecha de las pestañas. */


/* Fuente EPG: icono, título/URL, estado y botón "Eliminar" en la MISMA fila (antes el botón
   caía debajo). "Activado" se sube al inicio de la fila. */

.epg-source-item .badge {
  align-self: start;
}

.tmdb-tab-panel {
  display: none;
}

.tmdb-tab-panel.active {
  display: grid;
  gap: 14px;
}

#agent-form {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* start: los campos no se estiran para igualar la fila mas alta (Rol/ID XUI se veian enormes). */
  align-items: start;
}

.agent-add-form {
  display: grid;
  gap: 14px;
  padding: 18px;
  margin-bottom: 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* Fila que ocupa las dos columnas del formulario con 3 campos iguales (Rol, ID XUI, Dominios).
   Especificidad con .agent-add-form para ganar a la regla general de .form-row (que va después). */
.agent-add-form .form-row-full {
  grid-column: 1 / -1;
}

.agent-add-form .form-row-3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* --- Vista Servidor (FASE 65) --------------------------------------------------------------
   Grafica de barras del consumo diario, en HTML y CSS puros: la CSP no deja cargar librerias
   externas, y para una barra por dia no hace ninguna falta. La ALTURA de cada barra la fija el JS
   por CSSOM (no con style="..." en el HTML, que la CSP bloquea desde la FASE 59). */

.bandwidth-chart {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  min-height: 160px;
  padding: 8px 4px 0;
  overflow-x: auto;
}

.bandwidth-bar {
  /* Con `flex: 1 0 14px` un solo dia se estiraba hasta ocupar el panel entero: una mancha verde de
     900 px que no parecia una grafica (FASE 80). Ahora la barra crece hasta 48 px y ya esta. */
  flex: 1 0 14px;
  max-width: 48px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  height: 150px;
  gap: 4px;
}

.bandwidth-bar span {
  width: 100%;
  min-height: 2px;
  border-radius: 3px 3px 0 0;
  background: var(--green);
}

.bandwidth-bar small {
  color: var(--muted);
  font-size: 10px;
}

/* Grafica de meses: los medidos en verde, el mes en curso a medias y los PREVISTOS en hueco, para
   que no se confundan con datos reales. */
.bandwidth-chart.months {
  position: relative;
  gap: 6px;
}

.bandwidth-bar.actual span {
  background: var(--amber);
}

.bandwidth-bar.parcial span {
  background: repeating-linear-gradient(45deg, var(--muted), var(--muted) 3px, transparent 3px, transparent 6px);
}

.bandwidth-bar.previsto span {
  background: transparent;
  border: 1px dashed var(--muted);
}

/* Linea de la cuota contratada: la altura la fija el JS en la variable --cuota (por CSSOM, que la
   CSP si permite; un style="..." en el HTML estaria bloqueado). */
.bandwidth-chart.con-cuota::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(16px + var(--cuota, 0px));
  border-top: 1px dashed var(--red);
  pointer-events: none;
}

.quota-label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  color: var(--muted);
}

.quota-label input {
  width: 130px;
}

/* Fila de "senal" de la tabla Cuando ampliar: nombre + veredicto.
   OJO con el nombre de la clase: se llamaba `.signal` y chocaba con el indicador REDONDO de
   Conexion XUI (`.signal`, 44x44 con border-radius 50%), que esta mas abajo en este mismo fichero.
   Como aquella no declara `display` ni tamaño, cada fila se convertia en un circulo de 44 px con
   el texto desbordandose sobre la fila siguiente: la pantalla quedaba ilegible (FASE 80). */
.table-row.senal {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
}

.pill {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  border: 1px solid var(--line);
}

.pill.bien {
  color: var(--green);
  border-color: var(--green);
  background: var(--green-soft);
}

.pill.regular {
  color: var(--amber);
  border-color: var(--amber);
}

.pill.mal {
  color: var(--red);
  border-color: var(--red);
}

/* --- Senal de actividad -------------------------------------------------------------------
   El panel no decia nada mientras trabajaba. api() enciende estas tres cosas: la barra de arriba,
   el cursor de la pagina y el boton que se acaba de pulsar. */

.actividad {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 9999;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}

.actividad::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 40%;
  background: var(--green);
  animation: actividad-recorre 1.1s ease-in-out infinite;
}

@keyframes actividad-recorre {
  0% { left: -40%; }
  100% { left: 100%; }
}

body.ocupado {
  cursor: progress;
}

/* El boton pulsado ensena que esta trabajando: se apaga el texto y sale el logotipo girando.
   ★★★★★ AEON — aqui habia `loading_duck.webp`, el pato de OmniDucks, un WebP animado.
   `aeon-cargando.svg` lleva la animacion dentro (y respeta `prefers-reduced-motion`), asi que
   sigue sin hacer falta animacion CSS aqui — y ademas es vectorial: a 22 px no se ve dentado. */
button.trabajando {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}

button.trabajando > * {
  visibility: hidden;
}

button.trabajando::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  background: url("/admin/aeon-cargando.svg") center / contain no-repeat;
}

/* Espera larga (mas de 600 ms): el logotipo a tamano de verdad, centrado sobre el panel. Es
   donde luce, al contrario que en un boton de 22px. La capa NO bloquea el raton a proposito: informa,
   no secuestra la pantalla. */
.cargando-aeon {
  position: fixed;
  inset: 0;
  z-index: 9998;
  display: grid;
  place-items: center;
  pointer-events: none;
  background: rgba(18, 18, 18, 0.45);
  animation: cargando-aparece 0.18s ease-out;
}

.cargando-aeon img {
  width: 128px;
  height: 128px;
}

.cargando-aeon span {
  display: block;
  margin-top: 10px;
  text-align: center;
  color: var(--text);
  font-size: 13px;
}

@keyframes cargando-aparece {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* Quien prefiera no ver animaciones: se queda la barra, quieta, y sin la capa de carga (un SVG
   animado no se puede parar desde CSS). */
@media (prefers-reduced-motion: reduce) {
  .actividad::after {
    animation: none;
    left: 0;
    width: 100%;
  }
  .cargando-aeon {
    display: none;
  }
}

/* Campo con una accion al lado (p. ej. "URL de canales destacados" + Probar). */
.field-with-action {
  display: flex;
  /* ⛔ 2026-09-13 — `flex-end`, no `stretch`: con `stretch` el botón tomaba el alto de la etiqueta
     ENTERA (rótulo + campo) y salía de 58 px junto a un campo de 36 — el «Guardar» de la credencial
     de TMDB, el del token de GitHub y el del enlace del servidor. Va a la altura del campo. */
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 6px;
}

/* ⛔ 2026-09-13 — Y un campo de contraseña no hereda los 130 px de los números de la cuota: la
   credencial de TMDB salía en una caja de 130 px para un token v4 de doscientos caracteres. */
.field-with-action > .quota-label:has(.password-field) {
  flex: 1 1 18rem;
}

.field-with-action > .quota-label:has(.password-field) input {
  width: 100%;
}

.field-with-action input {
  flex: 1;
  min-width: 0;
}

.field-with-action button {
  flex: 0 0 auto;
  padding: 0 12px;
  font-size: 12px;
}

.password-field {
  display: flex;
  align-items: stretch;
  gap: 6px;
}

.password-field input {
  flex: 1;
  min-width: 0;
}

.password-toggle {
  flex: 0 0 auto;
  padding: 0 12px;
  font-size: 12px;
  cursor: pointer;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.password-toggle:hover {
  color: var(--strong);
  border-color: var(--blue);
}

.form-error {
  color: var(--strong);
  font-size: var(--text-base);
  padding: 10px 12px;
  border: 1px solid var(--tint-red-border);
  border-radius: var(--radius-control);
  background: var(--tint-red);
}


.form-panel {
  display: grid;
  gap: 10px;
  align-content: start;
}

.compact-form {
  display: grid;
  /* FASE 95: misma razon que en `.connection-panel`. Aqui el que empujaba era un <code> con una URL
     de ejemplo sin espacios (357 px): sin el `minmax(0, ...)` la columna se dimensiona por ese
     minimo de contenido y el formulario se sale de su tarjeta. */
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  margin-bottom: 14px;
}

/* Una URL de ejemplo no tiene espacios por donde partir: sin esto, cualquier `code` largo fija el
   ancho minimo de su columna y arrastra a toda la pantalla en movil. */
code {
  overflow-wrap: anywhere;
}

.form-row {
  /* ⚠ 2026-09-13 — Cada campo a su alto. Estirados al del de al lado, el que llevaba una pista debajo
     o unos presets empujaba el campo de su pareja hacia abajo, y los dos de la fila no casaban. */
  align-items: start;
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* ⛔ 2026-09-13 — UN campo y su botón, en la misma fila y el botón a su tamaño. Estos formularios iban
   en `.form-row`, dos columnas iguales, y el botón se quedaba la segunda entera: en «Funciones» había
   un «Guardar» de 570 × 100 px al lado de las categorías de radio. `.con-boton` es lo mismo con dos
   campos delante (el reproductor del parque, los tokens de la API). */
.campo-y-boton {
  align-items: end;
  display: grid;
  gap: 10px;
  grid-template-columns: minmax(0, 1fr) auto;
}

.form-row.con-boton {
  align-items: end;
  grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
}

/* ⚠ 2026-09-13 — Tres campos, tres columnas: en dos, el tercero bajaba solo a otra fila
   (Servidor MySQL · Puerto / SSL). */
.form-row.form-row-3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

@media (max-width: 720px) {
  .form-row.form-row-3 {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ⚠ 2026-09-13 — El botón de un formulario va a su tamaño, a la izquierda. «Guardar ajustes»,
   «Publicar» o «Subir APK» iban de borde a borde de su cajón (1.100 px) porque el formulario es una
   rejilla y un hijo de rejilla se estira; los demás botones del panel no. */
.form-panel > button[type="submit"] {
  justify-self: start;
}

@media (max-width: 720px) {
  .campo-y-boton,
  .form-row.con-boton {
    grid-template-columns: minmax(0, 1fr);
  }

  .campo-y-boton > button,
  .form-row.con-boton > button {
    justify-self: start;
  }
}

/* Las dos clases que `index.html` usaba sin regla (`test_ninguna_clase_sin_estilo.py`). */
.hint {
  color: var(--muted);
  font-size: var(--text-sm);
  margin: 0;
}

.rating-block {
  display: grid;
  gap: 10px;
}

.rating-block > h3 {
  margin: 0;
}

/* ⚠ 2026-09-13 — El rótulo de un aviso en una línea («Accesos desde una IP nueva»): se partía en dos
   porque la frase de al lado se llevaba el sitio. En pantalla estrecha sí puede partirse. */
@media (min-width: 900px) {
  .notice > strong {
    flex: 0 0 auto;
    white-space: nowrap;
  }
}

/* ★★★★★ FASE AEON 2 — la casilla de «puede tener subagentes», al lado de su desplegable.
   Las etiquetas de `.form-row` son columnas apiladas (rotulo arriba, campo debajo); una casilla
   necesita lo contrario: el rotulo AL LADO y a la altura del campo de su izquierda, no flotando
   arriba con un hueco debajo. */
.check-inline {
  display: flex;
  align-items: center;
  gap: 8px;
  align-self: end;
  padding-bottom: 10px;
}

.check-inline input {
  width: auto;
  margin: 0;
}

.switches {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.switches label,
.inline-check {
  align-items: center;
  background: var(--surface-3);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  color: var(--text);
  display: flex;
  gap: 8px;
  min-height: 36px;
  padding: 8px 10px;
}

.switches input,
.inline-check input {
  width: auto;
}

.data-table {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* Scroll horizontal cuando una tabla ancha (p. ej. Series con 10 columnas) no cabe, en vez de
     recortar la última columna sin manera de alcanzarla en pantallas medianas. */
  overflow-x: auto;
  overflow-y: hidden;
}

.table-row {
  align-items: center;
  background: var(--surface);
  border-bottom: 1px solid var(--line-soft);
  display: grid;
  gap: 10px;
  min-height: 44px;
  padding: 9px 14px;
}

.table-row:last-child {
  border-bottom: 0;
}

/* Cabecera al modo de ODTVPannel: más hundida que las filas (--surface-3, no --surface-2) y en peso
   medio. Con negrita y fondo más claro competía con los datos, que es lo que hay que leer. */
.table-row.header {
  background: var(--surface-3);
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.04em;
  min-height: 34px;
  text-transform: uppercase;
}

/* Rayado y resaltado al pasar por encima: en una tabla de seis columnas es lo que impide saltar de
   fila al recorrerla con la vista. No hay cabecera pegajosa a propósito — `.data-table` scrollea en
   horizontal, y eso convierte al contenedor en el ancestro de scroll, donde `sticky` no se agarra
   a nada. */
/* El rayado se pinta como capa sobre el color de la fila (que ya es --surface), no cambiándolo: así
   el `:hover` de abajo solo tiene que tocar el color y el rayado sigue donde estaba. */
.data-table .table-row:not(.header):nth-child(even) {
  background-image: linear-gradient(var(--zebra), var(--zebra));
}

.data-table .table-row:not(.header):hover {
  background-color: var(--surface-2);
}

/* Centrado del contenido de cada columna respecto a su cabecera. La PRIMERA columna se queda a la
   izquierda (cabecera y dato); el resto se centra (cabecera y dato), de modo que siempre casan
   aunque la columna principal .cell-main no sea la primera (audit, telemetría, hero). */
.data-table .table-row > div {
  justify-self: center;
  text-align: center;
}

.data-table .table-row > div:first-child {
  justify-self: start;
  text-align: left;
}

/* Botones/badges dentro de una celda centrada. */
.data-table .table-row .button-row {
  justify-content: center;
}

/* ⚠ Ocho columnas desde el cupo (FASE AEON 7). Si se añade una y no se toca esta línea, la fila
   se descuadra entera y la última columna se sale: el grid reparte por lo que diga aquí, no por
   cuántos `<div>` haya dentro. */
/* ⚠ 2026-09-13 — «Agente» y su nota («admite subagentes») iban en la misma línea sin separación y se
   leían «Agenteadmite subagentes». La nota va debajo, como el agente debajo del cliente. */
.celda-rol small {
  display: block;
}

.table-row.agents {
  grid-template-columns: minmax(130px, 1.1fr) minmax(80px, 0.55fr) minmax(80px, 0.5fr) minmax(70px, 0.5fr) minmax(90px, 0.55fr) minmax(70px, 0.5fr) minmax(90px, 0.6fr) minmax(170px, 0.9fr);
}

/* ⚠ 2026-09-13 — Sin las dos columnas del XUI, que salían enteras de guiones sin MySQL. */
.table-row.agents.sin-xui {
  grid-template-columns: minmax(150px, 1.3fr) minmax(90px, 0.6fr) minmax(80px, 0.5fr) minmax(80px, 0.5fr) minmax(90px, 0.6fr) minmax(170px, 0.9fr);
}

/* ⭐ 2026-09-13 — «Editar» a la vista y lo demás en «Más». Ver `menuDeFila` en app.js. */
.acciones-de-fila {
  align-items: center;
  display: inline-flex;
  gap: 6px;
}

.acciones-de-fila > button,
.menu-de-fila > summary {
  min-height: 32px;
  padding: 6px 12px;
}

.menu-de-fila > summary {
  align-items: center;
  border-radius: var(--radius-control);
  cursor: pointer;
  display: inline-flex;
  font-weight: var(--weight-medium);
  gap: 6px;
  list-style: none;
}

.menu-de-fila > summary::-webkit-details-marker {
  display: none;
}

.menu-de-fila > summary::after {
  color: var(--muted);
  content: "▾";
  font-size: var(--text-xs);
}

.menu-de-fila__lista {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  display: grid;
  gap: 2px;
  min-width: 200px;
  padding: 6px;
  position: fixed;
  z-index: 40;
}

.data-table .menu-de-fila__lista > button,
.menu-de-fila__lista > button {
  background: transparent;
  border: 0;
  color: var(--text);
  justify-self: stretch;
  text-align: left;
}

.menu-de-fila__lista > button:hover,
.menu-de-fila__lista > button:focus-visible {
  background: var(--surface-3);
}

/* Lo que destruye, al final, separado y en rojo: como el «Delete» del menú de MaxPlayer. */
.menu-de-fila__lista > button.peligro {
  border-top: 1px solid var(--line-soft);
  border-radius: 0 0 var(--radius-control) var(--radius-control);
  color: var(--red);
  margin-top: 4px;
}

/* ★★★★★ FASE AEON 3 — la tabla de subagentes del panel del agente. Tres columnas y no siete: el
   agente no lleva el ID de XUI ni los dominios de nadie, y borrar es del proveedor. */
/* ★★★★★ FASE AEON 5 — las funciones del reproductor. */
.funciones-lista {
  display: grid;
  gap: 12px;
  margin-top: 12px;
  /* Dos columnas donde caben; una en cuanto no. Son doce tarjetas y en una sola columna la
     pantalla se hace larguísima. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
}

.funcion {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
}

.funcion-cabecera {
  display: flex;
  align-items: center;
  gap: 12px;
  justify-content: space-between;
}

.funcion-cabecera .check-inline {
  padding-bottom: 0;
  align-self: auto;
}

.funcion-que-hace {
  color: var(--muted);
  margin: 8px 0 0;
  font-size: 13px;
}

.requisitos {
  list-style: none;
  padding: 0;
  margin: 10px 0 0;
  font-size: 13px;
}

.requisitos li {
  padding-left: 20px;
  position: relative;
  margin-bottom: 6px;
}

/* El símbolo va con `::before` y no en el texto para que un lector de pantalla lea el requisito y
   no un carácter suelto. El estado lo lleva además la clase. */
.requisito-ok::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--green);
}

.requisito-falta::before {
  content: "!";
  position: absolute;
  left: 0;
  font-weight: 700;
  color: var(--amber);
}

.requisito-falta small {
  display: block;
  color: var(--muted);
  margin-top: 2px;
}

/* ⭐ 2026-09-12 — Las dos piezas de «elimina tanto texto explicativo» (encargo del dueño), como en
   MaxPlayer: la PISTA va pegada al control y cabe en una línea; la AYUDA va plegada, y la abre quien
   la necesita. Ver `docs-aeon/COMO-LO-HACE-MAXPLAYER.md` §3.2 y §3.3. */
.pista {
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
  margin: 0;
}

.ayuda {
  color: var(--muted);
  font-size: var(--text-sm);
  margin-top: var(--space-2);
}

.ayuda > summary {
  cursor: pointer;
  width: fit-content;
}

.ayuda > summary:hover {
  color: var(--strong);
}

.ayuda[open] > summary {
  margin-bottom: var(--space-2);
}

.ayuda p {
  margin: 0 0 var(--space-2);
}

.funcion-estado {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: flex-end;
}

.funciones-aviso {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  grid-column: 1 / -1;
  justify-content: space-between;
  margin: 0;
}

.funciones-lista > .pista {
  grid-column: 1 / -1;
}

/* Un campo con sus valores habituales debajo (los presets de «Ajustes»). */
.campo-con-presets {
  display: grid;
  gap: 6px;
}

.campo-con-presets .pildoras {
  margin: 0;
}

/* ★★★★★ FASE AEON 7 — los desplegables de motor dentro de una fila de botones: sin esto la
   etiqueta y el select se separan y la fila se lee mal. */
.campo-en-linea {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--muted);
}
.campo-en-linea select { min-width: 0; }

/* ★★★★★ FASE AEON 5 — la fuente de carátulas: la pone el proveedor y responde de ella.
   Mismo tratamiento que el aviso ámbar de arriba, porque es de la misma familia: algo que el
   operador tiene que leer ANTES de rellenar el campo, no una nota al pie. */
.aviso-responsabilidad {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-left: 2px solid var(--amber);
  background: var(--amber-soft);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text);
}

/* ★★★★★ FASE AEON 3 — los inquilinos, en el panel del owner. */
.table-row.providers {
  /* ⛔ FASE AEON 7: siete columnas. El código el segundo —es lo primero que el owner copia— y el
     paquete el tercero, que con el «a convenir» es lo que hay que ver de un vistazo. */
  grid-template-columns: minmax(140px, 1.3fr) minmax(95px, 0.6fr) minmax(110px, 0.8fr) minmax(110px, 0.8fr) minmax(85px, 0.5fr) minmax(75px, 0.4fr) minmax(205px, 1fr);
}

/* ── La escalera de paquetes ───────────────────────────────────────────────────────────────
 *
 * ⛔ 2026-09-12. Se mira dónde estás, no se lee. Por eso el paquete propio va marcado con **borde
 *   y fondo además de la etiqueta**: el apartado 2 de la identidad prohíbe decir estado sólo con
 *   color, y aquí el estado es «éste es el tuyo».
 */
.rejilla-paquetes {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}

.paquete-tarjeta {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

.paquete-tarjeta.es-el-mio {
  border-color: var(--accent);
  background: var(--surface-2);
}

.paquete-tarjeta .precio {
  font-size: 1.25rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.paquete-tarjeta .badge {
  align-self: flex-start;
  margin-top: 0.25rem;
}

/* ⛔ 2026-09-12 — Las dos de la API del proveedor. Una tabla nueva **no hereda rejilla de ninguna
   parte**: sin esto la cabecera se apila en vertical, que es el mismo aviso que dejó escrito la
   de planes justo debajo. */
.table-row.api-tokens {
  grid-template-columns: minmax(140px, 1.4fr) minmax(120px, 0.9fr) minmax(70px, 0.4fr) minmax(130px, 0.9fr) minmax(90px, 0.5fr) minmax(100px, 0.6fr);
}

.table-row.api-requests {
  /* La ruta es la que más crece: va como patrón (`/clientes/{referencia}`) y no como URL. */
  grid-template-columns: minmax(130px, 0.9fr) minmax(70px, 0.4fr) minmax(180px, 1.8fr) minmax(70px, 0.4fr) minmax(90px, 0.5fr) minmax(110px, 0.7fr);
}

.table-row.planes {
  /* ⛔ FASE AEON 7: seis columnas. Sin esto la cabecera se apila en vertical — una tabla nueva no
     hereda rejilla de ninguna parte, y el fallo solo se ve mirando la pantalla. */
  grid-template-columns: minmax(170px, 1.6fr) minmax(90px, 0.6fr) minmax(100px, 0.6fr) minmax(110px, 0.8fr) minmax(90px, 0.6fr) minmax(150px, 0.9fr);
}

/* El código de operador: se lee de un vistazo y se copia a mano, así que va en monoespaciada y con
   la letra un punto más grande de lo normal. Se dicta por teléfono más de lo que parece. */
.codigo-operador {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  letter-spacing: .08em;
  background: var(--surface-2, rgba(255, 255, 255, .04));
  /* ⚠ 2026-09-12: decía `var(--border)`, que no existe en `:root`. Una variable inexistente hace
     que el navegador **descarte la declaración entera**, así que este chip llevaba desde que se
     escribió sin borde ninguno — y sin dar el menor aviso. La buena es `--line`. */
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 2px 7px;
  user-select: all;
}

/* El desplegable de estado va dentro de una celda: sin esto hereda el ancho de un campo de
   formulario y desborda la columna. */
.proveedor-estado {
  width: 100%;
  min-width: 0;
}

/* ⛔★★★★★ 2026-09-13 — Lo bloqueado: qué, quién, de qué proveedor, por qué, quién lo puso y la acción. */
.table-row.bloqueados {
  grid-template-columns: minmax(80px, 0.5fr) minmax(140px, 1.1fr) minmax(110px, 0.8fr) minmax(130px, 1fr) minmax(140px, 0.9fr) minmax(120px, 0.6fr);
}

/* Sin la columna del proveedor: a un proveedor sólo le sale lo suyo. */
.table-row.bloqueados.sin-proveedor {
  grid-template-columns: minmax(80px, 0.5fr) minmax(150px, 1.2fr) minmax(140px, 1fr) minmax(140px, 0.9fr) minmax(120px, 0.6fr);
}

/* ⛔★★★★★ 2026-09-13 — El personal de AEON: cuenta, rol, estado y la acción. */
.table-row.personal {
  grid-template-columns: minmax(160px, 1.4fr) minmax(110px, 0.7fr) minmax(100px, 0.6fr) minmax(130px, 0.7fr);
}

/* ⚠ Cuatro columnas desde el cupo (FASE AEON 7): usuario, estado, sus clientes y su cupo. */
.table-row.subagentes {
  grid-template-columns: minmax(160px, 1.6fr) minmax(90px, 0.6fr) minmax(90px, 0.6fr) minmax(90px, 0.6fr);
}

/* Aclaración dentro de un formulario (p. ej. credenciales opcionales con MySQL). */

/* Salud del catálogo (P9c): tablas con explicación y solución por indicador. */
.catalog-health {
  display: grid;
  gap: 12px;
  margin-top: 12px;
}

.catalog-health-note {
  color: var(--muted);
  font-size: 0.85rem;
  margin: 0;
}

.table-row.health {
  grid-template-columns: minmax(160px, 0.9fr) minmax(90px, 0.45fr) minmax(230px, 1.4fr) minmax(230px, 1.4fr);
}

.table-row.health-sample {
  grid-template-columns: minmax(170px, 1fr) minmax(70px, 0.35fr) minmax(80px, 0.4fr) minmax(240px, 1.4fr);
}

/* --- FASE 87: salud del catalogo, de resumen a detalle ------------------------------------------
   Cada fila del resumen abre la pantalla con la lista de afectados de ese indicador. */

/* `.view.active` es un grid sin columnas declaradas, asi que su unica columna se dimensiona por el
   contenido: la tabla de detalle (titulos largos) estiraba la PAGINA entera a 740px en un movil de
   375 y aparecia scroll horizontal. Con minimo 0 la columna no crece, y la tabla usa su propio
   `overflow-x` como estaba pensado. Mismo criterio que la columna del shell. */
#catalog-issues {
  grid-template-columns: minmax(0, 1fr);
}

.table-row.salud-fila {
  cursor: pointer;
}

.table-row.salud-fila:hover {
  background: var(--surface-2);
}

/* ★★ Aeon 96 — ESTA FILA SE SALTABA LAS TRES DECISIONES DEL ANILLO (ver `:focus-visible`, arriba).
   Tenia `outline: none` + `box-shadow: inset`, y ademas compartia regla con `:hover`: el foco de
   teclado quedaba pintado EXACTAMENTE IGUAL que pasar el raton por encima, o sea que quien navega
   con teclado no podia saber donde estaba.
   Ahora usa el mismo `outline` que todo lo demas. El offset va NEGATIVO —y es la unica diferencia—
   porque la fila ocupa el ancho completo de la tabla: por fuera, el anillo lo recortaria el
   contenedor. */
.table-row.salud-fila:focus-visible {
  background: var(--surface-2);
  outline: var(--focus-width) solid var(--accent);
  outline-offset: calc(var(--focus-offset) * -1);
}

.table-row.salud-detalle {
  grid-template-columns: minmax(200px, 1fr) minmax(90px, 0.3fr);
}

/* Las pildoras de navegacion entre indicadores. Clase propia, NO `.pill` (esa es una insignia
   estatica de otro sitio): ver el aviso de colision de clases mas arriba en este fichero. */
.pildoras {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 12px 0 4px;
}

.pildora {
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--text-sm);
  cursor: pointer;
}

.pildora:hover {
  color: var(--strong);
  border-color: var(--accent);
}

/* El filtro elegido se marca con el BORDE de acento, no rellenándolo de azul: un relleno de acento
   es el vocabulario del botón primario, y una pastilla de filtro no ejecuta nada. Es el `.chip--on`
   de ODTVPannel. */
.pildora.activa {
  border-color: var(--accent);
  background: var(--surface-2);
  color: var(--strong);
}

.pildora-cuenta {
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
}

/* Las celdas de texto largo van alineadas a la izquierda (el centrado por defecto de
   .data-table es para valores cortos). */
.data-table .table-row > div.cell-wrap {
  justify-self: start;
  text-align: left;
  font-size: 0.85rem;
  color: var(--muted);
}

/* ★ Aeon 41: una columna mas ("Accion", el boton de la ficha). El numero de columnas del grid
   tiene que casar con el de celdas o la fila entera se descuadra. */
/* ★ FASE 126 (Aeon 100): una columna mas, «Ultima conexion», detras de Caducidad. El ancho sale
   del texto mas largo que puede llevar («hace 11 meses»), no de una fraccion a ojo. */
.table-row.agent-users {
  grid-template-columns: minmax(130px, 1fr) minmax(80px, 0.5fr) minmax(100px, 0.6fr) minmax(105px, 0.65fr) minmax(80px, 0.5fr) minmax(220px, 1.2fr) minmax(80px, 0.5fr);
}

.agent-domain-cell {
  display: grid;
  gap: 5px;
}

/* ⚠ 2026-09-13 — El desplegable y «Asignar», en UNA línea: apilados con su «Actual:» encima, cada
   cliente del agente medía 117 px de alto y nueve clientes no cabían en la pantalla. */
.agent-domain-cell .button-row.compact {
  flex-wrap: nowrap;
}

.agent-domain-cell select {
  flex: 1 1 auto;
  min-width: 0;
}

.table-row.subscribers {
  grid-template-columns: minmax(140px, 1.1fr) minmax(140px, 0.95fr) minmax(110px, 0.7fr) minmax(105px, 0.65fr) minmax(90px, 0.55fr) minmax(120px, 0.8fr) minmax(80px, 0.5fr);
}

/* La fila ENTERA abre la ficha (Aeon 52), como en ODTVPannel. El botón «Ficha» se queda porque es
   el camino con teclado: una fila de <div> no se puede tabular. */
.table-row.subscribers:not(.header),
.table-row.agent-users:not(.header) {
  cursor: pointer;
}

.table-row.catalog {
  grid-template-columns: minmax(140px, 1.3fr) minmax(80px, 0.55fr) minmax(110px, 0.8fr) minmax(130px, 0.9fr);
}

.table-row.audit {
  grid-template-columns: minmax(150px, 1fr) minmax(120px, 0.8fr) minmax(100px, 0.7fr) minmax(150px, 1.2fr) minmax(80px, 0.55fr);
}

.table-row.telemetry {
  grid-template-columns: minmax(130px, 0.8fr) minmax(120px, 0.8fr) minmax(180px, 1.4fr) minmax(130px, 0.9fr);
}

.table-row.top {
  grid-template-columns: minmax(140px, 1fr) minmax(100px, 0.6fr) minmax(110px, 0.7fr) minmax(180px, 1.1fr) minmax(130px, 0.8fr);
}

/* FASE 125: una columna mas, el DIA del parte. Va la 2a —justo detras del titulo— porque es lo
   primero que se mira para ordenar un reporte, y se le da el ancho justo de una fecha corta. */
.table-row.reports {
  grid-template-columns: minmax(170px, 1.4fr) minmax(92px, 0.55fr) minmax(105px, 0.7fr) minmax(88px, 0.6fr) minmax(88px, 0.6fr) minmax(78px, 0.5fr) minmax(250px, 1.35fr);
}

.table-row.agent-reports {
  grid-template-columns: minmax(170px, 1.4fr) minmax(92px, 0.55fr) minmax(105px, 0.7fr) minmax(88px, 0.6fr) minmax(78px, 0.5fr) minmax(210px, 1.15fr);
}

.table-row.actions {
  grid-template-columns: minmax(180px, 1.4fr) minmax(130px, 0.8fr) minmax(100px, 0.7fr) minmax(90px, 0.6fr) minmax(130px, 0.8fr);
}

/* ⭐ 2026-09-14 — Los reportes del sistema: la lista del proveedor y la del personal de AEON, con el proveedor delante. */
.table-row.sistema {
  grid-template-columns: minmax(220px, 1.8fr) minmax(130px, 0.8fr) minmax(190px, 1fr) minmax(130px, 0.7fr) minmax(80px, 0.4fr);
}

.table-row.sistema-aeon {
  grid-template-columns: minmax(220px, 1.6fr) minmax(150px, 0.9fr) minmax(130px, 0.8fr) minmax(190px, 1fr) minmax(130px, 0.7fr) minmax(80px, 0.4fr);
}

.sistema-descripcion {
  white-space: pre-wrap;
}

/* ⭐ 2026-09-14 — Los dados de alta en la app, y los nombres libres que se proponen cuando uno ya es de otro. */
.table-row.nombres {
  grid-template-columns: minmax(200px, 1.6fr) minmax(170px, 1fr) minmax(120px, 0.6fr);
}

.alternativas {
  align-items: center;
  margin: 8px 0 0;
}

.alternativas > span {
  color: var(--muted);
  font-size: var(--text-sm);
}

#nombres-en-bloque-resultado {
  margin-top: 12px;
}

.table-row.hero {
  grid-template-columns: 44px minmax(160px, 1.2fr) 72px minmax(140px, 0.9fr) minmax(100px, 0.6fr) minmax(150px, 0.9fr) minmax(80px, 0.5fr) minmax(160px, 0.9fr);
}

.table-row.ratings {
  grid-template-columns: minmax(150px, 1.3fr) minmax(80px, 0.5fr) minmax(90px, 0.55fr) minmax(60px, 0.4fr) minmax(90px, 0.55fr) minmax(120px, 0.8fr);
}

.table-row.tmdb {
  grid-template-columns: minmax(170px, 1.3fr) minmax(80px, 0.5fr) minmax(70px, 0.4fr) minmax(150px, 1fr) minmax(90px, 0.55fr) minmax(120px, 0.8fr);
}

.table-row.tmdb-movie {
  grid-template-columns: minmax(200px, 1.6fr) minmax(110px, 0.8fr) minmax(70px, 0.5fr) minmax(60px, 0.4fr) 68px 60px minmax(88px, 0.55fr) minmax(120px, 0.85fr);
}

.table-row.tmdb-series {
  grid-template-columns: minmax(190px, 1.6fr) minmax(100px, 0.7fr) minmax(70px, 0.45fr) minmax(56px, 0.4fr) minmax(78px, 0.45fr) minmax(78px, 0.45fr) 68px 60px minmax(88px, 0.55fr) minmax(120px, 0.8fr);
}

.cell-main strong,
.stack-item strong {
  color: var(--strong);
  display: block;
  overflow-wrap: anywhere;
}

.cell-main small,
.stack-item small,
.muted {
  color: var(--muted);
}

/* ★★ Aeon 102 — EL CODIGO DEL PARTE dentro de la celda principal.
   Ancho fijo y con aire entre letras porque se DICTA POR TELEFONO y se lee a tres metros de una
   tele: es el remate de la decision de la Aeon 82 de usar un alfabeto sin caracteres confundibles.
   Va como bloque para que no empuje al mensaje, y asi la fila no cambia de alto ni de rejilla. */
.codigo-parte {
  color: var(--strong);
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92em;
  letter-spacing: 0.04em;
  opacity: 0.85;
}

/* Pastilla de estado. Sin color declarado sale gris —y no transparente— para que un estado neutro
   siga pareciendo un estado y no un texto suelto entre columnas. */
.badge {
  align-items: center;
  background: var(--surface-2);
  border-radius: var(--radius-pill);
  color: var(--muted);
  display: inline-flex;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  min-height: 20px;
  padding: 2px 9px;
  width: fit-content;
}

.badge.green {
  background: var(--tint-green);
  color: var(--green);
}

.badge.blue {
  background: var(--tint-blue);
  color: #a3b8ff;
}

.badge.amber {
  background: var(--tint-amber);
  color: var(--amber);
}

.badge.red {
  background: var(--tint-red);
  color: var(--red);
}

.stack-list {
  display: grid;
  gap: 8px;
}

/* ⭐ 2026-09-13 — El buscador y los filtros, encima de la lista a la que se aplican. */
/* El formulario de una línea encima de una tabla (los tokens de la API) no va pegado a ella. */
.panel > .form-row.con-boton {
  margin-bottom: 12px;
}

.filtros-de-lista {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.filtros-de-lista input[type="search"] {
  flex: 1 1 16rem;
  max-width: 24rem;
}

.filtros-de-lista select {
  flex: 0 1 auto;
  min-width: 11rem;
  width: auto;
}

/* ⚠ 2026-09-13 — El Inicio de un nivel sin cifras (el soporte): a dónde ir, en vez de un párrafo. */
.inicio-sin-cifras {
  display: grid;
  grid-column: 1 / -1;
  gap: 10px;
  justify-items: start;
}

.inicio-sin-cifras p {
  color: var(--muted);
  margin: 0;
}

/* Una cifra de una tabla que lleva a su lista: se ve como la insignia de siempre, y se pulsa. */
.boton-cifra {
  border: 0;
  cursor: pointer;
  font: inherit;
  min-height: 0;
  padding: 2px 8px;
}

.boton-cifra:hover,
.boton-cifra:focus-visible {
  text-decoration: underline;
}

/* ★★★ FASE 123 (Aeon 97) — EL PAGINADOR de Clientes, Actualizaciones y los dispositivos de la ficha.
   Encargo suyo del 2026-08-27: «en los clientes debería haber un selector, de que se mostrasen 25 e
   ir pasando páginas». Todo con los tokens de la casa: un paginador es cromo, no un sitio para
   inventarse medidas.

   ⚠ `flex-wrap`: en la ficha vive dentro del cajón lateral, que es estrecho. Sin esto, el selector
   de tamaño se salía por la derecha — y lo que se sale de la caja no se ve, no se «aprieta». */
.paginador {
  align-items: center;
  border-top: 1px solid var(--line-soft);
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: space-between;
  margin-top: var(--space-3);
  padding-top: var(--space-3);
}

.paginador-cuenta strong {
  color: var(--strong);
  font-variant-numeric: tabular-nums;
}

.paginador-controles {
  align-items: center;
  display: inline-flex;
  gap: var(--space-2);
}

/* Ancho fijo para que «1 / 9» y «10 / 40» no muevan las flechas de sitio al pasar de página. */
.paginador-pagina {
  color: var(--strong);
  font-variant-numeric: tabular-nums;
  min-width: 5.5ch;
  text-align: center;
}

.paginador-tamano {
  align-items: center;
  display: inline-flex;
  gap: var(--space-2);
}

.paginador-tamano select {
  min-width: 4.5rem;
}

.button-row.compact {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.button-row.compact button {
  min-height: 32px;
  padding: 6px 10px;
}

.stack-item {
  align-items: center;
  background: var(--surface-3);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  display: grid;
  gap: 10px;
  grid-template-columns: 30px minmax(0, 1fr) auto;
  min-height: 46px;
  padding: 9px 12px;
}

/* ⛔ 2026-09-13 — Una fila de lista SIN icono no hereda la rejilla de tres columnas. Su texto caía en
   la de 30 px reservada al icono y se partía letra a letra: «Alta / sin / conf / igur / ar», cada
   aviso de «Esto necesita a alguien» de 400 px de alto en el Inicio del owner. Lo mismo le pasaba al
   token recién creado de la API. `test_las_cajas_no_se_estrujan.py`. */
.stack-item:not(:has(> .stack-icon)) {
  grid-template-columns: minmax(0, 1fr) auto;
}

.stack-icon {
  align-items: center;
  background: var(--blue-soft);
  border-radius: 50%;
  color: var(--blue-2);
  display: flex;
  flex: 0 0 auto;
  font-weight: 600;
  height: 28px;
  justify-content: center;
  width: 28px;
}

.stack-icon svg {
  display: block;
}

.empty {
  color: var(--muted);
  padding: 12px;
}


/* Modal pequeño (selección de dominios del agente). */
.picker-modal {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-overlay);
  color: var(--text);
  max-width: 460px;
  padding: 0;
  width: min(92vw, 460px);
}

.picker-modal::backdrop {
  background: var(--overlay);
}

.picker-body {
  display: grid;
  gap: 10px;
  max-height: 60vh;
  overflow: auto;
  padding: 16px 20px;
}

.picker-body .switches {
  grid-template-columns: 1fr;
}

.modal-footer {
  border-top: 1px solid var(--line);
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  padding: 14px 20px;
}

.hero-thumb {
  aspect-ratio: 16 / 9;
  border-radius: 8px;
  object-fit: cover;
  width: 64px;
}

.hero-preview {
  background: var(--surface-3);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  display: grid;
  gap: 12px;
  grid-template-columns: 160px minmax(0, 1fr);
  padding: 12px;
}

.hero-preview-media {
  align-items: center;
  aspect-ratio: 16 / 9;
  background: var(--bg-sidebar);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  display: flex;
  justify-content: center;
  overflow: hidden;
}

.hero-preview-media img {
  height: 100%;
  object-fit: cover;
  width: 100%;
}

.hero-preview-media span {
  color: var(--muted);
  font-size: 11px;
}

.hero-preview-text {
  align-content: center;
  display: grid;
  gap: 3px;
  min-width: 0;
}

.hero-preview-text span {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.hero-preview-text strong {
  color: var(--strong);
  font-size: 16px;
  overflow-wrap: anywhere;
}

.hero-preview-text small {
  color: var(--text);
  overflow-wrap: anywhere;
}

/* Marcadores de la previa: se distinguen del contenido real (y ganan al selector de elemento,
   que si no se lleva el color por especificidad). */
.hero-preview-text .preview-placeholder {
  color: var(--muted);
  font-style: italic;
  font-weight: 400;
}


.modal-heading {
  align-items: center;
  border-bottom: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  padding: 16px 20px;
}

.modal-heading h2 {
  font-size: 16px;
  margin: 0;
}

.modal-heading p {
  color: var(--muted);
  margin: 4px 0 0;
}


.domain-grid {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.domain-card {
  align-content: start;
  display: grid;
  gap: 12px;
  min-height: 118px;
  padding: 16px;
}

.domain-card header {
  align-items: center;
  display: grid;
  gap: 10px;
  grid-template-columns: 32px minmax(0, 1fr) auto;
}

.domain-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.domain-card-meta span {
  background: var(--surface-3);
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  color: var(--muted);
  min-width: 0;
  overflow-wrap: anywhere;
  padding: 3px 10px;
}

.domain-card header small {
  overflow-wrap: anywhere;
}

.lock-icon {
  align-items: center;
  background: var(--green-soft);
  border-radius: 50%;
  color: var(--green);
  display: flex;
  font-weight: 600;
  height: 32px;
  justify-content: center;
  width: 32px;
}

.domain-card strong {
  color: var(--strong);
  overflow-wrap: anywhere;
}

.domain-card footer {
  align-items: center;
  border-top: 1px solid var(--line-soft);
  color: var(--muted);
  display: flex;
  gap: 8px;
  justify-content: space-between;
  padding-top: 10px;
}

.domain-card footer > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-combo {
  display: block;
  position: relative;
}

.combo-results {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  display: grid;
  left: 0;
  max-height: 280px;
  overflow: auto;
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 40;
}

.combo-option {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line-soft);
  border-radius: 0;
  color: var(--text);
  display: grid;
  gap: 2px;
  min-height: 0;
  padding: 9px 12px;
  text-align: left;
  white-space: normal;
}

.combo-option:last-child {
  border-bottom: 0;
}

.combo-option:hover,
.combo-option.active,
.combo-option:focus-visible {
  background: rgba(255, 255, 255, 0.06);
}

.combo-option strong {
  color: var(--strong);
}

.combo-option small {
  color: var(--muted);
}

.combo-empty {
  color: var(--muted);
  margin: 0;
  padding: 10px 12px;
}

.segmented {
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  display: flex;
  overflow: hidden;
  padding: 3px;
  gap: 2px;
}

.method-grid {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.method-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft);
  cursor: pointer;
  display: grid;
  gap: 6px;
  /* Es un <button> (selector de método): anula el estilo global de botón. */
  font: inherit;
  font-weight: 400;
  min-height: 92px;
  padding: 16px;
  text-align: left;
  white-space: normal;
}

.method-card:hover {
  /* background explícito: si no, gana el button:hover global (azul sólido). */
  background: var(--surface-2);
  border-color: var(--blue);
}

.domain-mode-field {
  align-items: center;
  display: flex;
  gap: 8px;
}

.domain-mode-field select {
  flex: 1;
  min-width: 0;
}

.domain-mode-field .link-button {
  flex: 0 0 auto;
  white-space: nowrap;
}

.method-card span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.method-card strong {
  color: var(--strong);
  font-size: 17px;
}

.method-card small {
  color: var(--muted);
}

.method-card.active {
  border-color: rgba(62, 207, 142, 0.45);
  background: linear-gradient(180deg, var(--green-soft), transparent 70px), var(--surface);
}

.method-card.pending {
  border-color: rgba(245, 194, 102, 0.45);
  background: linear-gradient(180deg, var(--amber-soft), transparent 70px), var(--surface);
}

.mysql-fields {
  display: grid;
  gap: 10px;
}

.diagnostic-grid {
  background: var(--surface-3);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  padding: 14px;
}

.diagnostic-grid > div {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.diagnostic-grid span,
.diagnostic-grid small {
  color: var(--muted);
}

.diagnostic-grid strong {
  color: var(--strong);
  overflow-wrap: anywhere;
}

.diagnostic-error {
  background: var(--red-soft);
  border: 1px solid rgba(240, 113, 120, 0.32);
  border-radius: var(--radius-sm);
  color: #ffd6da;
  grid-column: 1 / -1;
  padding: 10px;
}

.detected-tables {
  border-top: 1px solid var(--line-soft);
  display: grid !important;
  gap: 6px;
  grid-column: 1 / -1;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  padding-top: 10px;
}

.segment {
  background: transparent;
  border-radius: 999px;
  color: var(--muted);
  min-height: 30px;
  padding: 5px 12px;
}

.segment.active {
  background: var(--blue);
  color: #ffffff;
}

.connection-panel {
  display: grid;
  /* FASE 95: `minmax(0, 1fr)` y no el implicito `auto`. Sin esto la columna se dimensiona por el
     contenido mas ancho —la rejilla de diagnostico MySQL, que pide 360 px— y la tarjeta crece por
     encima de su hueco (339 px a 375 de viewport) arrastrando la pagina entera: era la UNICA vista
     del panel que se salia en movil. El `0` del minmax es lo que permite encoger por debajo del
     minimo de contenido. */
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}

.connection-status {
  align-items: center;
  display: flex;
  gap: 12px;
}

.connection-status strong,
.connection-status span {
  display: block;
}

/* «Cambiar conexión» se va a la derecha del estado, como el «Edit panel connection» de MaxPlayer. */
.conexion-cambiar {
  margin-left: auto;
}

#conexion-editor {
  display: grid;
  gap: 12px;
}

.signal {
  align-items: center;
  background: var(--green-soft);
  border: 1px solid rgba(62, 207, 142, 0.3);
  border-radius: 50%;
  color: var(--green);
  display: flex;
  flex: 0 0 auto;
  height: 44px;
  justify-content: center;
  width: 44px;
}

.signal svg {
  display: block;
}

.connection-track {
  background: var(--surface-3);
  border-radius: 999px;
  height: 6px;
  overflow: hidden;
}

.connection-track span {
  background: var(--green);
  display: block;
  height: 100%;
  width: 0;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

@media (max-width: 1200px) {
  .metric-grid,
  .domain-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .split-grid.wide {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 900px) {
  /* Vertical: el nav se va al fondo, fijo y con scroll horizontal. La cabecera queda con logo,
     usuario y salir (el buscador desplegado ocupa una fila completa debajo). */
  .appbar {
    justify-content: space-between;
    padding: 10px 18px;
  }

  /* En vertical no hay sitio para dos columnas: la ventana pasa a ser una capa que tapa el área de
     trabajo (y deja hueco abajo para el nav fijo, que va por encima). El separador desaparece:
     no hay nada que repartir. */
  .shell {
    grid-template-columns: minmax(0, 1fr);
  }

  .ventanas-zona {
    bottom: 0;
    height: auto;
    left: 0;
    position: fixed;
    right: 0;
    top: var(--appbar-h);
    width: auto;
    z-index: 25;
  }

  .ventanas__separador {
    display: none;
  }

  .ventana__cuerpo {
    padding-bottom: 84px;
  }

  .appbar-session {
    justify-content: flex-end;
  }

  /* El buscador flotante ocupa todo el ancho bajo la cabecera (sigue sin desplazar el menú). */
  .appbar-session .search-wrap {
    left: 18px;
    right: 18px;
    width: auto;
  }

  /* El nav sale del flujo (fijo abajo), así que la cabecera solo lleva logo + sesión. */
  .nav-bar {
    background: var(--bg-sidebar);
    border-right: 0;
    border-top: 1px solid var(--line);
    bottom: 0;
    flex: none;
    flex-direction: row;
    height: auto;
    justify-content: flex-start;
    left: 0;
    overflow-x: auto;
    overflow-y: hidden;
    padding: 6px 10px;
    position: fixed;
    right: 0;
    top: auto;
    z-index: 30;
  }

  /* ⚠ En una barra de abajo no hay sitio para desplegar nada: los grupos se aplanan y cada
     apartado vuelve a ser un botón de la fila, que es como estaba antes del menú lateral. */
  .nav-grupo,
  .nav-sub {
    display: contents;
  }

  .nav-grupo__cabecera {
    display: none;
  }

  .nav,
  .nav-sub .nav {
    flex-direction: column;
    font-size: var(--text-base);
    gap: 3px;
    justify-content: center;
    min-height: 36px;
    min-width: 74px;
    padding: 6px 8px;
    width: auto;
  }

  /* Las cifras no caben junto al logo y la sesión en un móvil: se quedan en el Inicio. */
  .cifras {
    display: none;
  }

  .workspace {
    grid-column: 1;
  }

  .nav-label {
    font-size: 10px;
  }

  /* Hueco para que el nav fijo no tape el final de la vista. */
  .workspace {
    padding-bottom: 84px;
  }

  .view,
  .session-bar {
    padding-left: 18px;
    padding-right: 18px;
  }

  .status,
  .workspace > .notice {
    margin-left: 18px;
    margin-right: 18px;
  }

  .metric-grid,
  .dashboard-grid,
  .split-grid,
  .domain-grid,
  .method-grid,
  .diagnostic-grid,
  .form-row,
  .switches {
    grid-template-columns: 1fr;
  }

  /* Overrides con especificidad de ID que si no ganan al media query genérico. */
  #epg.view.active,
  #epg .split-grid,
  #agent-form,
  .agent-add-form .form-row-3 {
    grid-template-columns: 1fr;
  }

  .detected-tables {
    grid-template-columns: 1fr;
  }

  .tmdb-overview,
  .settings-list,
  .hero-preview {
    grid-template-columns: 1fr;
  }

  .data-table {
    overflow-x: auto;
  }

  /* Solo las tablas reales (dentro de .data-table, con scroll horizontal) llevan min-width.
     Las filas de "Cuando ampliar" son dos columnas de texto, no una tabla: con 720px forzados se
     salian del panel y obligaban a hacer scroll horizontal para leer el veredicto (FASE 80). */
  .data-table .table-row:not(.senal) {
    min-width: 720px;
  }

  /* En movil, nombre y veredicto se apilan: dos columnas de texto en 375px no caben. */
  .table-row.senal {
    grid-template-columns: minmax(0, 1fr);
    gap: 6px;
  }

  /* Los dos campos de ancho de banda (enlace + cuota) mas el boton no caben en una fila a 375px:
     sin esto el formulario se salia por la derecha y la pagina cogia scroll horizontal (FASE 80). */
  .panel-heading {
    align-items: flex-start;
    flex-wrap: wrap;
  }

  #quota-form {
    flex-wrap: wrap;
    width: 100%;
  }

  #quota-form .quota-label {
    flex: 1 1 140px;
  }

  #quota-form .quota-label input {
    width: 100%;
  }

  .data-table .table-row.senal > div,
  .data-table .table-row.senal > div:first-child {
    justify-self: start;
    text-align: left;
  }
}

/* ★★★★ Aeon 78 — LA FICHA DE UN PARTE DE SOPORTE (encargo del dueño, 2026-08-20).
   Dos rejillas: los datos del parte y la conversación. Nada nuevo de color ni de tipografía:
   se reutilizan las variables que ya usa el resto del panel. */
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 10px 24px;
  margin-bottom: 18px;
}
.detail-item { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.detail-key { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.6; }
/* `pre-wrap` porque el registro del dispositivo lleva saltos de línea y sin esto sale en un churro. */
.detail-value { white-space: pre-wrap; overflow-wrap: anywhere; }

.thread { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
.thread-item { border-radius: 12px; padding: 10px 14px; background: rgba(255, 255, 255, 0.04); }
/* El del cliente se separa del panel por el borde de color, no por el lado: en una lista larga
   alternar izquierda/derecha obliga a barrer con la vista. */
.thread-item.from-client { border-left: 3px solid var(--accent, #e0913f); }
.thread-item.from-panel { border-left: 3px solid rgba(255, 255, 255, 0.25); }
.thread-who { font-weight: 600; margin-right: 8px; }
.thread-when { font-size: 0.8rem; opacity: 0.6; }
.thread-item p { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* ── FASE 125 · el REGISTRO DEL DISPOSITIVO en su propia ventana ────────────────────────────────────
   Encargo suyo: dentro de la ficha del parte el registro «ocupa mucha pantalla», así que se saca a
   una ventana y en la ficha solo queda un botón «Abrir».
   ⚠ Es más ancha que el resto de `picker-modal` (460px) a propósito: un log con líneas de 100+
   caracteres dentro de 460px es una columna de sopa. */
.registro-modal {
  max-width: min(920px, 94vw);
  width: min(920px, 94vw);
}

.registro-texto {
  margin: 0;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--surface-muted, var(--surface));
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  /* El log ya viene con sus saltos: se respetan, y lo que se pase de ancho se desplaza en su caja
     — nunca empuja el ancho de la ventana. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 62vh;
  overflow: auto;
}

/* El aviso de borrar un dispositivo: tiene que LEERSE como una advertencia, no como una nota. */
.aviso-grave {
  margin: 0;
  padding: 12px;
  border: 1px solid var(--tint-red-border);
  border-radius: var(--radius-control);
  background: var(--tint-red);
  color: var(--strong);
  font-size: var(--text-base);
}

/* FASE 125 · el «Mostrar N» arriba y a la DERECHA de la tabla (encargo suyo).
   Va dentro de la `panel-heading`, que ya es flex con el título a la izquierda: con `margin-left:auto`
   se pega a la derecha sin tocar la cabecera de las demás pantallas. */
.tabla-tamano {
  margin-left: auto;
  display: flex;
  align-items: center;
}

.tabla-tamano .paginador-tamano {
  margin: 0;
}

/* ★ FASE 126 (Aeon 100) — la celda de «Ultima conexion». Va en relativo («hace 3 h») porque lo que
   se mira ahi es si el cliente esta vivo, y para eso una fecha obliga a restar mentalmente. La fecha
   exacta esta en el `title`. */
.ultima-conexion {
  white-space: nowrap;
}

/* ★★★★ FASE 126 (Aeon 100) — LOS AVISOS, ABAJO A LA DERECHA Y SIN OCUPAR SITIO.

   Decision suya: «las notificaciones solamente apareceran como aparecen las de Claude Code, en la
   parte inferior derecha». Lo importante de `position: fixed` aqui no es donde se ven, es que **no
   ocupan sitio en el flujo**: la barra de antes empujaba la pagina al aparecer y la volvia a subir al
   irse, asi que cada aviso movia lo que estabas mirando. */
.avisos {
  bottom: 16px;
  display: grid;
  gap: 8px;
  justify-items: end;
  /* Por encima de todo menos de los dialogos, que son capa del navegador. */
  z-index: 60;
  pointer-events: none;
  position: fixed;
  right: 16px;
}

.aviso {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid #20c76f;
  border-radius: 10px;
  box-shadow: var(--shadow-overlay);
  color: var(--text);
  display: flex;
  font-size: 13px;
  gap: 10px;
  /* Los hijos si reciben el raton: el contenedor no, para no tapar lo que hay debajo. */
  pointer-events: auto;
  max-width: min(92vw, 380px);
  padding: 10px 12px;
}

.aviso.error {
  border-left-color: #ee5c63;
}

.aviso-cerrar {
  background: none;
  border: 0;
  color: var(--muted);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  margin-left: auto;
  padding: 0 2px;
}

.aviso-cerrar:hover {
  color: var(--text);
}

/* ★★★ FASE 126 (Aeon 100) — DOMINIOS, EN TRES CAJONES.

   Encargo suyo: «Añadir un dominio ocupando la parte izquierda, y los otros dos la derecha (...)
   migrar por dominio ocupara la parte superior y migrar todos los dominios la inferior. (...) El
   alto y bajo de estos dos cajones debe estar a nivel con el cajon de Añadir dominio.»

   La columna izquierda manda el alto (es la del formulario, la mas larga). `align-items: stretch`
   --el valor por defecto-- hace que la derecha ocupe EXACTAMENTE ese alto. */
.dominios-columnas {
  align-items: stretch;
  display: grid;
  gap: 16px;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  margin-bottom: 16px;
}

/* Y aqui esta lo que el pidio con «el alto y bajo (...) a nivel»: los dos cajones conservan el alto
   que necesiten (`auto`) y la holgura sobrante se va AL MEDIO, no arriba ni abajo. Asi el borde
   superior del primero y el inferior del segundo quedan a ras del cajon de la izquierda.

   ⛔ Lo que NO se hace es estirarlos (`1fr 1fr`): eso los dejaria con un hueco vacio dentro y con el
      texto flotando en medio de la caja, que es peor que la desalineacion que corrige. */
.dominios-acciones {
  align-content: space-between;
  display: grid;
  gap: 16px;
}

/* El formulario ya no lleva margen propio: quien reparte el sitio ahora es la rejilla. */
.dominios-columnas > .form-panel {
  margin: 0;
}

/* El cuerpo de los dos cajones de la derecha. `.panel` solo pone la caja (fondo, borde, radio); el
   relleno lo pone cada contenido, y estos dos son texto + un boton. */
.dominios-acciones .panel-body {
  display: grid;
  gap: 10px;
  padding: 0 16px 16px;
}

/* En pantalla estrecha se apilan, y el orden es el que el describio de arriba abajo. */
@media (max-width: 900px) {
  .dominios-columnas {
    grid-template-columns: 1fr;
  }
}

/* ⛔ FASE AEON 7 — el selector de plataforma de «Actualizaciones». Va en la cabecera del panel,
   pegado al titulo, porque decide QUE se esta editando y no es un campo mas del formulario. */
.inline-select {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  /* ⚠ 2026-09-12: decía `var(--humo)` — el nombre del token de MARCA (`CLAUDE.md`, apartado 2), que
     en la hoja del panel no existe: aquí se llama `--muted`. Sin respaldo, el navegador descarta la
     declaración entera y esto se quedaba del color heredado. Es el mismo despiste que `--cian` en
     otras cuatro reglas, sólo que aquéllas sí llevan respaldo y por eso no se notan. */
  color: var(--muted);
  white-space: nowrap;
}

.inline-select select {
  min-width: 190px;
}

/* ⛔ FASE AEON 7 — dos acciones en la misma celda de la tabla de proveedores. */
.button-row.compacta {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

/* ⛔ FASE AEON 7 — un paquete retirado se ve, pero apagado: sigue existiendo para quien lo tiene. */
.table-row.atenuada {
  opacity: 0.55;
}

/* ⛔ FASE AEON 7 — el aviso de solo lectura del owner sobre lo que es de un operador. */
.notice.solo-lectura {
  margin-bottom: 16px;
}

/* ⛔ FASE AEON 7 — la versión de cada plataforma en «Actualizaciones».
   ⛔ 2026-09-07 — y una columna más: EL PARQUE, con lo que costaría forzar el mínimo. Va la más
   ancha después del nombre porque es la que lleva texto («12 si exiges la última»), no un número. */
.table-row.versiones {
  grid-template-columns: minmax(170px, 1.4fr) minmax(110px, 0.8fr) minmax(110px, 0.8fr) minmax(190px, 1.5fr) minmax(100px, 0.7fr);
}

/* ⛔ FASE AEON 7 — se conserva porque la usan otras ventanas anchas. La de «dentro de un
   proveedor» ya no existe: es una pantalla (`#provider-detail`). */
.picker-modal.ancho {
  max-width: 900px;
  width: 92vw;
}

/* ⛔★★★★★ 2026-09-13 — Aquí vivían los estilos de la ficha de dentro de un proveedor: agente → subagente →
   clientes, anidado. Desde el 13 la ficha es sólo comercial —cuántos, no quiénes— y ya no los pinta. */

/* ⛔★★★★★ FASE AEON 7 — EL PARQUE POR PLATAFORMA. Las ocho familias, siempre las ocho.
   Las que están a cero se atenúan pero NO se esconden: el hueco es el dato mientras se construyen
   las tres bases de código que faltan. */
.plataformas-grid {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.plataforma-fila {
  align-items: baseline;
  background: var(--surface-2);
  border-radius: 10px;
  display: grid;
  gap: 4px 10px;
  grid-template-columns: 1fr auto;
  padding: 10px 12px;
}

.plataforma-fila span {
  font-size: var(--text-xs);
}

.plataforma-fila strong {
  font-variant-numeric: tabular-nums;
}

.plataforma-fila small {
  color: var(--muted);
  font-size: var(--text-xs);
  grid-column: 2;
  text-align: right;
}

/* Sin nadie todavía. Se distingue por opacidad **y** por el cero que se lee: nunca sólo por color. */
.plataforma-fila.sin-nadie {
  opacity: 0.55;
}

/* El nombre del operador ES el enlace a su ficha. Encargo del dueño: «simplemente pulsando sobre
   su nombre se accederá a su perfil». Se pinta como el texto que era, no como un botón: en una
   tabla, un botón azul por fila compite con las acciones de la derecha. */
.enlace-de-fila {
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font: inherit;
  font-weight: var(--weight-semibold, 600);
  padding: 0;
  text-align: left;
}

.enlace-de-fila:hover,
.enlace-de-fila:focus-visible {
  color: var(--cian, #46c7e8);
  text-decoration: underline;
}

/* ⚠ 2026-09-13 — Que se vea que el nombre se pulsa ANTES de pasar el ratón por encima: el dueño pidió
   entrar en un proveedor pulsando su nombre, y en reposo se leía como texto. */
.enlace-de-fila::after {
  color: var(--muted);
  content: " ›";
}
