/* ============================================================================
   Sistema OSECAC — Movimiento
   Doce gestos con nombre. La regla que los ordena:
   si un movimiento no informa un cambio de estado, no va.
   Todo respeta prefers-reduced-motion: los estados cambian igual, sin recorrido.
   ========================================================================== */

/* 1 · Entrada escalonada — el orden de lectura de la pantalla.
       Cada elemento lleva --d con su desfase. Ésta es la versión de los mockups (transición
       atada a `[data-vivo]`); la del producto es la animación `entrar-al-listado` de
       `portal.css`, con los tiempos y el recorrido de los tokens `--cascada-*` (T164) en las filas de
       `.grilla`, `.listado > *`, `.lista > *` y `[data-en-cascada] > *`. */
.entra{opacity:0;transform:translateY(7px)}
[data-vivo] .entra{
  opacity:1;transform:none;
  transition:opacity var(--dur-media) ease,transform var(--dur-larga) var(--curva-salida);
  transition-delay:var(--d,0ms);
}
/* Variante sin desplazamiento. Va en TODO lo que tenga un transform propio en
   otro estado: el panel de detalle (que en móvil es una hoja) y los botones
   (que se elevan al hover). Con .entra, el transform:none de la entrada gana
   por especificidad y mata el otro estado — la hoja queda siempre abierta, o
   el botón no sube nunca. Ya pasó dos veces. */
.entra-fundido{opacity:0}
[data-vivo] .entra-fundido{opacity:1;transition:opacity var(--dur-media) ease;transition-delay:var(--d,0ms)}

/* 2 · Rodado de importes — la cifra cambió, y hacia dónde.
       El recorrido lo hace JS; el sistema sólo garantiza el ancho estable.

   ⚠️ **La regla de arriba tiene una excepción, y está fechada** (07/09/2026). Las
   cifras que encabezan una pantalla ruedan **al entrar a esa pantalla**, aunque
   entrar no sea un cambio de estado. La pidió el negocio: son lo primero que se mira
   y el recorrido es lo que hace que se las mire.

   La excepción es **a entrar y sólo a entrar** — no al filtrar, ni al buscar, ni al
   recargar, ni al volver con el atrás—, y se marca cifra por cifra con `data-rodar`.
   Eso último importa: `.valor` también dibuja el CUIT en el legajo de la empresa, y
   un identificador que cuenta hacia arriba miente sobre lo que es.

   No habilita animar cualquier cosa. La regla sigue siendo la de arriba; ésta es la
   única excepción y tiene nombre, fecha y alcance. */
.rueda{font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}

/* 3 · Barrido de fila — dónde está el cursor en una grilla ancha.
       JS fija --rx con la posición del puntero. Ver .grilla en componentes.css */

/* 4 · Traspaso al detalle — el panel ya habla de otra empresa. */
.traspasa::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(90deg,var(--acento),transparent 42%);
  opacity:.14;animation:barrido var(--dur-larga) ease-out forwards;
}
@keyframes barrido{from{opacity:.18;transform:translateX(-12px)}to{opacity:0;transform:none}}

/* 5 · Hoja sobre velo — el resto de la app queda en suspenso.
       Ver .velo y .hoja en componentes.css */

/* 6 · El sello — el acta quedó cancelada, con fuerza de acto. */
@keyframes sellar{
  0%  {opacity:0;  transform:translate(-50%,-50%) rotate(-13deg) scale(1.75)}
  22% {opacity:.96;transform:translate(-50%,-50%) rotate(-13deg) scale(1)}
  28% {transform:translate(-50%,-50%) rotate(-12.2deg) scale(1.03)}
  34% {transform:translate(-50%,-50%) rotate(-13deg) scale(1)}
  78% {opacity:.96}
  100%{opacity:0;  transform:translate(-50%,-50%) rotate(-13deg) scale(1)}
}

/* 7 · Latido de intimación — ese expediente corre plazo legal.
       Único movimiento permanente del sistema. Ver .estado--intimada */
@keyframes latido{
  0%,100%{opacity:1;transform:scale(1)}
  50%    {opacity:.35;transform:scale(.72)}
}

/* 8 · Deriva de aurora — si lo que mirás tiene deuda o está saldado. */
.aurora-lienzo{position:absolute;inset:0;z-index:0;pointer-events:none;background:var(--aurora);transition:filter 1.1s ease}
.aurora-lienzo[data-saldo="deuda"]{filter:hue-rotate(-16deg) saturate(1.14)}
.aurora-lienzo[data-saldo="saldado"]{filter:hue-rotate(12deg) saturate(.9)}

/* 9 · Continuidad entre páginas — el mismo contenido, no una pantalla nueva.
       Sin esto, cambiar de pantalla es un parpadeo blanco y lo que se fue no se
       fue a ningún lado: simplemente dejó de estar. Con la transición el
       navegador tiene las dos instantáneas y hace la continuidad solo.

       Sirve para las **dos** formas de cambiar de estado, con la misma
       maquinaria y el mismo nombre en el elemento:

       - una **navegación completa** — la declara este `@view-transition`;
       - un **intercambio de fragmento** — lo pide quien lo hace, con
         `hx-swap="… transition:true"` (ver la nómina de CUILes).

       Es **mejora progresiva pura**: donde no exista, el cambio es instantáneo,
       que es lo que hacía antes. Y `view-transition-name` va en el elemento que
       tiene que reconocerse de un estado al otro —**único por página**, o el
       navegador descarta la transición entera—. */
@view-transition{navigation:auto}

/* 10 · El giro del cielo — cambiaste de tema, y el fondo entero es el que cambia.
        ────────────────────────────────────────────────────────────────────────
        El fondo del sistema deja de ser un `background` del `<body>` y pasa a
        ser **dos capas apiladas**, las dos fijas al viewport y las dos detrás
        de todo (`z-index` negativo):

          .cielo__disco    el PISO. Un círculo de 340vh centrado abajo del
                           viewport, pintado con `--cielo-degrade` —día arriba,
                           noche abajo, crepúsculo en la costura—. Cambiar de
                           tema lo gira **180°**: la costura barre la pantalla
                           de lado a lado y amanece, o anochece.
          .cielo__manchas  las MANCHAS de la aurora, que no giran. Están las dos
                           siempre en el DOM y se cruzan por opacidad, porque un
                           `background` no transiciona entre dos pilas de
                           degradés: la que se va se desvanece mientras la que
                           llega aparece.

        ⚠️ **El disco gira, las manchas no**, y es a propósito. Nuestra aurora
        son manchas *ubicadas* —una arriba a la izquierda, otra arriba a la
        derecha, otra abajo—; si girasen con el disco, la identidad del portal
        daría una vuelta de campana. Lo que gira es el cielo detrás de ellas.

        Lo dispara el atributo `data-tema` del `<html>`, que cambia `portal.js`
        cuando el servidor confirma el tema nuevo. **No hay ningún otro estado**:
        ni clase mientras dura, ni temporizador — el gesto entero es CSS, y el JS
        sólo cambia un atributo. Sin JavaScript no hay giro y el cambio de tema
        llega por navegación completa, ya pintado, que es exactamente lo que
        pasaba antes de este gesto. */
.cielo{position:fixed;inset:0;overflow:hidden;z-index:-2;pointer-events:none;
  background:var(--cielo-piso)}
.cielo__disco{
  position:absolute;left:50%;top:100%;border-radius:50%;
  /* ⚠️ `340vh` a secas deja franjas a los costados en una pantalla ancha y
     baja —1920×500 da 1700 px de disco para 1920 de ancho—. El `max` con
     `vw` lo cubre en los dos sentidos. */
  width:max(340vh,220vw);height:max(340vh,220vw);
  transform-origin:50% 50%;
  transform:translate(-50%,-46%) rotate(var(--cielo-giro));
  background:var(--cielo-degrade);
  /* ⚠️ **Nada de filtros acá.** Un filtro obliga a rasterizar este elemento aparte,
     y con 2816 px de lado eso no entra en el presupuesto de un cuadro. Lo único
     que se anima es la transformación, que va por el compositor y es barata. */
  transition:transform var(--dur-cielo) var(--curva-cielo);
}
/* 🔴 **El desenfoque del viaje se sacó, y no por gusto.** El diseño de referencia
   lo tiene, y la primera versión lo copió con `filter:blur(9px)` sobre el disco.
   El usuario reportó dos veces que la animación parpadeaba al final; medido con
   los cuadros que entrega el compositor, en la ventana de 1000 ms del gesto:

       filter:blur(9px) sobre el disco ...... 27 cuadros
       backdrop-filter blur(9px), viewport .. 35 cuadros, salto de 61
       backdrop-filter blur(4px), viewport .. 28 cuadros, salto de 53
       backdrop-filter blur(2px), viewport .. 34 cuadros, salto de 63
       **sin desenfoque** .................... **52 cuadros, rampa monótona**

   ⚠️ **El radio no es lo que cuesta: es que haya un desenfoque.** Bajarlo de 9 px
   a 2 px no mejora nada. Con desenfoque, la rampa de diferencia entre cuadros
   pega un salto y vuelve —un cuadro perdido y otro que se pone al día—, y eso es
   exactamente lo que se ve como parpadeo, siempre sobre el final.

   Sin él el barrido queda suave y el gesto se lee mejor. Es la divergencia más
   grande respecto del diseño de referencia, y la única que se tomó midiendo. */

.cielo__manchas{position:absolute;inset:0;opacity:0;
  transition:opacity var(--dur-cielo) ease}
.cielo__manchas--noche{background:var(--aurora-manchas-nocturna)}
.cielo__manchas--dia{background:var(--aurora-manchas-diurno)}
[data-tema="nocturna"] .cielo__manchas--noche{opacity:1}
[data-tema="diurno"] .cielo__manchas--dia{opacity:1}

/* Las nubes viven en la mitad de día del disco, así que se ven sólo cuando esa
   mitad quedó arriba. No hay que apagarlas: el giro se las lleva. */
/* 🟡 **Quietas.** La primera versión las traía a la deriva y las estrellas
   titilando, como el diseño de referencia. Se sacó por dos motivos, y el primero
   es la regla que encabeza este archivo: **si un movimiento no informa un cambio
   de estado, no va**, y el único movimiento permanente del sistema es el latido
   de intimación, que sí informa —ese expediente corre plazo legal—. El segundo lo
   reportó el usuario mirando la pantalla: detrás de un panel con
   `backdrop-filter`, un fondo que nunca se queda quieto se lee como que los
   títulos y el texto **parpadean**.

   🔀 **Y las estrellas terminaron yéndose enteras** (07/09/2026). Quedaron dos
   capas de puntos en la mitad de noche, con una nota que decía que iban «más
   tenues que en el diseño de referencia» —opacidad .22— para que no compitieran
   con una grilla de importes. Que una textura necesite esconderse para no
   molestar es la señal de que no iba: en el diseño de referencia las estrellas
   **son** el tema de la pantalla, y acá el tema es un acta. La mitad de noche va
   limpia, que además es lo que hace legible el degradé del horizonte.

   Las nubes se quedan: son las que dan el cielo del lado del día. */
.cielo__nube{position:absolute;border-radius:50%;background:rgba(255,255,255,.5)}
.cielo__nube:nth-child(1){left:14%;top:33%;width:40vh;height:9vh;filter:blur(3.2vh)}
.cielo__nube:nth-child(2){left:58%;top:26%;width:30vh;height:7vh;filter:blur(3vh);
  background:rgba(255,255,255,.42)}
.cielo__nube:nth-child(3){left:34%;top:43%;width:56vh;height:8vh;filter:blur(4vh);
  background:rgba(255,228,214,.5)}


/* 11 · La espera — el servidor está pensando, y se ve DÓNDE.
   ────────────────────────────────────────────────────────────────────────────
   Hasta el 15/09/2026 el sistema no tenía ningún estado de carga. Cero. Cada
   acción es un viaje al servidor (HTMX), y entre el clic y la respuesta no pasaba
   nada en pantalla. En el escritorio de la oficina eso se disimula; en la tablet
   del inspector, con señal mala, son segundos de interfaz muerta en los que la
   única lectura posible es «no anduvo» — y el reflejo es volver a tocar.

   🔴 **Los primeros 140 ms no muestran nada, y es la mitad del diseño.** Una
   respuesta que llega en 80 ms con un indicador de por medio se ve PEOR que sin
   nada: es un parpadeo. El indicador existe para las esperas que se notan, no
   para todas. La demora la aplica `portal.js`, que es quien pone el atributo.

   **Se marca la región que cambia, no la pantalla.** Un velo global diría «la app
   está ocupada»; lo que hace falta decir es «esta tabla se está actualizando» —y
   sobre todo, cuando el objetivo es `closest tr`, «ESTA fila». Por eso el estado
   va sobre el objetivo del intercambio. El `<body>` queda excluido a propósito
   (`portal.js`): apagar la pantalla entera es exactamente lo que no queremos.

   **El hilo no es la única señal, y hasta el 19/09/2026 sí lo era.** Acá decía
   «no hay ruleta», citando taste-frontend §4.5 —*«avoid generic circular
   spinners»*—, y esa regla contestaba **una** pregunta: *¿qué región se está
   actualizando?* Para eso el hilo sigue siendo mejor que cualquier ruleta: ocupa
   cero espacio, no desplaza nada al aparecer y dice **dónde**.

   Lo que la regla no cubría es la otra pregunta, *¿hay algo en curso?*, que
   aparece donde **no hay región que marcar**: el botón que ya se apretó, el
   arranque de la aplicación, el ingreso, un pie de listado. Ahí no había nada.

   🔵 **Desde el 19/09/2026 (T084) conviven, y se reparten por la pregunta que
   contestan:**

   | Señal | Dice | Dónde |
   |---|---|---|
   | el hilo | «**esta región** se actualiza» | sobre el objetivo del intercambio |
   | el isotipo girando (`.espera`) | «**hay algo en curso**» | donde no hay región: botón, arranque, ingreso |

   Y la ruleta genérica que taste-frontend rechaza **sigue rechazada**: la que gira
   acá es la marca, no un anillo de biblioteca. Es la misma diferencia que el
   desplegable y el calendario propios: el sistema dibuja lo suyo.

   ⚠️ **Lo que no se afloja**, porque está medido: los 140 ms de demora valen para
   las dos señales, el velo global sigue prohibido y **el indicador no desplaza
   nada al aparecer** — por eso el isotipo del botón va superpuesto y no al lado. */

[data-esperando]{position:relative;cursor:progress}

/* El atenuado va en los HIJOS, no en el elemento: si fuera en el elemento, el
   hilo —que es un ::after suyo— se atenuaría con ellos y perdería contraste. */
[data-esperando] > *{
  opacity:.52;pointer-events:none;
  transition:opacity var(--dur-corta) var(--curva-salida);
}

/* El hilo se mete 6 px hacia adentro y redondea las puntas. No es cosmética: el
   objetivo puede tener esquinas redondeadas —un panel lleva 14 o 16 px— y una
   barra a ancho completo sobre `top:0` se sale de la curva por ambos lados. Con
   el margen entra en la forma de cualquier objetivo, tenga radio o no lo tenga
   (una fila de tabla no lo tiene, y ahí los 6 px se leen como intención). */
.hilo-de-espera,[data-esperando]::after{
  content:"";position:absolute;inset-inline:6px;top:0;height:2px;z-index:2;
  border-radius:2px;
  background:linear-gradient(90deg,transparent,var(--acento),transparent);
  background-size:38% 100%;background-repeat:no-repeat;
  animation:hilo 1.05s linear infinite;
}
@keyframes hilo{from{background-position:-38% 0}to{background-position:138% 0}}

/* El disparador: quién registró el clic. HTMX pone `.htmx-request` solo, apenas
   sale el pedido — así que la demora de 140 ms la pone acá un `animation-delay`
   con `forwards`, sin un timer en JavaScript. */
.boton.htmx-request,.mini.htmx-request,.pestana.htmx-request{
  animation:espera-del-disparador 0s linear var(--demora-espera) forwards;
}
@keyframes espera-del-disparador{to{opacity:.62;cursor:progress}}

/* El isotipo girando (T084). La curva es `--curva-cielo` —la misma
   `cubic-bezier(.62,.01,.32,1)` del entregable—: no es un giro de motor, respira
   una vez por vuelta. */
@keyframes espera-gira{to{rotate:360deg}}
.espera{animation:espera-gira 1.6s var(--curva-cielo) infinite}

/* 🔴 **El botón que envía no puede cambiar de ancho.** Si el isotipo entrara en
   la fila, la barra de acciones entera saltaría justo en el cuadro en que el
   operador acaba de apretar — el peor momento posible para mover algo.

   La etiqueta pasa a `color:transparent` en vez de ocultarse: el texto sigue
   midiendo lo mismo, así que el botón conserva su caja al píxel. El isotipo se
   apoya encima, centrado sobre las dos diagonales.

   Y **apaga el atenuado del disparador**: dos señales sobre el mismo botón es
   ruido, no énfasis. El .62 se queda para `.mini` y `.pestana`, que no reciben
   isotipo porque no tienen dónde ponerlo. */
.boton[data-espera]{color:transparent;animation:none;opacity:1;cursor:progress}
.boton[data-espera] > .espera{
  position:absolute;left:50%;top:50%;translate:-50% -50%;
  /* Se pinta con `color` y no con `fill`: el símbolo trae su propio
     `fill="currentColor"` dentro del shadow tree del `<use>`. */
  color:var(--acento-tinta);
}
/* 🔑 **El botón lleno es el único caso que no puede llevar el color de la marca.**
   Ya es del color de acento, así que el acento encima sería invisible. El valor
   por defecto —el acento— lo pone `componentes.css`, y acá se pisan los tres
   casos de botón: el lleno con su tinta, el secundario de vuelta al acento
   (porque la regla de arriba también lo alcanza) y el de peligro con el suyo,
   que es su propio acento. El rótulo que acompaña al isotipo sí se queda en el
   color del texto: es una etiqueta, no la marca. */
.boton--secundario[data-espera] > .espera{color:var(--acento)}
.boton--peligro[data-espera] > .espera{color:var(--estado-intimada)}

/* La barra indeterminada del barrido: no dice cuánto falta —nadie lo sabe— sino
   que algo sigue pasando. */
@keyframes espera-avanza{
  0%{left:-38%;width:38%}50%{width:58%}100%{left:100%;width:32%}
}
.barrido__avance{animation:espera-avanza 1.6s var(--curva-salida) infinite}

/* El brillo que recorre el esqueleto. Es lo que lo distingue de una lista
   apagada de verdad: se ve que está esperando, no que está vacía. El desfase
   entre filas es lo que lo vuelve un barrido y no tres parpadeos sincronizados. */
@keyframes espera-brilla{from{transform:translateX(-100%)}to{transform:translateX(260%)}}
.esqueleto__fila::after{
  content:"";position:absolute;inset:0;width:40%;
  background:linear-gradient(90deg,transparent,var(--superficie-alta),transparent);
  animation:espera-brilla 1.8s var(--curva-salida) infinite;
}
/* 🔴 **El desfase se cuenta con hermanos adyacentes, no con `nth-child`.**
   Medido en el banco: con `nth-child(2)` y `(3)` los retardos caían en la 1.ª y
   la 2.ª fila y **la tercera arrancaba en 0**, porque el rótulo del esqueleto es
   el primer hijo y corre la cuenta. O sea que la fila de abajo lideraba el
   barrido: tres parpadeos desordenados en vez de una luz que recorre.

   CSS no sabe decir «el enésimo de esta clase», pero `+` cuenta sólo filas,
   haya o no un rótulo arriba. */
.esqueleto__fila + .esqueleto__fila::after{animation-delay:.12s}
.esqueleto__fila + .esqueleto__fila + .esqueleto__fila::after{animation-delay:.24s}


/* 12 · El hilo de la casilla — el borde se desenrolla y se vuelve el tilde.
   ────────────────────────────────────────────────────────────────────────────
   🔀 **Reemplaza al sello de la casilla** (20/09/2026). Antes era un rebote de escala con un aro
   que se abría en el acento: un gesto **celebratorio**, y el propio entregable lo dice al revés
   —*«al desmarcar todo va al revés y más rápido: deshacer nunca debe celebrarse»*—. En una lista
   de doscientas actas donde se eligen quince, celebrar cada tilde es ruido quince veces.

   El hilo dice lo mismo sin festejar: **el aro se desenrolla en su lugar y ese mismo hilo se
   convierte en el tilde**. Al marcar no queda casillero, sólo el trazo.

   🔴 **Los dos sentidos siguen siendo distintos, y sigue sin ser decoración.** Tildar es más
   lento y el tilde entra **después** de que el aro terminó de irse —se lee como una sola línea
   que se recoloca—; destildar va a dos tercios de la duración y sin espera, porque deshacer no
   se anuncia. Las proporciones son las del entregable (`1,15 · 0,8 · 0,5 · 0,66`) sobre
   `--dur-media`, y la curva es `--curva-cielo`, que **es** la del entregable:
   `cubic-bezier(.62,.01,.32,1)`.

   🔒 **Tres estados, no dos.** El tercero es `indeterminate` —«algunas de esta página»—, que lo
   dibuja la raya y lo decide el control nativo. Era el motivo por el que esta casilla no se
   dibujaba; ahora se dibuja **con** él.

   ⚠️ **Y sigue valiendo que sólo el gesto de la persona se anima.** La casilla del encabezado
   marca quince filas de una, y quince hilos desenrollándose a la vez es ruido, no información.
   Antes eso se resolvía al revés —`portal.js` ponía un atributo para **encender** la
   animación—, porque un `@keyframes` colgado de `:checked` arranca también al cargar la página.
   Un recorrido de transición no tiene ese problema —una transición no corre en el primer
   pintado—, así que ahora el JS **apaga**: marca el formulario mientras cambia las quince y lo
   suelta dos cuadros después. Menos estado y ningún atributo que pueda quedar pegado. */
/* ── Los tiempos, y por qué van en variables ──────────────────────────────── */
/* 🔑 **Los tiempos viajan en variables, y no es un gusto: es lo único que hace que el apagado
   pueda ganar.** Escrito de la forma obvia —cada estado con su `transition` y un
   `[data-sin-recorrido] .casilla__aro{transition:none}` al final— el apagado pesa (0,2,0) y la
   regla de estado `.casilla:has(input:checked) .casilla__aro` pesa (0,3,1), así que **pierde**:
   medido, las quince casillas del encabezado se animaban igual.

   Con los tiempos en variables la pelea se muda a `.casilla`, donde el apagado puede ser
   **estrictamente más pesado** —(0,3,1) contra (0,2,1)— en vez de empatar y depender del orden
   de las reglas. La `transition` se declara una sola vez por trazo y los estados sólo cambian
   **valores**. */
.casilla{
  --hilo-aro:calc(var(--dur-media) * .66);
  --hilo-tilde:calc(var(--dur-media) * .33);
  --hilo-espera:0s;
}
.casilla:has(input:checked){
  --hilo-aro:calc(var(--dur-media) * 1.15);
  --hilo-tilde:calc(var(--dur-media) * .8);
  /* La espera es lo que hace que se lea como **el mismo hilo**: el tilde no empieza hasta que el
     aro recorrió la mitad. Sin ella son dos trazos distintos pasando a la vez. */
  --hilo-espera:calc(var(--dur-media) * .5);
}
/* El apagado para el cambio que no hizo una persona. Lo pone `portal.js` alrededor de un cambio
   en lote y lo saca dos cuadros después: el recálculo del estado nuevo ocurre sin recorrido, así
   que las quince saltan a su marca y ninguna se anima. */
[data-sin-recorrido] .casilla:has(input){--hilo-aro:0s;--hilo-tilde:0s;--hilo-espera:0s}

.casilla__aro{
  /* 🔴 **`--texto-2` y no `--borde`, y está medido.** `--borde` es el borde de un *panel* —una
     insinuación de 12 % de blanco de noche, y de día blanco sobre casi blanco—; el aro de una
     casilla sin marcar es **el límite de un control**, y eso pide 3:1 contra lo que tiene al
     lado (WCAG 1.4.11). Con `--borde` medía **1,83:1 de noche y 1,08:1 de día**: en el tema
     claro la casilla vacía era un cuadrado blanco sin contorno. Se vio mirando la pantalla. */
  stroke:var(--texto-2);
  fill:var(--superficie);
  stroke-dasharray:72.9;
  stroke-dashoffset:0;
  transition:
    stroke-dashoffset var(--hilo-aro) var(--curva-cielo),
    stroke var(--hilo-aro) var(--curva-cielo),
    fill calc(var(--hilo-aro) / 2) var(--curva-cielo);
}
.casilla:has(input:checked) .casilla__aro{
  stroke:var(--acento);
  fill:transparent;
  stroke-dashoffset:72.9;
}

.casilla__tilde{
  stroke:var(--acento);
  stroke-dasharray:20;
  stroke-dashoffset:20;
  transition:stroke-dashoffset var(--hilo-tilde) var(--curva-cielo) var(--hilo-espera);
}
.casilla:has(input:checked) .casilla__tilde{stroke-dashoffset:0}

/* La raya del estado intermedio no se desenrolla: **aparece**. El aro se queda dibujado, porque
   «algunas» todavía es un casillero con algo adentro y no un tilde. */
.casilla__raya{
  stroke:var(--acento);
  stroke-dasharray:12;
  stroke-dashoffset:12;
  transition:stroke-dashoffset calc(var(--hilo-tilde) * 1.2) var(--curva-cielo);
}
.casilla:has(input:indeterminate) .casilla__raya{stroke-dashoffset:0}
/* Con `indeterminate` puesto el control puede informar `checked` a la vez: el tilde se apaga
   explícitamente para que nunca convivan los dos trazos. */
.casilla:has(input:indeterminate) .casilla__tilde{stroke-dashoffset:20;transition:none}

/* ── Presión al tocar (T117, T117c) ─────────────────────────────────────────
   `-webkit-tap-highlight-color:transparent` va global, una vez, en `base.css`
   —reemplaza al velo celeste que el navegador pinta por su cuenta—. En su lugar,
   **todo lo pulsable se hunde**. No es un gesto numerado —no afirma un cambio de
   estado, afirma que el sistema registró el toque— así que no entra a
   `movimiento.yaml`.

   🔀 **Hasta el 27/09/2026 el botón, la pestaña y la barra lateral no se hundían:**
   su `:active` volvía a `translateY(0)`, que es el reposo, y la spec decía que «el
   reposo ES la presión». Sin un puntero fino —la tableta, el teléfono— no hay
   `:hover` que deshacer, así que presionar no cambiaba nada. El dueño de producto
   vio la galería y pidió que el botón «se hunda un poco» al presionarlo.

   | pieza | `:active` | dónde |
   |---|---|---|
   | `.boton` (todas), `.pestana`, `.mini`, `.conmutador > a` | `scale(--escala-presion-chip)` | `componentes.css`, `portal.css` |
   | `.nav a` | `scale(--escala-presion)` | `componentes.css` |
   | `.accion` | `scale(.96)`, sin cambios: ya se hundía | `componentes.css` |
   | `.barra--elegible`, tarjeta de teléfono | `scale(--escala-presion)` + tinte | acá |
   | fila enlazada en escritorio | sólo el tinte | acá |

   Cada `:active` se declara **junto a su `:hover` y después**, con el mismo peso, y
   así gana cuando están los dos puestos: el botón que subió 2 px baja y se hunde en
   un solo recorrido. Sólo `transform` y `box-shadow`: nada que mueva el layout. Un
   solo par duración/curva para toda la familia, `--dur-corta` + `--curva-salida`,
   que también es el de la vuelta al soltar. */

/* Fila enlazada en escritorio/tableta — el tinte, no la escala: un
   `transform:scale()` en un `<tr>` de una tabla ancha se lee raro al lado de
   filas quietas que no se tocaron, y el tinte es el mismo lenguaje que ya usa
   `aria-selected`. */
.grilla tbody tr:has(.enlace-fila:active),
.barra--elegible:active{
  background-color:var(--superficie-activa);
  transition:background-color var(--dur-corta) var(--curva-salida);
}
/* La barra elegible sí se hunde: es una fila suelta en una columna angosta, no una fila de tabla
   entre otras quietas. Su transición de vuelta está en `portal.css`. */
.barra--elegible:active{
  transform:scale(var(--escala-presion));
  transition:background-color var(--dur-corta) var(--curva-salida),
             transform var(--dur-corta) var(--curva-salida);
}

/* Tarjeta de teléfono — acá sí, la escala: es el dedo, no el mouse, y no
   compite con ninguna fila vecina porque las tarjetas ya se separan con
   `border-bottom`. La transición va también en el reposo: si no, al soltar la
   tarjeta volvía a su tamaño de golpe, sin recorrido. */
@container app (max-width:640px){
  .grilla tbody tr:has(.enlace-fila){
    transition:background-size .16s ease,
               transform var(--dur-corta) var(--curva-salida),
               background-color var(--dur-corta) var(--curva-salida);
  }
  .grilla tbody tr:has(.enlace-fila:active){
    transform:scale(var(--escala-presion));
    transition:transform var(--dur-corta) var(--curva-salida),
               background-color var(--dur-corta) var(--curva-salida);
  }
}

/* ── Transición entre secciones — gesto 9, en profundidad (T117, T117b) ─────
   🔀 **El menú principal y las pestañas del legajo ya no pasan por acá** (T117b). En T117 las dos
   se apoyaban en View Transitions: el menú en la de documento (`@view-transition`), el legajo en
   la de un intercambio (`transition:true`). Medido cuadro por cuadro en Chromium, a 1440 y a 390
   —la tabla está en `componentes.md § El relevo de sección`—, las dos parpadeaban y las
   dos saltaban:

   - **el menú**: durante ~500 ms había **dos** secciones marcadas a la vez —la foto vieja de la
     barra y la nueva, las dos opacas, y la barra es translúcida—; la vieja se apagaba de golpe al
     final, sin recorrido. Y la tabla nueva entraba vacía: sus filas caían escalonadas (gesto 1)
     durante 780 ms después de que la página ya estaba;
   - **el legajo**: el nombre, el CUIT y la ficha —que no cambian— se fundían y bajaban 6 px con
     la sección, porque `.cabecera__pagina` y `.cuerpo` tienen nombre propio y un intercambio del
     documento los arrastraba; la corredera desaparecía un cuadro y volvía a mitad de camino; y
     había un cuadro con la sección casi vacía (16 % de su tinta).

   Ahora los dos son **un relevo**: la barra no se vuelve a pintar nunca, su marca se desliza con
   una transición común —en todos los navegadores, no sólo en Chromium— y la sección nueva entra
   al lado de la vieja mientras la vieja se funde. Ver `portal.js`, «El relevo de sección».

   Lo que sigue acá es para las **otras** navegaciones de documento completo —abrir un acta,
   volver al legajo—, que siguen siendo navegaciones de verdad (ADR-0008) y siguen usando la
   continuidad del documento. */
.nav{view-transition-name:sof-nav}
.pestanas{view-transition-name:sof-pestanas}
.cabecera__chrome{view-transition-name:sof-cabecera}
.conmutador--vista,.conmutador--legajo{view-transition-name:sof-conmutador}
.conmutador .corredera{view-transition-name:sof-corredera}
/* 🔴 **Dos nombres, no uno — y está medido.** La spec original le daba el MISMO
   nombre (`sof-contenido`) a `.cabecera__pagina` y a `.cuerpo` con una lista de
   selectores separada por comas: eso no agrupa dos elementos bajo un nombre,
   los nombra a los DOS con el nombre repetido — y los dos existen a la vez en
   toda pantalla. Chromium lo reporta en la consola («Unexpected duplicate
   view-transition-name: sof-contenido») y **descarta la transición entera**. */
.contenido>.cabecera>.cabecera__pagina{view-transition-name:sof-cabecera-pagina}
.contenido>.cuerpo{view-transition-name:sof-contenido}

/* La barra, la cabecera y el conmutador NO se disuelven.

   🔴 **Y la foto vieja no se muestra** (T117b). T117 les sacaba la animación a las dos fotos
   —`animation:none` en la vieja y en la nueva— y las dos quedaban **opacas y apiladas** durante
   toda la transición. La barra es translúcida (`--superficie` sobre el cielo), así que por debajo
   de la nueva se leía la vieja: dos enlaces marcados a la vez, medido en los cuadros de +228 a
   +690 ms. La vieja se esconde y la nueva está desde el primer cuadro. */
::view-transition-old(sof-nav),::view-transition-old(sof-pestanas),
::view-transition-old(sof-cabecera),::view-transition-old(sof-conmutador){
  animation:none;opacity:0;
}
::view-transition-new(sof-nav),::view-transition-new(sof-pestanas),
::view-transition-new(sof-cabecera),::view-transition-new(sof-conmutador){
  animation:none;mix-blend-mode:normal;
}

/* La corredera SÍ se deja animar — es el shared element que viaja. Se le pone
   el par de la casa en vez del que trae el navegador por defecto (250ms,
   ease). */
::view-transition-group(sof-corredera){
  animation-duration:var(--dur-media);
  animation-timing-function:var(--curva-salida);
}

/* El contenido: fundido con desplazamiento corto, no el cross-fade genérico
   del navegador. */
::view-transition-old(sof-contenido),::view-transition-old(sof-cabecera-pagina){
  animation:sof-contenido-sale var(--dur-corta) var(--curva-salida) both;
}
::view-transition-new(sof-contenido),::view-transition-new(sof-cabecera-pagina){
  animation:sof-contenido-entra var(--dur-media) var(--curva-salida) both;
}
@keyframes sof-contenido-sale{to{opacity:0;transform:translateY(-6px)}}
@keyframes sof-contenido-entra{from{opacity:0;transform:translateY(6px)}}

/* ── El relevo de sección (T117b, T117c) ──────────────────────────────────────
   Lo dispara `portal.js` («El relevo de sección») sobre lo que cambia al cambiar de sección:
   `#pantalla` desde el menú principal, `#legajo-seccion` desde las pestañas del legajo. Cinco
   estados, y ninguno deja un cuadro vacío:

   | estado | quién lo pone | qué se ve |
   |---|---|---|
   | `data-esperando` | el gesto 11, a los 140 ms | la vieja atenuada y la espera encima |
   | `data-atenuada` | el relevo, en `beforeSwap` si estaba esperando | lo mismo, sin el pedido en vuelo |
   | `.relevo-sale` | el relevo, al llegar | la vieja, fuera del flujo, fundiéndose en `--dur-instante` |
   | `data-entrando` | el relevo, al llegar | la nueva, por encima de la vieja mientras las dos están |
   | `data-cascada` + `data-pieza` | el relevo, al llegar | las piezas de la nueva, entrando en orden de lectura |

   La vieja se va más rápido que lo que la nueva tarda en llegar: a los 120 ms ya no está, y la
   cabecera de la nueva —con `--curva-salida`, que arranca rápido, y sin espera— ya pasó el 70 % de
   su opacidad. Nunca se superponen dos pantallas legibles más de unos pocos cuadros, y nunca hay
   un cuadro sin ninguna.

   ⚠️ **En `#pantalla` no se mueve la fila de arriba de la cabecera** (`.cabecera__chrome`: la
   marca, quién está conectado, el engranaje). Es la misma en todas las secciones, así que entra
   quieta y en la vieja se esconde: fundir dos copias idénticas corridas 8 px se vería como un
   temblor. */
.hoja-de-la-pantalla{display:contents}
#legajo-vista{position:relative}
.legajo-seccion{position:relative}

/* `linear`, y probado lo contrario. Con `--curva-salida` la vieja caía antes de que la nueva
   subiera y el primer cuadro después del intercambio quedaba en 0,59 de opacidad sumada —una baja
   de brillo de un cuadro, que es exactamente lo que se lee como parpadeo—; con `linear` la suma no
   baja de 0,89 y lo que queda son dos o tres cuadros de las dos pantallas a la vez, fundiéndose. */
.relevo-sale{pointer-events:none;animation:sof-relevo-sale var(--dur-instante) linear forwards}
.relevo-sale > .cabecera > .cabecera__chrome{visibility:hidden}
/* La píldora de la espera se va en el cuadro del intercambio, y recién ahí arranca la cascada
   (T117c). Medido con 600 ms de latencia, fundiéndose con la vieja seguía «Cargando» encima de la
   cabecera nueva durante 130 ms. El contenido viejo, atenuado, sí se queda fundiéndose: es lo que
   evita el cuadro vacío mientras entra la primera pieza. En el legajo no: ahí el esqueleto **es**
   el contenido que se ve, y se funde con la vieja. */
.relevo-sale > .espera-global--seccion{visibility:hidden}
@keyframes sof-relevo-sale{to{opacity:0}}

/* La nueva por encima de la vieja mientras las dos están: la vieja es `position:absolute` y, sin
   esto, un bloque sin posicionar quedaría pintado debajo de ella. */
.legajo-seccion[data-entrando]{z-index:1}

/* 🔀 **La sección entra en cascada, no en bloque** (T117c, 27/09/2026). En T117b entraba entera
   —opacidad y 8 px sobre toda la sección—, para no repetir el parpadeo de T117: una tabla vacía
   que se llenaba sola durante 780 ms. El dueño de producto vio la galería y pidió lo otro: «que
   vayan apareciendo en cascada la información».

   La cascada es **la misma de la carga inicial** (gesto 1, `portal.css`): el mismo
   `entrar-al-listado` —subida, escala y opacidad, de tokens desde `T164`— en el mismo orden de lectura: la cabecera, los paneles,
   las filas. Lo que cambia es quién pone los tiempos. Al cargar el documento son `nth-child`; en
   un relevo los pone `portal.js` («La cascada del relevo»), pieza por pieza con `--d`, a
   `--cascada-paso` cada una y **con tope** en `--cascada-tope`: pasado el tope las piezas entran
   juntas y la cascada entera dura el tope más `--cascada-dur`, tenga la tabla diez filas o
   cincuenta.

   Y **no hay cuadro vacío**: la vieja se funde en `--dur-instante` mientras la cabecera de la
   nueva —que no espera— ya pasó la mitad de su opacidad.

   `:is(…)[data-cascada] [data-pieza]` pesa (0,3,0) y le gana a los `nth-child` de `portal.css`
   —que pesan hasta (0,2,2) y se cargan después—: una pieza del relevo toma su tiempo de acá, no
   del de la carga inicial. `backwards` y no `both`, por lo que explica `portal.css`: al terminar
   el `transform` vuelve a ser `none` de verdad. */
:is(.contenido,.legajo-seccion)[data-cascada] [data-pieza]{
  animation:entrar-al-listado var(--cascada-dur) var(--curva-cascada) backwards;
  animation-delay:var(--d,0ms);
}

/* La espera del menú: la sección vieja se atenúa —no la fila de arriba de la cabecera, que no es
   de la sección— y la píldora aparece encima. Sin hilo: la píldora ya dice lo mismo, y dos señales
   sobre la misma espera es ruido.

   🔴 **`[data-atenuada]` es la misma espera, pero ya sin pedido en vuelo** (T117b, medido con 600 ms
   de latencia). El gesto 11 le saca `data-esperando` al objetivo en `beforeSwap`, y hasta que el
   relevo lo marcaba como `.relevo-sale` la vieja pasaba **tres cuadros a opacidad 1** —de .4 a 1 y
   después a 0—: un destello de la pantalla vieja justo antes de irse (c028, +741 ms). El relevo le
   pone `data-atenuada` en `beforeSwap`, **antes** que el gesto 11 (en captura), y estas reglas no
   dependen del `id` ni de `.relevo-sale`: el valor no cambia en ningún cuadro, así que no hay nada
   que transicionar. */
#pantalla[data-esperando] > *{opacity:1;pointer-events:auto}
#pantalla[data-esperando]::after{content:none}
#pantalla[data-esperando] > .cabecera > .cabecera__pagina,
#pantalla[data-esperando] > .cuerpo,
.contenido[data-atenuada] > .cabecera > .cabecera__pagina,
.contenido[data-atenuada] > .cuerpo{
  opacity:.4;pointer-events:none;transition:opacity var(--dur-corta) var(--curva-salida);
}
#pantalla[data-esperando] > .espera-global--seccion,
.contenido[data-atenuada] > .espera-global--seccion{
  display:flex;animation:sof-aparece var(--dur-corta) var(--curva-salida) backwards;
}
@keyframes sof-aparece{from{opacity:0}}

/* La espera del legajo: el esqueleto de una lista, superpuesto, y la sección vieja se funde por
   debajo —el mismo mecanismo que Reasignar, más abajo: `opacity`/`visibility` y nunca `display`,
   porque un elemento fuera del árbol de render no anima—. Pausado mientras no se ve: el brillo y
   el isotipo son animaciones infinitas, y corriendo escondidas le piden cuadros al compositor. */
.legajo-seccion__carga{
  position:absolute;inset:0;overflow:hidden;opacity:0;visibility:hidden;pointer-events:none;
  padding-top:var(--esp-3);
  transition:opacity var(--dur-corta) var(--curva-salida),visibility 0s linear var(--dur-corta);
}
[data-carga-del-legajo] .espera,
[data-carga-del-legajo] .esqueleto__fila::after{animation-play-state:paused}
#legajo-seccion[data-esperando]::after{content:none}
#legajo-seccion[data-esperando] > [data-carga-del-legajo],
.legajo-seccion[data-atenuada] > [data-carga-del-legajo]{
  opacity:1;visibility:visible;transition:opacity var(--dur-corta) var(--curva-salida);
}
#legajo-seccion[data-esperando] > [data-carga-del-legajo] .espera,
#legajo-seccion[data-esperando] > [data-carga-del-legajo] .esqueleto__fila::after,
.legajo-seccion[data-atenuada] > [data-carga-del-legajo] .espera,
.legajo-seccion[data-atenuada] > [data-carga-del-legajo] .esqueleto__fila::after{animation-play-state:running}
/* `.legajo-seccion`, no cualquier `.relevo-sale`: escrito así apagaba también el contenido de la
   pantalla vieja del menú, que tiene que quedarse en .4 hasta fundirse. */
#legajo-seccion[data-esperando] > :not([data-carga-del-legajo]),
.legajo-seccion[data-atenuada] > :not([data-carga-del-legajo]){opacity:0}

/* ── El esqueleto en uso — elegir inspector en Reasignar (T117) ─────────────
   `.barra--elegible` pasa a HTMX (`_el_reparto.html`) y mientras la respuesta
   no llegó el panel de actas del inspector elegido muestra `.esqueleto` — la
   misma demora de 140 ms que ya usa el gesto 11 (`--demora-espera`), reusada
   y no reinventada, para que una respuesta rápida no parpadee.

   `hx-indicator="#reparto-actas"` es lo que hace que HTMX ponga `.htmx-request`
   en el contenedor y no sólo en el `<a>` que se tocó: acá lo que cambia de
   apariencia es el panel entero, no el enlace.

   🔴 **`opacity`/`visibility`, nunca `display` — medido, no la primera versión.**
   La primera versión ponía `hidden` (`display:none`) en el esqueleto y lo pasaba
   a `display:block` con una animación demorada. Contra el servidor real, con la
   respuesta demorada a propósito, **el esqueleto nunca aparecía**: un elemento en
   `display:none` está fuera del árbol de render, así que ningún `@keyframes` —ni
   siquiera su `animation-delay`— corre sobre él. El mismo problema tenía la otra
   mitad: esconder el contenido viejo con un `display:none` sin demora lo hacía
   desaparecer en el cuadro cero, antes de los 140 ms, así que una respuesta
   *rápida* sí destellaba — exactamente lo que la demora existe para evitar.

   Acá las dos mitades quedan superpuestas (`position:absolute` el esqueleto,
   `position:relative` el contenedor) y el toggle es `opacity`+`visibility`, que sí
   participan de una animación con demora — el mismo mecanismo que ya usa el
   isotipo superpuesto del botón (gesto 11): el elemento nunca deja de renderizarse,
   sólo deja de verse. */
#reparto-actas{position:relative}
[data-carga-del-reparto]{
  position:absolute;inset:0;opacity:0;visibility:hidden;pointer-events:none;
}
#reparto-actas.htmx-request [data-carga-del-reparto]{
  animation:sof-mostrar-esqueleto 0s linear var(--demora-espera) forwards;
}
@keyframes sof-mostrar-esqueleto{to{opacity:1;visibility:visible;pointer-events:auto}}
#reparto-actas.htmx-request > :not([data-carga-del-reparto]){
  animation:sof-esconder-el-reparto 0s linear var(--demora-espera) forwards;
}
@keyframes sof-esconder-el-reparto{to{opacity:0;visibility:hidden}}

/* ── Preferencia de movimiento reducido ─────────────────────────────────────
   No se apaga la información, se apaga el recorrido. */
@media (prefers-reduced-motion:reduce){
  .entra{opacity:1;transform:none}
  .entra-fundido{opacity:1}
  [data-vivo] .entra,[data-vivo] .entra-fundido{transition:none}
  .estado--intimada i{animation:none}
  .traspasa::after{animation:none;opacity:0}
  .velo,.hoja,.aurora-lienzo{transition:none}
  .mas,.mas__velo,.mas__hoja,.mas__item{transition:none}
  /* El cielo cambia igual —el tema es información—, pero sin recorrido: el disco
     salta a su posición, las manchas se cruzan de golpe y no hay desenfoque.
     Nubes y estrellas ya están quietas para todo el mundo, así que acá no hay
     nada más que apagar. */
  .cielo__disco,.cielo__manchas{transition:none}
  /* La transición entre páginas no se puede «no declarar» —el navegador ya tomó
     las instantáneas—, así que lo que se apaga es su recorrido. Sin `!important`:
     una regla de autor le gana a la hoja del navegador. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*){animation:none}
  /* El relevo de sección (T117b): la marca del menú salta a su lugar, la sección vieja se va en
     el mismo cuadro en que llega la nueva —`portal.js` ni siquiera la deja fundirse— y la nueva
     aparece sin subir. Lo que queda es la espera, quieta: informa. */
  .nav__indicador,.pestanas__indicador{transition:none}
  :is(.contenido,.legajo-seccion)[data-cascada] [data-pieza],
  .relevo-sale{animation:none}
  #pantalla[data-esperando] > .espera-global--seccion{animation:none}
  #pantalla[data-esperando] > .cabecera > .cabecera__pagina,
  #pantalla[data-esperando] > .cuerpo,
  .legajo-seccion__carga,
  #legajo-seccion[data-esperando] > [data-carga-del-legajo]{transition:none}
  /* El botón sigue desplegando su sombra: eso informa. Lo que se apaga es el
     recorrido, no el estado. */
  .boton,.accion,.pestana,.nav a{transition:box-shadow var(--dur-corta) ease}
  .boton:hover,.boton:active,.pestana:hover,.pestana:active,
  .nav a:hover,.nav a:active,
  .nav[data-indicador] a[aria-current="page"]:active{transform:none}
  .accion:hover,.accion:active{transform:translateX(-50%)}
  /* Presión al tocar (T117): el tinte de la fila y de `.barra--elegible` se
     conservan —informan que el toque entró— y se apaga el recorrido. La
     escala de la tarjeta de teléfono se apaga entera: un salto de escala sin
     transición se leería como un parpadeo, no como presión. */
  .grilla tbody tr:has(.enlace-fila:active),
  .barra--elegible:active{transition:none}
  .barra--elegible:active{transform:none}
  @container app (max-width:640px){
    .grilla tbody tr:has(.enlace-fila){transition:background-size .16s ease}
    .grilla tbody tr:has(.enlace-fila:active){transform:none;transition:none}
  }
  /* Gesto 11 · La espera **se conserva**: informa que el sistema está trabajando,
     y quitarla dejaría al usuario sin saber si su clic entró. Lo que se apaga es
     el recorrido del hilo, que pasa a ser una barra quieta del ancho completo. */
  [data-esperando]::after{animation:none;background-size:100% 100%;opacity:.5}
  [data-esperando] > *{transition:none}
  /* 🔒 Y el isotipo **no se va: se queda quieto**. Sacarlo dejaría al operador
     sin saber si su clic entró, que es el problema que T084 vino a resolver; lo
     que sobra con `reduce` es la vuelta, no la señal. Por eso `animation:none`
     y nunca `display:none` — la misma decisión que el hilo de acá arriba. */
  .espera{animation:none}
  /* La barra deja de correr y ocupa el riel entero: sigue diciendo «esto está en
     curso» sin nada moviéndose. El brillo del esqueleto sí se va del todo — es
     puro recorrido, y la forma de la lista ya informa por sí sola. */
  .barrido__avance{animation:none;left:0;width:100%}
  .esqueleto__fila::after{animation:none;content:none}
  /* Gesto 12 · La casilla **cambia igual** —el trazo está siempre, y es el que dice el estado—
     y se apaga el recorrido: el hilo no se desenrolla, el tilde aparece dibujado. Lo que se
     pierde es el viaje, no la información: el estado nunca depende sólo del color. */
  .casilla__aro,.casilla__tilde,.casilla__raya{transition:none}
  [data-sellado] .sello{animation:sellar-quieto 1.2s linear forwards}
  @keyframes sellar-quieto{
    0%,88%{opacity:.96;transform:translate(-50%,-50%) rotate(-13deg)}
    100%  {opacity:0;  transform:translate(-50%,-50%) rotate(-13deg)}
  }
}
