/* CiberLex — animaciones de diagramas, charts y detalles */

/* nodos y aristas de diagramas */
@media (prefers-reduced-motion: no-preference) {
  .mm-node { animation: mmnode calc(.55s / var(--motion,1)) var(--ease-fluid) backwards; }
  .mm-edge { stroke-dasharray: 600; stroke-dashoffset: 600; animation: mmedge calc(1.1s / var(--motion,1)) var(--ease-fluid) forwards; }
}
@keyframes mmnode { from { opacity: 0; transform: translateY(8px) scale(.92); } to { opacity: 1; transform: none; } }
@keyframes mmedge { to { stroke-dashoffset: 0; } }

/* charts */
@media (prefers-reduced-motion: no-preference) {
  .donut-seg { animation: donutin calc(.9s / var(--motion,1)) var(--ease-fluid) backwards; }
  .draw-line { stroke-dasharray: 1400; stroke-dashoffset: 1400; animation: mmedge calc(1.6s / var(--motion,1)) var(--ease-fluid) forwards; }
  .fade-line { animation: fadein calc(1.8s / var(--motion,1)) both; }
  .bar-grow { transform-origin: bottom; animation: barup calc(.7s / var(--motion,1)) var(--ease-fluid) backwards; }
}
@keyframes donutin { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
@keyframes barup { from { transform: scaleY(0); } to { transform: scaleY(1); } }

/* tabla admin */
.adm-row { transition: background .15s; }
.adm-row:hover { background: var(--bg-hover); }
.adm-act {
  width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center;
  color: var(--text-mute); border: 1px solid transparent; transition: all .15s;
}
.adm-act:hover { color: var(--text-hi); background: var(--bg-hover); border-color: var(--line-3); }

/* fila de notificación (campana abogado) */
.cl-notif-row { transition: background .15s; }
.cl-notif-row:hover { background: var(--bg-hover); }
.cl-notif-del {
  width: 24px; height: 24px; flex-shrink: 0; border-radius: 7px;
  display: grid; place-items: center; cursor: pointer;
  color: var(--text-dim); border: 1px solid transparent; background: transparent;
  opacity: 0; transition: opacity .15s, color .15s, background .15s, border-color .15s;
}
.cl-notif-row:hover .cl-notif-del { opacity: 1; }
.cl-notif-del:hover { color: var(--acc-red); background: rgba(248,113,113,.1); border-color: rgba(248,113,113,.4); }
@media (hover: none) { .cl-notif-del { opacity: 1; } }

/* línea de escaneo */
.scanline {
  position: absolute; left: 0; right: 0; height: 90px; top: -90px;
  background: linear-gradient(180deg, transparent, rgba(34,211,238,.14) 70%, rgba(34,211,238,.45));
  border-bottom: 1px solid var(--acc-cyan);
  box-shadow: 0 0 24px rgba(34,211,238,.4);
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .scanline { animation: scan calc(2.4s / var(--motion,1)) linear infinite; }
}
@keyframes scan { from { top: -90px; } to { top: 110%; } }

/* ---------- Flechas de plegar/desplegar: encendidas y con latido -----------------
   Por que una clase propia y no el .heartbeat de base.css: ese late con
   `box-shadow`, que en un <svg> dibuja el resplandor sobre la CAJA del icono y no
   sobre el trazo de la flecha. `drop-shadow` sigue la forma real del dibujo.
   Se conserva el MISMO ritmo del .heartbeat (1.4s, pasos 14/28/42/56%) para que el
   latido sea el mismo gesto en todo el producto.

   EL LATIDO VA EN LA LUZ, NO EN EL TAMANO. Antes escalaba hasta 1.18, y eso en un
   icono de 16 px son 4 cambios de direccion en 0.78 s: no se lee como latido, se
   lee como VIBRACION, y se notaba sobre todo al plegar los modulos, cuando el
   temblor coincidia con el cambio de alto del panel. Referencia: el .heartbeat
   original llega a 1.06 y se usa en botones .cta de ~200 px de ancho; la misma
   proporcion en 16 px es imperceptible, asi que el tamano no aporta nada aqui.
   Animando solo el resplandor el gesto se mantiene y la geometria NO se mueve:
   cero temblor por construccion.

   La rotacion (abierto/cerrado) sigue en un <span> aparte: aunque ya no haya
   `scale` que la sobrescriba, mantenerla separada deja el giro y el pulso
   independientes y evita que volver a tocar los keyframes rompa el giro. */
.chev-rot { display: inline-flex; transition: transform .18s var(--ease-fluid); }
.chev-late {
  color: var(--acc-cyan);
  filter: drop-shadow(0 0 4px currentColor);
  will-change: filter;   /* capa propia: el pulso no repinta lo que hay debajo */
}
@media (prefers-reduced-motion: no-preference) {
  .chev-late { animation: chevlate 1.4s ease-in-out infinite; }
}
@keyframes chevlate {
  0%, 100% { filter: drop-shadow(0 0 3px currentColor); }
  14%      { filter: drop-shadow(0 0 8px currentColor); }
  28%      { filter: drop-shadow(0 0 3px currentColor); }
  42%      { filter: drop-shadow(0 0 7px currentColor); }
  56%      { filter: drop-shadow(0 0 10px currentColor); }
}

/* ---------- «Ver todas (N)» del historial ----------------------------------------
   El cian a plena saturacion (--acc-cyan) sobre texto de 12.5 px quedaba chillon y
   ademas incoherente: el color venia de --acc-cyan (#22d3ee) pero el resplandor
   estaba escrito a mano en #38e0f5, que es otro token (--ia-1). Dos cianes
   distintos en la misma palabra.

   Ahora el texto va en --text-mid, que es el gris claro del sistema: destaca sobre
   los items del historial (--text-mute) sin gritar, y deja el color como refuerzo
   del NUMERO y del hover. El acento pasa a --acc-blue, que es el azul dominante de
   la marca y se integra mejor que el cian en un panel oscuro. */
.hist-vertodas {
  color: var(--text-mid);
  justify-content: center;
  gap: 7px;
  font-weight: 600;
  letter-spacing: .01em;
}
.hist-vertodas:hover {
  color: var(--text-hi);
  background: var(--bg-hover);
}
/* El numero: pastilla azul discreta. Sin borde duro ni doble resplandor; el relleno
   translucido ya lo separa del fondo y aguanta bien tanto 3 como 250. */
.hist-vertodas .n-late {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 700;
  padding: 1.5px 7px;
  border-radius: 999px;
  color: var(--acc-blue);
  background: rgba(91, 140, 255, .14);
  border: 1px solid rgba(91, 140, 255, .32);
  transition: background .15s, border-color .15s, color .15s;
}
.hist-vertodas:hover .n-late {
  color: #b9cbff;
  background: rgba(91, 140, 255, .24);
  border-color: rgba(91, 140, 255, .55);
}

/* sidebar */
.side-item {
  display: flex; align-items: center; gap: 11px;
  width: 100%; padding: 9px 12px; border-radius: 10px;
  font-size: 13.5px; color: var(--text-mute);
  border: 1px solid transparent;
  transition: all .18s var(--ease-fluid);
  text-align: left;
}
.side-item:hover { color: var(--text-hi); background: var(--bg-hover); }
.side-item.active {
  color: var(--text-hi); background: var(--bg-selected);
  border-color: var(--line-3);
  box-shadow: inset 2px 0 0 var(--acc-cyan), var(--glow-blue);
}
.hist-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%; padding: 7px 12px; border-radius: 9px;
  font-size: 12.5px; color: var(--text-mute);
  transition: all .15s; text-align: left;
}
.hist-item:hover { color: var(--text-mid); background: var(--bg-hover); }
.hist-item.active { color: var(--text-hi); background: var(--bg-selected); }
.hist-item .t { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* menú de perfil */
.menu-pop {
  position: absolute; bottom: calc(100% + 10px); left: 10px; right: 10px;
  z-index: 40; padding: 6px;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lift);
}
.menu-pop button {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 9px 12px; border-radius: 8px;
  font-size: 13px; color: var(--text-mid);
  transition: background .15s;
  text-align: left;
}
.menu-pop button:hover { background: var(--bg-hover); color: var(--text-hi); }

/* ---------- nombre del plan: color profundo y latido, sin brillo ----------
   El latido va en `transform`, y eso es deliberado: es una propiedad que el navegador
   compone en la GPU, así que la animación no obliga a recalcular la maquetación de la
   tarjeta en cada fotograma. Animar `font-size` daría el mismo efecto a la vista y
   haría trabajar al procesador en la pantalla donde el abogado compara precios.

   El ritmo imita un latido de verdad: dos pulsos seguidos y una pausa larga, no un
   vaivén regular. Por eso los fotogramas se agrupan al principio (0-30%) y el resto
   del ciclo queda en reposo.
   SIN BRILLO: la animación es solo `transform`. Llevaba `text-shadow` y el halo hacía
   que un color intenso se leyera lavado -el resplandor se mezcla con la letra y le baja
   la saturación aparente-, así que quitarlo es lo que deja verse el color profundo. */
@keyframes cl-latido {
  0%   { transform: scale(1); }
  10%  { transform: scale(1.07); }
  20%  { transform: scale(1); }
  30%  { transform: scale(1.04); }
  40%,
  100% { transform: scale(1); }
}

.plan-latido {
  /* CENTRADO en la tarjeta. `block` + `text-align: center` en vez de `inline-block`:
     así el nombre ocupa el ancho de la tarjeta y queda centrado de verdad, en lugar de
     ajustarse al texto y pegarse al borde izquierdo.
     Y el origen del latido pasa a `center`: con el origen a la izquierda, un elemento
     centrado se agranda hacia la derecha y el nombre se ve «empujarse» en cada pulso
     en vez de respirar en su sitio. */
  display: block;
  text-align: center;
  transform-origin: center center;
  animation: cl-latido 2.6s var(--ease-fluid, ease-in-out) infinite;
  will-change: transform;
}

/* Quien pidió que no se le muevan las cosas en pantalla, no se le mueven: se queda el
   brillo -que es información: el color del plan- y se va el latido. No es opcional,
   es una preferencia del sistema operativo y hay gente a la que el movimiento le
   produce mareo. */
@media (prefers-reduced-motion: reduce) {
  .plan-latido { animation: none; }
}

/* PUNTO DE «VERIFICANDO…» del reenvío pasivo. Late suave mientras el auto-aprobador del
   servidor confirma la regla en segundo plano: comunica que el proceso está VIVO, no
   atascado, justo cuando el abogado no puede hacer nada más que esperar. Reutiliza el
   ritmo de `cl-latido` -dos pulsos y pausa, como un latido- pero animando también la
   opacidad, que en un punto de 6 px se lee antes que la escala. */
.cl-latido-punto {
  animation: cl-latido-punto 2.6s var(--ease-fluid, ease-in-out) infinite;
  will-change: transform, opacity;
}
@keyframes cl-latido-punto {
  0%   { transform: scale(1);    opacity: 1; }
  10%  { transform: scale(1.35); opacity: .55; }
  20%  { transform: scale(1);    opacity: 1; }
  30%  { transform: scale(1.2);  opacity: .7; }
  40%,
  100% { transform: scale(1);    opacity: 1; }
}
/* Misma cortesía que el latido de los planes: quien pide menos movimiento ve el punto
   quieto -sigue siendo ámbar, que es la información- sin el pulso. */
@media (prefers-reduced-motion: reduce) {
  .cl-latido-punto { animation: none; }
}

/* ---------- burbuja de soporte ----------
   En el teléfono el panel ocupa TODO. Un panel de 400 px anclado a una esquina de una
   pantalla de 360 px no es una burbuja: es un recorte, con la mitad del texto fuera y
   el compositor pegado al borde. Y el botón flotante se sube un poco, porque abajo a la
   derecha es donde muchos navegadores móviles ponen su propia barra.

   EL LANZADOR SE DISEÑÓ ENTERO AQUÍ, y el motivo es de fondo: su aspecto estaba en un
   `style` en línea del JSX, y una animación por keyframes GANA a un estilo en línea. Con
   el rebote en CSS y la geometría en línea, el primero pisa a la segunda y el síntoma no
   se parece en nada a su causa. Un solo sitio manda. */

.sop-burbuja-btn {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 302;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  height: 56px;
  min-width: 56px;
  padding: 0 20px 0 17px;
  border: 1px solid var(--line-3);
  border-radius: 99px;
  background: var(--grad-ia);
  color: var(--ink-cta);
  box-shadow: var(--glow-ia);
  /* MISMA tipografía que `.cta`, el botón primario del producto (base.css): fuente de
     display, 14 px, 600. El rótulo es una llamada a la acción más, y si se escribiera
     con otra fuente o otro peso se leería como algo pegado por fuera del diseño. */
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: transform .18s var(--ease-fluid), box-shadow .22s var(--ease-fluid);
}
.sop-burbuja-rotulo { white-space: nowrap; }

/* Con el chat abierto vuelve a ser un círculo (ver el JSX). */
.sop-burbuja-btn.sop-redonda {
  width: 56px;
  min-width: 0;
  padding: 0;
  justify-content: center;
  border-radius: 50%;
}

/* EL LEVANTAMIENTO al apuntarla. Estaba en dos manejadores `onMouseEnter/onMouseLeave`
   que escribían `style.transform` a mano, y eso era incompatible con el rebote de abajo:
   el `transform: none` del mouseleave se queda EN LÍNEA para siempre y desde ese momento
   pisa cualquier regla. En CSS, además, lo gana el TECLADO (`:focus-visible`), que con un
   manejador de mouse no tenía nada. */
.sop-burbuja-btn:hover,
.sop-burbuja-btn:focus-visible { transform: translateY(-2px) scale(1.03); }

/* El CONTADOR de mensajes sin leer. Lleva z-index porque el anillo del pulso es un
   `::after` -o sea el último hijo- y sin esto le pasaría la línea por encima. */
.sop-burbuja-cuenta {
  position: absolute;
  top: -3px;
  right: -3px;
  z-index: 1;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: 10px;
  background: var(--acc-red);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  display: grid;
  place-items: center;
  box-shadow: 0 0 8px var(--acc-red);
}

/* ANILLO DE PULSO. Va en `::after` y NO en `::before` porque `::before` lo ocupa el
   tooltip de `[data-tip]` (base.css): un pseudoelemento para cada cosa, y el día que
   alguien mueva uno al otro, una de las dos desaparece SIN dar error. El `inset`
   negativo lo deja FUERA del botón, así que no oscurece el icono ni el rótulo, y
   `pointer-events: none` para que no se coma el clic.
   Las dos capas -anillo de 1 px y halo difuso- multiplican su opacidad por var(--glow),
   que es el ajuste de intensidad del abogado: sin ese factor, quien baje el neón a cero
   seguiría viendo esto y el ajuste dejaría de significar lo que dice. */
.sop-burbuja-btn::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow:
    0 0 0 1px rgba(91,140,255, calc(.34 * var(--glow, 1))),
    0 0 18px 2px rgba(91,140,255, calc(.26 * var(--glow, 1)));
  opacity: .5;
}

/* EL MOVIMIENTO, los dos gestos detrás de la MISMA puerta: `prefers-reduced-motion` es
   una preferencia del sistema operativo y hay gente a la que el movimiento le produce
   mareo. Sin animación no se pierde el rediseño: el anillo se queda quieto y encendido,
   que es justo lo que hacía falta para que la burbuja no se confunda con el fondo.
   Las duraciones se dividen por var(--motion), el control de velocidad del abogado (su
   mínimo es .4, así que nunca hay división por cero). */
@keyframes clSopFlota { 0%, 100% { translate: 0 0; } 50% { translate: 0 -5px; } }
@keyframes clSopPulso {
  0%, 100% { opacity: .38; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.06); }
}
@media (prefers-reduced-motion: no-preference) {
  /* `translate` y no `transform`: son propiedades INDEPENDIENTES, así que el rebote y el
     levantamiento del hover se COMPONEN en vez de pisarse. Con las dos cosas en
     `transform` habría que elegir una. */
  .sop-burbuja-btn { animation: clSopFlota calc(3.6s / var(--motion, 1)) ease-in-out infinite; }
  .sop-burbuja-btn::after { animation: clSopPulso calc(2.9s / var(--motion, 1)) ease-in-out infinite; }
  /* Quieta mientras el abogado la apunta: a un blanco que se mueve se le falla. */
  .sop-burbuja-btn:hover,
  .sop-burbuja-btn:focus-visible { animation-play-state: paused; }
}

@media (max-width: 640px) {
  .sop-burbuja-panel {
    right: 0 !important;
    left: 0 !important;
    bottom: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    height: 88vh !important;
    max-height: 88vh !important;
    border-radius: var(--r-lg) var(--r-lg) 0 0 !important;
  }
  /* En 360 px el rótulo se comería el ancho de la pantalla, así que vuelve el círculo.
     Aquí tampoco hay tooltip (los táctiles lo desactivan, ver base.css): quien identifica
     el botón es el icono, y el `aria-label` para quien use lector. */
  .sop-burbuja-btn {
    right: 14px !important;
    bottom: 76px !important;
    width: 56px;
    min-width: 0;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
  }
  .sop-burbuja-rotulo { display: none; }
}

/* ---------- pantalla de acceso ----------
   EL BÚHO GRANDE YA NO ESTÁ EN ESTA PANTALLA: se quitó al entrar el fondo en vídeo, porque
   la ilustración competía con él. Con él se fueron las reglas que lo ocultaban aquí
   (`.login-owl { display: none }`) y el `order: 2` que subía el pie por encima suyo en el
   móvil: ahora el pie ya está DEBAJO de la tarjeta en el propio marcado, así que no hay
   nada que reordenar.

   LO QUE SÍ SE CONSERVA, y es la razón por la que este bloque sigue existiendo: la firma
   con los enlaces a Términos y Privacidad. La verificación de marca de Google exige que la
   página de inicio enlace la política de privacidad, así que no se oculta en ningún ancho
   de pantalla. */
/* ---------- el encuadre del vídeo de fondo ----------
   VIVE AQUÍ Y NO EN EL ESTILO EN LÍNEA porque tiene que cambiar en el móvil, y un estilo en
   línea gana a cualquier regla de una `@media` (salvo con `!important`). El elemento se queda
   con la geometría en línea; el ENCUADRE se decide aquí.

   `object-position: 20%` EN ESCRITORIO: con `cover`, un vídeo horizontal en una ventana ancha
   se recorta por los lados y el navegador, por omisión, conserva el centro. Eso dejaba el búho
   y el logotipo justo en el medio, peleando con la tarjeta. Moviendo el punto de interés al
   20% del ancho, el motivo se va hacia la izquierda y la mitad derecha queda despejada para el
   formulario: el hueco lo hace el encuadre y no un margen forzado.

   ATENCIÓN: EN ESCRITORIO ESTE 20% NO HACE NADA, y conviene saberlo antes de perder una tarde
   moviéndolo. `object-position` solo desplaza si hay RECORTE en ese eje, y con `cover` el
   recorte se da en el eje donde sobra imagen. El vídeo es 16:9 (1920x1080, relación 1,78); en
   una ventana de escritorio la relación es MAYOR -1,88 en una de 1024x545, 1,94 en una de
   1512x780-, así que `cover` escala por el ANCHO, la imagen encaja exacta en horizontal y lo
   que sobra es la ALTURA. Sin recorte horizontal no hay nada que desplazar: el 20% y el 50%
   pintan exactamente igual. Se probó 30% y luego 20% en producción y no se movió un píxel, y
   este es el motivo.

   SE MANTIENE EN 20% PORQUE SÍ ACTÚA en las ventanas MÁS ESTRECHAS que 16:9 -una ventana de
   escritorio a media pantalla, una tableta en vertical-. Ahí sobra ancho, hay recorte
   horizontal, y el 20% corre el motivo a la izquierda como se espera.

   PARA MOVER EL BÚHO EN ESCRITORIO NO HAY CSS QUE VALGA. Está centrado DENTRO del archivo y
   es una animación: crece y se desplaza durante los 13 s. Se intentó recortar el vídeo
   (`crop=1440:810:480:135`) para dejarlo a un tercio del ancho, y el resultado le cortaba las
   orejas y partía el logotipo en los últimos segundos, porque ningún recorte fijo encuadra
   bien todos los fotogramas de una animación. La única vía real es re-exportar la pieza con
   el búho descentrado a la izquierda y aire a la derecha para el formulario. */
/* ---------- EL ENCUADRE, CON LA GEOMETRÍA MEDIDA ----------
   MEDIDO SOBRE FOTOGRAMAS DEL VÍDEO (perfil de luminancia por columna, no a ojo): en el
   fotograma final el búho ocupa de x 17% a 40% -su centro está en el 29%- y el logotipo
   «CIBERLEX» de x 15% a 46%, bajando hasta y 90% con su reflejo. O sea que el motivo YA está
   en el tercio izquierdo del archivo y los dos tercios derechos son placa de circuito vacía.
   Eso se consiguió re-exportando la pieza, no con CSS.

   `21%` Y NO `80%`, y la diferencia es que el 80% CORTA EL BÚHO. `object-position` X coloca el
   punto X de la imagen sobre el punto X del contenedor, así que con el motivo a la izquierda
   hay que mirar HACIA la izquierda del archivo, no hacia la derecha. Comprobado con la
   geometría en una ventana de 900x800: con X=80% la franja visible es del 29% al 93% del
   ancho -el búho, que empieza en el 17%, queda partido- y con X=21% es del 8% al 71%, con el
   búho y el logotipo completos.

   EL 21% SALE DE UNA CUENTA, no de probar. Para que la franja visible contenga el búho entero
   en un teléfono de 390x844 hace falta X entre 18,9% y 23,0%; en uno de 360x800, entre 18,8%
   y 22,9%; y en una tableta de 768x1024 el margen es mucho más ancho. El 21% cae dentro en
   todos, así que un solo valor sirve para teléfono, tableta y ventana estrecha de escritorio.

   `42%` EN VERTICAL, ligeramente por encima del centro. Solo actúa en ventanas MÁS ANCHAS que
   16:9, que es donde `cover` recorta altura. Hasta una relación de 2,2 no se pierde nada; por
   encima -un 21:9 maximizado- hay que sacrificar algo, y el 42% sacrifica el reflejo del
   logotipo antes que las orejas del búho. En 2560x1080 la franja visible pasa a ser del 10,5%
   al 85,5%: el búho entra justo y del logotipo solo se pierde el reflejo.

   POR QUÉ NO HAY VALOR DISTINTO PARA MÓVIL: en vertical no sobra altura -el recorte de `cover`
   es HORIZONTAL-, así que la Y es inerte y la X ya es la correcta. Un `center` en móvil sería
   un error: en un 390x844 la franja visible sería del 37% al 63% y el búho (17%-40%) se vería
   solo por su borde derecho. */
/* EL REALCE DE COLOR, Y POR QUÉ HACE FALTA. El máster es HEVC de 10 bits, pero un H.264 de
   10 bits es el perfil High 10, que la mayoría de navegadores no decodifica: para que el vídeo
   se vea en Firefox hay que entregarlo en 8 bits (ver el PR #854, donde justamente no se veía).
   Ese paso de 10 a 8 bits es el que aplana los degradados del neón y la fibra de carbono.

   Se comprobó que NO es un problema de etiquetas: máster y publicado van los dos como
   `tv, bt709, progressive`, así que el navegador interpreta el color correctamente. Lo que se
   perdió son niveles, no la interpretación, y eso no se recupera con metadatos: se compensa
   devolviendo un poco de pendiente a la curva. De ahí estos tres valores y no otros.

   El `filter` va SOBRE EL VÍDEO y no sobre un contenedor a propósito: aplicado al padre
   afectaría también al velo, al búho y al pie, y subirle el contraste al texto del pie es
   justo lo contrario de lo que se quiere. Aquí el realce muere en los píxeles del vídeo.

   NO SE TOCA `object-position`: el encuadre medido abajo sigue siendo el mismo. */
.login-video {
  object-fit: cover;
  object-position: 21% 42%;
  filter: contrast(1.08) brightness(1.05) saturate(1.1);
}

/* ---------- EL VELO ----------
   EN ESCRITORIO NO HAY VELO. El vídeo es oscuro por naturaleza -placa de circuito negra- así
   que la capa solo apagaba el neón sin aportar nada: lo que tenía que proteger era el pie, y
   eso ya lo hace su `text-shadow`, que oscurece el contorno de las letras en vez de los dos
   millones de píxeles de la imagen. Queda `transparent` para que el vídeo se vea crudo.

   EL `<video>` LLEVA UN `filter` DE COLOR -contraste, brillo y saturación, ver `.login-video`
   arriba- para compensar el paso de 10 a 8 bits, pero NO lleva `opacity` ni ningún desenfoque:
   la imagen se ve entera y nítida. El único desenfoque de la pantalla sigue siendo el
   `backdrop-filter` de `.glass`, que actúa SOLO bajo la tarjeta y es su glassmorphism. */
.login-velo { background: transparent; }

@media (max-width: 820px) {
  /* `!important` porque estos valores vienen en estilo en línea, que gana a las `@media`. */
  .login-wrap { flex-direction: column; gap: 20px !important; }
  /* `justify-content` DE VUELTA A CENTER: en línea va `flex-end` para que la tarjeta flote a
     la derecha en escritorio, pero al pasar a `column` ese eje deja de ser el horizontal y
     pasa a ser el VERTICAL, así que sin esto la tarjeta se iría al fondo de la pantalla.

     EL `!important` FALTABA Y ESTA REGLA NO HACÍA NADA. Se escribió sin él, y como el valor
     está en línea, el navegador se quedaba con `flex-end`: en tableta y en móvil la tarjeta
     ha estado cayendo al fondo desde que se introdujo. Es el mismo motivo que ya obligaba al
     `!important` del `gap` dos líneas más arriba —estaba documentado y aun así se escapó—. */
  .login-wrap { justify-content: center !important; }
}

/* ---------- móvil: Android y iPhone ----------
   ES UN SEGUNDO CORTE, MÁS ESTRECHO QUE EL DE ARRIBA, y los dos hacen cosas distintas: el de
   820 px cambia la DISPOSICIÓN (dos ejes a uno), y este cambia el TAMAÑO y el encuadre para
   una pantalla que se sostiene con una mano. Entre 768 y 820 px -tabletas en vertical- se
   aplica solo el primero, que es lo correcto: ahí sobra ancho para el padding de escritorio.

   TODO LO QUE PISA UN ESTILO EN LÍNEA LLEVA `!important`. No es descuido ni pereza: el
   `padding` del wrap y el `width`/`max-width` de la tarjeta se declaran en línea en auth.jsx,
   y sin `!important` estas reglas se ignoran en silencio -exactamente lo que le pasó a la
   línea de `justify-content` de arriba-. */
@media (max-width: 768px) {
  /* AQUÍ NO SE REDEFINE `object-position`, Y ES DELIBERADO. En vertical el recorte de `cover`
     es HORIZONTAL -la altura encaja exacta-, así que la componente Y es inerte y un
     `center 10%` no subiría el búho ni un píxel. Lo que decide el encuadre en un teléfono es
     la X, y el 21% de la regla base ya está calculado para que el búho quepa entero en
     390x844, 360x800 y 430x932. Poner `center` aquí sería el error caro: la franja visible
     pasaría a ser del 37% al 63% del ancho y del búho (17%-40%) solo se vería el borde. */

  /* AQUÍ SÍ HACE FALTA EL VELO, al contrario que en escritorio. Allí el vídeo queda a un lado
     del formulario y puede ir crudo; aquí la tarjeta ocupa el 90% del ancho, así que el vídeo
     está DIRECTAMENTE detrás de los campos y sin contraste los rótulos EMAIL y CLAVE se pierden
     sobre los fotogramas claros del neón. */
  .login-velo { background: rgba(8,10,18,.6) !important; }

  .login-wrap {
    /* `flex-end` EN COLUMNA ES ABAJO, y es lo que se quiere: la tarjeta baja al pie de la
       pantalla y deja la franja superior libre para el búho, que es donde el vídeo lo pone.

       LLEVA `!important` POR DOS MOTIVOS A LA VEZ: el valor está en línea en auth.jsx, y el
       corte de 820 px -que también encaja en un teléfono- declara `center !important`. Sin
       `!important` aquí perdería contra los dos.

       NO PUEDE DEJAR EL BOTÓN FUERA DE ALCANCE: con `min-height: 100%`, si la tarjeta de
       «Registrarse» es más alta que la pantalla el contenedor CRECE y entonces no queda espacio
       libre que repartir, así que `justify-content` deja de tener efecto y el contenido se
       desplaza desde arriba. Es el patrón del commit d802281, que arregló justo ese defecto. */
    flex-direction: column !important;
    justify-content: flex-end !important;
    align-items: center !important;
    /* 20px por los cuatro lados: se van los 7vw/8vw de escritorio y también los 76px que
       reservaban la banda del pie, que en el móvil vuelve al flujo y ya no necesita hueco. */
    padding: 20px !important;
  }

  /* La tarjeta ocupa el 90% del ancho con un techo, en vez de los 420 px fijos. El 90% deja
     un canal de 5% a cada lado que enseña el vídeo por los bordes: sin él la tarjeta se lee
     como una pantalla pegada y no como una tarjeta flotando encima. */
  .login-wrap .glass {
    width: 90% !important;
    max-width: 400px !important;
    /* MARGEN ABAJO Y NO ARRIBA. Antes había `margin-top: 10vh` para bajar la tarjeta y no
       tapar al búho; ahora eso lo hace `justify-content: flex-end`, que la lleva al fondo. Lo
       que hace falta es lo contrario: separarla del canto inferior para que no parezca pegada
       a la barra del navegador. En `vh` para que escale con la altura del teléfono. */
    margin-bottom: 4vh;
  }

  /* EL PIE VUELVE AL FLUJO. En escritorio flota anclado en la esquina; aquí se apila debajo
     de la tarjeta, que es donde se lee ordenado y donde no puede taparla. Los cuatro valores
     deshacen el anclaje: sin devolver `position` a `relative`, `bottom` y `right` seguirían
     mandando y el bloque quedaría flotando sobre el formulario. */
  .login-pie {
    position: relative !important;
    right: 0 !important;
    bottom: 0 !important;
    text-align: center !important;
    margin-top: 2vh;
  }
}

/* ---------- búho que se desarma / rearma ---------- */
.owl-piece {
  position: absolute;
  /* Por TOKEN, para que el tema claro use el emblema en tinta -sin medallón- sin tocar
     JavaScript. Ver el comentario de `--owl-emblema` en tokens.css. */
  background-image: var(--owl-emblema, url('../assets/owl-emblem-blue.png'));
  background-repeat: no-repeat;
  mix-blend-mode: screen;
  transition: transform .62s var(--ease-fluid), opacity .62s var(--ease-fluid);
  will-change: transform;
  pointer-events: none;
}
.owl-shatter { transition: filter .4s; }
.owl-shatter.broken { filter: drop-shadow(0 0 30px rgba(56,224,245,.7)) brightness(1.15); }
.owl-shatter.broken .owl-piece {
  transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(.42);
  opacity: .92;
}

/* Resplandor detrás del búho: la energía de los fragmentos ilumina el entorno.
   Va en `::before` porque un pseudoelemento generado es el PRIMER hijo que se
   pinta: queda debajo de las piezas sin necesidad de z-index, y las piezas
   (`mix-blend-mode: screen`) se encienden contra él en vez de taparlo.
   Apagado del todo mientras el búho está armado: un resplandor permanente sería
   una mancha azul fija en media pantalla, y entonces no diría nada al encenderse. */
.owl-shatter::before {
  content: "";
  position: absolute;
  inset: -26%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 56%,
    rgba(91,140,255,.55) 0%, rgba(183,148,255,.34) 38%,
    rgba(56,224,245,.16) 62%, transparent 76%);
  filter: blur(24px);
  opacity: 0;
  transition: opacity .5s var(--ease-fluid);
  pointer-events: none;
}
/* .85 y no 1: detrás del login corre el Aurora animado, y a opacidad plena el
   resplandor lo tapa y se lee como un círculo pegado encima, no como luz. */
.owl-shatter.broken::before { opacity: calc(.85 * var(--glow, 1)); }

/* ---------- ojos vivos: la IA está encendida y observando ---------- */
/* DÓNDE VAN LOS OJOS NO SE PUSO A OJO. Se midió sobre `owl-emblem-blue.png`
   (543x522 px) aislando por componentes conexas los dos núcleos casi blancos de la
   imagen: sus centros caen en (231,192) y (318,192) px —o sea 42.51 % y 58.57 % del
   ancho y 36.75 % del alto— con 26 px de diámetro. Un par de porcentajes puestos a
   ojo dejan el pulso medio píxel fuera de la pupila, y eso se ve.

   Y los porcentajes de la imagen valen TAL CUAL como porcentajes del contenedor
   porque `.owl-piece` estira el PNG al cuadrado del contenedor
   (`backgroundSize: size x size`): no hay letterbox que corregir. Si algún día se
   pasara a `contain`, habría que recalcular el eje vertical.

   `mix-blend-mode: screen` es el mismo de las piezas, y es lo que hace que el pulso
   SUME luz sobre el ojo que ya trae la imagen en vez de taparlo con un disco cian. */
.owl-eye {
  position: absolute;
  top: 36.75%;
  width: 11%;
  height: 11%;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(224,252,255,.95) 0%, rgba(56,224,245,.60) 30%,
    rgba(56,224,245,.20) 55%, transparent 72%);
  mix-blend-mode: screen;
  pointer-events: none;
  transform: translate(-50%, -50%);
  transition: opacity .3s var(--ease-fluid);
}
.owl-eye-izq { left: 42.51%; }
.owl-eye-der { left: 58.57%; }

/* Al dispersarse las piezas la cara ya no está ahí: los ojos se apagan. Sin esto
   quedarían dos puntos cian flotando sobre el hueco de una cara que voló, que es el
   tipo de detalle que delata que el efecto está pegado por encima. */
.owl-shatter.broken .owl-eye { opacity: 0; animation: none; }

/* El `translate(-50%,-50%)` se repite en los dos fotogramas porque una animación de
   `transform` REEMPLAZA el valor de la regla base: sin él, los ojos saltarían a la
   esquina en cuanto arrancara el pulso. */
@keyframes ojos_vivos {
  0%, 100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: .72;
    filter: drop-shadow(0 0 5px rgba(56,224,245,.55))
            drop-shadow(0 0 13px rgba(56,224,245,.28));
  }
  50% {
    transform: translate(-50%, -50%) scale(1.24);
    opacity: 1;
    filter: drop-shadow(0 0 11px rgba(56,224,245,.95))
            drop-shadow(0 0 28px rgba(91,140,255,.55));
  }
}
@media (prefers-reduced-motion: no-preference) {
  /* 3.4 s es el ritmo de una respiración tranquila: más rápido parece nervioso y más
     lento no se lee como vivo sino como un fallo de dibujado.
     `ease-in-out` y NO `var(--ease-fluid)`, que es una curva de desaceleración
     (cubic-bezier(.22,.8,.28,1)): con ella el brillo salta y frena, que es un latido
     brusco. Respirar es simétrico al entrar y al salir. */
  .owl-eye { animation: ojos_vivos calc(3.4s / var(--motion, 1)) ease-in-out infinite; }
}

/* ---------- tarjeta del login: línea de escaneo perimetral ---------- */
/* Mismo molde que `.grad-border` y `.mirror-glass` en base.css: un pseudoelemento
   de 1 px de grosor recortado con dos máscaras —`exclude` deja SOLO el marco y
   descarta el relleno—, así que la luz recorre el borde y nunca se echa encima del
   formulario. `::after` y no `::before` porque `.glass::before` ya está ocupado por
   el brillo del cristal.
   Lo que cambia respecto al molde es que aquí el gradiente GIRA. Girar el
   pseudoelemento con `transform` no sirve: arrastraría también la máscara y el
   marco se saldría de la tarjeta. Se anima el ÁNGULO del conic-gradient, que mueve
   la luz y deja la geometría quieta. */
@property --cl-escaneo {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
/* `.glass` ya es `relative`, pero se declara aquí también para que la clase no
   dependa de con quién la combinen: sin esto el marco se anclaría al primer
   antepasado posicionado y aparecería a media pantalla. */
.login-escaneo { position: relative; }
.login-escaneo::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1px;
  /* Cian → magenta y el resto transparente: se ve UN arco de luz dando la vuelta,
     no un marco de colores encendido de forma permanente.
     El `0deg` de reserva en el `var()` es para el navegador que no entienda
     `@property`: ahí el ángulo no se puede animar, pero en vez de perderse el
     `background` entero queda el arco quieto en una esquina. */
  background: conic-gradient(from var(--cl-escaneo, 0deg),
    transparent 0deg, transparent 232deg,
    rgba(56,224,245,.95) 292deg, rgba(240,166,255,.95) 330deg,
    rgba(56,224,245,.60) 348deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
  opacity: calc(.9 * var(--glow, 1));
}
@media (prefers-reduced-motion: no-preference) {
  .login-escaneo::after { animation: escaneoborde calc(4.5s / var(--motion, 1)) linear infinite; }
}
@keyframes escaneoborde { to { --cl-escaneo: 360deg; } }

/* ---------- la tarjeta del login ENTRA, no aparece ----------
   `.fade-up` (base.css) ya hace el fundido con desplazamiento desde abajo. Lo único que le
   faltaba para que la entrada se lea como una entrada era el RETARDO: sin él, la tarjeta se
   anima mientras el resto de la pantalla todavía está montándose y el gesto se pierde entre
   el búho, la aurora y el primer fotograma del vídeo. Con medio segundo, el fondo se asienta
   primero y la tarjeta llega después, que es lo que hace que parezca intencionado.

   SE HACE POR SELECTOR Y NO CON UNA CLASE NUEVA. `tests/test_login_premium.js` comprueba el
   literal `className="glass fade-up login-escaneo"`; añadir una clase ahí dejaría ese
   `.test()` en falso y tumbaría la prueba del login sin que nadie haya tocado el login. El
   selector descendente consigue lo mismo sin tocar el marcado.

   SOLO EN ESTA PANTALLA. `.fade-up` lo usan muchas vistas y un retardo global haría que todo
   el producto pareciera lento al abrir. Aquí se justifica porque hay un fondo que tiene que
   establecerse antes.

   VA DENTRO DE `no-preference` Y DIVIDIDO POR `--motion` como el resto: quien pidió menos
   movimiento no tiene animación ninguna -y por tanto tampoco espera-, y quien subió la
   velocidad en los ajustes ve la tarjeta antes. Con `backwards`, durante el retardo la
   tarjeta se queda en el fotograma inicial (invisible); si algún día se le quita ese
   `fill-mode` a `.fade-up`, aparecería de golpe y LUEGO se animaría. */
/* SELECTOR DESCENDENTE Y NO HIJO DIRECTO. Era `.login-wrap > .glass.fade-up`, y al quitar
   el búho la tarjeta bajó un nivel -vive en `.login-col`; el pie ya salió de ahí-, así que
   el `>` habría dejado de encajar y el retardo se habría perdido EN SILENCIO: la tarjeta
   seguiría animándose, solo que sin esperar. Con el descendente da igual cuántos envoltorios
   haya. No hay otra `.glass.fade-up` dentro de esta pantalla, así que no se amplía a nada. */
@media (prefers-reduced-motion: no-preference) {
  .login-wrap .glass.fade-up { animation-delay: calc(.5s / var(--motion, 1)); }
}

/* ---------- borde vivo: el efecto del login, reutilizable en los modulos ----------
   Misma maquinaria que `.login-escaneo`: se REUSAN `@property --cl-escaneo` y
   `@keyframes escaneoborde` de arriba, que es lo que de verdad no debe duplicarse.

   POR QUE ES UN BLOQUE APARTE Y NO UNA LISTA DE SELECTORES. Dos razones, y las dos
   son de fondo:
   1. El arco es DISTINTO a proposito. El del login es cian -> magenta; este lleva
      cian -> violeta -> magenta, que es lo que se pidio para los modulos.
   2. `tests/test_login_premium.js` recorta el bloque del login buscando el literal
      `.login-escaneo::after {`. Convertirlo en `.login-escaneo::after, .cl-borde-vivo::after {`
      dejaria ese `indexOf` en -1 y la prueba del LOGIN se caeria sin que nadie haya
      tocado el login. La pantalla de acceso no se toca para adornar un modulo.

   EL ANGULO SE ANIMA, EL PSEUDOELEMENTO NO SE GIRA. `transform: rotate` arrastraria
   tambien la mascara y el marco se saldria de la caja.

   LOS EXTREMOS SE APAGAN CON EL MISMO TONO EN ALPHA 0, no con `transparent`.
   `transparent` es rgba(0,0,0,0): interpolar hacia el pasa por NEGRO transparente y
   ensucia el degradado justo en las puntas del arco, que es donde mas se nota.

   OPACIDAD .9, LA MISMA DEL LOGIN, y es una decision con historia: en el #589 se
   eligio un valor "sutil" (.10) que en pantalla resulto INVISIBLE y hubo que rehacerlo
   en el #590. Este es el mismo arco de 1 px sobre el mismo cristal, asi que bajarlo
   seria repetir aquel error a proposito.

   REQUISITOS DEL ANFITRION: `position: relative` (lo declara la clase), un
   `border-radius` propio -se hereda- y NO llevar `overflow: hidden`, que recortaria el
   marco de `inset: -1px`. Y no combinarla con `.grad-border` ni `.mirror-glass`: esos
   ya usan `::after` y esta clase, al cargarse despues, se lo borraria en silencio.
   Hay un guardian en `tests/test_borde_vivo.js` que lo vigila. */
.cl-borde-vivo { position: relative; }
.cl-borde-vivo::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(from var(--cl-escaneo, 0deg),
    transparent 0deg, transparent 226deg,
    rgba(56,224,245,0) 236deg,
    rgba(56,224,245,.95) 284deg,
    rgba(183,148,255,.95) 316deg,
    rgba(240,166,255,.95) 344deg,
    rgba(240,166,255,0) 356deg,
    transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
  opacity: calc(.9 * var(--glow, 1));
}
@media (prefers-reduced-motion: no-preference) {
  .cl-borde-vivo::after {
    animation: escaneoborde calc(4.5s / var(--motion, 1)) linear infinite;
    /* RETARDO NEGATIVO PARA DESINCRONIZAR, no positivo. Dos cuadros iguales lado a
       lado con el arco en el mismo punto se leen como una marcha militar, y eso delata
       que el efecto esta pegado por encima. Un retardo POSITIVO arreglaria la sincronia
       a costa de que el arco tarde en aparecer; uno NEGATIVO arranca la animacion como
       si ya llevara ese tiempo corriendo, asi que se ve descolocada Y visible desde el
       primer fotograma. Lo pone quien renderiza la lista: style={{'--cl-retardo': ...}}. */
    animation-delay: var(--cl-retardo, 0s);
  }
}

/* ---------- variante SOLO HOVER: para lo que se repite muchas veces ----------
   MISMO arco (no se duplica el gradiente: esto son modificadores de la clase de
   arriba), pero QUIETO y tenue en reposo, y animado al pasar el raton o al recibir
   el foco del teclado.

   POR QUE NO VA CONTINUO EN TODAS PARTES, que era la peticion inicial: un
   `conic-gradient` animado se REPINTA cada fotograma -no se compone en GPU como un
   `transform`-, y esto se aplica a las tarjetas de Mis casos y a los doce botones de
   la barra del mensaje. Treinta o mas animaciones de repintado a la vez, justo en la
   pantalla donde el abogado escribe, es ventilador encendido y bateria. En reposo
   aqui NO hay animacion: coste cero.
   Y la razon de diseno pesa igual: una luz que se mueve SIEMPRE en veinte tarjetas es
   ruido, y el ojo aprende a ignorarla. Moviendose al pasar por encima dice "esto se
   pulsa", que es justo lo que se queria decir.

   LA CASCADA SE RESUELVE POR ESPECIFICIDAD, NO POR ORDEN: `.cl-borde-vivo.cl-solo-hover`
   son dos clases (0,2,1) y le gana a `.cl-borde-vivo` (0,1,1) aunque esa este dentro de
   un @media -las media queries no suman especificidad-. Asi que mover este bloque no
   rompe nada, a diferencia de lo que pasa con `.cl-glow` y `.lift`. */
.cl-borde-vivo.cl-solo-hover::after {
  animation: none;
  opacity: calc(.30 * var(--glow, 1));
  transition: opacity .22s var(--ease-fluid);
}
.cl-borde-vivo.cl-solo-hover:hover::after,
.cl-borde-vivo.cl-solo-hover:focus-visible::after,
.cl-borde-vivo.cl-solo-hover:focus-within::after {
  opacity: calc(.9 * var(--glow, 1));
}
@media (prefers-reduced-motion: no-preference) {
  .cl-borde-vivo.cl-solo-hover:hover::after,
  .cl-borde-vivo.cl-solo-hover:focus-visible::after,
  .cl-borde-vivo.cl-solo-hover:focus-within::after {
    animation: escaneoborde calc(4.5s / var(--motion, 1)) linear infinite;
  }
}
/* Un modulo desactivado no invita a nada ni debe repintarse: aplica tanto al borde
   continuo (p. ej. el microfono) como a la variante solo-hover. */
.cl-borde-vivo:disabled::after,
.cl-borde-vivo[aria-disabled="true"]::after { opacity: 0; animation: none; }

/* ---------- variante FINA: los botones de 38 px de la barra ----------
   SOBRE EL GROSOR, dicho con precision: 1 px es el SUELO. Un `padding: .5px` en la
   mascara se dibuja distinto en cada navegador y a ciertos niveles de zoom desaparece
   del todo, asi que "medio pixel" no es una opcion fiable en un producto.
   Lo que si se adapta es el grosor APARENTE, y se hace con la opacidad: un navegador
   dibuja una linea de medio pixel exactamente asi -bajando el alfa por antialiasing-.
   Con .22 en reposo el arco se insinua y no satura el icono; con .62 al pasar el raton
   se lee claro sin competir con el.
   Y `inset: 0` en vez de -1px: en un boton de 38 px el borde que `iconBtn()` escribe en
   el style INLINE ya ocupa ese pixel. Con -1px se veian DOS marcos concentricos en 38
   px, que es el mismo apinamiento que hubo que deshacer en los cuadros de subir. Con 0
   el arco se dibuja ENCIMA del borde y lo sustituye mientras dura. */
.cl-borde-vivo.cl-fino::after {
  inset: 0;
  opacity: calc(.22 * var(--glow, 1));
}
.cl-borde-vivo.cl-fino:hover::after,
.cl-borde-vivo.cl-fino:focus-visible::after {
  opacity: calc(.62 * var(--glow, 1));
}

/* Pie del login: 3 px y no 2. `.kicker` ya trae `letter-spacing: .22em`, que sobre
   los 9.5 px de este pie son 2.09 px efectivos: fijar 2 px lo APRETARÍA en vez de
   estirarlo. 3 px es el otro valor pedido y sí se separa de lo que había.
   En `em` no: el tamaño de este pie viene en estilo en línea y cualquier ajuste de
   ahí movería el espaciado sin que nadie lo pidiera. */
.login-pie { letter-spacing: 3px; }
/* LA LEGIBILIDAD DEL PIE NO PUEDE DEPENDER DEL VELO, y por eso existe esta línea.
   El velo sobre el vídeo bajó a .10 para que la imagen se vea nítida, y a ese nivel un
   fotograma casi blanco dejaría este texto -9,5 px en `--text-mute`- ilegible. La sombra
   resuelve lo mismo sin coste visual: oscurece SOLO el contorno de las letras, en vez de
   oscurecer los dos millones de píxeles del vídeo para proteger tres líneas de tipografía.

   Son dos sombras: una muy pegada y opaca que da el filo, y otra amplia y suave que separa
   el bloque del fondo. Así el texto se sostiene igual sobre un fotograma negro que sobre
   uno blanco, que es la condición que un vídeo de fondo no garantiza nunca.

   IMPORTA QUE SEA ESTE TEXTO: aquí van los enlaces a Términos y Privacidad, y la
   verificación de marca de Google exige que la página de inicio enlace la política de
   privacidad. Un enlace que no se puede leer es un enlace que no está. */
.login-pie { text-shadow: 0 1px 2px rgba(0,0,0,.85), 0 0 12px rgba(0,0,0,.6); }

/* ---------- eliminar chat del historial ---------- */
.hist-item { cursor: pointer; }
.hist-item .hist-del {
  opacity: 0; flex-shrink: 0;
  width: 22px; height: 22px; border-radius: 6px;
  display: grid; place-items: center;
  color: var(--text-dim);
  transition: all .15s;
}
.hist-item:hover .hist-del { opacity: 1; }
.hist-item:hover .hist-time { display: none; }
.hist-del:hover { color: var(--acc-red); background: rgba(248,113,113,.12); }
.hist-confirm { display: inline-flex; gap: 4px; align-items: center; flex-shrink: 0; }
.hist-confirm button {
  width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center;
  transition: all .15s;
}
.hist-confirm .si { color: var(--acc-red); background: rgba(248,113,113,.12); }
.hist-confirm .si:hover { background: rgba(248,113,113,.25); }
.hist-confirm .no { color: var(--text-mute); }
.hist-confirm .no:hover { background: var(--bg-hover); }

/* ---------- modales ---------- */
.modal-overlay {
  position: fixed; inset: 0; z-index: 70;
  background: rgba(6,9,18,.82);
  display: grid; place-items: center;
  padding: 24px;
}
/* Estabilidad: nada de backdrop-filter sobre el Aurora animado.
   El blur en vivo se recalcula cada frame (parpadeo al escribir) y la
   animación de hover del CTA fuerza ese recálculo (temblor). Panel opaco. */
.modal-overlay > .glass {
  background: linear-gradient(160deg, #161f3c, #0b1224);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.toggle-track {
  width: 40px; height: 22px; border-radius: 99px; flex-shrink: 0;
  border: 1px solid var(--line-3);
  background: var(--bg-hover);
  position: relative; cursor: pointer;
  transition: all .2s var(--ease-fluid);
}
.toggle-track .knob {
  position: absolute; top: 2px; left: 2px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--text-mute);
  transition: all .2s var(--ease-fluid);
}
.toggle-track.on { background: rgba(91,140,255,.3); border-color: var(--acc-blue); box-shadow: var(--glow-blue); }
.toggle-track.on .knob { left: 20px; background: var(--grad-ia); }

/* ---------- búho pensando: anillo orbital + bits (video seg. 3–6) ---------- */
.owl-ring {
  position: absolute;
  transform: translate(-50%, -50%) rotateX(74deg);
  pointer-events: none;
  z-index: 0;
}
.owl-ring-spin {
  width: 100%; height: 100%; border-radius: 50%;
  background: conic-gradient(transparent 0deg 100deg, rgba(56,224,245,.95) 195deg, rgba(91,140,255,.95) 262deg, rgba(240,166,255,.95) 330deg, transparent 360deg);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
  mask: radial-gradient(closest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
  filter: drop-shadow(0 0 7px rgba(56,224,245,.85));
}
@media (prefers-reduced-motion: no-preference) {
  .owl-ring-spin { animation: ringspin calc(1.5s / var(--motion,1)) linear infinite; }
  /* Spinner reutilizable para botones en estado "cargando" (p. ej. el boton de
     descargar el .docx mientras el backend lo genera). Solo gira si el usuario no pidio
     reducir el movimiento; si lo pidio, el icono se queda quieto -el texto "Generando…"
     ya comunica el estado- en vez de girar igual. Comparte `ringspin` con el anillo del
     buho para no duplicar el keyframe. */
  .spin { animation: ringspin calc(.9s / var(--motion,1)) linear infinite; transform-origin: center; }
}
@keyframes ringspin { to { transform: rotate(360deg); } }
.owl-bit {
  position: absolute; bottom: 16%;
  font-family: var(--font-mono);
  color: var(--acc-cyan);
  text-shadow: 0 0 6px var(--acc-cyan);
  opacity: 0; pointer-events: none; z-index: 2;
}
@media (prefers-reduced-motion: no-preference) {
  .owl-bit { animation: bitrise calc(2s / var(--motion,1)) linear infinite; }
}
@keyframes bitrise {
  0% { transform: translateY(0); opacity: 0; }
  18% { opacity: .95; }
  70% { opacity: .45; }
  100% { transform: translateY(-3.4em); opacity: 0; }
}

/* ---------- select estilizado ---------- */
.nice-pop {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  z-index: 90; padding: 5px;
  border-radius: 12px;
  max-height: 250px; overflow-y: auto;
  box-shadow: var(--shadow-lift);
}
.nice-opt {
  display: flex; align-items: center; gap: 8px;
  width: 100%; padding: 8px 11px; border-radius: 8px;
  font-size: 13px; color: var(--text-mid);
  transition: background .12s; text-align: left;
}
.nice-opt:hover { background: var(--bg-hover); color: var(--text-hi); }
.nice-opt.sel { color: var(--acc-cyan); }

/* ---------- toast ---------- */
.toast {
  position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%);
  z-index: 120;
  display: flex; align-items: center; gap: 9px;
  padding: 11px 18px;
  border-radius: 12px;
  font-size: 13px; color: var(--text-hi);
  box-shadow: var(--shadow-lift), var(--glow-blue);
}
@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toastin .35s var(--ease-fluid); }
}
@keyframes toastin { from { opacity: 0; transform: translateX(-50%) translateY(14px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }


/* ---------- Badge «LIVE» (Abogado en vivo) + pulso del dictado ---------- */
@keyframes cl-live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
.live-badge {
  font-family: var(--font-mono);
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .14em;
  color: var(--acc-red);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.live-badge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--acc-red);
  box-shadow: 0 0 6px var(--acc-red);
  animation: cl-live-pulse 1.4s ease-in-out infinite;
}
/* Activo (al dar clic / en vivo): pasa de rojo a verde. */
.live-badge.on { color: var(--acc-mint); }
.live-badge.on::before { background: var(--acc-mint); box-shadow: 0 0 6px var(--acc-mint); }
.live-pulse { animation: cl-live-pulse 1.2s ease-in-out infinite; }


/* ============================================================================
   LA ISLA DE CRISTAL DE «ANALIZAR»: interruptor y estado de escaneo
   ============================================================================
   Las dos piezas van DENTRO de `prefers-reduced-motion: no-preference` y dividen su duracion
   por `var(--motion)`, igual que el resto del proyecto. Y esto incluye la TRANSICION del
   interruptor, no solo la animacion del boton: quien pide menos movimiento no quiere que un
   control se deslice bajo su dedo, quiere que cambie. Sin la transicion el indicador SALTA al
   sitio, que es exactamente lo correcto -no se pierde ninguna informacion, solo el viaje-. */
@media (prefers-reduced-motion: no-preference) {
  /* El indicador del interruptor. Se anima `transform` y `width`, y no `left`, para que el
     navegador lo componga sin recalcular la maquetacion en cada fotograma. */
  .cl-pill-ind {
    transition: transform calc(.3s / var(--motion, 1)) var(--ease-fluid),
                width calc(.3s / var(--motion, 1)) var(--ease-fluid);
  }

  /* ESTADO DE ESCANEO DEL BOTON MAESTRO: un brillo que recorre el boton de izquierda a
     derecha mientras la IA lee. No es decoracion: es la señal de que algo esta pasando en un
     proceso que puede tardar minutos con un PDF escaneado, y sin ella el abogado no sabe si
     pulsar otra vez.

     VA EN `::before` Y NO EN `::after` PORQUE `.cta::after` YA ESTA OCUPADO por el destello
     del boton (base.css). Es la misma coleccion de un solo inquilino que rompio los tooltips
     en el #694: dos reglas sobre el mismo pseudoelemento se pisan SIN dar error. `.cta` ya
     trae `overflow: hidden`, asi que el barrido queda recortado al boton sin añadir nada.

     Se reutiliza `@keyframes shimmer` (base.css), el mismo del `.skeleton`: dos definiciones
     del mismo barrido acabarian con dos velocidades distintas en la misma pantalla. */
  .cl-escaneando::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(100deg, transparent 22%,
                rgba(255, 255, 255, calc(.32 * var(--glow, 1))) 50%, transparent 78%);
    transform: translateX(-100%);
    animation: shimmer calc(1.25s / var(--motion, 1)) linear infinite;
  }
}

/* SIN MOVIMIENTO TAMBIEN HAY QUE SABER QUE ESTA TRABAJANDO. El barrido de arriba vive dentro
   de la puerta de `prefers-reduced-motion`, asi que quien la tenga puesta no lo ve; lo que no
   puede pasar es que el boton quede indistinguible de uno en reposo. El atenuado y el cursor
   van FUERA de la puerta, porque no son movimiento: son estado. */
.cl-escaneando {
  cursor: progress;
  filter: saturate(.75) brightness(.94);
}


/* ============================================================================
   EL CAJÓN: ENTRADA CON REBOTE Y ENTRADA ESCALONADA

   VA AL FINAL DEL ARCHIVO A PROPÓSITO. `tests/test_borde_vivo.js` recorta el bloque del
   borde vivo así:

       ini      = anim.indexOf('.cl-borde-vivo::after {');
       finMedia = anim.indexOf('@media (prefers-reduced-motion: no-preference) {', ini);

   O sea que CUALQUIER `@media (prefers-reduced-motion: no-preference) {` que se cuele entre
   esa regla y su propio bloque de movimiento trunca el corte y tumba tres secciones de esa
   prueba. Aquí abajo no hay nada entre medias.

   ---------------------------------------------------------------------------
   1. EL DESLIZAMIENTO CON REBOTE

   Lo que había era `transition: transform .24s var(--ease-fluid)` escrito en el `style`
   INLINE del envoltorio (main.jsx), y tenía dos defectos que no se ven pero que el proyecto
   ya había decidido no tener:
     · no dividía por `--motion`, así que el ajuste de velocidad del abogado no le afectaba;
     · no estaba detrás de `prefers-reduced-motion`, así que quien pide menos movimiento veía
       el deslizamiento igual. Es el único movimiento del producto que se había quedado fuera
       de esa puerta.
   Al traerlo a CSS se arreglan los dos de paso.

   ENTRADA CON ANIMACIÓN, SALIDA CON TRANSICIÓN, y no es una inconsistencia: son dos gestos
   distintos. Al abrir se quiere un sobreimpulso -el cajón pasa un poco de largo y se asienta,
   que es lo que hace que se sienta físico-, y eso son fotogramas intermedios que una
   `transition` no puede describir. Al cerrar no hay nada que asentar: el cajón se va, y lo
   natural es una curva de salida sin rebote. Poner rebote también al cerrar haría que el
   cajón «rebotara contra el borde de la pantalla», que es justo lo que no pasa en la realidad.

   EL SOBREIMPULSO ES DEL 2,4 % y se reparte en dos pasos: 2,4 % de más y luego 0,8 % de menos
   antes de asentar. Un solo sobreimpulso seguido del reposo se lee como un frenazo; el
   segundo rebote, mucho más pequeño, es lo que lo convierte en un muelle.

   NO SE USA `animation-fill-mode`. Con `both` el último fotograma se queda pegado, y al
   cerrar habría que esperar a que el navegador lo suelte para que la transición arranque:
   se veía un salto. Sin fill, al acabar la animación el valor vuelve al `transform` del
   `style` inline, que YA es el de abierto, así que no hay salto; y al cerrar, ese inline
   cambia y la transición lo recoge desde donde estaba. */
.cl-cajon {
  transition: transform calc(.28s / var(--motion, 1)) cubic-bezier(.32, 0, .24, 1),
              visibility calc(.28s / var(--motion, 1));
}
@media (prefers-reduced-motion: reduce) {
  .cl-cajon { transition: none; }
}
@keyframes drawer-spring {
  from { transform: translateX(-100%); }
  58% { transform: translateX(2.4%); }
  79% { transform: translateX(-0.8%); }
  to { transform: translateX(0); }
}

/* ---------------------------------------------------------------------------
   2. LA ENTRADA ESCALONADA

   Las cinco zonas del panel entran una detrás de otra en vez de aparecer de golpe. Los
   retardos los pone quien renderiza, con `--cl-paso`, porque el orden es una decisión de
   maquetación y no de la hoja de estilos: la zona del historial es condicional (el aviso de
   «historial apagado» solo existe a veces), así que un `:nth-child` se descolocaría solo.

   NO SE REUTILIZA `--cl-retardo`, que es la variable del borde vivo. Esa es para retardos
   NEGATIVOS -desincronizar arcos que ya están corriendo- y `tests/test_borde_vivo.js` §12
   falla a propósito si un .jsx le pasa un valor positivo. Son dos cosas distintas con dos
   nombres distintos.

   EL DESPLAZAMIENTO ES LATERAL y de 16 px: el panel entra desde la izquierda, así que sus
   piezas llegando desde la izquierda acompañan ese gesto. Con desplazamiento vertical
   parecería que el contenido cae dentro de un panel que ya estaba.

   `both` PARA QUE EL RETARDO SE NOTE: sin fill, durante los 40-300 ms de espera el elemento
   estaría ya visible y el escalonamiento no existiría. Va dentro de `no-preference`, así que
   quien pide menos movimiento no se queda con nada invisible: sin animación, no hay
   `opacity: 0` inicial que aplicar. */
@keyframes cl-entrada {
  from { opacity: 0; transform: translateX(-16px); }
  to { opacity: 1; transform: none; }
}

/* ============================================================================
   CRISTAL: base translúcida + barrido especular

   Se aplica a las filas del panel lateral y a la caja del login. Son las dos capas que
   faltaban: el ANILLO HOLOGRÁFICO ya existe en este archivo (`.cl-borde-vivo`, que reusa
   `@property --cl-escaneo` y `@keyframes escaneoborde`) y se compone con estas clases en vez
   de duplicarse. Las razones de no escribir un segundo anillo, por orden de peso:

     1. LAS FILAS DE CASOS DE ESTE MISMO PANEL YA LLEVAN EL ANILLO CÓNICO
        (`hist-item cl-borde-vivo cl-solo-hover cl-fino`, main.jsx). Un anillo distinto en los
        módulos dejaría DOS animaciones de borde diferentes a diez píxeles una de otra, en la
        misma lista. Eso no se lee como premium, se lee como dos manos distintas.
     2. `tests/test_borde_vivo.js` §9 exige que `conic-gradient(from var(--cl-escaneo` aparezca
        EXACTAMENTE DOS VECES en el archivo. Ese guardián existe justo para impedir un tercer
        sistema de anillos.
     3. El anillo continuo en muchas filas ya se descartó aquí por coste: repinta cada
        fotograma. Por eso `.cl-solo-hover` existe.

   EL BARRIDO SE MUEVE CON `background-position`, NO CON `transform`, y esto es una
   restricción real, no una preferencia. Una banda desplazada con `transform` se sale de la
   caja y hay que recortarla con `overflow: hidden` en el anfitrión — y la caja del login lleva
   su anillo en `::after` con `inset: -1px`, o sea UN PÍXEL POR FUERA: recortarla se lo comería.
   Moviendo el fondo dentro de un pseudoelemento que ya está a `inset: 0`, la banda no puede
   salirse y no hace falta recortar nada. Es además la mecánica pedida: `background-size: 200%`
   y la posición de 0 % a 200 %.

   SOLO AL PASAR EL RATÓN (o con el foco del teclado). En reposo no hay ninguna animación
   corriendo: coste cero con el panel quieto, que es lo que importa cuando hay catorce filas.

   `::before` Y NO `::after`: el `::after` de estas mismas filas y de la caja del login está
   ocupado por el anillo. En la caja del login `::before` lo usa hoy `.glass` para un brillo
   estático, y aquí se reaprovecha para el barrido -es el mismo papel, reflejo sobre el
   cristal, pero en movimiento-. */
/* LAS FILAS LLEVAN EL CRISTAL, PERO NO EL `backdrop-filter`, Y ESTO SE MIDIÓ.
   La receta pedida incluía `blur(14px) saturate(1.5)` también aquí. Se implementó, se comparó
   la captura del panel con y sin el filtro píxel a píxel (con el movimiento congelado, para que
   no lo ensuciara ninguna animación) y el resultado fue que el desenfoque solo cambia el 0,8 %
   de los subpíxeles, concentrados en una banda estrecha alrededor de las LÍNEAS DIVISORIAS: lo
   único que hacía era emborronar cantos que son nítidos a propósito. Tiene sentido: lo que hay
   detrás de una fila es el `--panel-bg` del nav -plano- y, en el cajón móvil, un `--bg-void`
   opaco. Desenfocar algo plano no puede producir cristal.
   A cambio costaba dos cosas de verdad: dieciséis capas con `backdrop-filter` dentro de un
   contenedor con scroll, que el navegador vuelve a muestrear en cada fotograma de
   desplazamiento -un presupuesto de repintado MAYOR que el del anillo cónico continuo que este
   mismo cambio evita-, y un defecto: `backdrop-filter` convierte cada fila en un contexto de
   apilamiento, así que el globo de ayuda del rayo de crédito -el único que se abre hacia ABAJO,
   `.tip-left`- quedaba atrapado dentro de su fila y lo tapaba la fila siguiente.
   Así que el cristal de las filas es la base translúcida, que es la que se ve de verdad. El
   desenfoque se queda donde sí trabaja: la caja del login, que tiene un vídeo detrás.

   VA TAMBIÉN EN `.hist-item`, no solo en las filas de módulo. Las filas de caso son
   `.hist-item` y se renderizan ENTRE las de módulo: platear unas y no otras deja una sola
   columna con dos acabados distintos, que es exactamente el argumento por el que aquí se reusa
   el anillo en vez de escribir otro. */
.side-item, .hist-item {
  position: relative;
  /* La base de cristal se MEZCLA: la superficie elevada del tema con el fondo de la página.
     Así el mismo valor funciona en los cuatro temas sin escribir un `rgba` por tema, que es
     como se colaron antes los fondos que no seguían al tema. */
  background: color-mix(in oklab, var(--bg-card) 22%, transparent);
}
/* En alto contraste NO hay cristal. Ese modo existe para quitar capas translúcidas, y su
   paleta lo hace por tokens (`--glass-a`, `--glass-sheen`…); una mezcla escrita a mano aquí se
   le escaparía, así que se apaga explícitamente. Las filas vuelven a ser planas, con su borde
   y sus estados de siempre, que es lo legible. */
html[data-contrast="alto"] .side-item,
html[data-contrast="alto"] .hist-item { background: transparent; }
/* Los estados de encima se redeclaran para que NO pierdan el cristal: las reglas originales de
   `:hover` y `.active` ponen un fondo opaco y, al tener más especificidad que la base, lo
   taparían. Aquí suben la mezcla en vez de sustituirla.
   EL SALTO REPOSO -> ENCIMA TIENE QUE SEGUIR SIENDO GRANDE. Antes iba de transparente a opaco;
   con la base de cristal se comprimió por los dos lados (22 % -> 82 %) y en dieciséis filas el
   `:hover` es la señal principal de «esto se pulsa». Así que la base baja a 22 % y los estados
   suben a 95 %: el cristal se nota en reposo y el salto se lee igual que antes. */
.side-item:hover, .hist-item:hover { background: color-mix(in oklab, var(--bg-hover) 95%, transparent); }
.side-item.active, .hist-item.active { background: color-mix(in oklab, var(--bg-selected) 95%, transparent); }

/* EL ANILLO ARRANCA INVISIBLE EN ESTAS FILAS, y esto se decidió MIRÁNDOLO.
   `.cl-fino` deja el arco al 22 % en reposo, que es lo correcto donde se diseñó: los botones
   de 38 px de la barra del mensaje, casi cuadrados, donde un arco cónico quieto se lee como un
   anillo tenue. Una fila del panel mide 272 × 36 px, y sobre esa proporción el mismo sector
   (226°-360°) no da un anillo: da UN TROZO DE LÍNEA en el canto de arriba que se corta a media
   fila. Con una sola fila pasa desapercibido; con dieciséis, la lista entera parece tener los
   bordes roved. Se comprobó en captura antes y después.
   Así que aquí el arco no existe hasta que hay ratón o foco, que además es cuando significa
   algo. La regla va DESPUÉS de `.cl-fino` en el archivo y con la misma especificidad (0,2,1),
   así que gana por orden; los estados de `:hover`/`:focus-visible` son (0,3,1) y le ganan a
   esta, que es justo lo que se quiere. */
.side-item.cl-borde-vivo::after,
.hist-item.cl-borde-vivo::after { opacity: 0; }
/* Y EL FOCO DE RATÓN NO DEJA EL ANILLO GIRANDO. `:focus-within` acierta también en el propio
   `<button>`, así que un clic en una fila la dejaba con el arco cónico repintándose PARA
   SIEMPRE: precisamente el gasto continuo que `.cl-solo-hover` existe para evitar, y encima
   visible porque ahora es la única fila con anillo. Se apaga el caso «tiene el foco pero no el
   ratón y no llegó por teclado»; con `:hover` o con `:focus-visible` el anillo sigue saliendo,
   que es cuando significa algo. */
.side-item.cl-borde-vivo:focus-within:not(:hover):not(:focus-visible)::after,
.hist-item.cl-borde-vivo:focus-within:not(:hover):not(:focus-visible)::after {
  opacity: 0;
  animation: none;
}

/* LA BANDA, declarada UNA vez para sus tres anfitriones. En las filas del panel cabe en el
   `::before` -ahí está libre-, pero en la caja del login NO: su `::after` lo ocupa el anillo y
   su `::before` lo ocupa el brillo del cristal de `.glass`. Robarle ese `::before` apaga el
   brillo sin que se note, y hay un guardián que lo impide a propósito
   (`tests/test_login_premium.js`, punto 5 de su cabecera: «que la linea de escaneo se mude a
   ::before y mate en silencio el brillo del cristal»). Así que allí la banda va en una capa
   propia, `.cl-brillo`, que es un elemento de verdad: ni se pelea con los pseudoelementos ni
   duplica la receta del cristal. */
.side-item::before, .hist-item::before, .cl-brillo {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background-image: linear-gradient(120deg,
    transparent 42%, var(--cl-destello) 50%, transparent 58%);
  background-repeat: no-repeat;
  background-size: 200% 100%;
  background-position: 0% 0;
}
@keyframes crystal-flow {
  from { background-position: 0% 0; opacity: 0; }
  16% { opacity: 1; }
  84% { opacity: 1; }
  to { background-position: 200% 0; opacity: 0; }
}
@media (prefers-reduced-motion: no-preference) {
  /* El escalonado del panel. El selector cuelga de `.cl-cajon-visible`, que el panel lleva
     mientras se ve: en escritorio la lleva siempre -así que el escalonado corre al montar y
     al desplegar- y en móvil la lleva solo con el cajón abierto, lo que hace que vuelva a
     correr en CADA apertura sin tener que remontar el panel (el historial no se recarga). */
  .cl-cajon-visible .cl-entra {
    /* `backwards` Y NO `both`, y la diferencia NO es cosmética. Lo que hace falta es cubrir el
       RETARDO -que durante los 40-300 ms de espera el elemento no se vea todavía-, y eso es
       exactamente `backwards`. Con `both` se retiene además el ÚLTIMO fotograma para siempre, y
       una declaración de animación gana a las del autor: el `transform: none` del final se
       quedaba pegado y dejaba MUERTO el `transform: translateY(-1px)` del `:hover` de `.cta`,
       que es el botón de «Nueva conversación». Se veía brillar y no levantarse.
       Es además lo que ya usan `.fade-up`, `.mm-node`, `.donut-seg` y `.bar-grow`. */
    animation: cl-entrada calc(.42s / var(--motion, 1)) var(--ease-fluid) backwards;
    animation-delay: var(--cl-paso, 0s);
  }
  .cl-cajon.cl-cajon-abierto {
    animation: drawer-spring calc(.52s / var(--motion, 1)) cubic-bezier(.22, .78, .3, 1);
  }
  /* UN SOLO disparador para los tres anfitriones. La duración se lee de `--cl-barrido` para
     que la caja del login pueda ir más lenta -es mucho más ancha, y a la misma duración el
     reflejo la cruzaría demasiado rápido- sin necesidad de una segunda regla ni de un segundo
     `@keyframes`. `focus-visible` solo en las filas: quien navega con teclado no puede pasar
     el ratón por encima, y en la caja del login un barrido en cada tabulación entre campos
     sería un parpadeo. */
  .side-item:hover::before,
  .side-item:focus-visible::before,
  .hist-item:hover::before,
  .hist-item:focus-visible::before,
  .login-wrap .glass.login-escaneo:hover .cl-brillo {
    animation: crystal-flow calc(var(--cl-barrido, .85s) / var(--motion, 1)) var(--ease-fluid);
  }
  /* En alto contraste el token del destello es `transparent`, así que la banda ya no se ve —
     pero la animación seguía CORRIENDO sobre un degradado invisible cada vez que el puntero
     cruzaba una fila. Aquí se apaga el trabajo, no solo el resultado. */
  html[data-contrast="alto"] .side-item:hover::before,
  html[data-contrast="alto"] .hist-item:hover::before,
  html[data-contrast="alto"] .login-wrap .glass.login-escaneo:hover .cl-brillo { animation: none; }
}
/* LA CAJA DEL LOGIN CONSERVA SUS 18 px DE DESENFOQUE. La receta pedía 14, y ponerlos aquí era
   una REBAJA que no se veía venir: `.glass` ya declara `blur(18px) saturate(1.3)` (base.css) y
   este selector es más específico, así que la tarjeta que tenía el cristal más espeso del
   producto se habría quedado con 4 px menos que cualquier otra. Y es justo la que lo necesita:
   es la única que se apoya sobre el vídeo del fondo, y hay un velo (`.login-velo`) puesto a
   propósito porque los rótulos de los campos se lavaban sobre los fotogramas claros.
   Lo que sí se sube es la SATURACIÓN, de 1,3 a 1,5, que es la otra mitad de la receta y no
   quita nada a nadie. */
.login-wrap .glass.login-escaneo {
  backdrop-filter: blur(18px) saturate(1.5);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  --cl-barrido: 1.15s;
}
