/*
 * Consola de flota — sistema visual.
 *
 * Este bloque :root ES el fichero de tokens de esta aplicación: la FleetApp no comparte hoja con
 * la AdminApp y está declarada FUERA DEL ALCANCE del design system (§11 de
 * docs/06-design-system.md), así que "ningún hexadecimal suelto ni ningún font-size numérico
 * fuera de tokens" se cumple aquí declarándolos todos una vez, arriba. Las hojas de pantalla
 * (auth.css, signups.css) solo usan estas variables.
 *
 * Por qué una sola hoja y no cuatro (tokens/base/componentes/pantalla) como en la AdminApp: la
 * consola tiene seis pantallas y un único chasis, así que partirla añadiría tres <link> y un
 * orden de cascada que vigilar a cambio de nada. Lo que sí está partido es lo que de verdad
 * tiene vocabulario propio y no se carga siempre: auth.css (única superficie sin sesión) y
 * signups.css (bandeja y conversión).
 *
 * MODO DE USO: esto es una herramienta de operación, no una landing. Manda la densidad —fila de
 * 32-34px, dato a 13px— y la escaneabilidad: el mismo estado se pinta igual en las seis
 * pantallas, y el color nunca es el único portador del significado.
 */
:root {
  /* ---- Planos neutros ---------------------------------------------------------------------
   * Cuatro capas y en este orden: carril (la más profunda) < lienzo < banda de cabecera de
   * tabla < tarjeta. El carril es lo que separa panel de contenido POR TONO y no con una línea:
   * un borde vertical de 240px de alto es lo que hacía que la consola pareciese dos páginas
   * pegadas en vez de una aplicación.
   */
  --rail: #e9edf3;
  --surface: #f6f8fa;
  --surface-low: #eef1f5;
  --card: #ffffff;

  /* ---- Tinta -------------------------------------------------------------------------------
   * Tres niveles. --tertiary bajó de #64748b a #5a6473 por contraste medido: el valor anterior
   * daba 4,14:1 sobre el carril y esta consola escribe ahí sus etiquetas de 11px. El nuevo da
   * 5,10:1 sobre el carril y 5,99:1 sobre blanco. Es el mismo slate, cuatro puntos más denso.
   */
  --on-surface: #191c1e;
  --on-surface-variant: #45464d;
  --tertiary: #5a6473;
  --on-primary: #ffffff;

  /* Hairlines. --outline-faint subió de #eceef0 a #e6e9ee: a fila de 33px el separador anterior
     desaparecía y la tabla se leía como un bloque. */
  --outline: #c6c6cd;
  --outline-faint: #e6e9ee;

  /* ---- Identidad ---------------------------------------------------------------------------
   * --primary y --accent no se tocan: son la marca de la consola.
   *
   * --accent-strong existe porque --accent NO SIRVE PARA TEXTO: #3b82f6 sobre blanco da 3,68:1,
   * por debajo del 4,5:1 que exige el texto normal, y hoy se usaba como color de enlace, de
   * summary de <details> y de pastilla informativa. #1d4ed8 da 6,66:1 sobre blanco y 5,8:1
   * sobre el tinte azul. Regla: --accent pinta bordes, rellenos y anillos de foco (3:1 basta
   * para un componente); --accent-strong pinta cualquier cosa que se lea.
   */
  --primary: #0f172a;
  --accent: #3b82f6;
  --accent-strong: #1d4ed8;

  /* ---- Semáforo ----------------------------------------------------------------------------
   * --warn bajó de #b45309 a #a14a08: el anterior daba 4,47:1 sobre su propio tinte y las
   * pastillas escriben justo ahí, a 11px. Falla por tres centésimas, pero falla.
   */
  --ok: #047857;
  --warn: #a14a08;
  --err: #ba1a1a;

  /*
   * Tintes de estado: el mismo tono del semáforo al 5-10 % sobre fondo claro. Estaban escritos
   * a mano como rgba() en dieciocho reglas distintas, así que un cambio de paleta exigía
   * acordarse de las dieciocho.
   */
  --accent-tint: rgba(59, 130, 246, .1);
  --ok-tint: rgba(4, 120, 87, .1);
  --warn-tint: rgba(161, 74, 8, .1);
  --err-tint: rgba(186, 26, 26, .1);
  --err-tint-soft: rgba(186, 26, 26, .08);
  --err-tint-row: rgba(186, 26, 26, .05);

  /* Selección de texto: también es superficie de diseño. Azul de marca al 20 %, que deja el
     texto en su propio color en vez de invertirlo al azul del sistema. */
  --selection: rgba(59, 130, 246, .2);

  /* ---- Tipografía --------------------------------------------------------------------------
   * Seis niveles, no ocho. Se retiraron --text-body (16px) y --text-display (32px): el cuerpo
   * de una consola de operación es 13px y un titular de 32px en una pantalla que existe para
   * barrer tablas es espacio gastado. --text-lg absorbe el 16px que usaba --text-body.
   *
   *   xs 11  cabecera de columna, etiqueta de dato, pastilla
   *   sm 12  meta secundaria, código en tabla
   *   md 13  DATO Y CUERPO POR DEFECTO
   *   base 14  controles de formulario y prosa de aviso
   *   lg 16  título de sección (h2)
   *   title 20  titular de pantalla (h1) y cifra de KPI
   */
  --text-xs: 11px;
  --text-sm: 12px;
  --text-md: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-title: 20px;

  /* Las dos familias, declaradas una vez. La monoespaciada estaba repetida literal en seis
     reglas y además había un var(--font-mono) sin declarar en .error-detail, que caía al
     monospace del navegador. */
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, 'Cascadia Code', Consolas, monospace;

  /*
   * Alturas de control -- mismo criterio que tokens.css de la AdminApp, aunque esta aplicación
   * esté fuera del alcance del design system (§11 de docs/06-design-system.md):
   * todo lo que sea "pastilla" (pill, badge, botón pequeño) fija su altura con `height`, nunca
   * con padding, y todo contenedor flex que las agrupe lleva `align-items: center`. Sin las dos
   * cosas un chip estrecho se estira a la altura de la fila y sale cuadrado entre píldoras.
   */
  --control-sm: 22px;
  --control-md: 30px;
  --control-lg: 36px;

  /* Radios. Estaban escritos como .5rem/.375rem en veintitantas reglas; el valor no cambia. */
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-pill: 9999px;

  /* Altura de la barra superior del móvil. Es token y no un 52px suelto porque lo usan seis
     reglas del cajón —el desplazamiento del lienzo, el techo de la barra, el arranque del cajón
     y el del velo— y descuadrar una de ellas deja una franja o un solape. */
  --bar-h: 52px;

  /*
   * Sombras: todas con desplazamiento vertical y desenfoque, ninguna a offset cero. Una sombra
   * sin desplazamiento no es luz, es un halo de color, y es lo que hace que una interfaz parezca
   * una maqueta. El tono es el azul de --primary y no negro puro, para que case con el lienzo.
   */
  --shadow-xs: 0 1px 2px rgba(15, 23, 42, .06);
  --shadow-sm: 0 1px 3px rgba(15, 23, 42, .09), 0 1px 2px rgba(15, 23, 42, .05);
  --scrim: rgba(15, 23, 42, .45);
  --shadow-modal: 0 16px 40px rgba(15, 23, 42, .2), 0 2px 8px rgba(15, 23, 42, .08);

  /* Una sola duración para todo. Las transiciones comunican un cambio de estado (hover, foco,
     activo); nada se anima al cargar. */
  --t-fast: 150ms ease;

  /*
   * Marca de producto (OmmiFX). Los SVG traen proporción fija, así que solo se fija la altura
   * y el ancho sale solo. El isotipo del carril baja a 28px: la cabecera del carril pasó de
   * 64px a 52px y un isotipo de 32px la llenaba entera.
   */
  --brand-lockup-height: 32px;
  --brand-mark-size: 28px;

  /* Controles nativos del navegador teñidos con la paleta: checkbox y radio del formulario de
     conversión dejan de salir en el azul del sistema operativo. */
  accent-color: var(--accent);
  color-scheme: light;
}

/* ============================================================================================
   BASE
   ============================================================================================ */

body {
  margin: 0;
  background: var(--surface);
  color: var(--on-surface);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: 20px;
  -webkit-font-smoothing: antialiased;
  /* Barras de scroll teñidas con la paleta: son superficie del navegador, pero se ven dentro de
     la aplicación. El <pre> oscuro de las excepciones las redeclara, que si no salen claras
     sobre fondo negro. */
  scrollbar-width: thin;
  scrollbar-color: var(--outline) transparent;
}

::selection { background: var(--selection); }

/*
 * Foco de teclado, una sola vez y para todo lo interactivo. Anillo de marca y no el azul del
 * navegador, con offset para que no se coma el borde del propio control. :focus-visible y no
 * :focus: el ratón no tiene por qué dejar anillo.
 */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

h1 {
  margin: 0;
  font-size: var(--text-title);
  line-height: 26px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--primary);
}

/* Título de sección dentro de una pantalla (/Esquema tiene dos). Va con una hairline debajo:
   es lo que convierte una lista de tablas seguidas en secciones. */
h2 {
  margin: 28px 0 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--outline-faint);
  font-size: var(--text-lg);
  line-height: 22px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--primary);
}

code {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: -.01em;
}
code.small { font-size: var(--text-xs); color: var(--on-surface-variant); }

.muted { color: var(--tertiary); font-size: var(--text-sm); }

/*
 * `.eyebrow` SE RETIRÓ ENTERO el 2026-09-19 y no se repone. Las ocho vistas abrían con un rótulo
 * "iThub Digital" sobre el <h1> que repetía lo que ya dice el carril, y el titular carga su propio
 * peso sin él. El último consumidor que quedó vivo —la cabecera del visor de logs— decía "Últimas
 * 24 horas" justo encima de una frase que empieza por "…en las últimas 24 horas": la misma
 * información dos veces.
 *
 * Si vuelve a hacer falta un rótulo sobre un titular, el problema es el titular.
 */

/* Iconos: SVG propios de esta aplicación (Views/Icons.cs). 18px sobre rejilla de 24 con trazo
   de 1,5 -- el mismo peso óptico que el texto de 13px al que acompañan. */
.icon { width: 18px; height: 18px; flex: none; }

/* ============================================================================================
   CHASIS — carril, lienzo y cabecera de página
   ============================================================================================ */

.fleet-shell { display: flex; min-height: 100vh; align-items: stretch; }

/*
 * Carril persistente. Sin borde derecho a propósito: lo que lo separa del lienzo es el tono
 * (--rail es un punto más frío y profundo que --surface), que es la forma de separar dos planos
 * sin dibujar una línea que recorra la pantalla entera.
 */
.fleet-sidebar {
  flex: 0 0 232px;
  display: flex; flex-direction: column;
  background: var(--rail);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}

/* ---- El carril como <details> ---------------------------------------------------------------
 * El contenido del carril vive dentro de un <details> para poder plegarse en móvil sin una línea
 * de JavaScript (ver el bloque @media del final). Aquí, en escritorio, el <details> NO se
 * comporta como tal: su contenido se revela siempre, esté el elemento abierto o cerrado, así que
 * el carril es la misma columna fija de siempre y el atributo [open] no cambia nada.
 *
 * Se revela por dos vías a propósito, y las dos hacen falta:
 *   - ::details-content es como los motores actuales ocultan lo plegado (content-visibility);
 *     un display en el hijo NO lo contrarresta, porque el salto de renderizado es del ancestro.
 *   - display en el hijo cubre a los motores anteriores, que ocultaban los hijos directos.
 * Donde el pseudo no existe, esa regla se descarta sola y queda la otra. Quitar cualquiera de las
 * dos deja el carril de escritorio vacío en la mitad del parque de navegadores.
 */
.fleet-rail { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.fleet-rail::details-content {
  content-visibility: visible;
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
}
.fleet-rail > .fleet-rail__panel { display: flex; }

/* El panel reparte los dos bloques del carril (destinos arriba, sesión abajo). Lo hacía el
   propio <aside> antes de que el <details> se metiera en medio. */
.fleet-rail__panel {
  flex-direction: column; justify-content: space-between;
  flex: 1 1 auto; min-height: 0;
}

/*
 * La barra del carril: es a la vez la marca y el <summary> que pliega. En escritorio no pliega
 * nada visible, así que no se anuncia como pulsable (cursor por defecto); sigue siendo
 * alcanzable por teclado, y eso es deliberado -- es la única salida si un motor raro se negara a
 * revelar el contenido.
 */
.fleet-rail__bar {
  display: flex; align-items: center; gap: 10px;
  min-height: 52px; padding: 0 16px; margin-bottom: 4px;
  cursor: default;
  /* El triángulo del navegador sobra: el indicador propio es el chevron, y solo en móvil. */
  list-style: none;
}
.fleet-rail__bar::-webkit-details-marker { display: none; }

/* Destino actual, hamburguesa y velo: piezas del cajón móvil, invisibles en escritorio. Allí el
   destino ya está resaltado dentro del carril, no hay nada que abrir y no hay nada que velar. */
.fleet-rail__current,
.fleet-rail__toggle,
.fleet-rail__chevron,
.fleet-rail__scrim { display: none; }

.fleet-sidebar__brand {
  display: flex; align-items: center; gap: 10px; min-width: 0;
}
/*
 * Isotipo real de OmmiFX: no lleva ni fondo ni color propios, el SVG ya trae los suyos. Altura
 * explícita para que la imagen no decida el alto de la fila de la cabecera.
 */
.fleet-sidebar__mark {
  display: block;
  width: var(--brand-mark-size); height: var(--brand-mark-size);
}
.fleet-sidebar__name { font-size: var(--text-md); font-weight: 600; color: var(--primary); line-height: 16px; }
.fleet-sidebar__tag {
  font-size: var(--text-xs); color: var(--tertiary); line-height: 14px;
  letter-spacing: .02em;
}

.fleet-sidebar__group {
  font-size: var(--text-xs); font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--tertiary); padding: 8px 20px 6px;
}
.fleet-sidebar__nav { display: flex; flex-direction: column; gap: 1px; padding: 0 10px; }

/*
 * Destino del carril. Los tres estados existen y se distinguen sin depender del color:
 *   hover  -> el elemento sube al plano de tarjeta (blanco sobre carril)
 *   activo -> además sombra, texto en peso 600 y acento; es una posición, no un resalte
 *   foco   -> anillo del sistema, heredado de la regla global
 * Sin filete de color a la izquierda: un borde de color de más de 1px en una fila es justo lo
 * que hace que una lista parezca una bandeja de avisos.
 */
.fleet-sidebar__link {
  display: flex; align-items: center; height: var(--control-md); padding: 0 10px; gap: 10px;
  border-radius: var(--radius-sm); font-size: var(--text-md); font-weight: 500;
  color: var(--on-surface-variant); text-decoration: none;
  transition: background-color var(--t-fast), color var(--t-fast), box-shadow var(--t-fast);
}
.fleet-sidebar__link:hover { background: var(--card); color: var(--primary); }
.fleet-sidebar__link.is-active {
  background: var(--card); color: var(--accent-strong); font-weight: 600;
  box-shadow: var(--shadow-xs);
}
.fleet-sidebar__icon { color: var(--tertiary); transition: color var(--t-fast); }
.fleet-sidebar__link:hover .fleet-sidebar__icon { color: var(--primary); }
.fleet-sidebar__link.is-active .fleet-sidebar__icon { color: var(--accent-strong); }

/* Zona horaria de la consola (1.75). Va pegada al pie y sin borde propio: es una nota de
   lectura, no una sección -- separarla con otra línea la convertiría en un bloque con más peso
   del que tiene. */
.fleet-sidebar__zone {
  padding: 12px 20px 0;
  font-size: var(--text-xs); color: var(--tertiary); line-height: 16px;
}

.fleet-sidebar__footer {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 12px 16px 16px;
  font-size: var(--text-xs); color: var(--tertiary); line-height: 16px;
}

/* Identidad de la sesión de personal (RF-45). min-width: 0 para que un correo largo se recorte
   en vez de empujar "Cerrar sesión" fuera del carril. */
.fleet-sidebar__user { display: flex; flex-direction: column; min-width: 0; }
.fleet-sidebar__username {
  font-size: var(--text-sm); font-weight: 600; color: var(--primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fleet-sidebar__usermail {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.fleet-main { flex: 1 1 auto; min-width: 0; }

/* Lienzo. 1440px de ancho útil: a 1440 de pantalla el carril se lleva 232 y el contenido no
   llega a tocarlo, así que el tope no recorta nunca en el ancho de referencia. */
.fleet { max-width: 1440px; margin: 0 auto; padding: 20px 28px 36px; }

/*
 * Cabecera de página: titular a la izquierda, tira de KPI a la derecha, hairline debajo. Es la
 * tercera pieza del chasis -- el carril dice dónde estás en la consola y esto dice qué pantalla
 * es y cómo está. Antes abría con un rótulo "iThub Digital" sobre el titular que repetía lo que
 * ya dice el carril; se retiró de las ocho vistas.
 */
.fleet-head {
  display: flex; flex-wrap: wrap; gap: 16px 24px;
  align-items: flex-end; justify-content: space-between;
  padding-bottom: 14px; margin-bottom: 20px;
  border-bottom: 1px solid var(--outline-faint);
}

/* Titular con su identificador al lado (la ficha de tenant lleva el slug). El slug era el
   rótulo de encima; como dato sigue siendo útil, así que pasa a chip junto al nombre en vez
   de desaparecer con el resto de eyebrows. */
.page-title { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }

.chip {
  display: inline-flex; align-items: center; height: var(--control-sm); padding: 0 8px;
  border: 1px solid var(--outline-faint); border-radius: var(--radius-sm);
  background: var(--surface-low); color: var(--on-surface-variant);
  font-family: var(--font-mono); font-size: var(--text-sm);
}

/* Vuelta a la pantalla anterior. Altura de control para que no descuadre la línea del titular. */
.back-link {
  display: inline-flex; align-items: center; gap: 6px; height: var(--control-sm);
  margin-bottom: 2px;
  font-size: var(--text-sm); font-weight: 500; color: var(--tertiary); text-decoration: none;
  transition: color var(--t-fast);
}
.back-link:hover { color: var(--accent-strong); }
.back-link .icon { width: 14px; height: 14px; }

/*
 * Tira de KPI. Un solo bloque segmentado y no cinco tarjetas sueltas: cinco cajas iguales con
 * su borde son el patrón de tarjeta-icono-título que esta consola no debe usar como estructura
 * de página, y además a cinco elementos ya no se leen como un conjunto.
 */
.stats {
  display: flex; align-items: stretch;
  background: var(--card); border: 1px solid var(--outline-faint); border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs); overflow: hidden;
}
.stat {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: 8px 16px; min-width: 96px;
  border-left: 1px solid var(--outline-faint);
}
.stat:first-child { border-left: 0; }
.stat-n {
  display: block; font-size: var(--text-title); line-height: 24px; font-weight: 600;
  color: var(--primary); font-variant-numeric: tabular-nums;
}
.stat-l {
  font-size: var(--text-xs); font-weight: 500; letter-spacing: .04em; text-transform: uppercase;
  color: var(--tertiary); line-height: 14px;
}

/* Un KPI que también es enlace (bandeja de signups, tickets abiertos): mismo bloque, sin
   subrayado, con el segmento resaltado al pasar el ratón para que se note que es interactivo. */
.stat--link { text-decoration: none; transition: background-color var(--t-fast); }
.stat--link:hover { background: var(--surface-low); }
.stat--link:hover .stat-n { color: var(--accent-strong); }

/* ============================================================================================
   TABLAS — la superficie principal de esta aplicación
   ============================================================================================ */

.table-wrap {
  background: var(--card); border: 1px solid var(--outline-faint); border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs); overflow-x: auto;
}

/*
 * tabular-nums en toda la tabla y no columna a columna. Es deliberado: aquí las columnas
 * numéricas y de fecha son mayoría, la clase que las marcaría (.num) solo existe en dos
 * vistas, y las cifras que aparecen dentro de un mensaje de log también bailan al ordenar.
 * Sobre el texto corriente no tiene efecto: solo cambia el avance de los dígitos.
 */
table {
  width: 100%; border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

th {
  text-align: left; font-size: var(--text-xs); font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--on-surface-variant);
  padding: 8px 14px; background: var(--surface-low); border-bottom: 1px solid var(--outline-faint);
  white-space: nowrap;
}
td {
  padding: 7px 14px; border-bottom: 1px solid var(--outline-faint);
  font-size: var(--text-md); line-height: 18px; white-space: nowrap;
  vertical-align: top;
}
tbody tr:last-child td { border-bottom: none; }

/* Barrido con la vista: la fila bajo el cursor se levanta al plano inferior. Es la única ayuda
   que necesita una tabla de 500 filas para no perder el renglón. */
tbody tr { transition: background-color var(--t-fast); }
tbody tr:hover { background: var(--surface-low); }

.num { text-align: right; }

/* Enlace dentro de una tabla: color legible (--accent-strong, no --accent) y subrayado solo al
   pasar, que a 500 filas subrayarlo todo es ruido. */
td a { color: var(--accent-strong); text-decoration: none; }
td a:hover { text-decoration: underline; }

/* El mensaje libre (log, ticket, error de corrida) no debe forzar la fila a lo ancho de una
   sola línea, a diferencia del resto de columnas (slugs, correos, fechas). */
td.message { white-space: normal; max-width: 520px; }

/* Dos datos en la misma celda (aplicación + componente, empresa + dominio): el segundo baja de
   nivel en vez de ocupar otra columna. */
td .muted { display: inline-block; }

/* ============================================================================================
   PASTILLAS DE ESTADO — un solo mecanismo, seis vocabularios
   ============================================================================================
 * Antes había cinco familias declaradas por separado repitiendo el mismo par
 * `background: var(--x-tint); color: var(--x)`, así que cambiar la forma de una pastilla
 * obligaba a tocar cinco sitios y nada garantizaba que un "cerrado" de soporte se pintase igual
 * que un "al día" de esquema.
 *
 * Ahora la FORMA se declara una vez (.pill) y las familias solo eligen TONO. Las familias
 * siguen teniendo nombre propio -- .pill-signup-*, .pill-run-*, .pill-schema-*, .pill-level-*,
 * .pill-support-* y .pill-verify-* (esta última en signups.css) -- porque son vocabularios
 * distintos y confundirlos haría que un cambio en uno arrastrase al otro; lo que comparten es
 * el mecanismo, no la clase.
 *
 * El color NUNCA es el único portador: toda pastilla lleva su texto, y el punto que la abre usa
 * currentColor, así que en escala de grises sigue habiendo cuatro pesos distinguibles.
 */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--control-sm); padding: 0 9px; border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: 600; letter-spacing: .03em; text-transform: uppercase;
  white-space: nowrap;
  /* Neutro por defecto: una pastilla sin variante (p. ej. "Sin migrar") tiene que verse como
     un estado y no como texto suelto. */
  background: var(--surface-low); color: var(--on-surface-variant);
}
.pill::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; flex: none;
}

/* -- Tono informativo: en marcha, recién llegado, sin veredicto todavía. */
.pill-signup-new,
.pill-level-information,
.pill-run-running,
.pill-support-open { background: var(--accent-tint); color: var(--accent-strong); }

/* -- Tono favorable: activo, convertido, cerrado, al día, migrado. */
.pill-active,
.pill-signup-converted,
.pill-run-succeeded,
.pill-schema-ok,
.pill-support-closed { background: var(--ok-tint); color: var(--ok); }

/* -- Tono de atención: algo está a medias o se ha quedado atrás, pero nada ha fallado. */
.pill-provisioning,
.pill-signup-contacted,
.pill-level-warning,
.pill-schema-behind,
.pill-support-inprogress { background: var(--warn-tint); color: var(--warn); }

/* -- Tono de fallo: falló, se descartó, está suspendido o archivado. */
.pill-suspended,
.pill-archived,
.pill-signup-discarded,
.pill-level-error,
.pill-level-fatal,
.pill-run-failed,
.pill-schema-failed { background: var(--err-tint); color: var(--err); }

/* "Muy visual y práctico para detectar fallos" (encargo literal): una fila Error/Fatal se
   resalta con un fondo tenue del mismo tono de error, para que salte a la vista sin tener que
   leer la columna de nivel. Gana al hover para que no se pierda al pasar por encima. */
tr.log-row--failure td,
tr.log-row--failure:hover td { background: var(--err-tint-row); }

/* ============================================================================================
   CONTROLES — filtros, botones y pastillas de filtro
   ============================================================================================ */

/* Formulario de filtro, recarga por GET, sin JavaScript obligatorio -- mismo criterio que
   /Tracking en la AdminApp. Todos los controles llevan altura explícita y el contenedor flex
   lleva align-items: center. */
/*
 * Todos los filtros de una pantalla en un solo bloque (instrucción del usuario, 2026-09-19).
 * Las pastillas de severidad y el formulario comparten tarjeta en vez de ser dos islas con el
 * gráfico en medio: acotar una búsqueda era antes tres zonas distintas de la página.
 *
 * Una sola tarjeta para los dos, así que .level-pills pierde aquí su margen inferior y .filters
 * su tarjeta propia: si las dos la dibujaran, saldría una tarjeta dentro de otra.
 */
.filters-bar {
  background: var(--card); border: 1px solid var(--outline-faint);
  border-radius: var(--radius-md); box-shadow: var(--shadow-xs);
  padding: 12px 16px; margin-bottom: 16px;
}

.filters-bar .level-pills { margin-bottom: 12px; }

.filters-bar .filters {
  background: none; border: 0; box-shadow: none; padding: 0; margin-bottom: 0;
  border-top: 1px solid var(--outline-faint); padding-top: 12px;
}

/* Sin pastillas de nivel arriba —el resto de pantallas— no hay nada de lo que separarse. */
.filters-bar .level-pills:empty + .filters,
.filters-bar > .filters:first-child { border-top: 0; padding-top: 0; }

.filters {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px;
  background: var(--card); border: 1px solid var(--outline-faint); border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  padding: 12px 16px; margin-bottom: 16px;
}
.field { display: flex; flex-direction: column; gap: 4px; }

/*
 * `display: flex` de arriba GANA a `hidden`, que solo vale `display: none` en la hoja del
 * navegador: un campo con [hidden] seguía viéndose. Es la misma trampa que CLAUDE.md ya documenta
 * para la AdminApp, donde se resolvió con una clase `.field--collapsed`; aquí basta con devolverle
 * su efecto al atributo, que además es lo correcto para quien navega por teclado — un campo
 * marcado como oculto no puede seguir recibiendo foco.
 */
.field[hidden] { display: none; }
.field--grow { flex: 1 1 240px; }
.field-l {
  font-size: var(--text-xs); font-weight: 500; letter-spacing: .04em; text-transform: uppercase;
  color: var(--tertiary);
}
.filters select,
.filters input[type="text"],
.filters input[type="date"] {
  height: var(--control-md); box-sizing: border-box; padding: 0 10px;
  border: 1px solid var(--outline); border-radius: var(--radius-sm); background: var(--card);
  font-family: inherit; font-size: var(--text-md); color: var(--on-surface); min-width: 140px;
  transition: border-color var(--t-fast);
}
.filters select:hover, .filters input[type="text"]:hover { border-color: var(--tertiary); }

.filters .button {
  display: flex; align-items: center; justify-content: center; height: var(--control-md);
  padding: 0 16px; border: 0; border-radius: var(--radius-sm);
  background: var(--primary); color: var(--on-primary);
  font-family: inherit; font-size: var(--text-md); font-weight: 500; cursor: pointer;
  transition: background-color var(--t-fast);
}
.filters .button:hover { background: var(--accent-strong); }

/* Pastillas de nivel del visor de logs: filtro rápido de un clic. Altura explícita y contenedor
   con align-items: center, la misma regla que cualquier control con forma de pastilla. */
.level-pills { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.level-pill {
  display: flex; align-items: center; height: var(--control-md); padding: 0 14px;
  border-radius: var(--radius-pill); border: 1px solid var(--outline); background: var(--card);
  font-size: var(--text-md); font-weight: 500; color: var(--on-surface-variant);
  text-decoration: none;
  transition: border-color var(--t-fast), background-color var(--t-fast), color var(--t-fast);
}
.level-pill:hover { border-color: var(--tertiary); color: var(--primary); }
/*
 * Pastilla de nivel seleccionada. UN NIVEL, UN COLOR, EN TODA LA PANTALLA.
 *
 * Antes la seleccionada se rellenaba en macizo con el color del nivel y la insignia de la tabla
 * usaba tinte + texto del mismo color, así que "Advertencia" se veía marrón en el filtro,
 * melocotón en la tabla y de un tercer tono en el gráfico: tres colores para el mismo nivel.
 * Ahora la seleccionada usa EL MISMO par tinte/color que su insignia y se distingue de las
 * demás por el aro y el peso, no por un tono distinto.
 *
 * "Todos" no es un nivel, así que se queda en el neutro de la marca: es el estado "sin filtrar",
 * no una severidad, y darle color lo haría competir con los que sí la tienen.
 */
.level-pill.is-active {
  border-color: transparent; background: var(--primary); color: var(--on-primary);
  font-weight: 600;
}

.level-pill--info.is-active {
  background: var(--accent-tint); border-color: var(--accent-strong); color: var(--accent-strong);
}

.level-pill--warn.is-active {
  background: var(--warn-tint); border-color: var(--warn); color: var(--warn);
}

.level-pill--err.is-active {
  background: var(--err-tint); border-color: var(--err); color: var(--err);
}

/* Botón que parece enlace: acción secundaria dentro de una fila (descartar signup, cerrar
   ticket), mismo criterio que .link-button en la AdminApp -- no compite visualmente con la
   acción principal. */
.link-button {
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: inherit; font-size: var(--text-sm); font-weight: 500; color: var(--tertiary);
  transition: color var(--t-fast);
}
.link-button:hover { color: var(--err); }

/* ============================================================================================
   DESPLEGABLES DENTRO DE UNA FILA
   ============================================================================================
 * Excepción completa, detalle campo a campo y caja de respuesta comparten mecanismo: <details>
 * nativo bajo la celda, para no ocupar espacio por defecto en una tabla ya densa.
 */
.log-exception, .audit-detail { margin-top: 6px; }
.reply-box { margin-top: 2px; }

.log-exception summary,
.audit-detail summary,
.reply-box summary {
  display: inline-flex; align-items: center; height: var(--control-sm);
  cursor: pointer; font-size: var(--text-sm); font-weight: 500; color: var(--accent-strong);
}
.log-exception summary:hover,
.audit-detail summary:hover,
.reply-box summary:hover { text-decoration: underline; }

.log-exception pre {
  margin: 8px 0 0; padding: 12px; background: var(--on-surface); color: var(--surface-low);
  border-radius: var(--radius-sm); font-family: var(--font-mono);
  font-size: var(--text-sm); line-height: 18px; white-space: pre-wrap; word-break: break-word;
  max-height: 320px; overflow-y: auto;
  /* Sobre fondo oscuro la barra clara del lienzo canta; se tiñe con el propio texto del bloque. */
  scrollbar-color: var(--tertiary) transparent;
}

/* Tabla anidada de cambios de auditoría: misma rejilla, un nivel menos de peso. */
.audit-detail table { margin-top: 8px; width: 100%; font-size: var(--text-sm); }
.audit-detail th, .audit-detail td {
  padding: 4px 8px; text-align: left; border-bottom: 1px solid var(--outline-faint);
  text-transform: none; letter-spacing: 0; background: none;
  /* Un valor de catálogo puede ser una cadena larga; aquí sí se parte, porque la rejilla está
     dentro de una celda y no hay a dónde crecer. */
  white-space: normal; overflow-wrap: anywhere;
}
.audit-detail th { font-size: var(--text-xs); color: var(--tertiary); }

.reply-box__text {
  margin: 8px 0 4px; padding: 10px 12px; background: var(--surface-low);
  border-radius: var(--radius-sm); font-size: var(--text-md); line-height: 18px;
  white-space: normal;
}
.reply-form { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; width: 280px; }
.reply-form textarea {
  min-height: 64px; padding: 8px 10px; border: 1px solid var(--outline);
  border-radius: var(--radius-sm);
  font-family: inherit; font-size: var(--text-md); color: var(--on-surface); resize: vertical;
}
.reply-form .button {
  align-self: flex-start; height: var(--control-md); padding: 0 14px; border: 0;
  border-radius: var(--radius-sm); background: var(--primary); color: var(--on-primary);
  font-family: inherit; font-size: var(--text-md); font-weight: 500; cursor: pointer;
  transition: background-color var(--t-fast);
}
.reply-form .button:hover { background: var(--accent-strong); }

/* ============================================================================================
   AVISOS Y ESTADOS VACÍOS
   ============================================================================================ */

.empty, .note {
  color: var(--tertiary); font-size: var(--text-sm); line-height: 18px; margin-top: 12px;
}

/*
 * Aviso de negativa del servidor (p. ej. SignupRefusedException al intentar descartar un signup
 * ya convertido). Borde completo de 1px y no un filete de 3px a la izquierda: un borde de color
 * grueso en un aviso es decoración, y la caja entera teñida ya dice de qué tono es.
 */
.notice {
  padding: 10px 14px; border: 1px solid var(--err); border-radius: var(--radius-sm);
  font-size: var(--text-base); line-height: 20px; margin-bottom: 20px;
  background: var(--err-tint-soft); color: var(--err);
}

/* Veredicto de la pantalla de esquema: lo primero que hay que saber tras un despliegue es si
   hay algo que mirar. Lleva color de fondo Y texto explicativo -- el color solo no vale
   (contraste, daltonismo), y el texto solo se pierde entre dos tablas. */
.verdict {
  margin: 0 0 16px;
  padding: 12px 16px;
  border: 1px solid;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  line-height: 20px;
  /* Tres líneas de prosa van en tinta, no en el color del semáforo: el tono lo llevan el fondo
     y el borde, y el significado lo lleva la frase, que dice cuántos tenants fallaron. */
  color: var(--on-surface);
}
.verdict code { font-size: var(--text-md); }
.verdict-ok { background: var(--ok-tint); border-color: var(--ok); }
.verdict-warn { background: var(--warn-tint); border-color: var(--warn); }
.verdict-bad { background: var(--err-tint); border-color: var(--err); }

/* Mensaje de la excepción que dejó un intento fallido. Monoespaciado y con salto de línea
   forzado: es texto de máquina, a veces largo, y truncarlo escondería justo la parte que dice
   qué pasó. */
.error-detail {
  display: block;
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 16px;
  color: var(--err);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ============================================================================================
   FICHA DE TENANT
   ============================================================================================ */

/* Secciones con título propio, más simples que las tarjetas de la AdminApp pero mismo lenguaje
   visual (borde + fondo blanco + esquina redondeada). */
.card-section {
  background: var(--card); border: 1px solid var(--outline-faint); border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  padding: 16px 20px; margin-bottom: 16px;
}
/* El h2 global lleva regla inferior y margen superior grande; dentro de una tarjeta sobra lo
   segundo y la regla se queda, que es lo que separa el título de la rejilla de datos. */
.card-section h2 { margin: 0 0 14px; }
/* Tabla dentro de una sección: ya está dentro de una tarjeta, así que .table-wrap no vuelve a
   dibujar borde ni sombra -- tarjeta dentro de tarjeta es justo lo que no debe pasar. */
.card-section .table-wrap { border: 0; border-radius: 0; box-shadow: none; }
.card-section .table-wrap th { border-top: 1px solid var(--outline-faint); }

.kv-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px 24px;
}
.kv { display: flex; flex-direction: column; gap: 1px; font-variant-numeric: tabular-nums; }
.kv-l {
  font-size: var(--text-xs); font-weight: 500; text-transform: uppercase; letter-spacing: .04em;
  color: var(--tertiary);
}

/* ============================================================================================
   DIÁLOGO DE CONFIRMACIÓN
   ============================================================================================
 * Port del componente que la AdminApp tiene desde la tanda 38: <dialog> nativo, sin librería y
 * sin alert del navegador. Se porta y no se comparte porque las dos aplicaciones no comparten
 * hoja de estilos.
 */
.confirm-dialog {
  width: min(420px, calc(100vw - 32px)); padding: 20px; border: 1px solid var(--outline-faint);
  border-radius: var(--radius-md); background: var(--card); color: var(--on-surface);
  font-family: inherit; box-shadow: var(--shadow-modal);
}
.confirm-dialog::backdrop { background: var(--scrim); }
.confirm-dialog__message { margin: 0 0 18px; font-size: var(--text-base); line-height: 20px; }
.confirm-dialog__actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.confirm-dialog__actions .button {
  display: inline-flex; align-items: center; height: var(--control-md); padding: 0 14px;
  border: 0; border-radius: var(--radius-sm); background: var(--primary); color: var(--on-primary);
  font-family: inherit; font-size: var(--text-md); font-weight: 500; cursor: pointer;
  transition: background-color var(--t-fast), color var(--t-fast);
}
.confirm-dialog__actions .button:hover { background: var(--accent-strong); }
.confirm-dialog__actions .button--ghost {
  background: transparent; color: var(--on-surface-variant); border: 1px solid var(--outline);
}
.confirm-dialog__actions .button--ghost:hover { background: var(--surface-low); color: var(--primary); }

/* ============================================================================================
   MÓVIL
   ============================================================================================
 *
 * `max-width` y no mobile-first, DELIBERADO y no por descuido. El design system de la AdminApp
 * prohíbe `max-width` y exige mobile-first, pero esta consola está declarada FUERA de su alcance
 * (§11 de docs/06-design-system.md) y su hoja es desktop-first de arriba abajo: invertirla sería
 * reescribir este fichero entero para un beneficio de usuario nulo. Lo que cambia aquí es un
 * chasis, no una escala tipográfica.
 *
 * ALCANCE, dicho para que nadie lo lea de más: esto hace la consola USABLE en un teléfono, no la
 * rediseña para él. Las tablas siguen teniendo de seis a once columnas y se desplazan en
 * horizontal dentro de .table-wrap, que es lo que ya hacían. Apilarlas en tarjeta por fila es
 * trabajo por tabla y se dejó fuera a propósito.
 *
 * 900px: por debajo de ese ancho, un carril de 232px se come una cuarta parte de la pantalla y
 * las tablas se quedan sin sitio. No es el ancho de ningún dispositivo, es donde este chasis
 * concreto deja de caber.
 */
@media (max-width: 899px) {
  /*
   * El carril deja de ser columna y se parte en dos piezas: una BARRA fija arriba (marca,
   * hamburguesa y destino actual) y un CAJÓN que flota sobre el lienzo.
   *
   * Cajón flotante y no acordeón que empuja: desplegando hacia abajo, los seis destinos y el
   * pie de sesión empujaban la página entera y había que cerrarlos para volver a ver el dato.
   * El cajón se superpone, se va igual de rápido y deja el contenido donde estaba.
   */
  .fleet-shell { flex-direction: column; padding-top: var(--bar-h); }

  .fleet-sidebar {
    position: fixed; top: 0; left: 0; right: 0;
    flex: none; height: var(--bar-h); max-height: var(--bar-h);
    overflow: visible;
    z-index: 30;
    box-shadow: var(--shadow-sm);
  }

  /*
   * Aquí el <details> SÍ se comporta como tal: se deshacen las dos revelaciones forzadas del
   * bloque de escritorio. Las dos, y por el mismo motivo por el que allí hacían falta las dos —
   * ::details-content en los motores actuales, `display` en los anteriores.
   */
  .fleet-rail:not([open])::details-content { content-visibility: hidden; display: none; }
  .fleet-rail:not([open]) > .fleet-rail__panel { display: none; }
  .fleet-rail::details-content { display: block; }

  /* La barra: la marca a la izquierda tras la hamburguesa, el destino actual a la derecha. */
  .fleet-rail__bar {
    cursor: pointer;
    height: var(--bar-h); min-height: var(--bar-h); margin-bottom: 0;
    padding: 0 12px;
  }

  /* La hamburguesa. 44px de lado: es el punto de toque, no un adorno al lado de la marca. */
  .fleet-rail__toggle {
    display: inline-flex; align-items: center; justify-content: center;
    flex: none; width: 44px; height: 44px; margin-left: -10px;
    border-radius: var(--radius-sm);
    color: var(--secondary);
  }
  .fleet-rail__bar:hover .fleet-rail__toggle { background: var(--card); }

  /* Los dos iconos ocupan el mismo hueco y se alternan con el estado: el botón que abre cierra. */
  .fleet-rail__toggle-icon--close,
  .fleet-rail[open] .fleet-rail__toggle-icon--menu { display: none; }
  .fleet-rail[open] .fleet-rail__toggle-icon--close { display: inline-flex; }

  .fleet-rail__current {
    display: inline-flex; align-items: center;
    margin-left: auto; padding-left: 10px;
    font-size: var(--text-sm); font-weight: 600; color: var(--tertiary);
  }

  /* El chevron era el indicador del acordeón. Con hamburguesa sobra: el icono ya dice el estado. */
  .fleet-rail__chevron { display: none; }

  /*
   * El cajón. Ancho fijo con tope relativo para que en una pantalla estrecha siga asomando el
   * lienzo por la derecha — ver el borde de lo que hay detrás es lo que dice que esto se cierra.
   */
  .fleet-rail[open] > .fleet-rail__panel {
    position: fixed; top: var(--bar-h); left: 0; bottom: 0;
    width: min(284px, 82vw);
    background: var(--rail);
    box-shadow: var(--shadow-modal);
    overflow-y: auto; padding-bottom: 12px;
    z-index: 31;
    animation: fleet-rail-in 180ms ease;
  }

  /*
   * El velo. Solo entra la apertura: al cerrar, el navegador retira el contenido del <details>
   * de golpe y una salida animada exigiría gestionar el estado con script — que es justo lo que
   * este cajón evita.
   */
  .fleet-rail__scrim {
    display: block;
    position: fixed; inset: var(--bar-h) 0 0 0;
    width: auto; height: auto; padding: 0; margin: 0;
    border: 0; border-radius: 0;
    background: var(--scrim);
    cursor: pointer;
    /* Arrastrar sobre el velo no desplaza la página de detrás. Es la alternativa barata a
       bloquear el scroll del <body> con script, que exigiría deshacerlo en cada salida. */
    touch-action: none;
    z-index: 30;
    animation: fleet-scrim-in 180ms ease;
  }

  @keyframes fleet-rail-in {
    from { transform: translateX(-100%); }
    to { transform: translateX(0); }
  }

  @keyframes fleet-scrim-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }

  /* Quien pidió no animar no recibe animación: aquí no se pierde nada, el cajón aparece y ya. */
  @media (prefers-reduced-motion: reduce) {
    .fleet-rail[open] > .fleet-rail__panel,
    .fleet-rail__scrim { animation: none; }
  }

  /* Área de toque de verdad en los destinos: 30px vale con un ratón, no con un pulgar. */
  .fleet-sidebar__link { height: auto; min-height: 44px; }

  /* Gutter del lienzo: 28px de margen lateral en 375px son 56px de los 375. */
  .fleet { padding: 16px 16px 28px; }

  /*
   * La tira de KPI pasa a desplazarse en horizontal en vez de envolverse. Envolviéndola, cinco
   * cifras se convierten en tres filas que empujan la tabla fuera de la primera pantalla; en
   * carrusel, la primera cifra —la que importa— se ve siempre y el resto se arrastra. El borde
   * de la siguiente asomando es lo que dice que hay más, sin pintar puntos ni flechas.
   */
  .fleet-head { align-items: flex-start; }

  .stats {
    width: 100%;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .stats::-webkit-scrollbar { display: none; }
  .stats > * { scroll-snap-align: start; flex: 0 0 auto; }

  /* El mensaje de un log deja de tener techo fijo: 520px no existen en un teléfono. */
  td.message { max-width: none; }
}

/* ---- Barra del sondeo en vivo ----------------------------------------------------------------
 * La voz del refresco en sitio (wwwroot/js/auto-refresh.js): avisa de registros nuevos, de que
 * el sondeo se ha parado o de que la sesión caducó, siempre con la acción al lado.
 *
 * Es informativa y no un error, así que no lleva el rojo de .notice: tono neutro, y el peso lo
 * pone el botón. La construye el módulo, nunca una vista — sin JavaScript no significa nada.
 */
.live-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px;
  margin-bottom: 10px; padding: 8px 12px;
  background: var(--accent-tint);
  border: 1px solid var(--outline-faint); border-radius: var(--radius-sm);
  font-size: var(--text-md); color: var(--on-surface-variant);
}

.live-bar[hidden] { display: none; }

.live-bar__action {
  height: var(--control-md); padding: 0 12px;
  border: 1px solid var(--outline); border-radius: var(--radius-sm);
  background: var(--card); color: var(--accent-strong);
  font: inherit; font-weight: 600; cursor: pointer;
  transition: background-color var(--t-fast), border-color var(--t-fast);
}

.live-bar__action:hover { background: var(--surface); border-color: var(--accent); }

/* El área de toque manda sobre la densidad en cuanto no hay puntero. */
@media (max-width: 899px) {
  .live-bar__action { height: 44px; padding: 0 16px; }
}
