/* ===========================================================================
   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. */
@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');
}

: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;

    --fondo:         light-dark(#f4f6f8, #151b21);
    --panel:         light-dark(#ffffff, #1d252c);
    --superficie:    light-dark(#eef1f4, #262f38);
    --borde:         light-dark(#dfe4e9, #2e3a44);
    --borde-fuerte:  light-dark(#c3cbd3, #45535f);
    --texto:         light-dark(#1f2933, #e3eaf1);
    --tenue:         light-dark(#667381, #94a3b1);

    --barra:         light-dark(#1f2933, #0f151a);
    --barra-texto:   #b9c5d1;
    --barra-hover:   light-dark(#2a3641, #222c35);
    --barra-activo:  light-dark(#33414e, #2b3742);
    --barra-borde:   light-dark(#3b4954, #2b3742);

    --info:          light-dark(#1a5fb4, #7fb2f0);
    --foco:          light-dark(#1a5fb4, #7fb2f0);
    --ok:            light-dark(#1c7c4a, #58c48b);
    --pending:       light-dark(#8a6d1f, #d9b656);
    --fail:          light-dark(#b3261e, #f08b83);

    --ok-fondo:      light-dark(#e4f1ea, #17301f);
    --ok-borde:      light-dark(#bcdcca, #2c5a3c);
    --info-fondo:    light-dark(#e6edf8, #19293d);
    --info-borde:    light-dark(#c3d5ee, #2b4a6d);
    --pending-fondo: light-dark(#f6f0dc, #322810);
    --pending-borde: light-dark(#e0d3a8, #5a481b);
    --fail-fondo:    light-dark(#fbe9e8, #351d1b);
    --fail-borde:    light-dark(#f0c9c6, #66302b);
    --neutro-fondo:  light-dark(#e9edf1, #29333d);

    /* El velo del modal. Es el mismo en los dos temas a propósito: oscurecer
       funciona tanto sobre la aplicación clara como sobre la oscura, y un velo
       claro sobre el tema oscuro se leería como un destello.

       Dos valores porque el trabajo se reparte distinto: con desenfoque el
       fondo ya es ilegible y el velo puede ser suave —conviene, porque deja
       reconocer que la aplicación sigue ahí detrás—; sin desenfoque el velo es
       lo único que separa, y tiene que apagar el fondo él solo. */
    --velo:       rgba(9, 13, 20, .42);
    --velo-solido: rgba(9, 13, 20, .66);

    /* El botón relleno necesita su propio par y no puede reusar --info: en
       oscuro --info es #7fb2f0, un azul CLARO pensado para leerse como texto
       sobre el fondo, y con texto blanco encima daba 2,2:1 —el mismo error que
       el comentario de --ing-acento documenta haber evitado en el ingreso—.
       La salida no es oscurecer el azul sino invertir el texto: #7fb2f0 con
       #0f151a da 8,3:1, y el botón resalta contra el panel en 7,0:1.

       --*-texto va SIEMPRE en sentido contrario a --texto. De eso depende que
       el hover por color-mix de más abajo suba el contraste en los dos temas. */
    --boton-fondo:   light-dark(#1a5fb4, #7fb2f0);   /* 6,3:1 y 8,3:1 con su texto */
    --boton-texto:   light-dark(#ffffff, #0f151a);
    --peligro-fondo: light-dark(#b3261e, #f08b83);   /* 6,5:1 y 7,4:1 */
    --peligro-texto: light-dark(#ffffff, #2a0f0c);

    --r1: 4px;  --r2: 6px;  --r3: 10px;  --r4: 16px;
    --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;
    --dur: .26s;

    /* Casi toda salida: arranca rápido y frena largo. Lo que se mueve se lee
       como que «llega» y no como que «pasa», y eso es lo que hace que un
       desplazamiento no se sienta un corte. */
    --curva: cubic-bezier(.32, .72, 0, 1);

    /* InterVariable es la que se vendorizó acá arriba y la que se va a usar
       siempre. `Inter` la sigue por si el archivo no llegó a bajar y el puesto
       la tiene instalada; el resto es la caída al sistema. Ninguna sale a la
       red: no hay una sola URL de terceros en esta hoja. */
    --tipo: InterVariable, Inter, Roboto, system-ui, -apple-system, "Segoe UI", sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    /* light-dark() devuelve un color, así que sirve dentro de una sombra: el
       par entero se declara una sola vez, como cualquier otro token. */
    --sombra-flota: 0 1px 1px    light-dark(rgba(31, 41, 51, .04), rgba(0, 0, 0, .3)),
                    0 10px 24px -10px light-dark(rgba(31, 41, 51, .16), rgba(0, 0, 0, .55)),
                    0 28px 64px -28px light-dark(rgba(31, 41, 51, .22), rgba(0, 0, 0, .7));

    /* Una hoja apoyada en el escritorio, no un panel que flota. Mucho más
       corta y tenue que --sombra-flota: si se parecieran, lo que flota dejaría
       de distinguirse de lo que solo está posado. */
    --sombra-hoja:  0 1px 2px    light-dark(rgba(31, 41, 51, .05), rgba(0, 0, 0, .35)),
                    0 4px 12px -6px light-dark(rgba(31, 41, 51, .12), rgba(0, 0, 0, .5));

    /* --- Ingreso ---------------------------------------------------------
       El ingreso se compromete con UN aspecto —oscuro— y no sigue al tema.
       Es una puerta, no un escritorio: se cruza una vez al día, no se trabaja
       en ella, y el interruptor de tema vive adentro. Por eso estos tokens son
       literales y no llevan light-dark(): no tienen dos columnas que elegir. */
    --ing-noche-1:   #0b1020;   /* azul noche, arriba a la izquierda */
    --ing-noche-2:   #131a2e;
    --ing-noche-3:   #1b1630;   /* un morado muy apagado, abajo */
    --ing-brillo-a:  rgba(79, 127, 232, .30);
    --ing-brillo-b:  rgba(126, 92, 200, .24);

    --ing-vidrio:        rgba(255, 255, 255, .075);
    --ing-vidrio-borde:  rgba(255, 255, 255, .14);
    --ing-vidrio-brillo: rgba(255, 255, 255, .22);   /* el filo superior */

    --ing-texto:  #eaf0f8;
    --ing-tenue:  rgba(234, 240, 248, .58);

    --ing-campo:        rgba(6, 10, 20, .34);
    --ing-campo-borde:  rgba(255, 255, 255, .10);
    --ing-campo-activo: rgba(6, 10, 20, .46);

    /* Dos azules y no uno, porque son dos trabajos opuestos: el relleno del
       botón tiene que ser OSCURO para que el blanco encima llegue a 4,5:1
       (#3a5fcc da 5,69:1), y la línea de foco sobre el cristal tiene que ser
       CLARA para llegar a 3:1 contra el fondo (#6b95f0 da 5,27:1). Un solo
       tono deja una de las dos por debajo; con #4f7fe8 el botón quedaba en
       3,80:1, y su texto es de 14 px en negrita, que no cuenta como grande. */
    --ing-acento:      #3a5fcc;
    --ing-acento-alto: #4269d6;
    --ing-linea-foco:  #6b95f0;
    --ing-foco:        rgba(107, 149, 240, .45);

    --ing-fail:        #ffb4ad;
    --ing-fail-fondo:  rgba(179, 38, 30, .22);
    --ing-fail-borde:  rgba(240, 137, 128, .38);

    --ing-sombra: 0 24px 60px -24px rgba(0, 0, 0, .75),
                  0 2px 10px -4px rgba(0, 0, 0, .5);
}

/* 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;
    background: var(--fondo);
    color: var(--texto);
    font-family: var(--tipo);
    font-size: 14px;
    line-height: 1.5;
    -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(--foco);
    outline-offset: 2px;
    border-radius: var(--r1);
}

/* --- 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(--tenue);
    text-align: center;
}

.vacio p { margin: 0; }
.vacio p:first-child { color: var(--texto); font-size: 16px; 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 {
    background: var(--panel);
    border: 1px solid var(--borde);
    border-radius: var(--r3);
    box-shadow: var(--sombra-hoja);
    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. */
.desplegable {
    overflow: visible clip;
    transition: height var(--dur) var(--curva);
}

/* 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. */
@keyframes desplegable-entra {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
}

.desplegable__entrada { animation: desplegable-entra var(--dur) var(--curva) both; }

/* --- 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(--borde);
    border-radius: var(--r3);
    background: var(--panel);
    color: var(--texto);
    box-shadow: var(--sombra-flota);
}

/* `::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 color
   va TAMBIÉN en el respaldo de `var()`: si el token no llega, el velo se pinta
   igual. Son los únicos colores repetidos a mano de la hoja, y este es el motivo.

   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: var(--velo, rgba(9, 13, 20, .42));
    -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: var(--velo-solido, rgba(9, 13, 20, .66)); }
}

@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) var(--curva); }
.modal[open]::backdrop { animation: velo-entra var(--dur) var(--curva); }

/* 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. */
.modal[open].modal--saliendo           { animation: modal-sale var(--dur) var(--curva) forwards; }
.modal[open].modal--saliendo::backdrop { animation: velo-sale var(--dur) var(--curva) forwards; }

.modal__titulo  { margin: 0 0 var(--e2); font-size: 16px; font-weight: 700; }
.modal__mensaje { margin: 0; color: var(--tenue); }

.modal__aviso {
    margin: var(--e3) 0 0;
    padding: var(--e2) var(--e3);
    border: 1px solid var(--fail-borde);
    border-radius: var(--r2);
    background: var(--fail-fondo);
    color: var(--fail);
    font-size: 13px;
}

/* 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. */
.boton {
    display: inline-flex;
    align-items: center;
    gap: var(--e2);
    padding: var(--e2) var(--e4);
    border: 1px solid transparent;
    border-radius: var(--r2);
    background: var(--boton-fondo);
    color: var(--boton-texto);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: background var(--dur);
}

/* Mezclar el fondo hacia --texto funciona en los DOS temas porque --boton-texto
   va siempre en sentido contrario a --texto: en claro el botón se oscurece bajo
   texto blanco y sube a 7,0:1; en oscuro se aclara bajo texto oscuro y sube a
   9,0:1. En el secundario el fondo sí se acerca a su texto —14,8:1 baja a
   11,4:1—, que sigue sobradísimo, y a cambio el salto de luminosidad es de
   ΔL* 8: ahí es donde se ve el hover que antes no existía.

   Reemplaza a `filter: brightness(1.08)`, que sobre el secundario no hacía
   nada en tema claro —su fondo es --panel, o sea #ffffff, y 255 × 1,08 se
   satura en 255—, y que además obligaba al ingreso a desarmarlo a mano.

   `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(--texto));
}

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

.boton--secundario {
    --boton-fondo: var(--panel);
    --boton-texto: var(--texto);
    border-color: var(--borde-fuerte);
}

/* 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(--peligro-fondo);
    --boton-texto: var(--peligro-texto);
}

.boton--chico { padding: 2px var(--e2); font-size: 12px; 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;
}

.boton__riel {
    display: grid;
    grid-auto-rows: 1lh;
    transition: transform var(--dur) var(--curva);
}

.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); }

.campo__etiqueta { color: var(--tenue); font-size: 12px; font-weight: 600; }

.campo input,
.campo select,
.campo textarea {
    padding: var(--e2) var(--e3);
    border: 1px solid var(--borde-fuerte);
    border-radius: var(--r2);
    background: var(--panel);
    color: var(--texto);
    font: inherit;
}

.campo__error { color: var(--fail); font-size: 12px; }

.campo__ayuda { margin: 0; color: var(--tenue); font-size: 12px; }

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

/* 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. */
.campo__control[aria-invalid="true"] {
    border-color: var(--fail);
    background: var(--fail-fondo);
}

/* --- 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. */
.tabla-caja { overflow-x: auto; }

.tabla {
    width: 100%;
    border-collapse: collapse;
    background: var(--panel);
}

.tabla th,
.tabla td {
    padding: var(--e2) var(--e3);
    border-bottom: 1px solid var(--borde);
    text-align: left;
    vertical-align: middle;
}

.tabla th {
    position: sticky;
    top: 0;
    background: var(--superficie);
    color: var(--tenue);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: .04em;
}

.tabla tbody tr:hover { background: var(--superficie); }

/* 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. */
.tabla tbody tr[aria-current] { background: var(--info-fondo); }

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

/* --- 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á. */
.estado {
    display: inline-flex;
    align-items: center;
    gap: var(--e1);
    padding: 2px var(--e2);
    border: 1px solid var(--neutro-fondo);
    border-radius: 999px;
    background: var(--neutro-fondo);
    font-size: 12px;
    font-weight: 600;
    white-space: nowrap;
}

.estado__figura {
    width: 9px;
    height: 9px;
    flex: 0 0 auto;
    background: currentColor;
}

.figura--cuadro    { }
.figura--circulo   { border-radius: 50%; }
.figura--rombo     { transform: rotate(45deg); }
.figura--triangulo { clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.figura--tri-abajo { clip-path: polygon(0 0, 100% 0, 50% 100%); }
.figura--barra     { height: 3px; }
.figura--aspa      {
    clip-path: polygon(20% 0, 50% 30%, 80% 0, 100% 20%, 70% 50%,
                       100% 80%, 80% 100%, 50% 70%, 20% 100%, 0 80%,
                       30% 50%, 0 20%);
}

.estado--ok      { background: var(--ok-fondo);      border-color: var(--ok-borde);      color: var(--ok); }
.estado--info    { background: var(--info-fondo);    border-color: var(--info-borde);    color: var(--info); }
.estado--pending { background: var(--pending-fondo); border-color: var(--pending-borde); color: var(--pending); }
.estado--fail    { background: var(--fail-fondo);    border-color: var(--fail-borde);    color: var(--fail); }

/* --- 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) {
    .hoja { padding: var(--e4); border-radius: var(--r2); }
}

/* ===========================================================================
   El armazón: barra, cabecera y lienzo
   =========================================================================== */

.app {
    display: flex;
    flex-direction: column;
    min-height: 100%;
}

.barra {
    display: flex;
    align-items: center;
    gap: var(--e3);
    padding: 0 var(--pad-lateral);
    height: 44px;
    background: var(--barra);
    color: var(--barra-texto);
    border-bottom: 1px solid var(--barra-borde);
    flex-wrap: wrap;
}

.barra__marca { font-weight: 700; color: #fff; letter-spacing: .01em; }
.barra__espacio { flex: 1 1 auto; }
.barra__usuario { color: var(--barra-texto); font-size: 13px; }

/* En qué establecimiento se está parado. Va en la barra y no escondido en un
   menú: actuar sobre el hospital equivocado es un error caro, y el único modo
   de no cometerlo es tenerlo siempre a la vista. */
.barra__estab {
    padding: 2px var(--e2);
    border: 1px solid var(--barra-borde);
    border-radius: var(--r1);
    background: var(--barra-hover);
    color: #fff;
    font-size: 13px;
    font-weight: 600;
}

.barra__boton {
    padding: 2px var(--e2);
    border: 1px solid var(--barra-borde);
    border-radius: var(--r1);
    background: transparent;
    color: var(--barra-texto);
    font: inherit;
    font-size: 13px;
    cursor: pointer;
}

.barra__boton:hover { background: var(--barra-hover); color: #fff; }

.cab {
    display: flex;
    align-items: center;
    gap: var(--e4);
    min-height: var(--alto-cab);
    padding: var(--e3) var(--pad-lateral);
    background: var(--panel);
    border-bottom: 1px solid var(--borde);
}

.cab__texto { flex: 1 1 auto; min-width: 0; }
.cab__titulo { margin: 0; font-size: 18px; font-weight: 700; }
.cab__subtitulo { margin: 0; color: var(--tenue); font-size: 13px; }
.cab__acciones { display: flex; gap: var(--e2); flex-wrap: wrap; }

.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 toda la barra. Pero es un destino
   de foco, no un control: marcarlo con el anillo confundiría. */
.lienzo:focus { outline: none; }

/* ===========================================================================
   Navegación
   =========================================================================== */

.nav {
    display: flex;
    gap: var(--e1);
    padding: 0 var(--pad-lateral);
    background: var(--superficie);
    border-bottom: 1px solid var(--borde);
    overflow-x: auto;
}

.nav:empty { display: none; }

.nav__enlace {
    padding: var(--e2) var(--e3);
    border-bottom: 2px solid transparent;
    color: var(--tenue);
    text-decoration: none;
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
}

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

.nav__enlace--activo {
    color: var(--info);
    border-bottom-color: var(--info);
}
}

@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 degradado cubra el viewport entero y no
   asome el fondo del tema por debajo. El centrado es `margin: auto` sobre la
   tarjeta y NO `align-items: center`: con centrado por flex, una tarjeta más
   alta que la pantalla —error visible, móvil en horizontal— se recorta por
   arriba y el usuario no puede llegar al primer campo. */
.ingreso {
    position: fixed;
    inset: 0;
    display: flex;
    overflow-y: auto;
    padding: var(--e5) var(--pad-lateral);
    background:
        radial-gradient(1200px 800px at 12% -10%, var(--ing-noche-2), transparent 60%),
        radial-gradient(900px 700px at 100% 110%, var(--ing-noche-3), transparent 62%),
        linear-gradient(160deg, var(--ing-noche-1), #0e1425 55%, var(--ing-noche-3));
    color: var(--ing-texto);
    /* Los controles nativos del navegador —el relleno automático, el
       calendario, la barra de scroll— se pintan oscuros sin que haya que
       perseguirlos uno por uno. */
    color-scheme: dark;
}

/* Las dos formas orgánicas del fondo. Van en pseudoelementos y no en el
   marcado: son decoración, y un lector de pantalla no tiene nada que anunciar
   sobre ellas.

   🔴 `fixed` y NO `absolute`, que es lo que eran y hacía que la pantalla se
   pudiera desplazar a lo ancho y a lo alto sobre degradado vacío:

   `.ingreso` declara `overflow-y: auto`, y cuando un eje queda en `visible` y
   el otro no, el `visible` COMPUTA A AUTO. O sea que `.ingreso` es contenedor
   de scroll también en horizontal, sin haberlo pedido. La forma de abajo se
   posiciona en `right: -10%; bottom: -14%`, fuera de la caja, y un descendiente
   absoluto que sobresale por la derecha o por abajo entra en la región de
   desplazamiento. La animación empeoraba el efecto: `scale(1.08)` y
   `translate3d(3%, 4%)` la empujan más afuera todavía, así que el área
   desplazable respiraba durante los 34 segundos del ciclo.

   Con `fixed` el bloque contenedor pasa a ser el viewport —que acá es la misma
   caja, porque `.ingreso` es `fixed` con `inset: 0` y sin borde, así que los
   porcentajes dan idéntico— y un elemento fijo no entra en la región de
   desplazamiento de ningún ancestro. Se dibuja exactamente igual, no desborda,
   y de paso deja de moverse cuando la tarjeta se desplaza: es fondo, no
   contenido. El recorte lo hace el viewport, que es lo que se quería. */
.ingreso::before,
.ingreso::after {
    content: "";
    position: fixed;
    border-radius: 50% 42% 58% 40% / 46% 52% 44% 56%;
    filter: blur(70px);
    pointer-events: none;
    z-index: 0;
}

.ingreso::before {
    width: min(52vw, 520px);
    aspect-ratio: 1.15;
    top: -12%;
    left: -8%;
    background: var(--ing-brillo-a);
    animation: ingreso-deriva 26s ease-in-out infinite alternate;
}

.ingreso::after {
    width: min(46vw, 460px);
    aspect-ratio: .9;
    right: -10%;
    bottom: -14%;
    background: var(--ing-brillo-b);
    animation: ingreso-deriva 34s ease-in-out infinite alternate-reverse;
}

/* Muy lenta y muy corta: tiene que leerse como luz y no como algo que se
   mueve. La regla global de `prefers-reduced-motion` ya la desarma. */
@keyframes ingreso-deriva {
    from { transform: translate3d(0, 0, 0) scale(1); }
    to   { transform: translate3d(3%, 4%, 0) scale(1.08); }
}

/* --- La tarjeta de cristal ---------------------------------------------- */

.ingreso__tarjeta {
    position: relative;
    z-index: 1;
    margin: auto;
    width: 100%;
    max-width: 400px;
    padding: var(--e6);
    border: 1px solid var(--ing-vidrio-borde);
    border-radius: var(--r4);
    background: var(--ing-vidrio);
    box-shadow: var(--ing-sombra);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
}

/* El filo de luz del borde superior. Es lo que hace que el cristal parezca
   tener canto en vez de ser un rectángulo translúcido. */
.ingreso__tarjeta::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    height: 1px;
    border-radius: var(--r4) var(--r4) 0 0;
    background: linear-gradient(90deg, transparent, var(--ing-vidrio-brillo), transparent);
    pointer-events: none;
}

/* Sin backdrop-filter el cristal es un velo gris y el texto pierde contraste
   contra el degradado. Se sube la opacidad del fondo: se ve distinto, pero se
   lee igual, que es lo que importa. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .ingreso__tarjeta { background: rgba(14, 20, 37, .88); }
}

.ingreso__marca {
    margin: 0;
    font-size: 28px;
    font-weight: 600;
    letter-spacing: -.02em;
    color: var(--ing-texto);
}

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

.ingreso__ayuda {
    margin: 0;
    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;
}

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

.ingreso .campo__control {
    width: 100%;
    padding: var(--e3);
    border: 1px solid var(--ing-campo-borde);
    border-radius: var(--r2);
    background: var(--ing-campo);
    color: var(--ing-texto);
    font: inherit;
    transition: border-color var(--dur), background var(--dur), box-shadow var(--dur);
}

.ingreso .campo__control::placeholder { color: rgba(234, 240, 248, .34); }

.ingreso .campo__control:hover { border-color: rgba(255, 255, 255, .18); }

/* 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 #131a2c;
    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;
    border-radius: var(--r2);
    background: var(--ing-acento);
    color: #fff;
    font-weight: 600;
    letter-spacing: .01em;
    transition: background var(--dur), transform var(--dur), box-shadow var(--dur);
}

.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; }

/* --- 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);
    border-radius: var(--r2);
    background: var(--ing-fail-fondo);
    color: var(--ing-fail);
    font-size: 13px;
}

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

@media (max-width: 480px) {
    .ingreso { padding: var(--e4) var(--e4); }
    .ingreso__tarjeta { padding: var(--e5); border-radius: var(--r3); }
    .ingreso__marca { font-size: 24px; }
}

/* 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; }
}

/* ===========================================================================
   La red · tablero de instalaciones
   =========================================================================== */

/* Una tabla por cliente y no una sola con la columna «cliente»: lo que se viene
   a mirar acá es si un hospital está hablando, y un hospital son sus
   instalaciones juntas. Mezcladas en una lista, esa lectura hay que rearmarla a
   ojo en cada visita. */
.red__resumen { display: flex; align-items: center; gap: var(--e3); }

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

.red__cliente-nombre {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--e2);
    margin: 0 0 var(--e2);
    font-size: 14px;
    font-weight: 700;
}

.red__cliente-rut { color: var(--tenue); font-size: 12px; font-weight: 400; }

/* Un cliente sin instalaciones no es un error, es un contrato recién firmado.
   Se dice con borde punteado —la forma de «acá todavía no hay nada»— y no con
   un estado teñido, que lo leería como problema. */
.red__sin-instalaciones {
    margin: 0;
    padding: var(--e3);
    border: 1px dashed var(--borde);
    border-radius: var(--r2);
    color: var(--tenue);
    font-size: 13px;
}

/* La contraseña de una instalación, la única vez que se ve. `user-select: all`
   la toma entera con un clic: son 43 caracteres que hay que leer en una
   pantalla y pegar en otra, y arrastrar hasta el borde es justo como se corta
   el último carácter y se pierde media tarde averiguando por qué el hospital
   recibe 401. */
.red__clave {
    margin: var(--e3) 0;
    padding: var(--e3);
    border: 1px solid var(--borde);
    border-radius: var(--r2);
    background: var(--superficie);
    font-size: 15px;
    letter-spacing: .02em;
    user-select: all;
    overflow-wrap: anywhere;
}

/* --- El acceso al servidor de un cliente ---------------------------------- */

/* 🔴 Los campos de contraseña de este panel son `type="text"` con la máscara
   puesta acá, no `type="password"`. A Chrome le basta ver un campo de
   contraseña para ofrecer «¿guardar la contraseña?», y lo que guardaría en la
   cuenta de Google de quien opera son las credenciales de los servidores de
   veinte hospitales. En Firefox, que no implementa `-webkit-text-security`, el
   JS vuelve a `type="password"`: ahí es el mal menor.

   `text-security` sin prefijo no existe todavía en ningún motor; el prefijado
   está desde hace años en WebKit y Blink. El día que se estandarice, van las
   dos líneas. */
.clave--oculta { -webkit-text-security: disc; }

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

/* `min-width: 0` porque un hijo flex no baja de su ancho de contenido por
   omisión, y sin esto el campo empuja al botón fuera del panel en angosto. */
.clave__fila .campo__control { flex: 1 1 auto; min-width: 0; }

.acceso__visor { margin: var(--e3) 0; }

.acceso__nota { margin: 0 0 var(--e2); color: var(--tenue); font-size: 12px; }

/* ===========================================================================
   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: 13px;
    cursor: pointer;
}

.lista__cuenta { color: var(--tenue); font-size: 13px; }

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

.acciones-celda { display: flex; gap: var(--e1); white-space: nowrap; }

/* ===========================================================================
   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: 16px; 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(--tenue); }

.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);
}

.panel__grupo {
    margin: 0;
    padding: var(--e4);
    border: 1px solid var(--borde);
    border-radius: var(--r2);
    background: var(--superficie);
}

.panel__grupo-titulo {
    padding: 0 var(--e2);
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--tenue);
}

.panel__grupo-ayuda { margin: 0 0 var(--e3); color: var(--tenue); font-size: 12px; }


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

.panel__aviso {
    margin: 0;
    padding: var(--e2) var(--e3);
    border: 1px solid var(--fail-borde);
    border-radius: var(--r2);
    background: var(--fail-fondo);
    color: var(--fail);
    font-size: 13px;
}

.panel__aviso--ok {
    border-color: var(--ok-borde);
    background: var(--ok-fondo);
    color: var(--ok);
}
}

@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 ingreso 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 { overflow: hidden; }
}
