/* ============================================================
 * Lean UI — biblioteca de componentes
 * ------------------------------------------------------------
 * Construidos SOLO sobre los tokens (lean-tokens.css). Prefijo
 * .lean- para convivir con el panel mientras se migra. El
 * comportamiento (toast, tabs) vive en lean.js. Documentado en /guia.
 * ============================================================ */

/* ==================== LA ENTRADA, Y CUÁNDO SE ANIMA ====================
 *
 * UNA sola animación de aparición para toda la aplicación: `lean-entra`, con la
 * duración (`--t-entra`) y la distancia (`--entra-y`) de los tokens. Quien
 * necesite que algo aparezca, usa ésta; no se escribe otra.
 *
 * LA REGLA DE CUÁNDO, que es la que de verdad importa:
 *
 *   SÍ se anima lo que APARECE porque alguien lo ha pedido: un panel al cambiar
 *   de pestaña, un diálogo, el cajón de una ficha, un menú, una fila que llega
 *   en vivo, una nota recién escrita.
 *
 *   NO se anima el REPINTADO de datos que ya estaban. La lista de equipos se
 *   refresca sola cada quince segundos y la de una red al terminar un recorrido:
 *   animar eso convierte una pantalla de trabajo en un cartel de feria, y además
 *   cada refresco reiniciaría la animación de las filas que el operador está
 *   leyendo. Por eso la tabla virtual reutiliza las filas que no han cambiado
 *   (ver tabla-virtual.js): lo que no cambia, ni se repinta ni se anima.
 *
 *   NO se anima el CAMBIO de vista principal. Es una herramienta donde se salta
 *   de pantalla decenas de veces al día, y ahí una entrada deja de ser un detalle
 *   y pasa a ser un peaje: son 150 ms de espera multiplicados por cada salto.
 *
 *   Sí se anima UNA sola vez por vista: la primera en que se llena, que es la
 *   única en que algo aparece de verdad. Las diez vistas viven en index.html y
 *   no se destruyen al salir, así que volver a Equipos no es que aparezca nada
 *   —son los mismos 169 equipos que se dejaron ahí—, y eso cae de lleno en el
 *   «NO» de abajo: es un repintado de datos que ya estaban. Lo aplica
 *   `cambiarVista` (arranque.js) llamando a `Lean.entra` sólo en el estreno.
 *
 * Y todo lo que se mueva anima `opacity` y `transform` y nada más: las dos las
 * resuelve el compositor sin repintar ni recalcular la maquetación. Animar
 * `height`, `top` o `width` cuesta un reflow por fotograma y se nota con una
 * tabla de doscientas filas debajo.
 */
@keyframes lean-entra {
  from { opacity: 0; transform: translateY(var(--entra-y)); }
  to   { opacity: 1; transform: none; }
}
/* La misma entrada, para contenido que se sustituye con `innerHTML` y por tanto
   no cambia de display: la aplica `Lean.entra(el)`. Ver la nota de esa función.

   LA CURVA ES `--ease-entra` Y NO `--ease`, que es lo que ponía. `--ease` va y
   vuelve —hover, color, lo que responde al ratón— y la tabla de curvas de
   `animaciones.md` §2 lo dice con todas las letras: «**No** para entradas». Esto
   es LA entrada de la aplicación, así que era la que más veces al día se veía
   con la curva del papel equivocado.

   Y el aviso está tres párrafos más abajo, escrito por quien no lo corrigió: la
   nota de «lo de dentro no entra a la vez» describe el defecto —«el lienzo con
   `--ease`, sus filas con `--ease-entra`»— y apaga las filas para que no se note
   la diferencia, en vez de quitar la diferencia. Al arreglarlo aquí, las dos
   capas ya contarían lo mismo; la regla de abajo se queda igualmente, porque su
   razón verdadera es otra y sigue en pie: una caja que entra es UNA cosa que
   entra, no un enjambre. */
.lean-entrando { animation: lean-entra var(--t-entra) var(--ease-entra) both; }

/* ---------- Y LO DE DENTRO NO ENTRA A LA VEZ ----------
 *
 * UNA CAJA QUE ENTRA ES UNA COSA QUE ENTRA, no un enjambre. Mientras el
 * contenedor se está desplazando, lo que lleva dentro se queda quieto en su
 * sitio: viaja con él, que es lo que hace un objeto de verdad.
 *
 * Sin esto se animaban las dos capas a la vez y con curvas distintas —el lienzo
 * con `--ease`, sus filas con `--ease-entra` y escalonadas—, y dos rectángulos
 * desplazándose uno dentro de otro a distinta velocidad NO se leen como dos
 * entradas: se leen como que la caja se deforma. Es de donde salía la sensación
 * de que la portada de la Wiki «entraba en escala», que no estaba escrita en
 * ninguna parte y por eso no había forma de encontrarla leyendo el CSS: no era
 * una animación mal hecha, era la suma de dos bien hechas.
 *
 * SE APAGA CON LOS TOKENS y no listando clases, por lo mismo que en la sección
 * de la Wiki: la lista habría que ampliarla cada vez que se anime algo nuevo y
 * nadie se acordaría. Los dos tokens los hereda todo lo que venga.
 *
 * Y VALE SÓLO MIENTRAS DURA, que es lo que lo hace seguro: `Lean.entra` quita la
 * clase al acabar la animación. Puesta para siempre, esta regla dejaría muda la
 * vista entera —ni el menú, ni el diálogo, ni la fila que llega en vivo—, que es
 * exactamente el fallo contrario y peor. */
.lean-entrando * { --t-entra: 0s; --stagger: 0s; }

/* La duración de una transición entre dos estados de la misma pantalla
   (`Lean.transicion`). Un poco más larga que la entrada, y a propósito: aquí no
   se trata de que algo aparezca, sino de que el ojo PUEDA SEGUIR una cosa
   concreta de un sitio a otro, y a 150 ms el recorrido no se ve, se adivina.

   ESE RAZONAMIENTO ES EL DE `--t-mueve`, palabra por palabra, y por eso ahora
   sale del token en vez de estar escrito aquí a mano: era el mismo número —260
   entonces, 320 ahora— gobernando lo mismo en dos sitios, y `Lean.mover` habría
   sido el tercero. La curva también: lo que vuela de un sitio a otro ya estaba
   en pantalla y no está apareciendo, así que le toca `--ease-mueve` y no
   `--ease`. */
::view-transition-group(*) { animation-duration: var(--t-mueve); animation-timing-function: var(--ease-mueve); }
/* Lo que no vuela se funde: sin esto, el navegador cruza las dos capturas
   enteras y durante un instante se ven las dos listas superpuestas. */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 180ms; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 1ms; }
}
/* Quien pide menos movimiento no lo pierde de vista: se queda el desvanecido,
   que es lo que dice que el contenido se ha sustituido, sin desplazamiento. */
@media (prefers-reduced-motion: reduce) {
  @keyframes lean-entra {
    from { opacity: 0; transform: none; }
    to   { opacity: 1; transform: none; }
  }
}

/* ==================== LO QUE ACABA DE LLEGAR ====================
 *
 * Una lista que se refresca sola tiene un problema que no tiene una que se
 * recarga a mano: algo APARECE sin que nadie lo haya pedido, y en una tabla de
 * cuarenta clientes una fila nueva no se distingue de las que ya estaban. La
 * marca de novedad existe para eso, y se apaga sola porque una novedad
 * permanente deja de ser una novedad y pasa a ser un adorno.
 *
 * LO QUE LLEVA EL MENSAJE ES EL LATIDO, no el color. El borde late en azul de
 * marca —muy despacio, cuatro segundos por ciclo— y va perdiendo fuelle hasta
 * quedarse exactamente igual que el resto.
 *
 * El azul es el de LO SELECCIONADO, y aquí no estorba porque lo seleccionado no
 * respira: son dos señales que no se pueden confundir aunque compartan tinta, y
 * eso evita tener que inventar un sexto color para algo que sale dos veces al
 * mes —que cuando volviera a salir ya nadie recordaría qué quería decir—. Antes
 * era tinta blanca por ese mismo razonamiento, pero el blanco a plena tinta
 * sobre el tema oscuro pesa como una alarma para decir «ha llegado una red».
 *
 * En TARJETA late el borde entero, que es lo que delimita la pieza. En FILA,
 * SÓLO la barra de la izquierda: el fondo ya está cogido por el resaltado del
 * ratón y por la fila abierta, y tres cosas distintas dichas con el mismo
 * recurso no se leen (llevaba fondo tenue además de la barra; se ha quitado).
 *
 * Aparece de GOLPE —si apareciera despacio nadie vería el momento en que
 * llegó— y se va despacio. Son DOS clases y no una: `lean-nueva` pinta y
 * `lean-saliendo` lleva la transición larga. Con una sola, la duración tendría
 * que vivir en `.lean-tarjeta`, y entonces el resaltado del ratón —que también
 * cambia el borde— tardaría doce segundos en aparecer en TODAS las tarjetas de
 * la aplicación. La duración larga sólo existe mientras algo se está apagando. */
/* DOS NÚMEROS, MULTIPLICADOS, Y AHÍ ESTÁ TODO EL EFECTO.
 *
 *   --np-lat  el LATIDO: sube y baja solo, en bucle, y no para nunca.
 *   --np-amp  la AMPLITUD: cuánto de ese latido se ve. Vale 1 mientras la marca
 *             está franca y baja a 0 durante el apagado.
 *
 * `--np-i` es el producto, y es lo único que miran las reglas de abajo. Separarlo
 * así es lo que permite que el latido se APAGUE en vez de cortarse: la animación
 * sigue corriendo igual de lenta hasta el final, y lo que se desvanece es su
 * recorrido. Bajando la opacidad de la animación entera —que es lo primero que
 * se intenta— el último ciclo se corta a mitad de camino y da un tirón justo en
 * el momento en que el efecto existía para no darlo.
 *
 * Los dos van registrados con `@property` porque una variable declarada a pelo
 * es sustitución de texto: no interpola. Sin registrar, `--np-amp` saltaría de 1
 * a 0 de golpe y no habría apagado ninguno, y `--np-lat` no latiría —los
 * fotogramas se aplicarían a saltos—. Es la diferencia entre que esto funcione y
 * que parezca que no hace nada.
 *
 * `inherits: true` a propósito en los dos: en una tabla quien lleva las clases
 * es el `<tr>` y quien pinta es el `<td>`. */
@property --np-lat { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --np-amp { syntax: "<number>"; inherits: true; initial-value: 0; }

/* El nombre lleva `novedad` dentro y no es capricho: se llamó `lean-latido` a
 * secas, y ese nombre YA ESTABA COGIDO más abajo por el punto verde de la barra
 * de estado. Dos `@keyframes` con el mismo nombre no son un error para CSS
 * —gana el último del archivo, sin una línea en la consola—, así que la primera
 * versión de esto se quedó sin latir y el borde no se movía: la animación
 * corría, pero corría los fotogramas del otro. Un nombre de `@keyframes` es
 * global a la hoja, y eso sigue siendo verdad aunque aquel otro ya se retirara:
 * el nombre específico es lo que impide que el choque vuelva. */
@keyframes lean-novedad-latido {
  0%, 100% { --np-lat: 0.30; }
  50%      { --np-lat: 1; }
}

/* El latido corre en los DOS estados —franco y apagándose—, que es justo lo que
 * hace que el apagado sea del movimiento y no un corte. */
.lean-nueva, .lean-saliendo {
  /* Los dos valores van declarados también aquí, y no sólo como `initial-value`
     de arriba, para que la marca siga viéndose donde el latido no llegue a
     correr: un navegador sin `@property`, la primera pantalla antes de que
     arranque la animación, una captura. Queda entonces el borde quieto a media
     tinta, que es exactamente lo que ve quien pide menos movimiento. Declararlos
     no le quita el sitio a la animación: una animación gana a una declaración
     normal en la cascada, pase lo que pase. */
  --np-lat: 0.75;
  --np-amp: 1;
  --np-i: calc(var(--np-lat) * var(--np-amp));
  animation: lean-novedad-latido var(--t-latido) ease-in-out infinite;
}
.lean-saliendo { --np-amp: 0; transition: --np-amp var(--t-novedad) linear; }

/* EN TARJETA se marca el borde entero, que es lo que delimita la pieza, con un
   halo muy flojo que le da cuerpo. Al 70 % en lo más alto del latido y no a
   plena tinta: lo que se dice es que ha llegado, no que sea lo más importante de
   la pantalla. */
.lean-tarjeta.lean-nueva, .lean-tarjeta.lean-saliendo {
  border-color: color-mix(in srgb, var(--nuevo) calc(var(--np-i) * 70%), var(--line));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--nuevo) calc(var(--np-i) * 14%), transparent);
}

/* EN FILA, SÓLO LA BARRA DE LA IZQUIERDA, y respirando igual.
 *
 * Llevaba además un fondo tenue en toda la fila y se ha ido: en una tabla el
 * fondo ya está cogido dos veces —el resaltado del ratón y la fila abierta—, así
 * que tres cosas distintas se decían con el mismo recurso y ninguna se leía. La
 * barra no la usa nadie más, no pelea con nada y basta: en una lista de cuarenta
 * filas, un borde que late a la izquierda se ve de reojo sin tapar el dato.
 *
 * El latido va en la ALFA de la barra y no mezclándola contra un fondo: la fila
 * puede estar resaltada por el ratón o abierta, y una barra mezclada contra
 * `--panel` saldría de un color sobre el fondo normal y de otro sobre el
 * resaltado. Con alfa se compone sobre lo que haya, sea lo que sea. */
tr.lean-nueva > td:first-child, tr.lean-saliendo > td:first-child {
  box-shadow: inset 3px 0 0 color-mix(in srgb, var(--nuevo) calc(var(--np-i) * 100%), transparent);
}

/* Y TODO LO DEMÁS SE MARCA COMO UNA FILA, que es el caso que faltaba.
 *
 * Media aplicación lista cosas en `<tr>` y la otra media en columnas de `div`
 * —Despliegue es la primera—, y para éstas no había dónde pintar la marca: la
 * regla de arriba pide un `<td>` y la de las tarjetas pinta un borde que estas
 * filas suelen tener cogido para su propio estado. Así que una lista hecha con
 * `div` se quedaba sin poder decir que algo acababa de llegar, que es
 * justamente lo que se mira en la pantalla donde se instalan los agentes.
 *
 * Va por descarte —lo que no es tarjeta ni fila de tabla— y no con una clase
 * nueva a propósito: así una lista futura hecha de cualquier cosa queda cubierta
 * sin que nadie tenga que acordarse de nada, que es la única forma de que esto
 * no se vuelva a quedar a medias. Y no puede pintar de más: nada de esto se
 * aplica si el componente no ha puesto la marca.
 *
 * Se dice igual que en una fila de tabla —la barra de la izquierda, mismo grosor
 * y mismo latido—, porque significa lo mismo: cambia de qué está hecha la fila,
 * no lo que se le está diciendo a quien mira.
 *
 * En `box-shadow` interior y no en `border-left`: el borde izquierdo de una fila
 * así suele decir otra cosa —en Despliegue, el ámbar de «sin vincular»— y las
 * dos pueden ser verdad a la vez. En la misma propiedad, la segunda borra a la
 * primera. */
.lean-nueva:not(.lean-tarjeta):not(tr),
.lean-saliendo:not(.lean-tarjeta):not(tr) {
  box-shadow: inset 3px 0 0 color-mix(in srgb, var(--nuevo) calc(var(--np-i) * 100%), transparent);
}

/* Quien pide menos movimiento no se queda sin saber qué ha llegado: se queda la
   marca QUIETA —el borde y la barra a media tinta, sin latir— y se apaga con la
   misma transición. Lo que sobra para esa persona es el latido, no el aviso. */
@media (prefers-reduced-motion: reduce) {
  .lean-nueva, .lean-saliendo { animation: none; --np-lat: 0.75; }
}

/* ---------- lean-reciente: y LA OTRA MITAD, la que no se mueve --------------
 *
 * `lean-nueva` dice «esto ha aparecido delante de ti»: es un suceso, tiene
 * testigo y se apaga. `lean-reciente` dice «esto se descubrió hace poco»: es un
 * DATO del servidor, vale igual para los cuatro técnicos que miren la pantalla y
 * sigue siendo verdad mañana por la mañana.
 *
 * Hacían falta las dos porque un panel lo miran varias personas a la vez y no a
 * la vez. Con una sola señal, o el que llega tarde no se entera de nada, o al
 * que llega tarde se le anima la entrada de algo que ocurrió hace dos horas, que
 * es mentir con movimiento. Quién decide quién lleva cuál es `Lean.novedades`.
 *
 * SE ESCRIBE, NO SE PINTA, y es la regla de la que sale toda su forma. El azul
 * de novedad es el mismo que el de lo seleccionado, y lo único que los separa es
 * que uno late (ver `--nuevo` en tokens.css): una barra azul QUIETA en una fila
 * se lee como «está seleccionada». Un distintivo con una palabra dentro no se
 * confunde con nada, dice cuánto hace en el globo y no cuesta un renglón.
 *
 * Va DENTRO de la fila o de la tarjeta, al lado del nombre, porque es un dato
 * más de esa cosa. Cuando además acaba de llegar, `lean-nueva` se lo pone
 * encima: primero se ve el latido con el distintivo dentro, y al calmarse queda
 * el distintivo. Una sola cosa que se sosiega, no dos señales que se relevan. */
.lean-reciente {
  display: inline-flex; align-items: center; flex: none;
  padding: 1px 6px; border-radius: var(--r-xs);
  font-size: var(--fs-xs); font-weight: var(--peso-titulo); line-height: var(--alto-ui);
  background: color-mix(in srgb, var(--nuevo) 12%, transparent);
  color: var(--nuevo-ink);
  border: 1px solid color-mix(in srgb, var(--nuevo) 30%, transparent);
}

/* ---------- lean-ico: el icono -----------------------------------------------
 * <svg class="lean-ico"><use href="/assets/lean/icons.svg#li-server"/></svg>
 *
 * Es el componente MÁS USADO del panel —243 veces en 24 vistas— y hasta ahora
 * era el único que no llevaba el prefijo: se llamaba `.ico`, a secas, en el
 * espacio global. Eso es una colisión esperando: cualquier `.ico` que alguien
 * escribiera en `app.css` para otra cosa heredaba 1,15em, o se lo llevaba por
 * delante. Y siendo el más usado, era además el peor sitio para tener la
 * excepción, porque es el que más gente copia.
 *
 * El tamaño va en `em` y no en píxeles a propósito: un icono acompaña a un
 * texto, así que crece con él. Puesto en píxeles habría que acordarse de
 * cambiarlo cada vez que cambia la letra de al lado, y no se acuerda nadie.
 *
 * `vertical-align: -.15em` es lo que lo sienta en la línea de base del texto en
 * vez de dejarlo flotando alto. `flex: none` evita que se encoja cuando el
 * texto de al lado es largo, que es como un icono acaba convertido en una raya.
 *
 * NO confundir con `.mi-ico`, que sigue en app.css: aquél es un dibujo escrito a
 * mano dentro del menú (SVG con su propio viewBox y su grosor de trazo), no un
 * símbolo del sprite. Son dos cosas distintas y juntarlas es rediseño. */
.lean-ico { width: 1.15em; height: 1.15em; flex: none; display: inline-block; vertical-align: -.15em; }

/* ---------- Utilidades ------------------------------------------------------
 * Lo poco que NO es un componente: hace una cosa, no tiene partes y no lleva
 * estados. El carril `lean-u-` existe para que se distinga de un vistazo, y
 * para que la lista se pueda mirar entera y siga siendo corta — una hoja de
 * utilidades que crece es una hoja de estilos con otro nombre.
 *
 * `lean-u-mono` era `.mono` en `app.css`: una palabra muy común ocupando el
 * espacio global, con 32 usos en seis vistas.
 *
 * Lleva tamaño, y no debería: una utilidad que hace dos cosas ya no es una
 * utilidad. Se conserva porque los 32 usos contaban con él y quitarlo los
 * cambiaba todos de golpe, que no es lo que toca en una mudanza. Lo que hay
 * detrás es que la monoespaciada se ve más grande que la de interfaz al mismo
 * tamaño nominal, así que baja un peldaño para que la línea no se desnivele.
 * Si algún día una serie tiene que ir en un titular, ese peldaño estorbará y
 * será el momento de separar las dos cosas. */
.lean-u-mono { font-family: var(--mono); font-size: var(--fs-sm); }

/* ---------- Qué se puede seleccionar con el ratón ---------------------------
 *
 * LA REGLA: se selecciona el CONTENIDO; no se selecciona la CAPA DE CONTROL.
 *
 * Un panel de gestión es un sitio donde se copia mucho —una MAC, un número de
 * serie, una IP, el nombre de un equipo para pegarlo en un correo— y por eso
 * aquí no se toca NADA de eso: en la ficha de un equipo se puede seguir
 * seleccionando hasta la última letra, incluidas tablas, notas y veredictos.
 *
 * Lo que estorba es lo otro: arrastrar para copiar una fila y llevarse de paso
 * el rótulo de los tres botones de acción; hacer doble clic en una pestaña para
 * cambiar de sitio y que se quede el texto pintado de azul; empezar una
 * selección dentro de un menú. Ninguna de esas palabras se copia jamás —«Editar»
 * o «Estado» no son datos, son mandos— y lo único que hacen al seleccionarse es
 * ensuciar la pantalla y hacer que el gesto siguiente empiece con una selección
 * heredada que hay que deshacer.
 *
 * Va aquí y no en cada componente porque es una decisión de sistema: cualquier
 * control nuevo la hereda por el hecho de ser un control, sin que nadie tenga
 * que acordarse.
 */
button, a.lean-boton, .lean-boton, .lean-tab, .lean-segmented, .menu-item, .nav-item,
.lean-check, .lean-radio, .lean-switch, summary,
.lean-ico, .mi-ico {
  user-select: none;
  -webkit-user-select: none;
}
/* Y nada de lo que haya DENTRO de un control: el icono, el contador, la etiqueta
   que lo acompaña. Es lo que se lleva por delante un doble clic. */
button *, .lean-boton *, .lean-tab *, .menu-item *, .nav-item * {
  user-select: none;
  -webkit-user-select: none;
}

/* ---------- Scrollbars ----------
 * Finos y discretos en todo Lean UI; el nativo del sistema es demasiado pesado. */
* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: var(--r-s); border: 3px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--faint); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ═══════════ EL BOTÓN ═══════════════════════════════════════════════════════
 *
 * SEIS PESOS Y UNA SOLA FORMA. Lo único que separa un peso del siguiente es
 * cuánta tinta lleva: mismo alto, mismo radio, misma tipografía, mismo hundido
 * al pulsar. Eso es lo que hace que una barra de cinco botones se lea como una
 * barra y no como cinco decisiones.
 *
 *   0  --llano    sin caja ............ apartarse: cerrar, cancelar, atrás, «…»
 *   1  --perfil   borde de tinta ...... acompañar: copiar, probar, editar
 *   2  --marcado  borde de acción ..... preferir: escanear, instalar, dar de alta
 *   3  --solido   relleno ............. resolver: guardar, crear, la consola
 *   4  --puerta   el sólido en talla lg ... entrar, terminar el asistente
 *   5  --peligro  rojo ................ deshacer, y sólido sólo al confirmar
 *
 * Y LA REGLA QUE LOS REPARTE, que es la parte que importa: **el peso lo decide
 * qué le pasa a la pantalla cuando pulsas**, no lo importante que le parezca al
 * que la escribe. Con eso el reparto se decide solo y deja de discutirse
 * pantalla por pantalla, que es lo que había: mil ciento cinco botones y cinco
 * maneras de pintarlos, repartidas por una «importancia» que nadie sabe medir.
 *
 * SE LLAMABAN primary · secondary · ghost · danger. Eran de los últimos
 * anglicismos que quedaban en una casa donde todo lo demás —`lean-tarjeta`,
 * `lean-rotulo`, `lean-cajon`— ya estaba en el idioma en que se habla del
 * panel. El renombrado es completo y sin alias: si quedara uno vivo habría dos
 * nombres para la misma cosa, que es exactamente lo que este sistema no
 * permite. */
.lean-boton {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  /* La altura la manda la talla, NUNCA el contenido: con sólo padding, el mismo
     botón medía 31 px con texto y 33 px si llevaba icono (1.15em pesa más que la
     línea de texto), y ninguno de los dos casaba con el campo de al lado. El
     padding vertical queda para que un texto que parta en dos líneas respire. */
  min-height: var(--ctl-h);
  padding: 6px 14px; border: 1px solid transparent;
  /* El radio sale de la TALLA (ver --r-ctl en tokens.css): 8 · 10 · 12 para
     30 · 36 · 44 px, que es lo que mantiene la misma proporción de esquina en
     las tres. Con `--r-s` fijo, el botón pequeño salía a 0,267 y el grande a
     0,182 y en la misma fila se leían como dos piezas distintas. */
  border-radius: var(--r-ctl);
  font: inherit; font-size: var(--fs-md); font-weight: var(--peso-medio); line-height: var(--alto-corto); white-space: nowrap;
  /* Un botón que es un enlace (`<a class="lean-boton">`) tiene que parecer un
     botón: sin subrayado ni al pasar por encima. */
  text-decoration: none;
  cursor: pointer; transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), transform var(--t-pulsa) var(--ease);
}
a.lean-boton:hover { text-decoration: none; }
/* EL ANILLO DE FOCO ES UN `outline`, Y NO UNA SOMBRA. Ésta es la parte que hay
   que respetar al tocarlo.

   Se pintaba con `box-shadow`, que es la MISMA propiedad con la que el sólido se
   levanta, y ahí se cruzaban dos reglas: `.lean-boton:focus-visible` suma (0,2,0)
   y `.lean-boton--solido:hover:not(:disabled)` suma (0,3,0). O sea que con el
   puntero encima de un botón enfocado —lo normal justo después de pulsarlo, y lo
   normal para quien navega con teclado y deja el ratón donde estaba— ganaba el
   hover y EL ANILLO DE FOCO DESAPARECÍA. Sin error y sólo en el peso más
   importante del panel.

   `outline` no compite con nada, sigue el `border-radius` él solo, y su `offset`
   da la separación que antes había que imitar con un aro de `--panel` por debajo.

   SIGUE BEBIENDO DE `--brand` Y NO DE `--accion`, a propósito y como el switch y
   el radio: el foco dice DÓNDE ESTÁS, que es lo que significa la marca; la
   acción es otra cosa desde que los dos tokens se separaron. */
.lean-boton:focus-visible {
  outline: var(--foco-ancho) solid var(--foco);
  outline-offset: var(--foco-hueco);
}
.lean-boton:disabled { opacity: .5; cursor: not-allowed; }

/* --- Y ACUSA RECIBO AL PULSARLO -------------------------------------------
   El botón tenía hover, foco y deshabilitado, pero no tenía `:active`: pulsar
   cualquier botón de este panel no producía ninguna respuesta física. La única
   excepción era `--cta`, que es uno por pantalla, así que el gesto que más veces
   se hace al día era justo el que no contestaba.

   UN PÍXEL Y UN 1,8 % DE ESCALA, en 70 ms. Los tres números van juntos y ninguno
   es decorativo: el desplazamiento solo se lee como que el botón se mueve, y la
   escala sola como que respira; los dos juntos son lo que el ojo interpreta como
   una tecla que se hunde. Y por debajo del 2 % de escala el gesto SE SIENTE sin
   llegar a verse, que es lo que separa un control sólido de un juguete.

   VA EN LA CLASE BASE, no en las variantes, y de ahí sale casi todo su valor:
   lo heredan los cientos de botones del panel —primarios, secundarios,
   fantasma, de icono, de peligro— sin tocar ni uno.

   NO SE APLICA AL DESHABILITADO NI AL QUE ESTÁ TRABAJANDO. Un botón que no va a
   hacer nada no debe contestar que sí, y uno que ya está en marcha tampoco: ahí
   el que habla es el icono que gira. */
.lean-boton:active:not(:disabled):not(.is-cargando) { transform: translateY(1px) scale(.982); }
@media (prefers-reduced-motion: reduce) {
  .lean-boton:active:not(:disabled):not(.is-cargando) { transform: none; }
}
/* --- 0 · LLANO ---------------------------------------------------------------
   Sin caja. Apartarse no es actuar: cerrar, cancelar, ignorar, volver atrás, el
   «…» que guarda el resto. Si llevara borde competiría con lo que sí hay que
   pulsar, y en un pie de diálogo eso cuesta una lectura: dos cajas del mismo
   tamaño obligan a leer las dos palabras para saber cuál es cuál. */
.lean-boton--llano     { background: transparent; color: var(--muted); }
.lean-boton--llano:hover:not(:disabled)     { background: var(--al-pasar); color: var(--ink); }

/* --- 1 · PERFIL --------------------------------------------------------------
   El caballo de batalla, y el peso mejor resuelto que ya tenía este sistema.

   El borde va con TINTA y no con `--line`, y es un arreglo medido: sobre un
   diálogo —que ya es `--panel`— el relleno del perfil desaparece y `--line`
   da 1,2:1 en oscuro, o sea que del botón sólo se veía el texto. Al lado de un
   sólido relleno, el ojo lee «el otro es más grande» midiendo los dos 36 px
   exactos: no era geometría, era un botón sin cuerpo visible. La mezcla con
   `--ink` se ve sobre cualquier fondo y en los tres temas.

   Y AL PASAR POR ENCIMA RESPONDE TAMBIÉN EL BORDE. Antes sólo cambiaba el
   fondo, y en el tema oscuro `--al-pasar` es tan tenue sobre el panel que el
   botón apenas acusaba el ratón: el hover se veía en los rellenos y no en el
   peso del que hay ciento sesenta y seis. Sube de 22 a 34, que es un peldaño de
   la misma escala y no un valor nuevo. */
.lean-boton--perfil    { background: var(--panel); color: var(--ink); border-color: color-mix(in srgb, var(--ink) 22%, transparent); }
.lean-boton--perfil:hover:not(:disabled) {
  background: var(--al-pasar);
  border-color: color-mix(in srgb, var(--ink) 34%, transparent);
}
/* El borde del perfil APAGADO se compensa: la opacidad del deshabilitado deja
   la mezcla de tinta a la mitad y el botón volvía a quedarse sin cuerpo —el
   defecto que arregló el borde con tinta, reabierto por otra puerta—. Al doble
   aquí, tras la opacidad se ve igual que el del encendido. */
.lean-boton--perfil:disabled { border-color: color-mix(in srgb, var(--ink) 44%, transparent); }

/* --- 2 · MARCADO -------------------------------------------------------------
   EL PELDAÑO QUE FALTABA: el perfil de siempre, escrito en la tinta de la
   acción. Es para la acción preferente de un bloque cuando no hay nada que
   RESOLVER —escanear una red, instalar el agente, dar de alta un equipo—, que
   hasta ahora obligaba a elegir entre gritar en relleno o no decir nada. Ese
   hueco es lo que llenaba el primario donde no tocaba, y por eso había ciento
   nueve rellenos de marca en un panel cuya regla dice «uno por pantalla».

   NO ES UN SÓLIDO FLOJO: es un perfil pesado. La diferencia importa al
   repartir, porque un marcado y un sólido en la misma barra siguen teniendo
   orden —el relleno gana siempre—, y dos sólidos no.

   Y EL LAVADO SE MEZCLA CONTRA `--panel`, NO CONTRA `transparent`: sobre un
   diálogo —que ya es panel— un lavado transparente se sumaría dos veces y el
   mismo botón saldría más teñido dentro de una ventana que fuera de ella. */
.lean-boton--marcado {
  background: var(--accion-lavado); color: var(--accion-tinta);
  border-color: var(--accion-filo);
}
.lean-boton--marcado:hover:not(:disabled) {
  background: var(--accion-lavado-2);
  border-color: var(--accion-filo-2);
}

/* --- 3 · SÓLIDO --------------------------------------------------------------
   La única pieza con relleno, y una por superficie.

   TRES CAPAS Y NINGUNA DECORACIÓN, que es la receta que hasta hoy sólo tenía el
   CTA —dos usos en todo el panel— mientras los ciento nueve rellenos de verdad
   se quedaban planos: el degradado corto que le da cuerpo, el filo de luz de
   1 px arriba —lo que hace que un rectángulo parezca una pieza física— y una
   sombra TEÑIDA del propio color en vez de gris, que es lo que separa un botón
   de 2026 de uno de hace diez años.

   EL CUERPO VA EN `background-color` Y EL VELO EN `background-image`, y esto no
   es una preferencia de escritura: un `linear-gradient` NO SE INTERPOLA, así que
   con el relleno entero en `background` el hover saltaba de un degradado a otro
   mientras el borde, la tinta y la sombra viajaban en 140 ms. Justo el defecto
   que la transición de la clase base existe para evitar, metido en el peso más
   visible del panel. Separados, lo que cambia es un color y eso sí viaja.

   Y EL VELO OSCURECE HACIA ABAJO en vez de aclarar hacia arriba. Con tinta
   blanca encima, la zona crítica de contraste es la MÁS CLARA del relleno: si el
   degradado la aclara, el valor que hay que auditar deja de ser el del token. En
   oscuro se ve sin discutir —#0F7FA8 da 4,54 con blanco, y un 10 % de blanco por
   arriba lo deja en 3,84—. Oscureciendo, `--accion` es el techo y todo lo demás
   sólo puede mejorar. Y es lo que hace una pieza iluminada desde arriba. */
.lean-boton--solido {
  background-color: var(--accion); background-image: var(--velo-cuerpo);
  color: var(--on-accion); font-weight: var(--peso-firme);
  /* SIN SOMBRA PROYECTADA, y queda solo el filo.
     Llevaba tres capas: este filo de luz —que no es una sombra sino el canto
     iluminado del bisel, y es lo que impide que el relleno se lea como un
     rectangulo pegado— mas dos de elevacion, una de contacto y otra larga que
     ademas crecia al pasar el raton. Decision de producto: el peso mas visible
     del panel no flota. El cuerpo, el velo y el filo bastan para que se lea como
     una pieza, y sin la elevacion deja de competir con las tarjetas, que son las
     que de verdad estan levantadas. */
  box-shadow: inset 0 1px 0 var(--filo-luz);
}
/* El hover cambia el CUERPO y no el velo, y en cada tema va en la dirección que
   ahí significa «te estoy respondiendo»: en claro oscurece —que además mejora la
   lectura de la tinta blanca— y en oscuro aclara. El token lo resuelve, así que
   aquí sólo hay una línea. */
.lean-boton--solido:hover:not(:disabled) {
  background-color: var(--accion-hover);
  /* El hover cambia SOLO el cuerpo. Antes tambien subia la elevacion de 14 a
     20 px, que era la mitad del efecto; sin sombra que crecer, lo que responde
     es el color, y eso es lo que la nota de arriba ya decia que tenia que ser. */
  box-shadow: inset 0 1px 0 var(--filo-luz);
}
/* Al pulsar, la sombra se mete DENTRO: la pieza deja de estar levantada. Es la
   otra mitad del hundido de la clase base, y sin ella un botón que baja un píxel
   sigue proyectando la sombra de cuando estaba arriba. */
.lean-boton--solido:active:not(:disabled):not(.is-cargando) {
  box-shadow: inset 0 1px 3px color-mix(in srgb, black 22%, transparent);
}
/* Apagado no flota: un botón que no va a hacer nada no puede seguir pareciendo
   una pieza levantada, y el velo se retira por lo mismo — el cuerpo se queda
   plano. */
.lean-boton--solido:disabled { box-shadow: none; background-image: none; }

/* --- 4 · PUERTA -------------------------------------------------------------
   No es otro peso: es el sólido en talla lg, para las dos pantallas que son
   literalmente una puerta —entrar y terminar el asistente—, donde el botón está
   solo en medio y no compite con nada.

   ERA UNA VARIANTE PROPIA (`--cta`) con su color, su degradado y su sombra
   escritos aparte, y ahí estaba el problema de fondo: la pieza mejor resuelta
   del sistema vivía en dos pantallas y no la heredaba nadie. Ahora la receta es
   del sólido y esto sólo cambia de talla, así que un ajuste en el relleno llega
   a los dos sitios a la vez. Se usa junto al peso: `--solido --puerta`. */
.lean-boton--puerta {
  min-height: var(--ctl-h-lg); border-radius: var(--r-ctl-lg);
  padding: 10px 22px; font-size: var(--fs-lg); font-weight: var(--peso-titulo); letter-spacing: var(--letra-ajuste);
}

/* --- 5 · PELIGRO ------------------------------------------------------------
   DOS FORMAS, Y LA DISTINCIÓN ES LA QUE FALTABA: perfil rojo donde se OFRECE
   —en una barra, en un menú, en una fila— y sólido rojo sólo donde se CONFIRMA,
   en el botón final del diálogo del que no se vuelve. Los veintiún botones de
   peligro del panel se pintaban todos igual, así que el que borra de verdad se
   veía exactamente como el que abre la pregunta.

   Es la misma gramática que el resto de la escala leída en rojo: el relleno es
   el compromiso. Y el rojo relleno es más oscuro que `--down` porque tiene que
   sostener tinta blanca (5,63) y porque un rojo claro relleno se lee como una
   alarma encendida y no como un botón que hay que pulsar. */
.lean-boton--peligro   { background: transparent; color: var(--down); border-color: color-mix(in srgb, var(--down) 42%, transparent); }
.lean-boton--peligro:hover:not(:disabled) {
  background: color-mix(in srgb, var(--down) 12%, transparent);
  border-color: color-mix(in srgb, var(--down) 66%, transparent);
}
.lean-boton--peligro-solido {
  background-color: var(--down-solido); background-image: var(--velo-cuerpo);
  color: #FFFFFF; border-color: transparent; font-weight: var(--peso-firme);
  /* Igual que el solido de marca: sin sombra proyectada, solo el filo. Los dos
     pesos rellenos se levantaban y ahora ninguno lo hace — que un «Guardar»
     fuera plano y un «Borrar» flotara habria sido una diferencia sin
     significado, y en esta casa la forma solo cambia cuando cambia el papel. */
  box-shadow: inset 0 1px 0 var(--filo-luz);
}
/* Y el rojo del hover OSCURECE en los dos temas, no como el cian. Aclarar un
   rojo de aviso lo acerca al rosa y le quita la seriedad justo en el botón que
   no tiene vuelta atrás. */
.lean-boton--peligro-solido:hover:not(:disabled) {
  background-color: color-mix(in srgb, var(--down-solido) 88%, black);
}
.lean-boton--peligro-solido:active:not(:disabled):not(.is-cargando) {
  box-shadow: inset 0 1px 3px color-mix(in srgb, black 22%, transparent);
}
/* El anillo de foco de lo irreversible es ROJO y no de marca: es el único sitio
   del panel donde el foco tiene que decir algo más que «estás aquí». */
.lean-boton--peligro:focus-visible,
.lean-boton--peligro-solido:focus-visible {
  outline-color: color-mix(in srgb, var(--down) 66%, transparent);
}

/* --- Tallas y formas, ortogonales al peso -----------------------------------
   El radio baja con la talla (--r-ctl-sm), que es la otra mitad de la escala:
   sin esto, un botón de 30 px con el radio de uno de 36 se ve más cuadrado que
   sus vecinos en la misma barra. */
.lean-boton--sm  { min-height: var(--ctl-h-sm); border-radius: var(--r-ctl-sm); padding: 4px 10px; font-size: var(--fs-sm); }
/* Un botón de icono es CUADRADO: el mismo lado que de alto, o en una fila de
   acciones queda más estrecho que sus vecinos y la fila se ve dentada. El ancho
   sale de la talla, no del padding —así `--sm` ya no depende de qué regla vaya
   después, que es lo que dejaba el botón de icono «pequeño» midiendo lo mismo
   que uno normal y estirando las filas de una tabla densa—. */
/* `flex: none` porque un botón de icono es CUADRADO por definición: su ancho no
 * es una preferencia que se pueda recortar, es su forma. Como ítem de un
 * contenedor flex apretado se encogía —el `flex-shrink: 1` de serie gana al
 * `width`, y por eso ponerlo a mano no arreglaba nada— y el icono se quedaba sin
 * el aire de sus lados, pegado a los bordes. Medido en la barra de una red, con
 * el desplegable de equipo al lado: 15,8 px de los 30 que le tocan. Le pasa a
 * cualquier botón de icono en una barra que se quede corta, y a ninguno le
 * conviene: el texto de al lado se lee igual con una palabra menos, un icono
 * recortado no se lee de ninguna manera. Es la misma razón por la que
 * `lean-brandmark` ya lo lleva. */
.lean-boton--icono { width: var(--ctl-h); padding: 0; flex: none; }
.lean-boton--icono.lean-boton--sm { width: var(--ctl-h-sm); padding: 0; }
.lean-boton--block { width: 100%; }

/* --- Botón trabajando (Lean.cargando) --------------------------------------
   Un botón que está haciendo lo que se le pidió no puede parecer apagado: se
   bloquea igual que uno deshabilitado pero conserva su color, y en el sitio de
   su icono aparece la rueda, que es lo que dice que la orden salió y sigue en
   marcha.

   EL ICONO SE SUSTITUYE POR LA RUEDA Y NO SE GIRA, y esto era un fallo que sólo
   se veía en pantalla: la regla decía «gírale el icono» sin mirar cuál, y por
   aquí pasan una flecha de descarga, una papelera, un candado, un enchufe y un
   más. **Girar sólo significa «espera» si lo que gira es una flecha circular**;
   cualquier otra cosa dando vueltas no dice que se esté trabajando, dice que
   algo se ha roto. Lo hace `Lean.cargando`, así que ningún botón tiene que
   acordarse. */
.lean-boton.is-cargando { cursor: progress; opacity: 1; }
@keyframes lean-girar { to { transform: rotate(360deg); } }

/* Y DENTRO DE UN CONTROL, LA RUEDA ES DEL COLOR DEL TEXTO. Sin esto, la rueda de
   marca sobre un botón primario —que ya es de marca— desaparece, y sobre uno de
   peligro diría el color equivocado. La pista se saca del propio color en vez de
   `--line` por lo mismo: sobre un relleno azul, una línea gris clara no se ve. */
.lean-boton .lean-rueda { --rueda: 15px; color: currentColor; }
.lean-boton .lean-rueda .lean-rueda__pista { stroke: color-mix(in srgb, currentColor 26%, transparent); }

/* --- EL ICONO ACOMPAÑA AL GESTO -------------------------------------------
   La regla, antes que los casos, porque de ella depende que esto no acabe
   siendo un circo: **un icono sólo se mueve si el movimiento dice algo que el
   icono quieto no dice**. No se anima porque quede bien; se anima cuando el
   gesto es parte del significado.

   Con eso, la lista de los que se mueven es corta y cada uno tiene su razón, en
   vez de una regla general que le dé vida a los noventa. Un panel donde todo se
   menea no se siente cuidado: se siente inquieto.

   SE SELECCIONA POR `data-ico`, que lo pone `Lean.icono` para todos. CSS no
   puede preguntar a qué símbolo apunta un `<use>`, así que sin el atributo la
   única forma de darle a un icono concreto un gesto propio sería envolverlo en
   una clase inventada para la ocasión — y entonces cada pantalla se inventaría
   la suya. Lo descubrió la consola con su electrocardiograma (`np-barrer-traza`
   en app.css), que es el precedente y sigue siendo el mejor ejemplo: el trazo se
   barre a sí mismo mientras se mide, como un monitor de verdad.

   EL PRIMERO Y EL PATRÓN: la flecha de descarga BAJA al pulsarla. Dos píxeles,
   con el mismo tiempo que el hundido del botón, así que el icono y el botón se
   mueven juntos y no como dos animaciones distintas sobre la misma pieza. Dice
   lo que va a pasar —algo viene hacia ti— y es exactamente lo que ese botón NO
   decía cuando la flecha giraba sobre su eje. */
   SE SELECCIONAN DE DOS FORMAS Y HACEN FALTA LAS DOS: por `data-ico`, que lo
   pone `Lean.icono()`, y por el destino del `<use>`, que es lo que alcanza a los
   iconos escritos a mano en el marcado de una vista — que son muchos. Sin la
   segunda, el gesto valdría en unos botones sí y en otros no según cómo se
   hubiera escrito el icono, que es la peor forma de fallar: la que parece
   capricho. */
.lean-ico[data-ico="li-download"],
.lean-ico:has(use[href$="#li-download"]) { transition: transform var(--t-pulsa) var(--ease); }
.lean-boton:active:not(:disabled):not(.is-cargando) .lean-ico[data-ico="li-download"],
.lean-boton:active:not(:disabled):not(.is-cargando) .lean-ico:has(use[href$="#li-download"]) {
  transform: translateY(2px);
}

/* LA PAPELERA SE INCLINA AL PASAR POR ENCIMA, y es la única de la lista que se
   mueve para AVISAR en vez de para acompañar: ocurre ANTES de pulsar, no
   después. Nueve grados bastan para que la mano se lo piense un segundo, que es
   todo lo que se le pide a un gesto delante de algo que no se puede deshacer.

   El giro es desde la BASE (`transform-origin: 50% 85%`) y no desde el centro:
   una papelera que pivota por su medio se lee como un objeto flotando; una que
   pivota por donde se apoya se lee como una que se vuelca. */
.lean-ico[data-ico="li-trash-2"],
.lean-ico:has(use[href$="#li-trash-2"]) {
  transform-origin: 50% 85%;
  transition: transform var(--t-fast) var(--ease);
}
.lean-boton:hover:not(:disabled) .lean-ico[data-ico="li-trash-2"],
.lean-boton:hover:not(:disabled) .lean-ico:has(use[href$="#li-trash-2"]),
.menu-item:hover .lean-ico[data-ico="li-trash-2"],
.menu-item:hover .lean-ico:has(use[href$="#li-trash-2"]) { transform: rotate(-9deg); }

/* LA IMPRESORA SE REIMPRIME AL PULSARLA. Es el mismo recurso que el
   electrocardiograma de la consola —animar el guion del trazo, que es heredable
   y por eso atraviesa el `<use>` hasta el dibujo del sprite— aplicado a un
   suceso en vez de a un estado: el icono se dibuja de nuevo, una vez, como sale
   una etiqueta de la máquina del armario.

   80 es la longitud aproximada del trazo en su rejilla de 24. No hace falta que
   sea exacta: de más, el trazado empieza con el dibujo ya completo un instante;
   de menos, se corta. De más es lo seguro. */
.lean-boton:active:not(:disabled) .lean-ico[data-ico="li-printer"] use,
.lean-boton:active:not(:disabled) .lean-ico use[href$="#li-printer"] {
  animation: lean-ico-traza 520ms var(--ease-entra) both;
}
@keyframes lean-ico-traza {
  from { stroke-dasharray: 80; stroke-dashoffset: 80; }
  to   { stroke-dasharray: 80; stroke-dashoffset: 0; }
}

/* Y LA FLECHA CIRCULAR SÍ GIRA MIENTRAS TRABAJA, que es el caso honesto del que
   sale toda esta sección: aquí el dibujo y el gesto dicen lo mismo, así que una
   flecha circular girando es sencillamente una vuelta más. Es uno de los dos
   iconos a los que `Lean.cargando` no le pone la rueda encima, y no rompe la
   uniformidad que la rueda vino a traer: las dos cosas son un arco girando:
   cambia el dibujo, no el gesto. */
.lean-boton.is-cargando .lean-ico[data-ico="li-refresh-cw"],
.lean-boton.is-cargando .lean-ico:has(use[href$="#li-refresh-cw"]) {
  animation: lean-girar var(--t-giro) linear infinite;
}

/* Y EL TRAZO DE MEDIR SE BARRE A SÍ MISMO, que es el otro. `li-activity` es el
   icono de medir en toda la casa —diagnosticar, recorrer, disponibilidad— y su
   dibujo es un electrocardiograma: animar el guion del trazo lo convierte en un
   monitor encendido, que es literalmente lo que el botón acaba de poner en
   marcha.

   NO ES UN GESTO NUEVO. Existe desde el primer día en la consola
   (`np-barrer-traza`, en app.css) y en el mando de la vista Red, y era el mejor
   ejemplo de la regla de esta sección encerrado en dos pantallas: los demás
   botones de medir del panel —el de una ficha, el del cajón de un equipo, el de
   una fila de la lista— se quedaban con la rueda genérica, que dice «espera»
   pero no dice QUÉ está pasando. Sube tal cual, con su duración y su guion.

   FUNCIONA A TRAVÉS DEL `<use>` porque `stroke-dasharray` y `stroke-dashoffset`
   son heredables: la animación va en el SVG y el dibujo la recibe dentro del
   sprite. Es el mismo recurso que la impresora de aquí arriba. */
.lean-boton.is-cargando .lean-ico[data-ico="li-activity"],
.lean-boton.is-cargando .lean-ico:has(use[href$="#li-activity"]) {
  stroke-dasharray: 68 32;
  animation: lean-barrer-traza var(--t-espera) linear infinite;
}
@keyframes lean-barrer-traza {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lean-ico[data-ico="li-download"], .lean-ico:has(use[href$="#li-download"]),
  .lean-ico[data-ico="li-trash-2"], .lean-ico:has(use[href$="#li-trash-2"]) { transition: none; }
  .lean-boton:active .lean-ico, .lean-boton:hover .lean-ico, .menu-item:hover .lean-ico { transform: none; }
  .lean-boton:active .lean-ico use, .lean-boton.is-cargando .lean-ico { animation: none; }
  /* Y el trazo se queda ENTERO, no a medio barrer: sin animación, un guion
     puesto deja el icono partido en trozos para siempre. */
  .lean-boton.is-cargando .lean-ico[data-ico="li-activity"],
  .lean-boton.is-cargando .lean-ico:has(use[href$="#li-activity"]) { stroke-dasharray: none; }
}

/* --- Y BOTÓN QUE HA TERMINADO (Lean.cargando(btn, false, {hecho})) ----------
   La otra mitad, que faltaba entera: el botón giraba y, cuando llegaba la
   respuesta, recuperaba su texto de un tirón. O sea que el principio del trabajo
   se veía y el final no, y el único acuse era una `toast` en la esquina opuesta
   de la pantalla — lejos de donde está mirando el ojo, que es el botón que
   acaba de pulsar.

   EL ACUSE DE UNA ACCIÓN VA DONDE OCURRIÓ LA ACCIÓN. La `toast` se queda para
   lo que hay que poder deshacer y para lo que falla: eso sí conviene que se lea
   en otro sitio, porque pide una decisión y no sólo enterarse.

   ES LA COREOGRAFÍA DE `lean-haciendo` Y NO UNA NUEVA, a propósito: en esta
   interfaz un halo que se expande significa siempre lo mismo —aquí ha pasado
   algo ahora mismo—, así que el remate es el suyo con el color del resultado.
   Y dura lo que dura leerlo: `Lean.cargando` lo retira a los 900 ms y devuelve
   el botón a su texto, porque un botón que se queda diciendo «Guardado» es un
   botón que ya no dice qué hace.

   NO CAMBIA DE TAMAÑO al pasar a «hecho», y por eso el texto lo pone quien
   llama en vez de salir de aquí: un botón que se encoge al terminar mueve la
   fila de botones de al lado, y eso se ve mucho más que el propio acuse. */
/* La tinta del acuse es BLANCA y no `--on-brand`. Sobre `--ok` —que es un verde
   medio, 4,5 sobre el panel— la tinta oscura del cian daba 2,9: el «Guardado»
   que remata la acción era lo peor legible de la secuencia. Y es el mismo
   cambio que hizo el sólido, por la misma razón: un relleno que sostiene texto
   lleva tinta clara. */
/* Y EL ACUSE NO HEREDA EL RELIEVE DEL PESO QUE VENÍA DEBAJO. `background` sí se
   pisa —la abreviatura además borra el velo—, pero la sombra no la pisa nadie:
   un «Guardado» verde seguía proyectando el halo cian del sólido, y el halo del
   acuse (`::after`) se pintaba encima del otro. Se aplana y se queda sólo el
   filo, que es lo que mantiene la pieza con cuerpo mientras dura. */
.lean-boton.is-hecho { position: relative; background: var(--ok); color: #FFFFFF; border-color: transparent;
  box-shadow: inset 0 1px 0 var(--filo-luz); }
/* El aro sigue al radio del botón, y por eso hereda de la talla en vez de fijar
   `--r-s`: sobre un botón `--sm` el halo salía dos píxeles más cuadrado que la
   pieza que rodea, y eso se ve justo en el momento en que todo el mundo está
   mirando ahí. Los 2 px que se sale se le suman al radio para que las dos
   curvas sean concéntricas. */
.lean-boton.is-hecho::after {
  content: ""; position: absolute; inset: -2px; border-radius: calc(var(--r-ctl) + 2px); pointer-events: none;
  box-shadow: 0 0 0 1.5px var(--ok);
  animation: lean-remate var(--t) var(--ease-entra) both;
}
@keyframes lean-remate {
  from { transform: scale(1);    opacity: .6; }
  to   { transform: scale(1.06); opacity: 0; }
}
/* La marca se traza en vez de aparecer dibujada: verla hacerse es lo que cuenta
   que ha terminado ahora, y no que llevaba ahí todo el rato. */
.lean-boton--sm.is-hecho::after { border-radius: calc(var(--r-ctl-sm) + 2px); }
.lean-boton--puerta.is-hecho::after { border-radius: calc(var(--r-ctl-lg) + 2px); }
.lean-boton.is-hecho .lean-ico { animation: none; }
.lean-boton.is-hecho .lean-ico path,
.lean-boton.is-hecho .lean-ico use { stroke-dasharray: 24; animation: lean-trazar var(--t) var(--ease-entra) both; }
@keyframes lean-trazar { from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .lean-boton.is-hecho::after { animation: none; opacity: 0; }
  .lean-boton.is-hecho .lean-ico path,
  .lean-boton.is-hecho .lean-ico use { animation: none; stroke-dasharray: none; }
}

/* --- lean-rueda: la ÚNICA rueda de espera ----------------------------------
   Un arco de marca girando sobre una pista, para cuando hay que esperar y no
   hay ni porcentaje ni cara a la que agarrarlo. Sustituye a los cuatro giros
   sueltos que decían lo mismo a tres velocidades distintas.

   NO ES UN DIBUJO NUEVO: es `lean-presencia-giro` —el anillo que ya gira
   alrededor del punto de un equipo que está trabajando— sacado del punto y
   puesto en una caja propia. Por eso va a juego con el resto sin tener que
   ajustar nada: es la misma pieza.

   LA PISTA ES OBLIGATORIA y es lo único que cambia respecto al anillo del que
   sale. Un arco suelto sobre un fondo claro no se lee como un indicador, se lee
   como un círculo al que le falta un trozo; con el aro de `--line` por debajo,
   el hueco se entiende como recorrido. Es lo que ya hace `lean-haciendo`, que
   nació con las dos circunferencias.

   `--rueda`: el lado, por si hace falta más grande. El grosor no escala con él a
   propósito: 2 px es lo que hace que un arco de 16 px y otro de 32 se lean como
   la misma pieza en dos tamaños y no como dos piezas distintas. */
.lean-rueda {
  --rueda: 18px;
  width: var(--rueda); height: var(--rueda); flex: none; color: var(--brand);
  animation: lean-girar var(--t-giro) linear infinite;
}
.lean-rueda circle { fill: none; stroke-width: 2; }
.lean-rueda .lean-rueda__pista { stroke: var(--line); }
.lean-rueda .lean-rueda__arco  { stroke: currentColor; stroke-linecap: round; stroke-dasharray: 25 75; }
/* --lenta: para la espera larga sin porcentaje —un escaneo de red, una
   importación—. El arco crece y mengua mientras gira, que es decir «sigo aquí»
   sin prometer un final que nadie ha calculado. Es la misma respuesta que da
   `lean-haciendo` sin plazo, pero suelta: aquél sólo existe donde hay un
   avatar debajo. */
.lean-rueda--lenta { animation-duration: var(--t-espera); }
.lean-rueda--lenta .lean-rueda__arco { animation: lean-rueda-respira var(--t-espera) var(--ease) infinite; }
@keyframes lean-rueda-respira {
  0%, 100% { stroke-dasharray: 14 86; }
  50%      { stroke-dasharray: 52 48; }
}
/* Quien pide menos movimiento ve el estado, no el gesto: el arco se queda quieto
   y sigue diciendo que hay algo en marcha. */
@media (prefers-reduced-motion: reduce) {
  .lean-rueda, .lean-rueda--lenta .lean-rueda__arco { animation: none; }
}

/* Aquí vivía `lean-boton--solido lean-boton--puerta`, la acción principal de una superficie: color,
   degradado, filo de luz y sombra teñida escritos aparte para dos usos en todo
   el panel —entrar y terminar el asistente—.

   Era la pieza mejor resuelta del sistema encerrada en dos pantallas, mientras
   los ciento nueve rellenos de verdad se quedaban planos. Su receta es ahora la
   de `--solido` y lo que queda de ella es una TALLA (`--puerta`, arriba), así
   que un ajuste en el relleno llega a los dos sitios a la vez en vez de a uno.

   Lo que sí se ha perdido a propósito: el `translateY(-1px)` al pasar por
   encima. Un botón que se levanta con el ratón está bien cuando hay uno por
   pantalla, y se convierte en un panel que hierve cuando lo heredan los mil
   ciento cinco. Lo que acusa el ratón es el cuerpo del relleno; lo que acusa el
   clic sigue siendo el hundido de la clase base. */

/* ---------- lean-mando: el botón que arranca algo con duración ----------
 *
 * EL MANDO Y LO QUE ARRANCÓ SON EL MISMO OBJETO, y ésa es toda la pieza.
 *
 * EL PROBLEMA, dicho por Leandro delante de la vista Red: «el live editing
 * aparece de la nada». Y no le faltaba una animación: le faltaba SER ALGO. Eran
 * dos hermanos turnándose el hueco —el botón se escondía con `display:none`, la
 * barra de progreso aparecía a su izquierda empujándolo, y al terminar el botón
 * volvía sin que nada lo trajera—.
 *
 * Aquí la caja es UNA y no se destruye nunca; lo único que cambia es su ancho y
 * qué capa está visible. De ahí salen tres cosas que antes no se podían tener:
 * el clic se acusa en el sitio donde has pulsado, nada de alrededor se mueve, y
 * el final se ve terminar en el mismo objeto que lo empezó.
 *
 * SUBE DE `red.js`, donde nació y donde llevaba tiempo siendo lo mejor resuelto
 * de esa pantalla sin que lo heredara nadie. El mismo problema —un control que
 * arranca algo que dura y tiene que contar en qué va— lo tienen el botón de
 * medir de una ficha, el de importar de Snipe, el de publicar un enlace y el de
 * recorrer una red desde la consola. Es el mismo camino que hizo `lean-relevo`.
 *
 * ANATOMÍA. Un contenedor con `data-e` y una capa por estado:
 *
 *     <div class="lean-mando" data-e="reposo">
 *       <button class="lean-boton lean-boton--marcado" data-mando="reposo">…</button>
 *       <div class="lean-mando__curso" data-mando="curso" hidden>…</div>
 *     </div>
 *
 * y `Lean.mando(pieza, "curso")` para conmutar. El estado «reposo» es el único
 * con nombre reservado: es el que mide `auto` y no recorta.
 *
 * `--w` VA REGISTRADA (más abajo) porque una variable escrita a pelo NO dispara
 * la transición de lo que la usa: el ancho saltaba de 91 a 286 en un fotograma.
 * Es la misma lección que `--np-lat` y `--np-amp` en la consola.
 *
 * Y SE ANIMA `width`, que es disposición, a sabiendas: `scaleX` aplastaría el
 * radio y deformaría el contenido —que es justo lo que se viene a evitar— y esta
 * caja es un elemento de 30 o 36 px de alto en una barra, así que su cálculo no
 * arrastra a nada. Es el mismo compromiso que hace la isla de iOS.
 *
 * OJO CON EL `initial-value`: tiene que ser un valor VÁLIDO PARA EL `syntax`, y
 * llegó a decir `auto`, que no es un `<length>`. Con eso el navegador descarta la
 * regla ENTERA en silencio —sin error, sin aviso— y `--w` vuelve a ser una
 * variable sin registrar, o sea otra vez sin transición. Costó una tanda de
 * medidas encontrarlo: el estado conmutaba, el radio y el recorte entraban, y el
 * ancho se quedaba en cero. */
@property --w { syntax: "<length>"; inherits: false; initial-value: 0px; }

.lean-mando {
  position: relative; flex: none;
  height: var(--ctl-h); width: var(--w);
  transition: width var(--t-asienta) var(--asienta);
}
/* La talla la manda el botón que lleva dentro, así que se declara aquí y no se
   deduce: un mando con un `--sm` dentro y 36 px de alto deja la capa de curso
   descolgada seis píxeles respecto a sus vecinos de la barra. */
.lean-mando--sm { height: var(--ctl-h-sm); }
/* Y PARA MEDIR HACE FALTA SOLTAR EL ANCHO UN FOTOGRAMA. Registrada, `--w`
   siempre tiene valor, así que `var(--w, auto)` no cae nunca al respaldo: la
   única forma de preguntarle a una capa cuánto mide de verdad es quitarle el
   corsé mientras se le pregunta. Dura lo que dura una lectura, sin pintar. */
.lean-mando.is-midiendo { width: auto; transition: none; }
/* EL RECORTE SÓLO MIENTRAS TRABAJA, y esto no es un detalle: en reposo la caja
   puede llevar dentro un botón partido, cuyo `lean-menu` se despliega POR
   DEBAJO. Con `overflow:hidden` puesto siempre, elegir desde qué ordenador se
   escanea dejaba de ser posible — el menú se recortaba contra los 30 px de la
   barra.

   El radio es el de la TALLA y no `--r-s`, para que el recorte del mando y la
   esquina del botón que lleva dentro sean la misma curva. Con dos px de
   diferencia se ve una uña clara en cada esquina mientras dura el trabajo. */
/* Y EN REPOSO SE VUELVE A `auto` SÓLO MIENTRAS NADIE HAYA MEDIDO.
 *
 * `width: auto` es lo que deja que la caja la mida el botón antes de que el JS
 * haya pasado por aquí: `--w` está registrada, así que su valor de partida es
 * `0px` y sin este respaldo un mando nacería invisible hasta la primera llamada
 * a `mando()`.
 *
 * PERO `auto` NO INTERPOLA, y mientras fuera el valor de reposo la VUELTA a
 * botón no podía animarse: da igual lo que valga `--w`, porque esta regla gana
 * por especificidad (0,2,0 contra 0,1,0) y el destino de la transición acaba
 * siendo la palabra `auto`. Medido en el mando de Red: la ida animaba —ahí el
 * destino sí es una longitud— y la vuelta pasaba de 250,4 px a 91,2 en un solo
 * fotograma, con `getAnimations()` a cero. Un control que crece con muelle y
 * vuelve de golpe se lee como que se traba.
 *
 * `is-medido` la pone `mando()` la primera vez que escribe `--w`, así que el
 * respaldo sigue existiendo justo donde hace falta —antes de que nadie mida— y
 * desaparece en cuanto hay una longitud de verdad que animar. */
.lean-mando[data-e="reposo"] { width: auto; }
.lean-mando.is-medido[data-e="reposo"] { width: var(--w); }
.lean-mando:not([data-e="reposo"]) { overflow: hidden; border-radius: var(--r-ctl); }
.lean-mando--sm:not([data-e="reposo"]) { border-radius: var(--r-ctl-sm); }
/* Las capas se superponen y se turnan. Ancladas a la IZQUIERDA porque ahí vive
   el icono, que es lo único que no cambia de sitio entre un estado y otro. */
.lean-mando > * {
  position: absolute; inset: 0 auto 0 0; display: inline-flex; align-items: center;
  white-space: nowrap;
}
.lean-mando[data-e="reposo"] > * { position: static; }
/* Entra con la curva de entrada y detrás de la caja; sale con la de salida, sin
   retardo y más corta. Antes compartían las dos, y por eso se cruzaban mal. */
.lean-mando__capa--sale {
  animation: lean-mando-sale var(--t-sale) var(--ease-sale) both;
}
.lean-mando__capa--entra {
  animation: lean-mando-entra var(--t-entra) var(--ease-entra) var(--t-pulsa) backwards;
}
@keyframes lean-mando-sale {
  from { opacity: 1; filter: blur(0);     transform: none; }
  to   { opacity: 0; filter: blur(2.5px); transform: translateY(2px); }
}
@keyframes lean-mando-entra {
  from { opacity: 0; filter: blur(2.5px); transform: translateY(2px); }
  to   { opacity: 1; filter: blur(0);     transform: none; }
}
/* La capa de trabajo se pinta como un `--marcado` que no se puede pulsar: es lo
   que ese botón ha pasado a ser, no una caja distinta. Así el relevo no cambia
   de color ni de borde, sólo de contenido. */
.lean-mando__curso {
  gap: var(--s2); padding: 0 14px; height: 100%;
  border: 1px solid var(--accion-filo); border-radius: var(--r-ctl);
  background: var(--accion-lavado); color: var(--accion-tinta);
  font-size: var(--fs-md); font-weight: var(--peso-medio);
}
.lean-mando--sm .lean-mando__curso { border-radius: var(--r-ctl-sm); padding: 0 10px; font-size: var(--fs-sm); }
/* Y EL TRAZO DE MEDIR BARRE MIENTRAS EL MANDO TRABAJA, que es el mismo gesto que
   el del botón `is-cargando` traído al otro estado que significa lo mismo: aquí
   el que espera no es un botón bloqueado, es una caja que ya se ha convertido en
   lo que arrancó. Sin esto, un mando cuyo trabajo aún no ha dado su primera
   cifra se queda con un icono quieto justo en el momento en que hay que decir
   que la orden salió. */
.lean-mando:not([data-e="reposo"]) .lean-ico[data-ico="li-activity"],
.lean-mando:not([data-e="reposo"]) .lean-ico:has(use[href$="#li-activity"]) {
  stroke-dasharray: 68 32;
  animation: lean-barrer-traza var(--t-espera) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .lean-mando { transition: none; }
  .lean-mando__capa--sale, .lean-mando__capa--entra { animation-duration: 1ms; }
  .lean-mando:not([data-e="reposo"]) .lean-ico[data-ico="li-activity"],
  .lean-mando:not([data-e="reposo"]) .lean-ico:has(use[href$="#li-activity"]) {
    animation: none; stroke-dasharray: none;
  }
}

/* ---------- Badge y tag: LA REGLA DEL DISTINTIVO ---------------------------
 *
 * **UN DISTINTIVO NO LLEVA BORDE. NUNCA.** Ni de color ni gris. Lo que lo dibuja
 * es su papel: un lavado del color que dice, y la tinta encima.
 *
 * Y NO ES UNA PREFERENCIA, SE MIDE. Con el borde puesto, `lean-tag` medía 27,2 px
 * y `lean-badge` 25,2 — mismo relleno, misma letra, misma línea— porque el borde
 * añade un píxel por arriba y otro por abajo. O sea que **dos distintivos de la
 * misma familia en la misma fila salían descuadrados por dos píxeles**, y a la
 * altura de un renglón de tabla eso se ve. Quitarlo los deja en los dos a 25,2 y
 * la alineación deja de depender de cuál se haya elegido.
 *
 * El borde además engorda: un distintivo tiene que ser FINO. Es una nota al
 * margen de un dato, no una caja con la que compita — y con filo se lee como un
 * segundo control metido en la línea.
 *
 * LOS LAVADOS SUBEN AL QUITARLO, porque el borde era parte de lo que los definía:
 * los del tag pasan de 12 a 15 %, que es donde ya estaban los del badge. Así los
 * dos se ven igual de presentes y hay UN solo juego de intensidades.
 *
 * ALINEACIÓN: `vertical-align: middle` para cuando el distintivo va DENTRO de una
 * línea de texto. Un `inline-flex` se alinea por la línea base de su contenido,
 * así que el relleno de abajo lo empujaba por debajo del renglón que acompaña —se
 * ve en cuanto un badge va detrás de un nombre en una celda—. Cuando va en un
 * contenedor flex manda el `align-items` del padre, y esta línea no estorba.
 *
 * (Estado semántico, SIEMPRE con texto o icono; el color sólo refuerza.) */
.lean-badge {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  vertical-align: middle;
  padding: 3px 10px; border: 0; border-radius: var(--r-pill);
  font-size: var(--fs-sm); font-weight: var(--peso-medio); background: var(--panel-2); color: var(--muted);
}
.lean-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
/* LA TINTA DEL DISTINTIVO NO ES EL TOKEN DE ESTADO PELADO, y es una corrección
   de contraste medida (16/9/26).

   El razonamiento de `tokens.css` fija los tres estados contra `--panel` y anota
   que sobre `--panel-2` se quedan en ~4,0. Pero un distintivo no se lee sobre el
   panel desnudo: se lee sobre SU PROPIO LAVADO, y ahí los tres caían a 3,75 /
   3,74 / 3,77 en tema claro. Comprobado que bajar el lavado no arregla nada —del
   1 % al 15 % ninguno llega a 4,5—: el que se queda corto es la tinta.

   SE MEZCLA CON `--ink` Y NO CON NEGRO, que es lo que hace que valga para los dos
   temas con una sola declaración: `--ink` es oscuro en claro y claro en oscuro,
   así que la misma regla oscurece donde hace falta y aclara donde hace falta.
   Mezclar con `#000` habría arreglado el tema claro y estropeado el oscuro, donde
   la tinta va clara sobre fondo oscuro.

   Medido tras el cambio: claro 4,53 / 4,57 / 4,53 · oscuro 5,96 / 6,07 / 5,20.
   Y los tokens de estado NO se tocan: el verde de la casa sigue siendo el mismo
   en el punto, en la cifra y en el veredicto. */
.lean-badge--ok    { color: color-mix(in srgb, var(--ok) 85%, var(--ink));   background: color-mix(in srgb, var(--ok) 14%, transparent); }
.lean-badge--warn  { color: color-mix(in srgb, var(--warn) 85%, var(--ink)); background: color-mix(in srgb, var(--warn) 15%, transparent); }
.lean-badge--down  { color: color-mix(in srgb, var(--down) 87%, var(--ink)); background: color-mix(in srgb, var(--down) 13%, transparent); }
.lean-badge--muted { color: var(--muted); }

/* Aquí vivió `lean-canto` —la barra de color que decía de qué clase es una fila—
   durante exactamente un día, y se va entero. No por cómo estaba resuelto: por
   lo que salió al mirar sus cuatro clientes uno a uno.

   EN LOS CUATRO LA BARRA ERA REDUNDANTE:

     · «Por instalar»: el rótulo del bloque ya dice NO SABEN DE QUÉ ORDENADOR SON
       y las treinta y siete filas son iguales. **Una marca que llevan todos no
       marca**; repite el título treinta y siete veces.
     · Asignar a otro cliente: la fila ya trae `lean-tag--warn` con «Es de otra
       empresa», que además lo DICE en vez de codificarlo en un color.
     · Una red con avisos: ya trae `lean-badge--warn` con cuántos dispositivos
       necesitan atención — más de lo que decía la barra.
     · La Wiki: la página abierta ya se distingue por papel, peso y tinta.

   Así que retirarla no quita una sola señal: quita una repetición. Lo que sí
   deja escrito es el diagnóstico que la trajo, porque el error de partida sigue
   siendo fácil de repetir: **un `border-left` no es una barra de acento**. El
   borde es parte de la caja, hereda su radio y se dobla contra la esquina. Si
   algún día hace falta una barra de verdad —una que distinga unas filas de otras
   y no todas de nadie—, se hace con un pseudo-elemento absoluto que no llegue a
   la curva, como el destino activo del lateral, y NO con un borde. */

/* ---------- Tag / etiqueta ----------
 * Para clasificar y filtrar (categorías, atributos). A diferencia del badge
 * (estado), el tag es más neutro y puede ser quitable (con la x).
 *
 * SIN BORDE, como el badge y por lo mismo (ver la regla ahí arriba). Lo que
 * separa a los dos es la FORMA —píldora contra esquina de 6 px— y la tinta, no
 * un filo; y ahora los dos miden lo mismo, que era el otro problema.
 *
 * Los lavados suben de 12 a 15 % porque el borde era la mitad de lo que los
 * dibujaba: sin él y al 12 %, `--warn` sobre un panel oscuro se queda en croma 3
 * y el distintivo desaparece. Al 15 % coinciden con los del badge, así que hay
 * un solo juego de intensidades para los dos. */
.lean-tag {
  display: inline-flex; align-items: center; gap: 6px;
  vertical-align: middle;
  padding: 3px 8px; border: 0; border-radius: var(--r-xs);
  font-size: var(--fs-sm); font-weight: var(--peso-medio); background: var(--panel-2); color: var(--ink);
}
.lean-tag .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
.lean-tag--brand { background: color-mix(in srgb, var(--brand) 15%, transparent); color: var(--brand-ink); }
/* Misma corrección de contraste que en `lean-badge`, y por el mismo motivo: la
   etiqueta lleva su tinta sobre su propio lavado. Ver el bloque de allí. */
.lean-tag--ok    { background: color-mix(in srgb, var(--ok) 15%, transparent);   color: color-mix(in srgb, var(--ok) 85%, var(--ink)); }
.lean-tag--warn  { background: color-mix(in srgb, var(--warn) 16%, transparent); color: color-mix(in srgb, var(--warn) 85%, var(--ink)); }
.lean-tag--down  { background: color-mix(in srgb, var(--down) 14%, transparent); color: color-mix(in srgb, var(--down) 87%, var(--ink)); }

/* ---------- Tarjeta ---------- */
.lean-tarjeta { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow); padding: var(--s5); }
/* En un móvil los 24 px de cada lado son 48 de los 390 que hay: un octavo de la
   pantalla gastado en aire, y el que lo paga es el contenido. Baja un peldaño
   de la escala, no a un número inventado. */
@media (max-width: 560px) { .lean-tarjeta { padding: var(--s4); } }

/* ---------- Formularios ----------
 * Anatomía: label + control + ayuda/error. El foco usa un anillo (ring), no
 * solo cambio de borde, como las UIs actuales. */

/* El contenedor de un formulario: apila sus filas con aire entre ellas.
 *
 * Existía en el marcado de seis pantallas —el diálogo de empresa y oficina, los
 * tres del catálogo, los dos de la integración— y NO existía en la hoja de
 * estilos. Es decir, `<div class="lean-form">` no hacía absolutamente nada y sus
 * campos salían pegados unos a otros, con la etiqueta de uno tocando el borde
 * del control anterior: en un diálogo de dos campos parecía un campo con dos
 * cajas. Los de la integración se salvaban porque `.isp-form` les ponía encima
 * una rejilla con su propio hueco.
 *
 * El hueco es s4 —16 px— y no s3: entre dos filas de formulario tiene que caber
 * más aire que entre la etiqueta y su propio control (6 px), o las dos
 * separaciones se leen igual y deja de verse qué etiqueta gobierna qué caja. */
.lean-form { display: flex; flex-direction: column; gap: var(--s4); }
/* En una rejilla de dos columnas, la fila que ocupa el ancho entero. Vale para
   cualquier `.lean-form` que alguien convierta en rejilla en su propio ámbito. */
.lean-form > .full { grid-column: 1 / -1; }

.lean-formrow { display: flex; flex-direction: column; gap: 6px; }
.lean-label { font-size: var(--fs-md); font-weight: var(--peso-medio); color: var(--ink); }
.lean-help  { font-size: var(--fs-sm); color: var(--muted); }
.lean-help--down { color: var(--down); }
/* Y `--warn` para lo que NO impide seguir pero conviene saber antes de pulsar:
   «con quince columnas el documento aprieta la letra». Un aviso así en rojo se
   lee como un error y frena a quien tenía razón en lo que estaba pidiendo; el
   color de estado aquí es semántico y no decorativo. */
.lean-help--warn { color: var(--warn); }
/* Y `--ok` para el veredicto que aprueba: la fuerza de una contraseña que ya
   vale. Cerraba un trío que estaba a dos tercios —el mismo rótulo que puede
   decir «le faltan tres caracteres» en rojo tiene que poder decir «buena»—, y sin
   él la pantalla de elegir contraseña se quedaba sin forma de dar el visto sin
   estrenar una clase suya. */
.lean-help--ok { color: var(--ok); }
/* --bloque: la misma ayuda cuando explica UNA SECCIÓN entera y no un campo —el
   párrafo que hay bajo el título de un ajuste—. Lleva su propio hueco debajo,
   más interlínea porque se lee de seguido, y un ancho máximo: un renglón de
   ayuda a lo ancho de la pantalla se lee mal y además parece contenido.
   No declara `display` a propósito: se usa en `<p>` casi siempre pero también en
   `<span>` para un «Cargando…», y forzarlo a bloque convertiría ése en un
   párrafo suelto.
   Venía de `app.css` como `.seg-help`, con el nombre de la pantalla donde nació
   —«seguridad»— y usada ya en siete vistas que no eran ésa. Es el mismo camino
   que hizo `.consola-rotulo` en su día. */
.lean-help--bloque { margin: 0 0 var(--s3); line-height: var(--alto-ui); max-width: 430px; }
/* Y EL HUECO A SU DERECHA EN UN DIÁLOGO ANCHO NO ES UN FALLO DE MÁRGENES.
   Lo pareció: en uno de 720 px, con una lista a todo lo ancho debajo, estos
   430 dejan 242 px vacíos y el párrafo se lee como mal colocado. Se probó a
   abrirlo «hasta 62 caracteres» y MEDIDO daba 401 px — la tipografía del panel
   es más estrecha de lo que parece, así que el arreglo lo encogía. A 430 px
   caben unos 66 caracteres, que está dentro de la medida de lectura de toda la
   vida (45-75); llenar los 672 serían 104. El hueco es la medida, no un
   descuido. */

.lean-input, .lean-select, .lean-textarea, .lean-multi__campo {
  width: 100%; min-height: var(--ctl-h); padding: 7px 12px;
  /* UN CAMPO ES UN RECEPTÁCULO Y SE HUNDE. Se pintaba con `--panel`, o sea con
     el mismo papel que la tarjeta que lo contiene: medido en Ajustes › Usuarios,
     el buscador y su tarjeta daban exactamente el mismo color y lo único que
     dibujaba la caja era `--line`, que sobre el panel da 1,2:1 en oscuro. Un
     campo que sólo se ve por su borde no invita a escribir en él.
     En claro `--hondo` es el gris de siempre; en oscuro BAJA por debajo del
     panel, que es donde tiene que estar algo en lo que se mete texto. */
  background: var(--hondo); color: var(--ink);
  /* El radio es el de la TALLA y no `--r-s`, por lo mismo que en el botón: en
     la barra de Equipos el buscador, los tres filtros y el botón de dar de alta
     miden los cuatro 36 px y comparten fila. Si el radio subiera sólo en el
     botón, la diferencia de dos píxeles se leería como un descuido. */
  border: 1px solid var(--line); border-radius: var(--r-ctl);
  font: inherit; font-size: var(--fs-lg); line-height: var(--alto-nombre);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.lean-input::placeholder, .lean-textarea::placeholder { color: var(--muted); }
.lean-input:hover:not(:focus):not(:disabled),
.lean-select:hover:not(:focus), .lean-textarea:hover:not(:focus),
.lean-multi__campo:hover:not(:focus) { border-color: color-mix(in srgb, var(--ink) 22%, var(--line)); }
.lean-input:focus, .lean-select:focus, .lean-textarea:focus, .lean-multi__campo:focus-visible {
  outline: none; border-color: var(--foco);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
/* Apagado sube a la superficie: un campo que no se puede rellenar deja de ser un
   receptáculo, y con el hueco todavía hundido seguía pareciendo que esperaba algo. */
.lean-input:disabled, .lean-select:disabled, .lean-textarea:disabled { background: var(--panel-2); opacity: .7; cursor: not-allowed; }
.lean-input--down { border-color: var(--down); }
.lean-input--down:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--down) 20%, transparent); }
.lean-input--sm { min-height: var(--ctl-h-sm); border-radius: var(--r-ctl-sm); padding: 4px 10px; font-size: var(--fs-md); }
/* Los campos de fecha y hora traen su propio alto: el navegador les monta dentro
   un widget con sus casillas y su icono de calendario, y ese widget mide más que
   una línea de texto. Salían a 37,6 px y en el alta de un equipo la fecha de
   compra quedaba metro y medio por encima de los campos de su misma fila. Alto
   exacto y sin relleno vertical, para que el widget se centre dentro. */
.lean-input[type="date"], .lean-input[type="time"],
.lean-input[type="datetime-local"], .lean-input[type="month"], .lean-input[type="week"] {
  height: var(--ctl-h); padding-block: 0;
}
.lean-input--sm[type="date"], .lean-input--sm[type="time"],
.lean-input--sm[type="datetime-local"], .lean-input--sm[type="month"], .lean-input--sm[type="week"] {
  height: var(--ctl-h-sm);
}
/* El área de texto es la excepción: se mide por líneas, no por la talla de fila. */
/* El área de texto es la excepción también aquí: no mide una talla —se mide por
   líneas— así que le toca el radio de una caja y no el de un control. Con el de
   36 px, un cuadro de ocho renglones sale con las esquinas de un botón. */
.lean-textarea { resize: vertical; min-height: 80px; border-radius: var(--r-s); }

/* Select con flecha propia (la nativa es fea e inconsistente). */
.lean-select-wrap { position: relative; display: inline-block; width: 100%; }
.lean-select { appearance: none; -webkit-appearance: none; padding-right: 34px; }
.lean-select-wrap::after {
  content: ""; position: absolute; right: 13px; top: 50%; width: 7px; height: 7px;
  border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
  transform: translateY(-70%) rotate(45deg); pointer-events: none;
}

/* Grupo con addon (icono a la izquierda / botón a la derecha). El foco ilumina
 * todo el grupo, no solo el input: es el patrón moderno de "search bar". */
.lean-field {
  display: inline-flex; align-items: center; gap: var(--s2); width: 100%; min-height: var(--ctl-h); padding: 0 12px;
  /* El grupo con addon es un campo con más cosas dentro, así que se hunde igual:
     si no, el buscador de una barra y el campo de un formulario —que son la misma
     cosa para quien escribe— salen a dos alturas distintas. */
  background: var(--hondo); border: 1px solid var(--line); border-radius: var(--r-ctl); color: var(--muted);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.lean-field:hover:not(:focus-within) { border-color: color-mix(in srgb, var(--ink) 22%, var(--line)); }
.lean-field:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent); }
.lean-field input { flex: 1; min-width: 0; border: none; background: transparent; font: inherit; font-size: var(--fs-lg); color: var(--ink); outline: none; height: calc(var(--ctl-h) - 2px); }
.lean-field .lean-ico { color: var(--faint); }

/* ---------- Casilla, radio e interruptor ----------
 *
 * Los tres tenían la forma resuelta y el GESTO no: la marca de una casilla
 * aparecía con `opacity`, el punto de un radio también, y el pulgar del
 * interruptor se limitaba a trasladarse. Tres controles que se pulsan a diario y
 * en ninguno pasaba nada: encendido y apagado eran dos fotogramas.
 *
 * LO QUE SE LES AÑADE NO ES FÍSICA NUEVA, y es a propósito. La marca **se traza**
 * en vez de aparecer, que es exactamente lo que hace `is-hecho` en un botón que
 * acaba de terminar —misma animación, `lean-trazar`, y misma duración— porque
 * dice lo mismo: verlo hacerse cuenta que ha pasado AHORA y no que llevaba ahí
 * todo el rato. Y el color cambia en `--t-fast` mientras el trazo dura `--t`,
 * que es el reparto que esa pieza ya tenía probado: el relleno responde al dedo
 * y la marca se dibuja encima. No compiten —van en secuencia, no en el mismo
 * sitio—, que es lo que la ley de «dos cosas del mismo gesto» pide mirar.
 *
 * Y LA CAJA VACÍA SE HUNDE, como el campo: los tres son receptáculos. Se pintaba
 * con `--panel`, o sea del color de la tarjeta que la contiene. */
.lean-check, .lean-radio { display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; font-size: var(--fs-lg); }
.lean-check input, .lean-radio input { position: absolute; opacity: 0; width: 0; height: 0; }
.lean-check .box, .lean-radio .box {
  position: relative; overflow: hidden;
  width: 18px; height: 18px; flex: none; border: 1.5px solid var(--line); background: var(--hondo);
  display: grid; place-items: center;
  transition: border-color var(--t-fast) var(--ease), transform var(--t-pulsa) var(--ease);
}
/* EL COLOR ENTRA DESDE EL CENTRO, y ésta es la mitad del gesto que faltaba.
 *
 * El fondo saltaba de gris a cian en un fotograma mientras la marca se trazaba
 * encima AL MISMO TIEMPO, así que el trazo competía con un color que ya había
 * cambiado y no se veía nada. En dos piezas de dieciocho píxeles, eso es toda la
 * diferencia entre notar algo y no notar nada: **es una secuencia, no un
 * fotograma**.
 *
 * Un círculo que crece dentro de la caja. En la casilla se recorta contra el
 * radio de 5 px (`overflow: hidden`), así que el relleno entra redondeado y
 * acaba cuadrado — que es exactamente lo que hace una gota al extenderse. En el
 * radio la caja ya es redonda y el círculo la llena tal cual.
 *
 * `inset: -1px` para que el relleno se meta bajo el borde: con 0 quedaba una uña
 * del papel hundido entre el color y la línea. */
.lean-check .box::before, .lean-radio .box::before {
  content: ""; position: absolute; inset: -1px; border-radius: inherit;
  background: var(--brand);
  transform: scale(0);
  transition: transform var(--t) var(--ease-entra);
}
.lean-check input:checked + .box::before, .lean-radio input:checked + .box::before { transform: scale(1); }
/* Y AL SOLTARLO SE VA MÁS DEPRISA DE LO QUE VINO, que es la ley 3.1. */
.lean-check input:not(:checked) + .box::before, .lean-radio input:not(:checked) + .box::before {
  transition: transform var(--t-sale) var(--ease-sale);
}
.lean-check .box { border-radius: var(--r-xxs); }
.lean-radio .box { border-radius: 50%; }
/* Al pasar por encima, el borde acusa: era lo único de esta familia que no
   respondía al ratón, y una casilla de 18 px sin hover es difícil de encontrar
   en un formulario de veinte campos. */
.lean-check:hover .box, .lean-radio:hover .box { border-color: color-mix(in srgb, var(--ink) 34%, transparent); }
/* Y AL PULSAR SE HUNDE, con el mismo 0,982 y los mismos 70 ms que la tecla de un
   botón. Va aquí y no en el `input` porque quien recibe el clic es la etiqueta
   entera: pulsar el texto de una casilla la marca, y también tiene que contestar. */
.lean-check:active .box, .lean-radio:active .box { transform: scale(.92); }
.lean-check input:checked + .box, .lean-radio input:checked + .box { background: var(--brand); border-color: var(--brand); }
.lean-check:hover input:checked + .box, .lean-radio:hover input:checked + .box { border-color: var(--brand); }
/* LA PALOMITA ES DEL CONTROL, NO DEL CATÁLOGO DE ICONOS, y esa es la decisión
 * que hay que respetar al tocarla.
 *
 * Llevaba `li-check` del sprite, y no acababa de leerse como la marca de una
 * casilla por dos motivos que sólo se ven a este tamaño:
 *
 *   · GROSOR. El sprite va a `stroke-width: 2` sobre una rejilla de 24, que a
 *     13 px de render son 1,08 px de trazo. Una palomita nativa es GRUESA: es lo
 *     que la distingue de una uve dibujada con un lápiz fino.
 *   · DIRECCIÓN. El path de Lucide es `M20 6 9 17l-5-5`, o sea que empieza en la
 *     punta LARGA, baja al vértice y sube al brazo corto. Al trazarlo, la
 *     palomita se dibujaba de derecha a izquierda — al revés de como la escribe
 *     una mano, y el ojo lo nota aunque no sepa decir qué.
 *
 * Así que se dibuja aquí, con las proporciones del control: vértice centrado,
 * brazo corto a la mitad del largo (1:2,09) y el trazo saliendo del corto. No es
 * romper «un concepto, un icono»: la marca de un control no se elige de una lista
 * —es su forma—, igual que el punto de un radio no es un icono.
 *
 * `stroke-dasharray: 19` es la longitud real del trazo, 18,81, redondeada hacia
 * arriba. Ese margen importa poco pero se mide: con 24 —lo que había— el dibujo
 * arranca ya completo durante el primer 22 % de la animación, o sea 48 ms de los
 * 220 en los que no pasa nada visible.
 *
 * Y LLEGA DESPUÉS DEL COLOR, con 90 ms de retardo: sin ellos el trazo corría
 * sobre un fondo que todavía estaba entrando y las dos cosas se anulaban. */
.lean-check__v {
  position: relative; width: 12px; height: 12px; color: var(--on-brand);
  fill: none; stroke: currentColor; stroke-width: 3.2;
  stroke-linecap: round; stroke-linejoin: round;
}
.lean-check input:not(:checked) + .box .lean-check__v { opacity: 0; }
.lean-check input:checked + .box .lean-check__v { opacity: 1; }
/* `backwards` Y NO `both`, que es lo que cazó el guardián de animaciones y tenía
   razón: lo único que hace falta del relleno es mantener el primer fotograma
   —el trazo oculto— durante los 90 ms de retardo. `both` además se quedaría con
   `stroke-dashoffset` el resto de la vida del elemento, y una propiedad bajo el
   control de una animación rellenada no admite transiciones nunca más. Es
   exactamente lo que dejó sin avance el anillo de progreso.
   Al terminar, `stroke-dashoffset` vuelve a su valor de siempre —0, el trazo
   entero— así que soltarla no cuesta nada. */
.lean-check input:checked + .box .lean-check__v path {
  stroke-dasharray: 19; animation: lean-palomita var(--t) var(--ease-entra) 90ms backwards;
}
@keyframes lean-palomita { from { stroke-dashoffset: 19; } to { stroke-dashoffset: 0; } }
/* EL PUNTO DEL RADIO CRECE DESDE EL CENTRO, que es lo que hace un radio de
   verdad: la aguja no se enciende, se mete. Con `opacity` el punto aparecía ya
   del tamaño final y el gesto no decía nada.
   La curva es `--ease-entra` y no `--ease` por lo que dice el token: esto no es
   un cambio de estado que va y vuelve, es algo que APARECE, y lo que hace que se
   sienta acabado en vez de sólo colocado es que frene largo al final.
   Y llega DESPUÉS del color, como la palomita, con 70 ms: aquí menos porque no
   hay nada que dibujar, sólo que aparecer. */
.lean-radio .box::after {
  content: ""; position: relative; width: 7px; height: 7px; border-radius: 50%;
  background: var(--on-brand);
  transform: scale(0); transition: transform var(--t) var(--ease-entra) 70ms;
}
.lean-radio input:checked + .box::after { transform: scale(1); }
.lean-radio input:not(:checked) + .box::after { transition: transform var(--t-sale) var(--ease-sale); }
/* El foco es un `outline`, como en el botón y por lo mismo: con `box-shadow`
   competía con el hundido y con cualquier sombra que la caja llegue a tener. */
.lean-check input:focus-visible + .box, .lean-radio input:focus-visible + .box {
  outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco);
}
@media (prefers-reduced-motion: reduce) {
  .lean-check .box, .lean-radio .box { transition: none; }
  .lean-check:active .box, .lean-radio:active .box { transform: none; }
  .lean-check .box::before, .lean-radio .box::before,
  .lean-radio .box::after { transition: none; }
  .lean-check input:checked + .box .lean-check__v path { animation: none; stroke-dasharray: none; }
}

/* Switch / interruptor: para activar/desactivar al momento (sin "Guardar").
 *
 * ES EL ÚNICO DE LOS TRES QUE TIENE MASA, y ahí está su gesto: el pulgar no
 * cambia de sitio, VIAJA. Por eso lleva `--ease-mueve`, que es el token de «lo
 * que cambia de sitio» y hasta ahora sólo usaba el FLIP de las listas; con
 * `--ease`, que frena igual que arranca, un pulgar de 18 px se lee como un
 * fotograma sustituido y no como una pieza que se ha desplazado.
 *
 * PISTA Y PULGAR VAN A LA MISMA DURACIÓN Y ARRANCAN JUNTOS (`--t-fast`), que es
 * la ley de §3.3 de docs/animaciones.md: dos cosas del mismo gesto se mueven
 * igual o se ven pelear. Aquí se vería enseguida — el carril encendiéndose antes
 * de que el pulgar llegue deja un hueco de cian a la derecha de la bola.
 *
 * Y EL PULGAR SE ESTIRA MIENTRAS SE APRIETA. Es lo único que no es una
 * transición sino un estado (`:active`), y es lo que separa un interruptor que
 * se siente de uno que se mira: la pieza cede bajo el dedo y se recoge al
 * soltarla, ya en el otro extremo. El estirón va hacia dentro del carril
 * —`transform-origin` en el lado desde el que empuja— para que la bola no se
 * salga de su pista. */
.lean-switch { display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; font-size: var(--fs-lg); }
.lean-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.lean-switch .track {
  width: 38px; height: 22px; flex: none; border-radius: var(--r-pill);
  background: var(--hondo); box-shadow: inset 0 0 0 1px var(--line);
  position: relative; transition: background var(--t-fast) var(--ease-mueve), box-shadow var(--t-fast) var(--ease-mueve);
}
.lean-switch .track::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: var(--r-pill); background: #fff;
  box-shadow: 0 1px 2px rgba(0,0,0,.35), 0 0 0 .5px rgba(0,0,0,.12);
  transform-origin: 50% 50%;
  transition: transform var(--t-fast) var(--ease-mueve), width var(--t-fast) var(--ease-mueve);
}
.lean-switch input:checked + .track { background: var(--brand); box-shadow: inset 0 0 0 1px transparent; }
.lean-switch input:checked + .track::after { transform: translateX(16px); }
/* Apretando: la bola se alarga hacia donde va a ir. Dos reglas y no una porque
   el punto de anclaje cambia de lado con el estado — estirar siempre desde la
   izquierda sacaría el pulgar del carril al volver. */
.lean-switch:active input:not(:checked) + .track::after { width: 22px; }
.lean-switch:active input:checked + .track::after { width: 22px; transform: translateX(12px); }
.lean-switch input:focus-visible + .track {
  outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco);
}
/* Apagado no se pulsa y no lo disimula: el carril pierde su tinta y el cursor lo
   dice. Faltaba, y un interruptor sin permiso se veía exactamente igual que uno
   que sí se puede tocar. */
.lean-switch input:disabled + .track { opacity: .45; }
.lean-switch:has(input:disabled) { cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
  .lean-switch .track, .lean-switch .track::after { transition: none; }
  .lean-switch:active input + .track::after { width: 18px; }
  .lean-switch:active input:checked + .track::after { transform: translateX(16px); }
}

/* Rango: un valor continuo cuyo EFECTO se ve mientras se mueve.
 *
 * Es su única razón de existir, y lo que lo distingue de un campo numérico: si
 * hay que mirar el número para saber dónde se está, el control correcto es un
 * `lean-input`. Aquí lo que se mira es la foto encuadrándose, no la cifra.
 *
 * Va con `appearance: none` y el carril dibujado a mano porque el nativo trae el
 * azul del sistema operativo, que es el único color de esta interfaz que no sale
 * de los tokens. El pulgar hay que declararlo dos veces —WebKit y Firefox usan
 * pseudoelementos distintos y ninguno de los dos entiende la regla del otro—: si
 * se juntan en un selector con coma, el navegador que no reconozca uno de los
 * dos tira la regla ENTERA y se queda sin pulgar. */
.lean-range { appearance: none; width: 100%; height: 20px; background: transparent; cursor: pointer; }
.lean-range::-webkit-slider-runnable-track { height: 4px; border-radius: var(--r-pill); background: var(--line); }
.lean-range::-moz-range-track { height: 4px; border-radius: var(--r-pill); background: var(--line); }
.lean-range::-webkit-slider-thumb {
  appearance: none; width: 16px; height: 16px; margin-top: -6px;
  border-radius: 50%; background: var(--brand); border: 2px solid var(--panel);
}
.lean-range::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; background: var(--brand); border: 2px solid var(--panel);
}
.lean-range:focus-visible { outline: none; }
.lean-range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 25%, transparent); }
.lean-range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 25%, transparent); }
.lean-range:disabled { opacity: .5; cursor: not-allowed; }

/* ---------- Control segmentado ----------
 * Para elegir entre pocas opciones excluyentes de una misma naturaleza
 * (p.ej. una vista). Más compacto que unas pestañas. */
/* `width: fit-content` y no sólo `inline-flex`: dentro de un contenedor en
 * COLUMNA —un `lean-formrow`, que es donde vive cuando es un ajuste— el
 * `align-items: stretch` del padre gana al display y el control se estira a todo
 * el ancho del panel. Tres opciones repartidas en 1.200 px dejan de leerse como
 * un conmutador y pasan a leerse como tres botones sueltos. Se arregla con el
 * tamaño y no con `align-self` porque eso tocaría además la alineación VERTICAL,
 * y en una barra de herramientas —donde vive el resto de las veces— el control
 * va centrado. */
/* EL CARRIL ES UN RECEPTÁCULO Y LA PASTILLA ES LO QUE SE LEVANTA. Iba con
   `--panel-2`, que en oscuro es más CLARO que el panel: el carril quedaba por
   encima de la pastilla que lleva dentro, o sea el relieve al revés. Con el
   carril hundido y la pastilla en el papel de la superficie, el conmutador se
   lee como lo que es sin tocar una medida. */
.lean-segmented { display: inline-flex; width: fit-content; align-items: stretch; min-height: var(--ctl-h); padding: 3px; gap: 2px; background: var(--hondo); border-radius: var(--r-ctl); }
/* La pastilla de dentro descuenta los 3 px de relleno del carril: sin eso, dos
   curvas de distinto radio a tres píxeles una de otra dejan una uña de fondo en
   cada esquina de la opción activa. */
.lean-segmented button { border: none; background: transparent; color: var(--muted); font: inherit; font-size: var(--fs-md); font-weight: var(--peso-medio); padding: 5px 12px; border-radius: calc(var(--r-ctl) - 3px); cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }
.lean-segmented button:hover:not(.is-active) { color: var(--ink); }
.lean-segmented button.is-active { background: var(--panel); color: var(--ink); box-shadow: var(--shadow); }
/* CON MUCHAS OPCIONES Y POCA PANTALLA, LAS QUE SOBRAN BAJAN.
   El conmutador es de una fila por definición, pero el de la bitácora lleva
   seis rangos —«Hoy», «7 días», «30 días», «1 año», «Todo», «Personalizado»— y
   son 365 px metidos en 298: se salía del panel entero. Envolver mantiene las
   seis a la vista y cada botón conserva su forma, porque aquí el redondeo es de
   cada uno y no del grupo. Arrastrando de lado se conservaría la fila, pero hay
   que adivinar que quedan opciones fuera. */
@media (max-width: 560px) { .lean-segmented { flex-wrap: wrap; min-width: 0; max-width: 100%; } }

/* --pill: el mismo control con forma de píldora, para cuando hace de NAVEGACIÓN
   y no de ajuste. Un segmentado cuadrado dice «elige un valor de este campo»; en
   redondo se lee como un grupo de destinos, que es lo que es cuando sustituye a
   unas pestañas. */
.lean-segmented--pill { border-radius: var(--r-pill); }
.lean-segmented--pill button { border-radius: var(--r-pill); padding: 5px 14px; }
/* --iconos: el conmutador cuando cada opción es un icono y no una palabra. Un
 * icono no necesita el aire lateral que pide el texto, y sin centrarlo en
 * rejilla queda descolgado dentro de su pastilla.
 *
 * Venía de `app.css`, donde lo usaba el conmutador de vista del inventario
 * («Tabla» y «Tarjetas» son dos palabras que se leen una vez y ocupan para
 * siempre). Se mueve sin tocar una medida. */
.lean-segmented--iconos button { padding: 5px 9px; display: grid; place-items: center; }
.lean-segmented--iconos .lean-ico { width: var(--ico-md); height: var(--ico-md); }

/* --sm: LA TALLA QUE FALTABA, y faltaba justo donde este componente vive.
 *
 * `lean-segmented` medía 36 px —`--ctl-h`, el defecto— y no tenía más talla, así
 * que en las tres cabeceras donde comparte fila con un botón `--sm` de 30 los
 * dos controles se alineaban por su centro y todo lo de al lado caía TRES
 * PÍXELES más abajo. Se ve al saltar de Ordenadores a Monitores: el botón de dar
 * de alta cambia de sitio entre dos listas hermanas, y ninguna de las dos está
 * mal por su cuenta.
 *
 * Es exactamente lo que la nota de `--ctl-h` pide no hacer: «todo lo que se
 * pulsa y puede acabar en la misma fila que otra cosa mide una de las tres».
 * Aquí había un control que sólo sabía medir una.
 *
 * El relleno del botón baja de 5 a 4 px porque si no el contenido pide 32 y el
 * `min-height` de 30 no lo puede encoger: la talla se quedaría en el nombre. */
.lean-segmented--sm { min-height: var(--ctl-h-sm); border-radius: var(--r-ctl-sm); }
.lean-segmented--sm button { padding: 4px 10px; border-radius: calc(var(--r-ctl-sm) - 3px); }
.lean-segmented--sm.lean-segmented--iconos button { padding: 4px 8px; }

/* --block: ocupa el ancho que le den, como `lean-boton--block`.
 *
 * `lean-segmented` es `width: fit-content` a propósito —un conmutador mide lo
 * que miden sus opciones— y eso deja de valer en cuanto comparte columna con
 * botones: en `lean-opcion` los mandos van uno debajo de otro, y el que mide lo
 * suyo canta. Reparte el sobrante entre las opciones en vez de amontonarlas a un
 * lado, que es lo que haría con el ancho puesto a secas. */
.lean-segmented--block { width: 100%; }
.lean-segmented--block button { flex: 1; }

/* ---------- Pestañas ----------
 * Para separar secciones dentro de una misma pantalla (p.ej. en la ficha:
 * Inventario · Software · Intervenciones). */
.lean-tabs { display: flex; gap: var(--s4); border-bottom: 1px solid var(--line); position: relative; }
.lean-tab { border: none; background: none; font: inherit; font-size: var(--fs-lg); font-weight: var(--peso-medio); color: var(--muted); padding: 10px 2px; margin-bottom: -1px; cursor: pointer; border-bottom: 2px solid transparent; transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); }
.lean-tab:hover:not(.is-active) { color: var(--ink); }
.lean-tab.is-active { color: var(--ink); border-bottom-color: var(--brand); }

/* --- LA BARRA QUE VIAJA (Lean.marcarTab) -----------------------------------
   El mismo gesto —«estás aquí»— tenía dos respuestas en la misma pantalla: en el
   lateral la barra de acento RECORRE el camino de un destino al siguiente
   (`volarMarcador`, en arranque.js), y tres centímetros más abajo, en las
   pestañas de una ficha, la línea SALTA. Es la clase de incoherencia que no se
   sabe nombrar y se nota entera.

   ES UN AÑADIDO Y NUNCA LA SEÑAL, exactamente igual que en el lateral: quien
   dice cuál está activa sigue siendo el `border-bottom-color` de la regla de
   arriba, que es CSS puro. Si esto no se ejecuta —sin JavaScript, con menos
   movimiento pedido, o si nadie llama a `Lean.marcarTab`— las pestañas se
   comportan como siempre y la barra sencillamente no existe.

   Anima `transform` y `width`, y sólo esas dos. La barra se coloca con
   `translateX` en vez de con `left` porque un `left` animado recalcula la
   posición del elemento en cada fotograma; el ancho no tiene esa alternativa,
   pero es UNA caja de dos píxeles de alto y no arrastra a nadie. */
/* LA BARRA VIAJA CON EL GESTO, NO POR SU CUENTA.
 *
 * Iba a `--t-mueve` (320) porque cambia de sitio, y por vocabulario eso parece
 * lo correcto. Medido sobre la ficha de un equipo al pulsar «Historial»:
 *
 *     color de las pestañas   termina a los 143 ms
 *     el panel que entra      termina a los 153 ms
 *     la barra                termina a los 323 ms
 *
 * O sea que el gesto se asienta a los 150 y queda una barra de dos píxeles
 * moviéndose sola otros 170. Es el mismo resto, y casualmente el mismo número,
 * que §3.3 cuenta de la foto del equipo que volaba al cajón — el caso que se
 * midió y se retiró por esto exactamente.
 *
 * `--t-mueve` es para seguir con la vista un objeto que cruza la pantalla. Esto
 * no: la barra y el panel cuentan LA MISMA noticia —has cambiado de pestaña— y
 * por §3.3 tienen que moverse igual. `--t-entra` las deja terminando juntas.
 *
 * La curva sí se queda: lo que hace la barra es cambiar de sitio, y eso es
 * `--ease-mueve` aunque dure lo que dura la entrada de su panel. */
.lean-tabs__barra {
  position: absolute; bottom: -1px; left: 0; height: 2px; width: 0;
  background: var(--brand); border-radius: var(--r-micro); pointer-events: none;
  transition: transform var(--t-entra) var(--ease-mueve), width var(--t-entra) var(--ease-mueve);
}
/* Con la barra puesta, la pestaña activa suelta su propio subrayado: si no, se
   ven los dos y durante el viaje hay dos marcas de «estás aquí» a la vez. */
.lean-tabs:has(.lean-tabs__barra) .lean-tab.is-active { border-bottom-color: transparent; }
@media (prefers-reduced-motion: reduce) {
  .lean-tabs__barra { transition: none; }
}
/* CINCO PESTAÑAS NO CABEN EN UN MÓVIL, y aquí sí se arrastra en vez de envolver
   —al revés que en `lean-segmented`, dos componentes más arriba—.
   
   La diferencia es la forma: el conmutador es una caja con pastillas dentro y
   una segunda fila de pastillas sigue siendo lo mismo; una pestaña es una
   palabra sobre una línea, y en dos filas quedan dos líneas de subrayado, una
   de ellas suelta a media altura. Además, aquí el corte de la última palabra
   por el canto ES la señal de que hay más, que es la convención de toda la
   vida en una tira de pestañas.

   La barra de scroll se esconde: bajo una tira de 40 px de alto, quince píxeles
   de barra gris pesan más que las pestañas. Y `flex: none` para que las cinco
   conserven su ancho en vez de repartirse el que hay. */
@media (max-width: 560px) {
  .lean-tabs { overflow-x: auto; gap: var(--s3); scrollbar-width: none; }
  .lean-tabs::-webkit-scrollbar { display: none; }
  .lean-tab { flex: none; white-space: nowrap; }
}

/* EL PANEL QUE ENTRA. Cambiar de pestaña era un corte seco: el contenido de una
   desaparecía y el de la otra aparecía ya puesto, en el mismo fotograma, y en
   una ventana como la consola —donde los tres destinos se parecen: cifras,
   gráficas, tablas— no quedaba ni rastro de que algo había cambiado. Un
   desplazamiento corto dice «esto es nuevo» sin que haya que leerlo.

   Cuatro píxeles y `--t-entra`, sin escala y sin rebote: lo justo para que el
   ojo registre la sustitución. Un panel que salta o crece se lee como un
   juguete, y esto es una herramienta con la que se trabaja ocho horas — lo que
   la primera vez hace gracia, a la cuarta del día estorba.

   (Este párrafo decía «160 ms» y la regla nunca los tuvo: sale de `--t-entra`,
   que son 150. Un comentario con un número a mano al lado de una regla que usa
   el token es la forma más barata de que alguien corrija el token equivocado.)

   LA CURVA, como en `.lean-entrando`: es una entrada, así que `--ease-entra`.

   Se dispara sola: el panel oculto está en `display:none` por el atributo
   `hidden`, y una animación arranca de nuevo cada vez que un elemento vuelve a
   mostrarse. Sin tocar el JS de las pestañas, que sólo pone y quita `hidden`. */
[data-tabs] > [data-panel]:not([hidden]),
[data-tabs] [data-panel]:not([hidden]) {
  animation: lean-entra var(--t-entra) var(--ease-entra) both;
}

/* ---------- Acordeón ----------
 * Nativo (<details>/<summary>): accesible y sin JS. Para agrupar contenido
 * secundario que no debe ocupar sitio hasta que se pide. */
.lean-accordion { border: 1px solid var(--line); border-radius: var(--r-s); background: var(--panel); overflow: hidden; }
.lean-accordion details + details { border-top: 1px solid var(--line); }
.lean-accordion summary { list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: var(--s3); padding: var(--s3) var(--s4); font-weight: var(--peso-medio); }
.lean-accordion summary::-webkit-details-marker { display: none; }
.lean-accordion summary:hover { background: var(--al-pasar); }
.lean-accordion summary .lean-ico { color: var(--muted); transition: transform var(--t) var(--ease); }
.lean-accordion details[open] > summary .lean-ico { transform: rotate(90deg); }
.lean-accordion .lean-accordion__body { padding: 0 var(--s4) var(--s4); color: var(--muted); font-size: var(--fs-md); }

/* Variante en FILAS: una lista larga donde cada entrada se resume en un
   renglón —una etiqueta, un titular— y guarda el detalle dentro.
 *
 * Es la forma de que una lista de quince asuntos siga siendo una lista. Con
 * todo desplegado hay que atravesarla entera para saber qué contiene, y lo que
 * se busca —¿está esto cubierto?— se responde con los titulares.
 *
 * El cuerpo va sin relleno para que lo de dentro pueda ir a sangre (una tabla,
 * dos columnas enfrentadas); quien meta texto suelto le pone el suyo. */
.lean-accordion--filas summary { align-items: center; gap: var(--s3); font-weight: var(--peso-texto); }
.lean-accordion--filas .lean-accordion__t { flex: 1; min-width: 0; font-size: var(--fs-md); font-weight: var(--peso-titulo); color: var(--ink); }
.lean-accordion--filas .lean-accordion__body { padding: 0; font-size: var(--fs-md); }
/* La flecha se separa del resto: en esta variante el renglón puede llevar más
   iconos —el de una etiqueta, el de una referencia— y `summary .lean-ico` los
   rotaría todos al abrir. */
.lean-accordion--filas details[open] > summary .lean-ico { transform: none; }
.lean-accordion--filas .lean-accordion__flecha { flex: none; color: var(--muted); transition: transform var(--t) var(--ease); }
.lean-accordion--filas details[open] > summary .lean-accordion__flecha { transform: rotate(90deg); }
.lean-accordion--filas details[open] > summary { background: var(--panel-2); }

/* Animación sutil de apertura/cierre (usa interpolate-size de los tokens). */
@media (prefers-reduced-motion: no-preference) {
  .lean-accordion details::details-content {
    block-size: 0; opacity: 0; overflow: clip;
    transition: block-size var(--t) var(--ease), opacity var(--t) var(--ease), content-visibility var(--t) allow-discrete;
  }
  .lean-accordion details[open]::details-content { block-size: auto; opacity: 1; }
}

/* ---------- Tooltip ----------
 * <button data-tip="Texto">, con `data-tip-lado` y `data-tip-tecla`. Lo coloca
 * lean.js (ver la nota de allí): era un pseudo-elemento del disparador y lo
 * recortaba cualquier ancestro con overflow —el cuerpo con scroll de un diálogo,
 * una tabla de esquinas redondeadas—, que es donde más falta hace. Es un único
 * globo en el top layer, encima incluso de un <dialog> modal.
 *
 * ═══ POR QUÉ SE REHIZO ═══
 *
 * Dicho por Leandro: «me resulta algo infantil, algo más serio, elegante, sobre
 * todo sutil y poco molesto, y que encaje perfecto en la UI». Eran tres cosas y
 * ninguna era el texto:
 *
 * 1. LA MUESCA. Un triángulo hecho con `border: 6px solid transparent`, que es
 *    el truco de 2010 y es LITERALMENTE lo que convierte un globo en un
 *    bocadillo de cómic. Se va: colocado a 8 px de su botón, un globo ya dice de
 *    quién es, y sin ella cabe en cualquier lado sin dibujar cuatro triángulos.
 *
 * 2. LA INVERSIÓN DE TINTA. Iba con `--ink` de fondo y `--bg` de texto, o sea
 *    era LA ÚNICA PIEZA DEL PANEL QUE SE PINTA AL REVÉS QUE TODO LO DEMÁS. Por
 *    construcción no podía encajar. Ahora usa `--alzado`, que es la altura que
 *    le toca —se levanta sin tapar, como un menú— y que existe desde la escalera
 *    de alturas. Medido: 15,91 de contraste en claro (mejor que los 14,92 de
 *    antes) y 12,33 en oscuro, que es casi el triple del mínimo y justo la zona
 *    donde vive `--ink-doc` a propósito, porque el blanco casi puro sobre casi
 *    negro sangra y cansa.
 *
 * 3. SALÍA AL INSTANTE, y ahí estaba el «molesta»: barrer una fila de botones
 *    disparaba cinco globos seguidos. Ahora espera (ver `TIP_ESPERA` en lean.js)
 *    y hay periodo de gracia, así que recorrer una barra sigue siendo inmediato.
 *
 * EL BORDE SALE DE LA TINTA y no de `--line`, por lo mismo que en el botón de
 * perfil: sobre `--alzado` —que en oscuro es casi el papel de una tarjeta—
 * `--line` da 1,2:1 y el globo se quedaría sin filo justo donde tiene que
 * despegarse de lo que tapa. La mezcla con `--ink` funciona en los tres temas. */
.lean-tip {
  position: fixed; margin: 0; inset: auto; overflow: visible;
  width: max-content; max-width: min(272px, calc(100vw - 24px));
  padding: 6px 10px; border-radius: var(--r-xs);
  background: var(--alzado); color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-sm); line-height: var(--alto-nombre); font-weight: var(--peso-medio);
  letter-spacing: var(--letra-ajuste); text-align: left;
  opacity: 0; pointer-events: none; z-index: var(--z-globo);
  /* La SALIDA es el estado de reposo de la transición, y por eso está aquí: sale
     en `--t-sale` con `--ease-sale`, y entra más despacio con las suyas. Antes
     compartían `--t-fast` y `--ease`, que es la curva de un cambio de estado —un
     hover, un color que se sustituye— y no la de algo que aparece y desaparece. */
  transform: var(--tip-desde);
  transition: opacity var(--t-sale) var(--ease-sale), transform var(--t-sale) var(--ease-sale);
}
/* Sin la Popover API el globo sigue siendo un div normal: se muestra igual,
 * sólo que sin top layer (basta fuera de diálogos). */
.lean-tip:not([popover]) { display: block; }
/* Entra DESDE el lado por el que ha salido, tres píxeles. Es lo que hace que el
   globo parezca venir del botón y no aparecer en el aire; con cuatro lados, cada
   uno tiene el suyo y por eso va en una variable en vez de en cuatro reglas. */
.lean-tip[data-lado="arriba"]    { --tip-desde: translateY(3px); }
.lean-tip[data-lado="abajo"]     { --tip-desde: translateY(-3px); }
.lean-tip[data-lado="derecha"]   { --tip-desde: translateX(-3px); }
.lean-tip[data-lado="izquierda"] { --tip-desde: translateX(3px); }
.lean-tip.show {
  opacity: 1; transform: none;
  transition: opacity var(--t-entra) var(--ease-entra), transform var(--t-entra) var(--ease-entra);
}
/* Y SI LA ACCIÓN TIENE ATAJO, EL GLOBO ES DONDE SE APRENDE. Es el único sitio
   donde alguien se va a encontrar la tecla de «Actualizar» sin buscarla: la hoja
   de atajos hay que ir a abrirla, y esto sale solo al dudar. */
.lean-tip kbd {
  font-family: var(--mono); font-size: var(--fs-xs); margin-left: 7px;
  padding: 1px 5px; border-radius: var(--r-xxs); color: var(--muted);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .lean-tip, .lean-tip.show { transition-duration: 1ms; transform: none; }
}

/* ---------- Toast ----------
 * Aviso efímero de resultado (guardado, error). Se apila abajo a la derecha.
 * Se dispara con Lean.toast(mensaje, {variant}). */
/* EL APILADOR VIVE EN EL TOP LAYER, y no es un refinamiento: es la única forma
   de que una toast se vea con la consola abierta.

   La consola —y los diálogos, y los editores— se abren con `showModal()`, que
   los promueve al top layer: una capa que el navegador pinta por encima de todo
   el documento. El apilador se crea con `document.body.appendChild`, así que no
   compite en la misma pila; da igual que `--z-aviso` fueran veinte mil. Lo que
   se veía era la toast DEBAJO del velo, apagada al 55 %, y ahí es justo donde
   más falta hace: reiniciar un proceso, mandar una orden, copiar una
   contraseña — casi todo lo que acusa una toast se lanza desde dentro.

   `popover="manual"` lo sube a esa misma capa SIN MOVERLO UN PÍXEL: el `fixed`
   con su esquina sigue valiendo igual. Lo que hay que deshacer es el estilo que
   el navegador le pone de oficio a un popover —caja centrada, con borde y
   fondo—, que es lo que hacen las cinco primeras declaraciones.

   Y el top layer es una PILA: manda el último que entró. Si el apilador se
   promoviera al arrancar y la consola se abriera después, la consola volvería a
   taparlo — por eso `Lean.toast` lo vuelve a subir en cada aviso.

   Y HAY UN LÍMITE, medido con un modal delante: se VE, pero no se puede PULSAR.
   `showModal()` vuelve inerte todo lo que queda fuera del diálogo, y el top
   layer no lo cambia — `elementFromPoint` sobre la toast devuelve el diálogo.
   Así que una toast con `actions` lanzada desde la consola enseña sus botones y
   no responden. No empeora nada (antes ni se veía) y para lo que de verdad se
   acusa desde ahí —«orden enviada», «copiado»— no hace falta pulsar nada; pero
   si algún día hace falta un «Deshacer» ahí dentro, el sitio es el diálogo. */
.lean-notificaciones { position: fixed; z-index: var(--z-aviso); bottom: var(--s5); right: var(--s5); display: flex; flex-direction: column; align-items: flex-end; gap: var(--s2); }
.lean-notificaciones:popover-open,
.lean-notificaciones[popover] {
  inset: auto; margin: 0; padding: 0; border: 0; background: transparent;
  overflow: visible; color: inherit;
  bottom: var(--s5); right: var(--s5);
}
.lean-notificacion {
  width: 340px;  /* ancho fijo: todas las toasts miden lo mismo */
  display: flex; flex-direction: column; gap: var(--s2);
  padding: 12px 14px; border-radius: var(--r-s); background: var(--alzado); color: var(--ink);
  border: 1px solid var(--line); box-shadow: var(--shadow-lg);
}
/* CÓMO LLEGA Y CÓMO SE VA, que hasta aquí era una sola transición para las dos
   cosas: `--t` y `--ease` para entrar y lo mismo para salir.

   Son dos gestos distintos y la regla está escrita a tres mil líneas de aquí, en
   `lean-haciendo`: entrar es una noticia y hay que poder leerla; irse es quitar
   de en medio algo que ya no dice nada, y ahí cualquier demora se siente como
   que la interfaz va lenta. Y `--ease` es la curva de un cambio de estado —un
   hover, un color que se sustituye—, no la de algo que aparece.

   Y EL DESPLAZAMIENTO MIRA DÓNDE ESTÁ. Era `translateY(10px)` fijo, con lo que
   una toast arriba —`position: "top-right"`, que la API admite y el contenedor
   respeta con la clase `.top`— entraba SUBIENDO: alejándose del borde por el
   que estaba llegando. `--entra-y` con el signo que le pone su apilador.

   Los 6 px son los de la escala; los 10 eran un número escrito a mano. */
.lean-notificacion {
  --dir: 1;
  opacity: 0; transform: translateY(calc(var(--entra-y) * var(--dir))) scale(.985);
  transition: opacity var(--t-sale) var(--ease-sale),
              transform var(--t-sale) var(--ease-sale);
}
.lean-notificaciones.top .lean-notificacion { --dir: -1; }
.lean-notificacion.show {
  opacity: 1; transform: none;
  transition: opacity var(--t-entra) var(--ease-entra),
              transform var(--t-entra) var(--ease-entra);
}
/* LA SALIDA CAMBIA SEGÚN POR QUÉ SE VA, y eso es lo que antes no existía.

   Se le acabó el tiempo: se retira por donde vino y no hace más. Nadie ha
   pedido que se vaya. La quita el usuario: se aparta HACIA EL CANTO, que es la
   dirección del gesto de apartar algo, y el aro del tiempo se apaga sin
   completar — porque no ha terminado, lo han interrumpido. Es la misma idea
   que `lean-haciendo.is-cerrando`, que cierra el arco entero antes de
   despedirse justamente para no decir «se dejó a medias». */
.lean-notificacion.is-apartada { opacity: 0; transform: translateX(16px) scale(.99); }
.lean-notificacion.is-apartada .lean-notificacion__aro {
  opacity: 0; transition: opacity var(--t-sale) linear;
}
/* Y LOS QUE SE QUEDAN SUBEN, NO SALTAN: cerrar la de arriba de una pila movía
   las de abajo de golpe, y nadie las ha tocado. Eso no lleva regla aquí porque
   lo hace `devolverASuSitio` con `el.animate()`, en `--t-mueve` y con la curva
   de lo que cambia de sitio. */

/* EL ICONO SE TRAZA AL LLEGAR, y detrás entra el texto.

   Escalonado y no simultáneo, que es la regla 3 de `lean-haciendo`: primero se
   lee QUÉ ha pasado y después el porqué. Entre uno y otro, `--t-pulsa`.

   `pathLength="100"` EN EL SPRITE es lo que lo hace posible con cualquiera de
   los cuatro iconos. Sin eso hay que escribir la longitud del trazo a mano —lo
   que hace `is-hecho` con su 24, que le vale porque SIEMPRE pinta el mismo
   check—, y aquí los iconos son cuatro: el círculo mide 62,8 y la barra del «i»
   mide 4. Con un dasharray fijo, el círculo se queda partido para siempre y la
   barra no llega a trazarse nunca.

   Los PUNTOS del «i» y del «!» no llevan `pathLength` a propósito: miden 0,01,
   así que el guion heredado los deja fuera del patrón hasta que el desfase
   llega a cero. O sea que aparecen al final en vez de dibujarse, que es lo que
   hace un punto. */
.lean-notificacion__main > .lean-ico use,
.lean-notificacion__main > .lean-ico path,
.lean-notificacion__main > .lean-ico circle,
.lean-notificacion__main > .lean-ico line {
  animation: lean-traza-100 var(--t) var(--ease-entra) var(--t-pulsa) backwards;
}
.lean-notificacion__title,
.lean-notificacion__text .msg,
.lean-notificacion__text .solo {
  animation: lean-notificacion-asoma var(--t-entra) var(--ease-entra) var(--t-pulsa) backwards;
}
/* El detalle, dos peldaños de cascada por detrás del titular (18 ms cada uno,
   redondeados al fotograma). */
.lean-notificacion__text .msg { animation-delay: calc(var(--t-pulsa) + 40ms); }
@keyframes lean-notificacion-asoma {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: none; }
}

/* EL ARO DEL TIEMPO, alrededor de la equis.

   Una toast dura 3.400 ms y no decía nada de eso: o la lees a tiempo o
   desaparece a mitad de frase. Es un trabajo con principio, final y duración,
   que es exactamente la definición con la que se separó `lean-haciendo` del
   anillo de presencia — así que el aro es el suyo, descontando en vez de
   avanzando, con su pista obligatoria de `--line`.

   Y VA EN LA EQUIS, NO EN EL ICONO. Tres de los cuatro iconos de una toast SON
   círculos (`li-info`, `li-circle-check`, `li-circle-alert`), así que un aro
   alrededor da dos circunferencias concéntricas. La pieza de la que sale lo
   decía y no lo leí a la primera: aquel anillo «envuelve un avatar», y un
   avatar no tiene contorno propio. La equis tampoco.

   De paso queda mejor repartido: el icono de la izquierda dice QUÉ ha pasado y
   el aro de la derecha CUÁNDO se va, justo encima del botón que hace eso mismo
   a mano.

   SE PARA SI LA ESTÁS LEYENDO. Ratón encima o foco dentro y el aro se congela;
   `Lean.toast` mira ese mismo estado antes de retirarla. Una toast que se va
   mientras la lees es el defecto clásico de este componente. */
/* Y LA EQUIS VA EN EL CENTRO DEL ARO, que es todo lo que este componente le
   pide a esta regla y estuvo sin cumplirse desde que se puso el aro.

   Esto se declaraba en DOS reglas separadas por sesenta líneas —aquí el tamaño y
   la caja, y más abajo, con los cuatro tonos, el reseteo del botón— y las dos
   decían `display`: `grid` ésta e `inline-flex` aquélla. Misma especificidad,
   así que ganaba la de abajo por estar después, y CSS no avisa: la regla que
   pierde sencillamente no se aplica.

   Lo que se veía es que `place-items: center` sólo centraba la mitad. En un
   contenedor flex esa propiedad vale por `align-items` —el eje transversal, que
   aquí es el vertical— y `justify-items`, que en flexbox NO HACE NADA; el eje
   principal se reparte con `justify-content`. O sea que la equis quedaba
   verticalmente centrada y pegada al borde IZQUIERDO de su aro. Medido: 5 px de
   desvío sobre un botón de 22.

   Ahora es UNA regla. No es una manía de orden: es el mismo fallo que ya se cazó
   en el lateral con los dos `::before` de `nav-item` —dos reglas de igual peso
   compitiendo por la misma plaza, y la pantalla saliendo mal en silencio—. */
.lean-notificacion__close {
  position: relative; width: 22px; height: 22px; flex: none;
  display: grid; place-items: center;
  padding: 0; border: none; background: none; color: var(--muted); cursor: pointer;
}
.lean-notificacion__close .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
.lean-notificacion__aro { position: absolute; inset: 0; transform: rotate(-90deg);
  pointer-events: none; }
.lean-notificacion__aro circle { fill: none; stroke-width: 1.5; }
.lean-notificacion__aro .pista { stroke: var(--line); }
.lean-notificacion__aro .arco  { stroke: var(--muted); stroke-linecap: round;
  stroke-dasharray: 100; stroke-dashoffset: 0;
  /* `forwards` A PROPÓSITO, y esto no es el `both` que muerde: lo que retiene al
     acabar es el estado final —el aro vacío, el tiempo agotado— y de ahí la
     toast ya no vuelve; se va. `stroke-dashoffset` no tiene ninguna transición
     que esto pueda dejar muerta, que es el daño real del relleno. Sin él, el aro
     saltaría a lleno en el último fotograma, justo cuando dice que se acabó. */
  animation: lean-notificacion-vida var(--vida, 3400ms) linear forwards; }
@keyframes lean-notificacion-vida { to { stroke-dashoffset: 100; } }
.lean-notificacion:hover .lean-notificacion__aro .arco,
.lean-notificacion:focus-within .lean-notificacion__aro .arco {
  animation-play-state: paused;
}

/* Quien pide menos movimiento ve el resultado, no el gesto: la toast aparece y
   desaparece sin desplazarse, el icono no se dibuja y el aro no cuenta —pero
   sigue ahí, marcando que esto se va solo—. */
@media (prefers-reduced-motion: reduce) {
  .lean-notificacion { transform: none; }
  .lean-notificacion.is-apartada { transform: none; }
  .lean-notificacion__main > .lean-ico use,
  .lean-notificacion__main > .lean-ico path,
  .lean-notificacion__main > .lean-ico circle,
  .lean-notificacion__main > .lean-ico line,
  .lean-notificacion__title,
  .lean-notificacion__text .msg,
  .lean-notificacion__text .solo { animation: none; }
  .lean-notificacion__aro .arco { animation: none; stroke-dashoffset: 0; }
}
.lean-notificacion__main { display: flex; align-items: flex-start; gap: var(--s2); }
.lean-notificacion__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-md); line-height: var(--alto-nombre); }
/* Declara su tamaño en vez de heredarlo, que es lo que lo separaba del aviso.
   Las CINCO piezas que ponen título a una superficie ocupan el mismo escalón y
   lo resolvían de cuatro maneras: el diálogo y la cabecera de sección en
   `--fs-xl`, la tarjeta en `--fs-lg`, y estas dos sin declarar nada — así que
   lo único que las distinguía era que el aviso pesaba 650 y ésta 600, cincuenta
   unidades que no estaban en ninguna tabla. Tres escalones sí se defienden
   (pantalla · tarjeta · caja dentro del flujo); cuatro por descuido, no. */
.lean-notificacion__title { font-size: var(--fs-md); font-weight: var(--peso-titulo); }
.lean-notificacion__text .msg { color: var(--muted); }
.lean-notificacion__text .solo { color: var(--ink); }
.lean-notificacion__main > .lean-ico { flex: none; margin-top: 1px; }
/* Posiciones alternativas del apilado de toasts. */
/* Van con `[popover]` para pesar lo mismo que el reseteo de arriba: sin eso,
   una lista de dos clases pierde contra `.lean-notificaciones[popover]` y todas
   las toasts acabarían abajo a la derecha dijera lo que dijera `position`. */
.lean-notificaciones.top,    .lean-notificaciones.top[popover]    { top: var(--s5); bottom: auto; }
.lean-notificaciones.left,   .lean-notificaciones.left[popover]   { left: var(--s5); right: auto; align-items: flex-start; }
.lean-notificaciones.center, .lean-notificaciones.center[popover] { left: 50%; right: auto; transform: translateX(-50%); align-items: center; }
/* Los cuatro tipos, con los mismos nombres y los mismos colores que en
   `lean-aviso`: la misma noticia no puede llamarse distinta según salga en la
   esquina o en el flujo. `--error` es el nombre viejo de `--down` y sigue
   pintando por las llamadas que aún lo dicen (ver ALIAS en lean.js). */
.lean-notificacion--ok    .lean-notificacion__main > .lean-ico:first-child { color: var(--ok); }
.lean-notificacion--down  .lean-notificacion__main > .lean-ico:first-child,
.lean-notificacion--warn  .lean-notificacion__main > .lean-ico:first-child { color: var(--warn); }
.lean-notificacion--info  .lean-notificacion__main > .lean-ico:first-child { color: var(--brand-ink); }
/* Aquí vivía la otra mitad de `.lean-notificacion__close` —el reseteo del botón,
   con su propio `display`— y se ha ido con el bloque del aro, que es donde se
   explica qué tiene que hacer esta caja. Ver allí por qué tenerlo en dos sitios
   descentraba la equis. */
.lean-notificacion__close:hover { color: var(--ink); }
/* Fila de acciones opcional (botones al pie de la toast). */
.lean-notificacion__actions { display: flex; justify-content: flex-end; gap: var(--s2); }

/* Aquí vivía `lean-kpi`, el TERCER componente para «una cifra con su rótulo».
   No lo usaba ninguna pantalla —sólo la guía, igual que pasaba con el segundo
   esqueleto—, así que el escaparate enseñaba una forma de pintar cifras que la
   aplicación no gastaba, y quien lo copiara de ahí escribía marcado que no se
   parecía al del resto del panel.

   Además era el único que nombraba sus hijos `.n` y `.k`, sueltos y sin
   prefijo, en vez de `__n` y `__k`. Dos problemas en uno: una clase de una letra
   en el espacio global dentro de un componente, y una API distinta de la de sus
   dos hermanos para hacer exactamente lo mismo.

   Se queda `lean-vital`, que es el que trabaja, tiene los estados resueltos
   (`is-ok`, `is-warn`, `is-down`, `is-vacio`) y su rejilla. Para la cifra
   grande de una portada vale `lean-vital` con `--fs-4xl`; que hicieran falta
   veintiocho píxeles no justificaba un componente entero.

   `lean-meter` corrió después la misma suerte, y el porqué merece anotarse: al
   pasar los tamaños a la escala, su cifra y la de `lean-vital` convergieron
   SOLAS en el mismo peldaño, y entonces lo único que quedaba distinto entre los
   dos componentes era una barra de tres píxeles. Ordenar un eje hizo evidente
   un duplicado que llevaba meses ahí. */

/* ---------- Cabecera y pie de tarjeta ----------
 * Sangran hasta el borde de la tarjeta (asumen padding --s5). */
.lean-tarjeta__head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin: calc(var(--s5) * -1) calc(var(--s5) * -1) var(--s4); padding: var(--s4) var(--s5); border-bottom: 1px solid var(--line); }
.lean-tarjeta__title { font-weight: var(--peso-titulo); font-size: var(--fs-lg); }
.lean-tarjeta__foot { display: flex; align-items: center; justify-content: flex-end; gap: var(--s2); margin: var(--s4) calc(var(--s5) * -1) calc(var(--s5) * -1); padding: var(--s4) var(--s5); border-top: 1px solid var(--line); }
/* Y SI LA TARJETA ENCOGE, ESTAS DOS TIENEN QUE ENCOGER CON ELLA. Sangran hasta
   el borde con un margen negativo que vale exactamente lo que el relleno de la
   tarjeta, así que en cuanto uno de los dos cambia y el otro no, la cabecera
   sobresale por los lados —8 px por cada uno, medidos en la tarjeta de una red—
   y arrastra a su tarjeta fuera del contenedor. */
@media (max-width: 560px) {
  .lean-tarjeta__head { margin: calc(var(--s4) * -1) calc(var(--s4) * -1) var(--s4); padding: var(--s3) var(--s4); }
  .lean-tarjeta__foot { margin: var(--s4) calc(var(--s4) * -1) calc(var(--s4) * -1); padding: var(--s3) var(--s4); }
}

/* ---------- Tabla ----------
 * Para datos tabulares (inventario, intervenciones). Cabeceras en versalita,
 * filas con hover, números a la derecha con cifras tabulares. */
.lean-table { width: 100%; border-collapse: collapse; font-size: var(--fs-lg); }
.lean-table thead th { text-align: left; font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: var(--letra-rotulo); color: var(--muted); font-weight: var(--peso-medio); padding: 10px var(--s3); border-bottom: 1px solid var(--line); white-space: nowrap; }
.lean-table tbody td { padding: 12px var(--s3); border-bottom: 1px solid var(--line); }
/* Vivía en `app.css` sin acotar, o sea pisando el componente desde fuera: la
   tabla se veía de una forma en el panel y de otra en la guía, que es justo donde
   uno va a ver cómo es la pieza. Es propiedad del componente y su sitio es éste. */
.lean-table td { vertical-align: middle; }
.lean-table tbody tr:last-child td { border-bottom: none; }
.lean-table tbody tr { transition: background var(--t-fast) var(--ease); }
.lean-table tbody tr:hover { background: var(--al-pasar); }
.lean-table .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Tabla a sangre dentro de una tarjeta: las celdas se alinean con el margen de
 * la tarjeta, no con el suyo propio, o la primera columna queda descolgada
 * respecto al título de la cabecera. */
.lean-table--holgada thead th,
.lean-table--holgada tbody td { padding-left: var(--s5); padding-right: var(--s5); }
/* Y en un móvil, un peldaño menos por el mismo motivo que la tarjeta: la tabla
   ya se lee arrastrándola de lado, así que cada píxel de relleno es una letra
   menos de la columna que se está mirando. Baja con la cabecera de la barra que
   tiene encima —las dos van a sangre dentro de la misma tarjeta—, o la primera
   columna quedaría descolgada respecto al buscador. */
@media (max-width: 560px) {
  .lean-table--holgada thead th,
  .lean-table--holgada tbody td { padding-left: var(--s4); padding-right: var(--s4); }
}

/* LA COLUMNA DE LOS TRES PUNTOS, la última de casi todas las tablas.
 *
 * Su ancho tiene que contar EL RELLENO DE LA CELDA, no sólo el botón, y ahí es
 * donde se falla: en una tabla holgada son 24 px por lado, así que una columna
 * de 52 px dejaba cuatro para un botón de treinta y el botón acababa saliéndose
 * por el canto de la tabla, pegado al borde y sin aire.
 *
 * Se le quita el relleno de la IZQUIERDA —no hay nada de lo que separarse, la
 * columna es sólo para él— y se conserva el de la derecha, que es lo que impide
 * que quede lamiendo el borde. El botón se alinea a ese lado.
 *
 * Va aquí, en el componente, y no en cada pantalla: lo necesitan por igual las
 * seis tablas que tienen menú de fila, y escrito en `app.css` empataría en
 * especificidad con la regla de arriba y perdería por orden de carga. */
.lean-table th.acciones-col, .lean-table td.acciones-col,
.lean-table th.th-acc, .lean-table td.th-acc {
  width: 60px; padding-left: 0; padding-right: var(--s4); text-align: right;
}

/* Cuando la fila entera abre algo (una ficha, un detalle). */
.lean-table--clicable tbody tr { cursor: pointer; }

/* Celda de dos pisos: lo que identifica la fila arriba, y debajo el dato que
 * la desambigua (el login, el número de serie, el identificador en otro
 * sistema). Casi todas las tablas del panel lo necesitan y cada una lo tenía
 * escrito por su cuenta —`usu-cuenta__txt` + `usu-login` en cuentas,
 * `cat-nombre` + `cat-numero` en el catálogo—, con tres tamaños distintos para
 * lo mismo. La sub-línea es callada a propósito: está para confirmar cuál es
 * ésta de las tres parecidas, no para leerla de arriba abajo. */
.lean-celda { min-width: 0; }
.lean-celda__n { font-weight: var(--peso-firme); display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
.lean-celda__s { font-size: var(--fs-xs); color: var(--muted); margin-top: 1px; }
/* El identificador de un sistema ajeno se lee carácter a carácter (se coteja
 * con otra pantalla), así que va en monoespaciada aunque esté en la sub-línea. */
.lean-celda__s code { font-family: var(--mono); font-size: var(--fs-xs); }

/* Tabla densa, para una ventana de herramienta: se ven más filas sin que deje
 * de poder pulsarse una. */
.lean-table--densa { font-size: var(--fs-md); }
.lean-table--densa thead th { padding: 6px var(--s3); font-size: var(--fs-xs); }
.lean-table--densa tbody td { padding: 7px var(--s3); }

/* Mapa de calor: la celda se tiñe en proporción a lo que mide, con --calor de 0
 * a 1. Es lo que hace legible de un vistazo una tabla de consumos —lo que más
 * gasta se ve antes de leer la cifra— y es el patrón que ya reconoce cualquiera
 * que haya abierto el administrador de tareas de su sistema. */
.lean-calor { position: relative; isolation: isolate; }
.lean-calor::before {
  content: ""; position: absolute; inset: 0; z-index: var(--z-fondo); pointer-events: none;
  background: color-mix(in srgb, var(--brand) calc(var(--calor, 0) * 18%), transparent);
}
.lean-calor.is-warn::before { background: color-mix(in srgb, var(--warn) calc(var(--calor, 0) * 22%), transparent); }
.lean-calor.is-down::before { background: color-mix(in srgb, var(--down) calc(var(--calor, 0) * 26%), transparent); }

/* Cabeceras ordenables: la flecha sólo aparece en la columna que ordena. */
.lean-table thead th.sortable { cursor: pointer; user-select: none; transition: color var(--t-fast) var(--ease); }
.lean-table thead th.sortable:hover { color: var(--muted); }
.lean-table thead th.sortable::after {
  content: ""; display: inline-block; width: 0; height: 0; margin-left: 5px;
  vertical-align: middle; opacity: 0; transition: opacity var(--t-fast) var(--ease);
  border-left: 4px solid transparent; border-right: 4px solid transparent;
}
.lean-table thead th.asc::after { opacity: 1; border-bottom: 5px solid var(--brand); }
.lean-table thead th.desc::after { opacity: 1; border-top: 5px solid var(--brand); }
.lean-table thead th.asc, .lean-table thead th.desc { color: var(--brand-ink); }

/* ---------- Modal / diálogo ----------
 * <dialog class="lean-dialog"> nativo: showModal()/close(), foco atrapado y
 * cierre con Escape gratis. El pie usa el patrón de acciones a la derecha. */
/* LA TRANSICIÓN ES DE IDA Y VUELTA, y la vuelta necesita dos propiedades más
 * que no son opcionales. Un <dialog> pasa a `display: none` al cerrarse, y una
 * propiedad discreta corta cualquier transición en el fotograma uno: el diálogo
 * entraba con `@starting-style` y al cerrar DESAPARECÍA, igual que el velo de
 * detrás. `transition-behavior: allow-discrete` es lo que hace que el navegador
 * aguante el `display` (y el `overlay`, que es quien lo mantiene en la capa
 * superior) hasta que el resto de la transición termina.
 *
 * `overlay` HAY QUE LISTARLO APARTE aunque no se le dé valor: sin él el diálogo
 * sale del top layer en el acto y durante la salida se pinta por debajo de todo
 * lo demás, que se ve peor que no animar.
 *
 * Y SE VA MÁS DEPRISA DE LO QUE VIENE, que es la regla de la casa:
 * `--t-entra` para abrir, `--t-sale` para cerrar. */
/* UN DIÁLOGO ESTÁ DELANTE DE TODO, y era el mismo papel que la tarjeta a la que
   tapa: con el velo por medio se distinguía, sí, pero por el velo y no por él.
   `--delante` es la altura de arriba del sistema —en claro sigue siendo blanco,
   porque allí lo que dice la altura es la sombra, y ahí entra `--shadow-xl`—.
   La cabecera y el pie pegajosos suben con él unas líneas más abajo: son la
   misma superficie y con `--panel` se veían como dos franjas de otro color al
   desplazar el cuerpo. */
.lean-dialog { width: min(460px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--linde-alto); border-radius: var(--r); background: var(--delante); color: var(--ink); box-shadow: var(--shadow-xl); opacity: 0; transform: translateY(var(--entra-y)) scale(.98);
  transition: opacity var(--t-sale) var(--ease-sale), transform var(--t-sale) var(--ease-sale),
              display var(--t-sale) allow-discrete, overlay var(--t-sale) allow-discrete; }
.lean-dialog[open] { opacity: 1; transform: none;
  transition: opacity var(--t-entra) var(--ease-entra), transform var(--t-entra) var(--ease-entra),
              display var(--t-entra) allow-discrete, overlay var(--t-entra) allow-discrete; }
/* --wide: un paso por encima del ancho normal, para lo que no llega a rejilla
 * pero se ahoga en 460 —la matriz de permisos de un grupo, en dos columnas—.
 *
 * Vivía en `app.css`, que es donde no puede vivir: se llamaba `lean-` pero no
 * estaba en el sistema, así que no salía en la guía, nadie sabía que existía y
 * la siguiente pantalla que necesitara este ancho se habría inventado el suyo.
 * Se mueve tal cual, sin tocar una medida. */
.lean-dialog--wide { max-width: 560px; width: calc(100vw - 2 * var(--s5)); }
/* --lg para diálogos con formulario de varias columnas (alta de un equipo,
 * elegir entre una lista). El ancho por defecto ahoga una rejilla de campos. */
.lean-dialog--lg { width: min(720px, calc(100vw - 32px)); }
/* --lateral: el cajón de detalle, pero como DIÁLOGO. Entra por la derecha y
 * ocupa el alto entero.
 *
 * Convive con .lean-cajon y no lo sustituye. La diferencia no es estética, es
 * de mecanismo: .lean-cajon es un <aside> que abre y cierra quien lo usa;
 * esto es un <dialog> nativo, así que trae el Escape, el foco atrapado y el
 * resto de la página inerte, y lo cierra solo el enrutador al salir de su
 * dirección. Para un detalle que se puede enlazar y compartir, ésta es la
 * pieza. */
.lean-dialog--lateral { width: min(620px, 100vw); max-width: none; height: 100dvh; max-height: 100dvh;
  margin: 0 0 0 auto; border-radius: 0; border-right: 0; border-top: 0; border-bottom: 0;
  display: flex; flex-direction: column; transform: translateX(24px) scale(1); }
.lean-dialog--lateral[open] { transform: none; }
@starting-style { .lean-dialog--lateral[open] { opacity: 0; transform: translateX(24px); } }
/* El cuerpo es lo único que se desplaza: la cabecera dice qué se está mirando y
 * el pie lleva las acciones, y ninguno de los dos debe irse al hacer scroll. */
.lean-dialog--lateral .lean-dialog__body { flex: 1; overflow-y: auto; overscroll-behavior: contain; }
@media (max-width: 620px) { .lean-dialog--lateral { border-left: 0; } }
@starting-style { .lean-dialog[open] { opacity: 0; transform: translateY(8px) scale(.98); } }
/* --doc: la ventana de un DOCUMENTO LARGO QUE SE CONSULTA POR PARTES. La estrena
 * la ficha de Lean UI —173 versiones, 97 componentes y 92 iconos—, que antes era
 * un `--wide` con el CHANGELOG entero dentro de un cuerpo de 60vh, y ese cuerpo
 * se desplazaba dentro de un diálogo que también se desplazaba: dos barras.
 *
 * Ancho y alto FIJOS, y columna flexible sin `__head` ni `__body`: la cabecera
 * la pone un `lean-membrete` y el resto lo reparte quien la usa, cada zona con
 * su desplazamiento. Así la equis no se va nunca al bajar.
 *
 * `display: flex` SÓLO CON `[open]`. `--lateral` lo declara a secas y funciona
 * porque el nodo se quita al cerrar; pero la regla con la que el navegador
 * esconde un diálogo cerrado pierde siempre contra una de autor, así que uno que
 * se quedara en el documento seguiría pintado. */
.lean-dialog--doc { width: min(1100px, calc(100vw - 32px)); max-width: none; height: min(760px, calc(100dvh - 48px)); max-height: none; overflow: hidden; }
.lean-dialog--doc[open] { display: flex; flex-direction: column; }
/* Y `lean-membrete` DENTRO SUELTA SU CAJA: el papel y el canto ya los pone la
   ventana, y con los suyos dibujaba un marco dentro de otro. Se queda el filete
   de abajo, que es lo que lo separa de lo que viene. */
.lean-dialog--doc > .lean-membrete { flex: none; background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; }
/* En un móvil, entera: una ventana con márgenes deja a los lados una franja de
   panel que no se puede usar. */
@media (max-width: 860px) { .lean-dialog--doc { width: 100vw; height: 100dvh; border-radius: 0; border: 0; } }

/* --- lean-viajando: la fila que se mueve TAPA lo que cruza ------------------

   `Lean.devolverASuSitio` mueve filas con `transform`, y un `<tr>` no tiene
   fondo propio —lo pone la tabla— ni crea contexto de apilado. Resultado: dos
   filas que se cruzan se dibujan una encima de otra sin ocultarse, y se lee el
   texto de las dos superpuesto.

   Medido a 0,08× sobre una tabla de doce filas ordenada por nombre: seis filas
   viajando en un sentido y cinco en el otro, con CUATRO pares pisándose y los
   nombres ilegibles mientras duraba. No era la curva ni la duración: faltaba la
   mitad física del gesto, que es que lo que pasa por delante tapa.

   El fondo es el del panel y no `--panel-2`: la fila viaja sobre la tabla, así
   que lo que tiene que ocultar es lo que hay al mismo nivel. `position:
   relative` es lo que hace que el `z-index` que pone el JavaScript cuente —sin
   él, en un `<tr>`, no aplica—.

   Se pone y se quita SOLA, al terminar la animación. Permanente rompería el
   color de la fila al pasar por encima y el de la fila seleccionada. */
.lean-viajando {
  position: relative; background: var(--panel);
  /* SIN TRANSICIÓN, y esto se vio midiendo. Las filas de una tabla clicable
     tienen `transition: background` para el color al pasar por encima, así que
     al poner esta clase el fondo opaco entraba DESVANECIÉNDOSE durante 140 ms
     — o sea que justo al principio del viaje, que es cuando las filas se
     cruzan, todavía era medio transparente y seguía viéndose el texto de
     debajo. La medición lo delató: la primera animación de la fila no era el
     desplazamiento, era un fundido de fondo que nadie había pedido. */
  transition: none;
}
/* Y LAS CELDAS HEREDAN, que es la línea que lo resuelve del todo.
   Un `<td>` es transparente de fábrica, así que con el fondo sólo en el `<tr>`
   quedaba una traza fantasma del texto de debajo —se ve a 0,08×—. Poniéndoles
   `--panel` a pelo tapaba bien, pero apagaba a una cabecera de cajón abierta,
   que se pinta con `--panel-2` por una regla más específica sobre la fila.
   `inherit` hace las dos cosas: opaca siempre, y del color que tenga SU fila. */
.lean-viajando > td, .lean-viajando > th { background: inherit; }
@media (prefers-reduced-motion: reduce) { .lean-viajando { background: none; } }

/* --- LAS TRES SALIDAS, porque un diálogo tiene tres finales -----------------

   Cancelar, guardar y borrar se veían igual, y la salida es el único instante
   en que la ventana puede decir qué ha pasado. La de arriba —retirarse por
   donde vino— es la de CANCELAR, y se queda como está: no ha pasado nada, así
   que el gesto correcto es deshacer la entrada.

   `is-entregando` es la de GUARDAR. La ventana no se desvanece: colapsa en la
   dirección de la fila que acaba de cambiar, y cuando el velo termina de
   levantarse esa fila releva su dato (ver `Lean.cerrarEntregando`). Sin esto
   eran dos sucesos sin relación: una ventana que se va y, por su cuenta, un
   número distinto en una lista.

   El vector lo escribe el JavaScript en `--entrega-x/y` porque sale de medir
   las dos posiciones; aquí sólo está la forma. Y la escala baja a .94 y no a
   .98 como la entrada: lo que se retira con un destino se encoge un poco más
   que lo que sólo desaparece, que es lo que hace que se lea como «se ha ido
   ahí» y no como «se ha ido». */
.lean-dialog.is-entregando {
  transform: translate(var(--entrega-x, 0px), var(--entrega-y, 0px)) scale(.94);
}
/* En el cajón lateral la entrega no aplica: ése sale por su lado, que es de
   donde vino, y mandarlo hacia una fila sería cruzar la pantalla entera. */
.lean-dialog--lateral.is-entregando { transform: translateX(24px); }

/* `is-yendose` es la de BORRAR, y no va en la ventana sino en LA FILA.
   El alto se anima a cero con el relleno y el borde, que es lo que hace que las
   de abajo suban en vez de dar un salto: un borrado es el cambio más grande que
   puede hacer una lista y era el único que no se veía ocurrir.
   Corre DESPUÉS de que el diálogo se haya ido, no a la vez (ver Lean.filaSeVa):
   con las dos cosas moviéndose el ojo no sabe cuál mirar, y la que importa es
   la fila. */
/* `--fila-alto` VA REGISTRADA, y no es adorno: sin `@property` una variable que
   se escribe desde JavaScript no se puede animar —la declaración se toma como
   un valor cualquiera y el fotograma inicial del `from` no existe—. Es el mismo
   fallo que dejó sin transición el anillo de progreso. */
@property --fila-alto { syntax: "<length>"; inherits: false; initial-value: 0px; }

.is-yendose {
  animation: lean-fila-se-va var(--t-vista) var(--ease-sale) forwards;
  overflow: hidden;
}
@keyframes lean-fila-se-va {
  from { opacity: 1; height: var(--fila-alto); }
  to   { opacity: 0; height: 0; transform: translateX(-10px);
         padding-top: 0; padding-bottom: 0; margin-top: 0; margin-bottom: 0;
         border-top-width: 0; border-bottom-width: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .lean-dialog.is-entregando { transform: none; }
  .is-yendose { animation: none; }
}
/* EL VELO SE FUNDE CON LA VENTANA, en los dos sentidos. Sin esto aparecía y
 * desaparecía de golpe, o sea que la ventana entraba con cuidado sobre un fondo
 * que se había oscurecido de un salto: el gesto más suave del panel montado
 * encima del más brusco. Va con la misma pareja de tiempos, porque dos cosas que
 * ocurren a la vez se mueven igual o se ven pelear. */
.lean-dialog::backdrop { background: color-mix(in srgb, var(--velo-tinta) 55%, transparent); opacity: 0;
  transition: opacity var(--t-sale) var(--ease-sale), display var(--t-sale) allow-discrete, overlay var(--t-sale) allow-discrete; }
.lean-dialog[open]::backdrop { opacity: 1;
  transition: opacity var(--t-entra) var(--ease-entra), display var(--t-entra) allow-discrete, overlay var(--t-entra) allow-discrete; }
@starting-style { .lean-dialog[open]::backdrop { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .lean-dialog, .lean-dialog[open],
  .lean-dialog::backdrop, .lean-dialog[open]::backdrop { transition-duration: 1ms; }
}
/* LA CABECERA Y EL PIE NO SE VAN AL HACER SCROLL.
 *
 * El scroll de un diálogo alto lo lleva el propio `<dialog>` —el navegador le
 * pone un alto máximo—, así que la cabecera y el pie se iban con el contenido:
 * en una ventana larga, cerrar obligaba a subir hasta arriba del todo a buscar
 * la equis, y el botón de guardar quedaba abajo, más allá del final.
 *
 * `sticky` y no `fixed`: pegado al contenedor que se desplaza, sin sacar la
 * pieza del flujo ni tener que calcularle sitio. El fondo es obligatorio —si no,
 * el contenido se ve pasar por debajo— y el z-index tiene que ganarle a lo que
 * se desplaza, no a los menús, que viven en la capa de encima.
 *
 * El BORDE de la cabecera aparece sólo cuando hay algo debajo desplazándose,
 * pero eso pide un observador en JS; aquí se queda siempre, que es lo que ya
 * hacía y no molesta: una ventana con cabecera separada del cuerpo se lee igual
 * de bien esté arriba o a medio recorrer. */
.lean-dialog__head { position: sticky; top: 0; z-index: var(--z-pegado); background: var(--delante);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3); padding: var(--s4) var(--s5); border-bottom: 1px solid var(--line); }
/* El `inline-flex` y el hueco del icono estaban en `app.css`, acotados sólo por
   `.lean-dialog` —que es una clase de Lean, no de la aplicación—, así que el
   título de un diálogo se componía distinto dentro y fuera del panel. */
.lean-dialog__title { font-family: var(--disp); font-weight: var(--peso-titulo); font-size: var(--fs-xl); letter-spacing: var(--letra-ajuste);
  display: inline-flex; align-items: center; gap: var(--s2); }
.lean-dialog__title .lean-ico { color: var(--muted); flex: none; }
.lean-dialog__body { padding: var(--s5); font-size: var(--fs-lg); color: var(--muted); }
.lean-dialog__foot { position: sticky; bottom: 0; z-index: var(--z-pegado); background: var(--delante);
  display: flex; align-items: center; justify-content: flex-end; gap: var(--s2); padding: var(--s4) var(--s5); border-top: 1px solid var(--line); }
/* En el cajón lateral el que se desplaza es el CUERPO, no la ventana: ahí la
   cabecera y el pie ya están fuera del área que se mueve (ver --lateral), y
   pegarlos además no cambia nada. Se deja por uniformidad: la misma pieza se
   comporta igual en las dos formas del componente. */

/* En un PIE todos los botones miden lo mismo, lo diga o no su clase. La talla
   la impone el sitio, no quien escribe el marcado: en el pie del cajón de la
   bitácora había un `--sm` de 30 px («Copiar enlace») al lado de un `Cerrar` de
   36, y en una fila de tres o cuatro botones eso se ve antes que lo que dicen.
   Sale más barato aquí que confiando en que nadie se equivoque de clase. */
.lean-dialog__foot .lean-boton--sm,
.lean-tarjeta__foot   .lean-boton--sm { min-height: var(--ctl-h); font-size: var(--fs-md); }
.lean-dialog__foot .lean-boton--icono.lean-boton--sm,
.lean-tarjeta__foot   .lean-boton--icono.lean-boton--sm { width: var(--ctl-h); }

/* ---------- lean-salto: ir a cualquier sitio escribiendo su nombre ----------
 *
 * La ventana que se abre encima de todo (Ctrl+K), se escribe y lleva a donde
 * sea. NO es un filtro: `lean-toolbar` lleva el buscador que acota la lista que
 * ya se está mirando, y `lean-picker` el que elige un valor para un formulario.
 * Éste NAVEGA, y por eso sus filas son enlaces de verdad.
 *
 * Va sobre `lean-dialog` y no por su cuenta: de ahí saca la superficie, el velo,
 * el foco atrapado, el Escape y la animación de entrada. Lo suyo es la forma
 * —campo arriba, lista debajo, pie con las teclas— y dónde se coloca.
 *
 * ARRIBA Y NO EN EL CENTRO. Un diálogo centrado salta hacia abajo al crecer la
 * lista, y aquí la lista crece con cada letra: el campo se movería mientras se
 * escribe en él. Anclado arriba, lo único que se mueve es lo que hay debajo. */
.lean-salto {
  width: min(560px, calc(100vw - 2 * var(--s4)));
  margin: 12vh auto auto;
  display: flex; flex-direction: column;
  max-height: min(560px, 76vh);
  overflow: hidden;
}
/* El campo no es un `lean-input`: no tiene caja propia porque la caja ES la
   ventana. Un campo con su borde dentro de un modal de bordes dibuja dos marcos
   concéntricos y el de dentro no delimita nada. */
.lean-salto__campo {
  display: flex; align-items: center; gap: var(--s3);
  padding: 0 var(--s4); flex: none;
  border-bottom: 1px solid var(--line);
}
.lean-salto__campo .lean-ico { color: var(--muted); flex: none; }
.lean-salto__input {
  flex: 1; min-width: 0;
  height: 52px; padding: 0;
  border: 0; background: transparent; color: var(--ink);
  font: inherit; font-size: var(--fs-xl);
}
.lean-salto__input:focus { outline: none; }
.lean-salto__input::placeholder { color: var(--muted); }

.lean-salto__lista { overflow-y: auto; overscroll-behavior: contain; padding: var(--s2) 0; }
/* El rótulo de cada bloque. Va en versalitas y apagado porque es un letrero, no
   un resultado: se usa para saltar con la vista al grupo que interesa, y en
   cuanto se lee una vez deja de mirarse. */
.lean-salto__grupo {
  padding: var(--s2) var(--s4) var(--s1);
  font-size: var(--fs-xs); font-weight: var(--peso-titulo); letter-spacing: var(--letra-rotulo);
  text-transform: uppercase; color: var(--muted);
}
.lean-salto__op {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s4);
  color: var(--ink); text-decoration: none; cursor: pointer;
}
/* UNA SOLA MARCA, y la mueve el teclado. El ratón no pinta la suya: al pasar por
   encima se marca la fila, así que `:hover` y la marcada son siempre la misma.
   Con dos estilos distintos habría dos filas resaltadas a la vez y al pulsar
   Intro se abriría la que no se estaba mirando. */
.lean-salto__op.is-activa { background: var(--panel-2); }
.lean-salto__ico { color: var(--muted); flex: none; }
.lean-salto__op.is-activa .lean-salto__ico { color: var(--brand-ink); }
.lean-salto__txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
/* Los dos renglones de un resultado: qué es y de quién es. No hay un tercero;
   lo que explica por qué ha salido va a la derecha, en su columna. */
.lean-salto__n { font-size: var(--fs-lg); font-weight: var(--peso-firme); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lean-salto__n b { font-weight: var(--peso-fuerte); color: var(--brand-ink); }
.lean-salto__s { font-size: var(--fs-sm); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Por qué ha salido esta fila: la serie, la dirección, el CIF. A la derecha y en
   la tipografía de datos, que es lo que son. Cede el sitio antes que el nombre
   —`min-width: 0` y recorte— porque sin nombre la fila no dice nada. */
.lean-salto__pista {
  flex: 0 1 auto; min-width: 0; text-align: right;
  font-family: var(--mono); font-size: var(--fs-xs); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lean-salto__pista b { font-weight: var(--peso-fuerte); color: var(--brand-ink); }
.lean-salto__vacio { padding: var(--s5) var(--s4); text-align: center; color: var(--muted); font-size: var(--fs-md); }
/* Cuando la mitad que viene del servidor no ha podido llegar. Es un aviso y no
   un vacío —lo de esta página sigue ahí, debajo—, así que se separa con un filete
   y va apretado: no puede empujar los resultados que sí valen fuera de la vista. */
.lean-salto__fallo { border-top: 1px solid var(--line); padding: var(--s3) var(--s4); text-align: left; font-size: var(--fs-sm); }
/* El pie con las teclas. Se queda siempre, también cuando la lista es larga: es
   la única parte de esto que hay que aprender, y se aprende viéndola. */
.lean-salto__pie {
  flex: none; display: flex; gap: var(--s4);
  padding: var(--s2) var(--s4);
  border-top: 1px solid var(--line); background: var(--panel-2);
  font-size: var(--fs-xs); color: var(--muted);
}
.lean-salto__pie kbd {
  font-family: var(--mono); font-size: var(--fs-xs);
  border: 1px solid var(--line); border-radius: var(--r-xxs); padding: 0 4px; margin-right: 2px;
  background: var(--panel);
}
/* En una pantalla estrecha sube casi hasta arriba: el teclado en pantalla se
   come la mitad de abajo, y con el 12 % de margen la ventana quedaba debajo de
   él en cuanto se ponía a escribir, que es lo primero que se hace aquí. */
@media (max-width: 640px) {
  .lean-salto { margin-top: var(--s4); max-height: 80vh; }
  .lean-salto__pie { display: none; }
}

/* ---------- lean-skeleton: el hueco mientras llega el contenido -------------
 *
 * NO ES UN «CARGANDO…» BONITO, y la diferencia decide dónde se usa: un esqueleto
 * PROMETE la forma de lo que viene. Puesto donde la respuesta puede ser «aquí no
 * hay nada», promete cinco filas y entrega un mensaje de vacío, y esa
 * promesa-decepción se percibe como un error de la aplicación. Así que:
 *
 *   · va donde el contenido tiene forma conocida —una tabla, una tira de cifras,
 *     una lista— y la espera es de red;
 *   · NO va donde el resultado normal puede ser vacío sin que sea un fallo. Ahí
 *     manda el estado vacío, que además explica qué hacer.
 *
 * Y sirve para otra cosa que el texto no hace: reserva el sitio. Con «Cargando…»
 * el bloque mide una línea y al llegar los datos la pantalla pega un salto de
 * cuatrocientos píxeles; con el esqueleto, el contenido aparece donde ya estaba
 * el hueco. Esa quietud es la mitad de la sensación de que va rápido.
 *
 * El brillo recorre de izquierda a derecha en 1,4 s: lo bastante lento para que
 * no parezca una alarma y lo bastante vivo para que se lea como «esto está en
 * marcha» y no como «esto se ha quedado colgado». */
.lean-skeleton { display: flex; flex-direction: column; gap: var(--s3); }
.lean-sk {
  --sk-alto: 14px;
  height: var(--sk-alto); border-radius: var(--r-xs);
  background: linear-gradient(90deg,
    var(--panel-2) 25%,
    color-mix(in srgb, var(--ink) 7%, var(--panel-2)) 37%,
    var(--panel-2) 63%);
  background-size: 400% 100%;
  animation: lean-sk-brillo var(--t-espera) var(--ease) infinite;
}
/* Las medidas son las de lo que sustituyen, no una escala nueva: una línea de
   texto, un rótulo corto, una cifra grande, una fila de tabla. */
.lean-sk--rotulo { --sk-alto: 11px; width: 90px; }
.lean-sk--linea  { --sk-alto: 14px; }
.lean-sk--cifra  { --sk-alto: 28px; width: 64px; }
.lean-sk--fila   { --sk-alto: 38px; }
/* Una lista de renglones no se lee como una lista si todos miden igual: el
   último se corta, como se corta un párrafo de verdad. */
.lean-sk--corta { width: 60%; }

/* --- lean-relevo: el hueco de un dato que ACABA de cambiar -----------------
   El hermano del esqueleto y su contrario: aquél reserva sitio para algo que
   todavía no ha llegado; éste tapa un dato que ya estaba mientras se sustituye
   por otro. Los dos usan la misma barra con brillo, y por eso viven juntos.

   PARA QUÉ: un panel que se refresca solo cambia cifras delante de quien mira
   sin que nadie haya pulsado nada, y una cifra que salta de 168 a 171 entre dos
   fotogramas no se lee — el ojo ve que algo ha pasado y no llega a saber qué.

   VA DENTRO DEL ELEMENTO Y A `inset` NEGATIVO: así copia su caja exacta sin que
   nadie tenga que calcularla, y desborda lo justo para tapar también el
   descuelgue de las letras. El `z-index` es porque debajo ya está pintado el
   valor nuevo, esperando a que se levante el hueco.

   Y LO QUE LLEGA ENTRA, que es la otra mitad. Sube tres píxeles porque el gesto
   de un dato que se sustituye es VERTICAL, como el de un marcador de
   aeropuerto — no un fundido, que no tiene dirección y por tanto no dice que
   una cosa ha dado paso a otra.

   Viene de la consola (`np-relevo`), donde estaba probada y encerrada. */
.lean-relevo { position: relative; }
/* EL HUECO TAPA LO ESCRITO, NO LA CELDA, y esto costó verlo en pantalla.

   Aquí ponía `inset: -2px -3px`, o sea «ocupa la caja entera del elemento». En la
   consola, de donde viene, eso estaba bien: allí el elemento es un `<span>` que
   mide justo lo que su cifra. En una FICHA no: `.lean-fact__v` es la columna del
   valor y mide 288 px aunque dentro ponga «Marta». Medido: el hueco salía de 294
   px de ancho para tapar sesenta de texto — una losa gris cruzando el renglón.

   Y el alto lo remataba: `.lean-sk` fija `height: 14px`, que gana al `inset`, así
   que sobre una fila de 21 px el hueco quedaba corto Y descolgado hacia arriba. El
   valor asomaba por debajo: se leía el dato A TRAVÉS del hueco que venía a
   taparlo, que es exactamente lo contrario de lo que este gesto significa.

   La caja la calcula ahora `relevarUno` con un `Range` sobre el contenido —la
   TINTA, no la celda— y la escribe en el estilo del propio nodo. Lo que queda aquí
   es el sitio por defecto para quien no la mida (y el `z-index`, que es lo que
   pone el hueco por delante del valor nuevo, que ya está pintado debajo
   esperando a que se levante). */
.lean-relevo > .lean-relevo__capa {
  position: absolute; inset: -2px -3px; z-index: 2;
  border-radius: var(--r-xs);
}
.lean-relevo--entra { animation: lean-relevo-entra 340ms var(--ease-entra); }
@keyframes lean-relevo-entra {
  from { opacity: 0; transform: translateY(-3px); }
}

/* --- Y DE DÓNDE VIENE EL VALOR NUEVO, cuando es una cifra -------------------
   El gesto ya era vertical —«como un marcador de aeropuerto, no un fundido, que
   no tiene dirección»— pero caía siempre hacia el mismo lado, así que un número
   que sube y uno que baja se movían igual. En un contador mecánico no: el dígito
   entra por abajo cuando la cuenta sube y por arriba cuando baja, y eso se lee
   sin haber visto el número anterior.

   LA DIRECCIÓN LA DEDUCE `relevar` SOLA comparando las dos cifras, así que esto
   no le cuesta una decisión a nadie. Lo que NO deduce es el color, y es la
   diferencia importante: subir no es bueno ni malo por sí mismo —en «En línea»
   subir es la buena noticia y en «Sin agente» es la mala—, así que el tinte lo
   pone la pantalla que sabe qué significa esa cifra, sobre estas mismas clases.
   Es la regla 5: el color es semántico, y aquí el sistema no tiene la semántica.

   Es un DESTELLO y no una marca: dura lo que la entrada. Una cifra teñida de
   verde a perpetuidad porque alguna vez subió no cuenta nada. */
.lean-relevo--sube.lean-relevo--entra { animation-name: lean-relevo-sube; }
.lean-relevo--baja.lean-relevo--entra { animation-name: lean-relevo-baja; }
@keyframes lean-relevo-sube {
  from { opacity: 0; transform: translateY(4px); }
}
@keyframes lean-relevo-baja {
  from { opacity: 0; transform: translateY(-4px); }
}

@media (prefers-reduced-motion: reduce) {
  .lean-relevo--entra { animation: none; }
}
@keyframes lean-sk-brillo {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}
/* Sin movimiento, el hueco se queda: sigue reservando el sitio y sigue diciendo
   que falta algo, que es para lo que está. */
@media (prefers-reduced-motion: reduce) {
  .lean-sk { animation: none; background: var(--panel-2); }
}
/* Lo que carga no se anuncia a un lector de pantalla renglón a renglón: se dice
   una vez, desde el contenedor. */
.lean-skeleton[aria-busy="true"] .lean-sk { pointer-events: none; }

/* ---------- Botón partido ----------
 * .lean-split — un botón que HACE lo de siempre y una flecha que abre el resto.
 *
 * POR QUÉ NO ES UN DESPLEGABLE A SECAS, que es lo que se pide siempre primero:
 * cuando una de las opciones es la inmensa mayoría de las veces, un menú cobra
 * un clic de más precisamente al caso común. En Equipos se dan de alta muchos
 * más ordenadores que pantallas, así que «Equipo» sigue costando UN clic —el
 * mismo que costaba antes de que hubiera nada que elegir— y la flecha guarda lo
 * demás. Un menú de dos donde el primero es el 90 % de los casos es un peaje
 * disfrazado de orden.
 *
 * Y CUANDO SÓLO QUEDA UNA COSA QUE HACER, LA FLECHA NO SE PINTA. Es lo que pasa
 * en cuanto un permiso retira la segunda opción, o cuando en una red hay un solo
 * ordenador en línea desde el que escanear: el control vuelve a ser el botón de
 * siempre, sin un hueco muerto al lado ni un menú de un solo elemento. Quien
 * pinta esconde la flecha y el radio se recompone solo (ver `:has` más abajo).
 *
 * Se apoya en `.lean-dropdown` para abrir y cerrar: la flecha lleva
 * `data-dropdown-toggle` y el menú es un `.lean-menu` normal, así que no hay
 * comportamiento nuevo que mantener. */
.lean-split { position: relative; display: inline-flex; }
/* Y CON `.lean-dropdown` PUESTO TAMBIÉN, que es como se usa SIEMPRE: la flecha
   necesita el desplegable para abrir su menú, así que las dos clases van juntas
   en los dos sitios donde hay un botón partido.
 *
 * `.lean-dropdown` declara `inline-block`, va después en esta hoja y con una
 * clase contra una clase gana por orden: el partido se quedaba en `block`, los
 * dos botones volvían a ser `inline-block` y **el salto de línea entre ellos se
 * convertía en un espacio**. Cuatro píxeles medidos, con las esquinas cuadradas
 * del medio a la vista, que es justo lo que un botón partido no puede tener: se
 * leía como dos botones mal cortados en vez de como una pieza.
 *
 * Llevaba así desde que existe el componente y salía en las dos pantallas que lo
 * usan —el alta de Equipos y el escaneo de Red—. Es el mismo fallo que la regla
 * de más abajo ya documenta para el fondo de la flecha, y se arregla igual: dos
 * clases, así que gana por especificidad y el orden dentro de la hoja deja de
 * importar. Lo que lo hace invisible es que un hueco de cuatro píxeles no da
 * ningún error y sólo se ve mirando la pantalla de cerca. */
.lean-split.lean-dropdown { display: inline-flex; }
/* Los dos botones se tocan, así que las esquinas del medio se cuadran y el
   borde interior lo pone la flecha —uno solo, no dos pegados—.

   CUADRAN SÓLO SI HAY UNA FLECHA A LA VISTA, y ésa es la parte que estaba mal.
   Se escribió con `:not(:only-child)` y más abajo un `:only-child` que le
   devolvía el radio entero al botón cuando se queda solo; el comentario que
   sigue lo cuenta con esas palabras —«se consigue sin JavaScript»— y **no se
   cumplía nunca**: dentro del partido hay TRES hijos, el botón, la flecha y el
   `.lean-menu` con las opciones, así que el botón no es hijo único jamás por
   mucho que la flecha esté escondida.

   Consecuencia, y se ve mirando: con una sola opción —que es el caso normal, un
   alta de ordenador o una red con un solo agente en línea— el botón salía con
   las dos esquinas de la derecha EN PICO, al lado de un botón de icono
   redondeado. Un canto recto en mitad de una barra se lee como una pieza
   partida a la que le falta el trozo de al lado, que es literalmente lo que
   parecía.

   `:has()` pregunta lo que de verdad importa —¿hay flecha visible?— en vez de
   contar hijos, así que da igual lo que se meta dentro del contenedor. */
.lean-split:has(> .lean-split__flecha:not([hidden])) > .lean-boton:first-child {
  border-top-right-radius: 0; border-bottom-right-radius: 0;
  /* Y NO PINTA SU BORDE DERECHO, que es la mitad que faltaba de «el borde
     interior lo pone la flecha —uno solo, no dos pegados—».
   *
   * Estaba dicho arriba y no se cumplía en las variantes que llevan borde
   * propio. La flecha pone su raya de 1 px, y el botón de la izquierda ponía
   * ADEMÁS su borde derecho: dos rayas contiguas, de dos colores distintos —el
   * filo de acción del botón y la raya translúcida de la flecha— sumando 2,2 px
   * medidos en el panel. Se lee exactamente como lo que es, un borde grueso en
   * mitad de la pieza, y parte en dos el control que este componente existe para
   * que se lea como uno.
   *
   * No se veía en el sólido, porque ahí el borde va transparente; sí en el
   * marcado, que es el del botón de escanear de Red y el del alta de Equipos, o
   * sea los dos únicos partidos que hay. Y no da ningún error: hay que mirar la
   * juntura de cerca, o medir los dos bordes calculados.
   *
   * Se quita el del botón y no la raya de la flecha porque la raya ya sabe
   * teñirse con la tinta de cada variante —sale de `currentColor`, ver la nota
   * de `.lean-split__flecha`— mientras que el borde del botón es el perímetro de
   * la pieza, y en la juntura ya no hay perímetro. Con `box-sizing: border-box`
   * el ancho total no cambia. */
  border-right: 0;
}
.lean-split__flecha {
  border-top-left-radius: 0; border-bottom-left-radius: 0;
  padding-left: var(--s2); padding-right: var(--s2);
  /* La raya del medio sale de la TINTA QUE LLEVA EL BOTÓN, no de un color fijo:
     así vale para el sólido —tinta blanca— y para el marcado y el perfil sin una
     regla por peso. Antes era `--on-brand`, que era la tinta oscura de encima
     del cian; con la tinta de acción en blanco, esa mezcla habría dejado la raya
     casi invisible sobre el relleno hondo, que es justo donde tiene que verse. */
  border-left: 1px solid color-mix(in srgb, currentColor 26%, transparent);
}
/* Y EL FILO DE LUZ NO PUEDE PARTIRSE EN DOS.
   El sólido lleva un `inset 0 1px 0` arriba, y en un partido eso son dos filos
   consecutivos con la raya del medio cortándolos: la pieza deja de leerse como
   una y se lee como dos botones pegados con una junta iluminada. La sombra
   proyectada tampoco se puede repetir —dos halos solapados dan el doble de tinta
   justo en la juntura—, así que las capas de relieve se ponen UNA VEZ, en el
   contenedor, y las mitades sólo aportan su relleno. */
.lean-split:has(> .lean-split__flecha:not([hidden])) {
  border-radius: var(--r-ctl);
  /* AQUI IBA LA ELEVACION DEL PARTIDO, y se va con la del solido.
     Estaba puesta en el CONTENEDOR a proposito —dos halos solapados dan el doble
     de tinta en la juntura—, y eran las mismas dos capas que llevaba el boton
     entero. Al quedarse el solido plano, dejarla aqui habria hecho que el unico
     boton del panel que sigue flotando fuera justo el partido: el mismo peso
     visual diciendo dos cosas segun lleve flecha o no. El reparto del filo entre
     las dos mitades se queda, que es lo que hace que se lean como una pieza. */
}
.lean-split:has(> .lean-split__flecha:not([hidden])) > .lean-boton--solido { box-shadow: none; }
.lean-split:has(> .lean-split__flecha:not([hidden])) > .lean-boton--solido:first-child {
  box-shadow: inset 0 1px 0 var(--filo-luz);
}
/* La flecha aporta la otra mitad del filo, recortada a su lado: sumados, los dos
   dan una sola línea de luz continua a lo ancho de la pieza. */
.lean-split:has(> .lean-split__flecha:not([hidden])) > .lean-split__flecha.lean-boton--solido {
  box-shadow: inset 0 1px 0 var(--filo-luz);
}
/* La talla pequeña recorta el contenedor con su propio radio: es el partido de
   la barra de Red, y es el único que hay a 30 px. */
.lean-split:has(> .lean-boton--sm) { border-radius: var(--r-ctl-sm); }
.lean-split .lean-ico { flex: none; }

/* ABIERTO NO PUEDE DESPINTAR LA MITAD DE UN BOTÓN.
 *
 * La regla general del desplegable manda el disparador a `--panel-2` mientras
 * el menú está fuera —«abierto se ve abierto»—, y en un mando suelto está bien.
 * Aquí no: la flecha es la mitad de una pieza que el ojo lee como UNA, así que
 * teñirla de gris la parte en dos, y sobre el tema oscuro además se lee como
 * deshabilitada. Medido al mirarlo: el cuerpo en cian y la flecha en gris de
 * panel, pegados.
 *
 * Lo que dice aquí que está abierto es el propio menú, que cuelga justo debajo
 * y pegado; la flecha se limita a hacer lo que hace al pasar por encima.
 *
 * LLEVA `.lean-split` DELANTE PARA GANAR POR ESPECIFICIDAD Y NO POR ORDEN, y
 * ésa es la parte que hay que respetar al tocarlo. La regla general
 * —`.lean-dropdown.is-open > [data-dropdown-toggle]:not(.is-activo)`— suma
 * exactamente lo mismo que esto sin el primer selector: cuatro. En un empate
 * gana la que va después en el archivo, que es la suya, así que esto se escribió
 * primero y no se aplicaba: la flecha seguía saliendo gris y no había ningún
 * error que lo dijera. Con el selector de más, el orden dentro de la hoja deja
 * de importar. */
.lean-split.lean-dropdown.is-open > .lean-split__flecha.lean-boton--solido {
  /* EL MISMO DEGRADADO DEL HOVER, no un color plano. Aquí ponía `--brand-hover`,
     que era el cian aclarado y funcionaba mientras el relleno era plano; con el
     sólido en degradado, un color liso en la mitad derecha parte la pieza en dos
     otra vez — el mismo defecto que esta regla existe para evitar, entrando por
     la puerta de al lado. Se le pide al botón lo que ya hace al pasar por
     encima, que es lo que dice «me estás usando». */
  background-color: var(--accion-hover);
  border-color: transparent;
}
.lean-split.lean-dropdown.is-open > .lean-split__flecha.lean-boton--marcado {
  background: var(--accion-lavado-2);
  border-color: var(--accion-filo);
}

/* ---------- Menú desplegable ----------
 * .lean-dropdown envuelve el disparador ([data-dropdown-toggle]) y el .lean-menu.
 * lean.js abre/cierra con clic y cierra al pulsar fuera.
 *
 * El menú va en position: fixed y lean.js le pone las coordenadas al abrirlo,
 * porque colgado del disparador lo recorta cualquier ancestro con overflow —y
 * las tablas del panel lo tienen. Donde haya soporte de popover, además sube al
 * top layer, así que tampoco depende del z-index de quien lo contenga. */
.lean-dropdown { position: relative; display: inline-block; }
.lean-dropdown:not(.is-open) .lean-menu { display: none; }
/* ABIERTO SE VE ABIERTO. El disparador de un desplegable se pintaba EXACTAMENTE
   igual con el menú desplegado que sin él —mismo fondo y mismo borde, medido—,
   así que lo único que decía que estaba abierto era el propio menú flotando
   debajo. Y el panel ya lo hacía bien en un sitio: el botón de usuario del
   lateral pasa a superficie mientras su menú está fuera.

   Va con `--panel-2` y no con el lavado de marca porque abierto NO es elegido:
   mientras el menú está desplegado, el disparador deja de ser un control y pasa
   a ser la cabecera de lo que hay debajo. Lo que SÍ está elegido —un filtro
   puesto— conserva su borde y su tinta de marca, y por eso se excluye: si no,
   abrir el mando le borraría la señal de que está filtrando. */
.lean-dropdown.is-open > [data-dropdown-toggle]:not(.is-activo) {
  background: var(--panel-2); border-color: color-mix(in srgb, var(--ink) 22%, var(--line));
}
/* UN MENÚ ESTÁ POR ENCIMA DE LO QUE TAPA, y hasta hoy se pintaba con el mismo
   `--panel` que la tarjeta de debajo: en el tema oscuro, donde una sombra sobre
   casi negro no se ve, un desplegable sobre una tarjeta era el mismo papel con
   un borde en medio. `--alzado` sube el papel donde el tema lo permite y en
   claro vale blanco, que es lo correcto: allí la altura la dice la sombra. */
/* ── EL MENÚ ENTRA Y SALE, Y SALE DE SU BOTÓN ──────────────────────────────
 *
 * No tenía nada: `display:none` y ya, en las dos variantes —el menú global de
 * las fichas y el del botón partido—. Y es el componente más repartido que hay:
 * DIECISIETE módulos lo usan, de Equipos a la Wiki. Un menú tapa lo que hay
 * debajo por definición, así que por la ley del movimiento entra y sale
 * (docs/animaciones.md §1 y su corolario).
 *
 * SE HACE CON `allow-discrete` Y `@starting-style`, que es la técnica que
 * `.lean-dialog` ya usa aquí mismo: así el elemento sigue abriéndose con
 * `hidden` —como hoy, sin tocar una línea de JavaScript— y aun así anima en los
 * dos sentidos. `display` es la propiedad discreta que hay que dejar pasar; el
 * `overlay` del diálogo no aplica porque esto no está en la capa superior.
 *
 * Y CRECE DESDE EL PUNTO DONDE ESTÁ SU BOTÓN, no de su propio centro. Un menú
 * que sale del centro de sí mismo se lee como un cuadro de diálogo pequeño; lo
 * que dice «esto lo has desplegado tú desde ahí» es que el origen esté en el
 * disparador. Las coordenadas las escribe `colocarMenu`, que es quien ya sabe si
 * cabe por debajo o ha tenido que abrirse hacia arriba, y con qué desplazamiento
 * lateral por el borde de la ventana.
 *
 * Los valores son los del diálogo y no otros: `--entra-y` de recorrido y .98 de
 * escala. Inventar aquí un .96 es como empezaron las nueve entradas con seis
 * duraciones distintas que costó una tarde unificar. */
.lean-menu { position: fixed; z-index: var(--z-menu); min-width: 190px; margin: 0; padding: var(--s1); background: var(--alzado); border: 1px solid var(--linde-alto); border-radius: var(--r-s); box-shadow: var(--shadow-lg); overflow: hidden;
  transform-origin: var(--menu-org-x, 100%) var(--menu-org-y, 0%);
  opacity: 0; transform: translateY(calc(var(--entra-y) * -1)) scale(.98);
  transition: opacity var(--t-sale) var(--ease-sale),
              transform var(--t-sale) var(--ease-sale),
              display var(--t-sale) allow-discrete;
}
/* Abierto hacia ARRIBA entra desde abajo: el recorrido va siempre en el sentido
   en que el menú se despliega, o el gesto contradice a la colocación. */
.lean-menu[data-hacia="arriba"] { transform: translateY(var(--entra-y)) scale(.98); }
.lean-menu:not([hidden]) {
  opacity: 1; transform: none;
  transition: opacity var(--t-entra) var(--ease-entra),
              transform var(--t-entra) var(--ease-entra),
              display var(--t-entra) allow-discrete;
}
@starting-style {
  .lean-menu:not([hidden]) { opacity: 0; transform: translateY(calc(var(--entra-y) * -1)) scale(.98); }
  .lean-menu[data-hacia="arriba"]:not([hidden]) { opacity: 0; transform: translateY(var(--entra-y)) scale(.98); }
}
@media (prefers-reduced-motion: reduce) {
  .lean-menu, .lean-menu:not([hidden]) { transition-duration: 1ms; transform: none; }
}
/* Sin coordenadas todavía (el primer fotograma, antes de que lean.js mida):
 * debajo del disparador y no en la esquina de la ventana. */
.lean-menu:not([style*="top"]) { top: 100%; right: 0; }
.lean-menu:popover-open { display: block; }
.lean-menu button, .lean-menu a { display: flex; align-items: center; gap: var(--s2); width: 100%; padding: 8px 10px; border: none; background: none; font: inherit; font-size: var(--fs-md); color: var(--ink); text-align: left; border-radius: var(--r-xs); cursor: pointer; text-decoration: none; }
.lean-menu button:hover, .lean-menu a:hover { background: var(--al-pasar); }
.lean-menu .danger { color: var(--down); }
/* Una opción que ahora mismo no se puede usar se queda a la vista y apagada, no
   desaparece: que falte una línea del menú no cuenta POR QUÉ falta, y aquí el
   porqué es justo lo que hay que saber —«no se puede borrar, tiene tres modelos
   detrás»—. El motivo va en su `data-tip`, que los tooltips de Lean sí salen
   sobre un botón deshabilitado (se buscan con elementFromPoint). */
.lean-menu button:disabled { opacity: .45; cursor: not-allowed; }
.lean-menu button:disabled:hover { background: none; }
.lean-menu .lean-ico { color: var(--muted); width: var(--ico-md); height: var(--ico-md); }
.lean-menu .danger .lean-ico { color: var(--down); }
.lean-menu hr { border: none; border-top: 1px solid var(--line); margin: var(--s1) 0; }
/* Un rótulo que dice SOBRE QUÉ actúa el menú. Lo pide el de acciones en lote de
   Equipos: las mismas dos acciones alcanzan a todo el inventario o sólo a los
   que se están viendo, y eso hay que decirlo donde se pulsa, no en el botón. */
.lean-menu__rotulo { margin: 2px 10px 6px; font-size: var(--fs-xs); font-weight: var(--peso-titulo);
  text-transform: uppercase; letter-spacing: var(--letra-rotulo); color: var(--muted); }
/* El recuento de un ítem, alineado a la derecha: cuántos alcanza esa acción. */
.lean-menu__n { margin-left: auto; padding-left: var(--s3); color: var(--muted);
  font-variant-numeric: tabular-nums; }

/* --- lean-maq: CÓMO SE LLAMA UN ORDENADOR ------------------------------------

   Dos cosas en un renglón: cuál es, y de quién.

     005PC00123 (gabriel)

   NACIÓ DENTRO DE UN MENÚ Y SE SALIÓ DE ÉL. Se llamaba `lean-menu__maq` y era
   de «escanear desde» —los ordenadores desde los que se puede recorrer la red de
   un cliente—, que era una lista de códigos: `005PC00123`, `005PC00119`, y nada
   más. Elegir entre dos así es elegir a ciegas: son consecutivos, son de la
   misma oficina y no hay nada que los distinga. Con el nombre de quien lo usa,
   el técnico que está al teléfono con Gabriel sabe cuál es sin mirar nada.

   Y ESO NO ERA UN PROBLEMA DE ESE MENÚ. Era el problema de las otras diecinueve
   pantallas que nombran un ordenador: «Visto por 005PC00123» en la ficha de un
   dispositivo de red, «Está en 005PC00123» en la de un monitor, seis códigos
   seguidos en Ctrl+K. El arreglo estaba escrito y encerrado en un sitio, así que
   la pieza se sacó del menú —el menú es ahora un cliente más— y el nombre se
   compone en un solo lugar: `app/maquina.js`, que es quien decide además cuándo
   el paréntesis NO se pone (una cuenta «Administrador» no distingue nada).

   EL PUNTO DEL MENÚ SIGUE SIENDO SUYO. Lo de abajo —el aro de `lean-presencia`
   sobre `--alzado`, el visto a la derecha— es del ítem de menú y se queda con
   él: son tres cosas en el renglón cuando además hay que decir si está.

   EL PUNTO ES `lean-presencia` Y VA DELANTE, que es donde vive en toda la
   aplicación: sobre la foto de un equipo, delante del nombre de un dispositivo,
   al principio de una fila de `lean-opcion`. Aquí además cambia el aro, y no es
   un detalle: el punto lleva `box-shadow` del color del fondo para despegarse de
   lo que tenga debajo, y ese color es `--panel`. Un menú está en `--alzado`, así
   que con el aro heredado el punto sale con un halo de otro color — el mismo
   error que se arregló cuando el punto bajó a las tarjetas.

   EL DUEÑO VA ENTRE PARÉNTESIS Y EN GRIS, no en una segunda línea. Un menú de
   elección se lee en vertical, de un barrido, y una segunda línea por ítem
   dobla la altura de la lista para un dato que sólo sirve para reconocer cuál es
   el equipo que se busca. En gris queda claro qué es el nombre de la cosa y qué
   es la pista: la placa es lo que identifica, la persona es lo que lo hace
   reconocible.

   Y LO QUE SE CORTA ES EL NOMBRE DE LA PERSONA, nunca la placa. Una placa
   truncada no identifica nada —«005PC001…» puede ser cualquiera de nueve—
   mientras que «(maria.fer…)» sigue diciendo de quién es. Por eso el
   `text-overflow` va en el paréntesis y la placa lleva `flex: none`.

   EL VISTO BUENO SE VA A LA DERECHA. Estaba a la izquierda, con un hueco de un
   carácter reservado en los demás para que no baile el texto, y ese hueco es
   ahora del punto. A la derecha es donde esta hoja ya pone lo que acompaña a un
   ítem sin ser el ítem (ver `lean-menu__n`), y de paso deja de competir con el
   punto por el mismo sitio. */
/* `inline-flex` y NO `flex`: esto se pinta dentro de una frase, dentro de una
   celda y dentro de un renglón que ya tiene más cosas, y un bloque ahí rompe la
   línea. Y sin `overflow:hidden` en el contenedor a propósito: un `inline-flex`
   recortado deja de dar su línea base y se descuadra medio píxel respecto al
   texto de al lado. Quien recorta es el paréntesis, que es quien tiene que
   recortarse. `max-width:100%` es lo que impide que se salga de su hueco. */
/* SIN `gap`: el hueco entre la placa y el paréntesis es un espacio duro del
   CONTENIDO (ver `maquina.js`), para que copiar el nombre con el ratón se lleve
   también el espacio. Con los dos, el hueco saldría del doble. */
.lean-maq { display: inline-flex; align-items: baseline;
  min-width: 0; max-width: 100%; }
/* LA PLACA HEREDA EL SUBRAYADO DEL ENLACE, y esto no es un adorno: sin estas
   cuatro líneas un `lean-maq` que además es enlace SE VE SIN SUBRAYAR.
   `text-decoration` NO se propaga a un hijo que es flex item —el hijo abre su
   propio contexto de formato y el subrayado del padre se queda fuera—, así que
   `lean-ir` y `lean-enlace` puestos aquí pintaban su línea sobre un contenedor
   que no tiene texto propio: la regla se aplicaba, el subrayado no aparecía y no
   fallaba nada. Medido en la ficha de un monitor: el `<a>` decía
   `text-decoration-line: underline` y sus dos hijos, `none`.
   Con `inherit` la placa se lleva el subrayado QUE TOQUE —punteado de `lean-ir`,
   sólido de `lean-enlace`, ninguno si no es enlace— sin que esta pieza tenga que
   saber cuál de los dos la envuelve, y sin duplicar aquí sus dos recetas.
   Y lo lleva la PLACA y no el paréntesis a propósito: lo que lleva a la ficha es
   el nombre; el dueño es la pista que lo hace reconocible. */
.lean-maq__id {
  flex: none;
  text-decoration-line: inherit; text-decoration-color: inherit;
  text-decoration-style: inherit; text-decoration-thickness: inherit;
}
/* Y CON LA LETRA DE MÁQUINA cuando quien llama pide `mono`: una placa es un
   identificador igual que una IP o una MAC, se lee carácter a carácter y se
   dicta por teléfono. Es exactamente lo que hacía `lean-u-mono` en los sitios
   donde esta pieza entró, y por eso lleva su mismo cuerpo: sin esto, un `<code>`
   se quedaría con la monoespaciada que traiga el navegador, que no es la del
   panel. */
code.lean-maq__id { font-family: var(--mono); font-size: var(--fs-sm); }
/* Y LO QUE SE CORTA ES EL NOMBRE DE LA PERSONA, nunca la placa. Una placa
   truncada no identifica nada —«005PC001…» puede ser cualquiera de nueve—
   mientras que «(maria.fer…)» sigue diciendo de quién es. Por eso el
   `text-overflow` va aquí y la placa lleva `flex: none`. */
.lean-maq__de { min-width: 0; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color var(--t-fast) var(--ease); }
/* Y AL APUNTARLO REACCIONA LA PIEZA ENTERA, no media. El área pulsable es todo
   —placa y paréntesis—, así que dejar el gris de `--muted` quieto mientras la
   placa se enciende parte en dos algo que es una sola cosa. Sigue el color del
   enlace, atenuado, para no dejar de ser la pista. */
a.lean-maq:hover .lean-maq__de,
button.lean-maq:hover .lean-maq__de {
  color: color-mix(in srgb, currentColor 72%, transparent);
}

/* En una cabecera la placa manda y el dueño va detrás sin competir con ella. */
.lean-maq--titulo .lean-maq__de { font-size: var(--fs-sm); font-weight: var(--peso-texto); }
/* Dentro de un menú sí ocupa el renglón entero y sí recorta: el ítem tiene un
   ancho fijo y a la derecha le espera el visto bueno. */
.lean-menu .lean-maq { display: flex; overflow: hidden; }
.lean-menu button > .lean-presencia { flex: none; width: 8px; height: 8px;
  box-shadow: 0 0 0 2px var(--alzado); }
.lean-menu button > .lean-presencia.is-sin {
  box-shadow: 0 0 0 2px var(--alzado), inset 0 0 0 1.5px currentColor;
}
/* El visto del elegido, en el borde derecho del ítem. */
.lean-menu__elegido { margin-left: auto; padding-left: var(--s3); color: var(--brand); }

/* Aquí vivía `lean-avatar`, un círculo de 36 px con una inicial gris. No lo usó
   NUNCA ninguna pantalla: el avatar de esta aplicación es `.avatar` de
   `app.css`, que tiñe el círculo con el tono de la persona (`--av-h`), admite
   foto y tiene el sello del sistema. Dos avatares es no tener ninguno, y el que
   sabe quién es cada uno es el de la aplicación. */

/* --- lean-verificado: esta identidad la garantiza el sistema ------------------

   El sello que va DETRÁS DE UN NOMBRE para decir que quien firma no es una
   persona con ese nombre, sino el propio sistema.

   Existe porque un nombre es texto y el texto se imita: en este panel el nombre
   visible se lo cambia cada uno desde su cuenta, así que «NetProof» escrito en
   una fila no prueba absolutamente nada. Lo que prueba es de dónde sale la fila,
   y eso lo sabe el backend; este sello es cómo se ve esa garantía.

   POR ESO NO SE PINTA NUNCA A PARTIR DEL TEXTO. Quien lo use tiene que poder
   señalar el dato del servidor que lo enciende. Pintarlo comparando el nombre
   con una cadena sería devolverle el sello a quien se lo quiera poner.

   Va después del nombre y no encima del avatar: en el avatar, a 26 px y entre
   otros veinte, no se ve; detrás del nombre se lee en la misma pasada que el
   nombre.

   Y ES UNA FORMA RELLENA, no un trazo: la insignia de doce lóbulos con el visto
   dentro, que la dibuja `verificado()` en base.js. Un icono de línea al lado de
   un nombre se lee como un adorno; ésta se reconoce sin mirarla, que es de lo
   que vive un sello. */
.lean-verificado {
  display: inline-flex; vertical-align: -3px; margin-left: 4px;
  width: 15px; height: 15px; flex: none; color: var(--brand);
}
.lean-verificado svg { width: 100%; height: 100%; display: block; }

/* Aquí vivía `lean-progress`, la barra de progreso, con sus tres estados y su
   franja de «no se sabe cuánto queda». Tampoco la usó ninguna pantalla: la
   barra que corre de verdad es `.prog` / `.prog-bar` de `app.css`, y tiene el
   mismo `is-indef` escrito aparte. La de aquí medía 8 px y la que trabaja mide
   4, porque va debajo de una fila de tabla; eso no es una variante que faltara,
   es la prueba de que el componente se escribió sin mirar dónde iba. */

/* Aquí vivía `lean-skel`, un SEGUNDO esqueleto: misma idea que `lean-sk`, otro
   gradiente y otra duración (1,3 s contra 1,4 s). No lo usaba ninguna pantalla
   —sólo la guía, o sea que el escaparate enseñaba un componente que la
   aplicación no gastaba— y tenerlo obligaba a elegir entre dos cosas iguales sin
   ningún criterio para hacerlo. Se queda `lean-sk`, que es el que trabaja y el
   que tiene escritas las medidas de lo que sustituye. */

/* ---------- Breadcrumb ---------- */
.lean-breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: var(--fs-md); color: var(--muted); }
.lean-breadcrumb a { color: var(--muted); text-decoration: none; }
.lean-breadcrumb a:hover { color: var(--ink); }
.lean-breadcrumb .sep { color: var(--muted); display: inline-flex; }
.lean-breadcrumb [aria-current] { color: var(--ink); font-weight: var(--peso-medio); }

/* Aquí vivía `lean-pager`, la paginación de páginas numeradas. Se ha ido porque
   contradecía una decisión ya tomada y escrita: aquí se pagina con «cargar
   más», nunca con números (ver la cabecera de `auditoria.js`). Un componente
   que enseña la forma que el proyecto descartó es peor que no tenerlo: quien
   abra el catálogo buscando cómo paginar se lo encuentra ofrecido. */

/* ---------- Estado vacío ---------- */
/* --breve: el estado vacío de UNA LÍNEA, para dentro de una tabla («Sin equipos
   que coincidan») donde no cabe el icono ni el botón. Es el mismo mensaje en su
   grado mínimo, no otro componente.
   Venía de `app.css` como `.tabla-vacia`, y llevaba `padding: … !important`.
   Ese !important es la pista de lo que pasaba: `lean-table td` son dos
   selectores y le ganaba, así que en vez de escribir dos selectores se usó la
   palabra mágica. Aquí se resuelve como manda la casa —una regla más específica
   para cuando vive en una celda— y sin !important. */
.lean-vacio--breve { padding: var(--s6) var(--s5); }
.lean-table td.lean-vacio--breve { padding: var(--s6) var(--s5); }
/* ENTRA, y es el sitio donde más falta hacía: filtrar hasta que no queda nada
   sustituía una tabla llena por un dibujo y dos renglones en un fotograma, y
   durante un instante parece que la pantalla se ha roto en vez de que la
   búsqueda no ha encontrado nada. Con la entrada de la casa —los mismos 150 ms y
   los mismos 6 px que todo lo que aparece— se lee como una respuesta.

   NO LLEVA RETRASO AQUÍ. Quien quiera el relevo completo —las filas yéndose
   primero y el mensaje llegando después— lo compone desde fuera, porque el
   orden depende de si había algo antes: llegar a una lista que ya estaba vacía
   no tiene nada que relevar. */
.lean-vacio { text-align: center; padding: var(--s7) var(--s5); color: var(--muted);
  animation: lean-entra var(--t-entra) var(--ease-entra) backwards; }
@media (prefers-reduced-motion: reduce) { .lean-vacio { animation: none; } }

/* EL TONO DEL ICONO, que es lo único que separa los tres estados de un hueco.
 *
 * Aquí no hay contenido que enseñar, y hay TRES razones distintas por las que
 * puede no haberlo. La anatomía es la misma en las tres —icono, título, línea y
 * la acción siguiente—, y lo demuestra `avisos.js`, que las pinta las tres con
 * este mismo componente sin tocar nada. Lo que cambia es el tono:
 *
 *   (sin modificador)  no hay nada que enseñar          --faint
 *   --down             no se ha podido saber si hay algo  --down
 *   --ok               no hay nada, y es la buena noticia --ok
 *
 * El color iba escrito a pelo en la regla de abajo y era INCONDICIONAL, así que
 * los tres salían del mismo gris apagado. Dos consecuencias, y la segunda es la
 * que costó ver: un fallo del servidor se leía igual que «aquí no hay nada»
 * —que es lo que dejó a la Wiki diciendo «Todavía no hay categorías» después de
 * un 500—, y el «Todo en orden» de Avisos, que es un ACIERTO, salía del gris de
 * lo apagado en vez del verde que le toca. La regla 5 lo dice sin matices: el
 * color de estado es semántico, nunca decorativo.
 *
 * Va por un token local y no repitiendo la regla con dos selectores, para que
 * los 38 usos que no declaran tono sigan saliendo exactamente igual que antes. */
.lean-vacio { --vacio-ico: var(--faint); }
.lean-vacio--down { --vacio-ico: var(--down); }
.lean-vacio--ok { --vacio-ico: var(--ok); }
.lean-vacio .lean-ico { width: 40px; height: 40px; color: var(--vacio-ico); margin-bottom: var(--s3); }
.lean-vacio h3 { font-family: var(--disp); font-size: var(--fs-xl); font-weight: var(--peso-titulo); color: var(--ink); margin: 0 0 6px; }
.lean-vacio p { margin: 0 auto var(--s4); max-width: 44ch; font-size: var(--fs-lg); }
/* El icono grande es el del hueco, no el de todo lo que caiga dentro: si el
   estado vacío lleva un botón con icono, el suyo es el de un botón. */
.lean-vacio .lean-boton .lean-ico { width: var(--ico); height: var(--ico); margin-bottom: 0; color: inherit; }

/* ---------- Navegación: la entrada ----------
 * Aquí vivía además `lean-sidebar`, el CONTENEDOR de 240 px, escrito «para
 * reutilizarlo en el portal futuro». Nunca lo usó nadie, y el portal tampoco lo
 * habría usado: el lateral de este panel es `.nav-item` de `app.css` porque
 * tiene una gramática que Lean no puede tener —el color de la pastilla es la
 * GRAVEDAD de lo que hay sin ver—, y eso lo vigila `lateral_test.go`. Lo que sí
 * era reutilizable es la ENTRADA, y está aquí abajo trabajando en tres navs. */
/* Sirve en un <a> y en un <button>, y por eso apaga lo que el navegador le pone
   de serie a un botón: fondo gris, borde y su propia tipografía. Sin esto el
   componente sólo valía para enlaces —el lateral y Herramientas usan <a>— y en
   un botón salía una pastilla gris con el texto casi ilegible. Y un botón es lo
   correcto cuando la entrada FILTRA en vez de llevar a otra dirección, que es lo
   que hacen las secciones del índice de la Wiki. */
.lean-nav-item { background: none; border: 0; text-align: left; font-family: inherit;
  position: relative; display: flex; align-items: center; gap: var(--s3); padding: 9px 12px; border-radius: var(--r-s); color: var(--muted); text-decoration: none; font-size: var(--fs-lg); font-weight: var(--peso-medio); cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }
.lean-nav-item:hover { background: var(--al-pasar); color: var(--ink); }
/* ACTIVO: TINTA, PESO Y LA BARRA. SIN FONDO, y lo mismo que el lateral.
 *
 * Llegó a llevar un radial del 20 % más un lineal del 9 % —su propia versión de
 * «estás aquí», a tres centímetros de la del lateral, que usaba un plano—. El
 * primer arreglo fue unificar los dos en `--elegido`; el segundo, quitarlo, y lo
 * pidió Leandro mirándolo: «parece IA slop». Y era contable: fondo, tinta, peso y
 * barra, cuatro señales para un hecho.
 *
 * Se va el fondo. Es la regla que este sistema ya se dio con el tema negro —«la
 * LUZ dice dónde estás; el COLOR, qué informa y qué actúa»— y la misma que se
 * aplicó a los botones al sacar el cian del relleno. Con ella, el velo vuelve a
 * ser exclusivamente del puntero: hasta ahora, apuntar el destino en el que ya
 * estabas no podía decir nada porque los dos se pintaban con fondo.
 *
 * El icono sí se tiñe —es una forma pequeña, no texto— y la barra se queda,
 * porque no es decoración: es la que VIAJA de un destino al siguiente. */
.lean-nav-item.is-active { color: var(--ink); font-weight: var(--peso-titulo); }
.lean-nav-item.is-active .lean-ico { color: var(--brand-ink); }
.lean-nav-item.is-active::before { content: ""; position: absolute; left: 0; top: 9px; bottom: 9px; width: 3px; border-radius: 0 var(--r-pill) var(--r-pill) 0; background: linear-gradient(var(--brand), var(--brand-ink)); }
.lean-nav-item .lean-ico { width: var(--ico-md); height: var(--ico-md); }

/* ---------- Tarjetas: variantes ----------
 * --hover: elevable/clicable. --pad0: sin relleno, para lo que va A SANGRE
 * dentro de la caja —hoy, una tabla que llega a los bordes—.
 *
 * Aquí vivían además `__media` (imagen o banner de cabecera) y `__body` (el
 * relleno de dentro cuando la tarjeta va sin él). Se han ido: en este panel una
 * tarjeta presenta DATOS y nunca una foto, así que en cuatro años no los pidió
 * ninguna pantalla. Y con ellos se va la razón que `--pad0` decía tener, que no
 * era la suya: lo que de verdad le pide sitio pegado al borde es la tabla. */
.lean-tarjeta--hover { transition: transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease); cursor: pointer; }
.lean-tarjeta--hover:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); border-color: color-mix(in srgb, var(--brand) 40%, var(--line)); }
.lean-tarjeta--pad0 { padding: 0; overflow: hidden; }

/* Tendencia (delta) para tarjetas de estadística.
 *
 * OJO CON LOS NOMBRES: `--up` y `--down` NO dicen hacia dónde apunta la flecha,
 * dicen SI LA NOTICIA ES BUENA. Quién es cuál lo decide quien llama, y la flecha
 * (▲/▼) la pone él por su cuenta. El caso que lo enseña está en `resumen.js`:
 * «MÁS AVISOS ES PEOR, así que la flecha que sube va en rojo» —o sea, un delta
 * positivo pintado con `--down`—.
 *
 * Se escribe aquí porque leído sin esto parece un fallo: «una tendencia al alza
 * no es "bien"». Lo es o no lo es según lo que se mida, y por eso el color no
 * puede salir del signo. Una auditoría llegó a marcarlo como color de estado
 * usado de adorno; no lo es. Si algún día se renombran, `--bien`/`--mal` dirían
 * lo que hacen. */
.lean-trend { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-sm); font-weight: var(--peso-titulo); }
.lean-trend--up   { color: var(--ok); }
.lean-trend--down { color: var(--down); }
.lean-trend .lean-ico { width: var(--ico); height: var(--ico); }

/* --- lean-bancada: las pantallas de una sección, en una tira ---------------
   La navegación de una sección que tiene POCOS destinos y en la que se
   TRABAJA, no se configura. Va arriba, horizontal, y deja el ancho entero
   debajo.

   ES LA OTRA MITAD DE `lean-nav-item`, y la diferencia es de uso, no de forma.
   Una nav lateral está bien donde se entra a cambiar un ajuste y salir: la
   lista es larga, se agrupa, y el contenido de la derecha es un formulario que
   no necesita ancho. Herramientas no es eso —se entra a resolver un ticket, y
   lo que hay a la derecha es una traza de protocolo o una tabla de registros—,
   así que la nav lateral se llevaba 214 px permanentes para cinco palabras y
   dejaba el resto de su columna en blanco toda la sesión.

   Y HACE UN SEGUNDO TRABAJO QUE LA LATERAL NO HACÍA: enseñar de una vez todo lo
   que hay. En una tira de cinco, quien entra por primera vez ve las cinco sin
   pulsar nada; en la lateral las veía también, pero en gris y sin forma de
   control, así que se leían como un índice y no como un conmutador.

   Cuándo NO: con más de seis o siete destinos —ahí la tira envuelve o hace
   scroll, y las dos cosas rompen lo único que la justifica— y cuando los
   destinos se AGRUPAN, porque una tira no admite rótulos. En los dos casos,
   nav lateral. */
.lean-bancada {
  display: flex; align-items: center; gap: var(--s1);
  padding: var(--s1); width: fit-content; max-width: 100%;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-pill);
  /* En una pantalla estrecha la tira se desliza en vez de envolver: envolviendo,
     la segunda fila queda descolgada dentro de una píldora que ya no lo parece. */
  overflow-x: auto; scrollbar-width: none;
}
.lean-bancada::-webkit-scrollbar { display: none; }
.lean-bancada__it {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  height: var(--ctl-h); padding: 0 var(--s4) 0 var(--s3); border-radius: var(--r-pill);
  border: 0; background: transparent; cursor: pointer; text-decoration: none;
  font-family: inherit; font-size: var(--fs-md); font-weight: var(--peso-medio); color: var(--muted);
  white-space: nowrap;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lean-bancada__it .lean-ico { width: var(--ico); height: var(--ico); opacity: .85; }
.lean-bancada__it:hover { background: var(--al-pasar); color: var(--ink); }
/* «Estás aquí» se dice igual en todas partes, y desde 1.129.0 se dice SIN FONDO:
   tinta, peso y el icono a plena opacidad. Es el mismo cambio que el destino del
   lateral y la nav de Ajustes —cuatro señales para un hecho eran demasiadas— y
   aquí además devuelve el velo al puntero, que es de quien era. */
.lean-bancada__it.is-active,
.lean-bancada__it[aria-current] { color: var(--ink); font-weight: var(--peso-titulo); }
.lean-bancada__it.is-active .lean-ico,
.lean-bancada__it[aria-current] .lean-ico { opacity: 1; }
.lean-bancada__it:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
/* El filete separa la salida —volver al índice de la sección— de los destinos.
   Es la misma raya de un píxel que separa las lecturas de un membrete: dos
   grupos dentro de una pieza, no dos piezas. */
.lean-bancada__sep { width: 1px; height: 18px; flex: none; background: var(--line); margin-inline: var(--s1); }

/* ---------- Barra de herramientas / filtros de tabla ----------
 * Sobre una tabla: búsqueda + filtros a la izquierda, acciones a la derecha;
 * debajo, los filtros activos como tags quitables. */
.lean-toolbar { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.lean-toolbar .grow { flex: 1; min-width: 12px; }
.lean-toolbar .lean-field { width: auto; min-width: 200px; flex: 1; max-width: 340px; }
/* Los desplegables de una barra miden todos lo mismo: con el ancho del
 * contenido, tres filtros seguidos dan una fila de dientes de sierra. */
.lean-toolbar .lean-select-wrap, .lean-toolbar .lean-multi { width: auto; min-width: 168px; }
/* Un filtro puesto tiene que verse puesto, o no hay manera de saber por qué la
 * lista está recortada. */
.lean-select.is-activo { border-color: var(--brand); color: var(--brand-ink); font-weight: var(--peso-firme);
  background: var(--elegido); }
.lean-select-wrap:has(.is-activo)::after { border-color: var(--brand-ink); }
/* Los campos llevan ancho completo, que en una barra se come la fila entera y
 * empuja el resto de controles a una segunda línea. Aquí se acotan. */
.lean-toolbar .lean-input { flex: 0 1 280px; width: auto; }
/* En una barra TODO mide la talla md, incluidos los controles marcados `--sm`:
 * el buscador de Equipos venía en `--sm` y quedaba 6 px por debajo de los tres
 * desplegables que tiene al lado. La talla pequeña es para densidad dentro de
 * una tabla o de una ficha, no para la barra que las encabeza. */
/* Y CON EL ALTO SE LES DEVUELVE EL RADIO, que es la mitad que se olvidaba: un
   `--sm` estirado a 36 px conserva el radio de 30 y sale más cuadrado que los
   controles de al lado. Es exactamente el defecto que la escala de radios viene
   a arreglar, reabierto por esta regla. */
.lean-toolbar .lean-input--sm,
.lean-toolbar .lean-boton--sm { min-height: var(--ctl-h); border-radius: var(--r-ctl); }
.lean-toolbar .lean-boton--icono.lean-boton--sm { width: var(--ctl-h); }
/* Aquí vivía `lean-filters`, «la fila de filtros aplicados, bajo la barra». Los
   filtros de esta aplicación van DENTRO de la barra (`lean-toolbar`) y los de
   varios valores son `lean-multi`; una segunda fila debajo nunca hizo falta. */

/* La barra como cabecera de una tarjeta con tabla: se pega al borde superior,
 * así que necesita su propio margen y la línea que la separa de las filas. El
 * margen lateral es el mismo que el de `lean-table--holgada` para que el título
 * caiga a plomo sobre la primera columna. */
.lean-toolbar--cabecera { padding: var(--s4) var(--s5); border-bottom: 1px solid var(--line); }
/* La otra mitad del ajuste de `lean-table--holgada`: las dos sangran hasta el
   borde de la misma tarjeta y tienen que medir lo mismo. */
@media (max-width: 560px) { .lean-toolbar--cabecera { padding: var(--s3) var(--s4); } }

/* ---------- Barra de acciones tipo píldora ----------
 * Grupo compacto y flotante de acciones (iconos), separadas por divisores.
 * Para actuar rápido sobre un elemento seleccionado. */
.lean-pillbar { display: inline-flex; align-items: center; gap: 2px; min-height: var(--ctl-h); padding: 4px; background: var(--alzado); border: 1px solid var(--linde-alto); border-radius: var(--r-pill); box-shadow: var(--shadow-lg); }
/* 5 px y no 6: la píldora comparte fila con botones —en el pie del cajón de la
   bitácora va entre «Copiar enlace» y «Cerrar»— y con 6 sumaba 38 contra los 36
   de sus vecinos. Los cuatro píxeles de la caja más los 26 del botón dan la
   talla md justa. */
.lean-pillbar button { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border: none; background: transparent; color: var(--muted); font: inherit; font-size: var(--fs-md); font-weight: var(--peso-medio); border-radius: var(--r-pill); cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }
.lean-pillbar button:hover { background: var(--al-pasar); color: var(--ink); }
.lean-pillbar button.danger:hover { background: color-mix(in srgb, var(--down) 12%, transparent); color: var(--down); }
/* La acción que ahora no lleva a ningún sitio se apaga en vez de desaparecer: en
   una barra de dos flechas, quitar una mueve la otra de sitio, y lo que hay que
   decir no es «esta acción ya no existe» sino «por aquí se ha acabado». */
.lean-pillbar button:disabled { opacity: .35; cursor: not-allowed; }
.lean-pillbar button:disabled:hover { background: transparent; color: var(--muted); }
.lean-pillbar .sep { width: 1px; align-self: stretch; margin: 4px 2px; background: var(--line); }
.lean-pillbar .lean-ico { width: var(--ico-md); height: var(--ico-md); }

/* ============================================================
 * Encabezados (lean-head) — familia de cabeceras
 * ------------------------------------------------------------
 * Un encabezado = antetítulo (opcional) + título + descripción
 * (opcional) + slot de acciones a la derecha (opcional). La
 * IDENTIDAD la lleva un acento de color —una barra o una banda con
 * degradado, o el antetítulo—, NUNCA un "chip de icono" al lado del
 * título. Los stops del degradado salen de --ha1/--ha2 (marca por
 * defecto); los modificadores de estado solo los reasignan.
 * ============================================================ */

/* Aquí vivía `lean-antetitulo`, la línea que iba ENCIMA de un título: «Cuenta»
   sobre «Contraseña». Se va con la cabecera de página que lo sostenía (ver más
   abajo), porque no era un componente suelto: vivía dentro de
   `lean-head__text`, y sin cabecera de página no hay dónde ponerlo.

   Se llevó por delante una distinción que costó explicar —antetítulo contra
   rótulo, once píxeles y versalitas los dos, haciendo lo contrario—, y la mitad
   que queda es la que trabaja: `lean-rotulo` pertenece al BLOQUE que empieza
   debajo y por eso lleva margen. Ya no hay con qué confundirlo. */

/* Base común a todos los tipos. */
/* --ha1/--ha2 pintan el ACENTO (la barra de 3 px): ahí el color de marca es la
   identidad y va a plena saturación. --ha-lavado tiñe la SUPERFICIE de la banda,
   que es otra cosa: cubre media cabecera, y a ese tamaño el cian de marca se lee
   como un fondo de color, no como un acento. Por defecto es el gris azulado que
   sale de tirar la marca hacia el gris de texto —queda el frío, se va el brillo—.
   Con estado sí manda el color: un aviso tiene que verse. */
.lean-head { --ha1: var(--brand); --ha2: var(--brand-ink); --ha-lavado: color-mix(in srgb, var(--brand) 22%, var(--muted));
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s4); }
.lean-head__text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.lean-head__title { margin: 0; font-family: var(--disp); font-weight: var(--peso-titulo); letter-spacing: var(--letra-titular); color: var(--ink); line-height: var(--alto-corto); font-size: var(--fs-xl); }
.lean-head__desc { margin: 0; font-size: var(--fs-md); color: var(--muted); line-height: var(--alto-ui); max-width: 60ch; }
.lean-head__actions { display: flex; align-items: center; gap: var(--s2); flex: none; }

/* Estado: recolorea el acento (barra o banda) sin tocar nada más. */
.lean-head.is-ok   { --ha1: var(--ok);   --ha2: color-mix(in srgb, var(--ok) 55%, var(--ink));   --ha-lavado: var(--ok); }
.lean-head.is-warn { --ha1: var(--warn); --ha2: color-mix(in srgb, var(--warn) 55%, var(--ink)); --ha-lavado: var(--warn); }
.lean-head.is-down { --ha1: var(--down); --ha2: color-mix(in srgb, var(--down) 55%, var(--ink)); --ha-lavado: var(--down); }

/* --- A) y B) LA CABECERA DE PÁGINA Y LA DE SECCIÓN SE RETIRARON, y es el mismo
 *     motivo por el que se fue la banda que se cuenta aquí abajo, sólo que
 *     tardó más en verse.
 *
 *     `--page` era el título grande de una vista, con su antetítulo encima y su
 *     línea debajo; `--section` era la cabecera de un bloque con barra de
 *     acento. Las dos dejaron de usarse cuando la MIGA pasó a decir dónde
 *     estás: con la miga arriba, el título de página repetía en 32 px lo que ya
 *     ponía en 13, y la barra de acento marcaba un bloque en una pantalla donde
 *     todos los bloques son iguales de importantes.
 *
 *     Lo que quedó vivo es la cabecera EN LÍNEA (`--inline`), que es la única
 *     que hace un trabajo que no hace nadie más: poner título, recuento y
 *     acciones en el borde de una caja. --- */
/* --- C) LA BANDA SE RETIRÓ, y merece la pena saber por qué: era una cabecera a
 *     sangre con lavado de degradado, «para la caja protagonista de una
 *     pantalla».
 *
 *     El problema no era cómo se veía, era lo que IMPLICABA. Al ser la cabecera
 *     de una caja y no de la pantalla, sólo cubría la PRIMERA: una sección que
 *     necesitaba dos cajas se quedaba sin título, y la que quería título metía
 *     todo en una y la partía por dentro con rótulos. Con las dos formas
 *     disponibles, cada pantalla elegía una — y en Ajustes convivían las dos.
 *
 *     La pantalla se titula ahora arriba, con la miga y el h1 de la vista, y
 *     todas las cajas de dentro pesan igual (ver `vistaCabecera` y `secc()` en
 *     assets/app/base.js). Lo «protagonista» dejó de ser un problema de
 *     cabecera. --- */

/* --- D) Lista: cabecera compacta de un panel de lista/tabla, con recuento y
 *     acción a la derecha y filete inferior. Título en fuente de interfaz. --- */
.lean-head--inline { align-items: center; padding-bottom: var(--s3); border-bottom: 1px solid var(--line); margin-bottom: var(--s4); }
.lean-head--inline .lean-head__title { font-family: var(--ui); font-size: var(--fs-lg); letter-spacing: var(--letra-normal); }
.lean-head__count { font-size: var(--fs-sm); font-weight: var(--peso-titulo); color: var(--muted); background: var(--panel-2); padding: 2px 9px; border-radius: var(--r-pill); font-variant-numeric: tabular-nums; }

/* LA NOTA: el resumen de lo que hay dentro cuando no cabe en un número.
 * «8 correctas · 7 sin evaluar» no es un recuento, es una frase, y metida en la
 * píldora de `__count` se lee como una etiqueta enorme. Va en texto tenue, que
 * es lo que ya hacía a mano el plegable de Estado del sistema. */
.lean-head__nota { flex: none; font-size: var(--fs-sm); color: var(--muted); }

/* Y TODO LO QUE NO SEA EL TÍTULO SE VA A LA DERECHA, JUNTO.
 *
 * `lean-head` reparte con `space-between`, que con DOS hijos deja uno en cada
 * extremo —lo que se quería— y con TRES deja el de en medio CENTRADO. En cuanto
 * una cabecera llevó título, recuento y flecha, el número apareció flotando en
 * mitad del renglón. El `auto` del primero que venga detrás del título los
 * empuja a todos a la derecha, y con dos hijos da exactamente lo mismo que
 * antes. */
.lean-head__count, .lean-head__nota { margin-left: auto; }
.lean-head__count + .lean-head__nota,
.lean-head__nota + .lean-head__count,
.lean-head__count ~ .lean-head__actions,
.lean-head__nota ~ .lean-head__actions { margin-left: 0; }

/* Caja de una sección: una tarjeta que envuelve UN bloque (cabecera + cuerpo).
 * Convención: un box por cada h1/h2; las secciones hermanas se separan solas
 * con el gap de .lean-sections. overflow:hidden para que la banda recorte. */
.lean-sections { display: flex; flex-direction: column; gap: var(--s4); }
.lean-section { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow); overflow: hidden; }
.lean-section__body { padding: var(--s5); }

/* EL RITMO LO PONE EL CONTENEDOR, Y ÉSTE ES EL SITIO.
 *
 * Un componente no trae margen exterior: `lean-toolbar`, `lean-avisos` y
 * `lean-table` no llevan ninguno, y hacen bien —una pieza con margen propio se
 * descuadra en cuanto cambia de sitio, sin que nadie la toque—. Lo que faltaba
 * era el otro lado del trato: que quien los mete diga cuánto aire va entre
 * ellos. Sin eso, el resultado es lo que se veía en Marcas y modelos: el bloque
 * de avisos, la barra de búsqueda y la tabla, los tres pegados sin un píxel.
 *
 * Se resolvía pantalla por pantalla, con un `margin-bottom` en el aviso aquí y
 * un `style="margin-top"` escrito a mano allá, y por eso cada sección tenía su
 * propio aire — o ninguno.
 *
 * `> * + *` y no `gap`: así el margen COLAPSA con el que ya traiga el elemento
 * de al lado en vez de sumarse, que es lo que impide que esto duplique
 * separaciones en los cuerpos que ya tenían las suyas. Y no toca al primero: ese
 * aire lo pone el relleno del cuerpo.
 *
 * `lean-pila` es lo mismo para un envoltorio de dentro —una zona que se repinta
 * entera, como el cuerpo de una solapa—: sin ella, el ritmo se para en el primer
 * `<div>` que alguien meta por medio. */
.lean-section__body > * + *,
.lean-pila > * + * { margin-top: var(--s4); }

/* LA CABECERA DE UN BLOQUE VA FUERA DEL CUERPO, como hijo directo de la caja.
 *
 * `--band` ya iba así y `--inline` iba DENTRO de `__body`: dos anatomías para la
 * cabecera de la misma caja, y por eso no se podían intercambiar. Una sección
 * que necesitaba dos cajas perdía el título —la banda sólo cubre la primera— y
 * la que quería título metía todo en una y la partía por dentro con rótulos. De
 * ahí salían las dos formas que convivían en Ajustes.
 *
 * Como hijo directo se comporta igual que la banda —a sangre, con su filete—
 * pero sin el lavado de color, que es lo que hacía protagonista a una caja. Aquí
 * ninguna lo es: son hermanas. */
.lean-section > .lean-head--inline,
.lean-section > details > .lean-head--inline { padding: var(--s4) var(--s5); margin-bottom: 0; }

/* Y MIDE SIEMPRE LO MISMO, LLEVE LO QUE LLEVE.
 *
 * El alto lo marcaba lo más grande que hubiera dentro, así que cinco bloques
 * seguidos daban cinco alturas: 49 px con sólo título, 51 con una nota, 55 con
 * una píldora y 63 con un botón. Medido en Estado del sistema, uno debajo de
 * otro. No se ve como «este bloque es más alto», se ve como que la pantalla está
 * mal alineada.
 *
 * La medida sale de `--ctl-h` y no de un número: es la altura de un control, que
 * es lo más alto que puede vivir en una cabecera —un botón, un interruptor—, más
 * el relleno. Así el que lleva botón no crece y el que sólo lleva título no
 * encoge. Es la regla 5: toda altura sale de `--ctl-h`. */
.lean-section > .lean-head--inline,
.lean-section > details > .lean-head--inline,
.lean-section > .lean-tabs { min-height: calc(var(--ctl-h) + var(--s4) * 2); }

/* Y LAS SOLAPAS TAMBIÉN VAN FUERA DEL CUERPO, por lo mismo: cuando una caja se
   navega por pestañas, las pestañas SON su cabecera y ocupan su sitio.
   Necesitan el relleno lateral de la cabecera, que el cuerpo ya no les da.

   Sin esto, dos secciones con la misma forma no se veían igual: las solapas de
   «Marcas y modelos» iban aquí, pegadas al techo de la caja, y las de «Usuarios
   y permisos» dentro del cuerpo, 24 px más abajo. Con la banda puesta no se
   notaba, porque encima de las dos había una cabecera que igualaba el arranque. */
.lean-section > .lean-tabs { padding: 0 var(--s5); align-items: flex-end; }

/* EL PIE: lo que CONFIRMA lo que hay en el cuerpo.
 *
 * `lean-tarjeta` tenía el suyo desde el principio y `lean-section` no, así que
 * sus botones colgaban del final del cuerpo con un margen escrito a mano en cada
 * pantalla. El reparto, que es lo que hay que respetar al añadir un mando: en la
 * CABECERA lo que gobierna el bloque —un interruptor, una acción sobre la lista
 * entera—; en el PIE lo que confirma lo que has escrito dentro.
 *
 * Va como HERMANO del cuerpo y no dentro, al revés que `lean-tarjeta__foot`:
 * ése sale a sangre con márgenes negativos que valen exactamente el relleno de
 * su tarjeta, y esa pareja hay que mantenerla a mano en cada media query —ya se
 * descuadró una vez—. Aquí la caja ya recorta y el cuerpo tiene su relleno.
 *
 * Un `lean-help` dentro se va al otro extremo: es la nota de lo que el botón va
 * a hacer, y se lee antes de pulsarlo. */
.lean-section__pie { display: flex; align-items: center; justify-content: flex-end; gap: var(--s2);
  padding: var(--s3) var(--s5); border-top: 1px solid var(--line); background: var(--panel-2); }
.lean-section__pie .lean-help { margin-right: auto; }

/* UN BLOQUE SE PUEDE PLEGAR, y sigue siendo el mismo bloque: lo que cambia es
 * que su cabecera es el `<summary>`. Ni un componente aparte ni una caja
 * distinta — plegar es un estado, no otra pieza.
 *
 * Hacía falta porque ya se había resuelto a mano una vez: «Verificado», en
 * Estado del sistema, era un `lean-accordion` al que había que subirle el
 * relleno a `--s5` para que su título cayera en la misma columna que el de los
 * bloques de al lado. El comentario que lo hacía lo decía con todas las letras
 * —«este plegable es un bloque de PRIMER NIVEL de la pantalla»—, que es la señal
 * de que la pieza no era de esa pantalla.
 *
 * QUÉ SE PLIEGA: lo que se CONSULTA y no se toca — la lista de lo que el sistema
 * vigila solo, la prueba de que las comprobaciones pasaron—. Lo que se opera va
 * abierto. Un bloque plegado dice «esto está aquí si lo necesitas» sin cobrar
 * media pantalla por decirlo. */
.lean-section > details > summary.lean-head--inline { list-style: none; cursor: pointer; }
.lean-section > details > summary.lean-head--inline::-webkit-details-marker { display: none; }
.lean-section > details > summary.lean-head--inline:hover { background: var(--al-pasar); }
/* Sin filete mientras está cerrado: no separa nada, y una caja de una sola línea
   con una raya al fondo parece una cabecera a la que le falta el cuerpo. */
.lean-section > details:not([open]) > summary.lean-head--inline { border-bottom-color: transparent; }
.lean-head__flecha { flex: none; color: var(--muted); transition: transform var(--t) var(--ease); }
.lean-section > details[open] > summary .lean-head__flecha { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .lean-head__flecha { transition: none; } }

@media (max-width: 560px) {
  .lean-head { flex-direction: column; align-items: stretch; }
  .lean-head--inline { align-items: stretch; }
  .lean-head__actions { align-self: flex-start; }
  /* La cabecera y el pie encogen CON la caja: los dos llevan el relleno lateral
     del cuerpo, y en cuanto uno cambia y el otro no, el bloque deja de tener una
     sola columna. Es la misma pareja que ya se descuadró en `lean-tarjeta`. */
  .lean-section > .lean-head--inline,
  .lean-section > details > .lean-head--inline { padding: var(--s3) var(--s4); }
  .lean-section > .lean-tabs { padding: 0 var(--s4); }
  .lean-section__pie { padding: var(--s3) var(--s4); }
}

/* ============================================================================
   1.4.0 — componentes para presentar el estado de una cosa
   ----------------------------------------------------------------------------
   Los anteriores resolvían formularios, tablas y navegación. Estos resuelven lo
   otro: enseñar de un vistazo cómo está algo y qué se puede hacer con ello.
   Hasta ahora cada pantalla lo inventaba por su cuenta, y por eso cada una lo
   hacía distinto.
   ========================================================================= */

/* --- lean-vitales: los signos vitales de una cosa ---------------------------
   Tira densa de cifras con estado. No es un lean-kpi: aquél es una tarjeta de
   tablero, y esto va bajo la cabecera de una ficha, donde el espacio vertical
   es el que hay. Responde «¿está bien?» sin leer una palabra.

   Sin cajas ni bordes: una rejilla de celdas con marco parece una hoja de
   cálculo y pesa más que lo que enseña. Lo que separa un dato del siguiente es
   el aire, y lo que los jerarquiza es el tamaño. */
/* Las columnas salen de una variable para que una pantalla pueda cambiarlas SIN
   escribir un selector doble contra el sistema. Hacía falta: `app.css` tenía dos
   parches (`lean-vitales.red-tarjeta__vitales`, `…emp-tarjeta__vitales`) y el
   comentario de uno de ellos cuenta que el primer intento no llegó a aplicarse
   nunca —una clase contra una clase, y `components.css` carga después—. Cuando
   la aplicación tiene que pelearse con el sistema por especificidad, al
   componente le falta una variante. */
.lean-vitales { display: grid;
  grid-template-columns: var(--vitales-cols, repeat(auto-fit, minmax(84px, 1fr)));
  gap: var(--vitales-gap, var(--s4) var(--s5)); }
/* --tres: tres columnas fijas, para cuando la tira vive en una tarjeta que se
   compara con la de al lado. Con `auto-fit` reflowaba a dos-y-una y entonces dos
   tarjetas contiguas tenían sus cifras a distinta altura. */
.lean-vitales--tres { --vitales-cols: repeat(3, 1fr); }
/* --- `--linea` SE RETIRÓ, y el porqué es el mismo que el de `--band`: era la
 * respuesta a medias de un problema que otra pieza resuelve entero.
 *
 * Ponía la tira en UN RENGLÓN —cifra y rótulo uno al lado del otro— para poder
 * meterla dentro de una cabecera de página sin añadirle un tercer piso. Tuvo un
 * solo cliente en toda su vida, la cabecera de una red, y ahí no llegó a
 * funcionar: con tres cifras y un logotipo al lado, la fila envolvía igual y las
 * dos primeras quedaban en un renglón y la tercera en otro. Un componente que
 * existe para no añadir un piso y acaba añadiéndolo no resuelve nada.
 *
 * Lo que hacía falta no era APILAR MENOS: era sacar las cifras de la columna del
 * título y ponerlas en su propia zona, con su ancho y su filete. Eso es
 * `lean-membrete`, y dentro van `lean-vital` normales.
 *
 * La tira en renglón sigue existiendo donde de verdad se necesita —dentro de una
 * ficha, con `lean-facts`— así que no queda ningún hueco. */
.lean-vital { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
/* El cuerpo de la cifra sale de una variable por lo mismo que las columnas: una
   portada la quiere grande —el CHANGELOG ya decía «lean-vital con --fs-4xl»— y
   sin variable eso obliga a la pantalla a pelearse con el sistema a base de
   selectores dobles. La escala sigue mandando: lo que se pasa es un peldaño. */
.lean-vital__n { font-family: var(--disp); font-size: var(--vital-n, var(--fs-2xl)); font-weight: var(--peso-titulo);
  line-height: var(--alto-corto); letter-spacing: var(--letra-ajuste); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* --portada: la tira de cifras que preside una vista. Cifra grande, algo de
   aire y CADA CIFRA EN SU TARJETA (el marco lo pone la regla que comparte con
   --pulsable, más abajo); sigue siendo el mismo componente, con los mismos
   estados. */
.lean-vitales--portada { --vital-n: var(--fs-4xl); --vitales-cols: repeat(auto-fit, minmax(150px, 1fr)); }
/* __u: LA UNIDAD, pegada a la cifra y en el cuerpo de lo que acompaña.
 *
 * Existe porque sin ella una magnitud con unidad no cabe en su celda, y lo que
 * se hacía en su lugar era meterla dentro de la cifra —«hace 43 min» entero a
 * `--fs-2xl`—: ochenta y tres píxeles de texto en una columna de ochenta y
 * cuatro, o sea recortado con puntos suspensivos justo en el dato. Y peor que
 * eso: `lean-vital__n` promete un NÚMERO, que es lo que hace que una tira se
 * barra con la vista sin leerla, y una frase ahí dentro rompe esa promesa para
 * las demás cifras de al lado.
 *
 * La unidad se lee con la cifra y no debajo, así que va en la misma línea y con
 * el cuerpo del rótulo: pesa lo que tiene que pesar —«43 min» es un dato, no
 * dos— y no compite con el número, que es lo que se compara. */
.lean-vital__u { font-size: var(--fs-sm); font-weight: var(--peso-medio); color: var(--muted);
  margin-left: 3px; letter-spacing: var(--letra-normal); }
/* Etiqueta en minúsculas y callada. Las versalitas diminutas encima de cada
   cifra convierten seis datos en seis títulos, y deja de leerse ninguno. */
.lean-vital__k { font-size: var(--fs-xs); color: var(--muted); }
.lean-vital__sub { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
/* Variante con marco, para cuando la tira va suelta sobre el fondo y necesita
   delimitarse (un tablero, no una ficha). */
.lean-vitales--caja { gap: 1px; background: var(--line); border: 1px solid var(--line);
  border-radius: var(--r-s); overflow: hidden; }
.lean-vitales--caja .lean-vital { padding: var(--s3); background: var(--panel); }
/* El color sólo aparece cuando hay algo que mirar: si todo va bien la tira es
   neutra, y así lo que destaca destaca de verdad. */
/* --- lean-vital--destacado: el que preside la tira -------------------------
   De cuatro cifras con su rótulo, una suele ser la que se viene a mirar y las
   otras tres el contexto que la explica. Al mismo tamaño las cuatro dicen que
   valen lo mismo, y entonces la tira no tiene entrada: el ojo empieza por la
   izquierda, que es donde suele estar el total.

   ES SÓLO TAMAÑO, y a propósito. Se probó con fondo y con borde de su tono y las
   dos cosas chocan con lo que esos dos canales YA significan aquí: el borde de
   marca y el fondo `--elegido` dicen «este filtro está puesto» (`is-activo`), y
   un destacado permanente pintado igual haría que un vital pareciera pulsado sin
   estarlo. La jerarquía por talla no se pisa con ningún estado.

   Y NO LLEVA COLOR PROPIO: el tono sigue saliendo de `is-ok` / `is-warn` /
   `is-down`, que es lo que significa la cifra. Destacar es decir cuál se mira
   primero, no de qué color va. */
.lean-vital--destacado { --vital-n: var(--fs-5xl); }
/* El rótulo del que preside sube a la voz normal: la cifra ha crecido seis
   píxeles y con el rótulo en `--fs-xs` apagado quedaba una cabeza grande sobre
   un pie que ya no se leía como suyo. */
.lean-vital--destacado .lean-vital__k { font-size: var(--fs-sm); color: var(--ink-2, var(--muted)); }

.lean-vital.is-warn .lean-vital__n { color: var(--warn); }
.lean-vital.is-down .lean-vital__n { color: var(--down); }
.lean-vital.is-ok   .lean-vital__n { color: var(--ok); }
.lean-vital.is-vacio .lean-vital__n { color: var(--muted); font-weight: var(--peso-titulo); }

/* --- --pulsable: la cifra que ADEMÁS es un filtro ---------------------------
   En Equipos, las tres cifras de arriba filtran la lista al pulsarlas, y hasta
   ahora se veían exactamente igual que las de un tablero, que no hacen nada. La
   única pista era un realce al pasar el ratón, que en una tableta no existe: en
   la prueba de uso nadie las pulsó y se fueron al desplegable teniendo el filtro
   delante.

   Así que lo pulsable se dice con la forma —marco propio y superficie que se
   levanta— y lo elegido se queda marcado sin depender del ratón.

   EL MARCO LO COMPARTE CON `--portada`, y eso es lo que arregla una tira donde
   unas cifras filtran y otras no. En Red son cinco: dos dicen el total —que es
   el estado sin filtro, así que no llevan a ninguna parte— y tres son los
   valores del desplegable de estado. Con el marco puesto sólo en las pulsables,
   la misma tira enseñaba dos diseños a la vez: dos celdas planas de una rejilla
   y tres tarjetas con esquina redondeada, cada grupo con su sangrado. Lo que
   dice «esto se pulsa» pasa a ser lo que de verdad lo dice —el cursor, el
   realce al apuntar y el borde de marca de la elegida—, y la FORMA vuelve a ser
   la misma para las cinco, que es lo que se lee de un vistazo. */
.lean-vitales--portada .lean-vital,
.lean-vital--pulsable {
  border: 1px solid var(--line); border-radius: var(--r-s);
  padding: var(--s3) var(--s4); background: var(--panel);
}
.lean-vital--pulsable {
  text-align: left; font: inherit; color: inherit; cursor: pointer;
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
/* Y SIRVE IGUAL SOBRE UN `<a>`, que es la cifra que en vez de filtrar la lista
   de debajo LLEVA a otra pantalla: los monitores de un cliente, contados en la
   ficha de la empresa, viven en otra sección (ver `vitales` en lista.js).

   Hacen falta las tres declaraciones y ninguna sobra: `text-decoration:none`
   quita el subrayado que trae un enlace de fábrica —sin ella la cifra sale
   subrayada dentro de su propio marco, que es un enlace disfrazado de tarjeta—,
   y `display:flex` con su dirección repite lo de `.lean-vital` porque un `<a>`
   es `inline` por defecto y la especificidad de la clase base no lo cambia: sin
   esto la cifra y su rótulo se ponen en la misma línea y la tira entera se
   descuadra. Un `<div>` y un `<button>` ya venían con `display` de bloque, y por
   eso el componente ha vivido hasta hoy sin decirlo. */
a.lean-vital--pulsable {
  display: flex; flex-direction: column; text-decoration: none;
}
.lean-vital--pulsable:hover { border-color: color-mix(in srgb, var(--brand) 40%, var(--line)); }
.lean-vital--pulsable:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
.lean-vital--pulsable.is-activo { border-color: var(--brand); background: var(--elegido); }
.lean-vital--pulsable.is-activo .lean-vital__k { color: var(--brand-ink); font-weight: var(--peso-titulo); }

/* --- __barra: la cifra que además es una PROPORCIÓN -------------------------
   CPU, memoria, disco: un 81 % no se entiende igual que «81 equipos», porque
   lleva dentro un techo. La barra dice de cuánto, y por eso va pegada abajo
   (`margin-top: auto`): con varias en fila, todas las barras quedan a la misma
   altura aunque un subtítulo ocupe dos renglones y otro uno.

   Aquí vivía `lean-meter`, que era este mismo componente con dos píxeles más de
   cifra y su propia rejilla. Al pasar los tamaños a la escala, las dos cifras
   convergieron solas en `--fs-2xl`, y entonces lo único que quedaba distinto era
   la barra. Un componente entero por una barra opcional no se sostiene.
   El color de la barra sigue al estado de la cifra, que es lo que ya hacía. */
/* `display: block` Y NO SE PUEDE QUITAR, aunque parezca que sobra.
 *
 * Los cuatro sitios que montan esta barra la escriben con un `<span>`, y un
 * `<span>` es `inline` mientras nadie diga otra cosa. Una caja inline IGNORA
 * `height`, así que los 3 px de aquí abajo no se aplicaban, `overflow: hidden`
 * tampoco recortaba nada y el `<span>` de dentro —que sí es `block` y pide
 * `height: 100%`— se estiraba a la altura de la línea del contenedor: 76 px de
 * `--brand` macizo en vez de una barra de tres píxeles.
 *
 * Y NO SE VEÍA SIEMPRE, que es lo que lo mantuvo vivo. En las listas —Equipos,
 * Empresas— el signo vital es un contenedor flex, y un flex item se blockifica
 * solo: ahí la barra salía perfecta. En la portada el contenedor es un bloque
 * normal, así que sólo ahí se veía la mancha. En el tema negro además salta a
 * la cara, porque allí `--brand` es #E6E9EC: un rectángulo blanco en mitad del
 * panel.
 *
 * Es la trampa que esta hoja ya tiene escrita en otro sitio con `lean-rowactions`
 * y su `tr:hover`: un componente que depende del `display` de quien lo contiene
 * sin decirlo promete más de lo que puede cumplir. Quien lo puso en la portada
 * no hizo nada raro.
 *
 * `margin-top: auto` sí es sólo para el caso flex —empuja la barra al pie del
 * vital— y en bloque no hace nada, que es lo correcto. */
.lean-vital__barra { display: block; height: 3px; border-radius: var(--r-micro);
  background: var(--panel-2); overflow: hidden; margin-top: auto; }
.lean-vital__barra > span { display: block; height: 100%; background: var(--brand);
  border-radius: inherit; transition: width var(--t) var(--ease); }
.lean-vital.is-warn .lean-vital__barra > span { background: var(--warn); }
.lean-vital.is-down .lean-vital__barra > span { background: var(--down); }
.lean-vital.is-ok   .lean-vital__barra > span { background: var(--ok); }

/* --- lean-rotulo: el nombre de un bloque dentro de una superficie ----------
   El renglón fino en versalitas que separa dos bloques dentro de una ficha, un
   cajón o una ventana de trabajo: OFICINAS, MEDICIONES, TRATAMIENTO DE DATOS.

   No es un encabezado de sección —ésos son de página, y dentro de un diálogo no
   van—. Es lo contrario: dice dónde empieza otro asunto sin abrir una jerarquía
   nueva, y por eso va callado, en el gris más flojo y sin tamaño de titular.
   Un `<h3>` aquí compite con el nombre de lo que se está mirando, que es lo
   único que debería leerse primero.

   Vivía en `app.css` como `.consola-rotulo`, y ya lo usaba la ficha de una
   empresa: una clase con el nombre de la pantalla donde nació, puesta en otras
   dos. El margen de arriba es mayor que el de abajo a propósito —el rótulo
   pertenece a lo que viene DEBAJO, no a lo que deja atrás—. */
/* --pegado: el mismo rótulo cuando el espacio de arriba ya lo da su contenedor
   —la cabecera de una tarjeta, una rejilla con `gap`—. Sin él, el margen se
   suma al del padre y el bloque queda descolgado.
   Venía de `app.css` como `.section-title`, que era el TERCER nombre de esta
   misma cosa: once píxeles, versalitas, gris y peso 600, exactamente igual, y
   distinguiéndose sólo en que no llevaba margen. Cuando la diferencia entre dos
   componentes es un margen, no son dos componentes: es un modificador.
   Se usa en `<h2>` o `<h3>` cuando lo que rotula ES un encabezado del documento
   —una sección de una página— y en `<p>` cuando sólo separa dos bloques dentro
   de una superficie, que es el caso del rótulo a secas. */
.lean-rotulo { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--letra-rotulo);
  color: var(--muted); font-weight: var(--peso-titulo); margin: var(--s4) 0 var(--s2); }
/* El modificador va DETRÁS de la base, y no es cosmética del archivo: las dos
   reglas tienen una sola clase, así que empatan en especificidad y decide el
   orden. Escrito antes —como estaba— `--pegado` no quitaba ningún margen y los
   rótulos seguían con sus 16 px, sin dar el menor error. */
.lean-rotulo--pegado { margin: 0; }
/* --- lean-enlace: un enlace DENTRO DE UN TEXTO -----------------------------
   El caso más corriente que había en el panel y el único que no tenía
   tratamiento: «queda en Registros», «eso se marca en Red». No existía regla
   base para `<a>`, así que un enlace en prosa salía con el azul del navegador
   —doce de ellos, medidos, repartidos por Equipos, Empresas, Red, la portada y
   Ajustes—; y quien se acordaba de vestirlo copiaba `acerca-commit`, una clase
   que nació para el hash del commit en «Acerca de» y acabó sosteniendo siete
   enlaces que no tienen nada que ver con eso.

   VA SUBRAYADO DE SERIE, y esto es lo que hay que respetar al tocarlo. Un
   enlace que sólo se distingue por el color exige 3:1 contra la tinta que lo
   rodea, y aquí no se llega ni de lejos: el cian contra `--ink` da 1,63 en
   oscuro, y dentro de un `lean-help` —que es donde vive la mitad de ellos—
   contra `--muted` da 1,13 en claro y 1,39 en oscuro. O sea que los que ya
   tenían color tampoco se veían: eran una palabra de otro tono dentro de una
   frase gris. El subrayado no es adorno, es lo que los hace enlaces.

   Lo que cambia al pasar por encima es la INTENSIDAD del subrayado, no su
   presencia: de serie va al 40 % para no rayar un párrafo entero, y al pasar se
   cierra del todo. Así hay respuesta al ratón sin que la identificación dependa
   de tenerlo encima.

   El color sale de `--enlace-ink` y no de `--brand-ink`: un enlace es texto y
   le toca el 4,5:1 de texto, que `--brand-ink` no da en claro (4,09). Ver
   tokens.css. */
/* Y VALE TAMBIÉN EN UN `<button>`, que es lo que hay que usar cuando el enlace
   NO NAVEGA: abre un diálogo, despliega algo, cambia lo que se ve. Un `<a>` sin
   href no lo alcanza el teclado y el lector de pantalla no lo anuncia como nada;
   un `<button>` sí, y por eso es el elemento correcto ahí.
   Hasta 1.86.0 esta clase sólo servía en un `<a>`: puesta en un botón salía con
   el fondo, el borde, el relleno y la letra del control nativo —un recuadro gris
   en medio de una frase— porque no reseteaba ninguno de los cuatro. Ninguno de
   los reset cambia nada en un `<a>`, que no trae ninguna de esas cuatro cosas. */
.lean-enlace {
  appearance: none; background: none; border: 0; padding: 0;
  font: inherit; text-align: inherit; cursor: pointer;
  color: var(--enlace-ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  text-underline-offset: 2px;
  transition: text-decoration-color var(--t-fast) var(--ease);
}
.lean-enlace:hover { text-decoration-color: currentColor; }
/* El foco de teclado no puede ser el subrayado —ya lo lleva puesto—, así que es
   un cerco, y el cerco lo pone ya el reset de `tokens.css`. Aquí queda sólo el
   radio: un enlace en línea no tiene esquina propia que el anillo pueda seguir.

   Decía «`--brand` y no `--enlace-ink`, que a un cerco le basta con 3:1 y ahí el
   cian vivo va mejor que el apagado», y el argumento estaba del revés: el cian
   vivo da 2,51 sobre el papel claro y NO llega a ese 3:1. Es el apagado el que
   cumple, y es de donde sale `--foco`. */
.lean-enlace:focus-visible {
  border-radius: var(--r-micro);
}

/* ---------- lean-ir: un DATO que además lleva a su sitio -------------------
 *
 * EL NIVEL QUE FALTABA, y sale de censar los 136 enlaces visibles del panel: hay
 * TRES cosas que se llaman enlace y sólo dos tenían forma.
 *
 *   1. En PROSA (`lean-enlace`) — dentro de un párrafo, un aviso, una ayuda. Ahí
 *      el ojo no sabe dónde acaba una palabra normal y empieza una pulsable, así
 *      que hace falta color y subrayado permanente.
 *   2. Un DATO que además navega (esto) — el modelo de un ordenador, su pedido,
 *      la empresa de una fila. No es prosa: está en su casilla y el ojo ya sabe
 *      que es un dato. Pero tampoco es toda la fila, así que sin marca NADIE
 *      DESCUBRE QUE SE PUEDE PULSAR.
 *   3. El OBJETO ENTERO — una fila, una tarjeta, una cifra de portada. Lo
 *      pulsable es la superficie, no unas palabras dentro, así que no lleva
 *      marca: lo dicen el cursor y el velo del puntero.
 *
 * LA ESCALA NO ES DE IMPORTANCIA, ES DE AMBIGÜEDAD: un enlace se marca en la
 * medida en que, sin la marca, no se sabría que lo es. Con eso los tres se
 * reparten solos y deja de decidirse pantalla por pantalla — que es lo que había,
 * y por eso salieron SIETE pintas distintas para 136 enlaces.
 *
 * EL PUNTEADO ES LA MITAD DE LA IDEA. Un subrayado continuo aquí gritaría: en una
 * ficha con doce datos, tres de ellos subrayados a plena línea se leen como tres
 * avisos. El punteado está presente en reposo —que es lo que hace que se
 * descubra— y se enciende al apuntar, que es cuando hace falta que sea explícito.
 * Y NO cambia de `dotted` a `solid` al pasar por encima aunque quede mejor
 * quieto: `text-decoration-style` no se puede animar, así que ese cambio salta;
 * lo que viaja es el color del subrayado y la tinta, que sí son animables.
 *
 * LA TINTA ES LA DEL DATO, no la de marca. Un valor que se lee —un modelo, un
 * número de albarán— sigue siendo el dato de esa fila; teñirlo de cian de entrada
 * lo convertiría en otra cosa. El cian llega al apuntarlo, y entonces sí. */
/* VALE TAMBIÉN EN UN `<button>`, desde 1.148.0 y por lo mismo que `lean-enlace`
   en 1.86.0: un dato que ABRE algo en vez de navegar —la versión de Lean, que
   abre su ficha— es un botón, y sin estos reset salía con el recuadro gris del
   control nativo en mitad de una fila de datos. En un `<a>` no cambian nada. */
.lean-ir {
  appearance: none; background: none; border: 0; padding: 0;
  font: inherit; text-align: inherit;
  color: inherit; text-decoration: underline;
  text-decoration-style: dotted; text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, currentColor 34%, transparent);
  text-underline-offset: 3px; cursor: pointer;
  transition: text-decoration-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lean-ir:hover {
  color: var(--enlace-ink);
  text-decoration-color: color-mix(in srgb, currentColor 60%, transparent);
}
.lean-ir:focus-visible {
  border-radius: var(--r-micro);
}


/* --- lean-aviso: un aviso ------------------------------------------------
   Sustituye a las tres implementaciones que había (alerta de ficha, aviso de
   asignación, aviso de dispositivo). Apilado cuando hay uno; compacto cuando
   hay cuatro y no pueden llevarse la pantalla por delante.

   EL COLOR VA EN EL ICONO Y EN NINGÚN OTRO SITIO. La superficie es la del
   sistema —`--panel-2` sobre `--line`—, la misma que usa cualquier caja, y no
   se tiñe nunca.

   Antes sí se teñía: el fondo era `color-mix(var(--warn) 9%, transparent)` y el
   borde `color-mix(var(--warn) 35%, var(--line))`. Medido, eso daba:

     · en oscuro, un ámbar al 9 % sobre un panel frío se neutraliza. El fondo del
       aviso de atención salía #272A27 y el de avería #29262A: CROMA 3 SOBRE 255,
       o sea el mismo gris sucio para los dos. El color no informaba de nada y
       encima caía entre las dos superficies del sistema (#151F24 y #1C282E), sin
       pertenecer a ninguna;
     · en claro, `--warn` es #AD6512, que está elegido para ser TINTA legible
       (4,52:1 sobre blanco). Al 9 % da un beige, y el borde teñido un caqui
       (#CCBA9F) que además no se ve: 1,90:1 contra el panel.

   La causa de fondo es que había UN valor por estado y se le pedían tres
   trabajos —fondo, borde y tinta—. Se arregla quitándole dos, no añadiendo
   tokens: la superficie deja de ser suya y el color se queda donde siempre fue
   legible. De paso, el aviso pasa a hablar como el toast, que ya era así.

   El icono ES el que distingue un tipo de otro, no sólo su color: triángulo para
   lo que pide atención, círculo tachado para lo que no funciona, círculo con
   check para lo comprobado, «i» para el contexto. El color a secas nunca informa
   (ver la guía). */
.lean-aviso { display: flex; align-items: flex-start; gap: var(--s3);
  padding: var(--s3) var(--s4); border-radius: var(--r-s);
  border: 1px solid var(--line); background: var(--panel-2); font-size: var(--fs-md); }
.lean-aviso .lean-ico { width: var(--ico-md); height: var(--ico-md); flex: none; margin-top: 1px;
  color: var(--brand-ink); }
.lean-aviso__txt { min-width: 0; flex: 1; }
/* Mismo escalón que `lean-notificacion__title` y ahora escrito igual: las dos
   son una caja pequeña dentro del flujo, no una pantalla. Pesaba 650 contra los
   600 de la otra y heredaba el tamaño en vez de declararlo. */
.lean-aviso__title { font-size: var(--fs-md); font-weight: var(--peso-titulo); }
.lean-aviso__desc { color: var(--muted); font-size: var(--fs-sm); }

/* CUATRO TIPOS Y NI UNO MÁS, y el que no dice nada es `info`, exactamente como
   en el toast (`opciones.variant || "info"`). Antes eran tres modificadores y un
   cuarto estado sin nombre —el aviso «sin tono»—, que es como se acaba con dos
   maneras de decir lo mismo: los tres avisos de contexto que hay en el panel ya
   escribían `ico("info")` a mano.

   Van DESPUÉS de la regla de arriba porque empatan en especificidad —dos
   clases— y decide el orden. */
.lean-aviso--info .lean-ico { color: var(--brand-ink); }
/* Los CUATRO tonos, aunque hoy ninguna pantalla tenga un aviso de "todo bien":
   son los mismos cuatro del toast y del badge, y la simetría es lo que hace que
   la misma noticia no se llame distinta según dónde salga. Un modificador que
   completa una escala declarada no es peso muerto; lo es el que está solo. */
.lean-aviso--ok   .lean-ico { color: var(--ok); }
.lean-aviso--warn .lean-ico { color: var(--warn); }
.lean-aviso--down .lean-ico { color: var(--down); }

/* Aquí vivían nueve reglas que teñían los botones DE DENTRO de un aviso con el
   tono del aviso: el principal relleno de ámbar, el secundario en contorno. La
   razón que las justificaba era que «un aviso es una superficie de color, y un
   botón con sus colores de siempre encima parece pegado de otra pantalla».
   Dejó de ser verdad el día que el aviso dejó de ser una superficie de color:
   sobre `--panel-2`, el botón del sistema es exactamente lo que se espera, y un
   botón ámbar relleno pesa más que el aviso que lo contiene. */

/* Varios avisos: una caja con una fila por aviso, en vez de una pila de
   banners. */
.lean-avisos { display: flex; flex-direction: column; gap: var(--s2); }
.lean-avisos--compacto { gap: 0; border: 1px solid var(--line);
  border-radius: var(--r-s); overflow: hidden; }
.lean-avisos--compacto .lean-aviso { border: 0; border-radius: 0;
  border-bottom: 1px solid var(--line); padding: 7px var(--s3); font-size: var(--fs-sm); }
.lean-avisos--compacto .lean-aviso:last-child { border-bottom: 0; }

/* --- Y CÓMO LLEGA, CÓMO SE VA Y CÓMO SE DECIDE --------------------------
   Un `lean-aviso` no tenía una sola línea de movimiento, y no todos la piden.
   Aparece por cuatro motivos distintos y sólo dos se cuentan:

     · DE CONTEXTO — «No consta de dónde salió», «Sin inscribir». Describe cómo
       ES la ficha; está desde que se pinta. No se anima: la ficha entera ya
       entra, y animar además cada aviso son dos gestos para una sola llegada.
       No ha llegado un aviso, has abierto una ficha.
     · QUE CAMBIA SOLO — una alerta nueva en un refresco, «No se ve en ningún
       ordenador» cuando se enciende el equipo donde está enchufado. Nadie lo ha
       pedido, así que se cuenta entero: `is-llega` y `is-se-va`.
     · QUE ES LA RESPUESTA — el diagnóstico de un dominio, que sustituye a un
       `lean-skeleton`. Ahí el hueco YA estaba hecho, que es para lo que está el
       esqueleto, así que no hay altura que animar: entran escalonados y nada
       más. Lo pone `Lean.componer`.
     · QUE SE DECIDE — el de abajo.

   `is-llega` y `is-se-va` se ponen desde la vista, no solas: un aviso que ya
   estaba cuando se abrió la ficha no debe animarse, y desde CSS no hay forma de
   distinguir «acabo de nacer» de «me acaban de pintar». */
.lean-aviso.is-llega {
  animation: lean-aviso-entra var(--t-entra) var(--ease-entra) var(--t-releva) backwards;
}
@keyframes lean-aviso-entra {
  from { opacity: 0; transform: translateY(var(--entra-y)); }
  to   { opacity: 1; transform: none; }
}
/* Se va antes de que su hueco se cierre —los 60 ms de `--t-releva` los pone
   quien lo quita—, porque lo de debajo no puede subir mientras él sigue ahí. */
.lean-aviso.is-se-va {
  animation: lean-aviso-sale var(--t-sale) var(--ease-sale) both;
}
@keyframes lean-aviso-sale {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-4px); }
}

/* EL AVISO Y LA DECISIÓN SON EL MISMO OBJETO.
   Ésta es la única de las cuatro que se resuelve sin salir de la ficha:
   «Dejar de avisar» no mata el aviso, lo CAMBIA DE ESTADO — «se sigue
   enseñando, y ésa es la mitad que hace que esto no sea un sitio donde se
   esconden problemas», dice `invYaDecididos`. La batería sigue gastada; lo que
   ha cambiado es que ya se decidió qué hacer con ella.

   Así que la caja no se destruye ni se sustituye: cambia de alto y de contenido,
   que es exactamente lo que hace `lean-mando` con el botón de escanear. El
   cruce de capas son sus mismos números, sin tocar uno.

   Y EL CAMBIO DE SIGNIFICADO VIAJA POR EL ICONO, porque no tiene otro sitio por
   donde ir: la superficie de un aviso no se tiñe nunca —el porqué está arriba,
   con la medición—, así que el color vive sólo ahí. El triángulo se traza
   convirtiéndose en la «i» mientras la tinta pasa de `--warn` a `--brand-ink`.
   Es `lean-trazar`, el de un botón que ha terminado.

   `pathLength="100"` EN EL SPRITE es lo que lo hace posible: un `stroke-dasharray`
   con número fijo sólo vale para el trazo que se midió, y aquí los iconos son
   cuatro —el círculo mide 62,8 y la barra del «i» mide 4—. Con un número a mano,
   el círculo se queda partido para siempre. */
.lean-aviso.is-decidiendo { position: relative; overflow: hidden;
  transition: height var(--t-asienta) var(--asienta); }
.lean-aviso.is-decidiendo .lean-aviso__txt,
.lean-aviso.is-decidiendo .lean-boton {
  animation: lean-aviso-capa-sale var(--t-sale) var(--ease-sale) both;
}
.lean-aviso.is-decidido .lean-aviso__txt,
.lean-aviso.is-decidido .lean-boton {
  animation: lean-aviso-capa-entra var(--t-entra) var(--ease-entra) backwards;
}
@keyframes lean-aviso-capa-sale {
  from { opacity: 1; filter: blur(0);     transform: none; }
  to   { opacity: 0; filter: blur(2.5px); transform: translateY(2px); }
}
@keyframes lean-aviso-capa-entra {
  from { opacity: 0; filter: blur(2.5px); transform: translateY(2px); }
  to   { opacity: 1; filter: blur(0);     transform: none; }
}
/* El icono cambia de tinta con la caja y se vuelve a dibujar con su forma
   nueva. Los puntos del «i» no llevan `pathLength` en el sprite: miden 0,01, así
   que el guion los deja fuera del patrón hasta el final — aparecen en vez de
   dibujarse, que es lo que hace un punto. */
.lean-aviso .lean-ico { transition: color var(--t) var(--ease); }
.lean-aviso.is-decidido .lean-ico use,
.lean-aviso.is-decidido .lean-ico path,
.lean-aviso.is-decidido .lean-ico circle,
.lean-aviso.is-decidido .lean-ico line {
  animation: lean-traza-100 var(--t) var(--ease-entra) backwards;
}

/* UN SOLO TRAZO PARA LOS ICONOS DE `pathLength="100"`, y el número va en el
   nombre porque NO es intercambiable.
   
   Esto estaba escrito dos veces, palabra por palabra: `lean-aviso-traza` aquí y
   `lean-notificacion-traza` ochocientas líneas más arriba, idénticos. Es el
   mismo gesto sobre el mismo tipo de icono, y la casa ya sabe lo que cuesta un
   @keyframes duplicado — «se quedaba atrás cada vez que se tocaba el original»,
   dice la nota de `.presente` en app.css sobre el suyo.

   LOS OTROS TRES NO SE JUNTAN, y por eso el nombre lleva el 100. `lean-ico-traza`
   traza sobre 80 y `lean-trazar` sobre el dasharray fijo 24 del check; un trazo
   sólo se completa si el número coincide con la longitud declarada del camino, y
   cambiarlo deja el icono a medio dibujar sin que falle nada. `lean-haciendo-traza`
   tampoco: no lleva `to`, porque su destino lo pone el propio elemento. */
@keyframes lean-traza-100 {
  from { stroke-dasharray: 100; stroke-dashoffset: 100; }
  to   { stroke-dasharray: 100; stroke-dashoffset: 0; }
}
/* EL ACUSE VA DONDE OCURRIÓ LA ACCIÓN, y aquí es un barrido hacia dentro y no
   el halo de `is-hecho`: aquél se sale 2 px del contorno y `--compacto` lleva
   `overflow: hidden`, así que dentro de la caja se recortaría contra el borde y
   sólo se vería la mitad de arriba. Mismo gesto, hacia dentro. */
.lean-aviso.is-acusando {
  animation: lean-aviso-acuse var(--t) var(--ease-entra) both;
}
@keyframes lean-aviso-acuse {
  from { background: color-mix(in srgb, var(--brand-fijo) 14%, var(--panel-2)); }
  to   { background: var(--panel-2); }
}

/* Quien pide menos movimiento ve el resultado y no el gesto: el aviso llega y se
   va sin desplazarse, el icono no se dibuja y la altura no viaja. */
@media (prefers-reduced-motion: reduce) {
  .lean-aviso.is-llega,
  .lean-aviso.is-se-va { animation-duration: 1ms; }
  .lean-aviso.is-decidiendo { transition: none; }
  .lean-aviso.is-decidiendo .lean-aviso__txt,
  .lean-aviso.is-decidiendo .lean-boton,
  .lean-aviso.is-decidido .lean-aviso__txt,
  .lean-aviso.is-decidido .lean-boton,
  .lean-aviso.is-decidido .lean-ico use,
  .lean-aviso.is-decidido .lean-ico path,
  .lean-aviso.is-decidido .lean-ico circle,
  .lean-aviso.is-decidido .lean-ico line,
  .lean-aviso.is-acusando { animation: none; }
}

/* --- lean-veredicto: la respuesta de una pantalla de diagnóstico -----------
   Una pantalla que examina algo —la instalación, un equipo, una integración—
   tiene UNA pregunta detrás: ¿esto está bien? Lo demás es la prueba de por qué.

   El error que corrige este componente es enterrar la respuesta en la misma
   letra pequeña que las catorce líneas que la sostienen. Quien entra aquí casi
   siempre sale con la respuesta y nada más; que le cueste catorce líneas
   encontrarla es lo que hace que la pantalla se lea como un muro.

   Va como primer hijo de una .lean-section (que recorta el lavado), en lugar de
   la banda de cabecera: aquí el titular NO es el nombre de la pantalla —ése ya
   está en el lateral— sino el veredicto. El estado se declara en la raíz
   (is-ok / is-warn / is-down) y de ahí baja a todo lo demás. */
.lean-veredicto { --va: var(--faint); display: flex; align-items: center; gap: var(--s4);
  flex-wrap: wrap; padding: var(--s5); background:
    radial-gradient(130% 170% at 100% 0%, color-mix(in srgb, var(--va) 14%, transparent), transparent 60%),
    linear-gradient(180deg, var(--panel-2), var(--panel)); }
.lean-veredicto.is-ok   { --va: var(--ok); }
.lean-veredicto.is-warn { --va: var(--warn); }
.lean-veredicto.is-down { --va: var(--down); }

/* El emblema es el único sitio de la pantalla donde el color de estado ocupa
   superficie. Con un icono de 16 px como el de un aviso, el veredicto pesaría
   lo mismo que cada una de las líneas que resume. */
.lean-veredicto__emblema { width: 46px; height: 46px; flex: none; border-radius: 50%;
  display: grid; place-items: center; color: var(--va);
  background: color-mix(in srgb, var(--va) 13%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--va) 28%, transparent); }
.lean-veredicto__emblema .lean-ico { width: var(--ico-lg); height: var(--ico-lg); }
.lean-veredicto__txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.lean-veredicto__titular { margin: 0; font-family: var(--disp); font-size: var(--fs-3xl); font-weight: var(--peso-titulo);
  letter-spacing: var(--letra-titular); line-height: var(--alto-corto); color: var(--ink); }
.lean-veredicto__ctx { margin: 0; font-size: var(--fs-sm); color: var(--muted); }
.lean-veredicto__acciones { display: flex; align-items: center; gap: var(--s2); flex: none; margin-left: auto; }

/* El recuento por estado, en el pie de la banda. Es lo que convierte el titular
   en algo comprobable: «1 problema» y, debajo, de cuántas comprobaciones sale.
   Sin él hay que contar a mano lo que hay más abajo. */
.lean-veredicto__pie { display: flex; flex-wrap: wrap; gap: var(--s4);
  padding: var(--s3) var(--s5); border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--panel-2) 55%, var(--panel)); }
.lean-veredicto__cuenta { display: inline-flex; align-items: baseline; gap: 6px;
  font-size: var(--fs-sm); color: var(--muted); }
.lean-veredicto__cuenta b { font-size: var(--fs-md); font-weight: var(--peso-titulo); color: var(--ink);
  font-variant-numeric: tabular-nums; }
.lean-veredicto__cuenta::before { content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--faint); align-self: center; flex: none; }
.lean-veredicto__cuenta.is-ok::before   { background: var(--ok); }
.lean-veredicto__cuenta.is-warn::before { background: var(--warn); }
.lean-veredicto__cuenta.is-down::before { background: var(--down); }
/* Un recuento en cero se apaga en vez de desaparecer: «0 problemas» es
   información, y si la fila cambiara de forma según lo que hay, no se podría
   comparar de un día para otro. */
.lean-veredicto__cuenta.is-cero { opacity: .45; }
.lean-veredicto__cuenta.is-cero::before { background: var(--faint); }
@media (max-width: 560px) {
  .lean-veredicto__acciones { margin-left: 0; width: 100%; }
}

/* --- lean-caso: qué pasa a la izquierda, qué se hace a la derecha ----------
   Para un problema con su remedio: una comprobación que falla y cómo se
   arregla, un fallo corregido y en qué quedó. Los dos textos son igual de
   necesarios y se leen en pareja, así que van enfrentados y etiquetados.

   Apilados —diagnóstico, párrafo, solución, párrafo— hay que leer los cuatro
   para saber si esto ya está resuelto. Enfrentados se ve en el primer golpe de
   vista, y el ancho de columna baja a la medida en que se lee de corrido.

   Se pliega a una columna en pantalla estrecha: dos columnas de 30 caracteres
   son peores que una de 60.

   NO ES UNA CAJA, SON LAS TRIPAS DE UNA. Nació con caja propia y cabecera
   —borde, título, etiqueta de gravedad y una línea gris de contexto—, y esa
   mitad no la estrenó nadie: donde el caso se usa de verdad (`parches.js`) la
   caja ya la pone el acordeón que lo contiene, y el título va en el renglón que
   se pliega. Con las dos cosas, cada corrección salía con dos bordes y el
   asunto escrito dos veces. Así que lo que queda es lo que trabaja: los dos
   lados y su pie, para meterlos donde haga falta.

   Por eso `__lados` lleva su propia línea arriba: es lo que lo separa de lo que
   lo abre, sea un `summary` o un rótulo. */
.lean-caso__lados { display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
  background: var(--line); border-top: 1px solid var(--line); }
.lean-caso__lado { background: var(--panel); padding: var(--s3) var(--s4) var(--s4); min-width: 0; }
.lean-caso__et { display: block; margin-bottom: 5px; font-size: var(--fs-xs); font-weight: var(--peso-fuerte);
  letter-spacing: var(--letra-menuda); text-transform: uppercase; color: var(--muted); }
/* La medida de lectura se respeta aunque la superficie sea ancha: en un panel
   de 1400 px, media pantalla son noventa caracteres por renglón y hay que
   barrer la cabeza para encontrar dónde empieza el siguiente. */
.lean-caso__lado p { margin: 0; max-width: 64ch; font-size: var(--fs-md); line-height: var(--alto-lectura); color: var(--muted); }
/* El tono va en la ETIQUETA, no en el fondo. Dos superficies de color
   enfrentadas convierten cada caso en un semáforo y, con doce seguidos, la
   pantalla deja de tener jerarquía: lo importante ya no es lo grave, es lo
   último que se pintó. */
.lean-caso__lado--mal  .lean-caso__et { color: var(--down); }
.lean-caso__lado--bien .lean-caso__et { color: var(--ok); }
.lean-caso__lado--bien { background: color-mix(in srgb, var(--ok) 4%, var(--panel)); }
/* El pie es para la referencia: de dónde sale esto, dónde leer más. Nunca para
   una tercera idea —el caso son dos— y por eso va en letra de nota. */
.lean-caso__pie { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2) var(--s3);
  padding: var(--s3) var(--s4); border-top: 1px solid var(--line);
  font-size: var(--fs-sm); color: var(--muted); }
@media (max-width: 720px) {
  .lean-caso__lados { grid-template-columns: 1fr; }
}

/* --- lean-puntos: lo comprobado y correcto, en rejilla ---------------------
   Doce cosas que están bien no son doce avisos: son el respaldo de una sola
   frase («todo correcto»). Con el tratamiento de un aviso cada una —marco,
   fondo, color— ocupan la pantalla entera para no decir nada, y entonces lo
   que sí hay que mirar tiene que competir con ellas.

   Aquí van sin marco y en columnas, que es como se lee una lista de
   verificación: se recorre, no se estudia. El color sólo marca la marca de
   verificación; el texto se queda en negro sobre el fondo del panel. */
.lean-puntos { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--s4) var(--s5); align-items: start; }
.lean-punto { display: flex; align-items: flex-start; gap: var(--s2); min-width: 0; }
.lean-punto > .lean-ico { width: var(--ico); height: var(--ico); flex: none; margin-top: 2px; color: var(--faint); }
.lean-punto.is-ok   > .lean-ico { color: var(--ok); }
.lean-punto.is-warn > .lean-ico { color: var(--warn); }
.lean-punto.is-down > .lean-ico { color: var(--down); }
.lean-punto__txt { min-width: 0; }
.lean-punto__t { font-size: var(--fs-md); font-weight: var(--peso-titulo); color: var(--ink); }
.lean-punto__d { font-size: var(--fs-sm); line-height: var(--alto-ui); color: var(--muted); overflow-wrap: anywhere; }

/* --- lean-punto--accion: el punto que además se OPERA --------------------
   El punto de arriba se recorre: son doce cosas que están bien y no hay nada
   que hacer con ellas. En cuanto una lleva un mando al final deja de ser eso y
   pasa a ser una lista de decisiones que se pueden deshacer —las redes que
   alguien ignoró, los dispositivos que sacó de una red—, y ahí la caja de
   arriba no vale: es un flex con `align-items: flex-start` y sin nada que
   empuje, así que el botón se pega al texto y sube al renglón del título.

   El efecto no se ve en una fila, se ve en once: como cada botón arranca donde
   acaba SU texto, la columna de mandos sale dentada. Medido en «Lo ya
   decidido», que es donde se encontró: los de la columna izquierda empezaban en
   455, 460, 460, 465, 473 y 486 px. Con el mando anclado al canto derecho de su
   celda —y las celdas de la rejilla miden todas lo mismo— los once caen en la
   misma vertical y la lista vuelve a leerse como una columna.

   Va como MODIFICADOR y no en `.lean-punto` a secas porque el centrado es lo
   correcto aquí y lo incorrecto allí: en una lista de verificación el icono
   tiene que quedar a la altura de la PRIMERA línea del texto, no en el medio de
   un párrafo de tres. */
.lean-punto--accion { align-items: center; }
.lean-punto--accion > .lean-punto__txt { flex: 1; }
.lean-punto--accion > .lean-boton { margin-left: auto; flex: none; }

/* --lista: UNA COLUMNA, para cuando los renglones se OPERAN.
 *
 * `lean-puntos` es `repeat(auto-fill, minmax(280px, 1fr))`, o sea una rejilla
 * que mete tantas columnas como quepan. Para lo que es —una lista de
 * verificación que se recorre— está bien: doce cosas correctas en dos columnas
 * se leen igual de bien y ocupan la mitad.
 *
 * Deja de estarlo en cuanto cada renglón lleva un mando. `--accion` ancla el
 * botón al canto derecho de SU CELDA, y con dos columnas eso son DOS verticales
 * de mandos, más la última fila coja. Es exactamente lo que `docs/lenguaje.md`
 * §7 prohíbe: «en la misma vertical: aquí lo que se repite es el mando». Once
 * botones en una vertical son una columna de mandos; en dos, son ruido — que es
 * la diferencia entre leerse como un instrumento y leerse como desorden.
 *
 * Y NO SE ARREGLA SOLO PORQUE HOY QUEPA UNA. «Fuera de esta red» salía bien por
 * casualidad del ancho: vive en un `lean-dialog` de 460 px, donde 280 px de
 * mínimo sólo dan para una columna. «Lo ya decidido» es el mismo marcado en un
 * `--lg` de 720 y salía a dos. Un acierto que depende del ancho del contenedor
 * se rompe el día que alguien cambia el contenedor, sin que nada avise. */
.lean-puntos--lista { grid-template-columns: 1fr; }

/* --- lean-logo: el logotipo de un cliente ----------------------------------
   El logotipo que sube un cliente NO es un icono, y ahí estaba el error: se
   pintaba en una caja cuadrada de 32×32. Un logotipo real es horizontal —3:1 o
   4:1 es lo normal, un rótulo con el nombre dentro—, así que metido en un
   cuadrado y con `contain` se encogía hasta ocupar una franja de ocho píxeles de
   alto en mitad de un cuadrado casi vacío. Ilegible, y encima descolocado
   respecto al nombre que lleva al lado.

   Es el mismo problema que ya resolvió `lean-brandmark` con los logos de
   fabricante, y se resuelve igual: **el ALTO manda y el ancho es libre**, que es
   la convención de cualquier muro de logos. El tope de ancho existe para que un
   rótulo muy largo no pueda empujar la columna de una tabla: al tocarlo, el
   dibujo se encoge de alto dentro de él y se sigue leyendo entero.

   Cinco medidas, de acompañar a presidir: `--sm` para ir dentro de una línea de
   texto, la de por defecto para listas, `--md` para una cabecera compartida con
   otro título, `--lg` para presidir una tarjeta y `--xl` para el membrete de una
   pantalla que ES de ese cliente —su ficha, su red—.

   El TOPE DE ANCHO se abre según se sube de medida (de 5,5 veces el alto a 3,5)
   y no es una escala arbitraria: un logotipo grande que además puede estirarse
   cinco veces su alto se come media cabecera, y a esa medida ya no hace falta
   —el dibujo se lee de sobra— mientras que a 17 px, sí. */
.lean-logo { --logo-alto: 26px; --logo-veces: 4.5;
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--logo-alto); max-width: calc(var(--logo-alto) * var(--logo-veces));
  flex: none; overflow: hidden; }
/* LA ALTURA VA FIJA Y EL ANCHO LIBRE, y no es lo mismo que `auto` en las dos.
 *
 * Con `height: auto` el navegador necesita que el archivo diga cuánto mide, y un
 * SVG puede no decirlo: los que salen de SVGO llevan sólo `viewBox`, sin
 * `width` ni `height`. Sin tamaño intrínseco y con `max-height` como único
 * límite, la caja resuelta es CERO — el logotipo no se ve, no hay error en
 * consola y la imagen está cargada y entera. Se vio al estrenar el logotipo de
 * un proveedor: la vista previa del diálogo salía en blanco con un archivo de
 * 300 × 75 dentro.
 *
 * Sólo pasaba en las cajas que no fijan ancho (`--md`, `--lg`, `--xl`); en
 * `--celda`, que sí lo fija, el mismo archivo se pintaba. Por eso llevaba
 * escondido desde siempre: donde más se miran los logotipos es en las tablas.
 *
 * `object-fit: contain` es lo que hace que esto no deforme nada: la altura se
 * fija y el dibujo se ajusta dentro respetando su proporción. Lo único que
 * cambia respecto a antes es que un mapa de bits MÁS BAJO que su caja ahora se
 * amplía hasta llenarla. Medido sobre el parque real: de trece logotipos
 * guardados, doce miden 68 px de alto o más —el panel los reescala a 256 al
 * subirlos— y los dos que no son dos oficinas de 48 y 49 px, que sólo se
 * agrandan en `--xl` y sólo en la cabecera de una red. Un 33 % de ampliación en
 * dos dibujos, a cambio de que un formato entero deje de salir invisible. */
.lean-logo img { max-width: 100%; width: auto; height: 100%;
  object-fit: contain; display: block; }
.lean-logo--sm { --logo-alto: 17px; --logo-veces: 5.5; }
.lean-logo--md { --logo-alto: 36px; --logo-veces: 4.2; }
.lean-logo--lg { --logo-alto: 46px; --logo-veces: 4; }
.lean-logo--xl { --logo-alto: 64px; --logo-veces: 3.5; }

/* En una TABLA, el ancho libre deja de ser una virtud: una columna donde cada
   fila mide un ancho distinto no se puede recorrer con la mirada, que es lo
   único que se le pide a una lista de cuarenta clientes.
 *
 * Aquí la caja es uniforme —mismo alto Y mismo ancho en todas las filas— y el
 * dibujo se centra dentro: el alargado llena el ancho, el cuadrado llena el
 * alto, y los dos ocupan el mismo rectángulo. Es lo que hace cualquier muro de
 * logos, y el motivo de que no sea el comportamiento por defecto es que fuera de
 * una tabla —en una ficha, en una cabecera— reservar ese hueco fijo deja al logo
 * flotando en medio de la nada.
 *
 * 3,4 veces el alto y no 4,5: cuanto más ancha es la caja, más lejos del borde
 * queda un logo cuadrado y más se nota que «baila» respecto al de arriba. */
.lean-logo--celda { --logo-veces: 3.4;
  width: calc(var(--logo-alto) * var(--logo-veces)); max-width: none;
  justify-content: center; }

/* La PLACA: la superficie que un logo necesita debajo para verse.
 *
 * Un logotipo de cliente suele ser un PNG transparente, y muy a menudo con las
 * letras blancas: sobre el tema claro del panel desaparece. Lo que NO se hace es
 * tocar el archivo —invertir el logo de alguien produce algo que ya no es su
 * logo—; se le pone detrás el fondo sobre el que fue dibujado.
 *
 * Va en un color fijo y no en un token de tema a propósito: el sentido de la
 * placa es dar el fondo que le falta al dibujo, así que tiene que ser el mismo
 * en tema claro y en oscuro. Con `var(--panel)` se movería con el tema y en uno
 * de los dos volvería a desaparecer, que es justo lo que viene a arreglar.
 *
 * Sin placa —el caso normal, un logo a color— no hay caja ninguna: un marco
 * permanente alrededor de cada logo ensucia una lista entera para resolver el
 * problema de dos. */
/* `border-box` y no `content-box`: la placa es relleno POR DENTRO de la caja,
   nunca por fuera. Sumándose al tamaño, un logo con placa medía 100×32 donde el
   de al lado medía 88×26, se salía de su columna y empujaba al nombre hasta
   pegárselo. La caja manda; lo que la placa quita, se lo quita al dibujo. */
/* El relleno va PROPORCIONAL al alto y no en píxeles fijos: los 3×6 que están
   bien alrededor de un logo de 26 px son un cerco pegado al dibujo cuando el
   mismo logo preside una ficha a 64. Con la proporción, la placa se ve igual de
   holgada en las cinco medidas. */
.lean-logo--placa { padding: calc(var(--logo-alto) * .115) calc(var(--logo-alto) * .23);
  border-radius: var(--r-xs); box-sizing: border-box; }
.lean-logo--claro  { background: #FFFFFF; box-shadow: inset 0 0 0 1px rgba(22,36,42,.10); }
.lean-logo--oscuro { background: #16242A; box-shadow: inset 0 0 0 1px rgba(255,255,255,.10); }

/* La TINTA: la OTRA respuesta a la misma pregunta que contesta la placa.
 *
 * La placa resuelve el problema por debajo —le pone al dibujo el fondo sobre el
 * que fue dibujado—, y resuelve. Pero deja un recuadro de color en medio de la
 * pantalla, y en una tabla de cuarenta clientes eso se ve: cuatro rectángulos
 * blancos repartidos por una columna donde los demás logos flotan sin marco.
 *
 * Esto lo resuelve por encima. Del archivo se toma sólo la SILUETA —se usa como
 * máscara— y el color lo pone `--ink`, la tinta de texto del panel, que ya es
 * casi negra en el tema claro y casi blanca en el oscuro. Así el mismo archivo
 * se lee en los dos temas sin placa, sin marco y sin guardar dos versiones.
 *
 * NO ES UNA IDEA NUEVA AQUÍ: es exactamente lo que hace `lean-brandmark` con
 * los logos de fabricante, y por el mismo motivo —«el color no vive en el
 * archivo, lo pone quien lo usa»—. Lo que cambia es de quién es el logotipo, y
 * de ahí sale la única regla de uso que importa: **esto se ELIGE, nunca se
 * aplica solo.** Un logotipo monocromo ya no lleva el color de su marca, y esa
 * es una decisión del cliente, no una corrección que el panel pueda tomar por
 * su cuenta al ver que el contraste no llega. La placa se propone sola porque
 * no toca el dibujo; la tinta, no.
 *
 * Y LA TRANSPARENCIA NO SE PIERDE, QUE ES TODO EL TRUCO. Una máscara usa el
 * canal alfa del archivo: donde el PNG es transparente no se pinta nada, y
 * donde el borde está suavizado al 30 % se pinta el color al 30 %. No hay
 * umbral, ni recorte, ni halo — sale la misma silueta con el mismo
 * antialiasing, sólo que de otro color. Por eso funciona igual con un PNG y con
 * un SVG, y por eso el archivo del cliente no se toca: esto es una decisión de
 * PINTADO y se deshace eligiendo «Tal cual».
 *
 * Dos cosas de mecánica, que son las que muerden al tocarlo:
 *
 *   · EL `<img>` SE QUEDA, invisible pero ocupando su sitio. Es él quien le da
 *     a la caja el ancho que pide el dibujo —un rótulo 4:1 y un logo cuadrado
 *     no miden lo mismo—, y una máscara no tiene tamaño propio que ofrecer. Es
 *     la diferencia con `lean-brandmark`, que necesita que el backend le mida
 *     la proporción y se la mande en `--marca-ar`; aquí no hace falta ninguna
 *     columna porque la imagen sigue ahí midiendo. Con `visibility:hidden` y no
 *     con `display:none`: lo segundo lo saca del flujo y la caja se queda en
 *     nada. Y de propina, el respaldo de `data-si-falla` sigue funcionando —el
 *     `error` de la imagen se dispara igual—, que es lo que hace que un logo
 *     que no carga caiga a las iniciales también aquí.
 *   · `--logo-src` LO PONE QUIEN PINTA, en el atributo `style`, igual que
 *     `--marca`. No es una decisión de diseño repetible: es el dato de la fila.
 *     El valor de reserva es un degradado transparente y no `none` a propósito:
 *     con `mask:none` el pseudo-elemento no se recorta con nada y lo que sale
 *     es un RECTÁNGULO macizo de tinta tapando el logo entero. Que no se vea
 *     nada es un fallo que se mira; una mancha negra sobre la pantalla del
 *     cliente, también, pero después de enseñársela. Lo vigila
 *     `dash/lean_test.go`, que exige que la clase y la variable se escriban
 *     juntas. */
.lean-logo--tinta { --logo-src: linear-gradient(#0000, #0000); position: relative; }
.lean-logo--tinta img { visibility: hidden; }
.lean-logo--tinta::after { content: ""; position: absolute; inset: 0;
  background-color: var(--ink);
  -webkit-mask: var(--logo-src) center / contain no-repeat;
          mask: var(--logo-src) center / contain no-repeat; }

/* Sin logo: las iniciales. Nunca un hueco vacío, que se lee como una imagen que
   no ha cargado.
 *
 * El cuadro lo pinta el hijo y no la caja, y esa es toda la gracia: dentro de
 * una tabla la CAJA mide lo que la de un logotipo —para que los nombres de al
 * lado empiecen todos en la misma columna— pero el cuadro sigue siendo un
 * cuadrado del tamaño de un avatar. Pintando el fondo en la caja, una empresa
 * sin logo salía con una barra gris de 88 px y una letra perdida en medio. */
.lean-logo--iniciales { width: var(--logo-alto); max-width: none; }
.lean-logo__ini { display: grid; place-items: center; flex: none;
  width: var(--logo-alto); height: var(--logo-alto);
  border-radius: var(--r-xs); background: var(--panel-2); color: var(--muted);
  font-size: calc(var(--logo-alto) * .42); font-weight: var(--peso-titulo); letter-spacing: var(--letra-codigo); }
.lean-logo--celda.lean-logo--iniciales { width: calc(var(--logo-alto) * var(--logo-veces)); }

/* --- lean-emblema: el icono de algo, en su placa --------------------------
   Un icono suelto acompaña a un texto; un emblema IDENTIFICA una cosa. La
   diferencia se ve en la pantalla: quince píxeles al lado de una palabra se
   leen como parte de la frase, y los mismos quince en una placa se leen como
   «esto es lo que estás mirando».

   Existe porque el panel llevaba tiempo pintándolo a mano en tres sitios y
   cada uno lo medía distinto —el hueco de la foto de una ficha, el cuadro de
   las iniciales de un logotipo, el emblema del veredicto—. Los tres siguen
   siendo suyos: aquélla es una fotografía, aquél un respaldo de marca y el
   tercero es de dominio (el color de estado ocupando una superficie entera).
   Esto es el caso corriente, el que faltaba: un icono del sprite presidiendo
   algo que tiene nombre.

   El radio NO es 50 %: un círculo es una persona (`.avatar`) o un estado
   (`lean-veredicto__emblema`). Esto es una cosa, y las cosas de este panel son
   rectángulos con la esquina blanda —lo mismo que hacen la foto de un modelo y
   el cuadro de unas iniciales—.

   La tinta es de MARCA y no de estado: dice qué es, no cómo está. Si algún día
   hace falta un emblema que además informe, eso es otro componente, porque
   entonces el color pasa a significar. */
.lean-emblema {
  display: grid; place-items: center; flex: none;
  width: var(--emblema-lado, 38px); height: var(--emblema-lado, 38px);
  border-radius: var(--emblema-r, 10px);
  background: var(--panel-2); color: var(--brand-ink);
  transition: background var(--t-fast) var(--ease);
}
.lean-emblema .lean-ico { width: var(--emblema-ico, 19px); height: var(--emblema-ico, 19px); }
/* Nace con UNA sola medida, y es a propósito: las tallas se añaden cuando una
   pantalla las pide, no por si acaso. Escribí `--lg` y `--sm` en el mismo commit
   que el componente y no las gastaba nadie — que es exactamente cómo se llega a
   un sistema lleno de variantes que hay que mantener y que nadie usa. Los
   tamaños salen de variables, así que la talla que haga falta será una línea. */
/* Dentro de una tarjeta que se puede pulsar, el emblema acompaña al realce en
   vez de quedarse plano: es lo que hace que la tarjeta entera se lea como UNA
   pieza y no como una caja con un icono pegado. */
.lean-tarjeta--hover:hover .lean-emblema { background: var(--elegido-2); }

/* --- lean-facts: pares etiqueta → valor ------------------------------------
   Una lista donde todo pesa igual no se lee, se recorre. De ahí el modificador
   para destacar lo poco que importa de verdad, y el de lo sensible, que no
   puede tener el mismo trato que un número de pedido.

   LOS TRES PELDAÑOS, y no hay un cuarto (1.58.0). El valor de un dato es lo que
   se viene a leer y va en `--fs-lg`; lo que lo acompaña —la etiqueta de la
   izquierda, un metadato, una unidad— en `--fs-sm`; y la sub-línea, que ya es
   una nota al pie del valor, en `--fs-xs`.

   Se escribe aquí porque antes no se escribía en ninguna parte: `__v` no
   declaraba tamaño y heredaba 14 px del cajón, mientras las listas de las otras
   pestañas de la misma ficha declaraban `--fs-md` (13). Nadie eligió que el
   procesador de un equipo midiera un píxel más que un programa instalado; salió
   así, y se veía. */
.lean-facts { display: flex; flex-direction: column; font-size: var(--fs-lg); }
.lean-fact { display: grid; grid-template-columns: 34% 1fr; gap: var(--s3);
  align-items: baseline; padding: 9px 0; border-bottom: 1px solid var(--line); }
.lean-fact:last-child { border-bottom: 0; }
.lean-fact__k { color: var(--muted); font-size: var(--fs-sm); }
/* El valor y sus adornos —un icono de ayuda, un botón de copiar, un tag— van en
 * una línea, centrados entre sí y con aire: pegados al texto parecen un error
 * de maquetación, y sin línea base común el icono flota. La sub-línea rompe a
 * renglón aparte en vez de colocarse al lado. */
.lean-fact__v { min-width: 0; overflow-wrap: anywhere;
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px var(--s2); }
.lean-fact__v > .lean-fact__sub { flex-basis: 100%; }
/* Y el dato monoespaciado mide lo que mide el renglón donde vive. `lean-u-mono`
   clava `--fs-sm` para servir de utilidad en celdas y chips, y con eso el número
   de serie —el texto que peor se lee de la ficha, y el que se compara carácter a
   carácter contra un albarán— salía a 12 px dentro de una fila de 14. Se baja un
   pelo (.94) porque una mono al mismo cuerpo que una sans se ve más grande. */
.lean-fact__v .lean-u-mono { font-size: .94em; }
/* 600 y no 650: eran los dos pesos de «esto pesa más» que convivían en la misma
   ficha —650 aquí, 600 en los títulos de lista y en el nombre de un adaptador—,
   y a 13-14 px no se distinguen. Uno sobra, y el que se queda es el que ya usa
   todo lo demás. */
.lean-fact--destacado .lean-fact__v { font-weight: var(--peso-titulo); }
/* Una fila con un secreto NO se pinta distinta. La máscara y el botón de
   revelar ya dicen lo que es; un fondo permanente sólo señalaba con el dedo
   dónde está la contraseña, en todas las fichas y para siempre. */
.lean-fact--sensible .lean-fact__v { font-family: var(--mono); }
.lean-fact__sub { display: block; font-size: var(--fs-xs); color: var(--muted); margin-top: 1px; }
@media (max-width: 480px) { .lean-fact { grid-template-columns: 1fr; gap: 2px; } }

/* Ocho pares en una tira de ocho renglones son un scroll para leer datos que no
   se relacionan entre sí. En una superficie ancha van en dos columnas: la misma
   información en la mitad de recorrido. El filete de la última fila de cada
   columna se quita a mano —:last-child sólo alcanza a una de las dos—. */
.lean-facts--cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  column-gap: var(--s6); }
.lean-facts--cols .lean-fact { border-bottom: 1px solid var(--line); }
@media (max-width: 480px) { .lean-facts--cols { grid-template-columns: 1fr; } }

/* `--ajuste`: la misma lista cuando lo de la derecha NO es un dato, es un
   AJUSTE. Es lo que había en Ajustes › Alertas y Ajustes › Papelera con un
   segundo juego de clases (`ficha-facts`/`fact`, de antes de Lean), con otras
   medidas y otro cuerpo de letra: dos componentes para lo mismo, que es lo que
   la regla 5 viene a evitar. No son dos: la diferencia es el reparto, o sea un
   modificador.

   Tres cosas cambian, y las tres por la misma razón —esto se rellena, no se
   lee—:

   · LA REJILLA ES DEL CONTENEDOR y las filas van con `display: contents`. El
     34 % de la columna de una ficha son 300 px en una sección de Ajustes, que es
     ancha; con `max-content` la columna vale lo que el rótulo más largo y ni un
     píxel más. Tiene que ser del contenedor: puesto en cada fila, cada una
     calcula su propia columna y los controles empiezan a tres alturas distintas.
     Es lo que se vio al probarlo.

   · EL VALOR ES UNA FRASE, no una fila de piezas. `__v` es flex —para un dato y
     sus adornos: un icono de ayuda, un botón de copiar— y aquí eso convertía
     cada trozo de texto y cada `<em>` en un item suelto que envolvía por su
     cuenta: «…su oficina sigue en línea» y en el renglón siguiente «. Si se
     apaga la oficina entera…», con el punto abriendo línea.

   · CENTRADO Y NO POR LA BASE: lo que hay dentro mide `--ctl-h`, y un campo de
     32 px alineado por la base de su texto cuelga del renglón. */
.lean-facts--ajuste { display: grid; grid-template-columns: max-content 1fr; column-gap: 0; }
.lean-facts--ajuste .lean-fact { display: contents; }
/* CADA CELDA ESTIRA HASTA EL FONDO DE SU FILA, y esto es lo que hace que el
   filete sea una línea y no un escalón.

   Llevaba `align-self: center`, que es lo contrario: la celda dejaba de ocupar
   la fila y su borde se pintaba al final de SU contenido. Con las dos mitades a
   la misma altura no se notaba —de ahí que sobreviviera—, pero en cuanto una
   fila tuvo un rótulo de una línea y un valor de dos, las dos medias rayas
   salieron a alturas distintas. Se veía en la guía, que es donde se copia de
   qué va el componente.

   El centrado que hacía falta era el del RÓTULO respecto a un valor más alto, y
   ése se hace dentro de la celda —que ya estira— y no moviendo la celda. */
.lean-facts--ajuste .lean-fact__k,
.lean-facts--ajuste .lean-fact__v { padding: 9px 0; border-bottom: 1px solid var(--line); align-self: stretch; }
.lean-facts--ajuste .lean-fact__k { display: flex; align-items: center; }
/* El filete lo pintan las DOS celdas, así que sólo es una línea seguida si se
   tocan: la separación va como relleno de la primera y no como hueco de la
   rejilla, que dejaría el trazo partido en mitad de la fila. */
.lean-facts--ajuste .lean-fact__k { padding-right: var(--s5); }
.lean-facts--ajuste .lean-fact__v { display: block; line-height: var(--alto-lectura); }
.lean-facts--ajuste .lean-fact__v .lean-input { vertical-align: middle; margin: 0 4px; }
.lean-facts--ajuste .lean-fact:last-child .lean-fact__k,
.lean-facts--ajuste .lean-fact:last-child .lean-fact__v { border-bottom: 0; }
@media (max-width: 480px) { .lean-facts--ajuste { grid-template-columns: 1fr; } }

/* --- lean-opcion: una DECISIÓN tomada sobre algo ----------------------------
   Una fila con tres cosas: cómo está, de qué es, y el mando para cambiarlo.
   `lean-opciones` es la lista.

   DE DÓNDE SALE. La ficha de una empresa terminaba en cuatro bloques —el
   acuerdo de tratamiento, la política de escaneo, los avisos silenciados y la
   contraseña de sus teléfonos— y los cuatro estaban dibujados de una manera
   distinta: una píldora con dos fechas y un botón, un botón suelto bajo un
   rótulo, un conmutador con su pie de texto, y una caja de aviso con el botón
   dentro. Seis rótulos hermanos para tres clases de cosa, 1163 px de contenido
   en un cajón de 916, y cada bloque pidiendo su propia lectura.

   Los cuatro son LA MISMA COSA: algo que alguien decidió, con un estado que se
   lee y un mando que lo cambia. Así que no es que estuvieran mal alineados: es
   que tenían que ser una sola pieza repetida cuatro veces.

   LA FRONTERA CON `lean-facts--ajuste`, que se parece y no es. Aquél es «para
   cuando lo de la derecha se RELLENA en vez de leerse»: un umbral, un campo, un
   valor que se teclea. Esto es para lo que se DECIDE: tiene un ESTADO que leer
   antes de tocarlo —y por eso lleva punto— y el mando no es un campo, es un
   botón que abre algo o un conmutador. Si lo que hay a la derecha se escribe,
   es el otro.

   EL PUNTO VA AL PRINCIPIO DEL TODO, y ésa es la decisión que hace que esto
   pertenezca a este panel y no a cualquier lista de ajustes. `lean-presencia` ya
   vive pegado a la identidad y a la izquierda en todas partes: sobre la foto de
   un ordenador, delante del nombre de un dispositivo, en su tarjeta. Estuvo en
   una tercera columna, en medio de la fila, y era el único sitio del panel donde
   ese punto flotaba suelto.

   Y NO HAY COLUMNA DE ESTADO ESCRITO. El estado lo dicen dos cosas a la vez: el
   punto, y el VERBO del mando, que cambia con él —«Registrar» si falta el
   acuerdo, «Editar» si está; «Guardar» si no hay contraseña, «Cambiar» si la
   hay—. Quien pinta la fila pone las palabras en el `data-tip` del punto, que es
   donde el resto del panel las pone cuando no hay sitio para escribirlas. Con
   una tercera columna de texto, el asunto se quedaba en 170 px de los 318 que
   tiene ahora.

   Tampoco lleva ICONO. Cabría —cada fila es una COSA y no una acción, que es el
   criterio del catálogo— pero no distingue nada que el rótulo no distinga solo,
   y el hueco de la izquierda ya tiene dueño. */
.lean-opciones { --opcion-mando: 160px; display: flex; flex-direction: column; }
.lean-opcion__grupo { border-bottom: 1px solid var(--line); }
.lean-opciones > .lean-opcion__grupo:last-child { border-bottom: 0; }
.lean-opcion {
  position: relative; display: grid;
  grid-template-columns: 8px minmax(0, 1fr) var(--opcion-mando);
  align-items: center; gap: 0 var(--s3);
  min-height: 52px; padding: var(--s2) 0;
}
.lean-opcion .lean-presencia { flex: none; width: 8px; height: 8px; }
.lean-opcion__que { font-size: var(--fs-md); color: var(--ink); font-weight: var(--peso-medio);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Cuando la fila tiene algo que desplegar, el ASUNTO es lo que lo abre —no un
   mando aparte—: la columna del mando ya tiene dueño, y meter ahí dos botones
   deshace la única columna que hace legible la lista. El chevón gira al abrir,
   que es lo que dice que hay algo debajo antes de pulsarlo.

   VA DETRÁS DEL RÓTULO, y no delante. Delante era donde tenía sentido —es donde
   lo pone un árbol de carpetas— y en una lista de cuatro filas hace lo contrario
   de lo que se busca: sólo una de las cuatro lo lleva, así que su renglón
   empezaba veinte píxeles más a la derecha que los otros tres y rompía la
   columna que esta pieza existe para tener. Detrás, el texto sigue arrancando
   en la misma x y el chevón queda pegado a la palabra que abre. */
button.lean-opcion__que { border: 0; background: none; font: inherit; font-weight: var(--peso-medio);
  text-align: left; cursor: pointer; padding: 0; display: flex; align-items: center;
  gap: var(--s2); white-space: nowrap; }
.lean-opcion__que .lean-ico { width: var(--ico); height: var(--ico); color: var(--muted); flex: none;
  transition: transform var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }
button.lean-opcion__que:hover .lean-ico { color: var(--ink); }
.lean-opcion__grupo.is-abierta .lean-opcion__que .lean-ico { transform: rotate(90deg); }
/* EL MANDO OCUPA SU COLUMNA ENTERA, y esto es lo que da la simetría.
   `lean-actionbar` iguala sus botones con `min-width: 88px` y su nota dice por
   qué: «lo que canta es el botón diminuto al lado del grande». En una barra
   horizontal eso basta; en una COLUMNA no, porque ahí se ve además el borde
   izquierdo de los cuatro. Medido: con el mínimo salen 88 · 140 · 88 · 88
   empezando en dos sitios; estirados, 160 · 160 · 160 · 160 empezando en uno.
   Lo que estira es `lean-boton--block`, que ya existía, y `lean-segmented--block`
   para el conmutador. */
.lean-opcion__mando { display: grid; grid-template-columns: 1fr; align-items: center; }
/* EL VELO DEL RENGLÓN ES UNA CAPA DETRÁS, no el fondo de la fila.
   Con el fondo llena los 52 px enteros: toca el punto y el borde del botón por
   los lados, y a las filas de arriba y de abajo por el canto — al recorrer la
   lista se lee una banda continua en vez de un renglón señalado. Como
   pseudo-elemento se le da aire por los cuatro lados sin que la fila cambie de
   alto: tres píxeles arriba y abajo, diez a los lados.
   Es el mismo recurso que ya usa la fila de una oficina (`emp-oficina__ir`, en
   app.css) con `padding: 4px 6px; margin: -4px -6px`; aquí va en una capa porque
   el sangrado vertical de un margen negativo sí movería a las vecinas. */
.lean-opcion::before {
  content: ""; position: absolute; inset: 3px -10px; z-index: 0;
  border-radius: var(--r-s); background: transparent;
  transition: background var(--t-fast) var(--ease);
}
.lean-opcion:hover::before { background: var(--al-pasar); }
.lean-opcion > * { position: relative; z-index: 1; }

/* EL DETALLE VA FUERA DE LA REJILLA, como hermano de la fila dentro de su grupo.
   Metido dentro como cuarto item se pelea con `align-items: center` y con la
   fila implícita: medido, un `height` de 94 px que renderizaba 0 porque la fila
   del grid se quedaba en dos píxeles y medio.
   Se abre con `--asienta`, que es la curva para «algo que aparece ENTRE lo que
   ya está y le abre hueco»: rebasa un 4 % y vuelve, así lo de debajo se lee
   cediendo en vez de saltando. El alto lo escribe quien lo abre. */
/* EL SANGRADO VA EN LA CAJA Y NO EN SUS HIJOS, y no es cosmética del archivo:
   escrito como `.lean-opcion__detalle > *` empata en especificidad con
   `.lean-opcion__fila`, que va después y declara `padding` en atajo — o sea que
   le pisaba el lado izquierdo entero y el detalle salía a ras del punto, quince
   píxeles a la izquierda del rótulo que lo abre. Medido: 0 px de sangrado.
   Alinea el detalle con el ASUNTO (el punto más su hueco), que es de lo que
   cuelga. */
.lean-opcion__detalle { overflow: hidden; height: 0;
  padding-left: calc(8px + var(--s3));
  transition: height var(--t-asienta) var(--asienta); }
.lean-opcion__fila { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: var(--s3); padding: var(--s2) 0;
  border-top: 1px solid var(--line); font-size: var(--fs-sm); color: var(--muted); }
.lean-opcion__fila b { color: var(--ink); font-weight: var(--peso-medio); }
@media (prefers-reduced-motion: reduce) {
  .lean-opcion__detalle { transition-duration: 1ms; }
}
/* Con poco sitio el mando baja a su propia línea: 160 px de columna fija más un
   rótulo dejan al asunto sin nada en un cajón estrecho. */
@media (max-width: 480px) {
  .lean-opcion { grid-template-columns: 8px minmax(0, 1fr); row-gap: var(--s2); }
  .lean-opcion__mando { grid-column: 2; }
}

/* --- lean-actionbar: qué se puede hacer ------------------------------------
   Una sola barra por objeto, no dos. Lo frecuente y reversible a la vista; lo
   raro e irreversible, dentro del botón de desbordamiento. */
.lean-actionbar { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
/* Suelo de ancho, no igualdad. En una barra de acciones los botones se igualan
   MAL: son de tres a cinco y con etiquetas muy dispares, así que llevarlos todos
   al más largo deja a «Red» con cincuenta píxeles de aire a cada lado. Lo que
   canta es el botón diminuto al lado del grande —«Consola» a 65 px junto a
   «AnyDesk» a 97, que lleva el logo dentro—, y eso lo arregla un mínimo.
   El de icono se queda fuera: cuadrado es su forma, no su tamaño. */
.lean-actionbar .lean-boton:not(.lean-boton--icono),
.lean-toolbar   .lean-boton:not(.lean-boton--icono) { min-width: 88px; }
/* El desbordamiento va pegado a la última acción, no empujado al otro extremo:
   cuando la barra parte en dos líneas, un botón solo y lejos parece un
   descuido. */
.lean-actionbar__more { flex: none; }
/* Con poco sitio se esconden las secundarias y quedan la primaria y el
   desbordamiento: mejor que partir la barra en dos líneas.

   `__keep` SÓLO SURTE EFECTO SOBRE UNA SECUNDARIA: es lo único que esta regla
   mira. En el botón primario no hace nada —no se esconde nunca— y ahí engaña,
   porque dice que el caso estrecho está pensado cuando no lo está; en las tres
   fichas del panel estuvo puesta justo donde no servía.

   Y la acción que se esconde tiene que estar EN EL DESBORDAMIENTO, o a 420 px
   se queda sin ninguna puerta. Si no lo está, lleva `__keep`. */
@media (max-width: 420px) {
  .lean-actionbar .lean-boton--perfil:not(.lean-actionbar__keep) { display: none; }
}

/* --- lean-ficha: la cabecera de una FICHA -----------------------------------
   Lo que preside un cajón de detalle: foto o logo a la izquierda, nombre,
   subtítulo, etiquetas, y la equis arriba a la derecha. Debajo va lo que sea
   —pestañas, datos, acciones—, que ya son otros componentes.

   Estaba en `app.css` como `.ficha-*`, y llevaba tiempo siendo un componente sin
   que nadie lo hubiera bautizado: **diez de sus clases las usaban las CUATRO
   vistas que abren cajón** —equipos, empresas, red y el detalle de un enlace
   compartido—. Cuando una clase con nombre de pantalla aparece en la cuarta
   pantalla, ya no es de esa pantalla.

   Lo que NO subió, y el criterio: las que no estilan nada (`ficha-acciones`,
   `ficha-avisos`, `ficha-panel` son ganchos que la aplicación usa para
   encontrar sus trozos) y las que sólo usa una vista (`ficha-swcab`,
   `ficha-notas`, `ficha-vitales`…). **Al sistema sube lo que estila y lo que se
   repite; un gancho sin estilo es de quien lo engancha.**

   `__sub` se llamaba `ficha-empresa`, y el nombre venía de su primer uso: bajo
   el nombre de un equipo va su empresa. Pero bajo el de una empresa va su CIF y
   bajo el de un enlace compartido, cuándo se creó. Es el subtítulo, sin más. */
.lean-ficha__cargando { padding: var(--s7) var(--s5); text-align: center; color: var(--muted); }
.lean-ficha { padding: var(--s5); position: relative; display: flex; flex-direction: column; gap: var(--s5); }

/* --- LA CORNISA: el renglón de mando del cajón ------------------------------
   A la izquierda, de dónde vienes —o de qué clase es esto—; a la derecha, la
   equis. Nada más.

   POR QUÉ EXISTE, que es lo único que hay que entender para no deshacerla. La
   equis estaba en `position: absolute` sobre la esquina superior derecha, y esa
   esquina la quiere también el logotipo del cliente. Dos cosas de distinto orden
   en el mismo sitio, y ninguna sabiendo de la otra: medido, un rótulo de 4:1
   pasaba 5 px POR DEBAJO de la equis y uno de 6:1, doce. La reserva existía
   —`.ficha-marca` llevaba 32 px de `padding-right`— y no servía, porque
   `.lean-logo` es `flex: none` y por tanto el tope de su contenedor no lo puede
   encoger: el dibujo se desborda de su propia caja. La miga de vuelta hacía lo
   mismo por su cuenta, 18 px, y ahí lo que se tapaba eran palabras.

   Se puede arreglar con más hueco, y volvería a pasar con la siguiente cosa que
   alguien ponga arriba a la derecha. Sacando la equis DEL FLUJO ABSOLUTO ya no
   hay nada que reservar y el problema no puede volver: en una fila, dos
   elementos no se solapan.

   CUESTA 30 px de alto y no está vacía nunca: sin miga lleva el rótulo de qué
   clase es lo que se abre —«Pantalla», «Albarán», «Visto en la red»—, que es lo
   que hasta ahora sólo decía el icono de la foto. Es el nivel más bajo de la
   jerarquía de la ficha y va como tal: versalitas apagadas.

   El margen negativo se come el `gap` de `.lean-ficha` y deja los 12 px que
   separan la cornisa de la cabecera: son dos zonas de la misma cosa, no dos
   bloques del cajón. */
.lean-ficha__cornisa {
  display: flex; align-items: center; gap: var(--s2); min-height: var(--ctl-h-sm);
  margin-bottom: calc(var(--s3) - var(--s5));
}
.lean-ficha__cornisa .lean-ficha__hueco { flex: 1; min-width: 0; }
.lean-ficha__clase { flex: none; padding-left: 2px;
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--letra-rotulo);
  color: var(--muted); font-weight: var(--peso-titulo); }
/* La equis vive en la cornisa y ya no flota. Se queda la regla vieja para los
   diálogos que la usan fuera de una ficha (`.lean-dialog` monta la suya), pero
   dentro de la cornisa manda ésta. */
.lean-ficha__cerrar { position: absolute; top: var(--s3); right: var(--s3); z-index: 2; }
.lean-ficha__cornisa > .lean-ficha__cerrar { position: static; z-index: auto; }

/* LA CABECERA DE UNA FICHA SON CUATRO RENGLONES, y son siempre los mismos:

     1. QUIÉN ES ..................... el nombre, con la marca a su derecha
     2. CÓMO SE LLAMA, si no basta 1 .. el nombre de red, el modelo, la IP, el CIF
     3. DE QUIÉN Y DÓNDE ............. una línea, nunca dos
     4. CÓMO ESTÁ .................... el estado y desde cuándo

   ERAN TRES Y NUNCA LO FUERON. Con `subtitulo` puesto —monitor, móvil, pedido—
   la cabecera medía cuatro renglones y la foto se quedaba en tres: medido, el
   texto acababa 23 px por debajo de la foto en un monitor y en un móvil, 22 en
   un dispositivo de red y 16 en un albarán. O sea que la promesa de que «la foto
   vale exactamente lo que valen los renglones» sólo se cumplía en la ficha de un
   ordenador, que es de donde se copiaron las demás antes de que existiera el
   cuarto.

   Ahora los cuatro tienen ALTURA FIJA y `--ficha-alto` es su suma exacta, así
   que la cabecera mide lo mismo en todas y en todos sus estados. Medido sobre
   veintidós pintadas —las diecisiete de un ordenador más una por cada otra
   ficha—: 110 px en las veintidós.

     36 el nombre (lo que mide `lean-logo--md`, la pieza más alta que cae ahí)
   +  4 (--s1)  + 20 el renglón 2
   +  4 (--s1)  + 20 el renglón 3
   +  8 (--s2)  + 18 el estado
   = 110

   EL RENGLÓN 2 PUEDE IR VACÍO y su hueco se reserva igual: la ficha de un
   ordenador cuya placa ya es su nombre no tiene un segundo nombre que dar. Veinte
   píxeles de aire no se leen como un fallo; una cabecera que a veces mide 90 y a
   veces 110, sí.

   Y LO QUE NO CABE SE RECORTA, nunca envuelve. Es lo que hace que la cuenta de
   arriba sea cierta y no una aspiración. Quien recorta pone su texto entero en
   `data-tip` (lo hace `fichaCabecera`, en app/ficha.js).

   `lean-ficha__cab--marca` se queda fuera del trato: ahí el logotipo del cliente
   ES la foto, ocupa la izquierda a su propia medida y el bloque se centra contra
   él. */
/* --- LA MIGA DE VUELTA, cuando se ha llegado desde otra ficha --------------
   La pinta `fichaRastro()` (app/ficha.js) y sólo aparece si se ha encadenado:
   de un ordenador al albarán del pedido en que entró, del albarán a otra de sus
   unidades. Sin ella el cajón se releva igual y no se sabe desde dónde, que es
   lo que hacía que un recorrido de dos pasos se leyera como dos pantallas.

   VA ENCIMA DE LA CABECERA Y NO DENTRO. La cabecera contesta «quién es esto»;
   esto contesta «de dónde vengo», que es una pregunta sobre el CAMINO y no sobre
   la cosa. Metida entre el nombre y la empresa competiría con el título por el
   primer renglón, que es el sitio del que se lee primero.

   Y OCUPA POCO A PROPÓSITO: tamaño de subtítulo y color apagado hasta que se
   pasa por encima. Es una salida, no un destino — si pesara como el título,
   media ficha estaría hablando de la anterior.

   ENTRA CON EL RELEVO, con su misma duración y su misma curva, porque ocurren a
   la vez y es la regla que dejó escrita la Wiki: dos cosas simultáneas usan la
   MISMA duración y la MISMA curva o se leen como dos sucesos. Viene desde la
   izquierda —de donde vienes— y no desde abajo como todo lo demás.

   VIVE EN LA CORNISA desde que la equis dejó de flotar. Antes era un bloque
   suelto encima de la cabecera con un margen negativo para pegarse a ella, y
   podía llegar al 100 % del ancho sin que nadie le hubiera reservado el hueco de
   la equis: medido con un título de albarán largo, pasaba 18 px por debajo. Ahora
   es un elemento más de una fila, y en una fila dos cosas no se solapan. */
.lean-ficha__rastro {
  display: inline-flex; align-items: center; gap: 2px; min-width: 0;
  font-size: var(--fs-sm); color: var(--muted); text-decoration: none;
  border-radius: var(--r-s); padding: 2px 6px 2px 2px; max-width: 100%;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  animation: lean-rastro-llega var(--t-vista) var(--ease-entra) backwards;
}
.lean-ficha__rastro .lean-ico { width: var(--ico); height: var(--ico); flex: none; }
.lean-ficha__rastro span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lean-ficha__rastro:hover { color: var(--ink); background: var(--al-pasar); }
.lean-ficha__rastro:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
@keyframes lean-rastro-llega { from { opacity: 0; transform: translateX(-6px); } }
@media (prefers-reduced-motion: reduce) { .lean-ficha__rastro { animation: none; } }

/* Los cuatro renglones y su suma, en un sitio: cambiar uno recalcula la foto. */
.lean-ficha__cab {
  --ficha-r1: 36px; --ficha-r2: 20px; --ficha-r3: 20px; --ficha-r4: 18px;
  --ficha-alto: calc(var(--ficha-r1) + var(--ficha-r2) + var(--ficha-r3) + var(--ficha-r4)
                     + var(--s1) + var(--s1) + var(--s2));
  display: flex; align-items: flex-start; gap: var(--s4); }
/* Ya no hay `padding-right`: la equis no está encima. Eran cinco reservas
   distintas —ésta, la de `.ficha-marca`, el `:has()` que la anulaba, y ninguna
   en la miga ni en el estado— para un hueco que en realidad medía 24 px. */
.lean-ficha__cab--marca { align-items: center; --ficha-alto: auto; }
/* Y AHÍ EL TÍTULO SIGUE SIENDO UNA COLUMNA. `lean-ficha__titulo` pasó a ser el
   PRIMER RENGLÓN de la rejilla de cuatro —una fila con el nombre y la marca—, y
   esta variante lo usaba para lo de antes: apilar el nombre y su subtítulo. Sin
   esto, el CIF de un cliente se iba a la derecha de su nombre, en la misma línea.
   Se vio abriendo la ficha de una empresa en el panel; ningún test lo dijo, y no
   podían: los dos miran el marcado, y aquí el marcado no cambió. */
.lean-ficha__cab--marca .lean-ficha__titulo {
  flex-direction: column; align-items: flex-start; gap: var(--s1); height: auto; }
.lean-ficha__cab--marca .lean-ficha__id { height: auto; display: flex; flex-direction: column; gap: var(--s2); }
/* Y el logotipo cede ancho antes que el nombre. Es apaisado y llega al tope de
   `lean-logo--xl` —224 px de los 464 que tiene la cabecera—, así que
   «ThePalmExperienceHotels» se partía a mitad de palabra en el renglón de al
   lado. El nombre del cliente en su propia ficha se lee entero o no se lee; el
   logotipo, en cambio, se inscribe con `contain` y encogerlo no lo rompe. */
.lean-ficha__cab--marca > .lean-logo { max-width: 40%; }
.lean-ficha__foto { position: relative; width: var(--ficha-alto); height: var(--ficha-alto); flex: none; border-radius: var(--r-s); border: 1px solid var(--line); background: var(--panel-2); display: grid; place-items: center; overflow: hidden; }
.lean-ficha__foto img { width: 100%; height: 100%; object-fit: contain; padding: var(--s1); border-radius: inherit; }
.lean-ficha__foto.sinimg { color: var(--muted); }
.lean-ficha__foto.sinimg .lean-ico { width: 26px; height: 26px; }
/* EL PUNTO DE PRESENCIA SOBRE LA FOTO, igual que en la fila y en la tarjeta de
   la lista: un equipo se ve en tres sitios y en los tres su estado va pegado a
   su identidad, en la misma esquina y del mismo color. Aquí llevaba una píldora
   entre los tags, con su propio catálogo: el mismo equipo apagado salía gris en
   la lista y rojo en su ficha.

   El recorte se levanta SÓLO cuando hay punto, porque el punto monta sobre el
   borde a propósito y `overflow: hidden` se lo comería. La imagen no lo echa de
   menos: hereda el radio y `object-fit: contain` no la deja salirse del marco.

   13 px, que es el peldaño que le toca a una foto de 110: 10 sobre los 40 de la
   fila, 13 sobre los 108 de la tarjeta. */
.lean-ficha__foto:has(.lean-presencia) { overflow: visible; }
.lean-ficha__foto .lean-presencia { position: absolute; right: -3px; bottom: -3px; width: 13px; height: 13px; }
/* La foto en gris de un aparato DADO DE BAJA. No es un estado de salud —ni
   bueno, ni de aviso—: es que ya no está, y por eso se apaga en vez de teñirse.
   Lo que lo dice con palabras es el renglón 4, que en una ficha archivada es
   entero de la baja (ver `fichaRetiradaTexto`, en app/ficha.js). */
.lean-ficha__foto.es-baja { filter: grayscale(1); opacity: .55; }
/* Y con el borde de puntos cuando lo que hay dentro NO ESTÁ CONFIRMADO: un
   dispositivo de la red cuyo tipo se ha deducido de sus puertos y su marca. Era
   un `lean-tag--warn` en la cabecera —una caja ámbar que se lleva la vista
   entera para decir una duda— y la duda se dice mejor con la palabra del
   renglón 2: «Parece una impresora». Esto sólo la acompaña. */
.lean-ficha__foto.es-dudoso { border-style: dashed; }
/* LA REJILLA DE LOS CUATRO RENGLONES. Alturas fijas y repartidos por
   `space-between`, que es lo que reproduce los huecos de --s1/--s1/--s2 sin
   escribirlos dos veces: la suma está declarada arriba en `--ficha-alto`. */
.lean-ficha__id { flex: 1; min-width: 0; height: var(--ficha-alto);
  display: grid; align-content: space-between;
  grid-template-rows: var(--ficha-r1) var(--ficha-r2) var(--ficha-r3) var(--ficha-r4); }
/* EL PRIMER RENGLÓN: el nombre y la marca, y sólo esos dos. Estaba en `app.css`
   como `.ficha-titular` y sube aquí con el resto de la cabecera. */
.lean-ficha__titulo { display: flex; align-items: center; gap: var(--s4);
  min-width: 0; height: var(--ficha-r1); }
/* EL NOMBRE RECORTA Y NO ENVUELVE, NUNCA. Llevaba `overflow-wrap: anywhere`, o
   sea que un nombre largo se partía en dos líneas y metía un quinto renglón: la
   cabecera crecía y dejaba de cuadrar con la foto, que es justo lo que esta
   rejilla existe para impedir. Con `min-width` el nombre tiene suelo y el
   logotipo techo, y los dos caben: 200 + 151 + 16 de hueco son 367 de los 386
   que hay.
   El respiro vertical es padding con margen negativo del mismo valor: no ocupa,
   y hace que el recorte caiga dos píxeles por fuera de las letras. Lo necesita
   el hueco del relevo, que desborda a propósito (ver `ajustarHueco`). */
.lean-ficha__nombre { font-family: var(--disp); font-size: var(--fs-2xl); font-weight: var(--peso-titulo);
  letter-spacing: var(--letra-ajuste); line-height: var(--alto-corto); flex: 1 1 auto; min-width: 200px;
  padding: 2px 0; margin: -2px 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Y EL LOGOTIPO CEDE ANCHO, que es lo que no hacía. `.lean-logo` es `flex: none`
   —bien puesto: lo que se aplasta deja de reconocerse— y eso también anulaba el
   tope de su contenedor, así que el dibujo se desbordaba de su propia caja. Aquí
   se le devuelve el encogimiento DENTRO del primer renglón y sólo aquí: el suelo
   del nombre ya garantiza que nunca se aplaste de más. */
.lean-ficha__titulo > .lean-logo,
.lean-ficha__titulo > .lean-brandmark { margin-left: auto; flex: 0 1 auto; min-width: 0; }
.lean-ficha__titulo > .lean-logo img { max-width: 100%; }
/* RECORTA, no envuelve, y es el cambio de opinión de 1.57.0. Envolvía —«cada uno
   se lee entero y la segunda línea sólo aparece cuando de verdad no cabe»— y era
   razonable mientras el logotipo se llevaba una columna de la cabecera entera y
   al subtítulo le quedaban 218 px: recortar ahí habría dejado «ThePalmExperie…»
   en casi todos los clientes.

   Ya no es el caso. La marca comparte renglón con el nombre y aquí hay el ancho
   del cajón, así que envolver sólo pasaría con un nombre extremo —y justo ahí es
   donde más daño hace: un renglón de más descuadra la cabecera entera contra la
   foto, que mide tres. Un cliente que se lee «ThePalmExperienceHo…» se reconoce;
   una cabecera que a veces mide tres renglones y a veces cuatro, no.

   Quien cede es el PRIMERO, que es de quién, no el último, que es dónde: el
   nombre del cliente ya preside la ficha en su logotipo, y la oficina —«Hotel
   Eurosalou»— es corta y es el dato que sitúa. Por eso `flex-shrink` desigual y
   no el reparto proporcional, que dejaría los dos a medias. */
.lean-ficha__sub { display: flex; flex-wrap: nowrap; align-items: center; gap: 3px 7px;
  color: var(--muted); font-size: var(--fs-md); line-height: var(--alto-ui);
  min-width: 0; white-space: nowrap; }
.lean-ficha__sub > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lean-ficha__sub > span:first-child { flex-shrink: 999; }
/* EL SEGUNDO RENGLÓN VA EN TINTA Y EL TERCERO APAGADO, y con eso hay cuatro
   voces en cuatro renglones sin cambiar de cuerpo más que una vez: el 2 es un
   SEGUNDO NOMBRE de la cosa —el nombre de red de un ordenador, el modelo de una
   pantalla, la dirección de un aparato— y el 3 es contexto, de quién y dónde.
   Con los dos en gris se leían como una sola cosa partida en dos.
   El del medio puede ir VACÍO y su hueco se reserva igual: es lo que hace que
   una cabecera mida siempre lo mismo. */
.lean-ficha__sub--que { color: var(--ink); height: var(--ficha-r2); }
.lean-ficha__sub--donde { height: var(--ficha-r3); }
/* Un dato del subtítulo puede llevar SU icono en vez de una etiqueta: el sitio
   donde está el equipo iba en un `lean-tag`, y una caja con borde en la cabecera
   se lee como algo que se pulsa o se quita. Aquí sólo sitúa, así que se escribe
   como lo que es —una palabra más de la línea— y el icono hace de separador y
   dice de qué habla. Hereda el color y no se pinta aparte: acompaña al texto, no
   compite con él. */
/* `flex: 0 1 auto` y no `none`: el que sitúa cede el último, pero cede. Con
   `none` una oficina de nombre largo se salía del cajón en vez de recortarse. */
.lean-ficha__sub > span:has(> .lean-ico) { display: inline-flex; align-items: center; gap: 5px; flex: 0 1 auto; }
.lean-ficha__sub .lean-ico { width: var(--ico-sm); height: var(--ico-sm); flex: none; }
/* Aquí vivía `lean-ficha__tags`, el renglón de etiquetas de una cabecera. Se
   retira en 1.57.0: era el CUARTO renglón, y una cabecera son tres. Lo que
   llevaba dentro contestaba a cómo está la cosa —«Tipo sin confirmar», «Un solo
   uso», el estado de un enlace—, así que va en `__cuando`, con el resto del
   estado. Dejarlo vacío en la hoja sólo servía para que alguien lo volviera a
   usar sin hacerse la pregunta. */
/* EL CUARTO RENGLÓN: cómo está y desde cuándo. UNA LÍNEA Y EN TEXTO.

   ── EN UNA CABECERA NO ENTRA NINGUNA CAJA, Y ESO ES UNA REGLA ──────────────

   Ni `lean-tag`, ni `lean-badge`, ni chip, ni píldora. Aquí ponía justo lo
   contrario —«admite dentro lo que MATICE ese estado»— y lo que salió de esa
   puerta abierta fue esto, censado sobre las siete fichas: NUEVE familias de
   pieza distintas contestando la misma pregunta, y en cuatro fichas dos o tres a
   la vez. Una píldora con punto para «Robado» al lado de un texto en negrita
   para «Desconectado». Dos `lean-tag` iguales para cosas de distinto orden: un
   juicio sobre la clasificación («Tipo sin confirmar») y un ajuste que alguien
   puso («Vigilado»). Dos ruedas girando a la vez —el anillo del punto y el
   spinner de `ficha-encurso`— para un solo hecho.

   El criterio no es nuevo: está escrito diez líneas más arriba, sobre el icono
   de la oficina —«una caja con borde en la cabecera se lee como algo que se
   pulsa o se quita»—. Lo que faltaba era aplicarlo como regla y no como la
   excepción de un caso. Una caja es un objeto que se mira; una cabecera es un
   renglón que se lee.

   QUÉ SE HACE CON LO QUE SE ECHA: lo que es un juicio va a su pestaña y a la
   PALABRA del renglón 2 («Parece una impresora»); lo que es un ajuste, al botón
   que lo enciende; lo que pide hacer algo, a `lean-aviso`, que está veinticuatro
   píxeles más abajo. Lo vigila `dash/ficha_comun_test.go`. */
.lean-ficha__cuando { color: var(--muted); font-size: var(--fs-sm); line-height: var(--alto-ui);
  height: var(--ficha-r4); display: flex; align-items: center; min-width: 0; }
.lean-ficha__cuando > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* LAS PALABRAS DEL PUNTO. El color de un indicador nunca informa solo, y la
   ficha es la pantalla donde sí hay sitio para escribirlo: el punto pinta y este
   renglón lo dice —«En línea · última señal hace 3 min»—.

   Va con el resto de la línea y no en una píldora aparte: una píldora es un
   objeto que se mira, y esto se lee de corrido con lo que tiene al lado. Lo que
   lo separa del gris que lo acompaña es el peso, y el color sólo cuando hay algo
   que decir; el estado normal —apagado— NO lo lleva, que es justo la decisión
   que se perdía al pintarlo de rojo. */
.lean-ficha__estado { font-weight: var(--peso-titulo); color: var(--ink); }
.lean-ficha__estado.is-ok    { color: var(--ok); }
.lean-ficha__estado.is-warn  { color: var(--warn); }
.lean-ficha__estado.is-brand { color: var(--brand-ink); }

/* --- lean-banco: se pide a la izquierda, sale a la derecha -----------------
   La superficie de una herramienta, y la respuesta a un problema muy concreto:
   cinco pantallas que hacen lo mismo —entra un dato, sale una respuesta— y que
   habían inventado cinco maquetas distintas. Formulario con consola al lado,
   caja centrada, campo suelto arriba, dos cajas apiladas, dos columnas con una
   tabla debajo. Cada una defendible por su cuenta y ninguna aprendible en
   conjunto: quien se maneja en una no adivina la siguiente.

   Aquí son dos columnas y siempre las mismas: `__pide` a la izquierda con
   ancho fijo, `__da` a la derecha con el resto. La sexta herramienta ya nace
   sabiendo dónde va cada cosa.

   EL ANCHO DE LA IZQUIERDA ES FIJO Y NO UNA FRACCIÓN. Un formulario tiene un
   ancho cómodo que no depende de lo larga que sea la respuesta de al lado: con
   `1fr` el mismo campo medía 300 px con una traza SMTP delante y 500 con un
   hueco vacío, y al llegar el resultado los campos se encogían solos.

   LAS DOS CIERRAN A LA MISMA ALTURA cuando manda la izquierda, y sólo entonces.
   `__pide` no se estira (`align-self:start`) y `__da` sí, así que el alto lo fija
   siempre la más alta: el formulario cuando la respuesta es corta, la respuesta
   cuando es larga. Sin esto se descuadraban hasta 120 px en una dirección o en
   la otra según la herramienta.

   Y `__da` NUNCA SE QUEDA EN BLANCO: mientras no hay respuesta lleva un
   `lean-vacio` que dice qué va a aparecer ahí. Es la diferencia entre una
   pantalla que espera y una que parece rota, y era literalmente lo que pasaba
   —media pantalla en negro, sin caja siquiera, bajo cuatro de las cinco—. */
.lean-banco {
  display: grid; grid-template-columns: var(--banco-pide, 388px) minmax(0, 1fr);
  gap: var(--s4);
}
.lean-banco__pide, .lean-banco__da {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r);
}
.lean-banco__pide { align-self: start; padding: var(--s5); display: flex; flex-direction: column; gap: var(--s4); }
.lean-banco__da { display: flex; flex-direction: column; min-height: var(--banco-alto, 340px); overflow: hidden; }
/* Lo que llena `__da` va a sangre —una traza, una tabla de registros, una lista
   de filas—, así que el relleno lo pone lo de dentro y no la caja. El hueco
   explicado trae el suyo y se centra en lo que sobre. */
.lean-banco__da > .lean-vacio { margin: auto; }

/* Por debajo de este ancho la columna fija deja de caber al lado de nada: las
   dos se apilan y la de pedir va primero, que es el orden en que se usan. */
@media (max-width: 900px) { .lean-banco { grid-template-columns: minmax(0, 1fr); } }

/* --- lean-membrete: la cabecera de un RECURSO --------------------------------
   El hermano de `lean-ficha`, y conviene decir en qué se diferencian porque las
   dos presiden algo: `lean-ficha` encabeza un CAJÓN —una cosa que se abre
   encima de otra y se cierra—, y esto encabeza una PÁGINA que ES ese recurso.
   De ahí sale lo demás: aquí hay sitio para la marca en grande y para las
   lecturas, y la ficha no puede permitírselo.

   Son DOS ZONAS dentro de una sola superficie, separadas por un filete:

     IDENTIDAD ............ de quién es esto, cómo se llama y dónde está
     LECTURAS ............. cómo está, en cifras que se barren sin leer

   LO QUE HACE QUE ESTO FUNCIONE ES QUE NADA SE MUEVE ENTRE UN RECURSO Y OTRO.
   Es la única propiedad que este componente no negocia, y viene de mirar lo que
   sustituye: la cabecera de una red pintaba el logotipo del cliente a su propio
   ancho, así que el título arrancaba en el píxel 482 en la red de Citra, en el
   484 en la de ThePalm y en el 276 en la de un cliente sin logotipo —ahí no se
   pintaba nada—. Tres redes, tres cabeceras distintas, y al ir de una a otra el
   nombre saltaba doscientos píxeles. Nadie escribió eso: es lo que pasa cuando
   el ancho de una columna lo decide el archivo que subió un cliente.

   Aquí la placa de la marca mide SIEMPRE lo mismo (`--membrete-placa-w` por
   `--membrete-placa-h`) y el dibujo se inscribe dentro anclado a la izquierda:

     · el logotipo empieza siempre en el mismo píxel,
     · el título empieza siempre en el mismo píxel,
     · y las lecturas caen siempre en las mismas columnas.

   Anclado a la IZQUIERDA y no centrado, que es la diferencia con
   `lean-logo--celda`: en una tabla la caja uniforme se centra porque lo que se
   recorre con la vista es una columna de logos, y aquí sólo hay uno. Centrado,
   un logotipo estrecho se despega del borde de su caja y queda descolocado
   respecto al título que empieza justo debajo de ese hueco.

   Y LA PLACA NO LLEVA MARCO. `lean-ficha__foto` sí lo lleva porque ahí dentro
   va la foto de un modelo, y una fotografía necesita un canto que la separe del
   papel. Un logotipo es un dibujo sobre transparente: metido en un recuadro
   permanente se lee como una insignia recortada y no como la marca de quien
   manda en esta pantalla. Lo que sí lleva caja es el respaldo de las iniciales,
   y ésa la pinta `lean-logo` por su cuenta. */
.lean-membrete {
  /* Las tres medidas de las que sale toda la retícula. Se pueden cambiar desde
     la pantalla —para eso son variables y no números escritos en la regla— pero
     lo que NO se puede es hacerlas depender del contenido. */
  --membrete-placa-h: 64px;
  --membrete-placa-w: 168px;
  --membrete-lectura: 124px;

  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r);
}

/* La zona de identidad: DOS COLUMNAS a toda la altura de la caja, no dos bloques
   dentro de un mismo relleno.

   Esa diferencia es la que hace que el filete de la marca llegue de arriba abajo
   igual que los que separan las lecturas. Con el relleno en el contenedor, la
   línea sólo medía lo que medía el texto —cincuenta y seis píxeles de noventa y
   seis— y quedaba flotando en mitad de la caja: dos maneras de dibujar la misma
   raya en la misma cabecera. El relleno lo pone cada columna, así que las dos se
   estiran y las cuatro rayas miden lo mismo.

   Dentro de cada columna, lo suyo va centrado: la placa y el bloque de texto
   miden distinto y ninguno es el sujeto del otro. */
.lean-membrete__id {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: stretch; min-width: 0;
}
/* `content-box` a propósito, contra el `border-box` que el reset da a todo: aquí
   lo que tiene que valer 168 px es EL HUECO DEL DIBUJO, no el hueco más su
   relleno. Con border-box, subir el relleno de la caja encogería la placa, así
   que el logotipo de un cliente dependería de un espaciado. */
.lean-membrete__marca {
  box-sizing: content-box;
  display: flex; align-items: center; justify-content: flex-start;
  width: var(--membrete-placa-w); padding: var(--s4) var(--s5);
}
/* El logotipo llena la placa y ni un píxel más. `lean-logo` trae su propio tope
   de ancho —de 3,5 a 5,5 veces el alto según la talla— y aquí manda la placa:
   los dos selectores son para eso, que es exactamente el caso que la regla 5
   permite. Sin esto, un rótulo 4:1 se sale de su hueco y empuja al título. */
.lean-membrete__marca > .lean-logo {
  height: var(--membrete-placa-h); max-width: 100%; width: auto;
}
/* EL FILETE QUE SEPARA LA MARCA DEL NOMBRE, y no es adorno: es lo que convierte
   el hueco en estructura.

   Una placa de ancho fijo deja aire a la derecha del dibujo cuando el dibujo es
   estrecho —el respaldo de las iniciales son 64 px en un hueco de 168—, y sin
   una línea que lo explique ese aire se lee como un descuadre: el título parece
   haberse quedado lejos de su logotipo en unos clientes y pegado en otros,
   aunque empiece en el mismo píxel en todos. Con el filete son dos columnas, y
   dos columnas de anchos distintos no descuadran: se leen.

   Y es el MISMO filete que separa las lecturas, así que la cabecera entera queda
   en una sola gramática —columnas separadas por una línea de un píxel— en vez de
   en dos maneras de agrupar dentro de la misma caja. */
.lean-membrete__txt {
  min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 2px;
  border-left: 1px solid var(--line); padding: var(--s4) var(--s5);
}
/* El título de la página. Mismo cuerpo que `.topbar h1` porque es el mismo
   nivel de la jerarquía: uno por pantalla. */
.lean-membrete__titulo {
  margin: 0; font-family: var(--disp); font-size: var(--fs-3xl); font-weight: var(--peso-texto);
  letter-spacing: var(--letra-titular); line-height: var(--alto-nombre);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* UN SOLO RENGLÓN DEBAJO, Y SIEMPRE UNO. Lo que va dentro cambia —de quién es,
   dónde está, qué le pasa— pero el número de renglones no: en cuanto el bloque
   de texto crece o mengua deja de estar centrado contra la placa, y la cabecera
   de dos recursos hermanos mide distinto. Lo que no cabe se recorta; lo que no
   entra en una línea no va aquí. */
.lean-membrete__sub {
  margin: 0; font-size: var(--fs-md); color: var(--muted); line-height: var(--alto-lectura);
  display: flex; align-items: center; gap: var(--s2);
  min-width: 0; white-space: nowrap;
  /* EL CINTURÓN. Lo que se recorta es el nombre —lleva su `text-overflow`— pero
     los trozos que NO ceden (una dirección, una etiqueta) pueden sumar más que
     la columna, y sin esto se salen POR ENCIMA de lo que hay a la derecha en vez
     de cortarse. Pasó con cuatro lecturas y una etiqueta: el nombre del cliente
     desaparecía y la etiqueta se pintaba sobre la primera cifra. Que se corte es
     un fallo de sitio; que se pinte encima es un fallo roto. */
  overflow: hidden;
}
/* Los trozos del renglón: lo que cede es el TEXTO y nunca lo que lo matiza. Una
   etiqueta de estado cortada a media palabra no dice nada; un nombre largo con
   puntos suspensivos, sí. */
.lean-membrete__quien { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.lean-membrete__sub > :not(.lean-membrete__quien) { flex: none; }
/* El separador entre trozos. Va como pseudo-elemento y no escrito en el texto
   para que se pueda quitar un trozo sin dejar el punto colgando. Delante de una
   etiqueta no va: una píldora ya se separa sola de lo que tiene al lado. */
.lean-membrete__sub > * + *::before {
  content: "\00B7"; margin-right: var(--s2); color: var(--faint);
}
.lean-membrete__sub > * + .lean-tag::before,
.lean-membrete__sub > * + .lean-badge::before { content: none; }

/* La zona de lecturas: una columna por cifra, TODAS DEL MISMO ANCHO y separadas
   por un filete de un píxel.

   El ancho es fijo y no una fracción del sobrante a propósito: con `1fr` las
   celdas cambiarían de tamaño según lo largo que sea el nombre del cliente, así
   que la misma cifra caería en un sitio distinto en cada recurso. Aquí una
   lectura ocupa lo que ocupa y lo que se estira es la identidad.

   El filete va a la IZQUIERDA de cada celda, así que la primera lo lleva y hace
   de separador entre las dos zonas, y la última no deja ninguno pegado al borde
   de la caja. Una regla, dos trabajos.

   Y dentro va `lean-vital`, que es EL componente para una cifra con su rótulo:
   lo que este contenedor añade es la retícula y el filete, no otra manera de
   escribir un número. */
.lean-membrete__lecturas {
  --vital-n: var(--fs-2xl);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--membrete-lectura);
}
.lean-membrete__lecturas > .lean-vital {
  justify-content: center; gap: 1px;
  padding: var(--s4);
  border-left: 1px solid var(--line);
}

/* LO QUE ACABA DE CAMBIAR SE DICE UNA VEZ Y SE CALLA.
   Cuando termina un escaneo las cifras cambian solas, y hasta ahora eso no lo
   veía nadie: la tabla se repintaba y el número de arriba pasaba de 5 a 3 sin
   más. El destello dura lo que dura levantar la vista, no se repite y no deja
   color detrás —no es un estado, es que el dato es nuevo—. Lo pone y lo quita
   quien pinta. */
@keyframes lean-membrete-destello {
  /* EN `--nuevo` Y NO EN `--elegido`, aunque el azul salga el mismo. Los dos
     tokens valen la marca y lo que los separa es el ROL: `--elegido` dice «esto
     está puesto» y no se apaga nunca, y esto dice «esto acaba de cambiar» y se
     apaga siempre. Escribirlo con el token de lo elegido sería quedarse con su
     nombre para decir otra cosa, que es como `--panel-2` acabó haciendo tres
     papeles a la vez. */
  0%   { background: color-mix(in srgb, var(--nuevo) 16%, transparent); }
  100% { background: transparent; }
}
.lean-membrete__lecturas > .lean-vital.is-fresco {
  animation: lean-membrete-destello 1400ms var(--ease) 1;
}

/* ESTRECHO: las lecturas bajan y se reparten el ancho.
   El filete izquierdo de la primera dejaría de separar dos zonas para quedarse
   colgando bajo la placa, así que ahí lo que separa es el filete de arriba. */
@media (max-width: 900px) {
  .lean-membrete { grid-template-columns: minmax(0, 1fr); }
  .lean-membrete__lecturas {
    grid-auto-flow: row;
    grid-template-columns: repeat(auto-fit, minmax(var(--membrete-lectura), 1fr));
    border-top: 1px solid var(--line);
  }
  .lean-membrete__lecturas > .lean-vital:first-child { border-left: 0; }
}

/* --- lean-cajon: el cajón de detalle --------------------------------------
   La superficie más usada de la aplicación, y hasta ahora la única sin
   sistema.

   Capas: el cajón viaja por debajo de los menús flotantes (60) y de los
   diálogos (80), porque desde dentro del cajón se abren ambos. */
.lean-cajon { position: fixed; inset: 0 0 0 auto; width: min(560px, 92vw);
  background: var(--delante); border-left: 1px solid var(--linde-alto);
  box-shadow: var(--shadow-xl); display: flex; flex-direction: column;
  z-index: var(--z-cajon); overflow-y: auto; overscroll-behavior: contain;
  animation: lean-cajon-in var(--t-entra) var(--ease-entra); }
/* El cajón conserva su eje y su distancia: no «aparece en su sitio», ENTRA POR
   EL BORDE, y con los 6 px de la escala común el gesto no se leería. Lo que sí
   adopta es la duración, que es lo que hace que se sienta parte del mismo
   sistema que el resto.

   Y LA CURVA ES `--ease-entra`, que es lo que decía el token y no lo que ponía
   aquí. Llevaba `--ease`, que es la curva de un cambio de estado —el hover de
   un botón, un color que se sustituye— y frena igual que arranca. Esto no es un
   cambio de estado: es algo que APARECE, y lo que hace que algo que aparece se
   sienta acabado en vez de sólo colocado es que frene largo al final. Era el
   gesto más repetido del panel (abrir una ficha) con la curva equivocada. */
@keyframes lean-cajon-in { from { transform: translateX(24px); opacity: .4; } to { transform: none; opacity: 1; } }
.lean-cajon__scrim { position: fixed; inset: 0; background: rgb(0 0 0 / .45); z-index: var(--z-velo);
  animation: lean-cajon-fade var(--t-entra) var(--ease-entra); }
@keyframes lean-cajon-fade { from { opacity: 0; } to { opacity: 1; } }

/* --- Y CÓMO SE VA, que es lo que faltaba -----------------------------------
   El cajón entraba por el borde y se cerraba poniéndole `hidden`: desaparecía
   entre dos fotogramas, con su velo, dejando al ojo sin ninguna referencia de
   dónde estaba lo que acababa de mirar. Es el gesto más repetido del panel
   —abrir una ficha y cerrarla— y era el único de los dos que no estaba contado.

   SALE POR DONDE ENTRÓ Y MÁS DEPRISA: `--t-sale` (120 ms) y `--ease-sale`, que
   son los dos tokens que esta hoja tenía escritos para esto exacto y no llamaba
   nadie. Lo que se va no merece la atención de quien mira, así que arranca sin
   ceremonia; con la misma curva de la apertura, cerrar «cuesta».

   LO PONE Y LO QUITA EL JS (`Lean.cerrarCajon`), porque un <aside> con `hidden`
   no puede transicionar: hay que esperar a que termine antes de ocultarlo. El
   velo se va CON él y no antes, o durante 120 ms se vería el cajón flotando
   sobre la lista ya despejada. */
.lean-cajon.is-saliendo { animation: lean-cajon-out var(--t-sale) var(--ease-sale) forwards; }
.lean-cajon__scrim.is-saliendo { animation: lean-cajon-fade var(--t-sale) var(--ease-sale) reverse forwards; }
@keyframes lean-cajon-out { from { transform: none; opacity: 1; } to { transform: translateX(26px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .lean-cajon.is-saliendo, .lean-cajon__scrim.is-saliendo { animation-duration: 1ms; }
}
/* Aquí vivían `__head`, `__body` y `__x`, la anatomía estándar del cajón. No la
   estrenó nadie: el único cajón del panel es el de lienzo, que es justo la
   variante que recibe el contenido ya maquetado. Al retirarlos, lo que dice el
   comentario de `--lienzo` deja de necesitar una nota al pie. */
@media (max-width: 560px) { .lean-cajon { width: 100vw; border-left: 0; } }

/* Variante lienzo: el cajón recibe contenido propio ya maquetado (la ficha del
   equipo), en vez de componerse por partes.
 *
 * LLEVA SU PROPIA ESCALERA, Y ÉSTE ES EL ARREGLO DE LAYERING QUE MÁS SE VE.
 *
 * Se pintaba con `--bg`, y el razonamiento era bueno a medias: «fondo de
 * aplicación, porque dentro van tarjetas que necesitan destacar sobre algo».
 * Cierto, pero resuelto a la altura equivocada — `--bg` es EL COLOR DEL SUELO
 * QUE ESTE CAJÓN ESTÁ TAPANDO. Medido en el parque: con el velo del 45 %, el
 * contenido de detrás queda en #0B1114 y el cajón salía #0E1519. Tres niveles de
 * gris entre lo que está delante y lo que está debajo, o sea que la ficha no se
 * leía como algo que se ha puesto encima: se leía como otra página.
 *
 * La respuesta no es darle un color: es SUBIRLE LA ESCALERA ENTERA. Dentro de
 * este cajón, el suelo es `--delante` y lo que descansa en él sube un peldaño,
 * así que las tarjetas de dentro siguen destacando sobre su suelo —que es lo que
 * el comentario original quería— y además todo el conjunto se lee por delante
 * del panel. Y no hay que tocar ni una regla del contenido: la ficha se pinta
 * con `--panel` y `--hondo` como en cualquier otro sitio, y son los tokens los
 * que se han movido debajo de ella.
 *
 * En claro los tres valores quedan a un peldaño de los de fuera y el cajón se
 * separa por su sombra, que es como se dice la altura en un mundo claro. */
.lean-cajon--lienzo { display: block; background: var(--bg);
  /* EL FILO SE QUEDA, y esto se decidió mirando el tema claro. Iba con
     `border-left: 0` porque el cajón se pintaba con el color del suelo y una raya
     sola no arreglaba nada. Ahora que la escalera sube, el borde es la otra mitad
     del gesto — y en CLARO es la mitad que trabaja: allí el suelo del cajón sigue
     siendo `--bg` (por encima del blanco no hay nada) y lo único que dice que
     está delante son su sombra, el velo y este filo. */
  border-left: 1px solid var(--linde-alto);
  padding: var(--s5) var(--s5) var(--s7); }
/* Y LA ESCALERA SUBE SÓLO EN LOS TEMAS OSCUROS, que es donde estaba rota.
 *
 * En CLARO no hace falta y sería imposible: el suelo del cajón es #F5F8FA y sus
 * tarjetas son blancas, o sea que la escalera de dentro ya existe y por encima
 * del blanco no hay nada. Allí lo que dice que el cajón está delante es su
 * sombra y el velo, que es como se dice la altura en un mundo claro.
 *
 * En OSCURO sí, y es la corrección que se ve: el cajón entero sube un peldaño y
 * conserva sus tres alturas propias —suelo, tarjeta y receptáculo—, así que la
 * ficha se lee por delante del panel sin que haya que tocar una sola regla de su
 * contenido. Se mueven los tokens debajo de ella.
 *
 * Va con los DOS selectores de tema, como manda la casa: un valor que sólo
 * existe dentro de un `@media` desaparece cuando alguien elige el tema a mano. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lean-cajon--lienzo {
    --bg: #1A252B; --panel: #223037; --panel-2: #283740; --hondo: #141F24; --line: #2E3D45;
  }
}
:root[data-theme="dark"] .lean-cajon--lienzo {
  --bg: #1A252B; --panel: #223037; --panel-2: #283740; --hondo: #141F24; --line: #2E3D45;
}
:root[data-theme="dark"][data-negro] .lean-cajon--lienzo {
  --bg: #1B1C1E; --panel: #242527; --panel-2: #2A2B2D; --hondo: #151617; --line: #34363A;
}

/* --- lean-statusbar: el estado de una herramienta --------------------------
   Para superficies que NO son una página: una consola, un panel de trabajo, una
   ventana de herramienta. Ahí un encabezado de sección (antetítulo + título +
   barra de acento) pesa el doble de lo que dice y roba el sitio a los datos,
   que es lo único que se ha venido a ver.

   Lo que hace falta saber es otra cosa: si esto está vivo, de cuándo es lo que
   estoy mirando, y qué puedo hacer. Una línea. */
.lean-statusbar { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  min-height: 30px; padding-bottom: var(--s3); margin-bottom: var(--s4);
  border-bottom: 1px solid var(--line); font-size: var(--fs-sm); color: var(--muted); }
.lean-statusbar__estado { display: inline-flex; align-items: center; gap: 6px; color: var(--ink); font-weight: var(--peso-firme); }
/* Aquí vivían el PUNTO de estado (`__punto`, con sus cuatro colores y su
   latido), el separador y el hueco de la derecha. Se han ido enteros, y lo que
   los mató fue quedarse a medias: la barra la usa una sola pantalla —el alcance
   de los umbrales, en Ajustes› Alertas— y para decir un HECHO que no tiene
   estado («Vale para todos los equipos»). Nunca se escribió un `is-ok` en
   ninguna parte, así que el punto no llegó a encenderse una vez.

   Ese punto ya lo hacen dos componentes que sí trabajan y que están escritos
   con la pregunta contestada: `lean-presencia` para una persona o una máquina, y
   `lean-senal` para «aquí hay algo que mirar». La consola tiene el suyo en el
   chasis (`np-testigo`). Un cuarto punto verde no le faltaba a nadie.

   Con él se va `@keyframes lean-latido`, que era su único cliente. */

/* Aquí vivía `lean-meters`/`lean-meter`, el hermano de `lean-vitales` para las
   cifras que son una proporción. Se ha ido: al pasar los tamaños a la escala,
   las dos cifras convergieron solas en `--fs-2xl` y lo único que quedaba
   distinto era la barra, que ahora es `lean-vital__barra`. Su rejilla se
   conservó un tiempo como `lean-vitales--anchas` —el mismo ancho mínimo, algo
   mayor— y también se ha ido: no la llegó a pedir ninguna pantalla.
   Un componente entero por una barra opcional no se sostiene, y con éste el
   catálogo baja de tres formas de pintar «una cifra con su rótulo» a una. */

/* --- lean-senal: «esto tiene algo» ----------------------------------------
   Un solo indicador para toda la aplicación, porque un aviso es la misma cosa
   esté en la lista de equipos o en la de dispositivos de red. Relleno mientras no
   lo ha mirado nadie, hueco cuando ya se revisó y el problema sigue. El texto
   va donde el aviso se lee —la vista de Avisos, la ficha—, no repetido en cada
   fila: en una lista, lo que hace falta es saber CUÁLES miran, no qué les pasa. */
.lean-senal { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  flex: none; vertical-align: middle; background: currentColor; }
.lean-senal--warn { color: var(--warn); }
.lean-senal--down { color: var(--down); }
.lean-senal.is-visto { background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }
/* Lo grave y sin mirar late; lo demás no. Si parpadeara todo, no destacaría nada. */
.lean-senal--down:not(.is-visto) { animation: lean-senal-latido 1.6s ease-in-out infinite; }
@keyframes lean-senal-latido { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
/* Cuando se puede pulsar para darlo por revisado. */
button.lean-senal { border: 0; padding: 0; cursor: pointer; }
@media (prefers-reduced-motion: reduce) { .lean-senal--down:not(.is-visto) { animation: none; } }

/* --- lean-haciendo: alguien está EJECUTANDO algo, y cómo va ----------------
   Un anillo alrededor de la cara de quien lo lanzó. Envuelve un avatar y no lo
   sustituye: la cara contesta QUIÉN y el anillo contesta CÓMO VA, que son las
   dos preguntas que se hacen al entrar en una pantalla donde ya hay algo en
   marcha. Sale en tres sitios: una orden en la portada, una medición y un
   escaneo en el zócalo de la consola, y el escaneo en el mando de Red.

   POR QUÉ ES UN COMPONENTE Y NO UNA VARIANTE DEL ANILLO QUE YA HABÍA:

   El panel tenía UN SOLO gesto —un anillo de marca que late— diciendo dos cosas
   distintas: «alguien está editando esta ficha» (la presencia) y «alguien está
   midiendo» (Diagnóstico). Son cosas que no se parecen: una es estar delante y
   la otra es un trabajo con principio, final y duración. Con el mismo gesto,
   quien mira no puede saber si le da tiempo a irse a por un café.

   LA LEY DEL ANILLO, y ésta es la regla para lo que venga:

     · LATE     → presencia. Alguien está AHÍ. Nada que terminar ni esperar.
     · AVANZA   → trabajo con plazo: el arco dice por dónde va.
     · GIRA     → trabajo sin plazo: «trabajando», sin prometer un porcentaje.
     · RESBALA  → NO AVANZA. Lo intenta —un paso corto— y vuelve a donde estaba.
     · HALO     → acaba de salir BIEN. Sólo eso: un halo que se expande no
                  puede significar otra cosa en esta interfaz.

   LOS ESTADOS (19/9/26), y por qué no bastaban tres.

   Hasta la 1.153 el anillo sabía decir «avanza», «gira» y «terminó», y con eso
   una orden que nadie recogía giraba UNA HORA igual que una que se estaba
   haciendo, una medición con el agente caído seguía avanzando con el reloj del
   navegador, y una orden fallida se despedía con el halo de «hecha» un instante
   antes de aparecer abajo como fallo. Ahora son cinco mientras dura y tres
   finales, y los declara quien lo usa —`Lean.haciendo(el, estado)`— sin animar
   nada por su cuenta:

     cola       nadie lo ha empezado. Pista PUNTEADA y ningún arco: nada gira
                porque nada trabaja.
     avanza     con plazo; el arco mide lo hecho, anclado arriba.
     sin-plazo  gira a su ritmo, 1,1 s la vuelta (ver `--t-giro` en tokens).
     alarga     tarda más de lo normal: gira a menos de la mitad. Sin color,
                porque todavía no pasa nada malo.
     atasca     no avanza: se para, pasa a ÁMBAR y resbala. El ámbar es el
                color de que algo va mal (ver la gramática del color).
     ─ finales ─
     hecha         cierra la vuelta entera y el halo remata (`is-cerrando`).
     fallida       se para en seco, ROJO, y el arco se DESENROLLA hacia atrás
                   hasta vaciarse; la cara se apaga (`is-fallo`). Sin halo.
     sin-noticias  el giro se apaga, la pista se deshace en puntos y todo se
                   desvanece (`is-suelta`): el panel dejó de saber, y no se
                   cierra ni se rompe lo que no se sabe cómo acabó.

   LA GEOMETRÍA DEL ARCO NO ESTÁ EN ESTA HOJA, y es a propósito. El paso de un
   estado a otro tiene que salir de DONDE ESTABA el arco —su ángulo, su ritmo y
   su largo en ese fotograma— y eso una animación de CSS no lo sabe: cambiar de
   `animation` reinicia desde el primer fotograma, que es el salto que se veía.
   Lo lleva un motor en `lean.js` (ver `Lean.haciendo`), que persigue objetivos
   con suavizado en vez de reproducir curvas. Aquí queda lo que es de la hoja:
   la caja, los colores de cada estado, el halo, la entrada y la salida. */
.lean-haciendo { position: relative; display: inline-flex; }
.lean-haciendo__anillo {
  position: absolute; inset: -4px; pointer-events: none;
  /* Para que el arco empiece ARRIBA y no a las tres en punto, que es donde
     empieza un círculo en SVG y donde nadie espera que empiece un progreso. */
  transform: rotate(-90deg);
}
.lean-haciendo__anillo circle { fill: none; stroke-width: 2.5; }
/* El color y la trama de la pista SÍ se interpolan en CSS: son un cambio de
   estado de un solo tramo (sólida ↔ punteada), sin posición de la que partir.
   Punteada → sólida es el ENGANCHE (alguien lo ha recogido) y sólida →
   punteada es perder la pista. Los mismos puntos en los dos sentidos. */
.lean-haciendo__pista {
  stroke: var(--line); stroke-dasharray: 100 0;
  transition: stroke var(--t) var(--ease), stroke-dasharray var(--t) var(--ease), opacity var(--t) var(--ease);
}
.lean-haciendo__arco {
  stroke: var(--brand); stroke-linecap: round;
  /* Arranca vacío: hasta que el motor lo ve no hay nada que dibujar, y un
     arco pintado por defecto asomaría un fotograma antes que la cara. */
  stroke-dasharray: 0 100; opacity: 0;
  transition: stroke var(--t) var(--ease);
}
/* Y DENTRO DE UN MANDO SE CIÑE, porque ahí hay un filo y en su casa no lo había.
 *
 * `lean-haciendo` nació para una barra de estado —el zócalo de la consola de un
 * equipo—, donde el anillo flota sobre el fondo y puede permitirse cuatro
 * píxeles de aro. Metido en `lean-mando__curso` la caja es un CONTROL: 30 px de
 * alto con un borde de 1, o sea 28 de hueco, y el anillo mide 26 (18 de cara más
 * 4 de aro por lado). Medido en el mando de Red: **queda 1 px de aire por arriba
 * y por abajo**, y a esta escala eso no se lee como holgura, se lee como que la
 * circunferencia toca el borde. Encima el mando lleva `overflow: hidden`
 * mientras trabaja, así que el halo con el que el anillo se despide al terminar
 * se recortaba contra ese mismo filo.
 *
 * Con el aro a dos píxeles el conjunto baja a 22 y quedan 3 de holgura por lado,
 * que es lo que hace falta para que dos formas redondas concéntricas no parezcan
 * rozarse. La cara no se toca: encogerla emborronaría las iniciales, que es lo
 * único que hay que poder leer ahí dentro. */
.lean-mando__curso .lean-haciendo__anillo { inset: -2px; }

/* LOS COLORES DE CADA ESTADO. Van con `.lean-haciendo` delante —dos clases— para
   pesar lo mismo que las reglas de la aplicación que tiñen el arco por su
   cuenta (la medición propia va en gris: `.contest-quien.is-mia`), y ganarles
   por orden de carga: esta hoja va después. Un atasco o un fallo en gris no se
   leería, y ésos son justo los dos que hay que ver. */
.lean-haciendo.lean-haciendo--cola .lean-haciendo__pista { stroke: var(--muted); stroke-dasharray: 1.4 4.85; }
.lean-haciendo.lean-haciendo--atasca .lean-haciendo__arco { stroke: var(--warn); }
.lean-haciendo.lean-haciendo--atasca.lean-haciendo--cola .lean-haciendo__pista {
  stroke: color-mix(in srgb, var(--warn) 70%, var(--line));
}
.lean-haciendo.is-fallo .lean-haciendo__arco { stroke: var(--down); transition-duration: var(--t-fast); }
.lean-haciendo.is-fallo .lean-haciendo__pista { stroke: color-mix(in srgb, var(--down) 40%, var(--line)); }
/* Lo que nunca llegó a empezar se deshace en sus propios puntos. */
.lean-haciendo.is-fallo.lean-haciendo--cola .lean-haciendo__pista { stroke-dasharray: .1 6.15; opacity: 0; }
/* La cara se apaga en el fallo: el trabajo no salió, y la noticia ya no es
   quién lo lanzó sino que no se hizo. No desaparece: sigue siendo de alguien. */
.lean-haciendo > :not(.lean-haciendo__anillo) { transition: opacity var(--t) var(--ease), filter var(--t) var(--ease); }
.lean-haciendo.is-fallo > :not(.lean-haciendo__anillo) { opacity: .55; filter: grayscale(.6); }
/* Perder la pista es el enganche al revés: la pista vuelve a sus puntos y se
   va. Más despacio que todo lo demás —el doble de `--t`, y después de que los
   puntos se hayan formado—, porque es la única despedida que no cuenta un
   hecho sino que el panel ha dejado de saberlo. */
.lean-haciendo.is-suelta .lean-haciendo__pista {
  stroke-dasharray: 1.4 4.85; opacity: 0;
  transition: stroke-dasharray var(--t) var(--ease), opacity calc(var(--t) * 2) var(--ease) var(--t);
}

/* CÓMO ENTRA Y CÓMO SE VA, que no es adorno: es lo que lo hace legible.

   Esto aparece y desaparece SOLO, sin que quien mira haya pulsado nada —el
   trabajo lo puede haber lanzado un compañero desde otro sitio—. Algo que se
   materializa de golpe en una barra de estado se lee como un fallo de pintado, y
   algo que desaparece de golpe deja la duda de si terminó o si el panel se ha
   equivocado.

   TRES REGLAS, y de ellas sale que se sienta bien hecho y no recargado:

   1. LOS TIEMPOS SON LOS DE LA CASA. `--t-entra` para entrar, `--t-sale` para
      salir, `--t` para lo que dibuja un dato. El motor del arco usa los suyos
      —son constantes de tiempo de un suavizado, no duraciones— y están
      explicados uno por uno en `lean.js`.

   2. SE VA MÁS DEPRISA DE LO QUE VIENE. Entrar es una noticia y hay que poder
      leerla; irse es quitar de en medio algo que ya no dice nada, y ahí
      cualquier demora se siente como que la interfaz va lenta.

   3. ESCALONADO, NO SIMULTÁNEO. Primero llega la cara —quién— y un instante
      después se traza el anillo —cómo va—. Es el orden en que se leen las
      dos cosas; el motor espera `--t-entra` antes de trazar.

   Y LA SALIDA SON DOS TIEMPOS, en este orden, porque cuentan el final: primero
   el gesto del final —cerrar, desenrollar o perder la pista— y sólo después
   `is-fin` lo despide. Desvanecerlo a mitad de gesto diría otra cosa. */
.lean-haciendo { animation: lean-haciendo-entra var(--t-entra) var(--ease-entra) both; }
@keyframes lean-haciendo-entra {
  from { opacity: 0; transform: scale(.82); }
  to   { opacity: 1; transform: none; }
}
/* El remate: un halo que sale del anillo cerrado y se desvanece. Es el MISMO
   gesto que `lean-presencia` usa para decir «vivo», reutilizado a propósito en
   vez de inventar otro: en esta interfaz, un halo que se expande significa
   siempre lo mismo —aquí ha pasado algo ahora mismo, y ha salido bien—. Por eso
   el fallo y la pérdida de pista NO lo llevan. */
.lean-haciendo.is-cerrando::after {
  content: ""; position: absolute; inset: -4px; border-radius: 50%;
  box-shadow: 0 0 0 1.5px var(--brand);
  animation: lean-haciendo-remate var(--t) var(--ease) both;
}
.lean-mando__curso .lean-haciendo.is-cerrando::after { inset: -2px; }
@keyframes lean-haciendo-remate {
  from { transform: scale(1);    opacity: .7; }
  to   { transform: scale(1.45); opacity: 0; }
}
/* `both` es correcto aquí: es una SALIDA y lo que retiene al acabar es el estado
   final —ya ido—. El `both` que muerde es el de una entrada, que deja la propiedad
   gobernada para siempre y mata cualquier transición posterior. */
.lean-haciendo.is-fin { animation: lean-haciendo-sale var(--t-sale) var(--ease-sale) both; }
@keyframes lean-haciendo-sale {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(.82); }
}

/* Quien pide menos movimiento ve el estado, no el gesto: el motor pone cada
   estado en su sitio sin recorrido —el arco quieto, en su color—, y la forma
   basta para leerlo: pista punteada = en cola; arco ámbar = no avanza; arco
   vacío y cara apagada = fallida. La entrada y la salida son un fundido. */
@media (prefers-reduced-motion: reduce) {
  .lean-haciendo__pista, .lean-haciendo__arco,
  .lean-haciendo > :not(.lean-haciendo__anillo) { transition: none; }
  .lean-haciendo.is-cerrando::after { animation: none; opacity: 0; }
  .lean-haciendo { animation: lean-haciendo-fundido var(--t-entra) linear both; }
  .lean-haciendo.is-fin { animation: lean-haciendo-fundido var(--t-fast) linear reverse both; }
  @keyframes lean-haciendo-fundido { from { opacity: 0; } to { opacity: 1; } }
}

/* --- lean-presencia: si la máquina está ahí ahora mismo ---------------------
   El punto que dice si un equipo responde. Vive PEGADO A SU IDENTIDAD —sobre la
   foto del equipo, en la esquina— y no en una columna propia: en una lista de
   doscientas filas, una píldora que repite «En línea» doscientas veces gasta
   ciento veinte píxeles de ancho para decir lo que dice un punto de diez.

   Tres estados y nada más, porque son tres preguntas distintas:

     · `is-ok`    responde ahora. Verde, con un halo que respira: es el único
                  estado VIVO y el respirar es lo que lo distingue de un punto
                  verde pintado en una captura de pantalla.
     · `is-off`   tiene agente y no contesta. GRIS, no rojo: un portátil apagado
                  por la noche no es una incidencia, y gastar el rojo aquí lo
                  desgasta para cuando algo va mal de verdad —que es lo que
                  marca `lean-senal`, y con el que este punto convive—.
     · `is-sin`   no hay agente que preguntar. Anillo hueco: no es que no
                  conteste, es que no hay nadie a quien llamar.

   Y un modificador encima de cualquiera de los tres: `is-trabajando`, un anillo
   que gira alrededor del punto mientras el equipo está aplicando algo (una
   actualización) o midiendo. Sustituye a la rueda y a la barra que antes vivían
   en otra columna: lo que está pasando le pasa al EQUIPO, así que se dice sobre
   el equipo.

   El halo y el anillo van en pseudoelementos y animan sólo `transform` y
   `opacity`, que es lo que el compositor hace sin repintar: con cien puntos
   latiendo a la vez, animar el tamaño costaría un reflow por fotograma. */
.lean-presencia {
  position: relative; display: inline-block; flex: none;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--faint); color: var(--muted);
  /* El aro del color del fondo separa el punto de lo que tenga debajo (la foto
     del equipo), sin lo cual un punto verde sobre una foto clara se pierde. */
  box-shadow: 0 0 0 2px var(--panel);
  /* EL COLOR SE INTERPOLA, y esto no es un adorno: es el único suceso que este
     producto existe para contar y era el único que ocurría sin verse ocurrir.
     Un equipo que se cae pasaba de verde a gris entre dos fotogramas, en una
     lista de doscientos, así que el cambio sólo se notaba comparando con lo que
     uno recordara.

     300 ms y no `--t`: aquí no hay que seguir nada con la vista ni esperar a
     nada, sólo que el cambio dure lo justo para engancharlo por el rabillo del
     ojo. Es de las poquísimas duraciones de la hoja que no sale de un token, y
     va escrita a mano a propósito — no es un tiempo de interfaz, es el de un
     hecho del mundo real llegando a la pantalla.

     Y SÓLO COLOR Y SOMBRA. Ni tamaño ni posición: con ciento setenta puntos en
     pantalla, animar la caja costaría un reflow por fotograma. Es la misma
     regla que ya sigue el halo de aquí abajo. */
  transition: background 300ms var(--ease), box-shadow 300ms var(--ease), color 300ms var(--ease);
}
@media (prefers-reduced-motion: reduce) { .lean-presencia { transition: none; } }
.lean-presencia.is-ok  { background: var(--ok);    color: var(--ok); }
.lean-presencia.is-off { background: var(--faint); color: var(--muted); }
/* `is-aviso`: no contesta Y eso ya es una avería que nadie ha mirado. Es el
   MISMO punto cambiando de color, no un segundo indicador al lado: la diferencia
   entre «apagado» y «se cayó» es de grado, y de grado se habla con el color. */
.lean-presencia.is-aviso { background: var(--warn); color: var(--warn); }
.lean-presencia.is-sin {
  background: var(--panel); color: var(--muted);
  box-shadow: 0 0 0 2px var(--panel), inset 0 0 0 1.5px currentColor;
}
/* El halo del que responde. Sale del punto y se desvanece: dice «vivo» sin
   pedir atención, que es lo que se necesita cuando ciento setenta filas están
   en línea a la vez. */
/* EL HALO EXISTE SIEMPRE Y LO QUE CAMBIA ES SU OPACIDAD, que es lo que le
   permite APAGARSE en vez de dejar de existir. Declarado sólo en `is-ok`, un
   equipo que se caía perdía el pseudoelemento entero y el halo se cortaba a
   mitad de ciclo: justo el tirón que el apagado gradual existe para evitar, y el
   mismo cuidado que `--t-novedad` explica para el latido de lo que acaba de
   llegar. Con `opacity: 0` de base, el navegador tiene de dónde y a dónde ir. */
.lean-presencia::before {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: currentColor; opacity: 0;
  transition: opacity 400ms var(--ease-sale);
}
.lean-presencia.is-ok::before {
  opacity: .35;
  animation: lean-presencia-halo var(--t-vivo) ease-out infinite;
}

/* --- Y LOS DOS SUCESOS: caerse y volver ------------------------------------
   El color dice en qué estado está; estos dos dicen que ACABA DE CAMBIAR, y son
   contrarios a propósito para que se lean sin leyenda: uno colapsa hacia dentro
   y el otro brota hacia fuera. Se ponen una vez y se quitan solos (los aplica
   `Lean.cambioDePresencia`); no son estados, son avisos de un instante.

   NO LLEVAN COLOR PROPIO: heredan el del punto, así que el que se cae usa el
   gris o el ámbar al que va y el que vuelve el verde. Un aro rojo aquí gastaría
   el color de la avería en algo que puede ser un portátil que alguien apaga por
   la noche. */
.lean-presencia.es-baja::after,
.lean-presencia.es-alta::after {
  content: ""; position: absolute; border-radius: 50%; pointer-events: none;
  border: 1.5px solid currentColor;
}
/* `both` en los dos aros: son SUCESOS de un solo uso —la clase se pone y se
   quita sola— y lo que retienen al acabar es el aro ya desvanecido. No hay
   transición posterior que puedan bloquear porque el pseudo deja de existir
   con la clase. El `both` que muerde es el de una entrada permanente. */
.lean-presencia.es-baja::after { inset: -7px; animation: lean-presencia-cae 460ms var(--ease-entra) both; }
.lean-presencia.es-alta::after { inset: 0;    animation: lean-presencia-vuelve 520ms var(--ease-entra) both; }
@keyframes lean-presencia-cae    { from { transform: scale(1);  opacity: .75; } to { transform: scale(.45); opacity: 0; } }
@keyframes lean-presencia-vuelve { from { transform: scale(.4); opacity: .9;  } to { transform: scale(3);   opacity: 0; } }
@keyframes lean-presencia-halo {
  0%   { transform: scale(1);   opacity: .35; }
  70%  { transform: scale(2.6); opacity: 0; }
  100% { transform: scale(2.6); opacity: 0; }
}
/* Trabajando: el anillo gira alrededor del punto, del color de la marca. Es el
   mismo gesto para «se está actualizando» y «está midiendo», porque para quien
   mira la lista son la misma noticia: a este equipo le está pasando algo ahora.
   Qué es exactamente lo dice la línea de debajo del nombre. */
.lean-presencia.is-trabajando::after {
  content: ""; position: absolute; inset: -5px; border-radius: 50%;
  border: 1.5px solid transparent; border-top-color: var(--brand);
  border-right-color: var(--brand);
  animation: lean-presencia-giro var(--t-giro) linear infinite;
}
.lean-presencia.is-trabajando::before { animation: none; opacity: 0; }
@keyframes lean-presencia-giro { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .lean-presencia.is-ok::before { animation: none; opacity: .28; }
  .lean-presencia.is-trabajando::after { animation: none; border-color: var(--brand); opacity: .55; }
  /* Los dos sucesos también: quien pide menos movimiento ve el color nuevo, que
     es el dato; el aro que colapsa o brota es sólo el subrayado. */
  .lean-presencia.es-baja::after, .lean-presencia.es-alta::after { animation: none; opacity: 0; }
}

/* --- lean-brandmark: el logo de un tercero ---------------------------------
   Un logo reconocible sustituye al texto; un icono genérico al lado del texto
   sólo hace ruido. Cuando no hay logo, el nombre en un chip: nunca un hueco
   vacío, que se lee como una imagen que no ha cargado.

   El dibujo se pinta como MÁSCARA, no como imagen. Suena raro para un logo,
   pero es lo que resuelve los dos problemas a la vez:

     · el color no vive en el archivo, lo pone quien lo usa. Así el mismo logo
       vale monocromo (hereda el gris del dato que acompaña) y a todo color
       (donde el logo es lo que identifica), sin servir dos versiones;
     · y el color de marca puede corregirse por tema. El de Apple y el de ASUS
       es el negro, que sobre fondo oscuro no se ve; el de Sony es el blanco,
       que sobre claro tampoco. El backend manda los dos tonos ya resueltos
       (ver internal/marcas) y aquí sólo se elige.

   Los logos salen de /assets/marca/<slug>.svg, del catálogo que va dentro del
   binario. Se ponen con `style="--marca:url(...)"`, que es lo que un logo
   necesita: no es una decisión de diseño repetible, es un dato de la fila.

   Y la CAJA tiene la forma del logo, no la del lienzo. Todos vienen en un
   cuadrado de 24×24, pero lo que dibujan dentro no es cuadrado: el de NVIDIA
   llena el cuadrado y el de AMD es un rótulo cuatro veces más ancho que alto.
   Metidos los dos en una caja cuadrada de 18 píxeles, el rótulo se queda en una
   franja de cuatro píxeles que no se lee — y un logo que no se lee es peor que
   el nombre escrito al que vino a sustituir. La proporción la mide el backend
   trazado a trazado y llega en `--marca-ar`. */
.lean-brandmark {
  --marca-ar: 1;      /* ancho/alto del dibujo; lo pone quien pinta */
  /* El alto NOMINAL, y el que de verdad se pinta. Están separados porque `--caja`
     necesita los dos: la caja mide siempre lo mismo (`--marca-base`) y el dibujo
     de dentro varía. Fuera de `--caja` son el mismo número, y quien quiera otro
     alto sigue poniendo `--marca-alto` como siempre. */
  --marca-base: 18px;
  --marca-alto: var(--marca-base);
  /* Hasta cuántas veces su alto puede estirarse un rótulo. Se sube donde el
     logo es lo único que hay que mirar (un selector de logos), no donde
     acompaña a un dato y tiene que caber en su renglón. */
  --marca-max: 2.6;
  /* Alineado por el centro y no por la línea base: un rótulo ancho llena su
     caja por el medio, y colgarlo de la línea base lo deja flotando por encima
     del texto al que acompaña. */
  display: inline-block; vertical-align: middle; flex: none;
  height: var(--marca-alto);
  /* Con topes por los dos lados: un rótulo muy largo no puede comerse la línea,
     y un logo estrecho necesita un mínimo para no quedar en un hilo. */
  width: clamp(calc(var(--marca-alto) * .55),
               calc(var(--marca-alto) * var(--marca-ar)),
               calc(var(--marca-alto) * var(--marca-max)));
  background-color: currentColor;
  /* El anclaje del dibujo dentro de su caja. Centrado, que es lo que quiere un
     logo que acompaña a un texto: el hueco sobrante se reparte a los dos lados y
     el conjunto se lee como una pieza. Se deja en una variable porque una
     COLUMNA de logos querría el canto izquierdo —ahí lo que se compara es dónde
     arranca cada uno—; hoy no hay ninguna, así que no la cambia nadie. */
  --marca-anclaje: center;
  -webkit-mask: var(--marca) var(--marca-anclaje) / contain no-repeat;
          mask: var(--marca) var(--marca-anclaje) / contain no-repeat;
}
.lean-brandmark--lg { --marca-base: 26px; }

/* --junto: el logo va DELANTE DE UN TEXTO y tiene que separarse de él.
   Sin esto el dibujo queda pegado a la primera letra —«[msi]MS-7E09»—, que es
   lo que pasaba en el modelo de las tres fichas y en la columna del albarán.

   VA EN LA PIEZA Y NO EN QUIEN LA COLOCA, y ése es el cambio de criterio. El
   hueco se ponía por contexto: una regla para `.hw-linea`, otra para
   `.lean-fact__sub`, y el `gap` de `.lean-fact__v` para lo demás. Con eso el
   logo nace PEGADO y se despega sólo donde alguien se acordó de añadir el
   selector — y cuando el marcado cambió, dejó de acordarse: al envolver el
   modelo en un `<a>` para poder pulsarlo, el logo y su texto pasaron a ser el
   MISMO hijo del flex, y un `gap` no separa lo que hay dentro de un hijo.

   Ahora se pide con el logo y viaja con él, así que un sitio nuevo funciona sin
   que nadie tenga que saber esta historia. Donde el contenedor ya separa por su
   cuenta se anula, que es una regla en `app.css` y no diez aquí. */
.lean-brandmark--junto { margin-right: 6px; }

/* --caja: todos los logos en la MISMA caja, en vez de medir cada uno por su alto
   y crecer a lo ancho lo que le pida su forma.

   Es lo que hace falta cuando se ENUMERAN logos —una columna de catálogo, la
   rejilla donde se elige uno—. Midiendo por el alto, la caja de un rótulo ancho
   («Hewlett-Packard» a 4:1) es cuatro veces la de un logo cuadrado (el de
   NVIDIA), y una columna con los dos parece descuadrada: no es que un logo sea
   más importante, es que su dibujo es más largo. Además, el más ancho se salía
   de la columna que le habían reservado.

   Con la caja fija, cada dibujo se encoge dentro de ella hasta caber —la máscara
   ya va con `contain`—, así que el rótulo ancho baja de alto y el cuadrado se
   queda como está. Es la convención de cualquier muro de logos.

   LA CAJA ES FIJA Y EL DIBUJO VARÍA DENTRO, que es lo que `contain` a secas no
   hacía. Con la caja fija y `contain`, el dibujo se ajusta al lado que primero
   tope: un rótulo 4:1 llena el ancho y se queda bajo, y uno cuadrado llena el
   alto y ocupa una cuarta parte del ancho. Medido en la lista de marcas: el
   rótulo de Acer pintaba 94×22 px y la manzana de Apple 21×26, o sea CUATRO
   VECES menos superficie. No se lee como «este logo es más ancho», se lee como
   que unas marcas están en grande y otras en pequeño.

   Ahora el alto del dibujo sube hasta 1,3 veces el nominal cuando el logo es
   compacto, y baja sólo lo justo para que el ancho quepa cuando es un rótulo
   largo (`base × caja / ar`). La CAJA no cambia: todas las filas de la lista
   miden lo mismo, que es lo que se rompería dejando crecer el elemento.

   Y CENTRADO DENTRO DE SU CAJA, que es lo contrario de lo que esto hizo durante
   un rato y conviene saber por qué cambió.

   Estuvo anclado a la IZQUIERDA mientras el logo vivía en una columna propia:
   ahí lo que se compara es dónde ARRANCA cada uno, el nombre está en otra
   columna, y un logo estrecho centrado dejaba un hueco enorme delante que
   descuadraba la lista. En cuanto el logo pasó a ir PEGADO al nombre, la
   comparación es otra: la caja mide siempre lo mismo, así que el nombre arranca
   siempre igual, y lo único que varía es el reparto del hueco DENTRO de la
   caja. Anclado a la izquierda, ese hueco se acumula todo a la derecha y cada
   logo queda a una distancia distinta de su palabra.

   Centrado, el hueco se reparte y el conjunto «logo + nombre» se lee como una
   pieza. La regla: se ancla por un canto lo que se compara EN COLUMNA; se centra
   lo que acompaña a un texto. */
.lean-brandmark--caja {
  width: calc(var(--marca-base) * var(--marca-caja, 2.2));
  height: calc(var(--marca-base) * 1.3);
  -webkit-mask-size: auto min(calc(var(--marca-base) * 1.3), calc(var(--marca-base) * var(--marca-caja, 2.2) / var(--marca-ar)));
          mask-size: auto min(calc(var(--marca-base) * 1.3), calc(var(--marca-base) * var(--marca-caja, 2.2) / var(--marca-ar)));
}

/* Dentro de un botón, un logo mide lo que mide un icono (1.15em) y se alinea
   con el gap del botón, no con márgenes propios. Así «logo + palabra» y «icono
   + palabra» son la misma pieza y se pueden intercambiar sin que el botón
   cambie de altura ni el texto baile. Lo usa el botón de AnyDesk de la ficha. */
.lean-boton .lean-brandmark { --marca-alto: 1.15em; }

/* Pegado a un texto, como una palabra más de la frase.
 *
 * El alto va en `em`, así que el logo escala con la letra que lo acompaña en vez
 * de quedarse en un tamaño fijo: en una línea de 13px, un logo de 18px sobresale
 * por arriba y por abajo y hace que el renglón cojee. Con esto mide lo mismo que
 * una mayúscula y la línea vuelve a leerse seguida. */
.lean-brandmark--inline { --marca-alto: 1.05em; }

/* Con el color de la marca, para cuando el logo es el protagonista y no el
   acompañante: el modelo del equipo, el acceso remoto, el catálogo. */
.lean-brandmark--color { background-color: var(--marca-claro, currentColor); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lean-brandmark--color { background-color: var(--marca-oscuro, var(--marca-claro, currentColor)); }
}
/* Y el mismo juego cuando el tema se elige a mano (ver tokens.css). */
:root[data-theme="dark"] .lean-brandmark--color { background-color: var(--marca-oscuro, var(--marca-claro, currentColor)); }


/* Sin logo: el nombre. Es un chip de texto, así que no lleva máscara ninguna. */
.lean-brandmark--texto {
  -webkit-mask: none; mask: none;
  display: inline-flex; align-items: center;
  width: auto; height: auto; min-width: 0; vertical-align: baseline;
  padding: 1px 6px; border-radius: var(--r-xs);
  background: var(--panel-2); border: 1px solid var(--line);
  font-size: var(--fs-xs); font-weight: var(--peso-fuerte); letter-spacing: var(--letra-codigo); color: var(--muted);
}
/* Y DENTRO DE UNA COLUMNA DE LOGOS ocupa la caja y pierde el recuadro.
   El chip existe para no dejar un hueco vacío donde debería ir un dibujo, y
   suelto se gana su marco. Enumerando, ese marco es lo único con borde de toda
   la columna —y, peor, el chip mide lo que miden sus letras: en la lista de
   marcas, «Desconocido» era la única fila cuyo nombre no arrancaba en el mismo
   píxel que las otras nueve, 25 px antes. Aquí el hueco ya lo reserva la caja,
   así que basta el texto, centrado como el dibujo al que sustituye. */
.lean-brandmark--caja.lean-brandmark--texto {
  width: calc(var(--marca-base) * var(--marca-caja, 2.2));
  height: calc(var(--marca-base) * 1.3);
  justify-content: center;
  padding: 0; background: none; border: 0;
}

/* Aquí vivía `lean-rowactions`, las acciones de una fila que asomaban al pasar
   por encima. Se quedó sin ningún sitio donde vivir y nadie lo retiró.

   Los dos que lo usaban se fueron por su cuenta y por buenas razones, cada uno
   contada donde pasó: la lista de oficinas de Empresas cambió a un desplegable
   —el componente escondía a sus hijos con `tr:hover`, y una oficina no es un
   `<tr>` sino un `<div>`, así que las acciones NO SE VEÍAN NUNCA salvo
   tabulando (ver `empOficinaFila` en `empresas.js`)—; y las tablas de la consola
   tienen su propio mando, `np-minimando`, porque dentro del aparato un botón de
   32 px descuadra la fila.

   Lo que queda es la lección: `tr:hover` ata el componente a una TABLA, y eso
   no estaba escrito en ninguna parte. Quien lo metió en un `div` no hizo nada
   raro; el componente prometía más de lo que podía cumplir. */

/* Picker: elegir de una lista larga escribiendo. Ver lean.js. -------------- */
.lean-picker { position: relative; }
.lean-picker__campo { width: 100%; }
.lean-picker__lista {
  position: absolute; z-index: var(--z-anclado); left: 0; right: 0; top: calc(100% + 4px);
  max-height: 244px; overflow-y: auto; padding: 4px;
  /* Desplegable anclado: `--alzado` + `--linde-alto`, y la sombra del sistema en
     vez de una escrita a mano (era el único literal de color de esta hoja). */
  background: var(--alzado); border: 1px solid var(--linde-alto); border-radius: var(--r-s);
  box-shadow: var(--shadow-lg);
}
.lean-picker__op {
  display: flex; align-items: baseline; gap: var(--s3);
  padding: 7px 10px; border-radius: var(--r-xs); cursor: pointer;
  font-size: var(--fs-md); color: var(--ink);
}
.lean-picker__op.is-activa { background: var(--panel-2); }
.lean-picker__op.is-elegido { font-weight: var(--peso-titulo); }
/* La pista (el número de modelo, la marca) va a la derecha y en pequeño: dice
   cuál es cuál cuando dos se llaman parecido, sin competir con el nombre. */
.lean-picker__pista { margin-left: auto; color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.lean-picker__vacio { padding: 10px; color: var(--muted); font-size: var(--fs-md); }
/* La salida del estado vacío: crear lo que se estaba buscando. Va pegada al
   mensaje, ocupando la lista entera, porque es lo único que queda por hacer ahí
   dentro. */
.lean-picker__accion {
  display: block; width: 100%; text-align: left;
  padding: 10px; border: 0; border-top: 1px solid var(--line);
  background: transparent; color: var(--brand-ink);
  font: inherit; font-size: var(--fs-md); cursor: pointer;
}
.lean-picker__accion:hover { background: var(--al-pasar); }
.lean-picker__accion:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-dentro); }

/* --- lean-multi: el filtro que admite varios ---------------------------------
   Un `lean-select` elige UNO. Éste elige varios de una lista corta, marcando
   casillas, y por fuera se ve igual que un select: en una barra de filtros lo
   que no puede pasar es que un mando parezca de otra familia que el de al lado
   —«Empresa» con caja y flecha, «Oficina» con pinta de botón— porque entonces se
   leen como dos cosas distintas cuando hacen exactamente lo mismo.

   Por dentro es un `lean-dropdown` con casillas dentro de su `lean-menu`, y eso
   no es casualidad: el motor del desplegable (lean.js) cierra el menú al pulsar
   un `button` o un `a` —«un ítem del menú es una acción, no un interruptor»— y
   una casilla no es ni lo uno ni lo otro, así que el menú se queda abierto
   mientras se marcan las que hagan falta. Que es justo lo que hay que poder
   hacer aquí y lo que no hay que poder hacer en un menú de acciones.

   El rótulo del campo lo mantiene lean.js: vacío dice de qué es el filtro
   («Empresa»), con uno dice cuál, y con varios cuántos («2 empresas»). Con la
   lista de los nombres no se puede: caben dos y a la tercera el mando se
   ensancha y descoloca la barra entera cada vez que se marca algo. */
.lean-multi { position: relative; display: inline-block; width: 100%; }
/* Hereda el aspecto de `.lean-input`/`.lean-select` en la propia declaración de
   arriba: aquí sólo va lo que un botón no trae de un campo. */
.lean-multi__campo {
  display: flex; align-items: center; gap: var(--s2);
  width: 100%; padding-right: 34px; cursor: pointer; text-align: left;
}
.lean-multi__txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* La flecha, copiada del select: es el mismo gesto y tiene que ser el mismo
   dibujo. Va en el contenedor y no en el botón para que no la tape el relleno. */
.lean-multi::after {
  content: ""; position: absolute; right: 13px; top: 50%; width: 7px; height: 7px;
  border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
  transform: translateY(-70%) rotate(45deg); pointer-events: none;
}
/* Puesto se ve puesto, igual que en `.lean-select.is-activo`: sin esto no hay
   manera de saber por qué la lista está recortada. */
.lean-multi__campo.is-activo {
  border-color: var(--brand); color: var(--brand-ink); font-weight: var(--peso-firme);
  background: var(--elegido);
}
.lean-multi:has(.is-activo)::after { border-color: var(--brand-ink); }
/* La lista puede ser larga —cuarenta clientes, diez oficinas—, así que se acota
   y hace scroll dentro. El ancho mínimo lo pone `lean-menu`. */
.lean-multi__lista { max-height: 280px; overflow-y: auto; }
.lean-multi__lista .lean-check,
.lean-multi__lista .lean-radio {
  display: flex; width: 100%; padding: 7px 10px; border-radius: var(--r-xs);
  font-size: var(--fs-md);
}
.lean-multi__lista .lean-check:hover,
.lean-multi__lista .lean-radio:hover { background: var(--al-pasar); }
/* LO MARCADO SE VE MARCADO, y no sólo por su casilla.
   En una lista de cuarenta clientes con scroll, la casilla en cian es una marca
   de 14 px que hay que ir a buscar renglón a renglón: la fila entera con el
   lavado de marca contesta «cuáles están puestos» de un vistazo. Es el mismo
   `--elegido` del destino del lateral, que es exactamente lo que esto es. */
.lean-multi__lista .lean-check:has(input:checked) { background: var(--elegido); }
/* En el modo de UNO va igual, con una salvedad: «todas» no se tiñe. Elegirla es
   NO filtrar, así que pintarla como puesta diría que hay un filtro donde no lo
   hay — que es el mismo motivo por el que `multiRefrescar` descarta su valor y
   el campo vuelve a decir su nombre. */
.lean-multi__lista .lean-radio:has(input:checked) { background: var(--elegido); }
.lean-multi__lista .lean-radio:has(input[value=""]:checked) { background: transparent; }
/* Quitar lo marcado, al pie y separado: es lo único de aquí dentro que no es una
   casilla, y sin la línea se leía como una opción más de la lista. */
.lean-multi__limpiar {
  margin-top: var(--s1); padding-top: var(--s1); border-top: 1px solid var(--line);
}


/* --- lean-criba: la barra de filtros de una lista ----------------------------
 *
 * La barra que llevan las listas del panel —Ordenadores, Monitores, Móviles,
 * Pedidos, Redes y Empresas— y que hasta ahora era `lean-toolbar` más un puñado
 * de ajustes en `app.css` por pantalla.
 *
 * QUÉ AÑADE sobre `lean-toolbar`, que es lo que justifica que sea un componente
 * y no tres reglas más:
 *
 *   1. UNA SOLA LÍNEA, SIEMPRE. `lean-toolbar` envuelve, y envolviendo el
 *      bloque de «cómo se mira» caía a un segundo renglón y la tabla bajaba con
 *      él: la misma pantalla medía distinto según los filtros puestos. Aquí lo
 *      que cede es el NÚMERO DE MANDOS A LA VISTA, y los que no caben se van
 *      detrás de uno solo.
 *   2. EL ANCHO DE UN MANDO NO DEPENDE DE LO QUE LLEVE DENTRO. `lean-multi` ya
 *      lo avisa —«con nombres el mando se ensancha al marcar y descoloca la
 *      barra entera»— y le pone un mínimo; sin tope arriba sigue creciendo.
 *   3. SE MIDE. Cuántos caben no es un número por pantalla: es lo que entra.
 *      Lo calcula `criba.js`, y por eso la misma lista enseña cuatro mandos en
 *      un monitor y ninguno en un móvil sin una consulta de ancho a mano.
 *   4. LO QUE SE VA, SE VA. Un mando que desaparece con `hidden` corta en seco;
 *      aquí se pliega y sus vecinos se deslizan a ocupar el hueco, que es lo que
 *      de verdad ha pasado.
 *
 * EL COMPÁS. Un cambio de la barra no son seis animaciones sueltas: es una, con
 * sus piezas escalonadas. Primero se hace el hueco y luego se ocupa — al revés,
 * dos cosas se disputan los mismos píxeles y se lee como un salto. Eso es lo que
 * mide `--releva`. */
/* ── QUIÉN ES EL CONTENEDOR ────────────────────────────────────────────────
   Las consultas de abajo son `@container` y no `@media`, y la diferencia muerde:
   con el lateral abierto en una pantalla de 1.280 px la vista mide 1.040; con el
   cajón del móvil, la misma ventana da 1.280. Una consulta de ventana se
   equivoca en los dos casos.
   Pero un elemento NO PUEDE CONSULTARSE A SÍ MISMO: hace falta que un ancestro
   se declare contenedor, y si no lo hace ninguno, las consultas no se equivocan
   — es que no existen. Así arrancó esto: los dos tramos escritos, el panel sin
   un solo `container-type` cerca, y la barra sin apilar en un móvil de 480 px.
   Va CON NOMBRE para que no la capture cualquier contenedor que alguien declare
   más adentro mañana. La clase se pone en la tarjeta que envuelve la barra. */
.lean-criba-caja { container: criba / inline-size; }
.lean-criba { display: flex; align-items: center; gap: var(--s2); flex-wrap: nowrap; position: relative; }
.lean-criba__acota { display: flex; align-items: center; gap: var(--s2); flex-wrap: nowrap; flex: 1 1 auto; min-width: 0; }
.lean-criba__mira  { display: flex; align-items: center; gap: var(--s2); flex: none; }

/* El ancho de un mando, en un token: `criba.js` lo LEE para repartir, en vez de
   llevarlo escrito por su cuenta. Escrito dos veces, una de las dos envejece — y
   el síntoma es una barra que desborda por unos píxeles, que es de lo que peor
   se tira del hilo. */
.lean-criba { --criba-mando: 156px; --criba-buscar: 232px; }
.lean-criba__mando { position: relative; flex: 0 0 var(--criba-mando); max-width: var(--criba-mando); }
.lean-criba__mando > .lean-multi,
.lean-criba__mando > .lean-boton { width: 100%; }
/* Los de «cómo se mira» no compiten por el sitio: su texto es de la casa y no
   crece con los datos. */
/* ── «CÓMO SE MIRA» NO ES UN FILTRO, Y NO SE PINTA COMO TAL ────────────────
   Un mando de acotar está apagado hasta que acota: encendido significa «por
   esto está recortada la lista». El de agrupar y ordenar SIEMPRE tiene valor
   —siempre se mira de alguna manera— así que encenderlo sería tenerlo encendido
   siempre, y un estado que nunca cambia deja de leerse a los dos días.
   Aquí manda lo que ya dice `lean-multi`: lo puesto se ve puesto. Lo que cambia
   es qué cuenta como puesto. */
.lean-criba__mira .lean-multi__campo.is-activo {
  border-color: var(--line); background: var(--panel); color: var(--ink);
  font-weight: var(--peso-texto);
}
.lean-criba__mira .lean-multi__campo.is-activo:hover { border-color: color-mix(in srgb, var(--ink) 22%, var(--line)); }
.lean-criba__mando--solo { flex: 0 0 auto; max-width: none; }
.lean-criba__mando--solo > * { width: auto; }

/* ── LO QUE SE PLIEGA ───────────────────────────────────────────────────────
   `hidden` corta de golpe. Esto se dobla sobre sí mismo y los vecinos se
   deslizan. El retardo va POR DIRECCIÓN, y ahí está el compás: el que se va no
   espera a nadie; el que llega espera `--releva` a que el hueco exista. */
.lean-criba__mando,
.lean-criba [data-criba-pliega] {
  overflow: hidden;
  transition:
    max-width   var(--t-mueve) var(--asienta) var(--releva),
    flex-basis  var(--t-mueve) var(--asienta) var(--releva),
    opacity     var(--t-entra) var(--ease-entra) var(--releva),
    transform   var(--t-mueve) var(--asienta) var(--releva),
    margin      var(--t-mueve) var(--asienta) var(--releva),
    padding     var(--t-mueve) var(--asienta) var(--releva);
}
/* `max-width: 0` NO lleva un botón a cero: su relleno y su borde siguen
   ocupando, y un flex item trae `min-width: auto`, así que se queda en unos 24
   px. En un contenedor sí funciona —ahí el relleno lo pone lo de dentro, que
   está en `overflow`— y por eso el muñón sólo asoma en los botones sueltos. */
.lean-criba .is-plegado {
  max-width: 0 !important; flex-basis: 0; min-width: 0;
  padding-inline: 0; border-inline-width: 0;
  opacity: 0; transform: scale(.94); margin-right: calc(var(--s2) * -1);
  pointer-events: none;
  transition-delay: 0s;
  transition-duration: var(--t-sale);
  transition-timing-function: var(--ease-sale);
}
/* Lo que este operador NO PUEDE VER no se pliega: es que nunca estuvo. Sin
   gesto, porque no ha pasado nada. */
.lean-criba .is-vetado { display: none !important; }
/* Y el primer pintado tampoco es un gesto: todavía no ha hecho nadie nada. */
.lean-criba.is-quieta * { transition: none !important; animation: none !important; }

/* ── EL BUSCADOR NO CEDE ────────────────────────────────────────────────────
   Al poner un filtro aparece «Quitar los filtros» y, con el buscador elástico,
   lo que encogía era él: el campo donde se escribe cambiaba de tamaño cada vez
   que se marcaba una casilla. Es lo ÚLTIMO que cede, y quien se aparta es un
   mando, que para eso están los de detrás.
   Con el ancho de la VISTA sí cambia, y no es lo mismo: una cosa es un gesto del
   operador y otra el sitio que hay. */
.lean-criba .lean-criba__buscar { flex: 0 0 var(--criba-buscar); width: var(--criba-buscar); max-width: none; min-width: 0; }
/* El atajo, a la vista y callado: es una pista, no un dato. Se va en cuanto el
   campo se queda sin sitio para la pista Y para lo que se escribe. */
.lean-criba__atajo {
  font: var(--peso-medio) 10px/1 var(--mono); color: var(--faint); flex: none;
  border: 1px solid var(--line); border-radius: var(--r-micro);
  padding: 3px 5px; background: var(--panel);
}
@container criba (max-width: 900px) { .lean-criba__atajo { display: none; } }

/* ── LA CUENTA DE UN MANDO ──────────────────────────────────────────────────
   Cuántos hay elegidos, en su propia pieza. Se intentó que el chevron SE
   CONVIRTIERA en ella —los dos estados en los mismos 16 px— y el disco salía de
   6,8 px: un número no se lee ahí en ningún tema, y los dos estados apilados se
   pisaban, así que el chevron se quedaba a medio girar.
   Son dos trabajos y son dos piezas: el chevron dice «esto se despliega», y eso
   es verdad siempre; la cuenta dice «cuántos has elegido», y eso sólo a veces.
   El par de color se invierte solo con el tema —en claro la marca es oscura
   sobre papel claro, en oscuro y en negro al revés— y por eso se lee en los tres
   sin una sola excepción escrita a mano. */
.lean-criba__cuenta {
  display: grid; place-items: center; flex: none;
  height: 18px; border-radius: var(--r-pill);
  background: var(--brand-ink); color: var(--bg);
  font: var(--peso-titulo) 11px/1 var(--ui); font-variant-numeric: tabular-nums;
  /* Plegada se come su propio hueco: un flex reserva el `gap` entre hermanos
     aunque el hermano mida cero, y esos ocho píxeles fantasma descuadran el
     reparto, que cuenta píxeles. */
  /* `min-width: 0` en el reposo, y no es de adorno: en CSS `min-width` GANA a
     `max-width`, así que una pastilla vacía con `min-width: 18px` seguía midiendo
     lo suyo por mucho `max-width: 0` — once píxeles de hueco en cada mando, en
     una barra que reparte contando píxeles. */
  min-width: 0; max-width: 0; padding-inline: 0; margin-left: calc(var(--s2) * -1);
  opacity: 0; transform: scale(.6); transform-origin: right center;
  transition:
    max-width var(--t) var(--asienta), padding var(--t) var(--asienta),
    margin    var(--t) var(--asienta), transform var(--t) var(--asienta),
    opacity   var(--t-fast) var(--ease-entra);
}
/* `:not(:empty)`, y sin eso salía un punto azul vacío. Un mando puede estar
   PUESTO sin tener cuenta que enseñar —el de «cómo se mira» siempre lo está, y
   uno de acotar con un solo valor dice CUÁL, no cuántos— y la píldora se
   encendía igual: dieciocho píxeles de color que no significan nada, justo al
   lado de los que sí. */
.is-activo .lean-criba__cuenta:not(:empty) {
  min-width: 18px; max-width: 40px; padding-inline: 5px; opacity: 1; transform: none;
  margin-left: 0; margin-right: var(--s2);
}
.lean-criba__mando--solo .is-activo .lean-criba__cuenta:not(:empty) { margin-right: 0; margin-left: var(--s2); }

/* ── EL CHEVRON ES UN TRAZO, NO DOS BORDES ─────────────────────────────────
   El truco de la caja con `border-right`/`border-bottom` girada 45° funciona
   mientras esté quieta. Al girarla 180° se rompe: el centro de esa caja no es el
   centro visual de la punta —media caja está vacía— así que la flecha se va de
   sitio a la ida y vuelve deforme.
   Y el dibujo es el que YA APUNTA ABAJO. Con uno que apunte a la derecha habría
   que ir de 90deg a -90deg, y CSS interpola esos dos números en línea recta:
   PASA POR 0, o sea que a mitad del giro la flecha apunta a un lado. Media
   vuelta exacta, un solo sentido, ningún estado intermedio que signifique otra
   cosa. Y nada gira la caja: gira el trazo de dentro y sólo él. */
.lean-criba__flecha {
  width: var(--ico); height: var(--ico); flex: none; margin-left: auto;
  display: grid; place-items: center; transform: none !important;
}
/* Y la flecha de `lean-multi` se calla dentro de una criba: ahí la pone el
   componente, con su trazo y su giro, y las dos juntas son dos chevrons en el
   mismo mando. */
.lean-criba .lean-multi::after { content: none; }
.lean-criba .lean-multi__campo { padding-right: var(--s3); }
.lean-criba__flecha .lean-ico { transition: transform var(--t) var(--mecanico); }
/* El estado abierto de un desplegable de esta casa es `is-open` EN EL
   CONTENEDOR, no `aria-expanded` en el botón: eso último lo pone quien lo monta
   a mano, y aquí lo monta `lean-dropdown`. Con el selector equivocado el chevron
   no giraba — y no fallaba nada, simplemente no pasaba. */
.lean-dropdown.is-open .lean-criba__flecha .lean-ico { transform: rotate(180deg); }

/* ── EL MENÚ YA ESTÁ RESUELTO, Y CONVIENE SABER POR QUÉ ────────────────────
   Plegar un mando necesita `overflow: hidden`, y `overflow` recorta a cualquier
   descendiente absoluto: un menú colgado del mando desaparecería entero — y con
   él el propio mando, porque el hueco seguiría reservado y dentro no se pintaría
   nada. El síntoma no sería «falta el menú», sería «falta el filtro».
   Aquí no pasa porque `lean-dropdown` ya sube su menú al TOP LAYER con
   `showPopover()` y lo coloca con coordenadas de ventana, exactamente por este
   motivo — lo dice su propia nota en `lean.js`. Así que el mando puede plegarse
   sin miramientos. Si algún día eso cambiara, esto se rompe por aquí. */

/* ── «MÁS» NO ES OTRA VENTANA: ES LA MISMA, UNA CAPA MÁS ADENTRO ───────────
   Eran tres saltos —barra, lista de filtros, valores del filtro— y cada uno
   cerraba una caja y abría otra en otro sitio. Y no son tres cosas: es UNA
   pregunta que se va concretando, «filtrar por…» → «…empresa» → «…éstas».
   Así que el menú no se cierra: se DESPLAZA. Los dos niveles viven en la misma
   caja, uno al lado del otro, y la caja enseña uno u otro deslizando, con su
   alto acompañando —si el alto saltara, el desplazamiento no engañaría a
   nadie—. Hay vuelta atrás, que es lo que convierte dos pantallas en una con
   profundidad. */
.lean-criba__pila { display: grid; grid-template-columns: 50% 50%; align-items: start; width: 200%;
  transition: transform var(--t-mueve) var(--asienta); }
.lean-criba__pila[data-nivel="1"] { transform: translateX(-50%); }
.lean-criba__nivel { min-width: 0; }
/* El nivel que no se ve no se tabula ni se lee: existe, pero no está. */
.lean-criba__nivel[aria-hidden="true"] { visibility: hidden; transition: visibility 0s linear var(--t-mueve); }
.lean-criba__pila[data-nivel="1"] .lean-criba__nivel[data-n="1"],
.lean-criba__pila[data-nivel="0"] .lean-criba__nivel[data-n="0"] { visibility: visible; transition-delay: 0s; }
/* `grid-template-columns` en porcentaje se mide contra EL GRID, que ya es el
   doble de ancho: al 100 % cada nivel medía dos menús y la lista se escribía
   fuera de la caja. Y el ancho del menú tiene que ser FIJO, porque un
   `width: max-content` con un hijo al 200 % es una referencia circular. */
.lean-menu--criba { width: 292px; max-width: 292px; overflow: hidden;
  transition: height var(--t-mueve) var(--asienta); }
/* La vuelta mira a la IZQUIERDA: arriba querría decir «cierra esto», y no es
   eso — es «un paso atrás». */
.lean-criba__atras {
  display: flex; align-items: center; gap: var(--s2); width: 100%;
  padding: 6px 10px; margin: 0 0 var(--s1);
  border: 0; border-bottom: 1px solid var(--line); background: none; cursor: pointer;
  color: var(--ink); font: var(--peso-firme) var(--fs-md)/var(--alto-nombre) var(--ui);
  border-radius: var(--r-xs) var(--r-xs) 0 0; transition: background var(--t-fast);
}
.lean-criba__atras:hover { background: var(--al-pasar); }
.lean-criba__atras .lean-ico:first-child { transform: rotate(180deg); color: var(--muted); }
.lean-criba__raya { height: 1px; background: var(--line); margin: var(--s1) 10px; }
/* Buscar DENTRO de un menú largo. Sangra igual que una fila (10 px) y su línea
   va a margen, no de borde a borde: con el menú a `--s1` de relleno, una raya
   completa queda flotando a cuatro píxeles de los cantos. */
.lean-criba__buscaop {
  display: flex; align-items: center; gap: var(--s2);
  padding: 4px 10px 9px; margin: 0 0 var(--s1);
  border-bottom: 1px solid var(--line); color: var(--muted);
}
.lean-criba__buscaop input {
  flex: 1; min-width: 0; border: 0; background: none; color: var(--ink);
  font: inherit; font-size: var(--fs-md); outline: none;
}
/* Lo que ya filtra, marcado en la lista de detrás: es el mismo `--elegido` que
   usa `lean-multi` para una casilla puesta, porque dice exactamente lo mismo. */
.lean-menu--criba button.is-elegida { background: var(--elegido); }
.lean-menu--criba button.is-elegida:hover { background: var(--elegido-2); }

/* ── EL MENÚ PRESTADO ──────────────────────────────────────────────────────
   El segundo nivel no copia el menú del mando: MUEVE el mismo nodo. Duplicarlo
   habría sido el mismo filtro en dos listas que se pueden desincronizar; movido,
   lo que se marca ahí es exactamente lo que se habría marcado en la barra, con
   sus eventos y su estado.
   Pero llega vestido de capa flotante —`position: fixed`, sombra, borde y su
   propio fondo— y aquí dentro ya está dentro de una. Se le quita el disfraz. */
.lean-criba__prestado { display: block; width: auto; }
/* Su campo se calla: el rótulo ya lo pone la cabecera del nivel, y dos rótulos
   del mismo filtro a un centímetro es decir dos veces lo mismo. */
.lean-criba__prestado > .lean-multi__campo { display: none; }
.lean-criba__prestado::after { content: none; }
/* Y el menú se quita el disfraz de capa flotante: aquí dentro ya está dentro de
   una, así que fuera posición, sombra, borde y fondo propio. */
.lean-criba__prestado-menu {
  position: static !important; inset: auto !important;
  width: auto !important; min-width: 0 !important; max-width: none !important;
  margin: 0; padding: 0; border: 0; background: none; box-shadow: none;
  transform: none !important; opacity: 1 !important; overflow: visible;
  display: block !important;
}

/* ── LOS ICONOS ACUSAN EL GESTO, Y NO SE DEJAN MACHACAR ────────────────────
   Un icono que se mueve al pulsarlo dice «te he oído» antes de que el menú
   llegue a abrirse. Pero una animación que se relanza en cada clic es un tic: se
   pone, se quita al terminar y el mando no admite otra hasta pasado el
   refractario — lo mismo que hace el toast, que no reapila el mismo aviso.
   `> .lean-ico`, HIJO DIRECTO: el que acusa es el icono DEL FILTRO, no todos los
   del botón. Con el descendiente suelto la animación caía también sobre el
   chevron, y una animación gana a una transición: el chevron hacía el aplastado
   en vez de girar y al acabar saltaba de golpe a su rotación. */
.lean-criba .is-acusando > .lean-ico,
.lean-criba .is-acusando > .lean-multi__campo > .lean-ico { animation: criba-acusa 420ms var(--ease-entra); }
@keyframes criba-acusa {
  0%   { transform: translateY(0); }
  35%  { transform: translateY(1.5px) scaleY(.88); }
  70%  { transform: translateY(-.5px) scaleY(1.04); }
  100% { transform: none; }
}
.lean-criba .is-acusando[data-gesto="mira"] > .lean-ico { animation: criba-reparte 420ms var(--ease-entra); }
@keyframes criba-reparte {
  0%   { transform: scaleY(1); }
  40%  { transform: scaleY(.82); }
  100% { transform: scaleY(1); }
}

/* La palabra que sólo aparece cuando el mando se queda solo en su línea. Va
   ANTES de las consultas y no después: misma especificidad, gana la última — y
   escrita debajo apagaba lo que el tramo estrecho acababa de encender. */
.lean-criba__solo-estrecho { display: none; }

/* ── RESPONSIVE: reacciona al ANCHO DE LA VISTA, no al de la ventana ───────
   Con el lateral abierto en una pantalla de 1.280 px la vista mide 1.040; con
   el cajón del móvil, la misma ventana da 1.280. Una consulta de ventana se
   equivoca en los dos casos. */
@container criba (max-width: 900px) { .lean-criba { --criba-buscar: 148px; --criba-mando: 132px; } }
@container criba (max-width: 560px) {
  .lean-criba { flex-direction: column; align-items: stretch; }
  .lean-criba__acota { flex-direction: column; align-items: stretch; }
  .lean-criba .lean-criba__buscar { flex: 1 1 auto; width: auto; }
  /* APILADA, LO QUE SE PLIEGA PLIEGA EL ALTO. `max-width: 0` en una COLUMNA no
     pliega nada: el mando se queda sin ancho y con sus 36 px de alto intactos, y
     la barra son huecos fantasma con un botón suelto flotando en medio. */
  .lean-criba__acota > .is-plegado {
    max-height: 0; min-height: 0; padding-block: 0; border-width: 0;
    margin-bottom: calc(var(--s2) * -1);
    transition: max-height var(--t-sale) var(--ease-sale), opacity 100ms linear,
                margin var(--t-sale) var(--ease-sale);
  }
  .lean-criba__acota > .lean-criba__mando { max-width: none; max-height: 60px;
    transition: max-height var(--t-mueve) var(--asienta) var(--releva),
                opacity var(--t-entra) var(--ease-entra) var(--releva),
                margin var(--t-mueve) var(--asienta) var(--releva); }
  /* Hasta el fondo: el `100 %` tiene que llegar al BOTÓN, no pararse en el
     `lean-multi` que lo envuelve. Quedándose a medias, el mando ocupaba la línea
     entera y dentro había un embudo de 36 px arrimado a la izquierda. */
  .lean-criba__acota .lean-criba__mando > *,
  .lean-criba__acota .lean-criba__mando > * > .lean-boton,
  .lean-criba__acota .lean-criba__mando > * > .lean-multi__campo { width: 100%; }
  /* Aquí el mando de detrás deja de ser un icono suelto: es LA puerta de los
     filtros —no cabe ninguno a la vista— así que se explica. */
  .lean-criba__mando--detras .lean-boton { justify-content: center; }
  .lean-criba__solo-estrecho { display: inline; }
  /* Los de «cómo se mira» juntos y las acciones al otro extremo:
     `space-between` con tres piezas repartía el aire entre ellas y las dejaba
     flotando sin decir cuál va con cuál. */
  .lean-criba__mira { justify-content: flex-start; }
  /* El último de la fila se va al otro extremo: los de «cómo se mira» quedan
     juntos y las acciones aparte. `space-between` con tres piezas repartía el
     aire entre ellas y las dejaba flotando sin decir cuál va con cuál. */
  .lean-criba__mira > :last-child { margin-left: auto; }
}

/* ── El documento ─────────────────────────────────────────────────────────────
   Una página de la Wiki: texto largo escrito por una persona, que es un animal
   distinto de todo lo demás del panel.

   Lo que cambia respecto a una tarjeta o una tabla: aquí se LEE de seguido, así
   que manda el ancho de línea (72 caracteres, que es donde el ojo vuelve sin
   perderse) y el ritmo vertical, y no la densidad. Por eso es el único sitio del
   panel donde el texto corrido va a `--fs-lg` y con interlineado holgado.

   Se apoya en la escala y en los tokens como todo lo demás: ni un tamaño suelto.
   Los elementos de dentro —h2, ul, blockquote— se estilan con DOS selectores
   (`.lean-doc h2`) en vez de inventarles una clase, que es exactamente para lo
   que la regla 5 los admite: fuera del documento siguen siendo lo que eran. */
.lean-doc {
  max-width: 72ch;
  /* CON LOS PELDAÑOS DE DOCUMENTO, que es lo que faltaba en la escala.
     El cuerpo iba a `--fs-lg`, o sea 14px: el tamaño con el que se lee una
     tabla, no un procedimiento entero. Y el h2 a 19px daba ×1,36 sobre el
     cuerpo, una jerarquía tan plana que un título de sección apenas se
     distinguía del párrafo que titula. Ver la nota de `--fs-doc` en tokens. */
  font-size: var(--fs-doc);
  line-height: var(--alto-lectura);
  /* Y con la tinta de lectura, que en oscuro baja de 15,5:1 a 12:1. Ver
     `--ink-doc`: el contraste máximo es correcto para un vistazo y cansa en
     media pantalla de prosa. */
  color: var(--ink-doc);
  /* La selección, con el color de la casa. Sin esto sale el azul del sistema,
     que sobre el tema oscuro es lo más agresivo de la pantalla — y aparece
     justo mientras se edita, o sea todo el rato. */
  --sel: color-mix(in srgb, var(--brand) 26%, transparent);
}
.lean-doc ::selection { background: var(--sel); }
.lean-doc::selection { background: var(--sel); }
.lean-doc > * + * { margin-top: var(--s3); }
.lean-doc h2, .lean-doc h3 { margin: 0; line-height: var(--alto-corto); text-wrap: balance; color: var(--ink); }
.lean-doc h2 { font-size: var(--fs-doc-h2); font-weight: var(--peso-titulo); letter-spacing: var(--letra-ajuste); }
.lean-doc h3 { font-size: var(--fs-doc-h3); font-weight: var(--peso-titulo); }
/* Un título respira por arriba y no por abajo: lo que lo separa de lo anterior
   es aire, y lo que lo une a lo que titula es la falta de él. */
.lean-doc .lean-bloque[data-tipo="titulo"] { margin-top: var(--s5); }
.lean-doc .lean-bloque[data-tipo="titulo"] + .lean-bloque { margin-top: var(--s2); }
.lean-doc p { margin: 0; }
/* El separador. Sin esto sale la raya en relieve que trae el navegador de serie,
   que en tema oscuro es una línea blanca de dos píxeles: lo único de la página
   que grita, y sirve justo para lo contrario. */
.lean-doc hr {
  border: 0; border-top: 1px solid var(--line);
  margin-block: var(--s3);
}

.lean-doc__lista, .lean-doc__tareas { margin: 0; padding-left: var(--s5); }
.lean-doc__lista li + li, .lean-doc__tareas li + li { margin-top: var(--s1); }
.lean-doc__lista li::marker { color: var(--faint); }
/* Las tareas llevan su casilla delante, así que no necesitan viñeta ni sangría
   de lista: la casilla ya es el marcador. */
.lean-doc__tareas { list-style: none; padding-left: 0; }
.lean-doc__tareas li { display: flex; gap: var(--s2); align-items: flex-start; }
/* La casilla. `lean-doc__marca` es el `lean-check` de siempre puesto en una
   lista: lo único que cambia es que se alinea con la primera línea del texto y
   no se estira. La caja se achica un punto, porque aquí acompaña a un renglón de
   documento y no a la etiqueta de un formulario. */
.lean-doc__marca { margin-top: 4px; flex: none; }
.lean-doc__marca .box { width: 16px; height: 16px; }
.lean-doc__marca .box .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
/* Una casilla que no se puede pulsar no debe decir que sí: leyendo sin permiso
   de escribir es un testigo, y el cursor tiene que contarlo. */
.lean-doc__marca:has(input:disabled) { cursor: default; }
.lean-doc__tareas li.is-hecha > span { color: var(--muted); text-decoration: line-through; }

/* Una cita es de alguien: la barra la separa del texto propio sin gritar. */
.lean-doc__cita {
  margin: 0; padding-left: var(--s4);
  border-left: 2px solid var(--line);
  color: var(--muted); font-style: italic;
}

.lean-doc__figura { margin: 0; position: relative; }
.lean-doc__figura img {
  max-width: 100%; height: auto; display: block;
  border: 1px solid var(--line); border-radius: var(--r-xs);
  background: var(--panel-2);
  /* La captura entera amplía, no sólo la lupa: es el gesto que hace cualquiera
     sobre una imagen que no se lee, y el cursor es lo único que lo cuenta antes
     de probar. La lupa se queda porque es la que se ve con el teclado. */
  cursor: zoom-in;
}
/* TRES TALLAS. Iban todas al ancho de la columna, así que una página con tres
   capturas era un scroll: el recorte de un cuadro de diálogo y el pantallazo de
   un monitor entero no piden el mismo sitio. Son tres y no un ancho a mano
   porque un número por imagen se convierte en veinte anchos distintos en la
   misma página, y lo que hace legible un documento es que las cosas del mismo
   tipo midan lo mismo. */
.lean-doc__figura[data-talla="media"] img { max-width: 66%; }
.lean-doc__figura[data-talla="chica"] img { max-width: 40%; }

/* VER A TAMAÑO COMPLETO. Sale al apuntar la figura, arriba a la derecha y
   dentro de ella: es un mando de la imagen, no del documento.

   DOS NOMBRES Y UNA SOLA DEFINICIÓN, desde Lean 1.150.0. `lean-figura` es la
   forma general —una figura con un mando de ampliar— y `lean-doc__figura` es su
   caso en un documento, que es donde nació. Son la misma pieza y por eso
   comparten cada regla: lo que NO se hace es copiarla en `app.css` para el
   segundo cliente, que es exactamente el defecto que la auditoría del 16/9/26
   vino a quitar —«la guía deja de enseñar una pieza distinta de la que pinta el
   panel»—.

   El segundo cliente es el croquis de una red por reconocer: un dibujo que
   explica por dónde sale a internet la red que se está decidiendo. Ahí la figura
   no es una captura subida sino un SVG que compone el panel, y el mando es el
   mismo: 28 px, dentro, al apuntar, con `cursor: zoom-in`.

   No se renombra `lean-doc__figura` a secas porque tiene cinco llamadas vivas en
   los dos módulos de la Wiki y sus apuntes en este registro, y un registro de
   cambios cuenta lo que pasó CON EL NOMBRE QUE TENÍA. Añadir el nombre general
   cuesta un selector; renombrar cuesta dos módulos y reescribe la historia. */
.lean-figura { margin: 0; position: relative; }

.lean-doc__ampliar,
.lean-figura__ampliar {
  position: absolute; top: var(--s2); right: var(--s2);
  width: 28px; height: 28px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: var(--r-xs);
  background: var(--panel); color: var(--muted); cursor: zoom-in;
  opacity: 0; transition: opacity var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lean-doc__figura:hover .lean-doc__ampliar,
.lean-figura:hover .lean-figura__ampliar,
.lean-doc__ampliar:focus-visible,
.lean-figura__ampliar:focus-visible { opacity: 1; }
.lean-doc__ampliar:hover,
.lean-figura__ampliar:hover { color: var(--ink); border-color: var(--faint); }
.lean-doc__ampliar .lean-ico,
.lean-figura__ampliar .lean-ico { width: var(--ico); height: var(--ico); }
/* Con la talla puesta, el mando sigue a la imagen y no al hueco de la figura. */
.lean-doc__figura[data-talla] .lean-doc__ampliar { right: auto; left: calc(min(66%, 100%) - 36px); }
.lean-doc__figura[data-talla="chica"] .lean-doc__ampliar { left: calc(min(40%, 100%) - 36px); }

/* MIENTRAS SUBE. El hueco se pone antes de empezar, con las mismas medidas de
   caja que tendrá la imagen: pegar una captura y no ver absolutamente nada
   durante dos segundos se lee como que no ha funcionado, y lo que hace uno
   entonces es volver a pegar. */
.lean-doc__subiendo {
  display: flex; align-items: center; justify-content: center; gap: var(--s3);
  min-height: 140px;
  border: 1px dashed var(--line); border-radius: var(--r-xs);
  background: var(--panel-2); color: var(--muted); font-size: var(--fs-sm);
  animation: lean-subiendo var(--t-espera) var(--ease) infinite;
}
.lean-doc__subiendo .lean-ico { width: var(--ico-lg); height: var(--ico-lg); }
@keyframes lean-subiendo { 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .lean-doc__subiendo { animation: none; } }
.lean-doc__figura figcaption {
  margin-top: var(--s2); font-size: var(--fs-sm); color: var(--muted);
}
.lean-doc__perdida {
  padding: var(--s5); border: 1px dashed var(--line); border-radius: var(--r-xs);
  color: var(--muted); font-size: var(--fs-sm); text-align: center;
}

/* La tabla scrollea DENTRO de su caja: una de seis columnas en un cajón estrecho
   no puede empujar el ancho de la página entera. */
.lean-doc__tabla { overflow-x: auto; }
.lean-doc__tabla table {
  border-collapse: collapse; width: 100%; font-size: var(--fs-md); min-width: 320px;
}
.lean-doc__tabla th, .lean-doc__tabla td {
  text-align: left; padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--line); vertical-align: top;
}
.lean-doc__tabla th {
  font-size: var(--fs-xs); font-weight: var(--peso-titulo); letter-spacing: var(--letra-rotulo);
  text-transform: uppercase; color: var(--muted); background: var(--panel-2);
}
.lean-doc__tabla tr:last-child td { border-bottom: 0; }

/* Un enlace que sale de NetProof se marca, porque en un procedimiento la mitad
   llevan a la ficha de un equipo y la otra mitad a la web de un fabricante, y
   pulsar una u otra no es lo mismo. */
.lean-doc__fuera::after {
  content: "↗"; font-size: .85em; margin-left: 2px; color: var(--faint);
}

/* El hueco de debajo del último bloque: sin él, en una página que acaba en una
   tabla no hay dónde pulsar para seguir escribiendo. */
/* En `--muted` y no en `--faint`: esto NO es un texto de relleno, es la única
   instrucción permanente de cómo se usa el editor —«pulsa / para elegir»—, y en
   `--faint` daba 3,46:1, por debajo del mínimo de cualquier texto. La regla 5 lo
   dice desde el principio: `--faint` es para bordes e iconos decorativos. */
.lean-doc__cola {
  padding: var(--s3) 0; color: var(--muted); font-size: var(--fs-md);
  cursor: text; user-select: none;
  transition: color var(--t-fast) var(--ease);
}
.lean-doc__cola:hover { color: var(--ink); }

/* ── Un bloque ────────────────────────────────────────────────────────────────
   La unidad de la que se compone un documento, y la unidad de todo lo demás:
   quién lo escribió, quién lo tiene tomado y qué se difunde cuando cambia. */
/* La transición del fondo venía de `app.css` sin acotar. Es del componente. */
.lean-bloque { position: relative; transition: background var(--t-fast) var(--ease); }
/* La caja de pulsar es mayor que el texto para que no haya que apuntar, pero el
   texto no se mueve: el hueco se saca hacia fuera con el margen negativo. */
.lean-doc--editable .lean-bloque {
  transition: background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  padding: var(--s1) var(--s2);
  margin-left: calc(var(--s2) * -1); margin-right: calc(var(--s2) * -1);
  border-radius: var(--r-xs);
  cursor: text;
}
.lean-doc--editable .lean-bloque:hover { background: var(--al-pasar); }
/* EL QUE SE ESTÁ ESCRIBIENDO NO SE MARCA.
   Esto ha tenido tres formas y conviene saber por qué acabó en ninguna:

   1. Compartía fondo con el hover, así que «aquí puedes pulsar» y «aquí estás
      escribiendo» se veían igual, y además metía el renglón que se teclea dentro
      de un rectángulo gris — lo contrario de lo que promete editar en el sitio.
   2. Pasó a una barra de marca a la izquierda, que decía lo mismo sin tocar el
      texto. Mejor, pero seguía marcando el bloque.
   3. Y ahora, nada: el bloque abierto no se distingue. Lo que aparece al apuntar
      son la manija y el más —fuera del ancho de lectura, sin desplazar una
      letra— y se quedan mientras el bloque está abierto.

   Lo que sostiene el 3 es que el aviso ya lo dan tres cosas a la vez: el cursor
   parpadeando, los mandos encendidos y el indicador de la página diciendo
   «Editando». Una barra encima de eso es un cuarto sitio contando lo mismo, y en
   un documento —donde lo que hay que ver es el texto— cualquier adorno compite
   con lo único que importa. */
.lean-doc--editable .lean-bloque.is-editando,
.lean-doc--editable .lean-bloque.is-editando:hover {
  background: none; cursor: auto; box-shadow: none;
}
/* Y los mandos se quedan puestos mientras se escribe: aparecen al apuntar, pero
   con el bloque abierto no se van al salir con el ratón. */
.lean-doc--editable .lean-bloque.is-editando > .lean-bloque__mando,
.lean-doc--editable .lean-bloque.is-editando > .lean-bloque__mas { opacity: 1; }

/* El campo hereda la letra de lo que sustituye, y ahí está la gracia: al abrir y
   al cerrar el bloque no se mueve ni un píxel. Un editor que da un salto cada vez
   que pulsas se siente roto aunque no lo esté. */
.lean-bloque__campo {
  display: block; width: 100%;
  font: inherit; color: inherit; line-height: inherit;
  background: none; border: 0; padding: 0; margin: 0;
  resize: none; overflow: hidden;
}
.lean-bloque__campo:focus { outline: none; }
.lean-bloque__campo::placeholder { color: var(--muted); }
.lean-bloque[data-tipo="titulo"] .lean-bloque__campo { font-size: var(--fs-2xl); font-weight: var(--peso-titulo); }
/* El pie de una imagen se escribe DEBAJO de ella, no en su lugar: la imagen es
   el contenido del bloque y su texto es sólo el pie. Va en letra pequeña y
   apagada, que es como se lee un pie. */
.lean-bloque__campo--pie {
  margin-top: var(--s2); font-size: var(--fs-sm); color: var(--muted);
}
.lean-bloque[data-tipo="codigo"] .lean-bloque__campo,
.lean-bloque[data-tipo="tabla"] .lean-bloque__campo {
  font-family: var(--mono); font-size: var(--fs-md);
}

/* Con qué lenguaje se enseña un bloque de código. Sólo aparece con el bloque
   abierto, arriba y en pequeño: es un ajuste del bloque, no parte de lo que se
   escribe, y a la altura del texto competiría con él. */
.lean-bloque__lengua {
  height: var(--ctl-h-sm); width: auto; max-width: 200px;
  margin-bottom: var(--s2); font-size: var(--fs-sm);
}

/* EL BLOQUE QUE SE ESTÁ ESCRIBIENDO. Es el mismo nodo que estaba pintado —un <p>,
   un <h2>, una <ul>— con `contenteditable` puesto, y por eso NO cambia de aspecto:
   ahí está la gracia de editar en el sitio, que lo que se ve mientras se escribe
   es exactamente lo que va a quedar. Lo único que se le añade es el hueco para
   cuando está vacío. */
.lean-bloque__vivo { outline: none; min-height: 1.2em; }
.lean-bloque__vivo[data-hueco]:empty::before,
.lean-bloque__vivo[data-hueco] > li:only-child:empty::before {
  content: attr(data-hueco); color: var(--muted); pointer-events: none;
}

/* La dirección de un enlace se escribe EN LA BARRA, donde estaba la selección: un
   diálogo modal corta la escritura para algo que casi siempre se resuelve pegando
   lo que ya está en el portapapeles. */
.lean-bloque__url {
  width: 230px; height: 24px; padding: 0 var(--s2);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  background: var(--panel); color: var(--ink);
  font-family: inherit; font-size: var(--fs-sm);
}
.lean-bloque__url:focus { outline: none; border-color: var(--foco); }

/* La manija sale FUERA del ancho de lectura y sólo al apuntar la fila: es la
   misma gramática que el minimando de la consola. Ocupando sitio siempre,
   estrecharía la columna de texto por un mando que se usa una vez cada veinte
   párrafos. */
.lean-bloque__mando {
  /* Pegada al bloque y no a 32 px, que era lo que la sacaba del panel hasta
     tocar el carril de secciones. El sitio se lo hace el documento con su propio
     padding (.wiki-doc), así que sigue fuera del ancho de lectura sin salirse de
     la columna. */
  position: absolute; left: calc(var(--s5) * -1); top: 2px;
  width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: var(--r-xs); background: none;
  color: var(--faint); font-size: var(--fs-lg); line-height: var(--alto-1);
  cursor: grab; opacity: 0; transition: opacity var(--t-fast) var(--ease);
}
.lean-bloque:hover .lean-bloque__mando, .lean-bloque__mando:focus-visible { opacity: 1; }
.lean-bloque__mando:hover { background: var(--al-pasar); color: var(--ink); }

/* AÑADIR UN BLOQUE DEBAJO. Comparte todo con la manija —tamaño, sitio, cómo
   aparece— porque son la misma familia de mandos y salen juntos al apuntar la
   fila; sólo cambia dónde: a su izquierda, que es el orden en que se leen
   —primero «meto algo aquí», después «muevo esto»—.

   Faltaba, y su falta costaba tres gestos: para poner algo entre dos bloques
   había que ir al final del de arriba, Enter y `/`. */
.lean-bloque__mas {
  position: absolute; left: calc(var(--s5) * -1 - 22px); top: 2px;
  width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: var(--r-xs); background: none;
  color: var(--muted); cursor: pointer;
  opacity: 0; transition: opacity var(--t-fast) var(--ease), background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lean-bloque__mas .lean-ico { width: var(--ico); height: var(--ico); }
.lean-bloque:hover .lean-bloque__mas, .lean-bloque__mas:focus-visible { opacity: 1; }
.lean-bloque__mas:hover { background: var(--al-pasar); color: var(--ink); }
.lean-bloque__mas:active { transform: scale(.9); }

/* La barra que sale al seleccionar texto.

   NO es un `lean-menu`, y lo fue un rato: aquél es el desplegable de un
   `lean-dropdown` —va en `position: fixed` y lean.js le pone las coordenadas al
   abrirlo—, así que colgada de un bloque se iba a donde no era y no se veía.
   Cuando hay que pelearse con un componente por dónde se coloca, el componente
   elegido no era el que tocaba (regla 5-bis).

   Va anclada ARRIBA del bloque y no pegada a la selección: en un <textarea> la
   selección no tiene rectángulo —no hay Range ni getClientRects—, así que ponerla
   encima de lo seleccionado exigiría medir caracteres con un lienzo y reproducir
   el ajuste de línea del campo. Arriba sale siempre en el mismo sitio, que además
   es predecible. */
.lean-bloque__barra {
  position: absolute; left: 0; bottom: 100%; z-index: var(--z-menu);
  display: flex; gap: 1px; padding: 3px;
  margin-bottom: var(--s1);
  /* Barra flotante de acciones: `--alzado` y `--linde-alto`, que es la altura que
     tokens.css le asigna por nombre. Iba con el papel de una tarjeta. */
  background: var(--alzado); border: 1px solid var(--linde-alto);
  border-radius: var(--r-s); box-shadow: var(--shadow-lg);
  /* `--t-entra` y `--ease-entra`, que es lo que hacía falta para cumplir lo que
     el menú de aquí abajo tiene escrito: «entra con el mismo gesto que la barra
     de selección, las dos salen del bloque en el que se está escribiendo, así que
     tienen que moverse igual». No se movían igual — ésta iba a `--t-fast` (140,
     el token del hover) y aquél a `--t-entra` (150) —, y diez milisegundos no se
     ven pero la regla de la que salían sí se había perdido. */
  animation: lean-bloque-entra var(--t-entra) var(--ease-entra);
}
@keyframes lean-bloque-entra {
  from { opacity: 0; transform: translateY(4px) scale(.97); }
  to   { opacity: 1; transform: none; }
}
.lean-bloque__marca {
  width: 30px; height: 26px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: var(--r-xs); background: none;
  color: var(--ink); font-family: inherit; font-size: var(--fs-md); line-height: var(--alto-1);
  cursor: pointer; transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.lean-bloque__marca:hover { background: var(--al-pasar); color: var(--brand-ink); }
/* PUESTA: lo seleccionado YA tiene esa marca, así que pulsar la quita. Sin esto
   la única forma de saber si algo era negrita era buscar los asteriscos, que es
   justo lo que no se le puede pedir a nadie —y sin saberlo, la misma tecla que
   quita parecía que ponía otra vez—. */
.lean-bloque__marca.is-puesta {
  background: var(--elegido-2);
  color: var(--brand-ink);
}
.lean-bloque__marca.is-puesta:hover {
  background: var(--elegido-2);
}
.lean-bloque__marca:active { transform: scale(.92); }
.lean-bloque__marca.es-negrita { font-weight: var(--peso-fuerte); }
.lean-bloque__marca.es-cursiva { font-style: italic; font-family: Georgia, serif; }
.lean-bloque__marca.es-codigo { font-family: var(--mono); font-size: var(--fs-sm); }

/* El menú de `/` cuelga del bloque en el que se está escribiendo. Se apoya en
   `lean-menu` —no es un menú nuevo— y sólo le pone dónde aparece. */
.lean-bloque__menu {
  position: absolute; left: 0; top: 100%; z-index: var(--z-menu);
  /* MÁS ANCHO Y SIN SCROLL. Cabían siete de los once tipos, así que «Código» —de
     los más usados en un manual de soporte— quedaba fuera y había que rodar la
     lista para llegar. Un menú que se abre docenas de veces al día no puede
     pedir dos gestos para lo de siempre. */
  min-width: 250px; max-height: min(440px, 60vh); overflow-y: auto;
  /* Entra con el mismo gesto que la barra de selección: las dos salen del
     bloque en el que se está escribiendo, así que tienen que moverse igual. */
  animation: lean-bloque-entra var(--t-entra) var(--ease-entra);
}
@media (prefers-reduced-motion: reduce) { .lean-bloque__menu { animation: none; } }
/* La fila sobre la que están las flechas. Es «lo elegido» y se pinta como se
   pinta lo elegido en todo el panel, no con el gris del hover: en un menú que se
   maneja con el teclado, esa fila es la que se va a activar al pulsar Enter. */
.lean-bloque__menu .is-sel { background: var(--elegido); }
/* Una fila: icono, rótulo y el atajo con el que se escribe sin abrir el menú.
   El icono va porque once renglones de texto igual obligan a leerlos todos; el
   atajo, porque enseñarlo justo donde se hace la cosa a mano es lo que consigue
   que se deje de hacer a mano. */
.lean-bloque__menu button { display: flex; align-items: center; gap: var(--s3); }
.lean-bloque__menu .lean-ico { width: var(--ico); height: var(--ico); flex: none; color: var(--muted); }
.lean-bloque__menu .is-sel .lean-ico, .lean-bloque__menu button:hover .lean-ico { color: var(--ink); }
.lean-bloque__menu button.danger .lean-ico { color: inherit; }
.lean-menu__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lean-menu__atajo {
  flex: none; font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--muted); background: var(--panel-2);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 0 5px; line-height: var(--alto-ui);
}
/* La opción que YA está puesta —hoy, la talla de una captura—: sin esto, tres
   opciones idénticas y ninguna dice cuál es la de ahora. */
.lean-bloque__menu button > .lean-ico:last-child { color: var(--brand-ink); margin-left: auto; }
.lean-menu__vacio {
  margin: 0; padding: var(--s2) var(--s3);
  color: var(--muted); font-size: var(--fs-sm);
}

/* Mientras se arrastra un bloque: lo que se mueve se apaga y donde va a caer se
   marca con una raya. La raya es un borde y no un hueco insertado a propósito —un
   hueco de verdad empujaría el resto del documento mientras arrastras, y el sitio
   al que apuntas cambiaría solo debajo del cursor—. */
/* ── LO QUE HACE EL EDITOR, CONTADO ──────────────────────────────────────────
   Un bloque que nace, uno que se mueve porque otro se ha ido, uno que cambia de
   tipo. Antes las tres pasaban de golpe, y no por decisión: `pintarTodo`
   reconstruía el documento entero y con nodos nuevos no hay nada que animar.
   Ahora se reconcilia por bloque (ver wiki-editor.js) y estas tres reglas son
   todo lo que hace falta.

   EL TIEMPO SALE DE LA TABLA, y esto era lo que estaba mal. Aquí ponía que
   ninguna podía pasar de 160 ms «porque por encima deja de leerse como
   consecuencia de lo que acabas de pulsar», y eso es verdad del ACUSE —el
   hundido de un botón, que tiene que caber en el tiempo de reacción— pero no de
   un desplazamiento: lo que se sigue con la vista necesita durar lo suficiente
   para poder seguirse. A 150 ms el recorrido no se ve, se adivina, que es la
   misma frase que ya estaba escrita tres pantallas más arriba sobre las
   transiciones de vista.

   Y la respuesta buena ya la tenía la casa en otro sitio: `entraFila` y
   `devolverASuSitio` resuelven exactamente esto para una tabla —380 ms con
   `--asienta`, y la que llega con 80 ms de retraso sobre las que se apartan—.
   No había motivo para que un documento se moviera con otra gramática que una
   lista; que fueran distintas era la herencia de haberlas escrito por separado.

   EL ORDEN ES LO QUE CUENTA LA HISTORIA: primero se abre el hueco y DESPUÉS
   aparece lo que lo ocupa. A la vez —que es como estaba— se lee como que el
   bloque nuevo empuja a los de abajo, o sea como un salto de maquetación. */
.lean-bloque--llega { animation: lean-bloque-nace var(--t-asienta) var(--asienta) 80ms backwards; }
@keyframes lean-bloque-nace {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
/* El viaje de un bloque que ha cambiado de sitio. La transición se pone SÓLO
   mientras dura: puesta siempre, cualquier reflujo de la página arrastraría los
   bloques detrás del cursor.

   Con `--asienta`, que es la curva que rebasa y vuelve: aquí no es que algo
   aparezca en un sitio vacío, es que el documento CEDE SITIO, y sin el rebase el
   desplazamiento se lee como un salto y no como que la lista se aparta. Es
   literalmente el caso para el que existe esa curva (ver `tokens.css`). */
.lean-bloque--viaja { transition: transform var(--t-asienta) var(--asienta); }
/* Y el que cambia de tipo: un fundido corto, para que se lea como que ESO se ha
   transformado y no como que ha aparecido otra cosa en su sitio. */
.lean-bloque--cambia { animation: lean-bloque-muda 120ms var(--ease); }
@keyframes lean-bloque-muda {
  from { opacity: .25; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .lean-bloque--llega, .lean-bloque--cambia { animation: none; }
  .lean-bloque--viaja { transition: none; }
}

.lean-doc.is-moviendo .lean-bloque { cursor: grabbing; }
.lean-doc.is-moviendo .lean-bloque:hover { background: none; }
.lean-bloque.cae-antes { box-shadow: 0 -2px 0 -1px var(--brand); }
.lean-bloque.cae-despues { box-shadow: 0 2px 0 -1px var(--brand); }
.lean-bloque__mando:active { cursor: grabbing; }

/* ── Código ───────────────────────────────────────────────────────────────────
   En un manual de soporte un bloque de código existe para COPIARSE: es la orden
   que el técnico va a pegar en el equipo del cliente. De ahí que lo primero sean
   el lenguaje escrito y el botón, y no el resaltado. */
.lean-codigo {
  border: 1px solid var(--line); border-radius: var(--r-xs);
  background: var(--panel-2); overflow: hidden;
}
.lean-codigo__barra {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding: var(--s1) var(--s2) var(--s1) var(--s3);
  border-bottom: 1px solid var(--line);
}
.lean-codigo__lengua {
  font-size: var(--fs-xs); letter-spacing: var(--letra-rotulo); text-transform: uppercase;
  color: var(--muted);
}
.lean-codigo__copiar {
  border: 1px solid var(--line); border-radius: var(--r-xs);
  background: var(--panel); color: var(--muted);
  font-size: var(--fs-xs); padding: 2px var(--s2); cursor: pointer;
}
.lean-codigo__copiar:hover { color: var(--ink); border-color: var(--faint); }
.lean-codigo pre {
  margin: 0; padding: var(--s3);
  font-family: var(--mono);
  font-size: var(--fs-md); line-height: var(--alto-lectura);
  overflow-x: auto; white-space: pre;
}
/* El código EN LÍNEA es el mismo componente en pequeño: se lee dentro de una
   frase, así que no lleva ni barra ni fondo de caja. */
.lean-codigo--linea {
  display: inline; padding: 1px 5px; border-radius: var(--r-micro);
  font-family: var(--mono);
  font-size: .9em; background: var(--panel-2); overflow: visible;
}

/* ── Una mención ──────────────────────────────────────────────────────────────
   `@` a un equipo, una empresa o alguien. En el texto se guarda el identificador
   opaco y NUNCA el nombre, así que esto pinta el nombre de AHORA: si el equipo
   cambió de etiqueta, aquí se lee la nueva.

   No es `lean-badge`: un badge dice un ESTADO y por eso lleva color semántico.
   Esto dice a QUÉ apunta una palabra dentro de una frase, así que va en la tinta
   de marca y se lee como parte del texto. */
.lean-mencion {
  display: inline; padding: 1px 5px; border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--brand-ink); font-weight: var(--peso-firme); text-decoration: none;
  white-space: nowrap;
}
a.lean-mencion:hover { background: color-mix(in srgb, var(--brand) 22%, transparent); }
/* Lo que ya no existe. Una wiki de dos años tendrá menciones a equipos que se
   tiraron, y eso no es un error: es historia. Se apaga, no se rompe. */
.lean-mencion--ida {
  background: none; color: var(--muted); font-weight: var(--peso-texto);
  text-decoration: line-through; text-decoration-color: var(--faint);
}

/* ── Casillas de un código de un solo uso ─────────────────────────────────────
   Seis huecos para seis cifras, que es como se pide un código en cualquier
   aplicación de 2026 y como NO se pedía aquí: era un campo de texto más, metido
   debajo de la contraseña, en el mismo formulario.

   La forma no es un capricho: seis casillas dicen cuántas cifras hacen falta sin
   escribirlo, enseñan por dónde va uno y convierten «pegar» en algo que reparte
   los dígitos. Un campo de texto no dice ninguna de las tres cosas.

   MÉTRICA: el alto es `--ctl-h-lg`, el de la talla de puerta, el mismo que el
   campo de contraseña y el botón «Entrar» de esa pantalla. Una columna de
   controles sólo se lee como una columna si sus cajas son la misma caja.

   Se centra con `text-align` dentro de cada casilla y con `gap` entre ellas: el
   contenedor es flex y aquí no hay nada que centrar con `place-items`, que en
   flexbox no haría lo que parece (ver el guardián de Lean). */
.lean-otp { display: flex; gap: var(--s2); }

.lean-otp__casilla {
  width: 46px; min-width: 0; flex: 0 0 auto;
  height: var(--ctl-h-lg);
  padding: 0; text-align: center;
  font-family: var(--mono); font-size: 20px; line-height: var(--alto-1);
  background: var(--hondo); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-ctl);
  transition: border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.lean-otp__casilla:hover { border-color: color-mix(in srgb, var(--ink) 22%, var(--line)); }
.lean-otp__casilla:focus {
  outline: none; border-color: var(--foco);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
/* La casilla escrita se distingue de la vacía sin gritar: quien mira quiere
   saber de un vistazo cuánto lleva puesto. */
.lean-otp__casilla:not(:placeholder-shown) { border-color: color-mix(in srgb, var(--ink) 26%, var(--line)); }
.lean-otp__casilla::placeholder { color: var(--muted); }

/* El error va en el GRUPO y no en la casilla que falló, porque no falló una:
   falló el código. Mismo 20 % que `.lean-input--down`. */
.lean-otp--down .lean-otp__casilla { border-color: var(--down); }
.lean-otp--down .lean-otp__casilla:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--down) 20%, transparent);
}

/* Y el temblor, que es lo único que dice «eso no es» sin tener que leer nada.
   Corto y de poca amplitud: es un «no», no un aspaviento. */
@keyframes lean-otp-no {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-5px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(3px); }
}
.lean-otp--no { animation: lean-otp-no 260ms var(--ease); }
@media (prefers-reduced-motion: reduce) { .lean-otp--no { animation: none; } }

/* El campo del CÓDIGO DE RESPALDO, que sustituye a las casillas cuando alguien
   dice que no tiene el móvil. No cabe en seis huecos —son diez caracteres con un
   guion en medio— y por eso es un campo aparte y no seis casillas más anchas. */
.lean-otp__respaldo {
  width: 100%; min-height: var(--ctl-h-lg);
  padding: 0 12px; text-align: center;
  font-family: var(--mono); font-size: 17px; letter-spacing: var(--letra-menuda);
  background: var(--hondo); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-ctl);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.lean-otp__respaldo:focus {
  outline: none; border-color: var(--foco);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}

/* «¿Sin el móvil?». Es una salida, no una alternativa, así que va en tinta
   apagada y debajo — igual que «¿No puedes entrar?» en la pantalla de acceso. */
.lean-otp__cambiar {
  /* 16 px y no 8: es una ACCIÓN —cambia lo que se está pidiendo—, no el pie de
     la última casilla. Con 8 se leía como la ayuda del campo de arriba, que es
     justo lo que no es. */
  display: block; margin-top: var(--s4); padding: 0;
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); color: var(--muted);
  transition: color var(--t-fast) var(--ease);
}
.lean-otp__cambiar:hover { color: var(--ink); text-decoration: underline; }

/* En pantallas estrechas las seis casillas no caben a 46 px con su hueco: se
   encogen a la vez en vez de salirse o partirse en dos filas. */
@media (max-width: 400px) {
  .lean-otp { gap: 6px; }
  .lean-otp__casilla { width: 100%; flex: 1 1 0; font-size: 18px; }
}

/* ── Un paso que deja sitio al siguiente ──────────────────────────────────────
   Las dos puertas de entrada —el acceso y el asistente de primer arranque— son
   tarjetas que cambian de contenido en el sitio, y el movimiento dice hacia
   dónde se ha ido: adelante o atrás.

   ESTABA ESCRITO DENTRO DE setup.html, y ahí no podía quedarse en cuanto la
   pantalla de acceso tuvo su segundo paso: dos copias de la misma curva es
   exactamente lo que a este proyecto ya le pasó una vez con las curvas de las
   transiciones. Una curva vive en un sitio.

   Dieciséis píxeles y no más: un desplazamiento largo llama la atención sobre sí
   mismo y convierte cada paso en un tránsito. Lo que tiene que quedar es la
   sensación de dirección, no el viaje. Y la salida es más rápida que la entrada,
   que es lo que evita el hueco en blanco entre las dos. */
@keyframes lean-paso-sale-izq { to { opacity: 0; transform: translateX(-16px); } }
@keyframes lean-paso-sale-der { to { opacity: 0; transform: translateX(16px); } }
@keyframes lean-paso-entra-der { from { opacity: 0; transform: translateX(16px); } }
@keyframes lean-paso-entra-izq { from { opacity: 0; transform: translateX(-16px); } }

/* Las dos salidas llevan `forwards` A PROPÓSITO, y no vale `backwards`: la
   tarjeta tiene que QUEDARSE transparente al acabar de irse, porque el cambio de
   contenido ocurre justo después, en el `animationend`. Sin el relleno, el
   navegador devuelve la opacidad a 1 durante el fotograma que hay entre que
   termina la animación y corre el manejador, y lo que se ve es un parpadeo del
   paso viejo antes del nuevo.

   Lo que el relleno cuesta es real y aquí no molesta: `opacity` y `transform`
   quedan bajo el control de la animación y dejarían de admitir transiciones —es
   lo que dejó sin avance el anillo de progreso—, pero la clase se quita en ese
   mismo manejador, así que la propiedad se suelta enseguida.

   LAS CURVAS ERAN DE SU PROPIA COSECHA: `--ease` —la de ir y volver— en las dos
   entradas Y en las dos salidas. Eso sí se corrige, porque el papel de cada una
   está claro y la tabla del §2 no admite `--ease` en ninguno de los dos.

   LOS NÚMEROS SE QUEDAN, y esto es una corrección de la auditoría del 12/9: los
   pasé a `--t-entra` y `--t-sale` porque 200 y 140 no eran tokens, y con eso el
   paso se acortó un cuarto sin que nadie lo pidiera. El párrafo de arriba dice
   que la salida es más rápida que la entrada, y 140 contra 200 ya lo cumple: la
   relación que el autor escribió se sostiene con SUS números igual que con los
   míos, así que cambiarlos no arreglaba nada — sólo dejaba el gesto más corto.
   Que un valor no salga de un token no lo hace un descuido; a veces es una
   decisión, y aquí lo era. */
.lean-paso--sale-adelante  { animation: lean-paso-sale-izq  140ms var(--ease-sale) forwards; }
.lean-paso--sale-atras     { animation: lean-paso-sale-der  140ms var(--ease-sale) forwards; }
.lean-paso--entra-adelante { animation: lean-paso-entra-der 200ms var(--ease-entra); }
.lean-paso--entra-atras    { animation: lean-paso-entra-izq 200ms var(--ease-entra); }

@media (prefers-reduced-motion: reduce) {
  .lean-paso--sale-adelante, .lean-paso--sale-atras,
  .lean-paso--entra-adelante, .lean-paso--entra-atras { animation: none; }
}

/* ══════════════════════════ lean-hilo · LO QUE LE HA PASADO A ALGO ══════════════════════════
   El historial de una ficha: un ordenador, un dispositivo de red, una página de
   la Wiki. Lo monta `Lean.hilo(caja, opciones)` y aquí sólo vive su forma.

   Sustituye a `np-hist`, que vivía en app.css y copiaban tres pantallas —la Wiki
   reconstruyéndolo nodo a nodo y pisándole el relleno con dos clases—. Sube a
   Lean por lo mismo que subió `lean-mando`: era el patrón de tres sitios
   encerrado en la hoja de uno.

   LA REJILLA MANDA y está escrita UNA vez: hora · nodo · frase · estado ·
   pliegue. La fila, lo que hay dentro al desplegarla, el día y lo abierto usan
   las mismas pistas, así que la hora cae en la misma vertical en doscientas
   filas y el carril pasa por el centro exacto del nodo sin medir nada en JS.

   LA GRAMÁTICA DEL NODO no es nueva: es la de `lean-emblema` (COMPONENTES.md)
   puesta en el carril. Un círculo es alguien, un rectángulo de esquina blanda es
   una cosa, un punto es un estado.
   ═══════════════════════════════════════════════════════════════════════════════════════════ */
.lean-hilo {
  /* EL PAPEL SOBRE EL QUE VA, que no es `--panel`: los tres que lo usan viven en
     `lean-cajon--lienzo`, que se pinta con `--bg` y en oscuro redefine los dos
     tokens dentro del cajón. Con `--panel`, el día pegado y el hueco de cada nodo
     salían como bandas más claras que el cajón. Quien lo monte sobre una tarjeta
     lo cambia aquí y en ningún otro sitio. */
  --hilo-papel: var(--bg);
  --hilo-hora: 38px; --hilo-nodo: 24px; --hilo-gap: 10px; --hilo-fila: 34px;
  --hilo-eje: calc(var(--hilo-hora) + var(--hilo-gap) + var(--hilo-nodo) / 2);
  --hilo-pistas: var(--hilo-hora) var(--hilo-nodo) minmax(0, 1fr) auto 14px;
  display: flex; flex-direction: column; gap: var(--s3); min-width: 0;
}

/* ── La barra: quién ha pasado por aquí ──────────────────────────────────────
   Cada cara ES un nodo del carril, así que filtrar no obliga a aprender otro
   vocabulario. Y CABE POR CONSTRUCCIÓN, nunca por desplazamiento: la primera
   versión metía las caras en un scroll lateral escondido y, con doce personas,
   de 1137 px se veían 346 — tres caras, una partida y ocho que nadie sabía que
   estaban. `Lean.hilo` las acomoda midiendo, en peldaños (ver `hiloAcomodar`). */
.lean-hilo__barra { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.lean-hilo__caras { display: flex; flex-wrap: nowrap; gap: var(--s1); min-width: 0; flex: 1; }
.lean-hilo__cara {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  height: var(--ctl-h-sm); padding: 0 9px 0 4px; border-radius: var(--r-ctl-sm);
  border: 1px solid color-mix(in srgb, var(--ink) 13%, transparent); background: var(--panel);
  font: inherit; font-size: var(--fs-sm); color: var(--ink); cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), transform var(--t-pulsa) var(--ease);
}
.lean-hilo__cara:hover { background: var(--al-pasar); }
.lean-hilo__cara:active { transform: scale(.97); }
.lean-hilo__cara:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
.lean-hilo__cara[aria-pressed="true"] { background: var(--elegido); border-color: var(--accion-filo); }
.lean-hilo__cara-n { font-family: var(--mono); font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.lean-hilo__cara .lean-hilo__marca,
.lean-hilo__cara .lean-hilo__emblema { width: 20px; height: 20px; box-shadow: none; }
/* Peldaño 2: la persona se queda en su cara; el nombre y la cuenta, en el globo. */
.lean-hilo__cara.es-compacta { width: var(--ctl-h-sm); padding: 0; gap: 0; justify-content: center; }
.lean-hilo__cara.es-compacta .lean-hilo__cara-t,
.lean-hilo__cara.es-compacta .lean-hilo__cara-n { display: none; }
/* Peldaño 3: «+N», con las caras de las que se esconden apiladas. */
.lean-hilo__cara--mas { padding: 0 9px 0 5px; gap: 0; }
.lean-hilo__cara--mas .lean-hilo__marca,
.lean-hilo__cara--mas .lean-hilo__emblema { width: 18px; height: 18px; border-radius: 50%; box-shadow: 0 0 0 2px var(--panel); }
.lean-hilo__cara--mas .lean-hilo__emblema .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
.lean-hilo__cara--mas > * + .lean-hilo__marca,
.lean-hilo__cara--mas > * + .lean-hilo__emblema { margin-left: -6px; }
.lean-hilo__cara--mas .lean-hilo__cara-n { margin-left: 7px; color: var(--ink); font-weight: var(--peso-titulo); }
.lean-hilo__cara--mas[aria-expanded="true"] { background: var(--al-pasar); }
/* Y el último: el mando de la barra se queda en su icono. */
.lean-hilo__barra .lean-boton.es-icono { width: var(--ctl-h-sm); padding: 0; }
.lean-hilo__barra .lean-boton.es-icono .lean-hilo__accion-t { display: none; }

/* La lista entera de «+N». Es un popover: va al top layer, así que no lo recorta
   el cajón, y el Escape y el clic fuera los pone el navegador. */
.lean-hilo__menu {
  position: fixed; inset: auto; margin: 0; padding: var(--s1); width: 240px; max-height: min(360px, 60vh); overflow-y: auto;
  background: var(--alzado); color: var(--ink); border: 1px solid var(--linde-alto); border-radius: var(--r-s); box-shadow: var(--shadow-lg);
  font-family: var(--ui); font-size: var(--fs-md);
  opacity: 0; transform: translateY(-4px);
  transition: opacity var(--t-sale) var(--ease-sale), transform var(--t-sale) var(--ease-sale),
    display var(--t-sale) allow-discrete, overlay var(--t-sale) allow-discrete;
}
.lean-hilo__menu:popover-open {
  opacity: 1; transform: none;
  transition: opacity var(--t-entra) var(--ease-entra), transform var(--t-entra) var(--ease-entra),
    display var(--t-entra) allow-discrete, overlay var(--t-entra) allow-discrete;
}
@starting-style { .lean-hilo__menu:popover-open { opacity: 0; transform: translateY(-4px); } }
.lean-hilo__menu-op {
  display: grid; grid-template-columns: 20px minmax(0, 1fr) auto 14px; align-items: center; gap: 9px;
  width: 100%; padding: 6px var(--s2); border: 0; border-radius: var(--r-xs); background: none; color: inherit;
  font: inherit; text-align: left; cursor: pointer; transition: background var(--t-fast) var(--ease);
}
.lean-hilo__menu-op:hover, .lean-hilo__menu-op:focus-visible { background: var(--al-pasar); outline: none; }
.lean-hilo__menu-op .lean-hilo__marca, .lean-hilo__menu-op .lean-hilo__emblema { width: 20px; height: 20px; box-shadow: none; }
.lean-hilo__menu-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lean-hilo__menu-v { width: 14px; height: 14px; color: var(--accion-tinta); opacity: 0; }
.lean-hilo__menu-op[aria-checked="true"] .lean-hilo__menu-v { opacity: 1; }

/* ── La lista y su carril ────────────────────────────────────────────────────
   Si hay más atrás, el carril NO acaba: se desvanece. Si no, acaba en el nodo del
   principio. Las dos cosas se leen sin rótulo. */
.lean-hilo__lista { position: relative; display: flex; flex-direction: column; container: hilo / inline-size; }
.lean-hilo__lista::before {
  content: ""; position: absolute; z-index: 0; width: 1px; background: var(--line);
  left: calc(var(--hilo-eje) - .5px); top: calc(var(--hilo-fila) / 2); bottom: calc(var(--hilo-fila) / 2);
}
.lean-hilo__lista.hay-mas::before { bottom: 0; -webkit-mask-image: linear-gradient(#000 calc(100% - 56px), transparent); mask-image: linear-gradient(#000 calc(100% - 56px), transparent); }

/* ── El día: pegado arriba al recorrer, con su rombo sobre el carril ─────────
   Lo que se busca en un hilo es CUÁNDO, y en doscientas filas la fecha tiene que
   estar a la vista sin volver arriba. `--hilo-pegado` es para quien lo monte en
   un desplazamiento CON relleno arriba: el pegado se mide contra el relleno y por
   encima asomaría la fila que pasa por detrás (visto en la maqueta). */
.lean-hilo__dia {
  position: sticky; top: var(--hilo-pegado, 0); z-index: var(--z-pegado);
  display: grid; grid-template-columns: var(--hilo-hora) var(--hilo-nodo) minmax(0, 1fr);
  column-gap: var(--hilo-gap); align-items: center;
  padding: var(--s4) 0 6px; background: var(--hilo-papel);
}
.lean-hilo__lista > .lean-hilo__dia:first-child { padding-top: var(--s1); }
.lean-hilo__dia::before { content: ""; position: absolute; left: calc(var(--hilo-eje) - .5px); top: 0; bottom: 0; width: 1px; background: var(--line); }
.lean-hilo__lista > .lean-hilo__dia:first-child::before { top: 50%; }
.lean-hilo__rombo {
  grid-column: 2; justify-self: center; position: relative; z-index: 1;
  width: 7px; height: 7px; border-radius: var(--r-micro); transform: rotate(45deg);
  background: var(--hilo-papel); box-shadow: inset 0 0 0 1.5px var(--faint), 0 0 0 3px var(--hilo-papel);
}
.lean-hilo__dia-t { grid-column: 3; display: flex; align-items: center; gap: var(--s3); min-width: 0; font-size: var(--fs-sm); font-weight: var(--peso-titulo); color: var(--ink); white-space: nowrap; }
.lean-hilo__dia-t > span { color: var(--muted); font-weight: var(--peso-texto); }
.lean-hilo__dia-t::after { content: ""; flex: 1; height: 1px; background: var(--line); }

/* ── La fila ── */
.lean-hilo__fila { position: relative; }
.lean-hilo__resumen {
  position: relative; list-style: none;
  display: grid; grid-template-columns: var(--hilo-pistas); column-gap: var(--hilo-gap); align-items: center;
  min-height: var(--hilo-fila); padding-right: 6px; border-radius: var(--r-xs);
  transition: background var(--t-fast) var(--ease);
}
.lean-hilo__resumen::-webkit-details-marker { display: none; }
details.lean-hilo__fila > .lean-hilo__resumen { cursor: pointer; }
details.lean-hilo__fila > .lean-hilo__resumen:hover { background: var(--al-pasar-leve); }
details.lean-hilo__fila > .lean-hilo__resumen:active { background: var(--al-pasar); transition-duration: var(--t-pulsa); }
.lean-hilo__resumen:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
.lean-hilo__fila[aria-current="true"] > .lean-hilo__resumen { background: var(--elegido); box-shadow: inset 0 0 0 1px var(--accion-filo); }

/* La hora NO va en --faint: tokens.css dice que no es un color de texto (2,18:1),
   y era justo lo que llevaba la de `np-hist`. */
.lean-hilo__hora { text-align: right; font-family: var(--mono); font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.lean-hilo__hora--ahora { font-family: var(--ui); font-weight: var(--peso-titulo); color: var(--accion-tinta); }

.lean-hilo__nodo { position: relative; z-index: 1; justify-self: center; display: grid; place-items: center; width: 22px; height: 22px; }
/* El hueco del carril alrededor de cada marca es del color del papel: es lo que
   impide que la línea la cruce. */
.lean-hilo__marca {
  width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; overflow: hidden;
  box-shadow: 0 0 0 3px var(--hilo-papel);
}
.lean-hilo__marca > * { width: 100%; height: 100%; min-width: 0; min-height: 0; }
/* «No consta quién»: el hueco de una cara, en discontinuo. No es el sistema ni
   nadie en concreto, y un avatar gris diría una de las dos cosas. */
.lean-hilo__nadie { background: var(--hilo-papel); color: var(--muted); border: 1.5px dashed var(--faint); font-size: 10px; font-weight: var(--peso-titulo); }
.lean-hilo__emblema {
  width: 22px; height: 22px; border-radius: var(--r-xs); display: grid; place-items: center;
  background: var(--accion-lavado); color: var(--accion-tinta);
  box-shadow: 0 0 0 3px var(--hilo-papel), inset 0 0 0 1px var(--accion-filo);
}
.lean-hilo__emblema .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
.lean-hilo__emblema--baja {
  background: color-mix(in srgb, var(--down) 12%, var(--panel)); color: var(--down);
  box-shadow: 0 0 0 3px var(--hilo-papel), inset 0 0 0 1px color-mix(in srgb, var(--down) 45%, var(--panel));
}
.lean-hilo__punto { position: relative; width: 10px; height: 10px; border-radius: 50%; background: var(--warn); color: var(--warn); box-shadow: 0 0 0 4px var(--hilo-papel); }
.lean-hilo__punto--down { background: var(--down); color: var(--down); }
.lean-hilo__punto--cierre { background: var(--hilo-papel); color: var(--tramo-tinta, var(--muted)); box-shadow: 0 0 0 4px var(--hilo-papel), inset 0 0 0 2px currentColor; }
.lean-hilo__punto--origen { width: 12px; height: 12px; background: var(--hilo-papel); color: var(--faint); box-shadow: 0 0 0 4px var(--hilo-papel), inset 0 0 0 2px currentColor; }
.lean-hilo__punto--abierta { width: 12px; height: 12px; background: var(--hilo-papel); color: var(--brand-fijo); box-shadow: 0 0 0 4px var(--hilo-papel), inset 0 0 0 3px currentColor; }
/* El SUCESO de un punto: se cae (el aro colapsa) o vuelve (brota). Son los
   mismos dos gestos y los mismos fotogramas que `lean-presencia`, a propósito:
   un aviso que salta y un equipo que se cae son la misma noticia.
   `both`, como allí: son sucesos de un solo uso —la clase se pone y se quita
   sola— y lo que retienen es el aro ya desvanecido; no hay transición posterior
   que puedan bloquear, porque el pseudo deja de existir con la clase. */
.lean-hilo__punto.es-baja::after, .lean-hilo__punto.es-alta::after { content: ""; position: absolute; border-radius: 50%; pointer-events: none; border: 1.5px solid currentColor; }
.lean-hilo__punto.es-baja::after { inset: -7px; animation: lean-presencia-cae 460ms var(--ease-entra) both; }
.lean-hilo__punto.es-alta::after { inset: 0; animation: lean-presencia-vuelve 520ms var(--ease-entra) both; }

/* EL MONTÓN: varias veces lo mismo. Las capas asoman hacia ABAJO, a lo largo del
   carril, porque son tiempo comprimido. Al desplegar se recogen: el montón ya
   está repartido debajo y seguir enseñándolo diría dos veces lo mismo. Y existen
   siempre, transparentes, para que ganar una capa sea una transición y no un salto. */
.lean-hilo__resumen > .lean-hilo__nodo::before, .lean-hilo__resumen > .lean-hilo__nodo::after {
  content: ""; position: absolute; left: 50%; z-index: -1; border-radius: 50%; opacity: 0;
  background: var(--hilo-papel); box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--ink) 24%, transparent);
  transform: translate(-50%, -8px); transition: transform var(--t-fast) var(--ease-mueve), opacity var(--t-fast) var(--ease);
}
.lean-hilo__resumen > .lean-hilo__nodo::before { width: 18px; height: 18px; top: 8px; }
.lean-hilo__resumen > .lean-hilo__nodo::after { width: 14px; height: 14px; top: 14px; }
.lean-hilo__resumen > .lean-hilo__nodo--monton::before,
.lean-hilo__resumen > .lean-hilo__nodo--monton3::after { opacity: 1; transform: translate(-50%, 0); }
.lean-hilo__fila[open] > .lean-hilo__resumen > .lean-hilo__nodo--monton::before,
.lean-hilo__fila[open] > .lean-hilo__resumen > .lean-hilo__nodo--monton3::after { opacity: 0; transform: translate(-50%, -8px); }

/* LO QUE SE RECORTA ES LA FRASE, NUNCA LA CUENTA NI EL VERBO. Con todo en una
   caja con puntos suspensivos, «pidió registrar el teléfono 192.168.1.201 ×5» se
   quedaba en «…teléfono 192…»; con el verbo encogiéndose primero, en «Leandro
   Pérez ×2», que no dice qué hizo. Las dos se vieron en la maqueta. */
.lean-hilo__frase { display: flex; align-items: baseline; min-width: 0; white-space: nowrap; overflow: hidden; font-size: var(--fs-md); color: var(--ink); }
.lean-hilo__frase > b { flex: none; font-weight: var(--peso-titulo); margin-right: .3em; }
.lean-hilo__que { flex: 0 1 auto; min-width: min(9ch, 100%); overflow: hidden; text-overflow: ellipsis; }
.lean-hilo__veces {
  flex: none; display: inline-block; margin-left: 6px; padding: 0 5px; min-width: 24px; text-align: center;
  font-family: var(--mono); font-size: var(--fs-xs); font-weight: var(--peso-titulo); line-height: 17px;
  color: var(--ink); background: color-mix(in srgb, var(--ink) 8%, transparent); border-radius: var(--r-xxs);
  font-variant-numeric: tabular-nums;
}
.lean-hilo__mas { flex: 0 100 auto; min-width: 0; overflow: hidden; margin-left: .3em; color: var(--muted); }
.lean-hilo__n-corto { display: none; }
.lean-hilo__matiz { color: var(--muted); }
.lean-hilo__mono { font-family: var(--mono); font-size: .94em; }
.lean-hilo__antes { color: var(--muted); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--muted) 55%, transparent); }
/* EN LA FILA, LO DE ANTES SE RECORTA POR SU CUENTA: con un disco entero a cada
   lado —«Samsung SSD 970 EVO 500 GB» → «Crucial P3 1000 GB»— el recorte de la
   frase se comía lo de AHORA, que es lo que se viene a mirar. Entero, al desplegar. */
.lean-hilo__que .lean-hilo__antes { display: inline-block; max-width: 14ch; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
.lean-hilo__flecha { color: var(--faint); }
.lean-hilo__ahora { font-weight: var(--peso-titulo); color: var(--ink); }
.lean-hilo__ancla .lean-hilo__frase { display: block; text-overflow: ellipsis; }

.lean-hilo__estado { position: relative; display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; }
.lean-hilo__estado .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
.lean-hilo__estado .lean-rueda { --rueda: 12px; }
.lean-hilo__estado[data-e="falla"] { color: var(--down); font-weight: var(--peso-medio); }
.lean-hilo__estado[data-e="abierto"] { color: var(--warn); font-weight: var(--peso-medio); }
.lean-hilo__estado[data-e="curso"] { color: var(--accion-tinta); }
.lean-hilo__estado[data-e="baja"] { color: var(--down); font-family: var(--mono); font-size: var(--fs-xs); font-weight: var(--peso-titulo); }
/* El remate de una orden que acaba bien: la rueda brota y el estado se va, porque
   lo bien hecho no deja marca. `both` por lo mismo que el aro del punto: la clase
   dura lo que el gesto y se quita sola. */
.lean-hilo__estado.es-remata::after {
  content: ""; position: absolute; left: 0; top: 50%; width: 12px; height: 12px; margin-top: -6px;
  border-radius: 50%; border: 1.5px solid var(--brand-fijo); pointer-events: none;
  animation: lean-presencia-vuelve 520ms var(--ease-entra) both;
}

.lean-hilo__chev { width: 14px; height: 14px; color: var(--faint); transition: transform var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }
details.lean-hilo__fila > .lean-hilo__resumen:hover .lean-hilo__chev { color: var(--ink); }
.lean-hilo__fila[open] > .lean-hilo__resumen .lean-hilo__chev { transform: rotate(90deg); color: var(--muted); }

/* EL PLIEGUE va en ::details-content (animaciones.md §5.3), nunca en el elemento
   cuyo display lo pone. Abrir abre HUECO, y ésa es la única cosa para la que la
   casa tiene `--asienta`; cerrar sale con `--ease-sale`, más deprisa. */
.lean-hilo__fila::details-content {
  height: 0; overflow: clip;
  transition: height var(--t-sale) var(--ease-sale), content-visibility var(--t-sale) allow-discrete;
}
.lean-hilo__fila[open]::details-content {
  height: auto;
  transition: height var(--t-asienta) var(--asienta), content-visibility var(--t-asienta) allow-discrete;
}
.lean-hilo__cuerpo { position: relative; padding-bottom: var(--s2); }

.lean-hilo__sub {
  position: relative; display: grid; grid-template-columns: var(--hilo-pistas); column-gap: var(--hilo-gap);
  align-items: start; padding: 5px 6px 5px 0; border-radius: var(--r-xs);
}
.lean-hilo__sub > .lean-hilo__hora { line-height: 18px; }
.lean-hilo__sub[role="button"] { cursor: pointer; transition: background var(--t-fast) var(--ease); }
.lean-hilo__sub[role="button"]:hover { background: var(--al-pasar-leve); }
.lean-hilo__sub[aria-current="true"] { background: var(--elegido); box-shadow: inset 0 0 0 1px var(--accion-filo); }
.lean-hilo__sub:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
.lean-hilo__sub-punto { grid-column: 2; justify-self: center; position: relative; z-index: 1; margin-top: 6px; width: 6px; height: 6px; border-radius: 50%; background: var(--faint); box-shadow: 0 0 0 3px var(--hilo-papel); }
.lean-hilo__sub-punto--falla { background: var(--down); }
.lean-hilo__sub-cuerpo { grid-column: 3 / 5; min-width: 0; font-size: var(--fs-md); line-height: 18px; }
.lean-hilo__sub-cab { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.lean-hilo__sub-cab > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lean-hilo__horas { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 1px 9px; font-family: var(--mono); font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }
.lean-hilo__horas .es-falla { color: var(--down); }
.lean-hilo__cambios { margin-top: 6px; display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 5px 14px; font-size: var(--fs-sm); }
.lean-hilo__campo { color: var(--muted); }
.lean-hilo__diff { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 7px; min-width: 0; font-family: var(--mono); font-size: var(--fs-sm); overflow-wrap: anywhere; }
.lean-hilo__nota-diff { font-family: var(--ui); color: var(--muted); }
.lean-hilo__motivo { margin-top: var(--s1); display: flex; gap: 6px; align-items: flex-start; font-size: var(--fs-sm); line-height: 17px; color: var(--muted); }
.lean-hilo__motivo .lean-ico { width: var(--ico-sm); height: var(--ico-sm); flex: none; margin-top: 2px; }
.lean-hilo__motivo--falla .lean-ico { color: var(--down); }
.lean-hilo__cita { margin: 2px 0 0; padding: 6px 10px; border-radius: var(--r-xs); background: var(--hondo); color: var(--ink); font-size: var(--fs-md); line-height: var(--alto-ui); white-space: pre-wrap; overflow-wrap: anywhere; }
.lean-hilo__acciones { margin-top: var(--s2); display: flex; gap: var(--s2); flex-wrap: wrap; }

/* ── EL TRAMO: lo que dura pinta el carril entre su principio y su final ─────
   Se reparte como atributo por fila (lo pone `Lean.hilo`), no como una barra
   posicionada: así se estira solo cuando una fila de en medio se despliega, y no
   hay que volver a medir nada. */
.lean-hilo [data-tramo]::after {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  left: calc(var(--hilo-eje) - 1.5px); width: 3px; top: 0; bottom: 0; border-radius: var(--r-micro);
  background: var(--tramo-c); transform-origin: 50% 100%;
}
.lean-hilo [data-tramo="warn"]  { --tramo-c: color-mix(in srgb, var(--warn) 62%, transparent); --tramo-tinta: var(--warn); }
.lean-hilo [data-tramo="down"]  { --tramo-c: color-mix(in srgb, var(--down) 62%, transparent); --tramo-tinta: var(--down); }
.lean-hilo [data-tramo="marca"] { --tramo-c: color-mix(in srgb, var(--brand-fijo) 50%, transparent); --tramo-tinta: var(--brand-fijo); }
.lean-hilo [data-tramo-lado="ini"]::after { bottom: auto; height: calc(var(--hilo-fila) / 2); }
.lean-hilo [data-tramo-lado="fin"]::after { top: calc(var(--hilo-fila) / 2); }
/* Un tramo que llega arriba del todo es que sigue abierto: se desvanece, no acaba. */
.lean-hilo [data-tramo-cabeza]::after { -webkit-mask-image: linear-gradient(transparent, #000 22px); mask-image: linear-gradient(transparent, #000 22px); }
.lean-hilo__dia[data-tramo] .lean-hilo__rombo { box-shadow: inset 0 0 0 1.5px var(--tramo-tinta), 0 0 0 3px var(--hilo-papel); }

.lean-hilo__pie { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; font-size: var(--fs-sm); color: var(--muted); padding-left: calc(var(--hilo-hora) + var(--hilo-gap) * 2 + var(--hilo-nodo)); }
.lean-hilo__vacio { padding: var(--s6) 0; text-align: center; color: var(--muted); font-size: var(--fs-md); }

/* EL HILO ESTRECHO (el portal, un móvil, un cajón a 400 px). Se mide el hilo y no
   la ventana, porque el mismo componente vive en un cajón y en una página. Lo que
   se sacrifica, por este orden: «y N más» —se ve igual al desplegar—, el
   apellido, y la palabra de lo que se quedó sin salir, que queda en su icono con
   el texto en el globo. Nunca el verbo ni la cuenta. */
@container hilo (max-width: 440px) {
  .lean-hilo__mas { display: none; }
  .lean-hilo__n-largo { display: none; }
  .lean-hilo__n-corto { display: inline; }
  .lean-hilo__estado:is([data-e="caduca"], [data-e="espera"], [data-e="curso"]) .lean-hilo__estado-t { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .lean-hilo__fila::details-content, .lean-hilo__fila[open]::details-content { transition-duration: 1ms; }
  .lean-hilo__punto.es-baja::after, .lean-hilo__punto.es-alta::after, .lean-hilo__estado.es-remata::after { animation: none; opacity: 0; }
  .lean-hilo__menu, .lean-hilo__menu:popover-open { transition-duration: 1ms; }
  .lean-hilo__resumen > .lean-hilo__nodo::before, .lean-hilo__resumen > .lean-hilo__nodo::after { transition: none; }
}

/* ══════════════════════════ lean-puerta · LA PUERTA ══════════════════════════
   Lo que ve alguien ANTES de estar dentro: la pantalla de acceso, el asistente
   de primer arranque, el enlace para volver a entrar y el que lleva algo dentro
   para un cliente. Cuatro páginas que no cargan `app.css` y que son las únicas
   que ve alguien de fuera.

   POR QUÉ ES UNA PIEZA DEL SISTEMA Y NO EL CSS DE CADA PÁGINA. Cada una se
   escribía la suya, y eso se veía: `login.html` y `setup.html` llevaban el MISMO
   lavado de tres degradados radiales con dos juegos de porcentajes distintos, y
   las otras dos no llevaban nada. Cuatro anchos (380, 460, 400, 460), tres
   métricas de campo y cuatro formas de separar un botón de lo de arriba. Un
   recibidor copiado es un recibidor que se separa.

   Y POR QUÉ NO ES UNA `lean-tarjeta`. Una tarjeta es papel de la aplicación: se
   apila con otras, se filtra, se ordena. Esto es lo contrario — hay UNA, no se
   apila con nada y lo que hace es dar paso. Es la misma distinción que la regla
   5-bis hizo con la consola, y por eso hereda su gramática: bisel de un píxel,
   filo de luz por arriba y zócalo con los testigos. No es la consola —eso son
   los técnicos ya dentro—: es la puerta del mismo aparato.
   ════════════════════════════════════════════════════════════════════════════ */

/* EL ANCHO ES UNO, y ésta es la mitad del valor de la pieza. Tres anchos para
   cuatro pantallas de la misma conversación hacen que la placa SALTE al pasar de
   una a otra, y un salto de ochenta píxeles entre dos pasos del mismo gesto se
   lee como que has cambiado de sitio. Con `--ancha` y `--extra` los tres saltos
   que quedan son a propósito y viajan: el asistente crece cuando enseña un
   bloque de configuración que a 400 px habría que partir. */
.lean-puerta {
  position: relative; z-index: 1;
  width: var(--puerta-w, 400px); max-width: 100%;
  transition: width var(--t) var(--ease);
}
.lean-puerta--ancha { --puerta-w: 468px; }
.lean-puerta--extra { --puerta-w: 660px; }

/* ── EL FONDO: la línea en reposo ──────────────────────────────────────────
   NetProof mide latencia, así que su puerta enseña una traza. No es una metáfora
   que se le haya ocurrido a nadie: el perfil que barre aquí ES EL DEL LOGOTIPO
   —los mismos seis puntos del icono, normalizados—, de modo que la marca y el
   ambiente son la misma figura a dos escalas.

   Y CUENTA ALGO, que es la condición para que exista (docs/animaciones.md §1):
   va al ritmo de sonda de la casa mientras se espera, se acelera mientras el
   servidor comprueba y da un pico cuando contesta que no. En una pantalla de
   acceso eso responde a la única pregunta que se hace quien mira: si hay alguien
   al otro lado.

   ES UNA EXCEPCIÓN DECLARADA a «el movimiento describe cambios en los datos», y
   es la MISMA que ya tiene la consola: esto no es papel de la aplicación, es la
   cara frontal de un instrumento, y un instrumento encendido se mueve. Lo que no
   vale es extenderla: dentro del panel esta pieza no pinta nada.

   `z-index: 0` con el contenido en 1, y no `--z-fondo`: los z negativos se
   pintan por detrás del fondo del elemento que los contiene, así que un `-1`
   aquí depende de si `body` acaba pintando su color o lo propaga al lienzo del
   navegador. Esto es orden dentro de una caja y no una capa de la aplicación,
   que es justo lo que la escala de `--z-*` dice que se queda con su número. */
.lean-puerta__fondo {
  position: fixed; inset: 0; z-index: 0;
  width: 100%; height: 100%; pointer-events: none;
  /* Nace invisible y lo enciende `puerta.js` con el primer fotograma pintado.
     Sin esto, entre que el lienzo existe y el guion mide la ventana hay un cuadro
     en blanco que se lleva por delante la entrada de la placa. */
  opacity: 0; transition: opacity var(--t-vista) var(--ease-entra);
}
.lean-puerta__fondo.es-viva { opacity: 1; }

/* ── LA PLACA ──
   `--delante` y no `--panel`: esto está DELANTE de todo, es la altura de un
   diálogo. En claro los dos valen lo mismo y está bien; en oscuro sube de papel,
   que es como un tema oscuro dice «esto se levanta» (tokens.css · las alturas). */
.lean-puerta__placa {
  background: var(--delante); border: 1px solid var(--linde-alto);
  border-radius: var(--r); padding: var(--s6) var(--s5) var(--s5);
  box-shadow: var(--shadow-xl), inset 0 1px 0 var(--filo-luz);
  display: flex; flex-direction: column; gap: var(--s5);
}
/* LA PLACA VIAJA AL CAMBIAR DE PASO, no salta. `--asienta` es la única curva de
   la casa que rebasa su destino un 4 %, y se usa exactamente donde algo ABRE
   HUECO — que es esto: la caja se hace sitio y lo de debajo se aparta.

   `overflow: hidden` sólo MIENTRAS dura el viaje: hace falta para que el
   contenido nuevo no asome por debajo del borde antes de que la caja llegue, y
   no puede quedarse puesto porque el globo de un campo y el menú de credenciales
   del navegador salen fuera. */
.lean-puerta__placa.es-viajando {
  overflow: hidden;
  transition: height var(--t-asienta) var(--asienta);
}
@media (prefers-reduced-motion: reduce) {
  .lean-puerta__placa.es-viajando { transition: none; }
}

/* ── LA CORNISA: el logotipo que se DIBUJA ──
   El trazo del icono de NetProof sale de su caja redondeada y se convierte en el
   logotipo. Es el mismo path, sin tocar un número: el icono ya era un trazo de
   latencia y estaba encerrado en un cuadrado de 44 px que no dejaba verlo —
   dentro de él lo que se ve es el cuadrado azul, que es el formato con el que un
   sistema operativo pide un lanzador. */
.lean-puerta__cornisa { display: flex; align-items: center; gap: var(--s3); }
.lean-puerta__firma { width: 62px; height: 30px; flex: none; overflow: visible; display: block; }
.lean-puerta__firma path {
  fill: none; stroke: var(--brand); stroke-width: 3.4;
  stroke-linecap: round; stroke-linejoin: round;
  /* El 95 no es redondo por gusto: el recorrido mide unas 79 unidades y el trazo
     tiene que quedar oculto del todo al empezar, con holgura para los cabos
     redondeados. Un valor corto deja un muñón visible en el primer fotograma. */
  stroke-dasharray: 95; stroke-dashoffset: 95;
  animation: lean-puerta-firma calc(var(--t-asienta) * 1.15) var(--ease-entra) forwards;
}
@keyframes lean-puerta-firma { to { stroke-dashoffset: 0; } }

/* El punto que remata el trazo. No es un adorno del logotipo: es el mismo eco de
   `lean-presencia`, y por eso llega DESPUÉS y con `--asienta` —lo que se posa
   rebasa un pelo y vuelve—. Escalonado, que es la ley: primero el trazo y un
   instante después lo que lo remata, en el orden en que se leen los dos. */
.lean-puerta__eco {
  r: 0; fill: var(--brand);
  animation: lean-puerta-eco var(--t) var(--asienta) calc(var(--t-asienta) * .95) forwards;
}
@keyframes lean-puerta-eco { to { r: 3.6; } }

/* LA INSIGNIA: la firma, el día de una fiesta.
   El día que toca un logo de temporada, el servidor cambia la firma por el
   dibujo de esa fiesta —la calabaza, la bola del árbol— en su badge, a 64 px
   (ver http_logo.go). Es la misma marca vestida, no una segunda: el pulso de
   dentro son los seis puntos del icono de siempre, y SE DIBUJA como la firma,
   con el mismo gesto y la misma duración. Lo demás no se dibuja, ya está cuando
   llega el trazo: una fiesta no se construye, se lleva puesta.

   El pulso se encuentra por su `d`, que es el mismo en todos los dibujos —lo
   vigila `http_logo_test.go`—, así que no hay que marcar nada dentro de ellos.
   Y 70 de guion por lo mismo que el 95 de la firma: el pulso mide unas 59
   unidades, y el cabo redondeado necesita holgura para no asomar en el primer
   fotograma. */
.lean-puerta__insignia { width: 64px; height: 64px; flex: none; display: block; }
.lean-puerta__insignia path[d="M9 26 H16 L19.5 18 L23 30 L27 34 L40 13"] {
  stroke-dasharray: 70; stroke-dashoffset: 70;
  animation: lean-puerta-firma calc(var(--t-asienta) * 1.15) var(--ease-entra) forwards;
}

.lean-puerta__nombre { font-family: var(--disp); font-size: var(--fs-2xl); font-weight: var(--peso-titulo); letter-spacing: var(--letra-titular); }
.lean-puerta__nombre span { display: block; font-family: var(--ui); font-size: var(--fs-sm);
  font-weight: var(--peso-texto); color: var(--muted); margin-top: 1px; letter-spacing: var(--letra-normal); }

/* ── EL RÓTULO DE LA PANTALLA ──
   Título y bajada son UNA cosa, así que van juntos y como un solo elemento de la
   columna: con el hueco de dos campos entre ellos, la bajada dejaría de leerse
   como del título y empezaría a parecer un párrafo suelto.

   Y LA BAJADA SOBRA CASI SIEMPRE. De las quince pantallas de esta familia, once
   no la llevan: el título, el nombre del campo y el verbo del botón ya dicen lo
   que hay que hacer, y repetirlo gasta el renglón más caro de la pantalla. Sólo
   se escribe cuando lleva un dato o una consecuencia que no está en ninguna otra
   parte —«al guardarla se cierran las demás sesiones»—, que es la regla de
   `docs/lenguaje.md` §3 aplicada aquí. */
.lean-puerta__rotulo { display: flex; flex-direction: column; gap: var(--s1); }
.lean-puerta__rotulo h1 { font-family: var(--disp); font-size: var(--fs-3xl); font-weight: var(--peso-titulo);
  letter-spacing: var(--letra-titular); margin: 0; text-wrap: balance; }
.lean-puerta__rotulo p { margin: 0; color: var(--muted); font-size: var(--fs-lg); line-height: var(--alto-ui); }
.lean-puerta__rotulo p strong { color: var(--ink); font-weight: var(--peso-titulo); }

/* 10 px entre campos y no 24: con la etiqueta DENTRO de la caja ya no hay
   rótulos sueltos de los que separarse, así que lo que separa dos campos sólo
   tiene que distinguirlos, no agruparlos con nada. */
.lean-puerta__campos { display: flex; flex-direction: column; gap: var(--s3); }

/* EL ZÓCALO SE RETIRÓ, y la razón es de una línea: **si la página se está
   viendo, el servidor ha contestado**. Llevaba un punto de presencia y el nombre
   de la máquina, y ninguno de los dos decía nada que no estuviera ya dicho — la
   dirección la acaba de escribir quien mira, y el punto sólo podía estar verde.
   Un testigo que no puede dar una mala noticia no es un testigo: es un adorno con
   forma de dato, que es la peor clase de adorno. */

/* ══════════════════════════════════════════════════════════════════════════
   lean-puerta__campo · EL CAMPO DE UNA PUERTA
   --------------------------------------------------------------------------
   El componente con más estados del sistema, y no por capricho: doce, de los
   cuales cuatro son los que ninguna de estas páginas atendía.

   LA ETIQUETA VIVE DENTRO. En un formulario del panel va encima porque hay
   veinte campos y el ojo recorre una columna de rótulos. En una puerta hay dos,
   y el rótulo encima gasta un renglón por campo para nombrar lo que el propio
   campo ya nombra — con el remate de que se decía DOS veces: la etiqueta ponía
   «Usuario» y el hueco de dentro ponía «tu.usuario».

   Con la etiqueta dentro se lee lo mismo en la mitad de renglones, y el campo
   puede medir 56 px: donde un control deja de parecer un formulario y empieza a
   parecer una pieza. Los 44 de `--ctl-h-lg` se quedan para el botón, que no
   lleva nada dentro que recolocar. Es una talla PROPIA de esta familia y no una
   cuarta talla del sistema: fuera de una puerta no la usa nadie.

   LO QUE MÁS CUESTA DE UNA ETIQUETA FLOTANTE NO ES LA ETIQUETA: es que el
   navegador rellena el campo sin que nadie lo toque, y entonces se queda encima
   del texto. Está resuelto abajo, y es el motivo de que esta pieza tenga doce
   estados en vez de cuatro.
   ══════════════════════════════════════════════════════════════════════════ */

.lean-puerta__campo {
  position: relative; display: flex; align-items: center;
  height: 56px; padding: 0 var(--s4); gap: 11px;
  background: var(--hondo); border: 1px solid var(--line); border-radius: var(--r);
  transition: border-color var(--t-fast) var(--ease),
              box-shadow   var(--t-fast) var(--ease),
              background   var(--t-fast) var(--ease);
}
.lean-puerta__campo:hover:not(:focus-within):not(.es-mal) {
  border-color: color-mix(in srgb, var(--ink) 22%, var(--line));
}
.lean-puerta__campo:focus-within {
  border-color: var(--brand); background: var(--panel);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}

/* El icono del campo dice QUÉ ES, así que no se mueve nunca (animaciones.md
   §3.4). El único que se mueve es el de la derecha, que dice qué se hace. */
.lean-puerta__campo > .lean-ico { width: var(--ico-md); height: var(--ico-md); flex: none; color: var(--faint);
  transition: color var(--t-fast) var(--ease); }
.lean-puerta__campo:focus-within > .lean-ico { color: var(--brand-ink); }
.lean-puerta__campo.es-mal > .lean-ico { color: var(--down); }

.lean-puerta__caja { position: relative; flex: 1; min-width: 0; height: 100%; }

/* El `input` ocupa la caja entera y empuja su texto hacia abajo para dejar sitio
   a la etiqueta cuando sube. `padding-top` y no `align-items`: con flex, el texto
   de un campo relleno por el navegador se recoloca un fotograma después y se ve
   el salto. */
.lean-puerta__campo input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border: 0; background: none; outline: none; color: var(--ink);
  font: var(--peso-medio) var(--fs-lg)/var(--alto-1) var(--ui); padding: 19px 0 0;
}
.lean-puerta__campo input::placeholder { color: transparent; }
.lean-puerta__campo input.lean-u-mono { font-size: var(--fs-lg); letter-spacing: var(--letra-codigo); }

.lean-puerta__rot {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  color: var(--muted); font-size: var(--fs-lg); pointer-events: none;
  transform-origin: left center; white-space: nowrap;
  transition: transform var(--t) var(--ease), color var(--t-fast) var(--ease);
}
/* Las tres condiciones que suben la etiqueta, y la tercera es la que se olvida:
   `:autofill` es lo que pone el navegador al rellenar una contraseña guardada, y
   sin ella la etiqueta se queda encima del texto que acaba de escribir. Va con
   `:-webkit-autofill` al lado porque el estándar es reciente y el prefijo sigue
   siendo el que contesta en la mayor parte del parque. */
.lean-puerta__campo input:focus ~ .lean-puerta__rot,
.lean-puerta__campo input:not(:placeholder-shown) ~ .lean-puerta__rot,
.lean-puerta__campo input:-webkit-autofill ~ .lean-puerta__rot,
.lean-puerta__campo input:autofill ~ .lean-puerta__rot {
  transform: translateY(-18px) scale(.76);
}
.lean-puerta__campo:focus-within .lean-puerta__rot { color: var(--brand-ink); }
.lean-puerta__campo.es-mal .lean-puerta__rot { color: var(--down); }

/* ── EL AUTOCOMPLETADO DEL NAVEGADOR ──
   Chrome pinta el fondo de un campo autorrellenado con un amarillo de hoja de
   usuario que `background` no pisa. La única forma de taparlo es una sombra
   interior maciza, y el color del texto hay que darlo con `-webkit-text-fill-color`
   porque `color` tampoco llega.

   Sin esto, el tema oscuro enseña un campo amarillo pálido con letra casi
   invisible — que es como se veía el acceso en cuanto el navegador recordaba la
   contraseña, o sea SIEMPRE para quien entra todos los días. Es el estado
   mayoritario de esta pantalla y era el único sin atender.

   La transición de 9999 s no es un truco sucio: es la forma documentada de
   impedir que Chrome reponga su amarillo, porque la propiedad no llega nunca a
   su destino. */
.lean-puerta__campo input:-webkit-autofill,
.lean-puerta__campo input:-webkit-autofill:hover,
.lean-puerta__campo input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 60px var(--hondo) inset;
  box-shadow: 0 0 0 60px var(--hondo) inset;
  caret-color: var(--ink);
  transition: background-color 9999s ease-in-out 0s;
}
.lean-puerta__campo:focus-within input:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 60px var(--panel) inset;
  box-shadow: 0 0 0 60px var(--panel) inset;
}

/* Y EL ACUSE DE QUE LO HA PUESTO EL NAVEGADOR.
   Un campo que aparece relleno sin que nadie lo haya tocado es lo único de esta
   pantalla que ocurre solo, así que se cuenta: un filo de marca recorre el canto
   inferior una vez y se apaga. Dice «esto no lo has escrito tú» en 416 ms y sin
   una palabra.

   Se detecta con `animationstart` sobre una animación vacía enganchada al
   selector, que es la única forma que hay de que el navegador avise de esto. */
@keyframes lean-puerta-autofill { from { opacity: 1; } to { opacity: 1; } }
.lean-puerta__campo input:-webkit-autofill { animation-name: lean-puerta-autofill; animation-duration: 1ms; }
.lean-puerta__campo input:autofill { animation-name: lean-puerta-autofill; animation-duration: 1ms; }

.lean-puerta__filo {
  position: absolute; left: 12px; right: 12px; bottom: -1px; height: 2px;
  border-radius: var(--r-micro); background: var(--brand); opacity: 0;
  transform: scaleX(0); transform-origin: left center; pointer-events: none;
}
.lean-puerta__campo.es-recordado .lean-puerta__filo {
  animation: lean-puerta-recordado calc(var(--t-mueve) * 1.3) var(--ease) forwards;
}
@keyframes lean-puerta-recordado {
  0%   { opacity: 0;  transform: scaleX(0); transform-origin: left center; }
  22%  { opacity: .9; transform: scaleX(1); transform-origin: left center; }
  55%  { opacity: .9; transform: scaleX(1); transform-origin: right center; }
  100% { opacity: 0;  transform: scaleX(0); transform-origin: right center; }
}

/* ── El error ──
   El filo rojo va en el CAMPO y el texto sale arriba, en la banda de la placa.
   Debajo del campo es exactamente donde el navegador despliega su lista de
   credenciales guardadas: un mensaje ahí está tapado la mitad de las veces. */
.lean-puerta__campo.es-mal { border-color: var(--down);
  background: color-mix(in srgb, var(--down) 5%, var(--hondo)); }
.lean-puerta__campo.es-mal:focus-within {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--down) 20%, transparent); }

/* ── Bloq Mayús ──
   El estado que más veces explica un «usuario o contraseña incorrectos», y que
   ninguna de estas páginas contaba. Va DENTRO del campo y a la derecha, por lo
   mismo que el error. */
.lean-puerta__mayus {
  display: inline-flex; align-items: center; gap: 5px; flex: none;
  font-size: var(--fs-xs); font-weight: var(--peso-titulo); letter-spacing: var(--letra-rotulo);
  color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, transparent);
  padding: 3px 8px; border-radius: var(--r-pill); white-space: nowrap;
  animation: lean-puerta-asoma var(--t-entra) var(--ease-entra);
}
.lean-puerta__mayus .lean-ico { width: var(--ico-sm); height: var(--ico-sm); }
@keyframes lean-puerta-asoma { from { opacity: 0; transform: translateY(3px) scale(.94); } }

/* ── EL OJO ──
   Un icono que cambia de forma DE VERDAD: el párpado se cierra sobre el iris y
   la barra se traza encima. Era un cambio de `href` del `<use>`, o sea un salto
   entre dos dibujos.

   Se anima `d` como propiedad, con los dos caminos escritos con la MISMA
   estructura de segmentos — que es la condición para que un morfeo exista y no
   sea un fundido cruzado disfrazado. Y por eso este dibujo NO sale del sprite:
   `li-eye` y `li-eye-off` son dos caminos distintos y entre ellos no hay
   interpolación posible. */
.lean-puerta__ojo {
  display: grid; place-items: center; width: 32px; height: 32px; flex: none;
  border: 0; background: none; border-radius: var(--r-s); color: var(--faint); cursor: pointer;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.lean-puerta__ojo:hover { color: var(--ink); background: var(--al-pasar); }
.lean-puerta__ojo:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }
.lean-puerta__ojo svg { width: 20px; height: 20px; fill: none; stroke: currentColor;
  stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.lean-puerta__ojo .parpado {
  d: path("M1.5 10 C 5 4.6, 13 4.6, 16.5 10 C 13 15.4, 5 15.4, 1.5 10 Z");
  transition: d var(--t) var(--ease);
}
.lean-puerta__ojo .iris { r: 3; transition: r var(--t) var(--ease); }
.lean-puerta__ojo .barra { stroke-dasharray: 22; stroke-dashoffset: 22;
  transition: stroke-dashoffset var(--t) var(--ease-entra); }
.lean-puerta__ojo[aria-pressed="true"] .parpado {
  d: path("M1.5 10 C 5 7.8, 13 7.8, 16.5 10 C 13 12.2, 5 12.2, 1.5 10 Z");
}
.lean-puerta__ojo[aria-pressed="true"] .iris { r: 0; }
.lean-puerta__ojo[aria-pressed="true"] .barra { stroke-dashoffset: 0; }

/* ── Sitio para el gestor de contraseñas ──
   1Password, Bitwarden y el resto inyectan su botón dentro del campo, pegado al
   canto derecho y por encima de lo que haya. Sin reserva cae justo sobre el ojo
   y se pulsa uno creyendo que se pulsa el otro. Doce píxeles no se notan cuando
   no hay extensión y salvan el campo cuando la hay. */
.lean-puerta__campo--conojo { padding-right: 10px; }
.lean-puerta__campo--conojo .lean-puerta__caja { padding-right: 12px; }

/* ── En espera mientras se comprueba ──
   No es `disabled`: un campo deshabilitado pierde el foco y el lector de pantalla
   deja de anunciarlo. Es `readonly` con la pinta de estar esperando. */
.lean-puerta__campo.es-esperando { opacity: .6; }
.lean-puerta__campo.es-esperando input { cursor: default; }

/* ══ La banda de aviso ══
   Va ARRIBA de la placa y a sangre: abre hueco con `--asienta` y empuja al
   contenido, que es lo que hace que se vea llegar sin necesidad de parpadear. Y
   se queda hasta que cambia lo que la causó. */
/* EL FALLO VA CON LOS CAMPOS, no con el logotipo. La banda es un hijo más de la
   placa, y la placa separa sus hijos con `gap`: con eso el aviso quedaba a 24 px
   del logotipo y a 40 de los campos (su margen más el hueco), o sea pegado a la
   cabecera y lejos de lo que explica. Los márgenes lo invierten SIN mover la
   placa cerrada: arriba suma un hueco (logotipo → campos sigue en 48 con la banda
   a cero, como antes), abajo lo devuelve, y el aviso queda a 12 px de los campos
   —el mismo orden de distancia que hay entre ellos—. */
.lean-puerta__banda {
  display: grid; grid-template-rows: 0fr;
  margin: var(--s5) 0 calc(-1 * var(--s5));
  transition: grid-template-rows var(--t-asienta) var(--asienta);
}
.lean-puerta__banda.es-vista { grid-template-rows: 1fr; }
.lean-puerta__banda > div { overflow: hidden; }
.lean-puerta__banda .lean-aviso { margin-bottom: var(--s3); }

/* ══ Las salidas ══
   «He olvidado la contraseña» y «Volver» son SALIDAS, no alternativas a entrar:
   tinta apagada, centradas y debajo del botón. Arriba competirían con él; al lado
   del campo parecerían una ayuda del campo. */
.lean-puerta__salida {
  display: block; margin: 0 auto; padding: 2px 6px; border: 0; background: none;
  font: inherit; font-size: var(--fs-md); color: var(--muted); cursor: pointer;
  text-decoration: none; border-radius: var(--r-xs);
  transition: color var(--t-fast) var(--ease);
}
.lean-puerta__salida:hover { color: var(--ink); text-decoration: underline; }
.lean-puerta__salida:focus-visible { outline: var(--foco-ancho) solid var(--foco); outline-offset: var(--foco-hueco); }

/* La letra pequeña de una puerta: lo que se lee despacio y una sola vez. */
.lean-puerta__nota { font-size: var(--fs-sm); line-height: var(--alto-ui); color: var(--muted); margin: 0; }

/* Quién está entrando, en el segundo paso. Va pegado al título porque es de lo
   que el título habla: se ha dado la contraseña de esta cuenta y falta el código. */
.lean-puerta__quien { display: flex; align-items: center; gap: var(--s2); margin: 0;
  font-size: var(--fs-md); color: var(--muted); }
.lean-puerta__quien strong { color: var(--ink); font-weight: var(--peso-titulo); }
.lean-puerta__quien .lean-ico { width: var(--ico); height: var(--ico); flex: none; color: var(--ok); }

/* ══ La entrada, escalonada ══
   18 ms entre hermanos, que es el retardo del sistema, y primero el continente y
   después el contenido. `--i` lo pone el marcado, que es quien sabe el orden. */
/* LA ENTRADA VA EN EL CONTENEDOR Y NO EN LA PLACA, y no es un detalle de gusto:
   la placa es el elemento que recibe `lean-paso--*` al cambiar de paso, y las dos
   reglas declaran `animation` con la MISMA especificidad. Gana la que va después
   en la hoja, que sería ésta —`lean-paso` vive ochocientas líneas más arriba—,
   así que con la entrada puesta en la placa el cambio de paso no llegaría a
   ejecutarse. Sin error, sin aviso, y con el paso cambiando de golpe: exactamente
   la familia de fallos que `docs/animaciones.md` §7 cataloga. */
.lean-puerta { animation: lean-puerta-entra var(--t-vista) var(--ease-entra) both; }
@keyframes lean-puerta-entra { from { opacity: 0; transform: translateY(10px) scale(.985); } }
/* EL ESCALONADO Y EL PASO SON DOS COSAS, y estuvieron en la misma clase hasta
   que se vio en pantalla. `__entra` es «lo de dentro llega de uno en uno» y lo
   lleva la placa; `__paso` es «esto es un paso de la conversación», que además
   APILA lo suyo con un hueco. Juntas, la placa heredaba el hueco del paso y en el
   paso no había ninguno: el «Volver» de la vía de soporte salía pegado al correo
   que tenía encima. */
.lean-puerta__entra > * { animation: lean-puerta-llega var(--t-entra) var(--ease-entra) both;
  animation-delay: calc(var(--i, 0) * var(--escalon) + var(--t-pulsa)); }
@keyframes lean-puerta-llega { from { opacity: 0; transform: translateY(7px); } }
/* Y UN `<form>` NO ENTRA: es un contenedor sin forma, y lo que se ve llegar son
   los campos y el botón que lleva dentro. Medido en la pantalla de acceso: sin
   esto el formulario se fundía con `--i` a cero, o sea a los 70 ms, ADELANTÁNDOSE
   a la banda de aviso que tiene encima —que va en 88— y arrastrando consigo a sus
   propios hijos, que además se funden por su cuenta. Dos animaciones sobre el
   mismo píxel y una de ellas fuera de orden. */
.lean-puerta__entra > form { animation: none; }

/* El paso apila lo suyo con el hueco de la casa. 16 y no 24: dentro de un paso
   las piezas se leen como una sola cosa —el rótulo, los campos, el botón y su
   salida—, y con el hueco de la placa se separarían como si fueran bloques. */
.lean-puerta__paso { display: flex; flex-direction: column; gap: var(--s4); }

/* Y UN `<form>` DENTRO DE UN PASO APILA IGUAL. Esto no es simetría: es que el
   formulario es transparente —no se ve, no entra y no tiene forma— así que lo que
   lleva dentro tiene que respirar como si colgara del paso directamente.

   Sin esta línea sus hijos salían PEGADOS, y medido en la pantalla de elegir
   contraseña era exactamente cero: el veredicto —«le faltan tres caracteres»— y
   el botón de guardar compartían el mismo píxel. El renglón que avisa de que algo
   no vale, tocando el botón que lo manda. */
.lean-puerta__paso > form { display: flex; flex-direction: column; gap: var(--s4); }
/* El zócalo es la base del instrumento y llega el último. SÓLO se funde: el
   desplazamiento ya lo hace el contenedor, y repetirlo aquí lo movería el doble. */
/* Y llega cuando la placa ha terminado de entrar, que es `--t-vista`: un número
   suelto aquí sería un plazo elegido a ojo para caer después de otro, y dos
   números así acaban solapándose el día que la máquina va lenta. */

/* ══════════════════════════ lean-puerta__vale ══════════════════════════
   EL ENLACE QUE SE CONSUME: a dónde fue y cuánto le queda.

   Aquí hubo un icono de sobre metido en un cuadrado redondeado con fondo lavado,
   que es el relleno con el que se tapa un hueco cuando no se sabe qué poner —
   decía «correo» a quien acababa de pedir un correo. Lo que de verdad hay que
   contar es que **el enlace es un objeto con vida útil**, y eso se enseña con una
   regla graduada que se consume, no con un porcentaje: un plazo en tanto por
   ciento obliga a convertirlo mentalmente a minutos, que es el dato que se
   quiere. Un panel que mide redes tiene instrumentos de medida, no insignias.

   Y LA MISMA PIEZA SIRVE PARA LOS DOS MOMENTOS: llena cuando el enlace acaba de
   salir y `--gastado` cuando ya no vale. No son dos pantallas que se parecen; es
   un objeto en dos momentos, y esa continuidad es la que hace que la segunda se
   entienda sin leer nada.

   OJO CON LO QUE SE ESCRIBE EN `__a`: ahí NO va la dirección de correo, ni
   siquiera tapada. Enseñarla confirma que esa cuenta existe y que tiene correo,
   que es justo lo único que esa pantalla no puede decir — el servidor contesta
   lo mismo exista o no. */
.lean-puerta__vale {
  border: 1px solid var(--line); border-radius: var(--r); overflow: hidden;
  background: var(--hondo);
}
.lean-puerta__vale-cab {
  display: flex; align-items: center; gap: var(--s2);
  padding: 12px var(--s4) 11px; border-bottom: 1px solid var(--line);
}
.lean-puerta__vale-cab .rot { font-size: var(--fs-xs); letter-spacing: var(--letra-rotulo);
  text-transform: uppercase; color: var(--muted); }
.lean-puerta__vale-cab .a { font-size: var(--fs-lg); color: var(--ink); }
.lean-puerta__vale-cuerpo { padding: 13px var(--s4) var(--s4); }

/* Treinta trazos, uno por minuto, y el de las decenas más largo — que es como
   está graduado cualquier instrumento y como se lee un plazo de un vistazo:
   «le queda más de media», no «le queda el 61 %». */
.lean-puerta__regla { display: flex; align-items: flex-end; gap: 2px; height: 22px; }
.lean-puerta__regla i {
  flex: 1; display: block; height: 8px; border-radius: var(--r-micro);
  background: var(--brand); opacity: .9;
  transition: background var(--t) var(--ease), opacity var(--t) var(--ease);
}
.lean-puerta__regla i:nth-child(10n + 1) { height: 15px; }
.lean-puerta__regla i.es-ido { background: var(--line); opacity: 1; }
.lean-puerta__vale--gastado .lean-puerta__regla i { background: var(--line); }

.lean-puerta__vale-pie {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); margin-top: 9px;
}
.lean-puerta__resto { font-family: var(--mono); font-size: var(--fs-2xl); font-weight: var(--peso-medio);
  color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: var(--letra-ajuste); }
.lean-puerta__vale--gastado .lean-puerta__resto { color: var(--muted); }
.lean-puerta__vale-pie .nota { font-size: var(--fs-sm); color: var(--muted); text-align: right; }

/* ══════════════════════════ lean-puerta__sobre ══════════════════════════
   LO QUE TE HAN ENVIADO, cerrado. Un lacre que se parte por el medio y deja el
   contenido a la vista: es exactamente lo que es —se abre y ya no se cierra— y
   por eso no hace falta un párrafo que lo explique.

   La pantalla que lo lleva NO tiene título, ni quién lo compartió, ni una línea
   de texto: el sobre y el botón. Es la más desnuda del producto y es la que más
   lo agradece — `Secreto.Titulo` lo escribe un técnico para SU historial, no
   para quien abre el enlace. */
.lean-puerta__sobre {
  position: relative; border-radius: var(--r); background: var(--hondo);
  border: 1px solid var(--line); overflow: hidden;
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
.lean-puerta__sobre-cierre { position: relative; height: 26px; overflow: hidden;
  border-bottom: 1px dashed var(--line); }
.lean-puerta__sobre-cierre i {
  position: absolute; top: 0; bottom: 0; width: 50%;
  background-image: repeating-linear-gradient(-52deg,
    color-mix(in srgb, var(--brand-fijo) 26%, transparent) 0 2px,
    transparent 2px 9px);
  transition: transform var(--t-mueve) var(--mecanico), opacity var(--t-mueve) var(--ease);
}
.lean-puerta__sobre-cierre i:first-child { left: 0; }
.lean-puerta__sobre-cierre i:last-child  { right: 0; }
.lean-puerta__sobre.es-abierto .lean-puerta__sobre-cierre i:first-child { transform: translateX(-101%); opacity: 0; }
.lean-puerta__sobre.es-abierto .lean-puerta__sobre-cierre i:last-child  { transform: translateX(101%);  opacity: 0; }
.lean-puerta__sobre.es-abierto { background: var(--panel); border-color: var(--accion-filo); }

.lean-puerta__sobre-dentro { padding: var(--s4); }
/* TEXTO, así que `--muted`: en `--faint` se leía a 2,51:1 sobre el papel del
   diálogo. Y el cuerpo sale de la escala (`--fs-xl` son esos mismos 17 px) en vez
   de escribirse a mano. */
.lean-puerta__tapado { font-family: var(--mono); font-size: var(--fs-xl); color: var(--muted);
  letter-spacing: var(--letra-menuda); }

@media (prefers-reduced-motion: reduce) {
  .lean-puerta__firma path { animation: none; stroke-dashoffset: 0; }
  .lean-puerta__insignia path { animation: none; stroke-dashoffset: 0; }
  .lean-puerta__eco { animation: none; r: 3.6; }
  .lean-puerta__fondo { transition: none; }
  .lean-puerta, .lean-puerta__entra > * { animation: none; }
  .lean-puerta__campo.es-recordado .lean-puerta__filo { animation: none; }
  .lean-puerta__mayus { animation: none; }
}
