/* ===========================================================================
   Vínculo · hoja única. Sin preprocesador, sin Tailwind, sin Bootstrap.

   Lo que hace coherentes veinte pantallas escritas en meses distintos son los
   tokens, no la disciplina. Reglas completas en la skill `estilos-vinculo`.

   TODO COLOR SE DECLARA UNA VEZ, con sus dos valores en el mismo renglón:

       --algo: light-dark(<claro>, <oscuro>);

   Antes eran tres bloques —`:root`, la media query de `prefers-color-scheme` y
   `:root[data-tema="oscuro"]`— con la paleta oscura escrita dos veces palabra
   por palabra. La regla «ningún color puede tener su única definición dentro de
   un bloque de tema» había que recordarla en cada edición; ahora no hay dónde
   incumplirla, porque no existe el bloque donde escribir un color a medias.

   El interruptor de tema ya no reescribe la paleta: solo cambia el
   `color-scheme` del documento, y `light-dark()` elige la columna.
   =========================================================================== */

/* Las capas fijan la cascada por ORDEN DE DECLARACIÓN y no por especificidad:
   `pantallas` gana a `componentes` aunque su selector sea más corto. Lo que
   compra es que una pantalla ajuste un componente sin inventar un selector más
   específico ni desarmar una propiedad heredada —el ingreso tenía un
   `filter: none` cuyo único trabajo era deshacer el hover del botón global—.

   El orden lo fija esta línea y no el lugar del archivo donde aparezca cada
   bloque, así que los `@layer` de abajo envuelven regiones en su sitio y no
   reindentan su contenido: el diff quedaría ilegible y la cascada es la misma.

   `utilidades` se declara vacía a propósito: reserva el último lugar para el
   día que haga falta un ayudante puntual, sin tener que reordenar nada. */
@layer base, componentes, pantallas, utilidades;

/* Inter, vendorizada en assets/vendor/inter/ y servida desde ESTE origen.
   La regla de «sin recursos externos» no prohíbe una @font-face: prohíbe que
   apunte a Google. En una red hospitalaria aislada esa fuente no baja, y la
   pantalla queda con la tipografía del sistema sin que nadie entienda por qué
   se ve distinta en cada puesto — que es exactamente lo que pasaba hasta ahora,
   con `Inter` prometida en --tipo y ningún archivo detrás.

   Se llama `InterVariable` y no `Inter` a propósito. Si el puesto tuviera
   instalada la Inter ESTÁTICA, un @font-face con ese mismo nombre competiría
   con ella; la local no tiene eje de peso variable y el 600 saldría
   sintetizado. Con otro nombre no hay ambigüedad, y `Inter` queda igual en el
   stack como segunda opción, para ese mismo puesto.

   Un solo archivo para los tres pesos que usa la interfaz (400, 600, 700):
   es la fuente variable, no tres estáticas.

   `swap` y no `block`: el texto se lee de inmediato con la del sistema y
   cambia cuando la fuente llega. Esperar a que baje para mostrar la agenda no
   es una opción en un mesón de atención.

   Prueba (25-09-2026): --tipo ya no arranca acá — ver RobotoVariable más
   abajo. Este @font-face y assets/vendor/inter/ NO se borran mientras dure la
   prueba: volver a Inter es poner `InterVariable` primero en --tipo. */
@font-face {
   font-family: InterVariable;
   font-style: normal;
   font-weight: 100 900;
   font-display: swap;
   src: url('/assets/vendor/inter/InterVariable-latin.woff2') format('woff2');
}

/* Roboto, vendorizada en assets/vendor/roboto/ con el mismo mecanismo que
   Inter arriba: un solo archivo variable, subconjunto latino, servido desde
   este origen. Ver assets/vendor/roboto/VERSION para el detalle de cobertura
   —el mismo límite que Inter: sin diacríticos eslavos—.

   Se llama `RobotoVariable` por el mismo motivo que `InterVariable`: si el
   puesto tuviera la Roboto ESTÁTICA del sistema (es común en Android/Chrome
   OS y en algunas instalaciones de Windows), un @font-face `Roboto` chocaría
   con ella y el peso variable no se activaría. `Roboto` queda como segunda
   opción en el stack para ese mismo puesto.

   Trae también el eje de ancho (wdth 75%-100%) porque el archivo vendorizado
   lo incluye sin costo relevante; hoy nada en la hoja lo usa, así que el
   ancho de reposo es 100% en todas partes. */
@font-face {
   font-family: RobotoVariable;
   font-style: normal;
   font-weight: 100 900;
   font-stretch: 75% 100%;
   font-display: swap;
   src: url('/assets/vendor/roboto/RobotoVariable-latin.woff2') format('woff2');
}

:root {
   /* `light dark` es lo que habilita light-dark(), y de paso hace que los
       controles nativos —scrollbars, selects, el calendario— sigan al tema. */
   color-scheme: light dark;

   /* ---------------------------------------------------------------------
       Roles de color MD3, esquema TonalSpot desde la semilla #004741.
       Calculados por `tools/paleta.php` (sRGB → CIELAB → LCh, gamut mapping),
       que documenta el cómo y el porqué de cada hue elegido. Las cuatro
       secciones que trajo `assets/css/_md3-tokens.css` —color, forma,
       elevación y movimiento— ya están todas acá abajo, generadas por las
       herramientas de `tools/`; ese archivo de trabajo se borró.

       No hay capa semántica intermedia: un componente usa `--md-primary`,
       `--md-error-container`, etc. directo. Los `--ok`/`--fail`/`--pending`/
       `--info` de antes desaparecen — duplicaban roles que MD3 ya trae
       (success/error/warning/tertiary) y ya no hay dos lugares donde un
       color pueda quedar mal actualizado.

       El bloque de acá abajo, hasta el marcador de cierre, lo escribe
       `tools/paleta.php` — no se edita a mano. Para cambiar la semilla o
       revisar los parámetros de cada familia: `php tools/paleta.php --help`
       no existe, pero el encabezado de `tools/paleta.php` los documenta. */
   /* >>> paleta generada por tools/paleta.php — no editar a mano <<<
       Semilla #523bf6 -> L*=40,53 C*=108,37 h=304,77°
       Para regenerar: php tools/paleta.php '#523bf6' --neutro-oscuro '#1a1522' --escribir
       Roles MD3, esquema TonalSpot. primary/secondary comparten el hue de la
       semilla; tertiary usa +60°. error/success/warning son colores de dominio
       con hue y croma propios (ver más abajo). El NEUTRO tiene dos familias,
       una por tema: la clara sale de la semilla (h=304,77°, croma 6/8) y la
       oscura es azul (h=306,60°, croma 10,1/12); la nota de familias() dice
       por qué. */
   --md-primary: light-dark(#66558f, #d2bcfe);
   --md-on-primary: light-dark(#ffffff, #461db8);
   --md-primary-container: light-dark(#e9ddff, #4d3d75);
   --md-on-primary-container: light-dark(#1d1244, #e9ddff);

   --md-secondary: light-dark(#635a74, #ccc2df);
   --md-on-secondary: light-dark(#ffffff, #342d44);
   --md-secondary-container: light-dark(#e8defb, #4b435b);
   --md-on-secondary-container: light-dark(#1e182d, #e8defb);

   --md-tertiary: light-dark(#844f5c, #f3b6c4);
   --md-on-tertiary: light-dark(#ffffff, #50212e);
   --md-tertiary-container: light-dark(#ffd9e1, #6a3744);
   --md-on-tertiary-container: light-dark(#380a1a, #ffd9e1);

   /* hue 36 en Lab, no el 25 que publica MD3 en HCT —un espacio que no
       comparte escala de matiz con CIELAB—: trasladar el 25 literal daba un
       carmesí virado a magenta. #c00013 (claro, T40) cae en h=36,02°,
       contra h=36,36° del --fail que este proyecto ya usaba y h=35,86°
       del rojo de referencia de MD3 (#ba1a1a): prácticamente el mismo matiz,
       y el que corresponde en este espacio. 🔴 NO devolver esto a hue 25. */
   --md-error: light-dark(#c00013, #ffb4a5);
   --md-on-error: light-dark(#ffffff, #690001);
   --md-error-container: light-dark(#ffdad3, #93000c);
   --md-on-error-container: light-dark(#390c00, #ffdad3);

   /* success/warning: MD3 los llamaría "custom colors" — acá son de primera
       clase porque son los estados de contactabilidad confirmada/pendiente
       del modelo de datos. El tercer estado, informativo, no tiene familia
       propia: ver la nota debajo de warning. */
   --md-success: light-dark(#2b6b37, #94d69a);
   --md-on-success: light-dark(#ffffff, #003912);
   --md-success-container: light-dark(#aff3b5, #0c5221);
   --md-on-success-container: light-dark(#002202, #aff3b5);

   --md-warning: light-dark(#7d5800, #f1be69);
   --md-on-warning: light-dark(#ffffff, #412d00);
   --md-warning-container: light-dark(#ffdeae, #5e4100);
   --md-on-warning-container: light-dark(#261a00, #ffdeae);

   /* Acá habría ido 'info'. Con hue 250/croma 40 (calculado, no emitido), el
       dE76 contra tertiary en T40 da 45,56 — dos azules a L*40
       indistinguibles a simple vista. En Vínculo los estados tienen que
       distinguirse entre sí (§4 de la skill), y un azul "casi tertiary" no
       resuelve eso, lo esconde. 🔴 NO reintroducir una familia `info`: el
       estado informativo usa los roles `tertiary` directo — más fiel a MD3,
       donde tertiary está pensado justo para ese acento de apoyo. El acento
       de INTERFAZ —foco, enlace de navegación activo, fila abierta de la
       tabla— es un caso distinto y va a `--md-primary`: no es un estado de
       dominio. */

   /* SUPERFICIES Y NEUTROS. La columna oscura sale de una familia azul
       (h=306,60°) y no de la semilla, y con menos contraste que antes.
       Contra #1a1522: texto 10,49:1, secundario 6,65:1,
       borde 1,66:1 — donde antes eran 14,95 · 11,34 · 2,06. Los dos
       primeros siguen sobre 4,5:1; el tercero es silueta y no texto.

       🔴 En oscuro -lowest (#1a1522) es MÁS CLARO que -low (#150e1c): el
       lienzo va al fondo y el cromo y las piezas apoyadas —navbar, riel,
       tabla, hoja, modal— quedan por encima. Está invertido respecto de MD3
       a propósito, y en claro NO se invierte. No «arreglar» esto sin leer
       antes la nota de mapaRoles() en el generador. */
   --md-surface: light-dark(#fbf8ff, #17111e);
   --md-on-surface: light-dark(#1d1a22, #cbc3d6);
   --md-surface-container-lowest: light-dark(#ffffff, #1a1522);
   --md-surface-container-low: light-dark(#f6f2fd, #150e1c);
   --md-surface-container: light-dark(#f0ecf7, #221d2b);
   --md-surface-container-high: light-dark(#eae6f1, #2d2736);
   --md-surface-container-highest: light-dark(#e5e1ec, #383241);
   --md-surface-variant: light-dark(#e6e0ef, #393243);
   --md-on-surface-variant: light-dark(#494551, #a39ab0);
   --md-outline: light-dark(#7a7582, #7c7388);
   --md-outline-variant: light-dark(#c9c4d3, #423a4d);
   --md-inverse-surface: light-dark(#322f37, #e7dff2);
   --md-inverse-on-surface: light-dark(#f3effa, #342e3c);
   --md-inverse-primary: light-dark(#d2bcfe, #66558f);

   /* scrim es N0 (negro puro) en las dos columnas: a L*=0 el cono de Lab se
       cierra y no admite croma, así que negro es el resultado del cálculo, no
       un atajo. Sigue con light-dark() de todos modos por el §1 de la skill:
       ningún color se define en un solo lado. */
   --md-scrim: light-dark(#000000, #000000);
   --md-shadow: light-dark(#000000, #000000);

   /* Barra superior — esquema oscuro FIJO, sin light-dark(). Chrome
       persistente que no sigue el interruptor de tema, igual que `.ingreso`
       fija `color-scheme: dark` para sí mismo. Se derivan de la columna OSCURA
       de neutral/neutral-variant (T8/T12/T80/NV65/NV26), no son tonos nuevos.

       Contraste verificado: barra-texto/barra-superficie 10,49:1,
       barra-tenue/barra-superficie 6,65:1 — sobre 4,5:1. */
   --md-barra-superficie: #1a1522;
   --md-barra-hover: #221d2b;
   --md-barra-texto: #cbc3d6;
   --md-barra-tenue: #a39ab0;
   --md-barra-borde: #423a4d;
   /* <<< fin de la paleta generada — tools/paleta.php <<< */

   /* Separador de fila, más tenue que el borde del contenedor
       (--md-outline-variant a secas): con el mismo peso, la línea entre filas
       competía con el borde que ahora delimita el cajón de la tabla. Escrito a
       mano, FUERA del bloque generado de arriba —regenerar la paleta no debe
       poder borrarlo—. No lleva su propio light-dark(): --md-outline-variant
       ya resuelve las dos columnas, así que envolverlo de nuevo repetiría la
       regla del §1 de la skill en vez de servirla; el color-mix() hereda el
       valor correcto del tema vigente porque su origen ya lo es. 60% deja la
       línea perceptible en claro —no es texto, el mínimo que le aplica es
       3:1 de silueta y no 4,5:1— y más suave que el borde del cajón en los
       dos temas. */
   --md-separador: color-mix(in srgb, var(--md-outline-variant) 60%, transparent);

   /* ---------------------------------------------------------------------
       FORMA — shape scale de Material Design 3. Reemplaza a --r1..--r4: esos cuatro
       radios eran una escala propia sin relación con el resto del sistema
       MD3 que ya gobierna color; con --md-shape-* un componente de acogida
       (large/extra-large) y uno de trabajo (extra-small/small) usan el
       mismo vocabulario que --md-primary o --md-surface-container-low, en
       vez de un radio "que quedaba bien" elegido aparte.

       El criterio de asignación es el de los dos registros (skill §3):
       acogida (modal, .hoja, el panel desplegable) lleva forma plena
       —large o extra-large según el peso de la pieza—, trabajo (tablas,
       celdas, elementos dentro de una fila) lleva forma contenida
       —extra-small o small—, y los campos de formulario van siempre en
       small. Cada uso puntual lleva su propio comentario cuando el porqué
       no es obvio por el nombre del token. */
   --md-shape-none: 0px;
   --md-shape-extra-small: 4px;
   --md-shape-small: 8px;
   --md-shape-medium: 12px;
   --md-shape-large: 16px;
   --md-shape-extra-large: 28px;
   --md-shape-full: 9999px;

   --e1: 4px;
   --e2: 8px;
   --e3: 12px;
   --e4: 16px;
   --e5: 24px;
   --e6: 32px;

   --ancho-contenido: 1600px;
   --pad-lateral: clamp(var(--e4), 2.5vw, var(--e5));
   --alto-cab: 60px;
   --ancho-riel: 248px;
   --ancho-riel-compacto: 76px;

   /* ---------------------------------------------------------------------
       MOVIMIENTO — spring tokens de Material Design 3, generados por
       tools/resorte.php. Reemplazan al par único --dur/--curva: ese par
       trataba «algo que se desplaza» y «algo que solo cambia de color» con
       la misma curva, y --curva («arranca rápido y frena largo») era una
       decisión razonable para lo primero que quedaba de prestado en lo
       segundo. Con seis tokens cada cual lleva el suyo — ver el criterio
       completo en el comentario del bloque generado, y en la skill
       `estilos-vinculo` §8.

       El bloque de acá abajo, hasta el marcador de cierre, lo escribe
       `tools/resorte.php` — no se edita a mano. Para regenerarlo:
       php tools/resorte.php --escribir */
   /* >>> movimiento generado por tools/resorte.php — no editar a mano <<<
       Curvas `linear()` de un resorte amortiguado (masa 1, rigidez +
       amortiguación por par), muestreado hasta asentarse dentro del 0,1 %.
       Para regenerar: php tools/resorte.php --escribir

       espacial-* : algo que se DESPLAZA (el panel que se despliega, el
       riel del ingreso, el rótulo del botón que sube). Sobrepasan 1 y
       vuelven — el rebote es lo que lo hace leerse como un objeto con
       inercia. efecto-* : algo que CAMBIA SIN MOVERSE (opacidad, color,
       fondo, borde, el velo del modal). No rebasan 1: un fundido que
       rebota en opacidad parpadea, no ayuda. Un hover de color NUNCA
       lleva un token espacial — es el error clásico al adoptar MD3: darle
       resorte a todo deja la interfaz entera gelatinosa. Detalle completo
       en la skill `estilos-vinculo` §8. */
   --resorte-espacial-rapido: linear(0, 0.049, 0.1628, 0.3037, 0.4476, 0.5803, 0.6946, 0.788, 0.861, 0.9156, 0.9547, 0.9814, 0.9984, 1.0084, 1.0134, 1.0151, 1.0147, 1.0132, 1.0111, 1.0089, 1.0068, 1.005, 1.0035, 1.0024, 1.0015, 1.0008, 1.0004, 1.0001, 1);
   --dur-espacial-rapido: 257ms;
   /* rebota, pico 1.0151 */
   --resorte-espacial-normal: linear(0, 0.0442, 0.1456, 0.2706, 0.3985, 0.5179, 0.6228, 0.7113, 0.7836, 0.8411, 0.8857, 0.9197, 0.945, 0.9634, 0.9766, 0.9857, 0.992, 0.9961, 0.9987, 1.0002, 1.001, 1.0014, 1.0015, 1.0015, 1.0013, 1.0011, 1.0009, 1.0007, 1);
   --dur-espacial-normal: 347ms;
   /* rebota, pico 1.0015 */
   --resorte-espacial-lento: linear(0, 0.0234, 0.0816, 0.1602, 0.2487, 0.3399, 0.4289, 0.5125, 0.5889, 0.6573, 0.7173, 0.7692, 0.8135, 0.8508, 0.882, 0.9077, 0.9286, 0.9455, 0.9591, 0.9698, 0.9782, 0.9846, 0.9896, 0.9933, 0.996, 0.998, 0.9994, 1.0003, 1);
   --dur-espacial-lento: 375ms;
   /* rebota, pico 1.0003 */

   --resorte-efecto-rapido: linear(0, 0.0606, 0.1885, 0.3333, 0.4703, 0.5889, 0.6867, 0.7645, 0.8249, 0.871, 0.9057, 0.9315, 0.9505, 0.9644, 0.9745, 0.9818, 0.9871, 0.9908, 0.9935, 0.9954, 0.9968, 0.9977, 0.9984, 0.9989, 0.9992, 0.9995, 0.9996, 0.9997, 1);
   --dur-efecto-rapido: 180ms;
   --resorte-efecto-normal: linear(0, 0.0544, 0.1718, 0.3078, 0.4393, 0.556, 0.6544, 0.7345, 0.7983, 0.8481, 0.8864, 0.9156, 0.9376, 0.9541, 0.9664, 0.9754, 0.9821, 0.987, 0.9906, 0.9932, 0.9951, 0.9965, 0.9975, 0.9982, 0.9987, 0.9991, 0.9993, 0.9995, 1);
   --dur-efecto-normal: 261ms;
   --resorte-efecto-lento: linear(0, 0.0513, 0.1632, 0.2943, 0.4227, 0.5381, 0.6365, 0.7177, 0.783, 0.8347, 0.8749, 0.906, 0.9297, 0.9476, 0.9612, 0.9713, 0.9789, 0.9845, 0.9886, 0.9917, 0.9939, 0.9956, 0.9968, 0.9977, 0.9983, 0.9988, 0.9991, 0.9994, 1);
   --dur-efecto-lento: 357ms;
   /* <<< fin del movimiento generado — tools/resorte.php <<< */

   /* Prueba (25-09-2026): Roboto en vez de Inter como tipografía principal,
       reversible igual que la del azul del armazón. Volver a Inter es poner
       `InterVariable` primero acá — el @font-face y assets/vendor/inter/
       siguen intactos mientras dure la prueba, no se borran.

       RobotoVariable es la que se vendorizó arriba y la que se usa ahora.
       `Roboto` la sigue por si el archivo no llegó a bajar y el puesto la
       tiene instalada (es común en Android/Chrome OS). Detrás queda
       InterVariable/Inter por si algún puesto ya la tuviera cacheada de antes
       de la prueba, y el resto es la caída al sistema. Ninguna sale a la red:
       no hay una sola URL de terceros en esta hoja. */
   --tipo: RobotoVariable, Roboto, InterVariable, Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
   --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

   /* ---------------------------------------------------------------------
       ESCALA TIPOGRÁFICA — fase 2 (densidad y tipografía). Pocos peldaños,
       cada uno con un trabajo fijo. Antes convivían números sueltos y
       cercanos —11, 12, 13, 14, 16, 18px— elegidos pantalla por pantalla, y
       la diferencia entre un dato y su metadato la hacía casi solo el
       color (dos señales que además debían viajar juntas, no una sola).
       Con la escala nombrada el tamaño mismo dice qué es cada texto:

         --fs-micro  rótulo de sección en versalitas: el nombre de grupo de
                     la matriz, la marca «crítico» junto a un código.
         --fs-meta   metadato — lo que CALIFICA al dato y no es el dato: la
                     etiqueta de un campo, la cabecera de una tabla, la
                     ayuda y el error de un formulario.
         --fs-dato   texto secundario de interfaz: subtítulo de panel,
                     conteo de una lista, el cuerpo de un aviso flotante.
         --fs-cuerpo el dato principal — es el tamaño base de <body>, así
                     que una celda de tabla sin regla propia ya cae acá;
                     se nombra igual para poder escribirlo explícito donde
                     haga falta decir «este es el peldaño base» sin dejar
                     un 14px suelto.
         --fs-titulo título de una pieza que presenta UN registro: panel,
                     modal, el primer renglón de un estado vacío.
         --fs-vista  el título de la VISTA, en el navbar. Hay uno solo por
                     pantalla y es el texto más grande de la interfaz: el
                     peldaño existe para que sea el tamaño, y no solo el
                     peso, lo que lo separa del módulo que lo antecede en
                     las migas. Lo agregó la fase 4 (riel y navbar), que es
                     la que se lo encontró; la fase 2 dejó el hueco escrito.

       El riel y el navbar entraron a la escala en esa misma fase 4: la marca
       «Vínculo» en --fs-titulo, los destinos en --fs-cuerpo, el segundo nivel
       y el pie en --fs-dato, el rótulo de módulo en --fs-micro. No quedan
       números sueltos en el armazón.

       Dos interlíneas y no una por componente: `--lh-ajustada` para texto
       corto que no es prosa —una celda de tabla, un código con su
       descripción debajo— y `--lh-texto` para lo que se lee de corrido —el
       mensaje de un modal, la nota de un aviso—. `--lh-texto` es el 1.5 que
       ya traía <body>: queda nombrado para no repetir el número suelto en
       cada regla que lo necesite explícito, no para cambiarlo. */
   --fs-micro: 11px;
   --fs-meta: 12px;
   --fs-dato: 13px;
   --fs-cuerpo: 14px;
   --fs-titulo: 16px;
   --fs-vista: 18px;

   --lh-ajustada: 1.3;
   --lh-texto: 1.5;

   /* ---------------------------------------------------------------------
       ELEVACIÓN — seis niveles MD3.
       Reemplaza a --sombra-hoja/--sombra-flota: esos dos eran EL PAR que el
       proyecto necesitaba hasta ahora (una hoja apoyada, un modal que
       flota), y seguían pensados como un par y no como una escala. Con los
       seis niveles, «apoyada» y «flota» pasan a ser dos puntos de la misma
       escala —nivel 1 y nivel 3— en vez de dos sombras inventadas aparte, y
       queda espacio para lo que necesite un nivel intermedio o mayor sin
       inventar un tercer token suelto.

       Offset/blur/alpha son literales en rgba() e IGUALES en los dos temas
       —no hay una segunda columna que light-dark() tenga que elegir—, pero
       cómo se LEE la elevación sí distingue por tema: en claro la sombra
       alcanza sola (una sombra oscura sobre superficie clara ya contrasta,
       y el paso entre surface-container-* aporta el resto); en oscuro una
       sombra negra sobre una superficie ya oscura apenas se nota, así que
       MD3 suma una segunda señal, el TINTE de --md-primary mezclado sobre
       la superficie, que crece con el nivel. Ese tinte se aplica en cada
       componente que usa un nivel —ver `.hoja` y `.modal` más abajo—, no
       acá: --md-elevation-tint-N es el porcentaje, no el color final,
       porque el color base a teñir cambia según qué superficie use cada
       pieza (surface-container-low en las dos que hay hoy, pero no tiene
       por qué ser siempre esa). */
   --md-elevation-0: none;
   --md-elevation-1:
      0px 1px 2px 0px rgba(0, 0, 0, .30),
      0px 1px 3px 1px rgba(0, 0, 0, .15);
   --md-elevation-2:
      0px 1px 2px 0px rgba(0, 0, 0, .30),
      0px 2px 6px 2px rgba(0, 0, 0, .15);
   --md-elevation-3:
      0px 1px 3px 0px rgba(0, 0, 0, .30),
      0px 4px 8px 3px rgba(0, 0, 0, .15);
   --md-elevation-4:
      0px 2px 3px 0px rgba(0, 0, 0, .30),
      0px 6px 10px 4px rgba(0, 0, 0, .15);
   --md-elevation-5:
      0px 4px 4px 0px rgba(0, 0, 0, .30),
      0px 8px 12px 6px rgba(0, 0, 0, .15);

   --md-elevation-tint-0: 0%;
   --md-elevation-tint-1: 5%;
   --md-elevation-tint-2: 8%;
   --md-elevation-tint-3: 11%;
   --md-elevation-tint-4: 12%;
   --md-elevation-tint-5: 14%;

   /* --- Vidrio del riel --------------------------------------------------
       El filo y el borde del cristal son BRILLO y no color de marca: un
       blanco a muy baja opacidad que simula la luz pegando en el canto de una
       lámina. Por eso no salen de la paleta generada y llevan literal, igual
       que --ing-papel-filo. En claro el filo es casi opaco —el brillo sobre
       una superficie clara tiene que ganarle al fondo— y en oscuro apenas se
       insinúa: ahí el mismo valor se vería como una línea blanca dibujada. */
   --riel-vidrio-filo: light-dark(rgba(255, 255, 255, .70), rgba(255, 255, 255, .13));
   --riel-vidrio-borde: light-dark(rgba(0, 45, 40, .10), rgba(255, 255, 255, .09));

   /* --- El roce del armazón ----------------------------------------------
       Un solo tono para el `:hover` de TODO el cromo —la palanca, el destino,
       el subenlace, el botón del pie y el ☰ del navbar—, porque desde la
       fase 4 el riel y el navbar son el mismo plano (-lowest) y un roce
       distinto en cada uno se leería como dos superficies.

       Es una CAPA DE ESTADO a la manera de MD3 —8 % del color del texto
       mezclado sobre la superficie— y no un escalón de la escala de
       contenedores, y el motivo es aritmético: en claro el riel es blanco
       puro y la píldora del destino activo, una vez resuelto su 55 % de
       --md-secondary-container, cae en rgb(219, 245, 241); los escalones
       candidatos caen en rgb(230, 247, 244), rgb(225, 241, 239) y
       rgb(219, 236, 233) — el último tiene EXACTAMENTE la misma claridad que
       la píldora—. Pasar el mouse por un destino cualquiera lo dejaba tan
       lleno como el destino donde uno está parado, y el relleno es la única
       señal que el riel tiene para decir dónde está parado.

       La capa de estado resuelve eso cambiando de eje: arrastra el gris del
       texto, así que el roce es NEUTRO y la píldora TEÑIDA, y lo que las
       separa pasa a ser el matiz y no dos puntos de claridad.

       8 % y no el 12 % que usa `.boton:hover`: un riel tiene quince destinos
       a la vez y una barra de acciones tiene tres. A esa cantidad, el roce de
       un botón sería ruido recorriendo la columna. */
   --armazon-roce: color-mix(in oklab, var(--md-surface-container-lowest) 92%, var(--md-on-surface));

   /* --- Prueba visual: el armazón azul (SOLO tema claro) -------------------
       Pedido de Rafael, 25-09-2026: un armazón azul en el riel y el navbar.
       El oscuro no cambia — el lado oscuro de cada `light-dark()` de acá abajo
       es exactamente lo que el riel y el navbar ya usaban (el escalón
       -lowest, --md-on-surface, --md-on-surface-variant, --armazon-roce,
       --md-outline-variant, --md-primary). Todo el color nuevo vive en LOS
       TOKENS de acá abajo: para revertir la prueba o probar otro azul, se
       edita el valor claro de --armazon-fondo y nada más en el resto de la
       hoja.

       No se reutiliza --armazon-roce para el roce de este cromo: ese token
       también lo usan `.panel-lateral__cerrar` y `.organigrama__accion`,
       que NO están sobre este azul, y sigue calibrado contra una superficie
       clara casi blanca — pintado como capa opaca (no es transparente)
       quedaría un rectángulo gris claro sobre el azul en vez del velo blanco
       fino que pide el roce de un cromo de color. Por eso --armazon-realce es
       un token aparte, con su propia opacidad. */
   /* Prueba 2 (25-09-2026): azul profundo #19369c en degradado hacia #3a64e0
       (primero fue hacia #2a4fc4; Rafael lo pidió más marcado). Blanco sobre
       los dos extremos: 10,25:1 y 5,15:1; el tenue al 92 %, 4,62:1 en el
       extremo claro — todo el texto del armazón sigue en AA. 🔴 #3a64e0 es
       el límite: más claro que eso, el tenue cae bajo 4,5:1 (con #3f6ae8,
       4,27:1). La prueba 1 era #5d87ff
       plano (3,29:1, no pasaba).

       El degradado se arma con DOS tokens de color y no con un token de
       imagen porque light-dark() solo acepta colores: en oscuro los dos
       extremos son la misma superficie y el degradado queda plano, sin una
       regla aparte por tema. Para volver a un color plano, igualar
       --armazon-fondo-fin a --armazon-fondo. */
   --armazon-fondo: light-dark(#19369c, var(--md-surface-container-lowest));
   --armazon-fondo-fin: light-dark(#3a64e0, var(--md-surface-container-lowest));
   /* Un solo degradado para riel y navbar, anclado a la ventana
       (`background-attachment: fixed` donde se usa): así los dos planos son
       tramos de la MISMA superficie y la esquina no puede abrir costura, que
       es lo que pasaría con un degradado vertical en el riel y otro
       horizontal en el navbar. */
   --armazon-degradado: linear-gradient(135deg, var(--armazon-fondo) 0%, var(--armazon-fondo-fin) 100%);
   /* Prueba (25-09-2026): el botón principal de toda la aplicación deja el
       violeta de la paleta y toma el azul profundo del armazón, para que haya
       un solo color de marca. Blanco sobre #19369c: 10,25:1; el hover mezcla
       hacia on-surface y solo sube. En oscuro sigue el primario de la paleta.
       Volver: poner var(--md-primary) / var(--md-on-primary) en el lado claro. */
   --boton-principal-fondo: light-dark(#19369c, var(--md-primary));
   --boton-principal-texto: light-dark(#ffffff, var(--md-on-primary));
   /* Prueba (25-09-2026): el botón oscuro, para las acciones que cada vista
       carga en el navbar (`.cab__acciones`: «Nuevo usuario», «Nueva cita»…).
       El primario de la paleta es violeta y sobre este azul se ve ajeno; un
       relleno casi negro de la misma familia se lee como parte del armazón.
       Blanco sobre #0b1437: 18,4:1. El borde blanco al 28 % es lo que lo
       despega del extremo oscuro del degradado, que está a solo 1,9:1 del
       relleno. En oscuro vuelve al primario de siempre. */
   --armazon-boton-fondo: light-dark(#0b1437, var(--md-primary));
   --armazon-boton-texto: light-dark(#ffffff, var(--md-on-primary));
   --armazon-boton-borde: light-dark(rgba(255, 255, 255, .28), transparent);
   /* El hover mezcla HACIA el blanco en claro: la fórmula general mezcla hacia
       on-surface, que sobre un relleno casi negro no se notaría. */
   --armazon-boton-roce: light-dark(#ffffff, var(--md-on-surface));
   --armazon-texto: light-dark(#ffffff, var(--md-on-surface));
   /* El verde de la marca, igual en los dos temas: es el color del logo y no
       cambia con la pantalla. 🔴 Decoración: sobre blanco no llega a AA, así
       que nunca lleva texto ni es la única señal de nada. */
   --marca-verde: #57b198;
   /* El pliegue de la V: el verde oscurecido, donde la cinta pasa bajo la azul. */
   --marca-pliegue: #3d8f78;
   /* .92 y no opaco: la diferencia con --armazon-texto es la única señal de
       jerarquía que le queda a un rótulo secundario del armazón, ahora que el
       color de fondo ya no es la superficie neutra de siempre. Sobre el azul
       profundo queda cerca de 9:1 en el extremo oscuro y sobre 6:1 en el
       claro; no bajarlo mucho más, porque el blanco translúcido se mezcla con
       el fondo y el extremo claro del degradado es el que manda. */
   --armazon-texto-tenue: light-dark(rgba(255, 255, 255, .92), var(--md-on-surface-variant));
   /* El separador (borde derecho del riel, raya del navbar, línea del pie, el
       trazo del árbol del segundo nivel): decorativo, no texto, así que no le
       aplica el 4,5:1 — alcanza con leerse como una línea sobre el azul. */
   --armazon-borde: light-dark(rgba(255, 255, 255, .24), var(--md-outline-variant));
   /* El roce (`:hover`) de este cromo. Ver la nota de arriba sobre por qué no
       es --armazon-roce. */
   --armazon-realce: light-dark(rgba(255, 255, 255, .14), var(--armazon-roce));
   /* El destino activo NO usa --md-secondary-container (el tono de la paleta
       que ya cumplía ese papel): sobre este azul, un lila casi blanco se
       funde con el fondo y deja de leerse como relleno. Pasa a blanco —sigue
       llevando el vidrio (blur + saturate) que ya tenía el riel— con el
       texto en un azul MÁS SATURADO que el de fondo: el mismo #5d87ff sobre
       blanco vuelve a dar 3,29:1, y el enlace activo es texto normal de
       verdad, no una silueta — 4,5:1 le corresponde. #3f6ae8 sobre blanco:
       4,73:1. */
   --armazon-activo-fondo: light-dark(rgba(255, 255, 255, .85), color-mix(in srgb, var(--md-secondary-container) 55%, transparent));
   /* La misma pieza sin `backdrop-filter` (el `@supports not` de más abajo):
       ahí no hay nada detrás que mezclar, así que el blanco va opaco. */
   --armazon-activo-fondo-opaco: light-dark(#ffffff, var(--md-secondary-container));
   --armazon-activo-texto: light-dark(#19369c, var(--md-on-secondary-container));
   /* El foco global (§5 de la skill) usa --md-primary, que sobre este azul da
       apenas 1,96:1 — casi no se ve. Este token lo reemplaza SOLO dentro del
       riel y el navbar (ver el selector puntual más abajo); en oscuro es el
       mismo --md-primary de siempre, sin cambio. La regla global de
       `:focus-visible` no se toca ni se apaga en ningún lado: sigue rigiendo
       para el resto de la aplicación. */
   --armazon-foco: light-dark(#ffffff, var(--md-primary));

   /* --- Panel de indicadores (27-09-2026) ---------------------------------
       Las series de los gráficos. Una por gráfico, nunca más de dos en la
       pantalla, y se validaron con el script de la skill `dataviz`
       (`validate_palette.js`): en claro contra #ffffff y en oscuro contra
       #1a1522 —la superficie -lowest donde se dibujan—, las dos pasan banda
       de claridad, croma, separación para daltonismo y 3:1 contra el fondo.
       El azul de la maqueta (#19369c) NO pasa la banda: es demasiado oscuro
       para una línea de 2 px junto a la tinta del texto, y por eso la serie
       usa el extremo más vivo de la familia del armazón. Ningún texto se
       pinta con estos colores: la línea lleva el color, el rótulo la tinta. */
   --grafico-serie-1: light-dark(#2f55d4, #5f7ff0);
   --grafico-serie-2: light-dark(#b35c00, #d57a22);
   /* La tarjeta ámbar de «Sin respuesta · hay que llamar». El fondo es la
       única superficie teñida del panel: es lo que hay que hacer primero. */
   --kpi-alerta-fondo: light-dark(#fffaf1, color-mix(in srgb, var(--md-warning-container) 30%, var(--md-surface-container-lowest)));
   --kpi-alerta-borde: light-dark(#f1be69, #7d5800);
   --kpi-alerta-texto: light-dark(#5e4100, #ffdeae);
   /* Las píldoras de variación: más suaves que los contenedores MD3 de
       éxito, que en claro saturan demasiado para seis píldoras a la vez. */
   --kpi-mejor-fondo: light-dark(#dcf5de, #0c5221);
   --kpi-mejor-texto: light-dark(#1f5a2b, #aff3b5);
   --kpi-peor-fondo: light-dark(#ffdad3, #5c1a12);
   --kpi-peor-texto: light-dark(#93000c, #ffdad3);
   /* La sombra de las tarjetas tira al azul del armazón en claro; en oscuro
       una sombra de color no se ve, así que es negra y más densa. */
   --kpi-sombra: light-dark(rgba(25, 54, 156, .18), rgba(0, 0, 0, .55));
   /* Los filtros sobre la franja: vidrio blanco sobre el azul en claro, y en
       oscuro —donde la franja es la superficie plana del armazón— los
       controles de siempre. */
   --kpi-filtro-fondo: light-dark(rgba(255, 255, 255, .14), var(--md-surface-container-high));
   --kpi-filtro-borde: light-dark(rgba(255, 255, 255, .40), var(--md-outline-variant));
   --kpi-filtro-activo-fondo: light-dark(#ffffff, var(--md-primary-container));
   --kpi-filtro-activo-texto: light-dark(#19369c, var(--md-on-primary-container));

   /* --- Ingreso ---------------------------------------------------------
       Hasta el 25-09-2026 el ingreso se comprometía con UN aspecto fijo y no
       seguía el tema, a propósito: «es una puerta, no un escritorio». Rafael
       pidió ese día llevar el azul del armazón (--armazon-fondo, más arriba)
       también al ingreso, con el formulario respetando los tres estados de
       tema — eso invierte la parte de la decisión vieja que mantenía el
       PAPEL siempre claro. Lo que se pierde: la garantía de «se ve igual en
       cualquier puesto, sin depender de qué tema tenga elegido ese
       navegador» — ahora si el puesto está en oscuro, el papel también.

       El PANEL DE MARCA (`.ingreso__identidad`, más abajo) sigue sin seguir
       el tema: es fieltro siempre oscuro, ahora en el azul del armazón en vez
       de verde, y con eso alcanza para que se vea igual en los dos temas —no
       hace falta light-dark() ahí. El PAPEL del formulario sí lo sigue desde
       hoy, así que sus tokens llevan light-dark() como el resto de la hoja
       (ver la nota del encabezado del archivo). */

   /* --- El panel de marca: el azul, no el fieltro verde ---
       Mismo azul y mismo ángulo que --armazon-degradado, pero en tokens
       propios y literales (no reutiliza --armazon-fondo): ese token vuelve a
       gris en tema oscuro porque el riel sí sigue el tema, y este panel no
       tiene que hacerlo — «se ve igual en los dos temas, porque ya es
       oscuro» es la instrucción, y --armazon-fondo no lo cumpliría acá. */
   --ing-marca-1: #19369c;
   --ing-marca-fin: #3a64e0;

   /* --- El velo, en DOS capas ---
       Pedido de Rafael, 25-09-2026 (segunda vuelta): con el velo parejo al
       94 % el video prácticamente no se veía —cumplía el contraste, pero ya
       no se leía como una toma—. La alternativa es partir el velo en dos:

       1. `.ingreso__velo` — PAREJO, sobre TODO el panel, mucho más tenue.
          Es el que deja que el video se vea de verdad.
       2. `.ingreso__identidad-contenido` — un fondo PROPIO, solo detrás del
          bloque de texto (nombre, bajada, detalle), con su propio borde
          difuso (`box-shadow` con blur, ver el CSS). Es el que carga TODO el
          trabajo de contraste, y solo ahí.

       🔴 Cómo se midió: mismo método que antes —`ingreso.jpg` es el primer
       cuadro exacto de `ingreso.mp4` (mismo `-ss 3`, ver ORIGEN); con PIL
       (`GaussianBlur` + `numpy`) el máximo de cada canal en cualquier píxel
       del cuadro da RGB(255,255,237), y esa sigue siendo la referencia—.
       Ahora la composición es en DOS pasos, cada uno en espacio sRGB:

         Paso 1 — el velo parejo (68 %, dentro del 65-70 % pedido) sobre el
         extremo MÁS CLARO del degradado (`--ing-marca-fin`, el peor caso)
         compositado sobre RGB(255,255,237):
            → color resultante ≈ rgb(121, 150, 228), L = 0,314.
            Solo, ese resultado da 2,89:1 contra blanco — bastante menos que
            4,5:1, y es justo lo que se espera: este velo NO tiene que cargar
            el contraste solo, para eso está el paso 2.

         Paso 2 — el fondo del bloque de texto: un navy casi negro,
         rgb(6, 10, 26), ENCIMA del resultado del paso 1, a distintas
         opacidades:
            30 % → contraste final 5,13:1 — ya cumple.
            35 % → contraste final 5,69:1.
            40 % → contraste final 6,34:1 (elegido).

       Con el velo parejo solo al 68 % —en vez del 94 % de la vuelta
       anterior— el video se ve como una toma en el 92 % del panel que no
       tiene texto encima. El fondo del bloque de texto es una caja SÓLIDA
       —no un degradado que se apaga hacia el borde—, así que el contraste
       vale igual en el centro que pegado al borde de la caja: lo que se
       difumina (`box-shadow` con blur) queda POR FUERA de esa caja, donde no
       hay ninguna letra.

       El botón de pausa queda FUERA de esta caja —esquina opuesta del
       panel— y sigue con su propio fondo oscuro de siempre
       (`.ingreso__video-boton`): contra este velo más claro da 8,54:1 para
       su ícono blanco, de sobra sobre el 3:1 que le corresponde por ser un
       control y no texto. */
   --ing-marca-velo-1: rgba(25, 54, 156, .68);
   --ing-marca-velo-fin: rgba(58, 100, 224, .68);
   --ing-marca-scrim: rgba(6, 10, 26, .40);

   /* El nombre («Vínculo») en blanco sólido: 6,34:1 contra el fondo del
       bloque de texto en el peor cuadro medido —de sobra, es lo primero que
       se lee—. La bajada y el detalle SÍ pueden volver a un blanco tenue
       —el fondo del bloque ya no es el video a secas, así que hay margen—:
       #e4e9f2 da 5,20:1 contra ese mismo peor caso, visiblemente más apagado
       que el blanco puro y todavía sobre 4,5:1. Los dos son sólidos y no
       traslúcidos: un tono que se mezcla con lo que tiene detrás vuelve a
       depender de qué tan claro está el cuadro de video de ese instante, que
       es justo lo que la caja del paso 2 existe para dejar de importar. */
   --ing-marca-texto: #ffffff;
   --ing-marca-texto-tenue: #e4e9f2;

   /* --- El papel del formulario ---
       Opaco y no translúcido: el cristal con backdrop-filter existía para que
       el degradado se viera a través de la tarjeta, y a través de una hoja de
       papel no se ve nada. Desde el 25-09-2026 sigue el tema (ver la nota de
       arriba): el valor claro es el papel crema de siempre, el oscuro es un
       azul-noche muy oscuro que combina con el acento —no un gris neutro—
       para que el formulario no se sienta desconectado del panel de marca. */
   --ing-papel: light-dark(#f0ede4, #12181f);
   --ing-papel-borde: light-dark(rgba(0, 45, 40, .16), rgba(255, 255, 255, .14));
   --ing-papel-filo: light-dark(rgba(255, 255, 255, .85), rgba(255, 255, 255, .10));
   /* el filo superior */

   --ing-texto: light-dark(#15302c, #eef1f5);
   --ing-tenue: light-dark(#4c5f5a, #b7c0cc);
   /* etiquetas y ayuda: 5,80:1 claro, 9,71:1 oscuro, los dos sobre --ing-papel */
   --ing-hueco: light-dark(#7d8a85, #7c8794);
   /* la sugerencia del campo: no es texto que haga falta leer, no le aplica 4,5:1 */

   --ing-campo: light-dark(#fbf9f4, #1c242e);
   --ing-campo-borde: light-dark(rgba(0, 71, 65, .22), rgba(255, 255, 255, .22));
   --ing-campo-borde-hover: light-dark(rgba(0, 71, 65, .38), rgba(255, 255, 255, .36));
   --ing-campo-activo: light-dark(#ffffff, #222c38);

   /* El relleno automático de Chrome se tapa con un color OPACO —una rgba con
       alfa dejaría ver el papel de abajo—, y tiene que ser el mismo que el
       campo activo o el campo autocompletado se ve de otro color que el resto. */
   --ing-campo-autofill: light-dark(#ffffff, #222c38);

   /* --- El azul, en el formulario ---
       Pedido de Rafael, 25-09-2026: el mismo azul del armazón —#19369c hacia
       #3a64e0— en los acentos del formulario. Dos tokens y no uno, porque
       cumplen roles distintos:

       `--ing-acento` es el RELLENO del botón: siempre #19369c con texto
       blanco, en los dos temas — un azul oscuro funciona igual de bien como
       fondo sólido tenga el papel el tono que tenga, así que no hace falta
       light-dark() acá.

       `--ing-acento-papel` es el azul TAL COMO SE LEE SOBRE EL PAPEL —el
       nombre en celular, los enlaces, el borde del campo enfocado—. Ahí sí
       hace falta light-dark(): #19369c da 8,75:1 sobre el papel claro pero
       apenas 1,74:1 sobre el papel oscuro —los dos son azules oscuros, se
       confunden—. #8fb0ff dando 8,35:1 sobre el papel oscuro es el
       reemplazo para ese lado. */
   --ing-acento: #19369c;
   --ing-acento-alto: #2a4fc4;
   /* hover: 6,97:1 con blanco encima, un tono intermedio hacia --ing-marca-fin */
   --ing-acento-texto: #ffffff;
   /* 10,25:1 sobre --ing-acento */

   --ing-acento-papel: light-dark(#19369c, #8fb0ff);

   /* La línea y el resplandor de foco usan el mismo azul que el resto del
       formulario (antes era un teal propio, `--ing-linea-foco`, que no tenía
       relación con ningún otro acento de la pantalla). */
   --ing-linea-foco: var(--ing-acento-papel);
   --ing-foco: light-dark(rgba(25, 54, 156, .22), rgba(143, 176, 255, .30));

   /* Los avisos van con texto OSCURO sobre un tinte claro en tema claro, y
       AL REVÉS en oscuro —texto claro sobre un tinte oscuro—, porque el
       papel se invirtió: pintar el mismo tono oscuro sobre un papel ya
       oscuro repite el error que esta nota describía antes (salmón sobre
       crema, ilegible) con los dos temas cambiados de lugar. */
   --ing-fail: light-dark(#a3271f, #ffb4ab);
   /* 5,34:1 claro / 7,84:1 oscuro, los dos sobre su propio fondo */
   --ing-fail-fondo: light-dark(rgba(163, 39, 31, .10), rgba(255, 180, 171, .14));
   --ing-fail-borde: light-dark(rgba(163, 39, 31, .32), rgba(255, 180, 171, .35));

   /* El verde de la confirmación se corre hacia el verde puro y se aleja del
       azul del botón: en una pantalla que ya lleva azul de sobra, un «listo»
       del mismo tono que el acento se lee como decoración y no como respuesta. */
   --ing-ok: light-dark(#0f6b3f, #9ee6b8);
   /* 4,73:1 claro / 8,84:1 oscuro, los dos sobre su propio fondo */
   --ing-ok-fondo: light-dark(rgba(15, 107, 63, .12), rgba(158, 230, 184, .14));
   --ing-ok-borde: light-dark(rgba(15, 107, 63, .30), rgba(158, 230, 184, .35));
}

/* Los tres estados del tema (§2). El de por omisión —«sistema»— es no llevar
   el atributo: ahí manda el `color-scheme: light dark` de arriba, que sigue al
   sistema operativo. `ts/lib/tema.ts` no cambia: pone y quita el mismo
   atributo, solo que ahora acciona el esquema en vez de repintar la paleta. */
:root[data-tema="claro"] {
   color-scheme: light;
}

:root[data-tema="oscuro"] {
   color-scheme: dark;
}

@layer base {
   /* --- Base ---------------------------------------------------------------- */

   *,
   *::before,
   *::after {
      box-sizing: border-box;
   }

   html,
   body {
      height: 100%;
   }

   body {
      margin: 0;
      /* El lienzo baja un escalón respecto de --md-surface: antes el fondo
       (--md-surface) y el contenido (--md-surface-container-low, en .hoja y
       la tabla) quedaban a UN escalón de distancia en la escala de contenedores
       y apenas se distinguían. Ahora el lienzo ocupa ese escalón y el
       contenido sube a --md-surface-container-lowest —.cab, .hoja, .modal,
       .menu, .tabla-caja, todas abajo—, así que el salto pasa a ser de DOS
       escalones en los dos temas: en claro el contenido se acerca al blanco
       puro y el lienzo queda apenas más gris; en oscuro el lienzo es el tono
       más oscuro de la escala y el contenido queda POR ENCIMA de él.

       Esa segunda mitad cambió el 20 de septiembre de 2026. Antes, con el
       orden de MD3, -lowest era el extremo más oscuro y el contenido quedaba
       MÁS oscuro que el lienzo: la tabla se leía recesada y no elevada. La
       columna oscura invirtió -lowest y -low a propósito —el porqué está en
       el bloque generado de la paleta— y ahora las dos columnas dicen lo
       mismo: más pasos de contenedor = más elevado, que es la dirección que
       .hoja ya usaba con el tinte. */
      background: var(--md-surface-container-low);
      color: var(--md-on-surface);
      font-family: var(--tipo);
      /* --fs-cuerpo/--lh-texto y no 14px/1.5 sueltos: es el mismo valor de
       siempre, ahora nombrado en la escala de :root (ver el comentario ahí),
       así que el peldaño base del sistema tipográfico se lee en un solo
       lugar y no como un literal que la escala «casi» repite. */
      font-size: var(--fs-cuerpo);
      line-height: var(--lh-texto);
      -webkit-text-size-adjust: 100%;
   }

   /* Cifras tabulares en todo lo que se lee de corrido y se compara entre filas.
   Un RUN o una hora mal leídos mandan a un paciente al box equivocado. */
   table,
   input,
   kbd,
   .mono,
   .cifra {
      font-variant-numeric: tabular-nums;
   }

   .mono {
      font-family: var(--mono);
   }

   /* El foco visible va global y NO se quita. En un mesón de atención, con el
   teclado y sin soltar el mouse, es la única forma de saber dónde se está.
   Nunca `outline: none` sin poner otra señal igual de visible en su lugar. */
   :focus-visible {
      outline: 2px solid var(--md-primary);
      outline-offset: 2px;
      /* Respaldo para lo que no define radio propio (un <a> suelto, por
       ejemplo): un componente con su propio --md-shape-* en una capa
       posterior lo pisa igual —misma especificidad, y `componentes` va
       después de `base` en el orden de capas de la línea 33. */
      border-radius: var(--md-shape-extra-small);
   }

   /* --- `hidden` gana siempre ------------------------------------------------ */

   /* 🔴 El `display: none` que el navegador le da a `[hidden]` es de la hoja de
   usuario-agente, así que CUALQUIER `display` de un componente le gana: un
   `.riel__subnivel { display: flex }` deja la lista dibujada aunque el
   JavaScript le haya puesto `hidden`. El elemento queda marcado como oculto
   para el lector de pantalla y visible para el ojo, que es la peor de las dos
   mitades.

   Es el único lugar de la hoja con `!important` y tiene motivo: es la forma de
   que un atributo del HTML le gane a la declaración de cualquier componente,
   presente o futuro. Va en `base` por lo mismo que el bloque de abajo — para
   las declaraciones `!important` el orden de las capas se invierte, así que
   desde acá le gana también a lo que venga en `componentes`. */
   [hidden] {
      display: none !important;
   }

   /* --- Movimiento reducido -------------------------------------------------- */

   /* Va en la primera capa a propósito: para las declaraciones `!important` el
   orden de las capas se INVIERTE, así que desde `base` este bloque le gana a
   cualquier !important que se agregue después en otra capa. */
   @media (prefers-reduced-motion: reduce) {

      /* `::backdrop` va en la lista porque el velo del modal se anima ahí y no
       lo alcanzaría ni `*` ni `*::before`. */
      *,
      *::before,
      *::after,
      *::backdrop {
         animation-duration: .01ms !important;
         /* el usuario pidió no ver movimiento */
         animation-iteration-count: 1 !important;
         transition-duration: .01ms !important;
      }
   }
}

@layer componentes {
   /* --- Estados de pantalla ------------------------------------------------- */

   .cargando,
   .vacio {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: var(--e2);
      min-height: 60vh;
      padding: var(--e5) var(--pad-lateral);
      color: var(--md-on-surface-variant);
      text-align: center;
   }

   .vacio p {
      margin: 0;
   }

   .vacio p:first-child {
      color: var(--md-on-surface);
      font-size: var(--fs-titulo);
      font-weight: 600;
   }

   /* --- La hoja ------------------------------------------------------------- */

   /* Una pantalla que presenta UN registro —la ficha de una persona, el detalle
   de una cita, el expediente de una anonimización— lleva `.hoja`. Es
   transversal: son la misma clase de pantalla y tienen que leerse igual.
   Un listado NO la lleva: una tabla no es una hoja, es un cajón. */
   .hoja {
      /* Tinte de elevación nivel 1: en claro el color es el llano
       surface-container-LOWEST —light-dark() ni evalúa el color-mix()—; en
       oscuro se mezcla el --md-elevation-tint-1 (5%) de --md-primary encima,
       reproduciendo la misma señal que ya aporta la sombra pero por el lado
       del color. `in oklab` y no sRGB por la misma razón que ya vale para el
       hover del botón (línea ~600): interpolar en sRGB ensucia el paso — ver
       ese comentario para el detalle.

       Antes era -container-low, el mismo tono que el lienzo (body): con el
       lienzo ahora en -container-low (ver `body`), quedarse acá lo hubiera
       fundido con el fondo. -lowest es el escalón del cromo y de todo lo
       apoyado —el mismo que usan .modal, .menu, .cab, .tabla-caja y el
       riel—: suben juntos, para no quedar cada pieza a una distancia
       distinta del lienzo. En claro es el blanco puro, el extremo de la
       escala; en oscuro está un escalón por encima del lienzo y no en el
       extremo, desde que la columna oscura invirtió los dos tonos. */
      background: light-dark(var(--md-surface-container-lowest),
            color-mix(in oklab,
               var(--md-primary) var(--md-elevation-tint-1),
               var(--md-surface-container-lowest)));
      border: 1px solid var(--md-outline-variant);
      /* Acogida: la ficha de una persona, el detalle de una cita. */
      border-radius: var(--md-shape-large);
      /* Apoyada en el escritorio, no flotando: nivel 1, el más bajo que trae
       sombra propia. Ver la nota junto a .modal, que usa el nivel 3 — si
       se parecieran, lo que flota dejaría de distinguirse de lo posado. */
      box-shadow: var(--md-elevation-1);
      padding: var(--e5);
   }

   /* --- El panel que se pliega ---------------------------------------------- */

   /* El alto lo escribe el JS —ver ts/ui/desplegable.ts—: el CSS no sabe animar
   hacia `auto`, y el truco de `grid-template-rows: 0fr/1fr` no cubre el caso
   que acá es la mitad del trabajo, que es REEMPLAZAR el contenido —pulsar
   «Parámetros» con el formulario de edición ya abierto—. Ahí el valor sigue
   siendo `1fr`, no hay transición, y el alto salta.

   `visible clip` recorta en vertical, que es lo que hace falta para que el
   contenido no asome mientras se pliega, y deja pasar el horizontal, donde la
   sombra de la hoja sobresale unos píxeles a cada lado: con `clip` en los dos
   ejes esa sombra quedaría cortada a plomo.

   Espacial: el panel se DESPLAZA —crece de alto y empuja lo que sigue—, no
   solo cambia de aspecto. Rápido porque es la pieza más chica de las tres que
   usan el resorte espacial (panel, riel del ingreso, rótulo del botón). */
   .desplegable {
      overflow: visible clip;
      transition: height var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   /* La separación con lo que sigue va ADENTRO del alto que se anima. Como margen
   del contenedor quedaría en pie con el panel cerrado, dejando un hueco encima
   de la lista sin nada que lo explique. De paso le da a la sombra de la hoja el
   espacio que necesita antes del recorte. */
   .desplegable__caja {
      padding-bottom: var(--e5);
   }

   /* El contenido no aparece de golpe al terminar de abrirse: entra subiendo un
   poco mientras el alto crece. Seis píxeles — lo justo para que se lea como
   que el panel trajo algo, y no como un salto. Lleva `translateY`, así que es
   espacial igual que el alto que la contiene —misma duración, para que las
   dos lecturas de «el panel llegó» terminen juntas—. */
   @keyframes desplegable-entra {
      from {
         opacity: 0;
         transform: translateY(-6px);
      }

      to {
         opacity: 1;
         transform: none;
      }
   }

   .desplegable__entrada {
      animation: desplegable-entra var(--dur-espacial-rapido) var(--resorte-espacial-rapido) both;
   }

   /* --- El panel lateral ------------------------------------------------------
   Ver `ts/ui/panelLateral.ts`: es para un lienzo (pan/zoom), no para una lista,
   así que no reemplaza a `.desplegable`. Mismo resorte que el riel angosto
   (`--dur-espacial-rapido`): las dos son piezas grandes que entran desde el
   borde de la pantalla. SIN velo: el lienzo de atrás sigue completo y usable,
   a propósito — por eso esto no es un `<dialog>`. */
   .panel-lateral {
      position: fixed;
      inset-block: 0;
      inset-inline-end: 0;
      z-index: 55;
      display: flex;
      flex-direction: column;
      width: min(440px, 100vw);
      background: var(--md-surface-container-lowest);
      border-inline-start: 1px solid var(--md-outline-variant);
      box-shadow: var(--md-elevation-3);
      transform: translateX(100%);
      transition: transform var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   @media (max-width: 899px) {
      .panel-lateral {
         width: 100vw;
      }
   }

   .panel-lateral--abierto {
      transform: none;
   }

   .panel-lateral__cerrar {
      position: relative;
      align-self: flex-end;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 36px;
      height: 36px;
      margin: var(--e3) var(--e3) 0 0;
      flex: 0 0 auto;
      border: 0;
      border-radius: var(--md-shape-full);
      background: transparent;
      color: var(--md-on-surface-variant);
      cursor: pointer;
   }

   .panel-lateral__cerrar:hover {
      background: var(--armazon-roce);
   }

   /* La equis, dibujada a mano igual que en `ts/ui/iconos.ts`, sin depender de
   ese módulo: dos trazos en `currentColor`, sin fuente de íconos. */
   .panel-lateral__cerrar::before,
   .panel-lateral__cerrar::after {
      content: '';
      position: absolute;
      width: 16px;
      height: 2px;
      background: currentColor;
      border-radius: var(--md-shape-full);
   }

   .panel-lateral__cerrar::before {
      transform: rotate(45deg);
   }

   .panel-lateral__cerrar::after {
      transform: rotate(-45deg);
   }

   .panel-lateral__caja {
      flex: 1 1 auto;
      overflow-y: auto;
      padding: 0 var(--e5) var(--e5);
   }

   /* --- El modal ------------------------------------------------------------ */

   /* `<dialog>` nativo, que es lo que trae resuelto el trabajo difícil: atrapa el
   foco, deja inerte lo de atrás, responde a Escape y devuelve el foco donde
   estaba al cerrar. Nada de eso se reescribe a mano.

   El navegador le pone borde y relleno de fábrica; se reemplazan enteros.
   `margin: auto` es lo que lo centra en la capa superior. */
   .modal {
      margin: auto;
      width: min(440px, calc(100vw - var(--e5) * 2));
      max-height: calc(100dvh - var(--e5) * 2);
      padding: var(--e5);
      border: 1px solid var(--md-outline-variant);
      /* Acogida, la pieza de más peso del registro: el dialog de MD3 usa
       extra-large por norma, y acá además es lo que distingue al modal de
       .hoja —large— siendo las dos piezas "sin tabla" del sistema. */
      border-radius: var(--md-shape-extra-large);
      /* Mismo mecanismo de tinte que .hoja, pero con --md-elevation-tint-3
       (11%): un modal se lee MÁS elevado que una hoja apoyada, así que el
       tinte tiene que crecer con el nivel igual que la sombra. Misma base
       -lowest que .hoja, por la misma razón: ver el comentario ahí. */
      background: light-dark(var(--md-surface-container-lowest),
            color-mix(in oklab,
               var(--md-primary) var(--md-elevation-tint-3),
               var(--md-surface-container-lowest)));
      color: var(--md-on-surface);
      /* Flota sobre la pantalla, no está apoyado: nivel 3, el que usa MD3
       para un dialog. Mucho más marcado que el nivel 1 de .hoja —si se
       parecieran, lo que flota dejaría de distinguirse de lo que solo está
       posado—. */
      box-shadow: var(--md-elevation-3);
   }

   /* El velo se deriva de --md-scrim con color-mix() y no de un token propio: dos
   alfas porque el trabajo se reparte distinto —con desenfoque el fondo ya es
   ilegible y el velo puede ser suave (42%, deja reconocer que la aplicación
   sigue detrás); sin desenfoque el velo es lo único que separa y tiene que
   apagar el fondo él solo (66%)—. Es el MISMO en los dos temas porque
   --md-scrim ya es light-dark(#000000, #000000): un velo claro sobre el tema
   oscuro se leería como un destello, y acá ni hace falta evitarlo a mano.

   `::backdrop` no hereda de `:root` en todos los navegadores del piso —heredar
   del elemento originario es un cambio reciente del estándar—, así que el
   respaldo va DENTRO del `var(--md-scrim, …)`: si el custom property no llega,
   color-mix() sigue recibiendo un color válido y el velo se pinta igual.

   El desenfoque del fondo es legítimo acá: el modal es registro de ACOGIDA
   —§3 lo pone junto al ingreso y las confirmaciones—, una tarea sola y nada que
   compita. No es el tratamiento que se le da a una tabla.

   `saturate(80%)` y no el `140%` del cristal del ingreso: ahí el vidrio tenía
   que brillar, y acá el fondo tiene que RETROCEDER. Desaturar un poco es lo que
   lo manda atrás sin apagarlo del todo. */
   .modal::backdrop {
      background: color-mix(in srgb, var(--md-scrim, #000) 42%, transparent);
      -webkit-backdrop-filter: blur(8px) saturate(80%);
      backdrop-filter: blur(8px) saturate(80%);
   }

   /* Sin desenfoque el velo suave deja el fondo legible, y un modal sobre una
   tabla que todavía se lee no detiene a nadie: ahí el velo tiene que hacer el
   trabajo entero. Mismo recurso que usa el cristal del ingreso. */
   @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
      .modal::backdrop {
         background: color-mix(in srgb, var(--md-scrim, #000) 66%, transparent);
      }
   }

   /* Se desplaza y escala al aparecer: espacial. El velo de abajo, en cambio,
   solo cambia de opacidad —efecto—, así que llevan resortes distintos aunque
   corran juntos; --dur-efecto-normal (261ms) queda pegado a
   --dur-espacial-rapido (257ms) para que las dos lecturas terminen casi a la
   vez sin que el velo, que no se desplaza, tome prestado un resorte que
   rebota. */
   @keyframes modal-entra {
      from {
         opacity: 0;
         transform: translateY(8px) scale(.98);
      }

      to {
         opacity: 1;
         transform: none;
      }
   }

   @keyframes velo-entra {
      from {
         opacity: 0;
      }

      to {
         opacity: 1;
      }
   }

   /* 🔴 La salida tiene fotogramas PROPIOS y no `modal-entra … reverse`.
   Cuando `animation-name` no cambia entre dos reglas, el navegador no reinicia
   la animación: actualiza la que ya está. Y al llegar acá la entrada ya terminó,
   así que invertirle la dirección la mandaba directo a su estado final —opacidad
   0— sin recorrer nada: el modal desaparecía de golpe. Con otro nombre empieza
   una animación nueva y sí se ve. Vale para cualquier par entrada/salida de esta
   hoja, no solo para el modal. */
   @keyframes modal-sale {
      from {
         opacity: 1;
         transform: none;
      }

      to {
         opacity: 0;
         transform: translateY(8px) scale(.98);
      }
   }

   @keyframes velo-sale {
      from {
         opacity: 1;
      }

      to {
         opacity: 0;
      }
   }

   .modal[open] {
      animation: modal-entra var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   .modal[open]::backdrop {
      animation: velo-entra var(--dur-efecto-normal) var(--resorte-efecto-normal);
   }

   /* Dos clases y no una para ganarle a `.modal[open]`, que si no repetiría la
   entrada. `forwards` deja el modal invisible hasta que el JS llama a `close()`
   al terminar la animación.

   🔴 --dur-espacial-rapido (257ms) está atado a `setTimeout(terminar, 400)` de
   `ts/ui/modal.ts:71` —la red de seguridad por si `animationend` no llega—: con
   143ms de margen el timeout no se dispara antes de tiempo, pero si esta
   duración sube hay que revisar ese número también. */
   .modal[open].modal--saliendo {
      animation: modal-sale var(--dur-espacial-rapido) var(--resorte-espacial-rapido) forwards;
   }

   .modal[open].modal--saliendo::backdrop {
      animation: velo-sale var(--dur-efecto-normal) var(--resorte-efecto-normal) forwards;
   }

   .modal__titulo {
      margin: 0 0 var(--e2);
      font-size: var(--fs-titulo);
      font-weight: 700;
   }

   .modal__mensaje {
      margin: 0;
      color: var(--md-on-surface-variant);
   }

   .modal__aviso {
      margin: var(--e3) 0 0;
      padding: var(--e2) var(--e3);
      border: 1px solid var(--md-error);
      /* Franja de aviso: contenida como un campo, aunque esté dentro de una
       pieza de acogida — no es el contenedor, es una nota adentro de él.
       Mismo criterio en .ingreso__aviso, .inicio__nota y .panel__aviso: las
       cuatro son la misma clase de elemento, tienen que leerse igual. */
      border-radius: var(--md-shape-small);
      background: var(--md-error-container);
      color: var(--md-on-error-container);
      font-size: var(--fs-dato);
   }

   /* A 400 px los dos botones envuelven antes que salirse. */
   .modal__acciones {
      display: flex;
      justify-content: flex-end;
      flex-wrap: wrap;
      gap: var(--e2);
      margin-top: var(--e5);
   }

   /* --- Botones ------------------------------------------------------------- */

   /* Cada variante redefine --boton-fondo y --boton-texto en vez de repintar la
   regla entera: así el hover es UNA línea que sirve para las tres. Ya no son
   tokens de :root —antes lo eran, junto con --peligro-fondo/--peligro-texto—
   sino variables locales de este componente: el valor por omisión (relleno)
   es --md-primary/--md-on-primary directo, sin capa semántica intermedia. */
   .boton {
      display: inline-flex;
      align-items: center;
      gap: var(--e2);
      /* Horizontal baja de --e4 a --e3: es el mismo paso que ya usa
       `.campo__control` (línea ~919), así que un botón y un campo de
       formulario en la misma fila comparten el mismo ritmo de relleno —
       fase 3 pide justo esa coherencia entre las tres familias. */
      padding: var(--e2) var(--e3);
      /* --lh-ajustada y no la prosa heredada de <body> (1.5): un rótulo de
       botón es texto corto de una sola línea, no algo que se lee de corrido,
       así que le corresponde la interlínea apretada de la fase 2 — y de paso
       es lo que más aligera el alto sin tocar el padding otra vez. */
      line-height: var(--lh-ajustada);
      border: 1px solid transparent;
      /* Todos los botones son píldora, incluidos los --chico dentro de una
       tabla: es una decisión tomada, no una lectura del registro de
       trabajo. El radio pedido (9999px) excede la mitad del alto de
       CUALQUIER botón de esta hoja, así que el navegador lo recorta ahí —es
       la propiedad de --md-shape-full, no algo que dependa del tamaño— y el
       ancho de la caja no cambia: border-radius nunca participa del cálculo
       de ancho (padding + borde + contenido), solo curva la esquina hacia
       ADENTRO de esa caja. Medido contra el botón --chico más largo de una
       fila de tabla («Parámetros», 12px/600), la curva no interfiere con el
       texto porque el texto está centrado en vertical, lejos de donde la
       esquina recorta. */
      border-radius: var(--md-shape-full);
      --boton-fondo: var(--boton-principal-fondo);
      --boton-texto: var(--boton-principal-texto);
      background: var(--boton-fondo);
      color: var(--boton-texto);
      font: inherit;
      font-weight: 600;
      cursor: pointer;
      /* Efecto, y de los rápidos: el hover solo cambia de color, no se
       desplaza. Un resorte espacial acá es el error clásico al adoptar
       MD3 —aplicarlo a todo deja la interfaz gelatinosa—; ver skill §8. */
      transition: background var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* El par anterior (--boton-texto/--texto) garantizaba POR CONSTRUCCIÓN que el
   hover subiera el contraste: --boton-texto se elegía siempre en sentido
   contrario a --texto. MD3 no ofrece esa garantía —on-primary/on-error no
   están pensados en función de on-surface—, así que esto se MIDIÓ, mezclando
   88 % del fondo con on-surface (antes --texto), en los dos temas y las tres
   variantes:

     relleno    claro 6,49:1 → 7,43:1   oscuro 7,76:1 → 8,05:1   (sube)
     peligro    claro 6,46:1 → 7,38:1   oscuro 7,70:1 → 7,95:1   (sube)
     secundario claro 15,47:1 → 11,82:1 oscuro 13,26:1 → 10,31:1 (baja, pero
                sigue sobradísimo: su --boton-texto ES --md-on-surface, así
                que mezclar hacia on-surface acerca el fondo a su propio
                texto — mismo efecto que ya tenía el sistema anterior).

   Sube en relleno y peligro porque on-primary/on-error, calibrados por MD3
   para el máximo contraste contra su propio acento, resultan alejados de
   on-surface EN LA MISMA DIRECCIÓN en los dos temas —blanco/casi-negro en
   claro, casi-negro/casi-blanco en oscuro—: es una propiedad de esta paleta
   verificada acá, no algo que el sistema garantice.

   Se probó también la alternativa obvia —mezclar hacia el texto PROPIO de
   cada botón (`var(--boton-texto)`) en vez de hacia on-surface— y empeora:
   relleno baja a 4,97:1 y peligro a 4,95:1, al filo del mínimo de 4,5:1. La
   fórmula que queda es la que ya estaba, apuntando siempre a on-surface.

   `in oklab` y no el sRGB por omisión: interpolar en sRGB ensucia el paso. */
   .boton:hover:not(:disabled) {
      background: color-mix(in oklab, var(--boton-fondo) 88%, var(--md-on-surface));
   }

   /* Prensado: el mismo mecanismo que el hover, con más mezcla —es la respuesta
   al gesto que pedía la fase 3, un tinte que se nota más fuerte bajo el dedo
   sin mover ni redimensionar la caja—. Los números de contraste de arriba
   siguen valiendo: 78 % de --boton-fondo está MÁS cerca de on-surface que el
   88 % del hover, así que el contraste solo sube respecto de lo ya medido.

   `:not(.ingreso__enviar)`: esa pieza fija su propio aspecto y
   deliberadamente NO sigue al tema (ver el bloque de Ingreso), pero comparte
   la clase `.boton` para heredar la forma. Sin la exclusión, este prensado
   mezclaría hacia --md-on-surface —que SÍ sigue al tema— y le abriría una
   grieta al único registro que tiene prohibido seguirlo; el ingreso ya trae
   su propio `:active` (el hundimiento de 1px, más abajo en su bloque). */
   .boton:active:not(:disabled):not(.ingreso__enviar) {
      background: color-mix(in oklab, var(--boton-fondo) 78%, var(--md-on-surface));
   }

   .boton:disabled {
      opacity: .55;
      cursor: not-allowed;
   }

   .boton--secundario {
      --boton-fondo: var(--md-surface-container-low);
      --boton-texto: var(--md-on-surface);
      /* --md-outline-variant y no --md-outline: el mismo trazo de bajo
       contraste que ya usan `.hoja` y `.tabla-caja` para un borde que separa
       sin competir — acá el secundario deja de ser el botón con el borde más
       marcado de los tres y pasa a leerse tan liviano como el resto de la
       fase. */
      border-color: var(--md-outline-variant);
   }

   /* Acciones sin retorno —anonimizar, revocar un enrolamiento—. El color no
   basta para señalarlas: la doble confirmación es lo que las frena. */
   .boton--peligro {
      --boton-fondo: var(--md-error);
      --boton-texto: var(--md-on-error);
   }

   /* Vive en celdas de tabla (skill §7, registro de trabajo): el padding y el
   tamaño NO se tocan en esta fase —la fase 2 ya calibró el alto de fila
   contra este mismo botón, ver el comentario de `.tabla th/td`—. Lo único
   que cambia es tokenizar el tamaño; el número sigue siendo 12px. */
   .boton--chico {
      padding: 2px var(--e2);
      font-size: var(--fs-meta);
      font-weight: 600;
   }

   /* --- El rótulo que se desliza -------------------------------------------- */

   /* El texto de carga no reemplaza al de reposo: los dos están montados, uno
   debajo del otro, y el riel sube. Reemplazar el texto es un corte seco, y en
   un botón que se pulsa y no responde de inmediato ese corte es justamente lo
   que hace dudar de si el clic entró.

   `1lh` es una línea de texto del propio botón, así que el visor no necesita
   medirse desde el JS como sí lo necesita el riel del ingreso: acá los dos
   rótulos miden lo mismo de alto por definición. */
   .boton__visor {
      display: block;
      overflow: clip;
      height: 1lh;
   }

   /* Espacial: el rótulo se DESPLAZA (sube medio riel) — el ejemplo que la skill
   §8 pone junto al panel que se despliega y al riel del ingreso. */
   .boton__riel {
      display: grid;
      grid-auto-rows: 1lh;
      transition: transform var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   .boton--cargando .boton__riel {
      transform: translateY(-50%);
   }

   /* El visor mide lo que el rótulo MÁS LARGO de los dos y ese ancho no cambia al
   pasar de uno a otro, así que el botón no da un salto de ancho a mitad del
   envío. `center` porque el rótulo corto ocupa una fila tan ancha como el
   largo, y sin esto quedaría descentrado respecto del botón. */
   .boton__rotulo {
      text-align: center;
      white-space: nowrap;
   }

   /* --- Formularios --------------------------------------------------------- */

   .campo {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
   }

   /* `flex` porque la etiqueta puede llevar la marca «opcional» al lado.
   `baseline` y no `center`: los dos textos son de tamaño distinto y alineados
   al centro la marca queda flotando sobre la línea del rótulo. */
   .campo__etiqueta {
      display: flex;
      align-items: baseline;
      gap: var(--e2);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      font-weight: 600;
   }

   /* Se marca lo que SE PUEDE dejar vacío, no lo que hay que llenar.
   En el mesón un campo sin marca se lee como obligatorio, y el funcionario se
   detiene a buscar un dato que el paciente no tiene —el apellido materno de
   quien no lo usa— cuando el formulario habría guardado igual.

   Mismo tratamiento que `.critico`: versalitas chicas y sin píldora. Acá la
   palabra ES la información, y una píldora ocuparía más que el rótulo que
   califica. Teñida como texto secundario y no como advertencia: no advierte
   nada, solo permite. */
   .campo__opcional {
      font-size: var(--fs-micro);
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: .06em;
      white-space: nowrap;
      color: var(--md-on-surface-variant);
      opacity: .75;
   }

   /* El atajo de teclado, contado al pie de los campos. Va visible y no en una
   pista que se abre al posar el cursor: un atajo que hay que descubrir con un
   gesto no lo descubre nadie. No es un aviso —no pasó nada— así que no lleva
   recuadro, solo el tono de lo secundario. */
   .formulario__atajo {
      margin: 0;
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
   }

   /* La clase y NO `.campo input, .campo select`: hay controles que llevan el
   formato sin estar dentro de un `.campo` —el filtro de establecimiento de un
   listado, el campo de solo lectura con el enlace para fijar la clave— y con el
   selector descendiente salían sin borde ni relleno, indistinguibles del fondo.
   Al revés también importaba: la casilla de `.campo__casilla` es un `input` y
   no tiene por qué heredar el relleno de una caja de texto. */
   .campo__control {
      padding: var(--e2) var(--e3);
      /* --lh-ajustada: el control de un formulario es texto corto de una sola
       línea, no prosa — mismo criterio que ya usa la fase 2 en la celda de
       tabla. De paso acerca el alto útil al de `.boton`, que ahora usa la
       misma interlínea y el mismo paso de relleno (--e2/--e3): las dos
       familias comparten vocabulario, aunque el radio siga siendo distinto
       a propósito (ver la nota de --md-shape-* en :root: campo va en
       `small`, botón en `full` — son registros de forma distintos, no un
       olvido). */
      line-height: var(--lh-ajustada);
      /* --md-outline-variant y no --md-outline: mismo trazo de bajo contraste
       que ya usan `.hoja`, `.tabla-caja` y el botón secundario — un campo en
       reposo no necesita competir por atención, la necesita el que está
       enfocado (ver `:focus-visible` más abajo). */
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-low);
      color: var(--md-on-surface);
      font: inherit;
      /* Efecto, no espacial: borde y halo cambian de color/opacidad, no se
       desplazan (skill §8). */
      transition:
         border-color var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         box-shadow var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* El foco visible global (§5 de la skill) NO se toca: el `outline` de
   `:focus-visible` en `base` sigue dibujándose igual, a 2px de distancia del
   borde. Esto se SUMA — un halo más cerca y más suave, mismo mecanismo que ya
   usa la casilla en `:hover` (más abajo: `color-mix()` transparente sobre
   `box-shadow`, sin ocupar espacio de layout) — así el campo se lee
   "iluminado" además de "rodeado", que es lo que un halo aporta y un
   contorno solo no. El borde también sube a --md-primary: en reposo es
   --md-outline-variant, apenas insinuado: entre los dos pasos el salto al
   enfocar tiene que notarse. */
   .campo__control:focus-visible {
      border-color: var(--md-primary);
      box-shadow: 0 0 0 4px color-mix(in srgb, var(--md-primary) 16%, transparent);
   }

   /* Un `<select>` no lleva rótulo deslizante como `botonEnvio()`: mientras
   carga sus opciones se deshabilita y solo cambia el cursor (§8 de la skill
   de estilos). */
   select.campo__control:disabled {
      cursor: progress;
      opacity: .6;
   }

   .campo__error {
      color: var(--md-error);
      font-size: var(--fs-meta);
   }

   .campo__ayuda {
      margin: 0;
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
   }

   .campo__casilla {
      display: inline-flex;
      align-items: center;
      gap: var(--e2);
      font-size: var(--fs-dato);
      cursor: pointer;
   }

   /* --- La casilla ----------------------------------------------------------
   Transversal y SIN CLASE a propósito: la regla toma cualquier
   `input[type="checkbox"]`, así una pantalla nueva la hereda sin que nadie
   tenga que acordarse. Si alguna vez hiciera falta la casilla del sistema
   operativo, eso sí pide una clase que la devuelva.

   La nativa mide 13 px y se pierde en una tabla densa. Acá el cuadro es de
   20 px: es lo que se marca sin apuntar con precisión y lo que se lee de un
   vistazo cuando hay una columna entera: la matriz de permisos es el caso, y
   ahí cada casilla es un permiso que alguien va a tener o no. */
   input[type="checkbox"] {
      appearance: none;
      -webkit-appearance: none;
      display: inline-grid;
      place-content: center;
      flex: 0 0 auto;
      width: 20px;
      height: 20px;
      margin: 0;
      border: 2px solid var(--md-outline);
      border-radius: var(--md-shape-extra-small);
      background: transparent;
      cursor: pointer;
      transition:
         background var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         border-color var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         box-shadow var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* La marca se dibuja con `clip-path` y no con un ícono de `iconoEstado()`:
   esto no es un estado de un registro, es el control mismo, y no necesita el
   vocabulario de significados de la skill §4 — solo un tilde. Nace en escala
   0 y crece al marcarse — el salto de una marca que aparece de golpe es lo
   que hace dudar de si el clic entró. */
   input[type="checkbox"]::before {
      content: "";
      width: 13px;
      height: 13px;
      background: var(--md-on-primary);
      clip-path: polygon(14% 44%, 0 59%, 39% 100%, 100% 16%, 85% 0, 39% 70%);
      transform: scale(0);
      transition: transform var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   input[type="checkbox"]:checked,
   input[type="checkbox"]:indeterminate {
      background: var(--md-primary);
      border-color: var(--md-primary);
   }

   input[type="checkbox"]:checked::before {
      transform: scale(1);
   }

   /* Ni marcada ni vacía: media raya, no un tilde a medias. Hoy no la usa nadie;
   está para la casilla de encabezado que marca una columna entera de la
   matriz, donde «algunos sí» es un estado real y decirlo con el cuadro vacío
   sería mentir. */
   input[type="checkbox"]:indeterminate::before {
      clip-path: inset(42% 12% 42% 12%);
      transform: scale(1);
   }

   /* El halo de estado de MD3: aparece bajo el cursor y no ocupa lugar, así una
   tabla con doscientas casillas no se recompone al pasar por encima. */
   input[type="checkbox"]:hover:not(:disabled) {
      border-color: var(--md-primary);
      box-shadow: 0 0 0 6px color-mix(in srgb, var(--md-primary) 12%, transparent);
   }

   /* .38 es el valor de MD3 para lo deshabilitado. Acá además significa algo
   preciso: lo que no se puede marcar porque el servidor lo va a rechazar —un
   perfil del sistema— se distingue de lo que no se marca porque no viene de
   `perfil_permiso`, que no lleva casilla sino figura. */
   input[type="checkbox"]:disabled {
      cursor: not-allowed;
      opacity: .38;
   }

   /* Un campo que el servidor rechazó. El borde acompaña; el mensaje de
   `.campo__error` es el que informa, y `aria-invalid` lo dice al lector de
   pantalla — el color solo no llega a ninguna de las dos partes.

   El borde va sobre el PANEL, no sobre este mismo fondo teñido, así que usa
   --md-error directo y no on-error-container —ese rol está calibrado para ir
   sobre error-container, no sobre superficie—. El texto que se escribe adentro
   sigue siendo --md-on-surface general (la regla de arriba no se toca acá): se
   midió contra error-container y da 13,23:1 en claro y 7,24:1 en oscuro, de
   sobra sobre el mínimo de 4,5:1. */
   .campo__control[aria-invalid="true"] {
      border-color: var(--md-error);
      background: var(--md-error-container);
   }

   /* El halo de foco (más arriba) es --md-primary a secas: sobre un campo
   inválido eso dejaba un borde rojo con un halo celeste, dos acentos
   distintos diciendo cosas distintas. Acá el halo se tiñe del mismo rojo que
   ya lleva el borde, así que enfocar un campo con error no agrega un tercer
   color a la lectura. */
   .campo__control[aria-invalid="true"]:focus-visible {
      box-shadow: 0 0 0 4px color-mix(in srgb, var(--md-error) 16%, transparent);
   }

   /* --- Tablas: el registro de trabajo -------------------------------------- */

   /* Plano, apretado, contraste alto. Sin sombra POR FILA y sin backdrop-filter:
   con el relleno de una pantalla de acogida la agenda mostraría 6 filas en vez
   de 15, y una sombra por fila es ruido multiplicado por doscientos.

   El cajón ahora tiene borde y superficie propios —antes era solo el
   `overflow-x` que le daba el scroll horizontal—: fondo -lowest (el mismo
   escalón que .hoja/.modal/.menu/.cab, ver ese comentario), borde de bajo
   contraste y radio contenido. SIN box-shadow: el borde y el escalón de
   superficie ya lo distinguen del lienzo sin necesitar elevación, y sumar una
   sombra lo acercaría a .hoja —una tabla sigue sin ser una hoja, sigue siendo
   un cajón (skill §3), esta fase solo le agrega bordes al cajón—.
   --md-shape-medium y no -small: el radio de un campo se queda corto para un
   contenedor de este tamaño, y -large es forma plena de registro de acogida,
   que acá no corresponde.

   `overflow-x: auto` ya alcanza para recortar las esquinas: con un eje en
   algo distinto de `visible`, el otro se computa `auto` también (nunca
   `visible` en los dos a la vez), así el contenedor recorta en las dos
   direcciones y el radio no se ve interrumpido por contenido que se escapa. */
   .tabla-caja {
      overflow-x: auto;
      background: var(--md-surface-container-lowest);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-medium);
   }

   .tabla {
      width: 100%;
      border-collapse: collapse;
      /* Transparente: el fondo ahora lo pone `.tabla-caja`. Que la tabla siga
       declarando su forma "sin forma" (ver el comentario de abajo) no cambia
       —lo que se redondeó es el cajón que la envuelve, no ella—. */
      background: transparent;
      /* Explícito y no simplemente omitido: el registro de trabajo reclama
       "sin forma" con la misma intención con que .hoja reclama "forma
       plena" — es el otro extremo de la misma escala, no un olvido. */
      border-radius: var(--md-shape-none);
   }

   .tabla th,
   .tabla td {
      /* Vertical a --e3 (antes --e2): una fila de una sola línea gana 8px de
       alto total, un 21 % más de aire y no el doble — el registro sigue
       siendo denso, ver el conteo de filas en el comentario de más abajo.
       Horizontal se queda en --e3, no cambia. */
      padding: var(--e3) var(--e3);
      /* --lh-ajustada (1.3) y no la prosa de <body> (1.5): antes una celda de
       una línea y una de dos (nombre + `.campo__ayuda` con su descripción,
       ver matriz/permisos/catálogo) no tenían la MISMA unidad de línea —la
       de arriba heredaba 1.5 porque nadie la declaraba acá—, así que el
       salto entre una fila y la de al lado era el que trajera el contenido,
       no un paso previsible. Con la interlínea fija en la celda, agregar una
       línea siempre suma lo mismo. Se hereda hacia `.campo__ayuda` y
       `.perfil__nombre` de adentro, que no declaran la suya propia. */
      line-height: var(--lh-ajustada);
      /* --md-separador y no --md-outline-variant a secas: con el mismo peso
       que el borde del cajón, la línea entre filas competía con él. La
       cabecera recupera el peso completo más abajo — ahí SÍ hace falta
       marcar dónde terminan los rótulos y empiezan los datos. */
      border-bottom: 1px solid var(--md-separador);
      text-align: left;
      vertical-align: middle;
   }

   .tabla th {
      position: sticky;
      top: 0;
      /* -container-low y no -container: la cabecera pesaba tanto como el borde
       del cajón que la contiene y competía con él. Con el mismo tono que el
       lienzo (ver `body`) queda apoyada al ras, un escalón por debajo del
       fondo -lowest de las filas — la jerarquía la sostienen la versalita,
       el tamaño y el borde inferior de peso completo, no el fondo. */
      background: var(--md-surface-container-low);
      /* Peso completo, a diferencia del separador entre filas de arriba: acá
       SÍ hace falta marcar el límite entre cabecera y datos. */
      border-bottom-color: var(--md-outline-variant);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      text-transform: uppercase;
      letter-spacing: .04em;
   }

   /* La fila cuyas celdas tocan el borde inferior del cajón no lleva línea
   propia: con el separador de arriba, esa línea quedaba colgando contra el
   borde del contenedor, una encima de la otra. */
   .tabla tbody tr:last-child>td,
   .tabla tbody tr:last-child>th {
      border-bottom: none;
   }

   /* Con las filas ahora en -lowest (antes -low, igual que la cabecera: apenas
   se notaba), el hover contra --md-surface-container vuelve a leerse —antes
   el salto entre fila y hover era de un escalón, ahora es de dos—. */
   .tabla tbody tr:hover {
      background: var(--md-surface-container);
   }

   /* La fila cuyo registro está abierto en el panel. Sin esto el panel dice
   «Hospital de Curicó» y nada en la tabla indica de cuál de las veinte filas
   salió.

   Va DESPUÉS de `:hover` porque empatan en especificidad y tiene que ganar
   esta: mientras se edita, pasar el mouse por encima no puede borrar la señal
   de dónde se está parado.

   La barra es un `inset` y no un `border-left` porque un borde correría el
   contenido de la celda tres píxeles y la columna entera bailaría al abrir y
   cerrar el panel. Y no lleva transición a propósito: es un marcador de estado
   en una tabla densa, no un movimiento — atenuar doscientas filas al pasar el
   mouse es justo lo que el registro de trabajo no quiere.

   Es el acento de INTERFAZ (--md-primary), no un estado de dominio: por eso
   usa primary-container y no tertiary. El texto general de la tabla está
   calibrado contra --md-surface-container-lowest (el panel; antes era -low,
   ver el cambio de superficie de esta fase), no contra este fondo teñido, así
   que acá se declara aparte con el on-container que corresponde:
   on-primary-container/primary-container da 13,32:1 en claro y 7,27:1 en
   oscuro — no depende del fondo, así que el cambio de escalón no lo afecta. */
   .tabla tbody tr[aria-current] {
      background: var(--md-primary-container);
      color: var(--md-on-primary-container);
   }

   .tabla tbody tr[aria-current]>td:first-child {
      box-shadow: inset 3px 0 0 var(--md-primary);
   }

   /* --- Estados: color MÁS silueta ------------------------------------------ */

   /* Entre el 5 y el 8 % de los hombres no distingue rojo de verde, y en la
   agenda confundir «confirmada» con «cancelada» manda a alguien a un box que
   no lo espera. El color acompaña; la forma es la que informa, y el texto
   siempre está — eso NO cambia en esta fase: lo único que se aligera es el
   peso visual alrededor de esas tres señales. */
   .estado {
      display: inline-flex;
      align-items: center;
      gap: var(--e1);
      /* Más chica que antes (2px de antes, borde y relleno al mismo tono
       pesado): lo que sostiene el estado sigue siendo .estado__figura + el
       texto, así que la píldora puede achicarse sin perder la garantía del
       §4 de la skill. */
      padding: 1px var(--e2);
      /* Transparente y no ausente — mismo patrón que `.boton` (más abajo): así
       la caja mide lo mismo tenga o no borde visible, y ninguna variante
       tiene que recalcular su tamaño. Hoy ninguna lo usa: el borde de acento
       plano de antes era una SEGUNDA silueta además de la figura, y quitarlo
       es lo que más aligera sin tocar la garantía de color+forma+texto. */
      border: 1px solid transparent;
      border-radius: var(--md-shape-full);
      background: var(--md-surface-container-high);
      color: var(--md-on-surface-variant);
      /* --fs-meta y no un 12px suelto: mismo tamaño de siempre, ahora en la
       escala de la fase 2. --lh-ajustada porque es texto corto de una sola
       línea dentro de una fila, no prosa. */
      font-size: var(--fs-meta);
      font-weight: 600;
      line-height: var(--lh-ajustada);
      white-space: nowrap;
   }

   /* Antes un punto sólido de 9×9 en `clip-path` (siete siluetas: cuadro,
   círculo, rombo, triángulo…). Un `<svg>` de `iconoEstado()` necesita más
   caja que un punto para leerse, y la skill pide medirlo a 12–14 px reales de
   píldora: se sube a 14. El ícono hereda `currentColor` de por sí —viene
   armado así desde `ts/ui/iconos.ts`— así que acá no hay color que declarar,
   solo tamaño y que no se aplaste en una fila flex. */
   .estado__figura {
      width: 14px;
      height: 14px;
      flex: 0 0 auto;
   }

   /* Fase 3 · píldoras más livianas — dos cambios respecto de lo que había:

   1. El borde de acento plano desaparece (queda transparente, ver `.estado`
      arriba): era una SEGUNDA silueta además de la figura, y en una tabla
      densa pesaba más que el dato que calificaba. La figura y el texto no se
      tocan — son las dos señales que el §4 de la skill exige y siguen ahí.

   2. El relleno se suaviza mezclando el container hacia el extremo neutro
      del tema (`--md-surface-container-lowest`, el mismo hacia el que ya
      mezcla `.hoja` para su tinte de elevación — ver ese comentario). No hace
      falta remedir el contraste del texto: `on-*-container` está calibrado
      contra el container PURO, que es el punto de MENOR contraste de toda la
      mezcla — en claro el fondo solo puede aclararse más (texto oscuro
      encima gana contraste) y en oscuro solo puede oscurecerse más (texto
      claro encima gana contraste). Cualquier paso hacia -lowest aleja el
      fondo del texto, nunca lo acerca.

   `in oklab`, no sRGB, por la misma razón que ya vale en `.hoja`: interpolar
   en sRGB ensucia el paso entre dos colores saturados. */
   .estado--ok {
      background: color-mix(in oklab, var(--md-success-container) 55%, var(--md-surface-container-lowest));
      color: var(--md-on-success-container);
   }

   .estado--info {
      background: color-mix(in oklab, var(--md-tertiary-container) 55%, var(--md-surface-container-lowest));
      color: var(--md-on-tertiary-container);
   }

   .estado--pending {
      background: color-mix(in oklab, var(--md-warning-container) 55%, var(--md-surface-container-lowest));
      color: var(--md-on-warning-container);
   }

   .estado--fail {
      background: color-mix(in oklab, var(--md-error-container) 55%, var(--md-surface-container-lowest));
      color: var(--md-on-error-container);
   }

   /* --- La hoja en pantalla angosta ------------------------------------------ */

   /* A 400 px nada puede desbordarse horizontalmente. Tablas, diagramas y bloques
   de código van cada uno en su propio contenedor con overflow-x: auto. */
   @media (max-width: 640px) {

      /* El padding baja de e5 a e4: con menos aire adentro, --md-shape-large
       (16px) empieza a comerse el margen reducido, así que el radio baja un
       escalón a medium (12px) — sigue siendo forma plena, solo un punto
       menos generosa, proporcional al tamaño de la pieza igual que en MD3. */
      .hoja {
         padding: var(--e4);
         border-radius: var(--md-shape-medium);
      }
   }

   /* ===========================================================================
   El armazón: riel, navbar y lienzo

   Antes eran TRES franjas apiladas —barra oscura, pestañas y cabecera—: unos
   135 px de cromo antes del contenido, y la ubicación dicha dos veces sin que
   ninguna nombrara el módulo. Ahora son dos piezas: el riel dice a dónde se
   puede ir, el navbar dice dónde se está.
   =========================================================================== */

   .app {
      display: grid;
      /* `minmax(0, 1fr)` y no `1fr`: una tabla ancha dentro de una columna de
       grid la estiraría, y el scroll horizontal de `.tabla-caja` no llegaría a
       activarse nunca. */
      grid-template-columns: var(--ancho-riel) minmax(0, 1fr);
      min-height: 100%;
      /* Espacial: el riel se DESPLAZA al contraerse, y el contenido se corre con
       él. Un navegador que no sepa interpolar `grid-template-columns` hace el
       cambio de golpe, que es exactamente lo que había antes de la animación. */
      transition: grid-template-columns var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   /* --- El riel: a dónde se puede ir ---------------------------------------- */

   .riel {
      display: flex;
      flex-direction: column;
      /* Sin relleno ni `gap` propios: cada bloque trae el suyo (`.riel__cabeza`,
       `.riel__nav`, `.riel__pie`). La cabeza tiene que ir A SANGRE para poder
       medir --alto-cab exacto y quedar en la misma línea que el navbar de al
       lado; con el relleno acá medía los 36 px de la palanca y el rótulo
       «Vínculo» caía 4 px más abajo que el título de la vista, con el bloque
       terminando 8 px antes que la raya del navbar. */
      /* Mismo escalón que `.cab`: el riel y el navbar son la MISMA pieza de
       cromo doblada en ángulo, y el lienzo es lo único recesado. La fase 1
       subió el navbar a -lowest y dejó el riel en -container, y eso abrió el
       salto de la esquina: en claro quedaba un rectángulo teñido de 248×60
       mordido del extremo izquierdo de una franja blanca, sin ninguna línea
       que explicara el cambio de tono. Ahora no hay cambio de tono que
       explicar — lo único que separa al riel del lienzo es su borde derecho,
       y la raya del navbar muere contra él en una T, que es como se encuentran
       dos planos del mismo color. */
      background: var(--armazon-fondo);
      background-image: var(--armazon-degradado);
      background-attachment: fixed;
      /* Tenue por omisión: es lo que hereda cualquier texto del riel que no
       fije su propio color, así que nada queda oscuro-sobre-azul por
       descuido. Lo fuerte («Vínculo», el usuario, el hover, el activo) lo
       pide cada pieza. */
      color: var(--armazon-texto-tenue);
      border-right: 1px solid var(--armazon-borde);
      /* Pegado y con alto de ventana: la navegación no se puede ir con el scroll
       de una agenda de doscientas filas. `overflow-y` es para el día que el
       árbol de módulos sea más largo que la pantalla. */
      position: sticky;
      top: 0;
      height: 100vh;
      overflow-y: auto;
   }

   /* Lo que el cristal tiene para refractar. Un backdrop-filter sobre un fondo
   PLANO no se ve: el desenfoque promedia un color con sí mismo y devuelve el
   mismo color. Sin estas dos manchas, la píldora activa quedaría apenas más
   saturada y todo el trabajo del vidrio se perdería.

   z-index negativo, no `order` ni un div: `position: sticky` ya abre contexto
   de apilamiento en `.riel`, así que el -1 queda contenido acá adentro —pasa
   por encima del fondo del riel y por debajo de los enlaces— sin filtrarse al
   resto de la aplicación.

   Las dos manchas bajan de 15/13 % a 9/8 % porque el riel cambió de fondo: un
   15 % de --md-primary sobre -container (#e1f1ef) es un velo; sobre -lowest,
   que en claro es blanco puro, el mismo 15 % da #d9e8e7 —el tono de
   -container-highest— y la mancha deja de leerse como profundidad para
   leerse como suciedad en una columna blanca. 🔴 Si la píldora activa pierde
   su lectura de vidrio, ESTOS dos números son los que hay que subir: son lo
   único que el backdrop-filter tiene para refractar. */
   .riel::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: -1;
      pointer-events: none;
      background:
         radial-gradient(58% 20% at 20% 28%,
            color-mix(in srgb, var(--md-primary) 9%, transparent), transparent 70%),
         radial-gradient(50% 24% at 82% 66%,
            color-mix(in srgb, var(--md-tertiary) 8%, transparent), transparent 72%);
   }

   /* Todo ícono del sistema mide lo mismo y hereda el color: se dibujan con
   `currentColor` en `ts/ui/iconos.ts`, así que un enlace activo tiñe su ícono
   sin una regla propia. `flex: 0 0 auto` para que el rótulo largo de un módulo
   no lo aplaste. */
   .icono {
      width: 20px;
      height: 20px;
      flex: 0 0 auto;
   }

   /* Mide --alto-cab, el mismo alto que el navbar, y por eso «Vínculo» y el
   título de la vista caen en la misma línea óptica a los dos lados del borde
   del riel. Es lo que hace que la esquina se lea como una decisión y no como
   un encuentro casual de dos piezas.

   NO lleva raya inferior aunque el navbar sí la lleve: el navbar CRECE cuando
   la vista fija subtítulo (60 → ~71 px), así que una raya acá coincidiría con
   la suya en unas pantallas y no en otras, y una línea que calza a veces es
   peor que ninguna. Lo que sí vale siempre es la T que forman el borde
   derecho del riel —que corre entero— y la raya del navbar muriendo contra él.

   La sangría izquierda es --e5 y no el relleno del riel: deja el rótulo en la
   misma columna que los íconos de los enlaces (--e3 del `.riel__nav` + --e3
   del enlace), que es la vertical que el ojo sigue al bajar por el riel. */
   .riel__cabeza {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--e2);
      min-height: var(--alto-cab);
      padding: 0 var(--e3) 0 var(--e5);
   }

   .riel__marca {
      /* Fuerte y no tenue: es la marca, el rótulo de más peso del riel. */
      display: inline-flex;
      align-items: center;
      gap: var(--e2);
      color: var(--armazon-texto);
      font-size: var(--fs-titulo);
      letter-spacing: .01em;
      white-space: nowrap;
      overflow: hidden;
   }

   /* Del alto de la letra mayúscula y un poco más: el símbolo acompaña al
   nombre, no compite con él. */
   .riel__simbolo {
      flex: none;
      width: 1.35em;
      height: 1.3em;
   }

   /* La palanca: contrae el riel en pantalla ancha, cierra el cajón en angosta.
   Cuadrada y sin rótulo — el nombre lo lleva en `aria-label`, que cambia con
   lo que la palanca va a hacer. */
   .riel__palanca {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 36px;
      height: 36px;
      flex: 0 0 auto;
      border: 0;
      border-radius: var(--md-shape-full);
      background: transparent;
      color: var(--armazon-texto-tenue);
      cursor: pointer;
   }

   /* --armazon-realce y no un escalón de la escala de contenedores: el porqué
   está en el token, en `:root`, y vale para los cinco hover del cromo —no es
   --armazon-roce, que usan otras piezas que NO están sobre este azul. */
   .riel__palanca:hover {
      background: var(--armazon-realce);
      color: var(--armazon-texto);
   }

   /* `flex: 1` empuja el pie del riel al fondo sin posicionarlo. El relleno es el
   que el riel dejó de tener: los 16 px de arriba son los que separan la cabeza
   del primer rótulo de módulo, y los 12 de los costados, los que ponen el
   ícono de cada enlace en la columna del rótulo «Vínculo». */
   .riel__nav {
      display: flex;
      flex-direction: column;
      gap: var(--e4);
      flex: 1 1 auto;
      padding: var(--e4) var(--e3);
   }

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

   /* El nombre del módulo. Es el mismo micro rótulo en versalitas que el
   `<legend>` de un grupo de campos: los dos dicen «esto de acá abajo va
   junto», y que se vean igual es lo que hace que se lean igual. */
   .riel__modulo-titulo {
      margin: 0 0 var(--e1);
      padding: 0 var(--e2);
      font-size: var(--fs-micro);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      color: var(--armazon-texto-tenue);
   }

   .riel__enlace {
      display: flex;
      align-items: center;
      gap: var(--e3);
      padding: var(--e2) var(--e3);
      /* Píldora: es la forma que MD3 le da al destino de un riel de navegación,
       y acá además separa el enlace del rótulo cuadrado del módulo. */
      border-radius: var(--md-shape-full);
      color: var(--armazon-texto-tenue);
      text-decoration: none;
      font-size: var(--fs-cuerpo);
      font-weight: 600;
      transition:
         background var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         box-shadow var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* El disparador de un grupo es un `<button>` y sus vecinos son `<a>`: sin este
   reinicio trae el borde y el fondo que le da el navegador, y el único destino
   del riel con marco sería justamente el que no lleva a ninguna parte. La
   señal de que tiene hijos son el galón y el segundo nivel, no un recuadro.

   `font-family` y no el atajo `font`: el atajo reiniciaría también el tamaño y
   el peso que `.riel__enlace` acaba de fijar, y el rótulo del grupo quedaría
   más chico que el de sus vecinos. */
   button.riel__enlace {
      width: 100%;
      background: none;
      border: 0;
      font-family: inherit;
      text-align: left;
      cursor: pointer;
   }

   .riel__enlace:hover {
      background: var(--armazon-realce);
      color: var(--armazon-texto);
   }

   /* El destino activo es el único relleno del riel, y el relleno es la señal —no
   el color del texto—: a la distancia de un mesón, una píldora llena se
   encuentra de un vistazo y un texto teñido hay que leerlo.

   PRUEBA: el relleno es una lámina de cristal tintado en vez de un plano. Lo
   que la hace leerse como vidrio son cuatro cosas juntas, y sacar cualquiera
   la devuelve a parecer una píldora translúcida y nada más: el tinte a media
   opacidad, el desenfoque de lo que hay detrás, el filo de luz en el canto
   superior y la sombra corta que la despega del riel.

   `saturate(170%)` hace el trabajo que el blur no puede hacer solo contra un
   fondo casi plano: sube el croma de lo que quedó debajo, y ese salto de
   saturación es lo que el ojo lee como «hay un vidrio acá». Va con el blur,
   no en su lugar.

   El borde va como `inset box-shadow` y no como `border`: un borde de 1px
   correría el texto y el ícono un píxel, y el enlace activo dejaría de estar
   alineado con los demás del riel. */
   .riel__enlace--activo,
   .riel__enlace--activo:hover,
   /* Un grupo desplegado también: mientras muestra sus hijos es la sección en la
   que uno está parado, y el vidrio es como este riel dice eso. Plegado vuelve
   a ser un destino más, sin marco ni relleno. */
   .riel__enlace[aria-expanded="true"],
   .riel__enlace[aria-expanded="true"]:hover,
   /* El hijo activo del segundo nivel se lee IGUAL que un destino de primer
   nivel: es el mismo hecho —acá estás— y darle un relleno distinto lo
   convertiría en otra cosa. Lo que cambia entre niveles es el tamaño y la
   sangría, no la señal. */
   .riel__subenlace--activo,
   .riel__subenlace--activo:hover,
   /* Contraído no se dibujan los hijos, así que la marca la lleva el grupo: sin
   esto, el riel de íconos no diría en qué pantalla está parado el usuario. */
   .app--compacto .riel__enlace--contiene {
      background: var(--armazon-activo-fondo);
      color: var(--armazon-activo-texto);
      -webkit-backdrop-filter: blur(10px) saturate(170%);
      backdrop-filter: blur(10px) saturate(170%);
      box-shadow:
         inset 0 0 0 1px var(--riel-vidrio-borde),
         inset 0 1px 0 var(--riel-vidrio-filo),
         var(--md-elevation-1);
   }

   /* Sin desenfoque el tinte a 55 % deja la píldora a medio camino entre el
   fondo del riel y el destino activo, que es justo la distinción que el riel
   no puede perder. Vuelve al relleno opaco de siempre. */
   @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {

      .riel__enlace--activo,
      .riel__enlace--activo:hover,
      .riel__enlace[aria-expanded="true"],
      .riel__enlace[aria-expanded="true"]:hover,
      .riel__subenlace--activo,
      .riel__subenlace--activo:hover,
      .app--compacto .riel__enlace--contiene {
         background: var(--armazon-activo-fondo-opaco);
      }
   }

   /* --- El segundo nivel del riel -------------------------------------------
   Un grupo no es un destino: su disparador pliega y despliega, y quien navega
   es alguno de sus hijos. */
   .riel__grupo {
      display: flex;
      flex-direction: column;
      gap: 2px;
   }

   /* El galón se come el espacio sobrante para quedar pegado al borde derecho, y
   no bailando según el largo del nombre del grupo. Apunta hacia abajo abierto
   y hacia el costado plegado, que es la convención que ya trae aprendida
   cualquiera que haya usado un árbol de carpetas. */
   .riel__galon {
      margin-left: auto;
      width: 0;
      height: 0;
      border-left: 4px solid transparent;
      border-right: 4px solid transparent;
      border-top: 5px solid currentColor;
      transition: transform var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .riel__enlace[aria-expanded="false"] .riel__galon {
      transform: rotate(-90deg);
   }

   /* Los hijos no llevan ícono: el del padre ya dijo de qué se trata, y repetirlo
   dejaría tres dibujos iguales en columna. Lo que dice que cuelgan de arriba
   son la sangría y la línea, que además le dan al ojo por dónde volver. */
   /* El segundo nivel se DESLIZA al abrirse: la fila de la grilla pasa de `0fr` a
   `1fr` y la caja de adentro recorta lo que todavía no cabe. Aparecer de golpe
   deja la duda de si el riel se reordenó solo.

   Por qué acá el CSS y no `desplegable()`, que es la pieza de plegado del
   proyecto: esa mide el alto en JavaScript al abrir, y el riel se repinta en
   cada navegación —vuelve a leer el hash para marcar el destino actual—, así
   que el mismo grupo se reanimaría cada vez que uno se mueve entre sus
   pantallas. Con una clase, el estado inicial se pinta sin transición y el
   movimiento queda solo para el clic, que es lo que el usuario provocó.

   La objeción del §8 de la skill a este truco es para el REEMPLAZO de
   contenido, donde no hay transición porque el valor sigue en `1fr`. Acá el
   contenido no cambia: son los mismos tres enlaces abriéndose y cerrándose.

   `margin-bottom` va en la caja interna y no acá: sobre el contenedor dejaría
   un hueco de 8 px debajo del grupo aun estando plegado. */
   .riel__subnivel {
      display: grid;
      grid-template-rows: 1fr;
      margin: 2px 0 0 var(--e5);
      padding-left: var(--e2);
      border-left: 1px solid var(--armazon-borde);
      transition:
         grid-template-rows var(--dur-espacial-rapido) var(--resorte-espacial-rapido),
         border-color var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   .riel__subnivel-lista {
      display: flex;
      flex-direction: column;
      gap: 2px;
      margin-bottom: var(--e1);
      /* El relleno existe por el recorte de abajo: sin él, el borde de la caja
       coincide con el borde del primer y del último enlace, y la sombra
       exterior del vidrio —`--md-elevation-1` cae 1 px con 3 px de
       difuminado— queda cortada justo en esos dos. El del medio se veía bien y
       los de las puntas, planos.

       A la izquierda no lleva: ahí el relleno correría los enlaces y los
       sacaría de la línea del segundo nivel. Esa sombra la recorta la caja
       para los tres por igual, que es lo que no se nota. */
      padding: var(--e1) var(--e1) var(--e1) 0;
      /* Lo que sostiene el recorte mientras la fila crece. Sin esto los enlaces
       se salen de la grilla y el deslizamiento no se ve. */
      overflow: hidden;
   }

   /* Plegado: la fila mide cero y el borde de la izquierda se va con ella, que si
   no queda una rayita colgando bajo el grupo cerrado. */
   .riel__subnivel--plegado {
      grid-template-rows: 0fr;
      border-left-color: transparent;
   }

   .riel__subenlace {
      padding: var(--e1) var(--e3);
      border-radius: var(--md-shape-full);
      color: var(--armazon-texto-tenue);
      text-decoration: none;
      font-size: var(--fs-dato);
      font-weight: 500;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      transition:
         background var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         box-shadow var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .riel__subenlace:hover {
      background: var(--armazon-realce);
      color: var(--armazon-texto);
   }

   .riel__subenlace--activo,
   .riel__subenlace--activo:hover {
      color: var(--armazon-activo-texto);
   }

   /* El rótulo de un enlace o de un botón del riel. Contraído se deja de dibujar
   —el `title` y el `aria-label` siguen diciendo el nombre—, y por eso el ícono
   nunca es la única señal: es lo que queda cuando el texto se va, no lo que
   reemplaza al texto. */
   .riel__rotulo {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }

   /* Su raya ahora cruza el riel de lado a lado —antes el relleno del riel la
   dejaba corta 12 px por cada punta— porque es la misma línea que separa
   bloques en el resto del armazón: la del navbar también va a sangre. Una raya
   con márgenes dice «esto es una lista»; una raya entera dice «acá termina el
   riel y empieza quién lo está usando», que es lo que el pie separa. */
   .riel__pie {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      padding: var(--e3) var(--e3) var(--e4);
      border-top: 1px solid var(--armazon-borde);
   }

   /* Quién sos acá: el nombre y los perfiles que rigen. Van en su propia caja
   con 2 px entre ellos —y no sueltos en `.riel__pie`, que separa con --e2—
   porque son una sola cosa: el hueco hacia los botones de abajo tiene que
   leerse mayor que el que hay entre estas dos líneas. */
   .riel__quien {
      display: flex;
      flex-direction: column;
      gap: 2px;
   }

   .riel__usuario {
      /* Fuerte: es el propio nombre, no un dato de apoyo. */
      color: var(--armazon-texto);
      padding: 0 var(--e2);
      font-size: var(--fs-dato);
      font-weight: 600;
      /* Un nombre largo no puede ensanchar el riel ni desbordarlo. */
      overflow-wrap: anywhere;
   }

   /* Los perfiles vigentes DONDE UNO ESTÁ PARADO. Existen en pantalla porque
   sin ellos no había forma de entender por qué un botón aparece en un hospital
   y no en el otro: los perfiles se suman —no se alternan— y cambiar de
   establecimiento cambia cuáles rigen. Esta línea es lo único que lo cuenta.

   Texto tenue y no píldoras: la píldora es para el estado de un registro
   dentro de una lista, donde hay que distinguir de un vistazo entre doscientas
   filas (§4 de la skill). Acá son dos nombres bajo el propio, que ya se están
   leyendo; con píldora ocupaban más que el nombre que califican.

   Se deja envolver: el riel mide 248 px y «Administrativo SOME» junto a
   «Superusuario» no entra en una línea. Cortarlos con puntos suspensivos sería
   peor —un perfil a medio nombre no dice cuál es—. */
   .riel__perfiles {
      padding: 0 var(--e2);
      color: var(--armazon-texto-tenue);
      font-size: var(--fs-meta);
      line-height: var(--lh-ajustada);
      overflow-wrap: anywhere;
   }

   /* En columna y no en fila: con ícono y rótulo, tres botones en fila se parten
   en dos líneas desparejas, y contraído tienen que quedar uno sobre otro de
   todos modos. */
   .riel__acciones {
      display: flex;
      flex-direction: column;
      gap: 2px;
   }

   .riel__boton {
      display: flex;
      align-items: center;
      gap: var(--e3);
      padding: var(--e2) var(--e3);
      border: 0;
      /* Píldora, igual que el enlace de al lado: en el riel las dos piezas hacen
       lo mismo —llevan a alguna parte— y tienen que verse iguales. */
      border-radius: var(--md-shape-full);
      background: transparent;
      color: var(--armazon-texto-tenue);
      font: inherit;
      font-size: var(--fs-cuerpo);
      font-weight: 600;
      text-align: left;
      cursor: pointer;
   }

   .riel__boton:hover {
      background: var(--armazon-realce);
      color: var(--armazon-texto);
   }

   /* «Salir» es un `botonEnvio`, y sus rótulos van centrados porque el visor mide
   lo que el más largo de los dos —en un botón normal, centrar es lo que evita
   que el rótulo corto quede descolgado—. En el riel eso lo dejaba desalineado
   respecto de «Cambiar clave» y «Tema», que son texto llano a la izquierda.
   Se corrige acá y no en `.boton__rotulo`: el centrado sigue siendo correcto
   para todos los demás botones. */
   .riel__boton .boton__visor,
   .riel__boton .boton__rotulo {
      text-align: left;
   }

   /* --- El navbar: dónde se está -------------------------------------------- */

   .marco {
      display: flex;
      flex-direction: column;
      min-width: 0;
   }

   .cab {
      display: flex;
      align-items: center;
      gap: var(--e4);
      min-height: var(--alto-cab);
      padding: var(--e3) var(--pad-lateral);
      /* -lowest y no -low: el navbar es `sticky` y convive con el lienzo todo
       el tiempo, así que es la pieza que más necesita distinguirse de él —
       ver el comentario en `.hoja` para la razón del escalón.

       🔴 `.riel` usa este MISMO token desde la fase 4, a propósito: los dos
       son el cromo de la aplicación y tienen que leerse como un solo plano en
       ángulo. Si alguna vez hay que mover este tono —o el azul de la prueba
       visual de --armazon-fondo—, hay que moverlo en los dos: moverlo en uno
       solo es exactamente lo que abrió el salto de la esquina entre la fase 1
       y la 4. */
      background: var(--armazon-fondo);
      background-image: var(--armazon-degradado);
      background-attachment: fixed;
      color: var(--armazon-texto-tenue);
      border-bottom: 1px solid var(--armazon-borde);
      /* Siempre arriba: en una agenda larga, saber en qué establecimiento se
       está parado no puede depender de haber vuelto al principio de la página.
       El z-index le gana al panel desplegable y a la fila marcada, no al modal
       —que vive en la capa superior del navegador por ser `<dialog>`—. */
      position: sticky;
      top: 0;
      z-index: 5;
   }

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

   /* Módulo › vista, en una sola línea. El módulo viene del árbol y la vista del
   `<h1>` que fija cada ruta: no se escriben dos veces. */
   .cab__migas {
      display: flex;
      align-items: baseline;
      gap: var(--e2);
      min-width: 0;
      flex-wrap: wrap;
   }

   .cab__modulo {
      color: var(--armazon-texto-tenue);
      font-size: var(--fs-dato);
      font-weight: 600;
   }

   .cab__separador {
      color: var(--armazon-texto-tenue);
   }

   .cab__titulo {
      /* Fuerte: es el título de la vista, lo más importante del navbar. */
      color: var(--armazon-texto);
      margin: 0;
      font-size: var(--fs-vista);
      font-weight: 700;
   }

   .cab__subtitulo {
      margin: 0;
      color: var(--armazon-texto-tenue);
      font-size: var(--fs-dato);
   }

   .cab__acciones {
      display: flex;
      gap: var(--e2);
      flex-wrap: wrap;
      align-items: center;
   }

   /* Las acciones de la vista sobre el azul: el botón oscuro. El secundario y
   el de peligro conservan lo suyo — el de peligro tiene que seguir diciendo
   que destruye. */
   .cab__acciones .boton:not(.boton--secundario):not(.boton--peligro) {
      --boton-fondo: var(--armazon-boton-fondo);
      --boton-texto: var(--armazon-boton-texto);
      border-color: var(--armazon-boton-borde);
   }

   .cab__acciones .boton:not(.boton--secundario):not(.boton--peligro):hover:not(:disabled) {
      background: color-mix(in oklab, var(--boton-fondo) 86%, var(--armazon-boton-roce));
   }

   .cab__acciones .boton:not(.boton--secundario):not(.boton--peligro):active:not(:disabled) {
      background: color-mix(in oklab, var(--boton-fondo) 76%, var(--armazon-boton-roce));
   }

   .cab__acciones .boton:focus-visible {
      outline-color: var(--armazon-foco);
   }

   /* El extremo derecho del navbar: el establecimiento y las acciones de la vista.
   Son dos cosas distintas y por eso el hueco entre ellas es mayor que el que
   separa a dos botones entre sí. */
   .cab__extremo {
      display: flex;
      gap: var(--e3);
      flex-wrap: wrap;
      align-items: center;
   }

   /* En qué establecimiento se está parado. Va en el navbar y no en el riel:
   actuar sobre el hospital equivocado es un error caro, y el riel se puede
   cerrar. Este no. */
   .cab__estab {
      padding: var(--e1) var(--e2);
      border: 1px solid var(--md-outline);
      /* No es un botón: es una etiqueta que dice dónde se está parado (el
       <select> de abajo comparte esta clase, pero tampoco lleva la píldora de
       los botones — un selector nativo no es la pieza que la decisión de forma
       plena cubre). Contenida, como un chip. */
      border-radius: var(--md-shape-extra-small);
      background: var(--md-surface-container-high);
      color: var(--md-on-surface);
      font: inherit;
      font-size: var(--fs-dato);
      font-weight: 600;
   }

   .cab__estab--vacio {
      color: var(--md-on-surface-variant);
      font-weight: 400;
      font-style: italic;
   }

   .cab__estab--select {
      cursor: pointer;
   }

   /* Un `<select>` no lleva rótulo deslizante: mientras cambia de establecimiento
   se deshabilita y solo cambia el cursor (§8 de la skill de estilos). */
   .cab__estab--select:disabled {
      opacity: .6;
      cursor: progress;
   }

   /* El botón que abre el riel. Solo existe en angosto — ver la media query. */
   .cab__menu {
      display: none;
   }

   .lienzo {
      flex: 1 1 auto;
      width: 100%;
      max-width: var(--ancho-contenido);
      margin: 0 auto;
      padding-block: var(--e5);
      padding-inline: var(--pad-lateral);
   }

   /* El foco del lienzo se lleva al cambiar de vista, para que quien navega con
   teclado no tenga que tabular de nuevo por todo el riel. Pero es un destino
   de foco, no un control: marcarlo con el anillo confundiría. */
   .lienzo:focus {
      outline: none;
   }

   /* --- El riel contraído: solo íconos --------------------------------------- */

   /* Solo en pantalla ancha. En angosto el riel es un cajón que se abre entero, y
   un cajón contraído sería un cajón que no dice nada. */
   @media (min-width: 900px) {
      .app--compacto {
         grid-template-columns: var(--ancho-riel-compacto) minmax(0, 1fr);
      }

      /* El relleno lateral se aprieta bloque por bloque y no en `.riel`, que ya
       no tiene: contraído el riel mide 76 px y los 12 de cada lado le comían
       un tercio del ancho útil. */
      .app--compacto .riel__nav,
      .app--compacto .riel__pie {
         padding-inline: var(--e2);
      }

      /* La cabeza pierde la sangría de --e5 y centra la palanca: sin rótulo no
       hay columna que alinear, y la palanca es lo único que queda. El alto
       sigue siendo --alto-cab, así que contraer no mueve la línea de la
       esquina. */
      .app--compacto .riel__cabeza {
         justify-content: center;
         padding-inline: 0;
      }

      /* Lo que se va es el TEXTO, no la pieza: el ícono se queda en su lugar y la
       fila no cambia de alto, así que contraer no recompone el riel entero. */
      .app--compacto .riel__marca,
      .app--compacto .riel__modulo-titulo,
      /* La caja entera y no solo sus dos líneas: vaciarla la dejaría contando
      como hijo de `.riel__pie`, y su `gap` de --e2 quedaría como un hueco sin
      nada adentro encima de los botones. */
      .app--compacto .riel__quien,
      .app--compacto .riel__rotulo,
      /* El segundo nivel no se dibuja adentro: el riel tiene `overflow-y`, que
       recorta también en horizontal, así que un desplegable acá quedaría
       cortado contra el borde. Contraído, los hijos salen en el emergente que
       abre el propio grupo — ver `ts/ui/armazon.ts`. */
      .app--compacto .riel__subnivel,
      .app--compacto .riel__galon,
      .app--compacto .riel__boton .boton__visor {
         display: none;
      }

      .app--compacto .riel__enlace,
      .app--compacto .riel__boton {
         justify-content: center;
         gap: 0;
         padding-inline: 0;
      }

      /* Sin los rótulos de módulo, lo que separa un módulo del siguiente es una
       línea. Sin esto, contraído los destinos de «Configuración» y los del
       módulo que venga después quedan en una sola lista corrida. */
      .app--compacto .riel__modulo+.riel__modulo {
         padding-top: var(--e3);
         border-top: 1px solid var(--armazon-borde);
      }
   }

   /* --- El riel en pantalla angosta ------------------------------------------ */

   /* Bajo 900 px el riel sale de cuadro y se abre con el botón del navbar. No se
   colapsa a iconos: sin recursos externos cada icono habría que dibujarlo a
   mano, y un riel de iconos mudos no dice más que ninguno. */
   @media (max-width: 899px) {
      .app {
         grid-template-columns: minmax(0, 1fr);
      }

      .riel {
         position: fixed;
         inset-block: 0;
         inset-inline-start: 0;
         z-index: 30;
         width: min(280px, 82vw);
         height: 100%;
         /* Espacial: el riel se DESPLAZA, así que le toca el resorte y no la
           curva de efecto (§8 de la skill). La regla global de
           `prefers-reduced-motion` ya lo desarma. */
         transform: translateX(-100%);
         transition: transform var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
         box-shadow: var(--md-elevation-3);
      }

      .riel--abierto {
         transform: none;
      }

      /* Cuadrado y del mismo tamaño que la palanca del riel: son el mismo gesto
       visto desde los dos lados, abrir y cerrar. */
      .cab__menu {
         display: inline-flex;
         align-items: center;
         justify-content: center;
         width: 36px;
         height: 36px;
         flex: 0 0 auto;
         border: 0;
         border-radius: var(--md-shape-full);
         background: transparent;
         color: var(--armazon-texto);
         cursor: pointer;
      }

      .cab__menu:hover {
         background: var(--armazon-realce);
      }
   }

   /* El foco global (§5 de la skill, regla en `base`) pinta el anillo con
   --md-primary, y sobre el azul de --armazon-fondo eso da 1,96:1 — casi no se
   ve. No se apaga ni se reemplaza la regla global: se le gana la especificidad
   acá, solo para lo que se apoya directo sobre el azul, con --armazon-foco
   (blanco en claro, el mismo --md-primary de siempre en oscuro). El chip del
   establecimiento (`.cab__estab`) queda AFUERA a propósito: tiene su propia
   superficie clara y el anillo morado de siempre ya contrasta ahí. */
   .riel__palanca:focus-visible,
   .riel__enlace:focus-visible,
   .riel__subenlace:focus-visible,
   .riel__boton:focus-visible,
   .cab__menu:focus-visible {
      outline-color: var(--armazon-foco);
   }

   /* El velo. Es un `<button>` y no un `<div>`: cerrar tocando fuera tiene que
   existir también para quien llega con el teclado, y así lo anuncia el lector
   de pantalla en vez de ser un rectángulo mudo. */
   .riel__velo {
      position: fixed;
      inset: 0;
      z-index: 20;
      border: 0;
      padding: 0;
      background: color-mix(in srgb, var(--md-scrim) 42%, transparent);
      cursor: pointer;
   }

   .riel__velo[hidden] {
      display: none;
   }
}

@layer pantallas {
   /* ===========================================================================
   Ingreso · registro de ACOGIDA

   Es la única pantalla que puede permitirse translucidez, desenfoque y
   profundidad (§3): tiene ocho elementos y ningún dato. **Este tratamiento no
   se traslada a las tablas** — ahí el backdrop-filter recompondría en cada
   cuadro del scroll y el relleno costaría filas visibles.
   =========================================================================== */

   /* `fixed` + `inset: 0` para que el marco cubra el viewport entero y no asome
   el fondo del tema por debajo. Es un grid de una fila: en celular una sola
   columna —el panel del formulario, más abajo—, y en pantalla ancha se suma
   la columna del panel de marca (§ pantalla ancha, al final de esta sección),
   con la misma composición partida —panel de identidad más formulario al
   lado— de las otras pantallas de la casa. `minmax(0, 1fr)` en la fila es lo
   que deja que el panel del formulario recorte y desplace su propio
   contenido en vez de crecer y empujar el marco entero —ver `min-height` en
   `.ingreso__panel`—. */
   .ingreso {
      position: fixed;
      inset: 0;
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: minmax(0, 1fr);
      overflow: hidden;
      background: var(--ing-papel);
      color: var(--ing-texto);
      /* Sin `color-scheme` propio desde el 25-09-2026: antes fijaba `light`
       para que el papel fuera siempre claro (ver la nota de los tokens
       `--ing-*` en :root). Ahora hereda el `color-scheme` de `:root`, que es
       el que mueve `data-tema` — es lo que hace que `light-dark()` en estos
       tokens resuelva la columna correcta. */
   }

   /* --- El panel de marca -----------------------------------------------------
   Solo se muestra en pantalla ancha —`display` se resuelve en el media query
   del final de esta sección—. En celular el video ni se agrega —lo decide
   `pantallaAcogida()`, mirando el mismo ancho— y el nombre pasa a una franja
   mínima arriba del formulario, `.ingreso__marca-movil`: mismo criterio que
   las referencias de la casa, que en celular sueltan la columna de marca en
   vez de encogerla. */
   .ingreso__identidad {
      display: none;
      position: relative;
      overflow: hidden;
      flex-direction: column;
      justify-content: center;
      padding: var(--e6);
      /* Se ve mientras el video no tiene todavía un cuadro pintado, y sigue
       viéndose entero si `pantallaAcogida()` no le agrega una fuente —ancho
       angosto, `prefers-reduced-motion` o ahorro de datos—: por eso es el
       MISMO azul del velo y no un color de espera aparte. */
      background: linear-gradient(160deg, var(--ing-marca-1), var(--ing-marca-fin));
      color: var(--ing-marca-texto);
      color-scheme: dark;
   }

   /* El video de la entrada del hospital (Coverr, sin personas reconocibles;
   ver `assets/media/ingreso/ORIGEN`). Puramente decorativo —`aria-hidden` lo
   pone `pantallaAcogida()`—, sin controles nativos, silenciado. `object-fit:
   cover` recorta el 16:9 del clip al alto y ancho reales del panel, que casi
   nunca son 16:9. */
   .ingreso__video {
      position: absolute;
      inset: 0;
      z-index: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
   }

   /* El velo azul encima del video: ver la nota de `--ing-marca-velo-1` en
   :root para la opacidad y cómo se verificó contra el cuadro más claro del
   clip. Reemplaza a las dos formas orgánicas animadas que había antes
   (`ingreso-deriva`): con el velo al 94 % de opacidad esas formas —debajo del
   video, y ahora también debajo de un azul casi opaco— dejarían de verse, así
   que se retiran en vez de quedar como código muerto. */
   .ingreso__velo {
      position: absolute;
      inset: 0;
      z-index: 1;
      pointer-events: none;
      background: linear-gradient(160deg, var(--ing-marca-velo-1), var(--ing-marca-velo-fin));
   }

   /* Envuelve el nombre, la bajada y el detalle: sin este contenedor
   `position: relative` quedarían PINTADOS DEBAJO del video y el velo, que son
   `position: absolute` — un elemento posicionado siempre pinta después que
   uno estático, sin importar el orden en el marcado.

   Es TAMBIÉN la caja que carga el contraste (ver la nota de `--ing-marca-
   scrim` en :root): `--ing-marca-scrim` es un fondo SÓLIDO y parejo, no un
   degradado que se apague hacia el borde, así que el contraste vale igual en
   el centro que pegado al padding. El padding agranda la caja más allá del
   texto —y el margen negativo lo compensa, para que el bloque no se corra ni
   ensanche el layout— porque el borde de la caja NO es donde el ojo lo ve: es
   donde termina el `box-shadow` difuminado de abajo, bastante más afuera. */
   .ingreso__identidad-contenido {
      position: relative;
      z-index: 2;
      padding: var(--e4) var(--e5);
      margin: calc(var(--e4) * -1) calc(var(--e5) * -1);
      border-radius: var(--md-shape-large);
      background: var(--ing-marca-scrim);
      /* El «borde difuso»: el mismo color de la caja, sin alcance (0 de
       spread) y con blur, así que se lee como un halo que se apaga hacia
       afuera y no como un segundo borde recto. Puramente decorativo — nada
       de texto llega hasta ahí, así que no necesita cumplir contraste. */
      box-shadow: 0 0 48px 0 var(--ing-marca-scrim);
   }

   /* Botón de pausa del video (WCAG 2.2.2: todo movimiento automático de más
   de 5 s tiene que poder detenerse). Solo existe en el marcado cuando
   `pantallaAcogida()` de verdad agregó una fuente al video — si no hay nada
   corriendo, no hay nada que pausar.

   Fondo propio y oscuro, no `--armazon-realce`: ese token está calibrado para
   una superficie clara casi blanca, y acá abajo puede haber cualquier cuadro
   del video. Un fondo oscuro fijo es legible sobre cualquiera de los dos. */
   .ingreso__video-boton {
      position: absolute;
      top: var(--e4);
      right: var(--e4);
      z-index: 3;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 32px;
      height: 32px;
      border: 0;
      border-radius: var(--md-shape-full);
      background: rgba(9, 14, 28, .55);
      color: #ffffff;
      cursor: pointer;
      transition: background var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .ingreso__video-boton:hover {
      background: rgba(9, 14, 28, .72);
   }

   /* La regla global de `:focus-visible` usa --md-primary, que sobre este
   panel —siempre azul, con o sin video— apenas se distingue (mismo motivo que
   --armazon-foco, más arriba). Blanco fijo y literal, como el resto de esta
   sección. */
   .ingreso__video-boton:focus-visible {
      outline-color: #ffffff;
   }

   /* --- El panel del formulario ------------------------------------------- */

   /* Contenedor de scroll propio: con dos columnas, cada una se desplaza por
   su cuenta y el panel de marca no arrastra al formulario con ella.
   `min-height: 0` es lo que permite que un hijo de grid recorte y desplace en
   vez de crecer más allá de su fila — sin esto, un aviso de error que
   agranda la tarjeta empujaría el marco entero fuera del viewport en vez de
   aparecer con scroll adentro del panel. El centrado es `margin: auto` sobre
   la tarjeta y NO `align-items: center`: con centrado por flex, una tarjeta
   más alta que el panel —error visible, celular apaisado— se recorta por
   arriba y no se llega al primer campo. */
   .ingreso__panel {
      display: flex;
      min-height: 0;
      overflow-y: auto;
      padding: var(--e5) var(--pad-lateral);
   }

   .ingreso__tarjeta {
      margin: auto;
      width: 100%;
      max-width: 400px;
   }

   /* Repite el nombre y la bajada del panel de marca: los dos nunca se ven a
   la vez —el CSS decide cuál según el ancho— y duplicar dos líneas de texto
   es más simple que mover un nodo entre dos padres al cruzar el punto de
   quiebre. */
   .ingreso__marca-movil {
      margin-bottom: var(--e5);
   }

   /* Tamaño y color de reposo: los de `.ingreso__marca-movil`, sobre el papel
   —el azul que se lee sobre el papel, no el sólido del botón; ver la nota de
   `--ing-acento-papel` en :root—. Dentro del panel de marca —azul, con video—
   el media query del final los invierte a blanco sólido. */
   .ingreso__marca {
      display: flex;
      align-items: center;
      gap: .3em;
      margin: 0;
      font-size: 24px;
      letter-spacing: -.02em;
      color: var(--ing-acento-papel);
   }

   .ingreso__simbolo {
      flex: none;
      width: 1.25em;
      height: 1.2em;
   }

   /* --- La marca: los pesos y el verde del logo ---------------------------
   Los mismos pesos que el SVG de `docs/marca/` (450 y 700), para que el
   nombre en pantalla y el logo impreso se lean como la misma pieza. La cinta
   izquierda del símbolo toma `currentColor` del contenedor; el verde
   y su pliegue son los de la marca en cualquier fondo. */
   .marca__vincula {
      font-weight: 450;
   }

   .marca__pro {
      font-weight: 700;
   }

   .marca__verde {
      fill: var(--marca-verde);
      stroke: var(--marca-verde);
   }

   .marca__pliegue {
      fill: var(--marca-pliegue);
      stroke: var(--marca-pliegue);
   }

   .ingreso__bajada {
      margin: var(--e1) 0 0;
      color: var(--ing-tenue);
      font-size: 13px;
      letter-spacing: .01em;
   }

   /* Solo existe dentro del panel de marca: es la línea que en pantalla ancha
   ocupa el espacio que el celular no tiene para nada más que el formulario. */
   .ingreso__detalle {
      margin: var(--e2) 0 0;
      max-width: 30ch;
      color: var(--ing-marca-texto-tenue);
      font-size: 13px;
      line-height: 1.5;
   }

   /* --- Las dos vistas sobre un riel ----------------------------------------- */

   /* `clip` y NO `hidden`: `hidden` crea un contenedor de scroll, y basta que algo
   lo desplace un píxel —enfocar un campo de la vista que está de lado— para que
   el riel quede corrido para siempre. `clip` no se puede desplazar, ni siquiera
   desde el JS.

   El recorte cae 8 px afuera del contenido y no pegado a él: el anillo de foco
   y el resplandor del campo activo sobresalen de la caja, y pegado los cortaría.
   El margen negativo devuelve esos 8 px, para que los campos queden alineados
   con el título de la tarjeta y no 8 px adentro. */
   .ingreso__visor {
      margin: calc(var(--e2) * -1);
      overflow: clip;
   }

   /* El alto lo escribe el JS midiendo la vista activa: las dos miden distinto y
   el CSS no sabe animar hacia `auto`. La clase la agrega el JS DESPUÉS de la
   primera medición — si la transición estuviera puesta desde el principio, la
   tarjeta se desplegaría sola al abrir la pantalla.

   Espacial y lento: una vista entera recorre toda la tarjeta, así que tarda
   más que un rótulo o un panel —los otros dos usos del resorte espacial van
   en `-rapido`—; ver el mismo criterio en `.ingreso__riel`, con quien esta
   transición corre siempre junta. */
   .ingreso__visor--animado {
      transition: height var(--dur-espacial-lento) var(--resorte-espacial-lento);
   }

   /* 200 % de ancho con dos vistas de 50 %: cada vista mide exactamente el visor,
   y `translateX(-50%)` —la mitad del riel— corre justo una vista.

   `align-items: flex-start` para que la vista corta no se estire hasta el alto
   de la larga: el riel desborda y el visor recorta, que es lo que se busca. */
   .ingreso__riel {
      display: flex;
      align-items: flex-start;
      width: 200%;
      transition: transform var(--dur-espacial-lento) var(--resorte-espacial-lento);
   }

   .ingreso__riel--recuperar {
      transform: translateX(-50%);
   }

   .ingreso__vista {
      flex: 0 0 50%;
      padding: var(--e2);
   }

   .ingreso__titulo {
      margin: 0 0 var(--e3);
      color: var(--ing-texto);
      font-size: 16px;
      font-weight: 600;
   }

   .ingreso__ayuda {
      margin: 0 0 var(--e4);
      color: var(--ing-tenue);
      font-size: 13px;
   }

   .ingreso__form {
      display: flex;
      flex-direction: column;
      gap: var(--e4);
   }

   .ingreso .campo__etiqueta {
      color: var(--ing-tenue);
      font-size: 12px;
      font-weight: 500;
      letter-spacing: .02em;
   }

   /* La fila del rótulo de la contraseña: la etiqueta a la izquierda y el
   enlace de recuperar a la derecha, en la misma línea — es donde alguien lo
   busca primero, apenas ve el campo, y no hace falta un renglón aparte para
   un enlace que solo hace falta cuando la clave falla. */
   .ingreso__campo-cabeza {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: var(--e2);
   }

   /* --- Campos -------------------------------------------------------------- */

   .ingreso .campo__control {
      width: 100%;
      padding: var(--e3);
      border: 1px solid var(--ing-campo-borde);
      border-radius: var(--md-shape-small);
      background: var(--ing-campo);
      color: var(--ing-texto);
      font: inherit;
      /* Efecto y rápido: borde, fondo y sombra cambian de aspecto en hover y
       foco, no se desplazan. */
      transition:
         border-color var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         background var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         box-shadow var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .ingreso .campo__control::placeholder {
      color: var(--ing-hueco);
   }

   .ingreso .campo__control:hover {
      border-color: var(--ing-campo-borde-hover);
   }

   /* El foco es un resplandor fino y NO reemplaza al anillo: la regla global de
   `:focus-visible` sigue en pie (§5), y esto se suma para que el campo activo
   se distinga también con el mouse. */
   .ingreso .campo__control:focus {
      outline: none;
      border-color: var(--ing-linea-foco);
      background: var(--ing-campo-activo);
      box-shadow: 0 0 0 3px var(--ing-foco);
   }

   .ingreso .campo__control:focus-visible {
      outline: 2px solid var(--ing-linea-foco);
      outline-offset: 2px;
   }

   /* Chrome pinta el relleno automático de blanco con un color de texto propio, y
   se lleva por delante todo el diseño. `box-shadow` interior es la única forma
   de taparlo, porque `background` lo ignora. */
   .ingreso .campo__control:-webkit-autofill,
   .ingreso .campo__control:-webkit-autofill:hover,
   .ingreso .campo__control:-webkit-autofill:focus {
      -webkit-text-fill-color: var(--ing-texto);
      -webkit-box-shadow: inset 0 0 0 1000px var(--ing-campo-autofill);
      caret-color: var(--ing-texto);
      transition: background-color 9999s ease-out;
   }

   /* --- Botón --------------------------------------------------------------- */

   .ingreso__enviar {
      justify-content: center;
      width: 100%;
      margin-top: var(--e1);
      padding-block: var(--e3);
      border: 1px solid transparent;
      /* Botón: píldora, igual que .boton. */
      border-radius: var(--md-shape-full);
      background: var(--ing-acento);
      color: var(--ing-acento-texto);
      font-weight: 600;
      letter-spacing: .01em;
      /* Los tres son efecto, incluido el `transform` de :active: ese 1px es el
       hundimiento del botón bajo el dedo, atado al puntero y no un objeto que
       viaja de un lugar a otro —no amerita el rebote de un resorte espacial,
       y agruparlo con el color evita una interfaz gelatinosa por un nudge de
       un píxel—. */
      transition:
         background var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         transform var(--dur-efecto-rapido) var(--resorte-efecto-rapido),
         box-shadow var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .ingreso__enviar:hover:not(:disabled) {
      background: var(--ing-acento-alto);
      box-shadow: 0 8px 22px -10px var(--ing-acento);
   }

   .ingreso__enviar:active:not(:disabled) {
      transform: translateY(1px);
   }

   .ingreso__enviar:disabled {
      opacity: .55;
      cursor: progress;
   }

   /* Un botón y no un enlace: no navega a ninguna parte, solo corre el riel. Se
   VE como enlace porque es lo que se busca debajo del botón de entrar, pero un
   <a> sin href no es alcanzable con teclado y uno con href recargaría.

   El relleno no es decorativo: con 13 px de texto, deja el blanco táctil por
   encima de los 24 px que pide WCAG 2.5.8 — esto se toca con el dedo en el
   mesón, no solo con el mouse. */
   .ingreso__enlace {
      align-self: center;
      padding: var(--e2);
      border: 0;
      background: none;
      color: var(--ing-acento-papel);
      font: inherit;
      font-size: 13px;
      text-decoration: underline;
      text-underline-offset: 3px;
      cursor: pointer;
      transition: color var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .ingreso__enlace:hover {
      color: var(--ing-texto);
   }

   /* El mismo enlace pero junto al rótulo de la contraseña (`.ingreso__campo-
   cabeza`), no debajo del botón: comparte línea con una etiqueta y no con el
   pulgar de un mesón, así que no lleva ni el centrado ni el relleno grande de
   `.ingreso__enlace`. Sigue siendo un `<button>` por el mismo motivo: no
   navega, solo corre el riel. */
   .ingreso__olvido {
      border: 0;
      background: none;
      padding: 0;
      color: var(--ing-acento-papel);
      font: inherit;
      font-size: 12px;
      font-weight: 600;
      text-decoration: underline;
      text-underline-offset: 3px;
      cursor: pointer;
      transition: color var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .ingreso__olvido:hover {
      color: var(--ing-texto);
   }

   /* --- Aviso --------------------------------------------------------------- */

   /* El mensaje es deliberadamente genérico —no dice si el correo existe—, así
   que se presenta como un aviso y no como el error de un campo: señalar
   «Correo» ya insinuaría cuál de los dos estuvo mal. */
   .ingreso__aviso {
      margin: 0;
      padding: var(--e2) var(--e3);
      border: 1px solid var(--ing-fail-borde);
      /* Misma franja contenida que .modal__aviso/.panel__aviso/.inicio__nota. */
      border-radius: var(--md-shape-small);
      background: var(--ing-fail-fondo);
      color: var(--ing-fail);
      font-size: 13px;
   }

   /* La confirmación de la recuperación. Mismo bloque que el aviso de error: es
   el mismo sitio de la tarjeta, cambia el tono. */
   .ingreso__aviso--ok {
      border-color: var(--ing-ok-borde);
      background: var(--ing-ok-fondo);
      color: var(--ing-ok);
   }

   /* --- Pie legal ------------------------------------------------------------
   Solo existe en la vista de entrar (lo decide `ingreso.ts`, no el CSS): es la
   puerta donde alguien está por consultar fichas, y ni «recuperar» ni fijar
   una clave nueva llegan a acceder a ningún dato todavía.

   `--ing-tenue` y no un tono más apagado: da 5,8:1 sobre `--ing-papel` claro y
   9,71:1 sobre el oscuro —de sobra para AA incluso en letra chica—, y este
   texto tiene consecuencia legal, así que no le sobra contraste para perder.
   Desde el 25-09-2026 el papel sigue el tema (ver la nota junto a los tokens
   `--ing-*` en :root), así que este bloque SÍ tiene una versión oscura —la
   trae `--ing-tenue` y `--ing-papel-borde` solos, sin una regla aparte. */
   .ingreso__legal {
      margin-top: var(--e5);
      padding-top: var(--e4);
      border-top: 1px solid var(--ing-papel-borde);
   }

   /* Ancho de línea contenido y no el de la tarjeta entera: a 400px de ancho
   una línea de fine print corrida de punta a punta es más difícil de leer,
   no menos. */
   .ingreso__legal p {
      margin: 0;
      max-width: 46ch;
      color: var(--ing-tenue);
      font-size: 11px;
      line-height: 1.5;
   }

   /* --- Pantallas angostas --------------------------------------------------- */

   @media (max-width: 480px) {
      .ingreso__panel {
         padding: var(--e4) var(--e4);
      }

      .ingreso__marca {
         font-size: 21px;
      }
   }

   /* En un móvil apaisado la tarjeta no cabe a lo alto: se apoya arriba y se
   desplaza, en vez de quedar centrada y recortada. */
   @media (max-height: 560px) {
      .ingreso__tarjeta {
         margin-block: 0 auto;
      }
   }

   /* --- Pantalla ancha: aparece el panel de marca -----------------------------
   Mismo punto de quiebre que el riel de la aplicación (`.app--compacto` /
   `.riel` en pantalla angosta, más arriba): es el ancho en el que sobra
   espacio para una columna que no hace ninguna tarea, solo ata la pantalla a
   la identidad del resto del software. */
   @media (min-width: 900px) {
      .ingreso {
         grid-template-columns: minmax(360px, 38%) minmax(0, 1fr);
      }

      .ingreso__identidad {
         display: flex;
      }

      /* Tamaño y color invertidos respecto de `.ingreso__marca-movil`: acá el
       fondo es azul con video detrás, así que el nombre necesita blanco
       sólido y con más peso —es la pieza más grande de la pantalla entera,
       no comparte columna con un formulario—. La bajada SÍ puede volver a un
       tono más apagado —ver la nota de `--ing-marca-texto-tenue` en :root—
       porque ahora las dos se leen sobre el fondo propio de
       `.ingreso__identidad-contenido`, no sobre el video a secas. */
      .ingreso__identidad .ingreso__marca {
         font-size: 34px;
         color: var(--ing-marca-texto);
      }

      .ingreso__identidad .ingreso__bajada {
         color: var(--ing-marca-texto-tenue);
      }

      /* La costura entre las dos columnas: papel claro contra fieltro oscuro ya
       se distingue solo, pero un trazo fino ata las dos piezas en vez de que
       se lean como dos rectángulos pegados por casualidad. */
      .ingreso__panel {
         border-left: 1px solid var(--ing-papel-borde);
      }

      /* El nombre ya está en el panel de marca: acá sobra. */
      .ingreso__marca-movil {
         display: none;
      }
   }

   /* ===========================================================================
   Inicio provisional · qué habilita esta sesión
   =========================================================================== */

   .inicio__saludo {
      margin: 0 0 var(--e2);
      font-size: var(--fs-titulo);
      font-weight: 600;
   }

   /* Es el estado INFORMATIVO de dominio (§4 de la skill), no el acento de
   interfaz: va a tertiary y no a primary — ver la nota junto a los roles de
   color en :root, donde se explica por qué no hay una familia 'info' propia. */
   .inicio__nota {
      margin: 0 0 var(--e4);
      padding: var(--e2) var(--e3);
      border: 1px solid var(--md-tertiary);
      /* Misma franja contenida que .modal__aviso/.ingreso__aviso/.panel__aviso. */
      border-radius: var(--md-shape-small);
      background: var(--md-tertiary-container);
      color: var(--md-on-tertiary-container);
      font-size: var(--fs-dato);
   }

   .inicio__modulo {
      margin-top: var(--e5);
   }

   .inicio__modulo-nombre {
      margin: 0 0 var(--e2);
      font-size: var(--fs-meta);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      color: var(--md-on-surface-variant);
   }

   .inicio__permisos {
      display: flex;
      flex-wrap: wrap;
      gap: var(--e2);
      margin: 0;
      padding: 0;
      list-style: none;
   }

   .inicio__permiso {
      padding: 2px var(--e2);
      border: 1px solid var(--md-outline-variant);
      /* Chip, no botón: no lleva la píldora, va contenido como .barra__estab. */
      border-radius: var(--md-shape-extra-small);
      background: var(--md-surface-container);
      font-family: var(--mono);
      font-size: var(--fs-meta);
   }

   /* Crítico = doble confirmación. Se tiñe, pero el texto lo dice además: el
   color no puede ser el único portador. Familia warning, no error: crítico
   pide una segunda persona, no es una falla. */
   .inicio__permiso--critico {
      border-color: var(--md-warning);
      background: var(--md-warning-container);
      color: var(--md-on-warning-container);
   }

   /* ===========================================================================
   Listados · registro de TRABAJO
   =========================================================================== */

   .lista__barra {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--e3);
      flex-wrap: wrap;
      margin-bottom: var(--e3);
   }

   .lista__filtro {
      display: inline-flex;
      align-items: center;
      gap: var(--e2);
      font-size: var(--fs-dato);
      cursor: pointer;
   }

   .lista__cuenta {
      color: var(--md-on-surface-variant);
      font-size: var(--fs-dato);
   }

   /* Modificador de `.panel__nota` para cuando aparece SUELTA dentro de un
   listado, antes de la tabla —el texto de consentimientos dice quién
   desarrolló el documento—. `.panel__nota` por sí sola no lleva margen porque
   normalmente vive dentro de un `.panel__form` que ya la separa con su `gap`;
   acá no hay tal contenedor, así que sin este margen queda pegada al cajón de
   la tabla. Mismo --e3 que ya separa `.lista__barra` de lo que sigue, para que
   toda la cabecera del listado respire con un solo paso. */
   .lista__nota {
      margin-bottom: var(--e3);
   }

   /* ===========================================================================
   Pacientes · la búsqueda y la ficha
   =========================================================================== */

   /* La caja de búsqueda es lo primero y lo único de la pantalla hasta que
   alguien escribe: esta pantalla NO abre con un listado, a propósito (ver
   `ts/routes/personas.ts`). Por eso el campo se lleva el ancho y el botón se
   queda con lo justo — lo que se mira acá es lo que se está tecleando. */
   .busqueda {
      display: flex;
      gap: var(--e2);
      align-items: center;
      flex-wrap: wrap;
      margin-bottom: var(--e4);
   }

   .busqueda__control {
      flex: 1 1 260px;
      min-width: 0;
   }

   /* El aspa de limpiar que Chrome pinta en un `type="search"` queda fuera del
   tema —gris fijo sobre fondo oscuro— y además duplica lo que ya hace Escape.
   El campo se vacía con el teclado, como todos los demás del sistema. */
   .busqueda__control::-webkit-search-cancel-button {
      appearance: none;
   }

   .persona__nombre {
      font-weight: 600;
   }

   /* Un dato de la ficha: rótulo arriba, valor abajo. Apilado y no en dos
   columnas porque el valor más largo —«Pasaporte VE AB1234567»— y el más corto
   —«72 años»— conviven en la misma lista, y una columna fija para el rótulo
   deja media hoja vacía o parte el valor en dos líneas. */
   .ficha__dato {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      padding: var(--e2) 0;
      border-bottom: 1px solid var(--md-outline-variant);
   }

   .ficha__etiqueta {
      font-size: var(--fs-meta);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      color: var(--md-on-surface-variant);
   }

   /* `flex` con `wrap`: el valor y su píldora de estado —«Sin verificar»— van en
   la misma línea mientras quepan, y a 400 px la píldora baja sola en vez de
   empujar el número del teléfono fuera de la hoja. */
   .ficha__valor {
      display: flex;
      align-items: center;
      gap: var(--e2);
      flex-wrap: wrap;
   }

   /* Dónde está inscrita. Lista y no párrafo: son recintos contables, y así el
   lector de pantalla los anuncia como los dos que son. */
   .ficha__inscripciones {
      margin: 0;
      padding-left: var(--e4);
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      font-size: var(--fs-dato);
   }

   /* Lo que va a pasar al guardar —«Queda inscrita en el Hospital de Curicó»—,
   dicho antes de pulsar. No es un aviso de error ni un resultado: no lleva
   recuadro, solo el tono de lo secundario. */
   .panel__nota {
      margin: 0;
      color: var(--md-on-surface-variant);
      font-size: var(--fs-dato);
   }

   /* --- Consentimiento ------------------------------------------------------ */

   /* El documento entero, a la vista y no detrás de un enlace: un
   consentimiento que el paciente no pudo leer no es un consentimiento, y el
   funcionario tiene que poder girarle la pantalla.

   `pre-wrap` porque el texto trae sus propios saltos de línea y su sangría —es
   un documento legal numerado, no prosa corrida— y reflowarlo lo vuelve
   ilegible. `overflow-y` con tope de alto para que las cuatro casillas y el
   botón queden a la vista sin tener que desplazar la página entera: lo que hay
   que decidir no puede quedar debajo de cuatro mil caracteres. */
   .consentimiento__texto {
      max-height: 40vh;
      overflow-y: auto;
      padding: var(--e3);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-low);
      color: var(--md-on-surface);
      font-size: var(--fs-dato);
      line-height: var(--lh-texto);
      white-space: pre-wrap;
      /* Es una región desplazable y lleva `tabindex="0"`: sin foco visible,
       quien navega con teclado no sabe que puede desplazarla. */
      overscroll-behavior: contain;
   }

   /* El editor del documento. `--lh-texto` y no la ajustada: acá se redacta
   prosa legal de cuatro mil caracteres, no se llena un campo de una línea.
   `resize: vertical` porque a lo ancho ya ocupa la hoja entera y dejar
   ensancharlo rompería la rejilla del panel. */
   .consentimiento__editor {
      width: 100%;
      min-height: 24em;
      resize: vertical;
      line-height: var(--lh-texto);
      white-space: pre-wrap;
   }

   .consentimiento__canales {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: var(--e3);
   }

   /* Los cuatro canales en la ficha, incluidos los rechazados. Un canal ausente
   y uno rechazado no son lo mismo —«no se le preguntó» contra «dijo que no»— y
   el mesón necesita distinguirlos para saber si queda algo que preguntar. */
   .ficha__canales {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-wrap: wrap;
      gap: var(--e1) var(--e3);
      font-size: var(--fs-dato);
   }

   /* Sin píldora: acá no hay doscientas filas que distinguir de un vistazo,
   son cuatro rótulos que ya se están leyendo (§4 de la skill). El ícono sigue
   estando —confirmado contra anulado— porque el color por sí solo no informa. */
   .canal {
      display: inline-flex;
      align-items: center;
      gap: var(--e1);
      white-space: nowrap;
   }

   .canal--si {
      color: var(--md-on-surface);
   }

   /* Rechazado se atenúa, no se tiñe de error: que el paciente no quiera
   WhatsApp no es una falla del sistema ni algo que haya que corregir. */
   .canal--no {
      color: var(--md-on-surface-variant);
      opacity: .7;
   }

   /* Mismo ajuste de tamaño que `.estado__figura` y mismo motivo: un ícono de
   `iconoEstado()` necesita más caja que el punto de `clip-path` de antes. */
   .canal__figura {
      width: 14px;
      height: 14px;
      flex: 0 0 auto;
   }

   .establecimiento__nombre {
      font-weight: 600;
      margin-right: var(--e2);
   }

   /* 🔴 NO lleva `display: flex`. Un `<td>` con `display: flex` deja de ser una
   celda de tabla: el navegador la envuelve en una celda anónima, pierde el
   `vertical-align: middle` de `.tabla td` y —con `align-items` en `stretch`—
   estira su contenido hasta el alto de la fila. En una fila de tres líneas
   (nombre, descripción y píldoras de estado) eso deja el botón convertido en
   una pastilla del alto de la fila.

   `width: 1%` es la manera de decirle a `table-layout: auto` «lo mínimo que
   quepa»: la columna se encoge a lo que miden sus botones y el ancho sobrante
   se lo quedan las columnas de datos, que es donde hace falta. */
   .acciones-celda {
      width: 1%;
      text-align: right;
      white-space: nowrap;
   }

   /* Separación entre botones cuando hay más de uno sin agrupar —el catálogo, por
   ejemplo—. Va como margen y no como `gap` justamente porque la celda no es un
   contenedor flex. */
   .acciones-celda>*+* {
      margin-left: var(--e1);
   }

   /* --- Menú de acciones ----------------------------------------------------
   La última celda de una tabla se llenaba de botones y esa columna crecía con
   cada funcionalidad nueva, empujando los datos fuera de cuadro. Con dos o más
   acciones se agrupan acá; con una sola se pinta el botón, porque esconder una
   acción única detrás de dos clics no ordena nada.

   El menú va `fixed` y colgado de <body>, no dentro de la celda: `.tabla-caja`
   desplaza en horizontal y recortaría contra su borde un menú posicionado
   adentro, y una fila del final lo abriría fuera de la caja. z-index 40 le gana
   al riel y a la barra (30); el modal no compite, porque <dialog> vive en la
   capa superior del navegador y está por encima de cualquier z-index. */
   .menu {
      position: fixed;
      z-index: 40;
      min-width: 176px;
      padding: var(--e1);
      display: flex;
      flex-direction: column;
      gap: 2px;
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      /* Mismo mecanismo de tinte que .hoja y .modal, con el nivel 2: flota sobre
       la tabla y por debajo del modal (nivel 3). Si los dos se leyeran igual,
       lo que exige una decisión se vería como lo que solo ofrece opciones.
       Misma base -lowest que .hoja y .modal: ver el comentario en .hoja. */
      background: light-dark(var(--md-surface-container-lowest),
            color-mix(in oklab,
               var(--md-primary) var(--md-elevation-tint-2),
               var(--md-surface-container-lowest)));
      box-shadow: var(--md-elevation-2);
      animation: menu-entra var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* Cae desde el disparador, no desde ninguna parte: la regla global de
   `prefers-reduced-motion` la desarma. */
   @keyframes menu-entra {
      from {
         opacity: 0;
         transform: translateY(-4px);
      }

      to {
         opacity: 1;
         transform: translateY(0);
      }
   }

   .menu__opcion {
      padding: var(--e2) var(--e3);
      font: inherit;
      font-size: 13px;
      text-align: left;
      white-space: nowrap;
      color: var(--md-on-surface);
      background: none;
      border: 0;
      border-radius: var(--md-shape-extra-small);
      cursor: pointer;
   }

   .menu__opcion:hover {
      background: var(--md-surface-container-high);
   }

   /* Sin retorno se tiñe distinto, y eso es una advertencia y no un control: lo
   que impide el accidente es la confirmación, no el color. */
   .menu__opcion--peligro {
      color: var(--md-error);
   }

   /* El disparador de dos o más acciones es solo el ícono de tres puntos: el
   texto «Acciones» nunca decía la acción y perdía la carrera de espacio de la
   columna angosta de siempre. El rótulo sigue ahí, en `aria-label` y `title`.

   32 px de lado y no los 40 ideales: es el piso del área táctil (WCAG 2.5.5
   AA), no el techo — la tabla es registro de TRABAJO (skill §3, plano y
   apretado) y 40 px por fila infla justo lo que esa densidad cuida. `padding:
   0` porque el tamaño ya lo fija el ancho/alto, no el relleno del texto que
   ya no lleva. */
   .menu__disparador {
      width: 32px;
      height: 32px;
      padding: 0;
      justify-content: center;
   }

   /* --- La nota de una pantalla ---------------------------------------------
   Lo que hay que saber antes de tocar nada, puesto desde que la pantalla se
   abre y con una cruz para sacarlo. Ver `ts/ui/nota.ts`: lo que se puede cerrar
   no puede ser lo que hace falta para operar bien.

   El fondo es un velo del rol primario sobre el lienzo del panel, y no un color
   propio: así el mismo 6 % da una caja apenas teñida en claro y una apenas
   iluminada en oscuro, sin dos declaraciones que alguien tenga que acordarse de
   mover juntas. Sin borde y con radio medio —es una zona del panel, no una
   pieza apoyada encima—: el borde la habría convertido en una tarjeta dentro de
   otra tarjeta. */
   .nota {
      display: flex;
      align-items: flex-start;
      gap: var(--e3);
      margin: 0 0 var(--e4);
      padding: var(--e3) var(--e4);
      border-radius: var(--md-shape-medium);
      background: color-mix(in oklab, var(--md-primary) 6%, var(--md-surface-container-lowest));
      /* Una hairline del mismo rol, no `--md-outline-variant`: la nota queda
       arriba de `.tabla-caja`, que sí lleva borde, y una caja teñida sin borde
       al lado de una con borde se lee como un descuido y no como otra cosa.
       Teñida y no neutra para que el borde sea el mismo gesto que el fondo, un
       escalón más marcado. */
      border: 1px solid color-mix(in oklab, var(--md-primary) 18%, transparent);
      /* --md-on-surface y no la variante: es texto que se lee de corrido,
       no el metadato de un campo. Con la variante sobre un fondo ya teñido, la
       caja entera se leía apagada —«esto no importa»— que es lo contrario de
       lo que hace ahí arriba. */
      color: var(--md-on-surface);
      /* --fs-dato y no --fs-meta: los 12 px son el peldaño de lo que CALIFICA a
       otra cosa —la ayuda de un campo, la cabecera de una tabla—, y esto no
       califica nada, se lee solo. */
      font-size: var(--fs-dato);
      line-height: var(--lh-texto);
      /* Para que el encogido del cierre recorte el texto en vez de dejarlo
       asomando fuera de una caja que ya mide cero. */
      overflow: hidden;
      transition:
         height var(--dur-efecto-normal) var(--resorte-efecto-normal),
         opacity var(--dur-efecto-rapido) linear,
         margin-bottom var(--dur-efecto-normal) var(--resorte-efecto-normal),
         padding-block var(--dur-efecto-normal) var(--resorte-efecto-normal);
   }

   /* Una nota ya cerrada en este navegador no deja su hueco. */
   .nota[hidden] {
      display: none;
   }

   /* Se va encogiendo y se lleva su propio espacio: si solo se desvaneciera,
   quedaría un hueco donde estuvo y la pantalla no se vería ordenada, que es
   justamente para lo que se cierra. */
   .nota--cerrando {
      height: 0;
      margin-bottom: 0;
      padding-block: 0;
      opacity: 0;
   }

   .nota__cuerpo {
      flex: 1 1 auto;
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      /* Una nota de dos renglones no se lee de lado a lado de una pantalla de
       27 pulgadas: el ojo pierde el renglón al volver. El tope es del TEXTO y
       no de la caja, y por eso la cruz se manda sola al extremo con
       `margin-left: auto` —si dependiera de este flexible, se quedaría pegada
       al final del párrafo, flotando en la mitad de una nota ancha—. */
      max-width: 78ch;
   }

   .nota__cuerpo>p {
      margin: 0;
   }

   /* Discreta: es la salida de algo que no molesta, no una acción de la
   pantalla. Cuadrada y chica, del tamaño del área que se pulsa sin apuntar. */
   .nota__cerrar {
      flex: 0 0 auto;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 28px;
      height: 28px;
      margin: -2px -6px 0 auto;
      padding: 0;
      color: var(--md-on-surface-variant);
      background: none;
      border: 0;
      border-radius: var(--md-shape-full);
      cursor: pointer;
      transition: background var(--dur-efecto-rapido) linear;
   }

   .nota__cerrar:hover {
      background: color-mix(in oklab, var(--md-on-surface) 8%, transparent);
   }

   .nota__cerrar .icono {
      width: 14px;
      height: 14px;
   }

   /* --- La pista -------------------------------------------------------------
   El texto de apoyo que solo aparece cuando se pide. Ver `ts/ui/pista.ts`: nació
   de la matriz, donde la descripción de cada permiso ocupaba dos renglones por
   fila en la única pantalla que se lee a lo ancho.

   🔴 El disparador NO se ve como un botón. Se ve como el texto que ya estaba,
   con un subrayado punteado que avisa que hay algo más: una columna de
   veinticinco códigos pintada como veinticinco botones es un muro de cajas, y
   ninguna de las veinticinco es la acción de la pantalla —la acción es marcar
   la casilla—. */
   .pista {
      padding: 0;
      font: inherit;
      color: inherit;
      background: none;
      border: 0;
      text-align: left;
      text-decoration: underline dotted var(--md-outline);
      text-underline-offset: 3px;
      /* `help` y no `pointer`: no lleva a ninguna parte, explica lo que ya está
       ahí. */
      cursor: help;
   }

   /* El permiso crítico tiñe su subrayado. La palabra «crítico» sigue estando
   —adentro de la caja— y esto solo dice que conviene mirarla. No lleva silueta
   propia: la silueta es obligatoria donde hay que distinguir un estado entre
   doscientas filas de un vistazo, y esto no es un estado, es una nota sobre un
   texto que se está leyendo (ver §4 de la skill `estilos-vinculo`). */
   .pista--critica {
      text-decoration-color: var(--md-error);
   }

   .pista__caja {
      position: fixed;
      /* Sobre el menú (40): una pista puede pedirse desde un menú abierto, y
       debajo del modal, que es el que exige una decisión. */
      z-index: 45;
      max-width: 320px;
      padding: var(--e2) var(--e3);
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      font-size: var(--fs-meta);
      /* --lh-texto y no la ajustada de la tabla: acá sí se lee de corrido. */
      line-height: var(--lh-texto);
      color: var(--md-on-surface);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      /* Mismo mecanismo de tinte y mismo nivel 2 que el menú: las dos flotan
       sobre la tabla. */
      background: light-dark(var(--md-surface-container-lowest),
            color-mix(in oklab,
               var(--md-primary) var(--md-elevation-tint-2),
               var(--md-surface-container-lowest)));
      box-shadow: var(--md-elevation-2);
      /* No intercepta el mouse: la caja se abre justo debajo del disparador y,
       si atrapara el cursor, taparía las casillas de las filas siguientes —que
       en esta pantalla es tapar justo lo que se venía a marcar—. */
      pointer-events: none;
      animation: pista-entra var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* Asoma desde el disparador, igual que el menú. La regla global de
   `prefers-reduced-motion` la desarma. */
   @keyframes pista-entra {
      from {
         opacity: 0;
         transform: translateY(-3px);
      }

      to {
         opacity: 1;
         transform: translateY(0);
      }
   }

   /* El título repite lo que dice el disparador, y solo va donde el disparador
   puede estar recortado —el nombre de un perfil de canto—. */
   .pista__titulo {
      margin: 0;
      font-weight: 600;
   }

   .pista__texto {
      margin: 0;
      color: var(--md-on-surface-variant);
   }

   /* Para el lector de pantalla y no para el ojo. Lo usa la matriz: donde el ojo
   ve una figura, el lector necesita la frase completa —«el módulo config, por
   superusuario»— y esa frase no puede ocupar la celda. No se usa `display:
   none` ni `hidden`: eso lo sacaría también del árbol de accesibilidad, que es
   justo lo contrario de lo que hace falta. */
   .visualmente-oculto {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
      border: 0;
   }

   /* «crítico» junto a un permiso: texto chico y teñido, sin píldora.

   La píldora con silueta es para el estado de un REGISTRO dentro de una lista
   —una cita confirmada o cancelada— donde hay que distinguir de un vistazo
   entre doscientas filas. Esto es otra cosa: una marca sobre un código que ya
   se está leyendo, y ahí la palabra ES la información. La píldora terminaba
   ocupando más que el código que calificaba.

   `--md-error` es un rol de primer plano y está calibrado para leerse sobre la
   superficie en los dos temas: en claro va oscuro sobre claro y en oscuro va
   claro sobre oscuro, sin tocar nada. */
   .critico {
      font-size: var(--fs-micro);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      white-space: nowrap;
      color: var(--md-error);
   }

   /* --- El aviso flotante ----------------------------------------------------
   Abajo a la derecha, para las pantallas que guardan solas: la casilla ya
   estaba marcada antes de que el servidor contestara, así que el aviso es lo
   único que separa «lo pulsé» de «quedó guardado».

   Los roles invertidos —`inverse-surface` sobre el fondo de siempre— son los
   que MD3 le da al snackbar, y acá hacen falta por lo mismo: un aviso del
   color de la superficie se confunde con una tarjeta más de la pantalla, y
   este tiene que encontrarse sin buscarlo. */
   .notificaciones {
      position: fixed;
      right: var(--e4);
      bottom: var(--e4);
      /* Sobre el riel (30) y sobre el menú de acciones (40). El modal no compite:
       <dialog> vive en la capa superior del navegador. */
      z-index: 45;
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      max-width: min(420px, calc(100vw - var(--e4) * 2));
      /* El contenedor ocupa la esquina entera pero no intercepta nada: sin esto
       taparía los botones que quedan debajo aunque no haya ningún aviso. */
      pointer-events: none;
   }

   .aviso {
      pointer-events: auto;
      display: flex;
      align-items: center;
      gap: var(--e3);
      padding: var(--e2) var(--e3);
      border-radius: var(--md-shape-small);
      background: var(--md-inverse-surface);
      color: var(--md-inverse-on-surface);
      box-shadow: var(--md-elevation-3);
      animation: aviso-entra var(--dur-espacial-rapido) var(--resorte-espacial-rapido);
   }

   /* Sube al entrar y baja al salir: viene de fuera de cuadro y vuelve ahí. La
   regla global de `prefers-reduced-motion` desarma las dos. */
   @keyframes aviso-entra {
      from {
         opacity: 0;
         transform: translateY(12px);
      }

      to {
         opacity: 1;
         transform: translateY(0);
      }
   }

   @keyframes aviso-sale {
      from {
         opacity: 1;
         transform: translateY(0);
      }

      to {
         opacity: 0;
         transform: translateY(12px);
      }
   }

   .aviso--saliendo {
      animation: aviso-sale var(--dur-efecto-rapido) var(--resorte-efecto-rapido) forwards;
   }

   /* Line-height explícito y no heredado: `.notificaciones` es `position: fixed` fuera
   del flujo normal, así que lo que hereda depende de dónde JS lo cuelgue —
   hoy da lo mismo que --lh-texto, pero un mensaje largo («No se pudo…» con
   el detalle de un campo) envuelve a dos líneas y ahí sí importa que la
   interlínea esté puesta acá y no confiada a la herencia. Prosa y no dato
   de tabla: se lee de corrido, así que es --lh-texto y no --lh-ajustada. */
   .aviso__texto {
      margin: 0;
      font-size: var(--fs-dato);
      line-height: var(--lh-texto);
   }

   /* La acción usa el primario invertido, que es el que MD3 calibra para leerse
   sobre `inverse-surface`: el --md-primary de siempre queda oscuro sobre este
   fondo y en oscuro casi desaparece. */
   .aviso__accion {
      flex: 0 0 auto;
      padding: var(--e1) var(--e2);
      border: 0;
      border-radius: var(--md-shape-full);
      background: none;
      font: inherit;
      font-size: var(--fs-dato);
      font-weight: 700;
      color: var(--md-inverse-primary);
      cursor: pointer;
   }

   .aviso__accion:hover {
      background: color-mix(in srgb, var(--md-inverse-primary) 14%, transparent);
   }

   .aviso__cerrar {
      flex: 0 0 auto;
      width: 24px;
      height: 24px;
      padding: 0;
      border: 0;
      border-radius: var(--md-shape-full);
      background: none;
      color: inherit;
      font: inherit;
      font-size: 18px;
      line-height: 1;
      cursor: pointer;
      opacity: .7;
   }

   .aviso__cerrar:hover {
      opacity: 1;
      background: color-mix(in srgb, var(--md-inverse-on-surface) 14%, transparent);
   }

   /* Un error no se va solo y se ve como error. No alcanza con el texto: quien
   está marcando casillas mira la grilla, no el rincón. */
   .aviso--error {
      background: var(--md-error-container);
      color: var(--md-on-error-container);
   }

   .aviso--error .aviso__accion {
      color: var(--md-on-error-container);
   }

   /* En angosto ocupa el ancho útil: a 400 px un aviso de 420 px se sale de
   cuadro, y arrinconarlo a la derecha lo dejaría partido. */
   @media (max-width: 899px) {
      .notificaciones {
         left: var(--e3);
         right: var(--e3);
         bottom: var(--e3);
         max-width: none;
      }
   }

   /* --- La matriz de perfiles × permisos -------------------------------------
   Una grilla ancha dentro de `.tabla-caja`, que ya desplaza en horizontal. */

   /* 🔴 La fila de perfiles tiene que quedarse quieta, y para eso el que
   desplaza en vertical tiene que ser el cajón y no la página.

   `position: sticky` se pega al scrollport más cercano, y el cajón ya lo era:
   `.tabla-caja` declara `overflow-x: auto` y eso deja el eje vertical en
   `auto` también. Solo que sin alto propio nunca desplazaba en vertical, así
   que el `top: 0` de `.tabla th` no tenía contra qué pegarse y los rótulos se
   iban con la página. Con el alto acotado, la cabecera y la primera columna
   —que ya era fija— sostienen las dos coordenadas de la grilla a la vez, que
   es lo único que impide repartir poder en la fila equivocada.

   Lo que se descuenta: el navbar, que es `sticky` y taparía lo de arriba, y el
   aire del lienzo. Sobra un poco a propósito —el navbar crece a ~71 px cuando
   la vista pone subtítulo— porque quedarse corto deja un hueco y pasarse
   esconde la última fila debajo del borde de la ventana.

   `dvh` y no `vh`: en el teléfono la barra del navegador se repliega al
   desplazar, y con `vh` el cajón mide siempre la ventana grande. */
   .matriz .tabla-caja {
      max-height: calc(100dvh - var(--alto-cab) - var(--e6) * 2);
   }

   /* Sin esto las columnas se reparten el ancho de la pantalla y quedan tan
   angostas que el nombre del perfil se parte en tres líneas. */
   .matriz__tabla {
      min-width: max-content;
   }

   /* Más apretada que cualquier otra tabla, y a propósito: es una grilla para
   comparar, no para leer. Lo que decide si sirve es cuántas columnas caben en
   el monitor de 1366 px del mesón, así que el relleno vertical baja a --e2 y el
   horizontal también —una columna de casillas no necesita 12 px a cada lado de
   un cuadro de 20—. Va después de `.tabla th/td` en el archivo y empata en
   especificidad: gana esta por el orden. */
   .matriz__tabla th,
   .matriz__tabla td {
      padding: var(--e2);
   }

   /* La primera columna se queda quieta mientras la grilla se desplaza. Sin ella,
   al llegar a la sexta columna ya no se sabe de qué permiso es la casilla que
   se está marcando — y acá marcar la fila equivocada es repartir poder que
   nadie pidió. El fondo es obligatorio: sin fondo, lo que pasa por debajo se
   ve a través de la columna fija. */
   .matriz__permiso,
   .matriz__esquina {
      position: sticky;
      left: 0;
      z-index: 2;
      /* 240px cuando la celda llevaba el código, su descripción completa y la
       marca «crítico». Con la descripción en la pista, lo que hay que
       acomodar es el código más largo del catálogo y nada más. */
      min-width: 168px;
      text-align: left;
   }

   /* El código de un permiso es un identificador en minúscula con puntos
   —`personas.anonimizar.confirmar`—, y `.tabla th` lo venía pasando a
   versalitas con interletrado: eso ensancha la columna fija que justamente
   queremos angosta, y de paso hace que dos códigos parecidos se distingan
   peor. Las versalitas son para los rótulos de la cabecera, no para el dato de
   la fila. */
   .matriz__permiso {
      text-transform: none;
      letter-spacing: normal;
   }

   .matriz__codigo {
      /* Al ras del texto principal: es el dato de la fila, no su metadato —lo
       que la cabecera y el rótulo de módulo sí son—. */
      color: var(--md-on-surface);
      font-weight: 600;
      font-size: var(--fs-meta);
   }

   /* 🔴 El fondo va en un selector propio, `.tabla th.matriz__…`, y NO en el
   bloque de arriba: `.matriz__permiso`/`.matriz__esquina` son `<th>` dentro
   de `<table class="tabla matriz__tabla">` (ver ts/routes/matriz.ts), así
   que también calzan con `.tabla th`. Esa regla tiene un tipo MÁS en el
   selector —especificidad (0,0,1,1) contra (0,0,1,0)— y le ganaba al
   `background` de acá arriba SIEMPRE, sin importar el orden en el archivo:
   la columna fija venía pintándose con el fondo de la cabecera
   (--md-surface-container) y no con el suyo propio. `.tabla th.…` iguala esa
   especificidad y de paso desempata a favor de esta regla por ir después en
   el archivo. Ese mismo hueco dejaba pasar `top: 0` —que `.tabla th` fija
   para que la fila de encabezado se pegue arriba— a estas celdas, que no lo
   declaraban y por eso no lo bloqueaban: cada `<th scope="row">` del cuerpo
   intentaba pegarse TAMBIÉN al techo del contenedor, no solo a la izquierda.
   `.matriz__esquina` sí quiere las dos cosas —cruza fila y columna fijas—, así
   que conserva `top: 0` de `.tabla th`; `.matriz__permiso` no, y lo anula. */
   .tabla th.matriz__permiso {
      /* -lowest: el mismo tono que el resto de la fila (ver `.tabla-caja`), para
       que la columna fija no se note como una tira de otro color al
       desplazar en horizontal. */
      background: var(--md-surface-container-lowest);
      top: auto;
   }

   .tabla th.matriz__esquina {
      /* -low: la esquina cruza con la fila de cabecera y tiene que verse como
       parte de ella, no de las filas de datos — mismo tono que `.tabla th`. */
      background: var(--md-surface-container-low);
   }

   /* 🔴 El orden de apilamiento de la grilla, en un solo lugar porque acá hay
   cuatro cosas pegadas a la vez y el empate se resuelve mal solo:

      1  el rótulo de módulo   — pegado a la izquierda
      2  la columna de permisos — pegada a la izquierda
      3  la fila de perfiles    — pegada arriba
      4  la esquina             — pegada a las dos

   Sin el 3 explícito, el rótulo de módulo se dibujaba SOBRE los nombres de los
   perfiles al desplazar hacia abajo: los dos son `sticky` —o sea posicionados—
   y ninguno declaraba z-index, y entre posicionados empatados gana el que
   aparece después en el documento, que es el rótulo por estar en el cuerpo.
   Que el nombre de un módulo tape la fila que dice de quién es cada columna es
   perder la mitad de las coordenadas justo mientras se reparte poder. */
   .matriz__tabla thead th {
      z-index: 3;
   }

   /* La esquina cruza las dos fijaciones, así que va sobre las dos. Y se apoya
   abajo: con los rótulos de canto la fila de cabecera mide 9.5 rem, y
   «Permiso» flotando en el medio de esa franja no se lee como el título de la
   columna que tiene debajo. */
   .matriz__esquina {
      z-index: 4;
      vertical-align: bottom;
   }

   /* El rótulo del perfil va de canto. Horizontal, cada columna medía lo que
   mide el nombre más largo —«Jefatura de agenda SOME» son 160 px— y seis
   perfiles ya no cabían en el monitor del mesón; de canto, la columna mide lo
   que mide una casilla y entran veinte. Es el único lugar del sistema donde el
   texto se gira, y se justifica porque acá el eje del contenido es el
   horizontal: la pregunta de esta pantalla es «¿quién tiene esto?» y se
   responde mirando a lo ancho. */
   .matriz__perfil {
      min-width: 0;
      padding-left: var(--e1);
      padding-right: var(--e1);
      text-align: left;
      vertical-align: bottom;
      /* Un nombre propio no va en versalitas: «SOME» y «Some» dejarían de
       distinguirse, y el interletrado de la cabecera alarga de gratis un
       rótulo que ahora se mide a lo alto. */
      text-transform: none;
      letter-spacing: normal;
   }

   .matriz__giro {
      /* `vertical-rl` más media vuelta: el texto se lee de abajo hacia arriba,
       que es como se leen los lomos de los libros y hacia donde la cabeza se
       inclina sin pensarlo. `vertical-lr` sin girar se lee de arriba hacia
       abajo y obliga a inclinarla al otro lado. */
      writing-mode: vertical-rl;
      transform: rotate(180deg);
      /* En escritura vertical el eje de bloque corre en horizontal, así que
       `column` pone el nombre y su alcance como dos líneas PARALELAS, una al
       lado de la otra. Con `row` quedarían una detrás de la otra y la cabecera
       mediría el doble de alto. */
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: var(--e1);
      /* Alto fijo y no automático: sin él, la cabecera mide lo que mide el
       nombre más largo y la fila de rótulos crece cada vez que alguien crea un
       perfil con nombre largo. Lo que no entra se corta con puntos suspensivos
       y se lee completo en la pista. */
      height: 9.5rem;
   }

   .matriz__perfil-nombre,
   .matriz__perfil-alcance {
      max-height: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
   }

   .matriz__perfil-nombre {
      color: var(--md-on-surface);
      font-weight: 600;
   }

   .matriz__perfil-alcance {
      color: var(--md-on-surface-variant);
      font-size: var(--fs-micro);
   }

   .matriz__celda {
      text-align: center;
   }

   /* El rótulo del módulo agrupa las filas de abajo, igual que el del riel y que
   el `<legend>` de un grupo de campos: mismo micro rótulo en versalitas. */
   .matriz__grupo-titulo {
      position: sticky;
      left: 0;
      /* Peldaño 1 de la escalera de arriba: sobre las celdas de datos, que no
       están posicionadas, y debajo de todo lo demás que se queda quieto. */
      z-index: 1;
      text-align: left;
      font-size: var(--fs-micro);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      color: var(--md-on-surface-variant);
   }

   /* Mismo bug de especificidad que `.matriz__permiso` —esta fila también es un
   `<th>` (`scope="colgroup"`) dentro de `.tabla`— y mismo arreglo. Tampoco es
   la cabecera fija: es un separador de módulo DENTRO del cuerpo, así que
   tampoco quiere pegarse arriba. El tono es el de la cabecera (-low): es un
   rótulo de sección, no una fila de datos. */
   .tabla th.matriz__grupo-titulo {
      background: var(--md-surface-container-low);
      top: auto;
   }

   /* Lo que no sale de `perfil_permiso` —el comodín, el `config` de un
   superusuario— no lleva casilla: lleva sello. Que se distinga a simple vista
   de una casilla marcada es justamente el punto, porque son dos cosas
   distintas: «se lo dieron» y «lo trae por lo que es».

   Antes era uno de los íconos de estado de `iconoEstado()`, y eso estaba mal
   de origen: ese vocabulario es para los ESTADOS de un registro —confirmado,
   revocado, pendiente— y uno de esos íconos suelto en una celda vacía se leía
   como una casilla trabada, que es justo lo contrario de lo que pasa ahí.

   Mide los mismos 20 px que la casilla para que la columna del comodín no sea
   más angosta que las demás, pero es redondo y va en relleno —no en trazo
   sobre el lienzo—: a este tamaño, redondo contra cuadrado es lo que separa
   dos objetos sin tener que mirarlos dos veces. Y va en el rol terciario, que
   en este sistema no lo usa ningún control: nada que se pueda pulsar es de ese
   color, así que el sello no se lee como algo que responda al clic. */
   .matriz__traido {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 20px;
      height: 20px;
      border-radius: var(--md-shape-full);
      color: var(--md-on-tertiary-container);
      background: var(--md-tertiary-container);
   }

   .matriz__sello {
      width: 12px;
      height: 12px;
      /* Más grueso que el 1,8 del resto de los íconos: al reducir el dibujo de
       24 a 12 px el trazo se va a la mitad y el asterisco queda como una mancha
       gris. Los íconos del riel se dibujan a 20 px, este no. */
      stroke-width: 2.6;
   }


   /* ===========================================================================
   Panel de edición · presenta UN registro, por eso lleva .hoja
   =========================================================================== */

   /* El panel ya no necesita reglas propias: `.desplegable` le da el plegado y el
   espacio de abajo. La regla `:empty` que lo escondía sobraba desde que el
   contenedor conserva su caja interior aunque esté cerrado. */

   .panel__titulo {
      margin: 0 0 var(--e4);
      font-size: var(--fs-titulo);
      font-weight: 700;
   }

   /* La espera adentro del panel. No usa `.cargando`, que lleva `min-height: 60vh`
   para ocupar la pantalla: acá abriría el panel a media pantalla para encogerlo
   de golpe al llegar los datos. */
   .panel__espera {
      margin: 0;
      color: var(--md-on-surface-variant);
   }

   .panel__form {
      display: flex;
      flex-direction: column;
      gap: var(--e4);
   }

   /* `auto-fit` con un mínimo: a 400 px cae a una columna sola sin necesidad de
   una media query, y en un monitor ancho no estira los campos a lo absurdo. */
   .panel__rejilla {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: var(--e3) var(--e4);
   }

   /* Una acción y lo que produce, apiladas. En la rejilla de campos las dos eran
   celdas de la MISMA fila: el botón crecía hasta el alto del aviso —tres líneas
   de alto para un rótulo de una— y el resultado aparecía al costado, que no es
   donde se lo busca después de pulsar. Lo que se emite va debajo de lo que lo
   emitió. */
   .panel__pila {
      display: flex;
      flex-direction: column;
      gap: var(--e3);
   }

   /* El botón conserva su ancho natural; el resultado ocupa el ancho completo. */
   .panel__pila>.boton {
      align-self: flex-start;
   }

   /* Cuando lo apilado son bloques con subtítulo propio y no una acción con su
   respuesta, el paso de --e3 a --e5 es lo que hace que se lean como tres
   partes y no como una lista larga. */
   .panel__pila--aire {
      gap: var(--e5);
   }

   /* El rótulo de cada bloque dentro de un grupo. NO repite las versalitas del
   `<legend>`: ese es el nivel del grupo entero, y dos niveles con la misma voz
   dejan de ser dos niveles. */
   .panel__subtitulo {
      margin: 0 0 var(--e2);
      font-size: var(--fs-dato);
      font-weight: 700;
      color: var(--md-on-surface);
   }

   .panel__grupo {
      margin: 0;
      padding: var(--e4);
      border: 1px solid var(--md-outline-variant);
      /* El panel vive sobre .hoja (forma plena), pero esto agrupa CAMPOS
       adentro de un formulario, no es el contenedor: mismo criterio que las
       franjas de aviso — un elemento contenido dentro de una pieza de
       acogida va al escalón de un campo, no al de la pieza que lo aloja. */
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container);
   }

   /* Un grupo colgado directamente del panel —«Acceso» y «Asignaciones de perfil»,
   que van después del formulario— necesita separarse de lo de arriba. El
   selector es hijo directo a propósito: los grupos de «Parámetros» viven dentro
   de un `.panel__form`, que ya los separa con su `gap`, y ahí este margen se
   sumaría al hueco en vez de reemplazarlo. */
   .panel>.panel__grupo {
      margin-top: var(--e5);
   }

   .panel__grupo-titulo {
      padding: 0 var(--e2);
      font-size: var(--fs-meta);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .06em;
      color: var(--md-on-surface-variant);
   }

   .panel__grupo-ayuda {
      margin: 0 0 var(--e3);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
   }

   /* Dos líneas en una celda: el nombre y, debajo, qué es. En perfiles y en el
   catálogo de permisos el código solo no alcanza —«¿qué hace
   `mensajes.plantillas`?»— y esa segunda línea es justo lo que se lee cuando
   alguien está decidiendo a quién darle qué. */
   .perfil__nombre {
      display: block;
      font-weight: 600;
   }

   /* Quiénes tienen puesto un perfil. Lista y no párrafo: son nombres contables, y
   así el lector de pantalla los anuncia como los cuatro que son. */
   .panel__lista {
      margin: 0;
      padding-left: var(--e4);
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      font-size: var(--fs-dato);
   }

   /* El grupo que se pliega —«lo tienen 14 funcionarios»— usa el mismo recuadro
   que los demás grupos del panel; lo único propio es que el puntero avise que
   se abre antes de que alguien lo pulse. */
   details.panel__grupo>summary {
      cursor: pointer;
   }


   .panel__acciones {
      display: flex;
      gap: var(--e2);
      flex-wrap: wrap;
      align-items: center;
   }

   /* Un control en una fila de acciones —el enlace de clave junto a su botón de
   copiar— se queda con el ancho sobrante en vez de repartirlo a medias: el
   enlace es largo y lo que hay que poder leer entero es él, no el rótulo del
   botón. Por debajo de 240 px la fila se parte sola (`flex-wrap`). */
   .panel__acciones>.campo__control {
      flex: 1 1 240px;
      min-width: 0;
   }

   .panel__aviso {
      margin: 0;
      /* `flex` porque el aviso puede llevar un botón adentro: el 409 del
       identificador repetido ofrece «Abrir la ficha que ya existe», que es lo
       que impide que el funcionario, sin salida, termine creando la segunda
       ficha del mismo paciente por otra puerta. Con solo texto —que es el resto
       de los casos— se comporta igual que antes. */
      display: flex;
      align-items: center;
      gap: var(--e3);
      flex-wrap: wrap;
      padding: var(--e2) var(--e3);
      border: 1px solid var(--md-error);
      /* Misma franja contenida que .modal__aviso/.ingreso__aviso/.inicio__nota. */
      border-radius: var(--md-shape-small);
      background: var(--md-error-container);
      color: var(--md-on-error-container);
      font-size: var(--fs-dato);
   }

   .panel__aviso--ok {
      border-color: var(--md-success);
      background: var(--md-success-container);
      color: var(--md-on-success-container);
   }

   /* ===========================================================================
   El organigrama de establecimientos · lienzo con pan y zoom

   Tarjetas posicionadas por un layout de árbol prolijo (JS, `calcularLayout()`
   en `ts/routes/establecimientos.ts`: ancho de subárbol de abajo hacia arriba,
   centrado del padre sobre sus hijos de arriba hacia abajo — sin posiciones
   guardadas, se recalcula en cada carga). El detalle ya no es la segunda
   columna: es `ts/ui/panelLateral.ts`, que flota encima sin desplazar nada.

   El pan y el zoom continuos (arrastrar el fondo, rueda+Ctrl, pellizco) NO
   llevan transición: la mano tiene que sentir que el lienzo responde al mismo
   ritmo del gesto, no una fracción de segundo después. La clase
   `--animado` la agrega JS solo para los movimientos DISCRETOS —los botones de
   zoom, «Ajustar a la pantalla», centrar un nodo recién seleccionado— y para
   el reacomodo de las tarjetas tras confirmar un «mover». Es la misma
   distinción del §8 de la skill, aplicada a un lienzo en vez de a una lista.
   =========================================================================== */

   /* Ya no es una grilla de dos columnas: el detalle salió al panel lateral
   (`ts/ui/panelLateral.ts`), que flota encima, así que el organigrama recibe
   TODO el ancho de la vista — el pedido explícito de «más espacio». */
   .arbol-establecimientos {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
   }

   /* --- La barra de herramientas ----------------------------------------------
   `role="toolbar"`: un solo control en la secuencia de Tab (tabindex en
   rotación, lo administra JS) y las flechas navegan adentro — WAI-ARIA
   toolbar pattern. Fija arriba del lienzo, siempre visible: expandir/contraer
   todo, zoom, centrar, buscar y los dos accesos que antes vivían sueltos
   (tipos de establecimiento, nuevo establecimiento) quedan todos en un lugar. */
   .organigrama__barra {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: var(--e1);
      padding: var(--e2);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-medium);
      background: var(--md-surface-container-lowest);
   }

   .organigrama__separador {
      width: 1px;
      align-self: stretch;
      min-height: 24px;
      background: var(--md-outline-variant);
   }

   .organigrama__accion {
      display: inline-flex;
      align-items: center;
      gap: var(--e1);
      padding: var(--e1) var(--e2);
      border: 0;
      border-radius: var(--md-shape-small);
      background: transparent;
      color: var(--md-on-surface);
      font-size: var(--fs-meta);
      white-space: nowrap;
      cursor: pointer;
   }

   .organigrama__accion:hover {
      background: var(--armazon-roce);
   }

   .organigrama__accion:disabled {
      opacity: .4;
      cursor: default;
      background: transparent;
   }

   .organigrama__accion .icono {
      width: 18px;
      height: 18px;
      flex: 0 0 auto;
   }

   /* Los dos glifos de zoom son texto, no ícono: un «−»/«+» se lee más rápido
   que dibujarlo a trazos, y son los botones que más se repiten. */
   .organigrama__accion--zoom {
      width: 32px;
      justify-content: center;
      font-size: var(--fs-titulo);
      line-height: 1;
   }

   .organigrama__zoom-porcentaje {
      min-width: 3.5ch;
      text-align: center;
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      font-variant-numeric: tabular-nums;
   }

   /* --- Buscar --------------------------------------------------------------
   Combobox con `aria-activedescendant`: el foco se queda en el campo de texto
   todo el tiempo —nunca salta a una opción— y lo que cambia es cuál opción se
   marca como activa. Es lo que deja escribir sin que una tecla de más mande el
   foco a otra parte. */
   .organigrama__buscar {
      position: relative;
      display: inline-flex;
   }

   .organigrama__buscar-input {
      width: 200px;
   }

   .organigrama__buscar-lista {
      position: absolute;
      top: calc(100% + var(--e1));
      left: 0;
      z-index: 20;
      width: max(240px, 100%);
      max-height: 320px;
      margin: 0;
      padding: var(--e1);
      overflow-y: auto;
      list-style: none;
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-lowest);
      box-shadow: var(--md-elevation-1);
   }

   .organigrama__buscar-opcion {
      padding: var(--e1) var(--e2);
      border-radius: var(--md-shape-small);
      font-size: var(--fs-dato);
      cursor: pointer;
   }

   .organigrama__buscar-opcion:hover,
   .organigrama__buscar-opcion--activa {
      background: var(--md-primary-container);
      color: var(--md-on-primary-container);
   }

   .organigrama__buscar-vacio {
      padding: var(--e1) var(--e2);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-dato);
   }

   .organigrama__contador {
      margin-left: auto;
      padding-left: var(--e2);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      white-space: nowrap;
   }

   /* En angosto, los botones de menor uso quedan solo con ícono — «Expandir
   todo», «Contraer todo» y el campo de búsqueda, que es lo que más se usa,
   conservan su rótulo siempre. `aria-label` y `title` no dependen de esto: ya
   están puestos en el botón completo. */
   @media (max-width: 899px) {
      .organigrama__accion--colapsable .organigrama__accion-texto {
         display: none;
      }

      .organigrama__buscar-input {
         width: 140px;
      }

      .organigrama__contador {
         display: none;
      }
   }

   .organigrama {
      position: relative;
      overflow: hidden;
      /* Mismo tratamiento que `.tabla-caja`: cajón de trabajo, no hoja. */
      background: var(--md-surface-container-lowest);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-medium);
      /* Alto generoso: un organigrama de un puñado de tarjetas en una caja de
       200px de alto no deja ver la forma del árbol. */
      height: calc(100dvh - var(--alto-cab) - var(--e6) * 2);
      /* El fondo es lo que se arrastra para desplazar: sin esto, un pellizco de
       dos dedos también intentaría hacer scroll de la página por debajo. */
      touch-action: none;
      cursor: grab;
   }

   .organigrama--arrastrando {
      cursor: grabbing;
   }

   @media (max-width: 899px) {
      .organigrama {
         height: calc(100dvh - var(--alto-cab) - var(--e5) * 2);
      }
   }

   .organigrama__lienzo {
      position: absolute;
      top: 0;
      left: 0;
      /* El origen en la esquina, no al centro: con `translate() scale()` en ese
       orden, el contenido crece hacia abajo-derecha y la conversión entre
       coordenadas de pantalla y coordenadas de contenido es una resta y una
       división, sin tener que descontar el centro del lienzo en cada cuenta. */
      transform-origin: 0 0;
   }

   .organigrama__lienzo--animado {
      transition: transform var(--dur-espacial-normal) var(--resorte-espacial-normal);
   }

   .organigrama__svg {
      position: absolute;
      top: 0;
      left: 0;
      /* Sin límite propio: crece con el `viewBox` que le fija JS al tamaño del
       árbol calculado. */
      overflow: visible;
      pointer-events: none;
   }

   .organigrama__linea {
      fill: none;
      stroke: var(--md-outline-variant);
      stroke-width: 2;
      /* Compatibilidad: donde el navegador no anima el atributo `d`, la línea
       salta en vez de acompañar — degrada a un cambio instantáneo, no rompe
       nada. Donde sí lo anima (motores actuales), se mueve junto a las
       tarjetas. */
      transition: d var(--dur-espacial-normal) var(--resorte-espacial-normal),
                  stroke var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   /* La arista del nodo que se está arrastrando, hacia su padre actual: se
   destaca para no perderla de vista mientras se decide un destino nuevo. */
   .organigrama__linea--arrastrando {
      stroke: var(--md-primary);
      stroke-dasharray: 6 4;
   }

   /* La línea provisoria hacia el destino bajo el puntero. Sin `transition`
   propia en `d`: aparece y sigue al destino en cada `pointermove`, y esos
   pasos son demasiado seguidos como para que una animación no se sienta como
   un rezago. */
   .organigrama__linea-provisoria {
      fill: none;
      stroke: var(--md-primary);
      stroke-width: 2;
      stroke-dasharray: 6 4;
   }

   .organigrama__capa-nodos {
      position: absolute;
      top: 0;
      left: 0;
   }

   /* La tarjeta. Posición absoluta con `left`/`top` en coordenadas de
   contenido — JS los escribe en cada layout— y transición en esas dos
   propiedades: es lo que anima el reacomodo tras un «mover» confirmado, sin
   agregar la clase `--animado` del lienzo (esa es para el propio lienzo, esto
   es para cada tarjeta y siempre puede animar: no compite con el pan de 1 a 1,
   que vive en el contenedor, no en la tarjeta).

   🔴 `height` fijo, NO `min-height`, con `box-sizing: border-box`. El layout
   (`calcularLayout()` en TS) reserva el espacio entre niveles asumiendo que
   TODAS las tarjetas miden `--organigrama-alto-tarjeta`, y la línea que las
   une parte exactamente de `top + ese alto` (el borde inferior real, borde de
   1px incluido porque `border-box` lo mete adentro de la medida). Con alto
   variable —el de antes— una tarjeta más baja que el resto dejaba un hueco
   entre su borde y el arranque de la línea, y una más alta tapaba el tramo:
   el bug que reportó Rafael. Si el contenido no entra en ese alto, se sube la
   variable — la tarjeta no puede crecer sola sin desincronizarse del layout.
   `overflow: hidden` de respaldo, por si algo igual se pasa por un pelo. */
   .organigrama__nodo {
      position: absolute;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: var(--e1);
      width: var(--organigrama-ancho-tarjeta, 220px);
      height: var(--organigrama-alto-tarjeta, 112px);
      overflow: hidden;
      padding: var(--e2) var(--e3);
      background: var(--md-surface-container-low);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      box-shadow: var(--md-elevation-1);
      cursor: pointer;
      touch-action: none;
      transition: left var(--dur-espacial-normal) var(--resorte-espacial-normal),
                  top var(--dur-espacial-normal) var(--resorte-espacial-normal),
                  opacity var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .organigrama__nodo:hover {
      border-color: var(--md-outline);
   }

   .organigrama__nodo--contexto {
      /* Ancestro fuera del alcance: se ve para que el árbol se lea completo,
       pero no se puede tocar ni arrastrar. Misma señal que `[disabled]`. */
      opacity: .55;
      cursor: default;
      touch-action: auto;
   }

   /* Mismo criterio que `.tabla tbody tr[aria-current]` y el `.arbol__nodo`
   anterior: es el mismo marcador y tiene que leerse igual en toda la
   aplicación. Sin transición de color a propósito — es estado, no movimiento. */
   .organigrama__nodo[aria-selected="true"] {
      background: var(--md-primary-container);
      border-color: var(--md-primary);
      color: var(--md-on-primary-container);
      box-shadow: var(--md-elevation-1), inset 0 0 0 2px var(--md-primary);
   }

   .organigrama__nodo--origen {
      opacity: .5;
   }

   .organigrama__nodo--invalido {
      opacity: .3;
      cursor: not-allowed;
   }

   .organigrama__nodo--destino {
      outline: 2px dashed var(--md-primary);
      outline-offset: 2px;
      background: var(--md-primary-container);
      color: var(--md-on-primary-container);
   }

   .organigrama__cabecera {
      display: flex;
      align-items: flex-start;
      gap: var(--e2);
   }

   .organigrama__disclosure {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 22px;
      height: 22px;
      flex: 0 0 auto;
      border: 0;
      border-radius: var(--md-shape-full);
      background: transparent;
      color: var(--md-on-surface-variant);
      cursor: pointer;
   }

   .organigrama__disclosure:hover {
      background: var(--armazon-roce);
   }

   /* Mismo triángulo que el disclosure de una lista: control de plegado, no
   silueta de estado, así que no reutiliza `.estado__figura` (skill §4). */
   .organigrama__disclosure::before {
      content: '';
      width: 8px;
      height: 8px;
      background: currentColor;
      clip-path: polygon(0 0, 100% 50%, 0 100%);
      transition: transform var(--dur-efecto-rapido) var(--resorte-efecto-rapido);
   }

   .organigrama__disclosure[aria-expanded="true"]::before {
      transform: rotate(90deg);
   }

   /* Dos líneas y corta con «…»: una sola se comía nombres largos de golpe —
   «Complejo Asistencial Dr. Sótero del Río»— y el `title` de abajo no ayuda a
   quien no puede posar el mouse. `-webkit-line-clamp` es el único mecanismo
   multiplataforma para cortar texto a N líneas sin medir alturas a mano; el
   prefijo sigue siendo necesario en 2026 en todos los motores, no solo WebKit. */
   .organigrama__nombre {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      overflow: hidden;
      font-weight: 600;
      line-height: var(--lh-ajustada);
   }

   /* Las marcas (contratante/no atiende/desactivado) y la insignia de rama
   plegada, todas en la MISMA fila — antes eran dos filas separadas, y con
   alto fijo eso era exactamente lo que hacía falta más espacio del que el
   layout reservaba. Una sola línea, sin envolver: con alto fijo no hay dónde
   poner una segunda. */
   .organigrama__estados {
      display: flex;
      flex-wrap: nowrap;
      overflow: hidden;
      gap: var(--e1);
   }

   .organigrama__meta {
      display: flex;
      align-items: center;
      gap: var(--e2);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
   }

   .organigrama__tipo {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      min-width: 0;
      flex: 1 1 auto;
   }

   .organigrama__conteos {
      flex: 0 0 auto;
      white-space: nowrap;
   }

   .organigrama__acciones {
      align-self: flex-start;
   }

   /* La insignia de rama plegada: cuántos descendientes esconde. Un número
   solo, sin figura de estado — no es un estado de contactabilidad, es una
   cuenta, y ya tiene su propio color de acento (`tertiary`, el mismo rol que
   usa el resto de la interfaz para un acento de apoyo que no es un estado de
   dominio). */
   .organigrama__plegado {
      align-self: flex-start;
      padding: 0 var(--e2);
      border-radius: var(--md-shape-full);
      background: var(--md-tertiary-container);
      color: var(--md-on-tertiary-container);
      font-size: var(--fs-meta);
      font-weight: 600;
      white-space: nowrap;
   }

   /* El rótulo que sigue al puntero mientras se arrastra una tarjeta. Mismo
   mecanismo que antes: `fixed`, fuera del lienzo transformado —en coordenadas
   de pantalla, no de contenido—, y `pointer-events: none` para no robarle el
   `elementFromPoint` al propio arrastre. */
   .organigrama__fantasma {
      position: fixed;
      z-index: 50;
      top: 0;
      left: 0;
      transform: translate(-50%, -140%);
      padding: var(--e1) var(--e3);
      border-radius: var(--md-shape-small);
      background: var(--md-inverse-surface);
      color: var(--md-inverse-on-surface);
      box-shadow: var(--md-elevation-3);
      font-size: var(--fs-dato);
      white-space: nowrap;
      pointer-events: none;
   }

   /* La franja para soltar en la raíz: anclada a la pantalla y no al lienzo
   —tiene que verse sin importar a dónde se haya paneado— y solo visible
   mientras se arrastra y el actor es comodín (`hidden` lo pone JS). */
   .organigrama__raiz-destino {
      position: absolute;
      top: var(--e3);
      left: 50%;
      transform: translateX(-50%);
      z-index: 15;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: var(--e2) var(--e4);
      border: 2px dashed var(--md-outline-variant);
      border-radius: var(--md-shape-medium);
      background: var(--md-surface-container-lowest);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-dato);
      pointer-events: none;
      text-align: center;
   }

   .organigrama__raiz-destino--destino {
      border-color: var(--md-primary);
      background: var(--md-primary-container);
      color: var(--md-on-primary-container);
   }

   /* Los controles de zoom ya no flotan sobre el lienzo: viven en
   `.organigrama__barra`, arriba de todo, junto con el resto de los controles
   generales — `.organigrama__accion`/`.organigrama__accion--zoom`. */

   /* --- La ficha flotante --------------------------------------------------
   Al posar o al enfocar una tarjeta, igual que `ts/ui/pista.ts` en su mecánica
   —demora al posar, sin demora al enfocar, una sola a la vez, se cierra con
   Escape o al desplazar—, pero sin reutilizar ese módulo: ahí el disparador ES
   el texto de apoyo; acá el disparador es la tarjeta completa, ya interactiva
   por su cuenta (se selecciona, se arrastra, tiene sus propios botones), y
   forzarla a ser el `<button class="pista">` de ese módulo le quitaría el rol
   `treeitem` que necesita para el árbol accesible. */
   .organigrama__ficha {
      position: fixed;
      z-index: 46;
      width: min(280px, calc(100vw - var(--e4) * 2));
      padding: var(--e3);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-medium);
      background: var(--md-surface-container-lowest);
      box-shadow: var(--md-elevation-3);
      font-size: var(--fs-dato);
      pointer-events: none;
   }

   .organigrama__ficha dt {
      margin-top: var(--e2);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
   }

   .organigrama__ficha dt:first-child {
      margin-top: 0;
   }

   .organigrama__ficha dd {
      margin: 0;
   }

   /* ===========================================================================
   Citas: la agenda, el filtro de casillas y «Nueva cita»

   Registro de trabajo como `.tabla`/`.tabla-caja`: la agenda es la lista que
   más se mira en un turno y no lleva relleno de acogida (skill §3). El panel
   de «Nueva cita»/«Reagendar»/«Cancelar» sí es `.hoja` (`panel hoja`, ya
   puesto desde TypeScript): presenta UN registro, igual que la ficha de
   `personas.ts`.
   =========================================================================== */

   /* Fecha, navegación y el interruptor día/semana en una sola fila que se
   quiebra en angosto — los tres grupos no caben juntos bajo 480px. */
   .citas__barra {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--e3);
      flex-wrap: wrap;
      margin-bottom: var(--e3);
   }

   .citas__nav {
      display: flex;
      align-items: center;
      gap: var(--e2);
      flex-wrap: wrap;
   }

   /* «miércoles 1 de octubre»: Day.js en español devuelve el día de la semana
   en minúscula porque así se escribe en medio de una frase. Acá es un
   encabezado suelto, y sin mayúscula inicial se lee como un error de
   tipeo — `::first-letter` evita tocar el string en TypeScript solo por eso. */
   .citas__fecha-actual {
      font-weight: 600;
      white-space: nowrap;
   }

   .citas__fecha-actual::first-letter {
      text-transform: uppercase;
   }

   /* El interruptor día/semana: dos botones que se leen como UN control con
   dos posiciones —comparten un borde exterior— y no como dos acciones
   sueltas. El que está activo se pinta con el color de interfaz, el mismo que
   ya usa cualquier otro control seleccionado del sistema. */
   .citas__vista-toggle {
      display: inline-flex;
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      overflow: hidden;
   }

   .citas__vista-boton {
      padding: var(--e1) var(--e3);
      border: none;
      background: transparent;
      color: var(--md-on-surface-variant);
      font: inherit;
      font-size: var(--fs-dato);
      font-weight: 600;
      cursor: pointer;
   }

   .citas__vista-boton+.citas__vista-boton {
      border-left: 1px solid var(--md-outline-variant);
   }

   .citas__vista-boton[aria-pressed="true"] {
      background: var(--md-primary-container);
      color: var(--md-on-primary-container);
   }

   /* El filtro de casillas que pidió Rafael para el usuario de la Dirección:
   uno o más grupos («Estados», «Establecimientos») en fila, cada uno con su
   etiqueta y sus casillas. Reusa `.lista__filtro` —la misma que ya usan
   `usuarios.ts` y `permisos.ts`— para que marcar y desmarcar se vea y se
   sienta igual en toda la aplicación, y no una variante más por pantalla. */
   .citas__filtros {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      margin-bottom: var(--e3);
   }

   .citas__filtro-grupo {
      display: flex;
      align-items: center;
      gap: var(--e3);
      flex-wrap: wrap;
   }

   .citas__filtro-etiqueta {
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: .04em;
   }

   /* La píldora de estado y, si corresponde, la insignia «Fonendo» al lado:
   comparten fila mientras quepan y saltan a la de abajo en una columna
   angosta, en vez de recortarse. */
   .citas__estado-celda {
      display: flex;
      align-items: center;
      gap: var(--e1);
      flex-wrap: wrap;
   }

   /* --- Nueva cita: buscar paciente ------------------------------------------- */

   .citas__resultados {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      margin-top: var(--e2);
   }

   /* Cada resultado es un `<button>` entero y no una fila angosta: en el
   mesón ese primer clic tiene que acertar rápido, y más área es menos
   reintentos. */
   .citas__resultado {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 2px;
      padding: var(--e2) var(--e3);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-lowest);
      color: var(--md-on-surface);
      font: inherit;
      text-align: left;
      cursor: pointer;
   }

   .citas__resultado:hover {
      background: var(--md-surface-container);
   }

   /* El paciente ya elegido, con el botón para volver a buscar. Misma fila
   flexible que `.ficha__valor`: el nombre y el identificador no se cortan en
   una pantalla angosta, se apilan. */
   .citas__paciente-elegido {
      display: flex;
      align-items: center;
      gap: var(--e2);
      flex-wrap: wrap;
      margin-bottom: var(--e3);
   }

   /* --- El combobox de especialidad -------------------------------------------
   Mismo patrón que `.organigrama__buscar-*` de más arriba —el foco nunca
   sale del campo de texto, se marca la opción activa con
   `aria-activedescendant`— con sus propias clases: son dos pantallas
   independientes y una dependencia cruzada por un puñado de reglas no vale
   lo que costaría acoplarlas. */
   .citas__combobox {
      position: relative;
   }

   .citas__combobox-lista {
      position: absolute;
      top: calc(100% + var(--e1));
      left: 0;
      z-index: 20;
      width: 100%;
      max-height: 280px;
      margin: 0;
      padding: var(--e1);
      overflow-y: auto;
      list-style: none;
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-lowest);
      box-shadow: var(--md-elevation-1);
   }

   .citas__combobox-opcion {
      padding: var(--e1) var(--e2);
      border-radius: var(--md-shape-small);
      font-size: var(--fs-dato);
      cursor: pointer;
   }

   .citas__combobox-opcion:hover,
   .citas__combobox-opcion--activa {
      background: var(--md-primary-container);
      color: var(--md-on-primary-container);
   }

   .citas__combobox-vacio {
      padding: var(--e1) var(--e2);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-dato);
   }

   /* --- Vista Mes: el calendario -----------------------------------------
   Reusa `.tabla-caja`/`.tabla` para el cajón, el borde y la cabecera pegada
   arriba: es el mismo registro de trabajo, siete columnas fijas en vez de
   una fila por cita. Lo que cambia es la celda, que pasa a ser un día
   entero y no un dato suelto — así que se sobreescribe lo que no calza
   (ancho parejo, texto centrado en la cabecera, alto mínimo). */
   .citas__mes-tabla {
      table-layout: fixed;
   }

   .citas__mes-tabla th {
      text-align: center;
   }

   /* El hover de fila de `.tabla` tiñe los siete días de la semana a la vez
   —tenía sentido cuando la fila era UN registro—; acá el hover tiene que
   marcar un día, no una semana entera. */
   .citas__mes-tabla tbody tr:hover {
      background: transparent;
   }

   .citas__mes-celda {
      width: 14.2857%;
      height: 96px;
      padding: var(--e2);
      vertical-align: top;
      cursor: pointer;
   }

   .citas__mes-celda:hover,
   .citas__mes-celda:focus-visible {
      background: var(--md-surface-container);
   }

   /* Los días del mes vecino que completan la primera y la última semana:
   se ven —la grilla no puede tener huecos— pero atenuados, y sin agenda
   porque no se les pidió (el rango que viaja al servidor es el mes exacto,
   ver `renderCalendarioMes` en `citas.ts`). */
   .citas__mes-celda--fuera {
      color: var(--md-on-surface-variant);
      background: color-mix(in oklab, var(--md-surface-container) 55%, var(--md-surface-container-lowest));
   }

   .citas__mes-cabecera-celda {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: var(--e1);
   }

   .citas__mes-numero {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 22px;
      height: 22px;
      padding: 0 var(--e1);
      border-radius: var(--md-shape-full);
      font-size: var(--fs-dato);
      font-weight: 600;
   }

   /* «Hoy» no puede depender solo del color (skill §4): además del tinte, es
   la única celda cuyo número lleva relleno y forma de píldora —el resto va
   sin fondo—, y el `aria-label` de la celda dice «hoy» explícito para quien
   no ve ninguna de las dos señales. */
   .citas__mes-celda--hoy .citas__mes-numero {
      background: var(--md-primary);
      color: var(--md-on-primary);
   }

   /* El contador («3») de la pantalla angosta: existe en el DOM siempre,
   oculto en escritorio, para no tener que reconstruir la celda al cruzar
   el punto de quiebre. */
   .citas__mes-contador {
      display: none;
      align-items: center;
      justify-content: center;
      min-width: 18px;
      height: 18px;
      padding: 0 var(--e1);
      border-radius: var(--md-shape-full);
      background: var(--md-surface-container-high);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      font-weight: 700;
   }

   .citas__mes-citas {
      display: flex;
      flex-direction: column;
      gap: 2px;
   }

   /* Línea compacta de una cita: hora + especialidad, y debajo la MISMA
   píldora de estado que la última columna de la tabla — nunca solo color,
   ver `.estado` más arriba. Es un `<button>` cuando tiene acciones
   disponibles y un `<div>` inerte cuando no (ver `lineaCita()`), y comparten
   clase porque se ven idénticas: lo único que cambia es si responde al clic. */
   .citas__mes-cita {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: 1px;
      width: 100%;
      padding: 1px var(--e1);
      border: none;
      border-radius: var(--md-shape-small);
      background: transparent;
      color: inherit;
      font: inherit;
      text-align: left;
      cursor: pointer;
   }

   .citas__mes-cita--inerte {
      cursor: default;
   }

   button.citas__mes-cita:hover {
      background: var(--md-surface-container-high);
   }

   .citas__mes-cita-texto {
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-size: var(--fs-meta);
      line-height: var(--lh-ajustada);
   }

   .citas__mes-mas {
      padding: 0 var(--e1);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      line-height: var(--lh-ajustada);
   }

   /* A 360 px la barra se apila: nav y el interruptor de vista no caben en la
   misma fila sin recortarse. */
   @media (max-width: 480px) {
      .citas__barra {
         flex-direction: column;
         align-items: stretch;
      }

      .citas__nav {
         justify-content: space-between;
      }

      /* Siete columnas con texto no caben en un teléfono sin desplazamiento
      horizontal, y esta grilla no lo permite (skill §7): la celda colapsa a
      solo el número y un contador. El texto completo de cada cita sigue
      disponible: es el `aria-label` de la celda («…, N citas»), no algo que
      se perdió al achicar la pantalla. Tocar el día lleva a la vista Día,
      donde la lista completa vuelve a estar una por línea. */
      .citas__mes-celda {
         height: 44px;
         padding: var(--e1);
      }

      .citas__mes-tabla th {
         padding: var(--e1) 2px;
      }

      .citas__mes-citas {
         display: none;
      }

      .citas__mes-contador {
         display: inline-flex;
      }

      .citas__mes-numero {
         font-size: var(--fs-meta);
      }
   }
}

@layer utilidades {
   /* ===========================================================================
   Estados que tienen que ganarle a todo

   Para esto se reservó la última capa: no son componentes ni pantallas, son
   interruptores que se encienden desde el JS y no pueden perder contra la
   regla de una pantalla.
   =========================================================================== */

   /* Mientras hay un modal abierto el fondo no se desplaza. `showModal()` deja
   inerte lo de atrás, pero la rueda del mouse sigue moviéndolo: además de
   desorientar, obliga a recalcular el desenfoque a pantalla completa en cada
   cuadro, y sobre una tabla larga en un puesto con gráficos integrados eso se
   traba. La clase la pone y la quita `ts/ui/modal.ts`.

   `scrollbar-gutter` va ACÁ y no en `html` suelto. Reservarlo siempre deja un
   canal vacío a la derecha de todas las pantallas para arreglar un salto que
   solo pasa al abrir un modal: se paga el remedio todo el día por una molestia
   de un segundo. Puesto junto al `overflow: hidden`, el canal aparece en el
   mismo instante en que desaparece la barra y ocupa lo mismo, así que no hay
   salto y el resto del tiempo no hay canal.

   Va en <html> porque `scrollbar-gutter` del elemento raíz es lo que el
   navegador aplica al viewport; en <body> no se propaga. El panel del
   ingreso —`.ingreso__panel`, no `.ingreso`, que ya es `overflow: hidden`
   siempre— lleva su propio contenedor de scroll y por eso necesita la línea
   aparte.

   En macOS/iOS las barras son superpuestas y no ocupan espacio: ahí no hay nada
   que compensar, y que Safari haya sumado `scrollbar-gutter` recién en 18.2 no
   cambia nada — el bloqueo, que es lo que importa, lo hace `overflow`. */
   .con-modal {
      overflow: hidden;
      scrollbar-gutter: stable;
   }

   .con-modal .ingreso__panel {
      overflow: hidden;
   }
}
/* ===========================================================================
   Panel de indicadores — `ts/routes/panel.ts` (27-09-2026)

   Registro de ACOGIDA arriba y de TRABAJO abajo, en la misma pantalla: las
   tarjetas son pocas, se leen de un vistazo y pueden llevar aire y sombra; las
   listas de «Qué hacer ahora» son tablas y se quedan planas (skill §3). La
   sombra va sobre la tarjeta, nunca por fila.
   =========================================================================== */
@layer pantallas {
   /* La franja continúa el navbar: el mismo degradado anclado a la ventana
      (`fixed`), así que se lee como el mismo plano que baja y no como un
      segundo bloque azul. Sale del relleno del lienzo con márgenes negativos;
      las esquinas de abajo redondeadas dicen dónde termina cuando el lienzo
      no llega al borde de la ventana (pantallas de más de 1600 px). */
   .kpi__franja {
      margin: calc(-1 * var(--e5)) calc(-1 * var(--pad-lateral)) 0;
      padding: var(--e6) var(--pad-lateral) calc(var(--e6) * 2 + var(--e5));
      background: var(--armazon-fondo);
      background-image: var(--armazon-degradado);
      background-attachment: fixed;
      color: var(--armazon-texto);
      border-radius: 0 0 var(--md-shape-extra-large) var(--md-shape-extra-large);
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      gap: var(--e6);
      flex-wrap: wrap;
   }

   /* Sobre el azul el anillo global (--md-primary) casi no se ve: el mismo
      reemplazo que usan el riel y el navbar. */
   .kpi__franja :focus-visible {
      outline-color: var(--armazon-foco);
   }

   .kpi__encabezado {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      min-width: 0;
   }

   .kpi__ambito {
      margin: 0;
      font-size: 28px;
      font-weight: 700;
      line-height: var(--lh-ajustada);
      letter-spacing: -0.01em;
   }

   .kpi__rango {
      margin: 0;
      font-size: var(--fs-cuerpo);
      color: var(--armazon-texto-tenue);
   }

   .kpi__filtros {
      display: flex;
      align-items: flex-end;
      gap: var(--e3);
      flex-wrap: wrap;
   }

   .kpi__filtro {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      font-size: var(--fs-meta);
      font-weight: 600;
      color: var(--armazon-texto-tenue);
   }

   .kpi__selector {
      font: inherit;
      font-size: var(--fs-cuerpo);
      font-weight: 500;
      min-height: 44px;
      max-width: 22rem;
      padding: var(--e2) var(--e3);
      border-radius: var(--md-shape-medium);
      border: 1px solid var(--kpi-filtro-borde);
      background: var(--kpi-filtro-fondo);
      color: var(--armazon-texto);
   }

   /* La lista desplegada del select la dibuja el sistema, sobre fondo claro u
      oscuro según el tema: con el texto blanco de la franja quedaría blanco
      sobre blanco. */
   .kpi__selector option {
      color: var(--md-on-surface);
      background: var(--md-surface-container-lowest);
   }

   .kpi__periodo {
      display: flex;
      gap: 2px;
      padding: 3px;
      border-radius: var(--md-shape-medium);
      border: 1px solid var(--kpi-filtro-borde);
      background: var(--kpi-filtro-fondo);
   }

   .kpi__periodo-boton {
      font: inherit;
      font-size: var(--fs-cuerpo);
      font-weight: 500;
      min-height: 38px;
      padding: var(--e2) var(--e4);
      border: 0;
      border-radius: calc(var(--md-shape-medium) - 3px);
      background: none;
      color: var(--armazon-texto);
      cursor: pointer;
   }

   .kpi__periodo-boton:hover {
      background: var(--armazon-realce);
   }

   .kpi__periodo-boton[aria-pressed="true"] {
      font-weight: 600;
      background: var(--kpi-filtro-activo-fondo);
      color: var(--kpi-filtro-activo-texto);
   }

   /* El cuerpo sube sobre la franja: las tarjetas quedan montadas en el azul,
      que es lo que la maqueta aprobada tiene de «moderno». */
   .kpi__cuerpo {
      position: relative;
      margin-top: calc(-1 * (var(--e6) * 2 - var(--e1)));
      display: flex;
      flex-direction: column;
      gap: var(--e5);
      transition: opacity var(--dur-efecto-rapido) linear;
   }

   /* Recargando: lo anterior se queda en su lugar, a media luz. Sin esqueleto
      ni salto. */
   .kpi--cargando .kpi__cuerpo {
      opacity: .55;
   }

   .kpi__espera {
      margin: 0;
      padding: var(--e5);
      border-radius: var(--md-shape-large);
      background: var(--md-surface-container-lowest);
      color: var(--md-on-surface-variant);
   }

   .kpi__fila {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: var(--e5);
   }

   /* --- Tarjetas ------------------------------------------------------------ */

   .kpi-tarjeta {
      display: flex;
      flex-direction: column;
      gap: var(--e3);
      min-width: 0;
      padding: var(--e5);
      border-radius: var(--md-shape-large);
      border: 1px solid var(--md-surface-container-highest);
      background: var(--md-surface-container-lowest);
      box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px -12px var(--kpi-sombra);
   }

   .kpi-tarjeta--grande {
      grid-column: span 2;
      gap: var(--e4);
      padding: calc(var(--e5) + var(--e1));
   }

   .kpi-tarjeta--alerta {
      border: 2px solid var(--kpi-alerta-borde);
      background: var(--kpi-alerta-fondo);
      color: var(--kpi-alerta-texto);
   }

   .kpi-tarjeta__rotulo {
      margin: 0;
      display: flex;
      align-items: center;
      gap: var(--e2);
      font-size: var(--fs-cuerpo);
      font-weight: 600;
      letter-spacing: .01em;
      color: var(--md-on-surface-variant);
   }

   .kpi-tarjeta__rotulo .icono {
      width: 18px;
      height: 18px;
      flex: none;
      color: var(--boton-principal-fondo);
   }

   .kpi-tarjeta--alerta .kpi-tarjeta__rotulo,
   .kpi-tarjeta--alerta .kpi-tarjeta__rotulo .icono,
   .kpi-tarjeta--alerta .kpi-tarjeta__ayuda,
   .kpi-tarjeta--alerta .kpi-numero {
      color: var(--kpi-alerta-texto);
   }

   .kpi-tarjeta__cabeza,
   .kpi-tarjeta__pie {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: var(--e3);
   }

   .kpi-tarjeta__cabeza {
      align-items: flex-start;
   }

   .kpi-tarjeta__medio {
      display: flex;
      justify-content: space-between;
      align-items: flex-end;
      gap: var(--e4);
   }

   .kpi-tarjeta__cifras {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      min-width: 0;
   }

   .kpi-tarjeta__detalle {
      margin: 0;
      font-size: var(--fs-dato);
      color: var(--md-on-surface-variant);
   }

   .kpi-tarjeta__ayuda {
      margin: 0;
      margin-top: auto;
      font-size: var(--fs-dato);
      line-height: 1.45;
      color: var(--md-on-surface-variant);
   }

   .kpi-tarjeta__pildoras {
      display: flex;
      gap: var(--e2);
      flex-wrap: wrap;
   }

   .kpi-tarjeta__accion {
      justify-content: center;
      min-height: 44px;
      font-weight: 600;
   }

   /* --- El número grande ----------------------------------------------------
      Cifras tabulares a propósito, aunque la skill `dataviz` prefiera las
      proporcionales en un número suelto: acá el número ENTRA CONTANDO, y con
      dígitos de ancho variable bailaría mientras sube. */
   .kpi-numero {
      display: flex;
      align-items: baseline;
      flex-wrap: wrap;
      font-size: 48px;
      font-weight: 700;
      line-height: 1;
      letter-spacing: -0.02em;
      font-variant-numeric: tabular-nums;
      color: var(--md-on-surface);
   }

   .kpi-numero--heroe {
      font-size: 72px;
   }

   .kpi-numero--medio {
      font-size: 40px;
   }

   .kpi-numero__unidad {
      font-size: .5em;
      font-weight: 600;
      color: var(--md-on-surface-variant);
   }

   .kpi-numero--vacio {
      flex-direction: column;
      align-items: flex-start;
      gap: var(--e1);
   }

   .kpi-numero--vacio .kpi-numero__cifra {
      color: var(--md-on-surface-variant);
   }

   .kpi-numero__nota {
      font-size: var(--fs-dato);
      font-weight: 600;
      letter-spacing: 0;
      color: var(--md-on-surface-variant);
   }

   /* --- Píldora de variación: flecha Y palabra, el color acompaña ----------- */
   .kpi-variacion {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      align-self: flex-start;
      padding: var(--e1) 10px;
      border-radius: var(--md-shape-full);
      font-size: var(--fs-dato);
      font-weight: 600;
      white-space: nowrap;
      font-variant-numeric: tabular-nums;
   }

   .kpi-variacion .icono {
      width: 14px;
      height: 14px;
      stroke-width: 2.5;
   }

   .kpi-variacion--mejor {
      background: var(--kpi-mejor-fondo);
      color: var(--kpi-mejor-texto);
   }

   .kpi-variacion--peor {
      background: var(--kpi-peor-fondo);
      color: var(--kpi-peor-texto);
   }

   .kpi-variacion--neutra {
      background: var(--md-surface-container-high);
      color: var(--md-on-surface-variant);
   }

   /* --- Mini curva ------------------------------------------------------------ */
   .curva-mini {
      width: 96px;
      height: 32px;
      flex: none;
      overflow: visible;
   }

   .kpi-tarjeta--grande .curva-mini {
      width: min(260px, 45%);
      height: 84px;
   }

   .curva-mini__linea {
      fill: none;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
      vector-effect: non-scaling-stroke;
   }

   .curva-mini--serie-1 { color: var(--grafico-serie-1); }
   .curva-mini--serie-2 { color: var(--grafico-serie-2); }

   .curva-mini__linea { stroke: currentColor; }

   .curva-mini__suelto,
   .curva-mini__final {
      fill: none;
      stroke: currentColor;
      stroke-linecap: round;
      stroke-width: 4;
      vector-effect: non-scaling-stroke;
   }

   .curva-mini__final {
      stroke-width: 6;
   }

   /* --- Tendencia --------------------------------------------------------------- */
   .kpi__tendencia {
      padding: calc(var(--e5) + var(--e1));
      gap: var(--e4);
   }

   .kpi__seccion-cabeza {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
      gap: var(--e3);
      flex-wrap: wrap;
   }

   .kpi__seccion-titulo {
      margin: 0;
      font-size: 20px;
      font-weight: 700;
      color: var(--md-on-surface);
   }

   .kpi__seccion-nota {
      font-size: var(--fs-dato);
      color: var(--md-on-surface-variant);
   }

   .kpi__graficos {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: var(--e6);
   }

   .grafico {
      margin: 0;
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      min-width: 0;
   }

   .grafico--serie-1 { --grafico-color: var(--grafico-serie-1); }
   .grafico--serie-2 { --grafico-color: var(--grafico-serie-2); }

   .grafico__titulo {
      font-size: var(--fs-cuerpo);
      font-weight: 600;
      color: var(--md-on-surface);
   }

   .grafico__sentido {
      font-weight: 400;
      color: var(--md-on-surface-variant);
   }

   .grafico__lienzo {
      position: relative;
      min-height: 200px;
   }

   .grafico__svg {
      display: block;
      max-width: 100%;
      overflow: visible;
      border-radius: var(--md-shape-small);
   }

   .grafico__rejilla line {
      stroke: var(--md-surface-container-high);
      stroke-width: 1;
   }

   .grafico__rejilla .grafico__base {
      stroke: var(--md-outline-variant);
   }

   .grafico__eje text {
      font-size: 11px;
      fill: var(--md-on-surface-variant);
      font-variant-numeric: tabular-nums;
   }

   .grafico__linea {
      fill: none;
      stroke: var(--grafico-color);
      stroke-width: 2;
      stroke-linejoin: round;
      stroke-linecap: round;
   }

   /* El anillo del color de la superficie despega el punto de la línea que
      cruza (skill `dataviz`, «surface ring»). */
   .grafico__punto {
      fill: var(--grafico-color);
      stroke: var(--md-surface-container-lowest);
      stroke-width: 2;
   }

   .grafico__valor-final {
      font-size: 13px;
      font-weight: 700;
      fill: var(--md-on-surface);
      font-variant-numeric: tabular-nums;
   }

   .grafico__cruz {
      stroke: var(--md-outline);
      stroke-width: 1;
   }

   .grafico__lectura {
      position: absolute;
      top: var(--e2);
      display: flex;
      flex-direction: column;
      gap: 2px;
      padding: var(--e2) var(--e3);
      border-radius: var(--md-shape-small);
      background: var(--md-inverse-surface);
      color: var(--md-inverse-on-surface);
      pointer-events: none;
      box-shadow: var(--md-elevation-2);
      white-space: nowrap;
   }

   .grafico__lectura-valor {
      font-size: var(--fs-titulo);
      font-variant-numeric: tabular-nums;
   }

   .grafico__lectura-detalle {
      font-size: var(--fs-meta);
   }

   .grafico__vacio {
      display: grid;
      place-items: center;
      min-height: 200px;
      margin: 0;
      border-radius: var(--md-shape-medium);
      background: var(--md-surface-container-low);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-dato);
      font-weight: 600;
   }

   .grafico__datos summary {
      cursor: pointer;
      font-size: var(--fs-dato);
      color: var(--md-on-surface-variant);
      width: fit-content;
   }

   .grafico__datos .tabla-caja {
      margin-top: var(--e2);
   }

   /* --- Qué hacer ahora ----------------------------------------------------------
      Plano y apretado: es registro de trabajo aunque viva en una tarjeta. */
   .kpi__acciones {
      padding: 0;
      gap: 0;
      overflow: hidden;
   }

   .kpi__acciones-cabeza {
      padding: var(--e5) var(--e5) var(--e4);
   }

   .kpi-accion {
      border-top: 1px solid var(--md-separador);
   }

   .kpi-accion__fila {
      display: flex;
      align-items: center;
      gap: var(--e4);
      width: 100%;
      padding: var(--e4) var(--e5);
      text-align: left;
      font: inherit;
      color: inherit;
   }

   .kpi-accion__fila--boton {
      border: 0;
      background: none;
      cursor: pointer;
   }

   .kpi-accion__fila--boton:hover {
      background: color-mix(in oklab, var(--md-surface-container-lowest) 94%, var(--md-on-surface));
   }

   .kpi-accion__cuenta {
      flex: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 56px;
      height: 40px;
      padding-inline: var(--e2);
      border-radius: var(--md-shape-medium);
      font-size: 18px;
      font-weight: 700;
      font-variant-numeric: tabular-nums;
   }

   .kpi-accion__cuenta--alerta { background: var(--md-warning-container); color: var(--md-on-warning-container); }
   .kpi-accion__cuenta--accion { background: var(--md-primary-container); color: var(--md-on-primary-container); }
   .kpi-accion__cuenta--bien   { background: var(--kpi-mejor-fondo); color: var(--kpi-mejor-texto); }
   .kpi-accion__cuenta--neutro { background: var(--md-surface-container-high); color: var(--md-on-surface-variant); }
   .kpi-accion__cuenta--error  { background: var(--kpi-peor-fondo); color: var(--kpi-peor-texto); }

   .kpi-accion__textos {
      flex: 1 1 auto;
      display: flex;
      flex-direction: column;
      gap: 2px;
      min-width: 0;
   }

   .kpi-accion__titulo {
      font-size: 15px;
      color: var(--md-on-surface);
   }

   .kpi-accion__resumen {
      font-size: var(--fs-dato);
      color: var(--md-on-surface-variant);
   }

   .kpi-accion__chevron {
      flex: none;
      display: inline-flex;
      color: var(--md-on-surface-variant);
   }

   .kpi-accion__chevron .icono {
      width: 20px;
      height: 20px;
   }

   .kpi-accion__panel .desplegable__caja {
      padding: 0 var(--e5) var(--e4);
   }

   .kpi-accion__espera {
      margin: 0;
      padding: var(--e3) 0;
      font-size: var(--fs-dato);
      color: var(--md-on-surface-variant);
   }

   .kpi-motivo {
      display: inline-block;
      padding: 2px var(--e2);
      border-radius: var(--md-shape-full);
      background: var(--md-surface-container-high);
      color: var(--md-on-surface-variant);
      font-size: var(--fs-meta);
      font-weight: 600;
      white-space: nowrap;
   }

   .kpi-averias {
      margin: 0;
      padding: 0 0 0 var(--e5);
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      font-size: var(--fs-dato);
      color: var(--md-on-surface);
   }

   /* La línea de «Nueva cita» que dice qué hora cancelada reemplaza, y la
      misma marca bajo el nombre en la agenda. */
   .citas__reemplaza {
      display: block;
      margin: 0 0 var(--e3);
      font-size: var(--fs-dato);
      color: var(--md-on-surface-variant);
   }

   td .citas__reemplaza {
      margin: 2px 0 0;
      font-size: var(--fs-meta);
   }

   /* --- Movimiento -----------------------------------------------------------------
      Las tarjetas entran con un desplazamiento corto y el chevron gira: lo
      que el funcionario provocó al abrir la pantalla o una lista. Nada de
      esto existe para quien pidió menos movimiento. */
   @media (prefers-reduced-motion: no-preference) {
      .kpi__cuerpo > * {
         animation: kpi-entrada var(--dur-espacial-normal) var(--resorte-espacial-normal) both;
      }

      .kpi__cuerpo > :nth-child(2) { animation-delay: 40ms; }
      .kpi__cuerpo > :nth-child(3) { animation-delay: 80ms; }
      .kpi__cuerpo > :nth-child(4) { animation-delay: 120ms; }

      .kpi-accion__chevron {
         transition: transform var(--dur-efecto-normal) var(--resorte-efecto-normal);
      }
   }

   [aria-expanded="true"] > .kpi-accion__chevron {
      transform: rotate(180deg);
   }

   @keyframes kpi-entrada {
      from {
         opacity: 0;
         transform: translateY(8px);
      }
   }

   /* --- Anchos ------------------------------------------------------------------------ */
   @media (max-width: 1199px) {
      .kpi__fila {
         grid-template-columns: repeat(2, minmax(0, 1fr));
      }
   }

   @media (max-width: 899px) {
      .kpi__graficos {
         grid-template-columns: minmax(0, 1fr);
      }
   }

   /* En celular, una columna: la maqueta del 27-09. */
   @media (max-width: 599px) {
      .kpi__franja {
         flex-direction: column;
         align-items: stretch;
         gap: var(--e4);
         padding-top: var(--e5);
         padding-bottom: calc(var(--e6) * 2);
      }

      .kpi__ambito {
         font-size: 22px;
      }

      .kpi__filtros,
      .kpi__filtro,
      .kpi__selector {
         width: 100%;
         max-width: none;
      }

      .kpi__periodo {
         display: grid;
         grid-template-columns: repeat(3, minmax(0, 1fr));
      }

      .kpi__cuerpo {
         margin-top: calc(-1 * (var(--e6) + var(--e3)));
         gap: var(--e3);
      }

      .kpi__fila {
         grid-template-columns: minmax(0, 1fr);
         gap: var(--e3);
      }

      .kpi-tarjeta--grande {
         grid-column: auto;
      }

      .kpi-tarjeta {
         padding: var(--e4);
      }

      .kpi-numero--heroe {
         font-size: 56px;
      }

      .kpi-numero {
         font-size: 40px;
      }

      .kpi-accion__fila {
         padding: var(--e3) var(--e4);
         gap: var(--e3);
      }

      .kpi-accion__cuenta {
         min-width: 44px;
         height: 36px;
         font-size: var(--fs-titulo);
      }

      .kpi-accion__panel .desplegable__caja {
         padding: 0 var(--e3) var(--e3);
      }

      .kpi__acciones-cabeza {
         padding: var(--e4);
      }
   }
}

/* ===========================================================================
   Configuración → Avisos a pacientes (`ts/routes/avisos.ts`, migración 027)

   Registro de trabajo: plano, con los grupos de campos de siempre. Lo único
   propio es la línea de tiempo, los ejemplos y la vista previa del aviso.
   =========================================================================== */
@layer pantallas {
   .avisos {
      display: flex;
      flex-direction: column;
      gap: var(--e5);
   }

   .avisos__hoja {
      display: flex;
      flex-direction: column;
      gap: var(--e4);
   }

   .avisos__hoja>.panel__titulo {
      margin: 0;
   }

   /* El selector no se estira al ancho de la hoja: los nombres de un árbol
   con sangría caben, y un select de 1200 px se lee como un campo de texto. */
   .avisos__selector {
      max-width: 520px;
   }

   .avisos__cabeza {
      display: flex;
      flex-wrap: wrap;
      gap: var(--e2);
      margin-bottom: var(--e3);
   }

   .avisos__modo {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      margin-bottom: var(--e3);
   }

   /* Lo que rige al heredar, dicho en una frase. Es un dato, no un aviso:
   sin recuadro de color, con el tono del texto principal. */
   .avisos__heredado {
      margin: 0 0 var(--e3);
      padding-left: var(--e3);
      border-left: 3px solid var(--md-outline-variant);
      color: var(--md-on-surface);
      font-size: var(--fs-dato);
   }

   .avisos__opcion {
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      margin-bottom: var(--e3);
   }

   /* --- La línea de tiempo --------------------------------------------------
   El SVG escala con el ancho (viewBox) y la caja desplaza si a 400 px no
   entra: los rótulos a menos de 10 px ya no se leen. */
   .avisos__linea {
      margin-top: var(--e4);
      overflow-x: auto;
   }

   .avisos__svg {
      display: block;
      width: 100%;
      min-width: 420px;
      height: auto;
   }

   .avisos__eje {
      stroke: var(--md-outline);
      stroke-width: 2;
   }

   .avisos__marca {
      stroke: var(--md-outline-variant);
      stroke-width: 1.5;
   }

   .avisos__punto {
      fill: var(--md-primary);
   }

   .avisos__cita {
      fill: var(--md-on-surface);
   }

   .avisos__rotulo {
      fill: var(--md-on-surface-variant);
      font: 600 13px/1 var(--tipo);
   }

   .avisos__rotulo--cita {
      fill: var(--md-on-surface);
   }

   /* --- Los tres ejemplos ---------------------------------------------------- */
   .avisos__ejemplos {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: var(--e3);
   }

   .avisos__ejemplo {
      padding: var(--e3);
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-lowest);
   }

   .avisos__ejemplo-titulo {
      margin: 0 0 var(--e2);
      font-weight: 700;
      font-size: var(--fs-dato);
   }

   .avisos__ejemplo-lista {
      margin: 0;
      padding: 0;
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: var(--e2);
   }

   .avisos__ejemplo-fila {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      gap: var(--e1);
      font-size: var(--fs-meta);
   }

   /* --- Los textos ------------------------------------------------------------ */

   /* La regla del 21-09 va a la vista y fija, no en una nota que se pueda
   cerrar: es lo que hace falta saber para escribir bien un aviso, cada vez. */
   .avisos__regla,
   .avisos__advertencia {
      display: flex;
      align-items: flex-start;
      gap: var(--e3);
      padding: var(--e3);
      border: 1px solid var(--md-warning);
      border-radius: var(--md-shape-small);
      background: var(--md-warning-container);
      color: var(--md-on-warning-container);
      font-size: var(--fs-dato);
   }

   .avisos__regla p,
   .avisos__advertencia p {
      margin: 0;
   }

   .avisos__advertencia>div {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
   }

   /* La advertencia de contenido clínico pesa más que la regla general: es
   ESTE texto el que la incumple. Mismo recuadro, con el borde del error. */
   .avisos__advertencia {
      border-color: var(--md-error);
      border-width: 2px;
   }

   .avisos__regla .estado__figura,
   .avisos__advertencia .estado__figura {
      width: 18px;
      height: 18px;
      margin-top: 2px;
   }

   .avisos__plantilla {
      display: flex;
      flex-direction: column;
      gap: var(--e3);
      padding-top: var(--e5);
      border-top: 1px solid var(--md-outline-variant);
   }

   .avisos__plantilla-titulo {
      margin: 0;
      font-size: var(--fs-titulo);
      font-weight: 700;
   }

   .avisos__cuerpo {
      resize: vertical;
      min-height: 6em;
      line-height: 1.5;
   }

   .avisos__huecos {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
   }

   .avisos__hueco {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      gap: var(--e1) var(--e3);
   }

   .avisos__hueco>.boton {
      flex: 0 0 auto;
      font-family: var(--mono);
   }

   .avisos__hueco>.campo__ayuda {
      flex: 1 1 260px;
   }

   .avisos__previa {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: var(--e4);
   }

   .avisos__previa-col {
      display: flex;
      flex-direction: column;
      gap: var(--e2);
      min-width: 0;
   }

   .avisos__previa-col .panel__subtitulo {
      margin: 0;
   }

   .avisos__correo-asunto {
      margin: 0;
      font-weight: 600;
      font-size: var(--fs-dato);
   }

   /* El correo trae su propio fondo claro (`color-scheme: light` en su HTML):
   así llega a la bandeja, y así se muestra aunque la pantalla esté en oscuro. */
   .avisos__correo {
      width: 100%;
      height: 380px;
      border: 1px solid var(--md-outline-variant);
      border-radius: var(--md-shape-small);
      background: var(--md-surface-container-lowest);
   }

   /* El SMS como globo de mensaje: se entiende de un vistazo que es otro canal. */
   .avisos__sms {
      margin: 0;
      padding: var(--e3);
      max-width: 36ch;
      border-radius: var(--md-shape-medium) var(--md-shape-medium) var(--md-shape-medium) var(--md-shape-extra-small);
      background: var(--md-surface-container-high);
      color: var(--md-on-surface);
      white-space: pre-wrap;
      overflow-wrap: anywhere;
   }

   .avisos__sms:empty {
      display: none;
   }

   .avisos__cifras {
      margin: 0;
      padding-left: var(--e4);
      display: flex;
      flex-direction: column;
      gap: var(--e1);
      font-size: var(--fs-meta);
   }

   .avisos__cifras-nota {
      color: var(--md-on-surface-variant);
   }
}

/* ===========================================================================
   Configuración → Especialidades (`ts/routes/especialidades.ts`, 28-09-2026)

   Un selector y, debajo, la misma pieza de administración que ya vivía en el
   panel de detalle del organigrama (`ts/ui/especialidades.ts`): sin estilos
   propios más allá del armazón, porque el contenido reutiliza `.panel__*` y
   `.tabla`.
   =========================================================================== */
@layer pantallas {
   .especialidades {
      display: flex;
      flex-direction: column;
      gap: var(--e5);
   }

   .especialidades__hoja {
      display: flex;
      flex-direction: column;
      gap: var(--e4);
   }

   .especialidades__hoja>.panel__titulo {
      margin: 0;
   }

   /* El selector no se estira al ancho de la hoja: los nombres de un árbol
   con sangría caben, y un select de 1200 px se lee como un campo de texto. */
   .especialidades__selector {
      max-width: 520px;
   }
}
