/* ---------------------------------------------------------------------------
   Sistema de Transfusiones · hoja única
   Sin preprocesador y sin framework: lo que hace coherentes veinte pantallas
   escritas en meses distintos son los tokens de aquí arriba, no la disciplina.
   --------------------------------------------------------------------------- */

/* --- 1. Tokens ----------------------------------------------------------- */

:root {
    color-scheme: light dark;

    --fondo:         #f4f6f8;
    --panel:         #ffffff;
    --superficie:    #eef1f4;
    --borde:         #dfe4e9;
    --borde-fuerte:  #c3cbd3;
    --texto:         #1f2933;
    --tenue:         #667381;

    --barra:         #1f2933;
    --barra-texto:   #b9c5d1;
    --barra-hover:   #2a3641;
    --barra-activo:  #33414e;
    --barra-borde:   #3b4954;

    --info:          #1a5fb4;
    --foco:          #1a5fb4;
    --ok:            #1c7c4a;
    --pending:       #8a6d1f;
    --fail:          #b3261e;

    --ok-fondo:      #e4f1ea;   --ok-borde:      #bcdcca;
    --info-fondo:    #e6edf8;   --info-borde:    #c3d5ee;
    --pending-fondo: #f6f0dc;   --pending-borde: #e0d3a8;
    --fail-fondo:    #fbe9e8;   --fail-borde:    #f0c9c6;
    --neutro-fondo:  #e9edf1;

    --r1: 4px;  --r2: 6px;  --r3: 10px;  --r4: 16px;
    --e1: 4px;  --e2: 8px;  --e3: 12px;  --e4: 16px;  --e5: 24px;  --e6: 32px;

    /* Ancho de lectura del contenido del lienzo (.cab y .app__contenido lo usan
       igual, ver los comentarios ahí). No es un límite de texto: hay tablas de
       siete columnas debajo, así que se eligió generoso a propósito — el tope
       solo tiene que evitar que
       esas columnas queden repartidas a lo largo de dos metros de pantalla en
       un monitor de 2560 px, no forzar un formulario angosto. El padding
       lateral crece con el viewport en vez de ser fijo, y nunca desaparece del
       todo en pantallas angostas. */
    --ancho-contenido: 1600px;
    --pad-lateral: clamp(var(--e4), 2.5vw, var(--e5));

    /* La marca de la barra y la cabecera comparten esta altura a propósito
       (ver .barra__marca y .cab): es lo único que garantiza que sus bordes
       inferiores sigan siendo la misma línea. */
    --alto-cab: 60px;
    /* Alto fijo de una fila de módulo: sin esto, el panel flotante del modo
       compacto (ver .modulo-grupo) no tiene forma de calcular dónde empieza
       su segundo tramo, porque en CSS puro no hay alto de contenido dinámico
       al que anclarse. */
    --alto-item-modulo: 34px;
    --dur: .26s;

    --tipo: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    --sombra-flota: 0 1px 1px rgba(31, 41, 51, .04),
                    0 10px 24px -10px rgba(31, 41, 51, .16),
                    0 28px 64px -28px rgba(31, 41, 51, .22);

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

/* Mismos roles, otros valores: ningún componente sabe en qué tema está.
   Los tokens viven en :root a propósito. Puestos en un descendiente, todo lo
   que hereda `color` del body arrastraría el valor claro ya calculado y
   quedaría texto oscuro sobre fondo oscuro. */
@media (prefers-color-scheme: dark) {
    :root:not([data-tema="claro"]) {
        --fondo: #151b21;  --panel: #1d252c;  --superficie: #262f38;
        --borde: #2e3a44;  --borde-fuerte: #45535f;
        --texto: #e3eaf1;  --tenue: #94a3b1;

        --barra: #0f151a;  --barra-texto: #b9c5d1;
        --barra-hover: #222c35;  --barra-activo: #2b3742;  --barra-borde: #2b3742;

        --info: #7fb2f0;  --foco: #7fb2f0;
        --ok: #58c48b;    --pending: #d9b656;  --fail: #f08b83;

        --ok-fondo:      #17301f;  --ok-borde:      #2c5a3c;
        --info-fondo:    #19293d;  --info-borde:    #2b4a6d;
        --pending-fondo: #322810;  --pending-borde: #5a481b;
        --fail-fondo:    #351d1b;  --fail-borde:    #66302b;
        --neutro-fondo:  #29333d;

        --sombra-flota: 0 1px 1px rgba(0, 0, 0, .3),
                        0 10px 24px -10px rgba(0, 0, 0, .55),
                        0 28px 64px -28px rgba(0, 0, 0, .7);

        /* En oscuro una sombra casi no se lee. Lo que levanta la hoja es que
           `--panel` sea más claro que `--fondo`; la sombra solo despega el
           borde. */
        --sombra-hoja:  0 1px 2px rgba(0, 0, 0, .35),
                        0 4px 12px -6px rgba(0, 0, 0, .5);
    }
}

:root[data-tema="oscuro"] {
    --fondo: #151b21;  --panel: #1d252c;  --superficie: #262f38;
    --borde: #2e3a44;  --borde-fuerte: #45535f;
    --texto: #e3eaf1;  --tenue: #94a3b1;

    --barra: #0f151a;  --barra-texto: #b9c5d1;
    --barra-hover: #222c35;  --barra-activo: #2b3742;  --barra-borde: #2b3742;

    --info: #7fb2f0;  --foco: #7fb2f0;
    --ok: #58c48b;    --pending: #d9b656;  --fail: #f08b83;

    --ok-fondo:      #17301f;  --ok-borde:      #2c5a3c;
    --info-fondo:    #19293d;  --info-borde:    #2b4a6d;
    --pending-fondo: #322810;  --pending-borde: #5a481b;
    --fail-fondo:    #351d1b;  --fail-borde:    #66302b;
    --neutro-fondo:  #29333d;

    --sombra-flota: 0 1px 1px rgba(0, 0, 0, .3),
                    0 10px 24px -10px rgba(0, 0, 0, .55),
                    0 28px 64px -28px rgba(0, 0, 0, .7);

    --sombra-hoja:  0 1px 2px rgba(0, 0, 0, .35),
                    0 4px 12px -6px rgba(0, 0, 0, .5);
}

/* --- 2. Base ------------------------------------------------------------- */

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

body {
    margin: 0;
    background: var(--fondo);
    color: var(--texto);
    font: 16px/1.55 var(--tipo);
    -webkit-font-smoothing: antialiased;
}

a { color: var(--info); text-decoration: none; }
a:hover { text-decoration: underline; }

/* En un mesón, con guantes y sin mouse, el foco visible no es accesibilidad
   opcional: es la única forma de saber dónde se está parado. Va global y no
   se quita nunca. */
:focus-visible {
    outline: 2px solid var(--foco);
    outline-offset: 2px;
    border-radius: var(--r1);
}

/* Todas las cifras alineadas: un código de unidad mal leído es un incidente. */
table, .cifra, input, kbd { font-variant-numeric: tabular-nums; }

/* --- 3. Armazón ---------------------------------------------------------- */

.app {
    display: grid; grid-template-columns: 240px 1fr; min-height: 100vh;
    /* Solo esta propiedad anima: es una lista de anchos de pista, que los
       navegadores actuales interpolan igual que cualquier otro valor
       numérico. */
    transition: grid-template-columns var(--dur) ease;
}
.app__lienzo { display: flex; flex-direction: column; min-width: 0; overflow: hidden; }

/* Modo compacto: la barra queda en 60px, solo íconos. Qué se oculta y qué se
   convierte en panel flotante vive junto a cada componente, más abajo. */
.app--compacta { grid-template-columns: 60px 1fr; }

/* La cabecera queda fija y el contenido larga scroll propio. Sin esto, una
   tabla de 200 unidades empuja la cabecera fuera de la pantalla y se pierde de
   vista en qué módulo se está. */
/* El `padding-inline` —no un `max-width` en la caja— es lo que centra: así la
   caja sigue midiendo el 100 % del lienzo y cualquier fondo o borde que se le
   ponga cruza todo el ancho. `.cab` usa exactamente la misma fórmula, para que
   el borde izquierdo del título y el de la primera columna de la tabla caigan
   siempre en la misma vertical — son hermanos dentro de .app__lienzo. */
.app__contenido {
    flex: 1; min-height: 0; overflow-y: auto;
    padding-inline: max(var(--pad-lateral), calc((100% - var(--ancho-contenido)) / 2));
}
.app__contenido:focus { outline: none; }

.cab {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--e4);
    height: var(--alto-cab);
    padding-inline: max(var(--pad-lateral), calc((100% - var(--ancho-contenido)) / 2));
    background: var(--panel);
    border-bottom: 1px solid var(--borde);
}
.cab__titulo { margin: 0; font-size: 19px; letter-spacing: -.01em; }
.cab__sub { margin: 2px 0 0; font-size: 12.5px; color: var(--tenue); }
.cab__acciones { display: flex; align-items: center; gap: var(--e2); }

/* --- 4. Barra de navegación ---------------------------------------------- */

.barra { display: flex; flex-direction: column; background: var(--barra); color: var(--barra-texto); min-width: 0; }

.barra__marca {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--e2);
    height: var(--alto-cab);
    padding: 0 var(--e4);
    border-bottom: 1px solid var(--barra-borde);
}
.barra__marca-texto { min-width: 0; overflow: hidden; }
.barra__marca-texto b { display: block; font-size: 14.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.barra__marca-texto span { display: block; margin-top: 2px; font-size: 11.5px; color: var(--tenue); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Botón real, con objetivo táctil amplio: se opera con guantes, no con la
   punta del dedo. */
.barra__colapsar {
    flex: none;
    display: grid; place-items: center;
    width: 36px; height: 36px;
    background: transparent;
    border: 1px solid var(--barra-borde); border-radius: var(--r2);
    color: var(--tenue); cursor: pointer;
}
.barra__colapsar:hover { background: var(--barra-hover); color: #fff; }

.barra__buscar {
    display: flex; align-items: center; gap: var(--e2);
    margin: var(--e3) var(--e2) 0;
    padding: 0 9px; height: 36px;
    background: var(--barra-hover);
    border: 1px solid var(--barra-borde);
    border-radius: var(--r2);
    color: var(--tenue);
}
.barra__buscar svg { flex: none; }

/* El campo filtra en línea, no abre una paleta: el anillo de foco va en la
   caja completa (:focus-within) y no en el input suelto, para que no se vean
   dos señales de foco superpuestas. */
.barra__buscar:focus-within {
    outline: 2px solid var(--foco);
    outline-offset: 2px;
}

.barra__buscar-campo {
    flex: 1; min-width: 0; height: 100%;
    padding: 0; margin: 0;
    background: transparent; border: 0;
    color: var(--barra-texto); font: 12.5px var(--tipo);
    /* type=search trae su propia cruz de limpiar y una apariencia con
       esquinas propias en algunos motores; ya existe un botón de limpiar
       propio, así que se neutraliza el del navegador. */
    appearance: none; -webkit-appearance: none;
}
.barra__buscar-campo::-webkit-search-cancel-button { display: none; }
.barra__buscar-campo::placeholder { color: var(--tenue); }
.barra__buscar-campo:focus { outline: none; }

.barra__buscar kbd {
    flex: none;
    padding: 2px 5px; font: 10px var(--mono);
    background: var(--barra); border: 1px solid var(--barra-borde);
    border-radius: 3px; color: var(--tenue);
}
/* Con el campo escrito, el atajo sobra: :has() ya lo soportan todos los
   navegadores vigentes, y evita que el TS tenga que ir a tocar estilos. */
.barra__buscar:has(.barra__buscar-campo:not(:placeholder-shown)) kbd {
    display: none;
}

.barra__buscar-limpiar {
    flex: none;
    display: grid; place-items: center;
    width: 22px; height: 100%;
    padding: 0;
    background: transparent; border: 0; border-radius: var(--r1);
    color: var(--tenue); font-size: 15px; line-height: 1;
    cursor: pointer;
}
.barra__buscar-limpiar:hover { background: var(--barra-borde); color: #fff; }
/* Declarar `display: grid` arriba le gana al `[hidden]` del navegador —los
   estilos de autor priman sobre los de UA aun con igual especificidad—, así
   que hay que devolvérselo explícito para que el botón desaparezca de verdad
   cuando el campo está vacío. */
.barra__buscar-limpiar[hidden] { display: none; }

/* overflow-y se anula en compacto (ver .app--compacta) porque recorta
   cualquier panel flotante que se salga del carril de 60px; el modo extendido
   sí necesita el scroll cuando el árbol de módulos no entra completo. */
.barra__nav { flex: 1; display: flex; flex-direction: column; gap: 1px; padding: var(--e2); overflow-y: auto; }

/* Ancla de posición para el panel flotante del modo compacto (problema 5) y
   para el cálculo del alto de un ítem (--alto-item-modulo). */
.modulo-grupo { position: relative; }

/* En compacto el `.modulo` sale del flujo al desplegarse el panel, y sin esta
   altura reservada su grupo se colapsa a cero: pasar el mouse por el tercer
   módulo hacía saltar hacia arriba a los cinco de abajo. El hueco se queda
   aunque el ícono flote. */
.app--compacta .modulo-grupo { min-height: var(--alto-item-modulo); }

.modulo {
    display: flex; align-items: center; gap: 9px;
    min-height: var(--alto-item-modulo);
    padding: 7px 8px 7px 10px;
    border-radius: var(--r2);
    color: var(--barra-texto);
    font-size: 13px;
}
.modulo:hover { background: var(--barra-hover); color: #fff; text-decoration: none; }
.modulo__nombre { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* line-height: 1 mantiene la caja del glifo ajustada a su propio tamaño: así
   el chevron crece sin empujar el alto de la fila (--alto-item-modulo), que
   sigue centrando por flex, no por el tamaño de este hijo. */
.modulo__chevron { flex: none; font-size: 17px; line-height: 1; transition: transform var(--dur) ease; }
.modulo[aria-expanded="true"] { background: var(--barra-activo); color: #fff; font-weight: 600; }
.modulo[aria-expanded="true"] .modulo__chevron { transform: rotate(90deg); }

/* El filete marca pertenencia sin gastar ancho: a 240 px, una segunda sangría
   parte «Bitácora de auditoría» en dos líneas.

   La técnica de la fila de grilla anima hasta el alto natural del contenido,
   algo que `height: auto` no puede transicionar. El contenido vive recortado
   en .submodulos__interior; el padding vertical se puso ahí adentro y no acá,
   porque un padding en el elemento que colapsa deja un resto visible incluso
   en 0fr. La visibilidad se retrasa al cerrar para que un tab no aterrice en
   un enlace aplastado a 0px. */
.submodulos {
    display: grid; grid-template-rows: 0fr;
    margin-left: 19px;
    padding-left: 11px;
    border-left: 1px solid var(--barra-borde);
    opacity: 0;
    visibility: hidden;
    transition: grid-template-rows var(--dur) ease, opacity var(--dur) ease, visibility 0s linear var(--dur);
}
.submodulos[data-abierto="true"] {
    grid-template-rows: 1fr;
    opacity: 1;
    visibility: visible;
    transition: grid-template-rows var(--dur) ease, opacity var(--dur) ease;
}
.submodulos__interior {
    overflow: hidden; min-height: 0;
    display: flex; flex-direction: column; gap: 1px;
    padding: 2px 0 var(--e1);
}
.submodulo { padding: 5px 8px; border-radius: var(--r1); color: var(--tenue); font-size: 12.5px; }
.submodulo:hover { background: var(--barra-hover); color: #fff; text-decoration: none; }
.submodulo[aria-current="page"] { background: var(--barra-activo); color: #fff; font-weight: 600; }

/* Mientras se filtra, el TS oculta con `hidden` lo que no calza. `hidden` ya
   gana solo —ningún selector de arriba le fija `display` a estos dos— pero se
   deja explícito para que agregar un `display` de layout más adelante no lo
   rompa en silencio. */
.modulo-grupo[hidden],
.submodulo[hidden] {
    display: none;
}

/* Con la navegación filtrada, todo submódulo que sobrevivió al filtro se ve
   igual de abierto, sin depender de `data-abierto`: el usuario está buscando,
   no navegando el acordeón. Misma especificidad (0,2,0) que
   `.submodulos[data-abierto="true"]`; va después en el archivo para ganar
   también en cerrado (sin el atributo) y en abierto (con él, por orden). */
.barra__nav--filtrando .submodulos {
    grid-template-rows: 1fr;
    opacity: 1;
    visibility: visible;
}

.barra__sin-resultados {
    margin: 0; padding: var(--e4) var(--e3);
    text-align: center; color: var(--tenue); font-size: 12.5px;
}

.barra__pie {
    display: flex; align-items: center; gap: 9px;
    padding: var(--e3) var(--e4);
    border-top: 1px solid var(--barra-borde);
    font-size: 12.5px;
}
/* min-width: 0 es lo que permite que el hijo con elipsis recorte dentro de un
   flex item; sin esto un correo largo empuja al botón en lugar de cortarse. */
.barra__pie-datos { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.barra__pie-datos span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.barra__pie-datos em { display: block; font-style: normal; font-size: 11px; color: var(--tenue); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Mismo lenguaje que .barra__salir —botón discreto sobre la barra oscura,
   mismo borde, mismo realce— pero cuadrado y sin texto: solo cicla el ícono. */
.barra__tema {
    flex: none;
    display: grid; place-items: center;
    width: 36px; height: 36px;
    background: transparent; border: 1px solid var(--barra-borde); border-radius: var(--r1);
    color: var(--tenue); cursor: pointer;
}
.barra__tema:hover { background: var(--barra-hover); color: #fff; }
.barra__salir {
    flex: none; display: flex; align-items: center; gap: 6px;
    padding: 6px 10px;
    background: transparent; border: 1px solid var(--barra-borde); border-radius: var(--r1);
    color: var(--tenue); font: 11.5px var(--tipo); cursor: pointer;
}
.barra__salir:hover { background: var(--barra-hover); color: #fff; }

/* --- 4b. Modo compacto ---------------------------------------------------- */

/* Solo íconos: se oculta todo lo textual y se centra lo que queda. El botón
   de colapsar y los íconos de módulo mantienen su objetivo táctil. */
.app--compacta .barra__marca { justify-content: center; padding: 0; }
.app--compacta .barra__marca-texto { display: none; }

.app--compacta .barra__buscar { justify-content: center; width: 36px; padding: 0; margin-left: auto; margin-right: auto; }
.app--compacta .barra__buscar-campo,
.app--compacta .barra__buscar kbd,
.app--compacta .barra__buscar-limpiar { display: none; }

.app--compacta .modulo { justify-content: center; padding: 7px; }
.app--compacta .modulo__nombre,
.app--compacta .modulo__chevron { display: none; }

/* El pie pasa de un renglón (datos + salir) a dos botones de ícono (tema +
   salir): en columna, porque uno al lado del otro no cabe en 60px de ancho. */
.app--compacta .barra__pie { flex-direction: column; justify-content: center; padding: var(--e3) var(--e2); }
.app--compacta .barra__pie-datos { display: none; }
.app--compacta .barra__salir { width: 36px; height: 36px; padding: 0; justify-content: center; }
.app--compacta .barra__salir span { display: none; }

/* El árbol nunca se ve en el carril de 60px: no hay espacio. En su lugar,
   .submodulos se convierte en panel flotante (problema 5). overflow: visible
   es lo que le permite salir del carril sin que .barra__nav lo recorte; el
   modo extendido no toca esta regla y conserva su scroll. */
.app--compacta .barra__nav { overflow: visible; }

/* El árbol en línea nunca se ve en compacto, así haya quedado abierto antes
   de colapsar: la única vía es el panel flotante de :hover/:focus-within, más
   abajo. `.submodulos[data-abierto="true"]` pesa (0,2,0); si esta regla fuera
   solo `.app--compacta .submodulos` empataría y ganaría nada más que por venir
   después en el archivo — un cambio de orden lo rompería sin avisar. Se repite
   el atributo para ganar por especificidad (0,3,0) y no por posición. Las
   reglas de :hover/:focus-within pesan (0,4,0) y siguen ganando encima de
   esta. */
.app--compacta .submodulos[data-abierto="true"] {
    grid-template-rows: 0fr;
    opacity: 0;
    visibility: hidden;
}

.app--compacta .modulo-grupo:hover .modulo,
.app--compacta .modulo-grupo:focus-within .modulo {
    position: absolute; top: 0; left: 0; z-index: 40;
    width: max-content; min-width: 180px; max-width: 260px;
    background: var(--panel); color: var(--texto);
    border: 1px solid var(--borde); border-bottom: 0;
    border-radius: var(--r2) var(--r2) 0 0;
    box-shadow: var(--sombra-flota);
}
.app--compacta .modulo-grupo:hover .modulo__nombre,
.app--compacta .modulo-grupo:focus-within .modulo__nombre,
.app--compacta .modulo-grupo:hover .modulo__chevron,
.app--compacta .modulo-grupo:focus-within .modulo__chevron {
    display: block;
}

.app--compacta .modulo-grupo:hover .submodulos,
.app--compacta .modulo-grupo:focus-within .submodulos {
    display: block; grid-template-rows: unset;
    position: absolute; top: var(--alto-item-modulo); left: 0; z-index: 40;
    width: max-content; min-width: 180px; max-width: 260px;
    margin: 0; padding: var(--e1) 0;
    border: 1px solid var(--borde);
    border-radius: 0 0 var(--r2) var(--r2);
    background: var(--panel); color: var(--texto);
    box-shadow: var(--sombra-flota);
    opacity: 1; visibility: visible;
}
.app--compacta .modulo-grupo:hover .submodulos__interior,
.app--compacta .modulo-grupo:focus-within .submodulos__interior {
    overflow: visible;
}
.app--compacta .modulo-grupo:hover .submodulo,
.app--compacta .modulo-grupo:focus-within .submodulo {
    color: inherit; padding: 7px 12px;
}
.app--compacta .modulo-grupo:hover .submodulo:hover,
.app--compacta .modulo-grupo:focus-within .submodulo:hover {
    background: var(--superficie); color: var(--texto);
}
.app--compacta .modulo-grupo:hover .submodulo[aria-current="page"],
.app--compacta .modulo-grupo:focus-within .submodulo[aria-current="page"] {
    background: var(--info-fondo); color: var(--info);
}

/* --- 5. Estado ----------------------------------------------------------- */

/* Píldora teñida más SILUETA propia. Siete formas distintas y no siete
   colores: entre el 5 y el 8 % de los hombres no distingue rojo de verde, y
   confundir «disponible» con «vencida» tiene consecuencia clínica. El color
   acompaña; nunca informa solo. */
.estado {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 2px 8px 2px 6px;
    border: 1px solid transparent; border-radius: var(--r1);
    font-size: 11.5px; font-weight: 600; white-space: nowrap;
}
.estado__figura { width: 9px; height: 9px; flex: none; background: currentColor; }

/* Las siete formas se eligieron midiendo, no a ojo: se rendiza cada una a su
   tamaño real de 9 px y se compara la diferencia de píxeles entre todos los
   pares. El primer conjunto tenía hexágono y octágono, y a 9 px el octágono
   quedaba a 9,9 del círculo —tres veces más cerca que cualquier otro par—.
   Con barra y triángulo invertido el peor par sube a 41,0: cuadrado contra
   círculo, que nadie confunde. Si alguna vez se agrega una forma, hay que
   volver a medir antes de darla por buena. */
.figura--cuadro    { }
.figura--triangulo { clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.figura--circulo   { border-radius: 50%; }
.figura--rombo     { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.figura--barra     { clip-path: polygon(0 32%, 100% 32%, 100% 68%, 0 68%); }
.figura--tri-abajo { clip-path: polygon(0 0, 100% 0, 50% 100%); }
.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      { color: var(--ok);      background: var(--ok-fondo);      border-color: var(--ok-borde); }
.estado--info    { color: var(--info);    background: var(--info-fondo);    border-color: var(--info-borde); }
.estado--espera  { color: var(--pending); background: var(--pending-fondo); border-color: var(--pending-borde); }
.estado--mal     { color: var(--fail);    background: var(--fail-fondo);    border-color: var(--fail-borde); }
.estado--neutro  { color: var(--tenue);   background: var(--neutro-fondo);  border-color: var(--borde-fuerte); }

/* --- 6. Vencimiento ------------------------------------------------------ */

/* Cifra más barra de vida restante. Las plaquetas duran cinco días y los
   glóbulos rojos cuarenta y dos: la urgencia tiene que leerse sin comparar
   fechas mentalmente. */
.vence { display: inline-flex; flex-direction: column; gap: 3px; width: 74px; }
.vence__cifra { font: 700 13px/1 var(--tipo); }
.vence__barra { display: block; height: 3px; background: var(--superficie); border-radius: 2px; overflow: hidden; }
.vence__vida { display: block; height: 100%; background: var(--ok); }

.vence--critico .vence__cifra { color: var(--fail); }
.vence--critico .vence__vida  { background: var(--fail); }
.vence--aviso   .vence__cifra { color: var(--pending); }
.vence--aviso   .vence__vida  { background: var(--pending); }
.vence--vencida .vence__cifra { color: var(--fail); text-decoration: line-through; }
.vence--vencida .vence__vida  { background: var(--borde-fuerte); }

/* --- 7. Tabla ------------------------------------------------------------ */

/* El padding lateral ya lo puso .app__contenido (para alinear con .cab): acá
   solo falta min-height: 0, sin eso un hijo flex no encoge bajo el alto de su
   contenido y es la página la que scrollea en vez de la tabla. */
.tabla-caja { flex: 1; min-height: 0; overflow: auto; }

.tabla {
    width: 100%; border-collapse: collapse;
    background: var(--panel);
    border: 1px solid var(--borde); border-radius: var(--r3);
    overflow: hidden;
    font-size: 13px;
}
.tabla th {
    position: sticky; top: 0; z-index: 1;
    padding: 7px 12px;
    background: var(--superficie);
    border-bottom: 1px solid var(--borde);
    text-align: left;
    font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--tenue);
}
.tabla td { padding: 5px 12px; border-bottom: 1px solid var(--borde); vertical-align: middle; }
.tabla tbody tr:last-child td { border-bottom: 0; }
.tabla tbody tr:hover td { background: var(--superficie); }

.codigo { font: 12.5px var(--mono); letter-spacing: -.02em; }
.grupo b { font-size: 13.5px; font-weight: 700; }
.grupo em { font-style: normal; font-weight: 700; color: var(--tenue); }

/* --- 8. Controles -------------------------------------------------------- */

.campo { display: flex; flex-direction: column; gap: 5px; }
.campo__etiqueta { font-size: 12px; font-weight: 600; }
.campo__ayuda { font-size: 11.5px; color: var(--tenue); }

.entrada {
    height: 38px; padding: 0 12px;
    background: var(--panel);
    border: 1px solid var(--borde-fuerte); border-radius: 9px;
    color: var(--texto); font: 14px var(--tipo);
}
.entrada:focus { outline: none; border-color: var(--foco); box-shadow: 0 0 0 3px rgba(26, 95, 180, .16); }
.entrada--compacta { height: 32px; font-size: 12.5px; border-radius: var(--r2); }

/* Casilla más etiqueta en línea: patrón nuevo, queda como el de todo checkbox
   que venga después. La etiqueta entra en el área clicable y el alto mínimo
   deja un objetivo táctil de sobra — esto se opera con guantes, y apuntar
   solo al cuadrado de 16 px no alcanza. */
.casilla {
    display: inline-flex; align-items: center; gap: 8px;
    min-height: 36px;
    padding: 6px 8px;
    border-radius: var(--r2);
    color: var(--texto); font-size: 13px;
    cursor: pointer;
}
.casilla:hover { background: var(--superficie); }
.casilla input[type="checkbox"] {
    width: 18px; height: 18px; flex: none; margin: 0;
    accent-color: var(--info);
}

.boton {
    height: 38px; padding: 0 16px;
    border: 0; border-radius: 9px;
    background: linear-gradient(180deg, #228852 0%, #1a6f42 100%);
    color: #fff; font: 600 14px var(--tipo); cursor: pointer;
    box-shadow: 0 1px 2px rgba(20, 80, 50, .28), inset 0 1px 0 rgba(255, 255, 255, .18);
}
.boton:hover { background: linear-gradient(180deg, #1f7d4b 0%, #17653c 100%); }
.boton:disabled { opacity: .55; cursor: not-allowed; }

.boton--secundario {
    background: var(--panel); color: var(--texto);
    border: 1px solid var(--borde-fuerte); box-shadow: none;
}
.boton--secundario:hover { background: var(--superficie); }

.boton--peligro { background: var(--fail); box-shadow: none; }

/* Mismas medidas que .entrada--compacta, misma lógica: una tabla larga con
   paginación y acciones por fila no puede darse el lujo de un botón de 38px. */
.boton--compacta { height: 32px; padding: 0 12px; font-size: 12.5px; border-radius: var(--r2); }

/* --- 9. Avisos ----------------------------------------------------------- */

.aviso {
    display: flex; gap: var(--e2); align-items: flex-start;
    padding: 10px 12px; border: 1px solid; border-radius: 9px;
    font-size: 12.5px; line-height: 1.45;
}
.aviso__figura { width: 9px; height: 9px; flex: none; margin-top: 4px; background: currentColor; }
.aviso--mal    { color: var(--fail);    background: var(--fail-fondo);    border-color: var(--fail-borde); }
.aviso--espera { color: var(--pending); background: var(--pending-fondo); border-color: var(--pending-borde); }
.aviso--info   { color: var(--info);    background: var(--info-fondo);    border-color: var(--info-borde); }

/* Mismo caso que .barra__buscar-limpiar[hidden]: `.aviso` ya trae su propio
   `display`, con igual especificidad que el `[hidden]` del navegador, así que
   hay que devolvérselo explícito o el aviso de alcance nunca desaparece
   aunque el TS le ponga `hidden = true`. */
.aviso[hidden] { display: none; }

/* --- 10. Ingreso --------------------------------------------------------- */

/* Registro de acogida: una tarea, ocho elementos, nada que compita. Aire y
   profundidad no cuestan nada donde no hay datos que tapar — y por eso mismo
   este tratamiento no se traslada a las tablas. */
.opalo {
    position: relative; min-height: 100vh;
    display: grid; place-items: center; padding: var(--e6); overflow: hidden;
    background:
        radial-gradient(58% 46% at 12% 8%,  rgba(246, 198, 205, .62) 0%, rgba(246, 198, 205, 0) 62%),
        radial-gradient(52% 44% at 88% 4%,  rgba(252, 223, 196, .58) 0%, rgba(252, 223, 196, 0) 62%),
        radial-gradient(62% 52% at 96% 72%, rgba(190, 231, 216, .58) 0%, rgba(190, 231, 216, 0) 64%),
        radial-gradient(58% 50% at 6% 92%,  rgba(198, 210, 246, .62) 0%, rgba(198, 210, 246, 0) 64%),
        radial-gradient(46% 40% at 48% 52%, rgba(226, 208, 242, .42) 0%, rgba(226, 208, 242, 0) 66%),
        linear-gradient(168deg, #fdfcfb 0%, #f6f7f9 100%);
}
/* El grano es lo que le quita el aire de degradado sintético. */
.opalo::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    opacity: .3; mix-blend-mode: soft-light;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
}

.ingreso {
    position: relative; width: 396px;
    display: flex; flex-direction: column; gap: var(--e3);
    padding: var(--e5) var(--e5) var(--e4);
    background: rgba(255, 255, 255, .82);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    backdrop-filter: blur(18px) saturate(140%);
    border: 1px solid rgba(255, 255, 255, .9); border-radius: var(--r4);
    box-shadow: var(--sombra-flota);
}
.ingreso__cab { display: flex; align-items: center; gap: var(--e3); }
.ingreso__marca {
    width: 40px; height: 40px; flex: none;
    display: grid; place-items: center;
    border: 1px solid #f3c8c2; border-radius: 12px;
    background: linear-gradient(150deg, #fde7e4 0%, #f8d3ce 100%);
    color: #b3423a;
}
.ingreso__gota {
    width: 19px; height: 19px; background: currentColor;
    /* Gota y no círculo: es lo único figurativo del sistema, y se lo permite
       porque en esta pantalla la marca no compite con ningún dato. */
    clip-path: path("M9.5 0C9.5 0 19 9.2 19 13.4A9.5 9.5 0 0 1 0 13.4C0 9.2 9.5 0 9.5 0Z");
}
.ingreso__titulo { margin: 0; font-size: 17.5px; letter-spacing: -.012em; }
.ingreso__est { margin: 1px 0 0; font-size: 12.5px; color: var(--tenue); }
.ingreso__saludo { margin: 0; font-size: 13.5px; line-height: 1.5; }
.ingreso__nota { margin: 2px 0 0; font-size: 11.5px; color: var(--tenue); line-height: 1.5; }
.ingreso__doc { display: flex; gap: var(--e2); align-items: flex-start; }

@media (prefers-color-scheme: dark) {
    :root:not([data-tema="claro"]) .opalo { background:
        radial-gradient(58% 46% at 12% 8%,  rgba(198, 84, 110, .30) 0%, rgba(198, 84, 110, 0) 62%),
        radial-gradient(52% 44% at 88% 4%,  rgba(206, 128, 52, .26) 0%, rgba(206, 128, 52, 0) 62%),
        radial-gradient(62% 52% at 96% 72%, rgba(52, 168, 132, .28) 0%, rgba(52, 168, 132, 0) 64%),
        radial-gradient(58% 50% at 6% 92%,  rgba(78, 110, 208, .32) 0%, rgba(78, 110, 208, 0) 64%),
        radial-gradient(46% 40% at 48% 52%, rgba(140, 96, 190, .24) 0%, rgba(140, 96, 190, 0) 66%),
        linear-gradient(168deg, #12171d 0%, #171d24 100%); }
    :root:not([data-tema="claro"]) .opalo::after { opacity: .22; mix-blend-mode: overlay; }
    :root:not([data-tema="claro"]) .ingreso { background: rgba(29, 37, 44, .82); border-color: rgba(255, 255, 255, .08); }
    :root:not([data-tema="claro"]) .ingreso__marca { color: #f2a49c; border-color: #4a2c28;
        background: linear-gradient(150deg, #3a2320 0%, #2c1a18 100%); }
}
:root[data-tema="oscuro"] .opalo { background:
    radial-gradient(58% 46% at 12% 8%,  rgba(198, 84, 110, .30) 0%, rgba(198, 84, 110, 0) 62%),
    radial-gradient(52% 44% at 88% 4%,  rgba(206, 128, 52, .26) 0%, rgba(206, 128, 52, 0) 62%),
    radial-gradient(62% 52% at 96% 72%, rgba(52, 168, 132, .28) 0%, rgba(52, 168, 132, 0) 64%),
    radial-gradient(58% 50% at 6% 92%,  rgba(78, 110, 208, .32) 0%, rgba(78, 110, 208, 0) 64%),
    radial-gradient(46% 40% at 48% 52%, rgba(140, 96, 190, .24) 0%, rgba(140, 96, 190, 0) 66%),
    linear-gradient(168deg, #12171d 0%, #171d24 100%); }
:root[data-tema="oscuro"] .opalo::after { opacity: .22; mix-blend-mode: overlay; }
:root[data-tema="oscuro"] .ingreso { background: rgba(29, 37, 44, .82); border-color: rgba(255, 255, 255, .08); }
:root[data-tema="oscuro"] .ingreso__marca { color: #f2a49c; border-color: #4a2c28;
    background: linear-gradient(150deg, #3a2320 0%, #2c1a18 100%); }

/* --- 11. Estados de carga y vacío ---------------------------------------- */

.cargando { padding: var(--e6); text-align: center; color: var(--tenue); font-size: 13px; }
.vacio { padding: var(--e6); text-align: center; color: var(--tenue); font-size: 13px; }

/* --- 12. Utilidades de datos ---------------------------------------------- */

/* Valor ausente que no es un fallo de carga (grupo sin tipificar, documento
   no disponible): tiene que leerse como "no se ha determinado", no como una
   celda que no cargó. El motivo va en `title`; `cursor: help` avisa que hay
   más detalle al pasar por encima, sin depender de que alguien pruebe suerte. */
.texto-tenue { color: var(--tenue); cursor: help; }

/* `persona.nombre_social`, si no es NULL, se muestra siempre —regla del
   sistema, no estilo: es cómo dirigirse a la persona sin tener que buscarlo.
   El nombre legal no desaparece, porque la identificación se hace contra el
   documento, pero pasa a segundo plano. Se llama "persona" y no "donante"
   porque la misma regla vale para paciente.
   Cuando no hay nombre social, el llamador pinta solo un <span> sin esta
   clase: por diseño no lleva ningún tratamiento especial, para que no se lea
   como si le faltara algo. */
.persona-nombre { display: flex; flex-direction: column; gap: 1px; }
.persona-nombre__social { font-weight: 600; }
.persona-nombre__legal { font-size: 11.5px; color: var(--tenue); }

/* --- 13. Paginación -------------------------------------------------------- */

/* Componente genérico: nace acá pero lo va a reusar cualquier listado. */
.paginacion {
    display: flex; flex: none; flex-wrap: wrap;
    align-items: center; justify-content: space-between;
    gap: var(--e3);
    padding-top: var(--e3);
    border-top: 1px solid var(--borde);
}
/* No cae dentro de la regla global de la base (table, .cifra, input, kbd):
   es un <span> suelto y el contador cambia con cada página, así que necesita
   su propia declaración para que las cifras no salten de ancho. */
.paginacion__resumen { color: var(--tenue); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.paginacion__controles { display: flex; gap: var(--e2); }

/* --- 14. Buscar donante ---------------------------------------------------- */

/* Primera pantalla de trabajo real: columna a toda la altura del lienzo, con
   la tabla como único elemento que scrollea. `height: 100%` fija el alto de
   esta caja al que ya le calculó el flex de .app__contenido (ver comentario
   ahí); sin eso la caja crecería con el contenido y sería la página la que
   scrollea, tapando la fila de filtros en cuanto hay más de una pantalla de
   resultados. */
.buscar-donante {
    display: flex; flex-direction: column; gap: var(--e3);
    height: 100%;
    padding-block: var(--e4) 0;
}

/* Envuelve, no corre: a 400 px el campo, el selector, la casilla y el aviso
   de alcance se apilan uno debajo del otro en vez de desbordar. */
.buscar-donante__filtros {
    display: flex; flex: none; flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--e3) var(--e4);
}
.buscar-donante__filtros .campo { flex: 1 1 200px; max-width: 320px; }

/* --- 14a. La hoja --------------------------------------------------------

   Una pantalla que presenta UN registro —una ficha de donante, una de paciente,
   el detalle de una unidad— se dibuja como una hoja apoyada en el escritorio.
   Es transversal a propósito: son todas la misma clase de pantalla y tienen que
   leerse igual.

   No contradice el registro de trabajo. La sombra es una, sobre un contenedor,
   y es lo que separa «este es un expediente» de «esta es una lista». Lo que §12
   prohíbe es la sombra POR FILA, que es ruido multiplicado por doscientos y se
   recompone en cada cuadro del scroll.

   Un listado no lleva `.hoja`: 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);
    /* Margen abajo también: sin él la hoja toca el filo del lienzo al final del
       scroll y deja de parecer una hoja. */
    margin-block: var(--e4) var(--e5);
}

@media (max-width: 560px) {
    /* A 400 px el margen lateral se paga en ancho de contenido. La hoja se
       vuelve del ancho del lienzo y conserva solo el relleno interior. */
    .hoja { padding: var(--e4); border-radius: var(--r2); }
}

/* --- 14b. Ficha del donante ------------------------------------------------

   Dos registros en una pantalla, que es el caso que §12 dejaba abierto. La
   cabecera es un momento de persona —quién es y cómo se le habla— y se permite
   aire y una tipografía grande. De la franja hacia abajo es trabajo: plano,
   apretado, sin sombra.

   La frontera es literal: `--borde` bajo la cabecera. No hay degradado entre
   los dos registros porque no hay transición conceptual, hay un cambio de
   tarea.                                                                      */

.ficha {
    display: flex; flex-direction: column;
    gap: var(--e4);
    min-height: 0;
}

.ficha__cabecera {
    display: flex; flex-wrap: wrap;
    align-items: flex-start; justify-content: space-between;
    gap: var(--e3) var(--e5);
    padding-bottom: var(--e4);
    border-bottom: 1px solid var(--borde);
}

.ficha__identidad { min-width: 0; }

.ficha__nombre {
    margin: 0;
    font-size: 22px; font-weight: 650; line-height: 1.2;
    letter-spacing: -.01em;
}

/* El nombre legal no desaparece cuando hay nombre social: la identificación se
   hace contra el documento. Pasa a segundo plano, no a ninguno. */
.ficha__nombre-legal {
    margin: 2px 0 0;
    font-size: 13px; color: var(--tenue);
}

.ficha__senas {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--e2);
    margin: var(--e2) 0 0;
    font-size: 13px; color: var(--tenue);
}

.ficha__punto { color: var(--borde-fuerte); }

.ficha__estado { flex: none; padding-top: var(--e1); }

/* Registro de trabajo: la franja de datos es una rejilla apretada y sin
   adornos. `auto-fill` y no `auto-fit` para que cuatro datos no se estiren a lo
   ancho de un monitor de mesón y queden ilegibles por separación. */
.ficha__franja {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: var(--e3) var(--e4);
    padding: var(--e3) 0;
}

.ficha__franja--identidad { border-top: 1px solid var(--borde); }

.dato { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.dato__etiqueta {
    font-size: 11px; font-weight: 600;
    letter-spacing: .04em; text-transform: uppercase;
    color: var(--tenue);
}

.dato__valor { font-size: 14px; overflow-wrap: anywhere; }

.ficha__nota { margin: 0; font-size: 12.5px; }

.ficha__seccion { display: flex; flex-direction: column; gap: var(--e2); min-height: 0; }

.ficha__seccion-cab {
    display: flex; flex-wrap: wrap;
    align-items: center; justify-content: space-between;
    gap: var(--e2) var(--e4);
}

.ficha__seccion-titulo {
    margin: 0;
    font-size: 12px; font-weight: 700;
    letter-spacing: .05em; text-transform: uppercase;
    color: var(--tenue);
}

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

/* Diferir es una decisión con consecuencia clínica, así que ocupa la pantalla
   sola en vez de ser un campo más entre otros. Aire, pero sin translucidez:
   esto sigue siendo una pantalla de trabajo. */
/* Va en `--superficie` y no en `--panel`: ahora vive sobre una hoja, y panel
   sobre panel no se distingue de nada. */
.ficha__diferir {
    display: flex; flex-direction: column; gap: var(--e3);
    padding: var(--e4);
    background: var(--superficie);
    border: 1px solid var(--borde);
    border-radius: var(--r2);
}

.ficha__diferir-campos {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: var(--e3) var(--e4);
    align-items: flex-end;
}

.campo--ancho { grid-column: 1 / -1; }

.vacio--compacto { padding: var(--e4); }

/* La fila entera abre la ficha. El cursor y el realce lo anuncian; el foco
   visible lo hace utilizable sin mouse, que es como se navega con guantes. */
.tabla__fila--abrible { cursor: pointer; }
.tabla__fila--abrible:focus-visible {
    outline: 2px solid var(--foco);
    outline-offset: -2px;
}

@media (max-width: 560px) {
    /* El estado baja bajo el nombre en vez de competir por el ancho: a 400 px
       la píldora y el nombre juntos parten el nombre en tres líneas. */
    .ficha__estado { padding-top: 0; }
    .ficha__nombre { font-size: 19px; }
}

/* --- 15. Movimiento -------------------------------------------------------- */

/* Quien pide menos movimiento no lo pide por gusto.
   El retraso va junto con la duración: `.submodulos` posterga su `visibility`
   para que el foco no aterrice en un enlace aplastado, y sin anular el retraso
   el panel seguiría tardando en irse aunque ya no se anime. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
        transition-delay: 0s !important;
    }
}
