/* ── Trinity Views ─────────────────────────────────────────────────────────────
   Capa de CONTENIDO del dashboard: lo que vive dentro de `.container__Main`.
   Hermana de trinity-shell.css (cascarón) y con su mismo lenguaje visual:
   superficies planas, hairlines en vez de sombras, tipografía tenue, acento en
   dosis pequeñas y modo oscuro por `data-theme`.

   CAPA NUEVA, EN PARALELO
   ───────────────────────
   Prefijo `upaep__trinity-*`. NO reemplaza a `.upaep__view*` de style.css: son
   dos sistemas que conviven mientras se migra. Ninguna clase de aquí colisiona
   con las viejas (`upaep__trinity-view-*` ≠ `upaep__view-*`), así que puedes
   cargar ambas hojas y migrar vista por vista.

   Contrato de estado idéntico al de la capa vieja (`.is-active` / `.is-leaving`),
   para que adaptar Atrium sea cambiar los nombres de clase en sus plantillas y
   nada más.

   ÍNDICE
     1. Tokens
     2. Vista        .upaep__trinity-views / -view / -view-header / -toolbar /
                     -view-body / -view-main / -view-aside
     3. Breadcrumb   .upaep__trinity-breadcrumb
     4. Botones      .upaep__trinity-btn (+ acciones con desborde a menú)
     5. Panel        .upaep__trinity-panel (+ header, body, footer, colapsable)
     6. Datos        .upaep__trinity-facts (lista clave–valor)
     7. Vacío        .upaep__trinity-empty
     8. Carga        .upaep__trinity-skeleton
     9. Layouts      .upaep__trinity-cards (rejilla automática) · -bento ·
                     -fieldset · -table-wrap / -table · -stat
                     + capas sobre el core: .upaep__trinity-alert (dos versiones,
                     dos formas .__tinted / .__clean con barra lateral y cinco tipos),
                     .upaep__trinity-tag (etiquetas) y variantes de
                     .upaep__badge y de .upaep__tab
    10. Responsive y movimiento reducido

   Los tokens leen primero los del cascarón (`--upaep-trinity-*`), luego los roles
   del core (`--ui-*`) y por último un literal → la hoja funciona sola, junto al
   cascarón, o con todo el design system cargado.
--------------------------------------------------------------------------------- */

/*? ═══════════════════════════════════════════════════════════════════════════
   1. TOKENS
════════════════════════════════════════════════════════════════════════════ */
:root {
    /*? Superficies y texto (heredados del cascarón si está cargado) */
    --upaep-trinity-view-surface: var(--upaep-trinity-surface, var(--ui-surface, #ffffff));
    --upaep-trinity-view-surface-2: var(--upaep-trinity-surface-2, var(--ui-surface-2, #f4f5f6));
    --upaep-trinity-view-text: var(--upaep-trinity-text, var(--ui-text, #52565A));
    --upaep-trinity-view-text-strong: var(--upaep-trinity-text-strong, var(--ui-text-strong, #1a1a1a));
    --upaep-trinity-view-text-muted: var(--upaep-trinity-text-muted, var(--ui-text-muted, #909496));
    --upaep-trinity-view-divider: var(--upaep-trinity-divider, var(--ui-divider, #e4e6e8));
    --upaep-trinity-view-hover-bg: var(--upaep-trinity-hover-bg, var(--ui-surface-2, #f4f5f6));
    /*? Hover para controles que viven SOBRE EL FONDO DE PÁGINA (cabecera de vista,
       toolbar, botón de regreso). El fondo de página ya es `surface-2`, así que un
       hover con ese mismo valor es invisible: hace falta un escalón más. */
    --upaep-trinity-view-hover-page: var(--ui-surface-hover, #dee2e6);
    --upaep-trinity-view-accent: var(--upaep-trinity-accent, var(--ui-primary, #e10023));
    --upaep-trinity-view-accent-hover: var(--ui-primary-hover, #c2001e);
    --upaep-trinity-view-on-accent: var(--ui-on-primary, #ffffff);
    --upaep-trinity-view-danger: var(--ui-danger, #d33a45);
    --upaep-trinity-view-danger-subtle: var(--ui-danger-subtle, #f8dcdf);
    --upaep-trinity-view-font: var(--upaep-trinity-font, var(--ui-font-base, var(--upaep-ff-fira-sans, system-ui, sans-serif)));

    /*? Ritmo — el espaciado general de la capa es 1rem: es el gap entre piezas
       hermanas (vistas, paneles, tarjetas, tramos de un asistente) y el que
       heredan `--upaep-trinity-cards-gap` y el mosaico. Un solo valor para todo
       el ritmo vertical u horizontal; si una pieza necesita más aire, lo pide
       ella y no se cambia el ritmo de la página. */
    --upaep-trinity-view-gap: var(--upaep-trinity-sp-4, 1rem);
    --upaep-trinity-view-radius: 12px;
    --upaep-trinity-view-maxwidth: 1200px;
    --upaep-trinity-view-transition: .28s cubic-bezier(.4, 0, .2, 1);

    /*? Objetivo accionable mínimo (WCAG 2.2 AA · 2.5.8). Se lee del cascarón si
       está cargado y cae al mismo literal si esta hoja va sola, para que las dos
       capas no puedan separarse en el número. */
    --upaep-trinity-view-target-min: var(--upaep-trinity-target-min, 1.5rem);

    /*? ALTURA DE LA FILA DE CONTROL. 2.4375rem = 39px, que es la caja natural de
       `.upaep__input` del core (16px de texto + .35rem de relleno + 2px de borde,
       arriba y abajo). No es un número elegido: es la medida que ya tienen los
       campos del kit, escrita como token para que todo lo que comparta fila con
       uno —botones incluidos— pueda medir lo mismo sin que nadie la recalcule. */
    --upaep-trinity-control-h: 2.4375rem;

    /*? Cabecera de vista */
    --upaep-trinity-view-header-gap: var(--upaep-trinity-view-gap);
    --upaep-trinity-view-title-fz: var(--upaep-trinity-fz-h1, 1.5rem);
    --upaep-trinity-view-title-fw: var(--upaep-trinity-fw-heading, 600);
    --upaep-trinity-view-subtitle-fz: var(--upaep-trinity-fz-body, .875rem);

    /*? Panel lateral de vista */
    --upaep-trinity-aside-width: 320px;
    --upaep-trinity-aside-bg: var(--upaep-trinity-view-surface);

    /*? Panel de contenido. El relleno sale de los ROLES de la escala, no de un
       numero propio: `1.35rem` (21.6 px) no era ningun peldano, y encima convivia
       con un `1rem` en la cabecera y un `.75rem` en el pie — tres rellenos
       verticales distintos para las tres partes de la MISMA superficie, sin que
       ninguno dijera por que. Ahora la cabecera y el pie comparten la logica de
       BARRA y el cuerpo la de SUPERFICIE, y los tres el mismo relleno lateral,
       que es lo que alinea el titulo con su contenido. */
    --upaep-trinity-panel-padding: var(--upaep-trinity-pad-surface, 1rem);
    --upaep-trinity-panel-radius: var(--upaep-trinity-view-radius);
    --upaep-trinity-panel-bg: var(--upaep-trinity-view-surface);
    --upaep-trinity-panel-border: var(--upaep-trinity-view-divider);
    --upaep-trinity-panel-shadow: none;

    /*? Esqueleto de carga */
    --upaep-trinity-skeleton-bg: var(--upaep-trinity-view-surface-2);
    --upaep-trinity-skeleton-shine: rgb(255 255 255 / .55);
    --upaep-trinity-skeleton-radius: 6px;
    --upaep-trinity-skeleton-duration: 1.4s;
}

[data-theme="dark"] {
    /*? En oscuro surface-2 casi no se distingue del panel: se sube un escalón */
    --upaep-trinity-skeleton-bg: #33333a;
    --upaep-trinity-skeleton-shine: rgb(255 255 255 / .07);
    --upaep-trinity-view-hover-page: var(--ui-surface-hover, #33333a);
}

@media (prefers-color-scheme: dark) {
    [data-theme="auto"] {
        --upaep-trinity-skeleton-bg: #33333a;
        --upaep-trinity-skeleton-shine: rgb(255 255 255 / .07);
        --upaep-trinity-view-hover-page: var(--ui-surface-hover, #33333a);
    }
}


/*? ── `hidden` MANDA sobre el `display` de cualquier pieza ─────────────────
   `hidden` es COMO se oculta algo en HTML, y el navegador lo aplica con
   `[hidden] { display: none }` en su hoja de usuario-agente — que cualquier
   regla de autor gana. Casi todas las piezas de esta capa declaran `display`
   (`flex`, `grid`, `block`), asi que un panel, una tarjeta o una region con
   `hidden` seguia PINTANDOSE: el consumidor alternaba el atributo estandar y no
   pasaba nada. Se reproducia con un estado vacio que aparecia a la vez que la
   tabla de resultados.

   El selector es por SUBCADENA del namespace a proposito: cubre las 20 piezas de
   la familia y las que vengan, sin tener que acordarse de anadirlas. El
   `!important` esta justificado aqui y solo aqui: cualquier variante puede
   redeclarar `display`, y `hidden` no puede perder contra ninguna. */
[class*="upaep__trinity-"][hidden] {
    display: none !important;
}


/*? ═══════════════════════════════════════════════════════════════════════════
   2. VISTA
   Estructura:
     .upaep__trinity-views
       .upaep__trinity-view.is-active
         .upaep__trinity-view-header      cabecera (regreso, títulos, acciones)
         .upaep__trinity-view-toolbar     barra de filtros/acciones (opcional)
         .upaep__trinity-view-body        contenido
           .upaep__trinity-view-main      columna principal
           .upaep__trinity-view-aside     columna lateral (opcional)
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-views {
    box-sizing: border-box;
    width: 100%;
    min-width: 0;
    font-family: var(--upaep-trinity-view-font);
    color: var(--upaep-trinity-view-text);

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

    /*? Red de seguridad contra el desborde horizontal: una sola palabra larga
       (un correo, un folio, un nombre de clase en <code>) ensancha su caja y
       empuja TODA la página, que en móvil se nota como scroll lateral. Los textos
       parten por palabra; `code` y las URLs, por donde sea. */
    & :is(p, h1, h2, h3, h4, dt, dd, li, span, td, th) {
        overflow-wrap: break-word;
    }

    & :is(code, kbd, samp, .upaep__trinity-empty-detail) {
        overflow-wrap: anywhere;
    }
}

/*? Mismo contrato que la capa vieja: solo se ve la vista activa */
.upaep__trinity-view {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-view-gap);
    width: 100%;
    max-width: var(--upaep-trinity-view-maxwidth);
    margin-inline: auto;

    &:not(.is-active):not(.is-leaving) {
        display: none;
    }

    /*? ── U-24 · ROLES DE ANCHO ─────────────────────────────────────────────
       Todas las vistas compartian el mismo tope —1200px— sin importar lo que
       alojaran. En una pantalla de 1920 el area util real es 1632px, asi que un
       tablero o un listado se quedaban en 1200 y dejaban 432px sin usar: no es
       "aire", es una columna de tabla que no se ve y una grafica mas estrecha de
       lo que podria ser. Y al reves: un formulario a 1200px de ancho obliga a
       recorrer la linea entera para leer una etiqueta y su campo.

       El tope no es una constante del sistema: es una propiedad del CONTENIDO.
       Cuatro roles, y el que ya existia (`__fluid`) se queda como el extremo:

         __reading   840px   texto largo y formularios de una columna: la medida
                             manda sobre el hueco disponible
         (por omision) 1200  vista mixta: cabecera, paneles, algo de dato
         __wide      1600px  tableros y listados anchos: la tabla y las graficas
                             usan el ancho que hay, sin dejar de estar centradas
         __fluid     sin tope  lienzos y mapas que no admiten margen

       Sigue siendo UNA sola variable: quien necesite otro valor la redefine sin
       tener que pelearse con un selector. */
    &.__reading {
        --upaep-trinity-view-maxwidth: 840px;
    }

    &.__wide {
        --upaep-trinity-view-maxwidth: 1600px;
    }

    /*? Ancho completo, sin caja centrada */
    &.__fluid {
        --upaep-trinity-view-maxwidth: none;
    }
}

/*? ── Cabecera ─────────────────────────────────────────────────────────────
   Sin banda blanca a sangre: los títulos respiran sobre el fondo de página y
   solo un hairline los separa del contenido. */
.upaep__trinity-view-header {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-view-header-gap);
    padding-bottom: var(--upaep-trinity-sp-4, 1rem);
    border-bottom: 1px solid var(--upaep-trinity-view-divider);

    /*? BLINDAJE — style.css estiliza el TAG `header` (fondo #e6e6e6, float:left,
       position:relative, z-index). Como esta cabecera se marca con <header> por
       semántica, hay que neutralizarlo o sale una banda gris flotada. */
    float: none;
    position: static;
    width: auto;
    height: auto;
    z-index: auto;
    background: transparent;

    /*? Fila títulos + acciones */
    & .upaep__trinity-view-header-row {
        display: flex;
        align-items: flex-end;
        justify-content: space-between;
        gap: var(--upaep-trinity-sp-4, 1rem);
        flex-wrap: wrap;
    }
}

/*? Botón de regreso: enlace tenue, no un botón sólido */
.upaep__trinity-view-back {
    display: inline-flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    align-self: flex-start;
    /*? Es un objetivo accionable real —el camino de vuelta de la vista— así que
       cae bajo 2.5.8 Target Size igual que cualquier botón. Medía 21 px de alto:
       el relleno estaba pensado para que el rótulo no se despegara de la ruta,
       no para el mínimo del objetivo. El suelo se pone con `min-height`, que no
       mueve nada mientras el contenido ya lo supere. */
    min-height: var(--upaep-trinity-view-target-min, 1.5rem);
    padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-1, .25rem);
    margin-left: -.3rem;
    border: 0;
    border-radius: var(--upaep-trinity-radius-sm, 6px);
    background: transparent;
    /*? El rótulo iba en tenue y el icono del mismo color: el control que MÁS se
       usa de una vista de detalle era el menos visible de la cabecera. Ahora el
       texto va en el color de cuerpo y con peso medio, y la flecha en el acento
       —es el único grafismo de color de la cabecera, así que se encuentra sin
       leer. El hover pinta la caja, no solo el texto. */
    color: var(--upaep-trinity-view-text);
    font: inherit;
    font-size: var(--upaep-trinity-fz-meta, .75rem);
    font-weight: var(--upaep-fw-medium, 500);
    text-decoration: none;
    cursor: pointer;
    transition: background .15s ease, color .15s ease;

    & svg {
        width: 1rem;
        height: 1rem;
        color: var(--upaep-trinity-view-accent);
        fill: none;
        stroke: currentColor;
        stroke-width: 2.2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    & p { margin: 0; }

    /*? El botón vive sobre el fondo de PÁGINA, que ya es surface-2: con ese mismo
       valor el hover era invisible en tema claro. Hace falta un escalón más. */
    &:hover {
        background: var(--upaep-trinity-view-hover-page);
        color: var(--upaep-trinity-view-text-strong);
    }

    &:focus-visible {
        outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
        outline-offset: var(--ui-focus-ring-offset, 2px);
    }
}

.upaep__trinity-view-heading {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-sp-1, .25rem);
    min-width: 0;
}

/*? Línea superior opcional (contexto / migas) */
.upaep__trinity-view-eyebrow {
    margin: 0;
    /*? Rol `label`. Medía 11 px: por debajo del suelo de 12 que el sistema fija
       para cualquier rótulo frecuente, y encima en versalitas, que es donde peor
       se lee un tamaño corto. */
    font-size: var(--upaep-trinity-fz-label, .75rem);
    font-weight: var(--upaep-trinity-fw-label, 600);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--upaep-trinity-view-text-muted);
}

.upaep__trinity-view-title {
    margin: 0;
    font-size: var(--upaep-trinity-view-title-fz);
    font-weight: var(--upaep-trinity-view-title-fw);
    line-height: var(--upaep-trinity-lh-heading, 1.25);
    color: var(--upaep-trinity-view-text-strong);
    text-wrap: balance;
}

.upaep__trinity-view-subtitle {
    margin: 0;
    font-size: var(--upaep-trinity-view-subtitle-fz);
    font-weight: var(--upaep-trinity-fw-body, 400);
    line-height: var(--upaep-trinity-lh-body, 1.45);
    color: var(--upaep-trinity-view-text-muted);
}

.upaep__trinity-view-actions {
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    flex-shrink: 0;
    flex-wrap: wrap;

    /*? Con desborde a menú la fila NO envuelve y SÍ se encoge: solo así
       `clientWidth` refleja el ancho disponible real y el runtime puede decidir
       qué mover al menú (ver sección 4). Sin `overflow: hidden`, que recortaría
       el desplegable. */
    &.__overflow {
        flex-wrap: nowrap;
        flex-shrink: 1;
        min-width: 0;
    }
}

/*? ── Toolbar ──────────────────────────────────────────────────────────────
   Fila de filtros/acciones bajo la cabecera. Dos grupos: el que empuja a la
   izquierda y el que se alinea a la derecha (`.__end`).

   U-26 · La barra solo sabia ENVOLVER. Con siete controles medidos a 320px se
   apilaba en CINCO lineas y ocupaba 206px de alto —casi una cuarta parte de la
   pantalla— antes de que empezara el contenido, y nunca movia nada a un menu:
   el desborde a «⋮» existia solo para las acciones de la cabecera de vista.
   Ahora es la MISMA capacidad, pedida con la misma clase, sobre un grupo de la
   barra. No se implementa un segundo menu: lo hidrata el mismo runtime. */
.upaep__trinity-view-toolbar {
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-view-gap);
    flex-wrap: wrap;
    justify-content: space-between;

    & .upaep__trinity-view-toolbar-group.__overflow {
        /*? Igual que en las acciones de la cabecera: la fila NO envuelve y SI se
           encoge, porque solo asi `clientWidth` refleja el espacio real que le
           queda y el runtime puede decidir que sobra. */
        flex-wrap: nowrap;
        flex-shrink: 1;
        min-width: 0;
    }

    & .upaep__trinity-view-toolbar-group {
        display: flex;
        align-items: center;
        gap: var(--upaep-trinity-sp-2, .5rem);
        flex-wrap: wrap;
        min-width: 0;

        &.__end { margin-left: auto; }

        /*? SOLO el grupo que lleva un campo envuelto en el contenedor de campo del
           kit crece hasta llenar la barra. Es la señal de «aquí se consulta»: un
           buscador dejado a su ancho intrínseco —el que da el `size` por defecto
           de un `<input>`, unos 250 px— truncaba su propio marcador de posición en
           una barra de 1150. Lo que la señal deja fuera importa igual: un grupo de
           etiqueta + `<select>` (un periodo, un filtro suelto) NO crece, porque un
           desplegable de «2026-2» estirado a 1150 px es tan defectuoso como un
           buscador de 250. Lo demás —insignias, notas, botones— nunca pidió ancho
           y se queda donde estaba. */
        &:has(.upaep__input-container) { flex: 1 1 18rem; }
    }

    /*? Variante en caja: útil cuando la toolbar acompaña a una tabla */
    &.__boxed {
        padding: var(--upaep-trinity-pad-bar, .75rem);
        background: var(--upaep-trinity-panel-bg);
        border: 1px solid var(--upaep-trinity-panel-border);
        border-radius: var(--upaep-trinity-panel-radius);
    }
}

/*? ── Cuerpo ─────────────────────────────────────────────────────────────── */
.upaep__trinity-view-body {
    display: flex;
    align-items: flex-start;
    gap: var(--upaep-trinity-view-gap);
    min-width: 0;
}

.upaep__trinity-view-main {
    display: flex;
    /*? `flex-basis: 0`, NO `auto`: el ancho lo pone el espacio disponible y
       nunca el contenido. Con `auto` la base del item es su max-content, y
       aunque `min-width: 0` deja encoger, cualquier cálculo intermedio parte de
       un número que no tiene nada que ver con la pantalla. Es el mismo criterio
       que `minmax(0, 5fr)` frente a `5fr` en grid. El aside no se ve afectado:
       es `flex: 0 0 <ancho>`. */
    flex: 1 1 0;
    flex-direction: column;
    gap: var(--upaep-trinity-view-gap);
    min-width: 0;
}

/*? ── Columna lateral de la vista ──────────────────────────────────────────
   En escritorio es una columna fija; con `.__offcanvas` (o en móvil) pasa a
   cajón lateral con scrim. Lo alterna trinity-views.js. */
.upaep__trinity-view-aside {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-sp-4, 1rem);
    flex: 0 0 var(--upaep-trinity-aside-width);
    width: var(--upaep-trinity-aside-width);
    max-width: 100%;
    padding: var(--upaep-trinity-panel-padding);
    background: var(--upaep-trinity-aside-bg);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-view-radius);

    & .upaep__trinity-view-aside-close {
        display: none;
        place-items: center;
        align-self: flex-end;
        width: 2rem;
        height: 2rem;
        margin: -.35rem -.35rem 0 0;
        padding: 0;
        border: 0;
        border-radius: var(--upaep-trinity-radius-md, 8px);
        background: transparent;
        color: var(--upaep-trinity-view-text-muted);
        cursor: pointer;

        & svg {
            width: 1.1rem;
            height: 1.1rem;
            fill: none;
            stroke: currentColor;
            stroke-width: 2;
            stroke-linecap: round;
        }

        &:hover {
            background: var(--upaep-trinity-view-hover-bg);
            color: var(--upaep-trinity-view-text-strong);
        }
    }
}

/*? ── Tercer estado: columna OCULTABLE ────────────────────────────────────
   La capa vieja tenía tres modos (columna, `is-toggle-offcanvas`, `is-offcanvas`).
   Aquí el intermedio es `.__collapsible`: sigue siendo columna en escritorio, pero
   el mismo disparador `[data-trinity-aside]` la esconde y la principal ocupa todo.
   En móvil manda el cajón, como siempre. */
.upaep__trinity-view-aside.__collapsible {
    /*? `overflow: hidden` SIEMPRE, no solo al ocultarse: si aparece al final de la
       animación, al reabrir el contenido se ve desbordar antes de acomodarse. */
    overflow: hidden;
    transition:
        flex-basis var(--upaep-trinity-view-transition),
        width var(--upaep-trinity-view-transition),
        opacity .18s ease;

    /*? Los hijos conservan su ancho mientras la caja se encoge: así el contenido se
       RECORTA en vez de reflowear (el texto no se re-parte en más y más líneas, que
       era lo que hacía que la columna creciera de alto y se viera rara). */
    & > * {
        flex-shrink: 0;
        width: calc(var(--upaep-trinity-aside-width) - 2 * var(--upaep-trinity-panel-padding));
        max-width: 100%;
    }

    &.is-hidden {
        flex-basis: 0;
        width: 0;
        min-width: 0;
        padding-inline: 0;
        border-width: 0;
        opacity: 0;
        pointer-events: none;
    }

    /*? …pero SOLO mientras se mueve. Desplegada y quieta, el recorte ya no
       protege de nada y en cambio se comía el menú de cualquier control que
       flote puesto en la columna (un selectpicker de filtros en el resumen),
       igual que pasaba en el panel plegable. `is-expanded` la pone
       trinity-views.js al terminar la transición y la retira antes de plegar,
       así que la animación sigue recortada de principio a fin.
       Los hijos no necesitan nada: su `max-width: 100%` ya los tiene dentro. */
    &.is-expanded:not(.is-hidden) { overflow: visible; }
}

/*? Cajón lateral */
.upaep__trinity-view-aside.__offcanvas {
    position: fixed;
    inset: 0 0 0 auto;
    z-index: var(--upaep-zindex-offcanvas, 1055);
    width: min(var(--upaep-trinity-aside-width), 92vw);
    height: 100dvh;
    margin: 0;
    border: 0;
    border-left: 1px solid var(--upaep-trinity-panel-border);
    border-radius: 0;
    overflow-y: auto;
    translate: 100% 0;
    transition: translate var(--upaep-trinity-view-transition);

    & .upaep__trinity-view-aside-close { display: grid; }
    &.is-show { translate: 0 0; }
}

.upaep__trinity-view-scrim {
    position: fixed;
    inset: 0;
    z-index: calc(var(--upaep-zindex-offcanvas, 1055) - 1);
    background: rgb(15 18 25 / .45);
    opacity: 0;
    transition: opacity var(--upaep-trinity-view-transition), visibility var(--upaep-trinity-view-transition);

    /*? CRÍTICO: el scrim es un nodo permanente en el <body>. Sin esto, al cerrar
       queda invisible pero cubriendo toda la pantalla y se COME TODOS LOS CLICS
       de la página (parece que la app se congeló). */
    &:not(.is-show) {
        visibility: hidden;
        pointer-events: none;
    }

    &.is-show {
        visibility: visible;
        opacity: 1;
    }
}


/*? ═══════════════════════════════════════════════════════════════════════════
   3. BREADCRUMB
   Estructura:
     <nav class="upaep__trinity-breadcrumb" aria-label="Ruta">
       <ol>
         <li><a href="…">Inicio</a></li>
         <li><a href="…">Vales de caja</a></li>
         <li><span aria-current="page">VC-009</span></li>
       </ol>
     </nav>

   · El separador lo pinta el CSS (`::before` del li), no va en el markup.
   · El último tramo es texto, no enlace, y lleva `aria-current="page"`.
   · Con `data-collapse` el runtime esconde los tramos intermedios que no caben
     detrás de un botón «…» que los despliega (ver trinity-views.js).
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-breadcrumb {
    min-width: 0;
    font-size: var(--upaep-fz12, .75rem);
    line-height: 1.4;
    color: var(--upaep-trinity-view-text-muted);

    /*? Una sola línea: la ruta no se apila, los tramos que no caben se colapsan
       tras el «…» (lo decide el runtime midiendo los tramos). */
    & > ol {
        display: flex;
        align-items: center;
        gap: var(--upaep-trinity-sp-1, .25rem);
        flex-wrap: nowrap;
        margin: 0;
        padding: 0;
        list-style: none;
        min-width: 0;
    }

    & li {
        display: flex;
        align-items: center;
        min-width: 0;
        max-width: 100%;
        /*? Los tramos NO se encogen: si se dejan encoger, la fila «cabe» siempre a
           base de truncar todo («In…», «Vales d…») y el colapso nunca se dispara.
           Al no encogerse, el desborde es real y el runtime esconde los de en medio. */
        flex-shrink: 0;

        /*? El tramo actual sí cede, por si él solo es larguísimo */
        &:last-child {
            flex-shrink: 1;
            min-width: 6rem;
        }

        /*? Separador: chevron tenue antes de cada tramo salvo el primero */
        &:not(:first-child)::before {
            content: "";
            flex-shrink: 0;
            width: .55rem;
            height: .55rem;
            margin: 0 var(--upaep-trinity-sp-1, .25rem);
            background: currentColor;
            opacity: .45;
            mask: var(--upaep-trinity-breadcrumb-sep) center / contain no-repeat;
            -webkit-mask: var(--upaep-trinity-breadcrumb-sep) center / contain no-repeat;
        }

        /*? Tramos escondidos por el colapso automático */
        &[hidden] { display: none; }
    }

    /*? El tramo de la ruta es un enlace de texto SIN clase propia: su caja la fija
       enteramente esta hoja, así que el mínimo de objetivo (2.5.8 Target Size)
       tiene que salir de aquí y no del marcado del consumidor — GAP-A11Y-002.
       Con `line-height: 1.4` sobre 12px y .15rem de relleno la caja medía 22 px.
       Se sube con `min-height` + `line-height`, no con más relleno: el relleno
       cambiaría el ritmo horizontal de la ruta y `display: block` se conserva
       porque el truncado por `text-overflow` no funciona en un contenedor flex. */
    & a,
    & button {
        display: block;
        max-width: 22ch;
        min-height: var(--upaep-trinity-view-target-min, 1.5rem);
        /*? Relleno de escala. La altura de linea se calcula RESTANDO ese mismo
           relleno al objetivo minimo, asi que los dos numeros tienen que salir
           del mismo sitio: si uno se toca y el otro no, la caja deja de medir 24
           px y la brecha de objetivo tactil vuelve sin que nadie lo note. */
        padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-1, .25rem);
        line-height: calc(var(--upaep-trinity-view-target-min, 1.5rem)
            - var(--upaep-trinity-sp-1, .25rem) * 2);
        border: 0;
        border-radius: var(--upaep-trinity-radius-sm, 6px);
        background: transparent;
        color: inherit;
        font: inherit;
        text-decoration: none;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        cursor: pointer;
        transition: background .15s ease, color .15s ease;

        &:hover {
            background: var(--upaep-trinity-view-hover-bg);
            color: var(--upaep-trinity-view-text-strong);
        }

        &:focus-visible {
            outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
            outline-offset: var(--ui-focus-ring-offset, 2px);
        }
    }

    /*? Tramo actual: sin enlace ni hover */
    /*? El tramo actual NO es un objetivo —es texto— así que no necesita el mínimo
       de 2.5.8; sí necesita la misma caja, o la ruta queda con los tramos
       enlazados 2 px más altos que el último y la línea base baila. */
    & [aria-current="page"] {
        display: block;
        max-width: 32ch;
        min-height: var(--upaep-trinity-view-target-min, 1.5rem);
        padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-1, .25rem);
        line-height: calc(var(--upaep-trinity-view-target-min, 1.5rem)
            - var(--upaep-trinity-sp-1, .25rem) * 2);
        color: var(--upaep-trinity-view-text-strong);
        font-weight: var(--upaep-fw-semibold, 600);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/*? Chevron del separador (SVG en línea, sin dependencias de fuente) */
:root {
    --upaep-trinity-breadcrumb-sep: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></svg>');
}

/*? ── Tramos colapsados ────────────────────────────────────────────────────
   El botón «…» y su desplegable los crea el runtime cuando la ruta no cabe. */
.upaep__trinity-breadcrumb-more {
    position: relative;
    display: flex;
    align-items: center;

    & > button {
        letter-spacing: .05em;
    }
}

.upaep__trinity-breadcrumb-menu {
    position: absolute;
    inset: calc(100% + .35rem) auto auto 0;
    z-index: var(--upaep-zindex-dropdown, 1000);
    min-width: 11rem;
    max-width: 18rem;
    padding: var(--upaep-trinity-sp-1, .25rem);
    background: var(--upaep-trinity-panel-bg);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-radius-md, 8px);
    box-shadow: var(--upaep-trinity-view-menu-shadow, 0 8px 24px rgb(16 20 28 / .12));

    &:not(.is-show) { display: none; }

    & a {
        display: block;
        max-width: none;
        padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-2, .5rem);
        border-radius: var(--upaep-trinity-radius-sm, 6px);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}


/*? ═══════════════════════════════════════════════════════════════════════════
   4. BOTONES Y ACCIONES
   `.upaep__trinity-btn` NO es un botón nuevo: es una CAPA sobre `.upaep__btn` del
   core. Se usa siempre junto a él —

       <button class="upaep__btn upaep__trinity-btn __primary">…</button>

   — y en vez de pelearse con sus propiedades le REMAPEA sus tokens
   `--upaep-btn-*`. Así hereda todo lo que el core ya resuelve (ripple, estados,
   iconos, badges anclados, `.upaep__btn-group`) y solo cambia la piel: fantasma
   por defecto, con variantes sólida / contorno / peligro / solo-icono.
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-btn {
    /*? ── Remapeo de los tokens del core ── */
    --upaep-btn-margin: 0;
    /*? 2.375rem (38px) y no 2.125 (34): a 34 el botón se leía como un control
       secundario al lado de un input, y el área de toque quedaba por debajo de
       los 40px que pide una pantalla táctil. No más: 38 + el gap sigue cabiendo
       en una toolbar de una fila. */
    --upaep-btn-height: var(--upaep-trinity-control-h-md, 2.375rem);
    /*? RELLENO DE CONTROL, de la escala. Estaba en `0 .95rem` (15.2 px) y el
       compacto en `0 .7rem` (11.2): dos medidas laterales fuera de escala, y CERO
       relleno vertical — la caja la daba entera el `height`, asi que el texto no
       tenia aire propio y cualquier cambio de alto lo descuadraba. El alto sigue
       mandando sobre la caja; el relleno es ahora su suelo, y esta en escala. */
    --upaep-btn-padding: var(--upaep-trinity-control-pad, .5rem 1rem);
    --upaep-btn-border-size: 1px;
    --upaep-btn-border-color: transparent;
    --upaep-btn-border-radius: var(--upaep-trinity-radius-md, 8px);
    --upaep-btn-bg: transparent;
    --upaep-btn-fz: var(--upaep-fz14, .875rem);
    --upaep-btn-fw: var(--upaep-fw-medium, 500);
    --upaep-btn-cont-fz: var(--upaep-fz14, .875rem);
    --upaep-btn-cont-fw: var(--upaep-fw-medium, 500);
    --upaep-btn-color: var(--upaep-trinity-view-text);
    --upaep-btn-cont-color: var(--upaep-trinity-view-text);
    /*? el hover vive sobre el fondo de página: hace falta un escalón más que surface-2 */
    --upaep-btn-hover-bg: var(--upaep-trinity-view-hover-page);
    --upaep-btn-hover-border-size: 1px;
    --upaep-btn-hover-border-color: transparent;
    --upaep-btn-hover-color: var(--upaep-trinity-view-text-strong);
    --upaep-btn-hover-cont-color: var(--upaep-trinity-view-text-strong);
    --upaep-btn-disabled-bg: transparent;
    --upaep-btn-disabled-border-color: transparent;
    --upaep-btn-disabled-color: var(--upaep-trinity-view-text-muted);

    /*? ── Lo que el core no tokeniza ── */
    flex-shrink: 0;
    gap: var(--upaep-trinity-gap-tight, .25rem);
    /*? `1` es la altura de linea de una etiqueta recortada al ras: con tildes y
       descendentes el texto llega justo al borde de su caja. La escala declara
       una altura de linea de CONTROL para lo que vive dentro de una caja. */
    line-height: var(--upaep-trinity-lh-control, 1.2);
    white-space: nowrap;
    transition: background .15s ease, color .15s ease, border-color .15s ease;

    & svg {
        flex-shrink: 0;
        width: 1rem;
        height: 1rem;
        fill: none;
        stroke: currentColor;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    &:focus-visible {
        outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
        outline-offset: var(--ui-focus-ring-offset, 2px);
    }

    &[aria-disabled="true"] {
        opacity: .6;
        pointer-events: none;
    }

    /*? Acción principal: la única sólida de la vista */
    &.__primary {
        --upaep-btn-bg: var(--upaep-trinity-view-accent);
        --upaep-btn-border-color: var(--upaep-trinity-view-accent);
        --upaep-btn-color: var(--upaep-trinity-view-on-accent);
        --upaep-btn-cont-color: var(--upaep-trinity-view-on-accent);
        --upaep-btn-hover-bg: var(--upaep-trinity-view-accent-hover);
        --upaep-btn-hover-border-color: var(--upaep-trinity-view-accent-hover);
        --upaep-btn-hover-color: var(--upaep-trinity-view-on-accent);
        --upaep-btn-hover-cont-color: var(--upaep-trinity-view-on-accent);
    }

    /*? Acción secundaria: contorno */
    &.__outline {
        --upaep-btn-border-color: var(--upaep-trinity-view-divider);
        --upaep-btn-hover-border-color: var(--upaep-trinity-view-text-muted);
    }

    /*? ── U-02 · ACCIONES POR FUNCION ──────────────────────────────────────
       Hasta aqui el boton tenia UNA variante con color —`__primary`, que pinta el
       acento de MARCA— y una destructiva. Con eso el color no informaba: "guardar"
       y "publicar" salian del mismo rojo institucional que la identidad, y la
       unica forma de saber que hacia un boton era leerlo.

       Estas variantes consumen los roles de FUNCION del kit, no colores. Como el
       resto de la capa, remapean los tokens del core en vez de pisar selectores.

       `__start` y `__continue` estan declaradas pero se pintan en CONTORNO, no en
       relleno: sus rellenos solidos no alcanzan 4.5:1 con texto blanco y esa
       decision (U-12) es del PO/UX-UI. En contorno el color va como texto y borde
       sobre la superficie, que es donde si se puede cumplir en cuanto se cierre.
       `__neutral` y `__negative` estan cerradas y son usables ya. */
    &.__neutral {
        --upaep-btn-color: var(--ui-action-neutral, #52565A);
        --upaep-btn-cont-color: var(--ui-action-neutral, #52565A);
        --upaep-btn-border-color: var(--ui-action-neutral-border, #52565A);
        --upaep-btn-hover-bg: var(--ui-action-neutral, #52565A);
        --upaep-btn-hover-color: var(--ui-on-action-neutral, #ffffff);
        --upaep-btn-hover-cont-color: var(--ui-on-action-neutral, #ffffff);
    }

    &.__negative {
        --upaep-btn-color: var(--ui-action-negative, #CF081E);
        --upaep-btn-cont-color: var(--ui-action-negative, #CF081E);
        --upaep-btn-border-color: var(--ui-action-negative-border, #A80618);
        --upaep-btn-hover-bg: var(--ui-action-negative, #CF081E);
        --upaep-btn-hover-color: var(--ui-on-action-negative, #ffffff);
        --upaep-btn-hover-cont-color: var(--ui-on-action-negative, #ffffff);
    }

    /*? Destructiva: alias historico de `__negative`. Se conserva porque esta
       escrita en paginas publicadas; pasa a consumir el MISMO rol para que las dos
       no puedan divergir. */
    &.__danger {
        --upaep-btn-color: var(--ui-action-negative, var(--upaep-trinity-view-danger));
        --upaep-btn-cont-color: var(--ui-action-negative, var(--upaep-trinity-view-danger));
        --upaep-btn-hover-bg: var(--upaep-trinity-view-danger-subtle);
        --upaep-btn-hover-color: var(--ui-action-negative, var(--upaep-trinity-view-danger));
        --upaep-btn-hover-cont-color: var(--ui-action-negative, var(--upaep-trinity-view-danger));
    }

    /*? Solo icono: caja CUADRADA, asi que el ancho es la altura de control y el
       relleno es cero a proposito — no es un literal fuera de escala, es la
       ausencia declarada de relleno con el icono centrado. */
    &.__icon {
        --upaep-btn-width: var(--upaep-trinity-control-h-md, 2.375rem);
        --upaep-btn-padding: 0;
    }

    /*? Compacto, para toolbars y cabeceras de panel */
    /*? ── El ancho SIEMPRE lo pone el contenido ────────────────────────────
       `.upaep__btn.__sm` del core fija `--upaep-btn-width: 130px` (y `__lg`,
       282px). Como este botón lleva `white-space: nowrap`, un rótulo más largo
       que esa caja NO se corta ni envuelve: se DESBORDA y se encima con el
       botón de al lado. Es la causa de los textos amontonados en una fila de
       acciones con etiquetas largas.

       Se devuelve a `auto` desde aquí. Va en `&.__sm` / `&.__lg` y no en la
       raíz del bloque a propósito: `.upaep__btn.__sm` son DOS clases (0,2,0) y
       `.upaep__trinity-btn` una sola, así que desde la raíz el core ganaría.
       A la misma profundidad manda esta hoja, que carga después. */
    &.__sm,
    &.__lg {
        --upaep-btn-width: auto;
    }

    &.__sm {
        --upaep-btn-height: var(--upaep-trinity-control-h-sm, 2rem);
        --upaep-btn-padding: var(--upaep-trinity-control-pad-sm, .25rem .75rem);
        /*? `--upaep-fz13` NO existe en el core: cada uso caia en su literal de
           reserva, es decir, en un tamano fuera de escala. El rol `meta` es el
           peldano que el diccionario si publica. */
        --upaep-btn-fz: var(--upaep-trinity-fz-meta, .75rem);
        --upaep-btn-cont-fz: var(--upaep-trinity-fz-meta, .75rem);
        line-height: 1.25;

        & svg { width: .875rem; height: .875rem; }
        &.__icon {
            --upaep-btn-width: var(--upaep-trinity-control-h-sm, 2rem);
            --upaep-btn-padding: 0;
        }
    }
}

/*? Red de seguridad: si alguien olvida `.upaep__btn`, el botón sigue siendo
   presentable (los tokens de arriba no tendrían quién los consuma). */
.upaep__trinity-btn:not(.upaep__btn) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: var(--upaep-btn-height);
    width: var(--upaep-btn-width, auto);
    padding: var(--upaep-btn-padding);
    border: var(--upaep-btn-border-size) solid var(--upaep-btn-border-color);
    border-radius: var(--upaep-btn-border-radius);
    background: var(--upaep-btn-bg);
    color: var(--upaep-btn-color);
    font-family: inherit;
    font-size: var(--upaep-btn-fz);
    font-weight: var(--upaep-btn-fw);
    text-decoration: none;
    cursor: pointer;

    & p { margin: 0; }

    &:hover {
        background: var(--upaep-btn-hover-bg);
        border-color: var(--upaep-btn-hover-border-color);
        color: var(--upaep-btn-hover-color);
    }
}

/*? ── Acciones con desborde a menú ──────────────────────────────────────────
   `.upaep__trinity-view-actions.__overflow` — se escriben TODOS los botones en
   línea y el runtime mueve al menú «⋮» los que no caben (mueve los nodos, así
   que conservan sus listeners). `data-keep` fija los que nunca se mueven.
   Sustituye al patrón `.upaep__view-dropdown-buttons` de la capa vieja, que
   exigía duplicar los botones a mano en el markup. */
.upaep__trinity-actions-more {
    position: relative;
    display: none;   /* el runtime lo muestra solo si movió algo */
    align-items: center;

    &.is-active { display: inline-flex; }
}

.upaep__trinity-actions-menu {
    position: absolute;
    inset: calc(100% + .35rem) 0 auto auto;
    z-index: var(--upaep-zindex-dropdown, 1000);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    min-width: 12rem;
    padding: var(--upaep-trinity-sp-1, .25rem);
    background: var(--upaep-trinity-panel-bg);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-radius-md, 8px);
    box-shadow: var(--upaep-trinity-view-menu-shadow, 0 8px 24px rgb(16 20 28 / .12));

    &:not(.is-show) { display: none; }

    /*? Dentro del menú los botones son filas: a lo ancho y alineados a la izquierda.
       Se hace por tokens para no romper el contrato de `.upaep__btn`. */
    & .upaep__trinity-btn,
    & .upaep__btn {
        --upaep-btn-width: 100%;
        --upaep-btn-height: auto;
        --upaep-btn-padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-2, .5rem);
        --upaep-btn-margin: 0;
        --upaep-btn-border-radius: var(--upaep-trinity-radius-sm, 6px);
        --upaep-btn-border-color: transparent;
        --upaep-btn-bg: transparent;
        /*? aquí el fondo ya es una superficie, no la página */
        --upaep-btn-hover-bg: var(--upaep-trinity-view-hover-bg);
        --upaep-btn-hover-border-color: transparent;
        justify-content: flex-start;
        min-height: 2rem;
        box-shadow: none;
    }
}

[data-theme="dark"] {
    --upaep-trinity-view-menu-shadow: 0 8px 24px rgb(0 0 0 / .55);
}


/*? DÓNDE VIVE ESTE BLOQUE, y por qué al final de la sección de botones y no en
   la de la vista: a igual especificidad manda la regla que carga DESPUÉS, y
   `.upaep__trinity-btn.__sm` —dos clases— es exactamente igual de específica que
   `.upaep__trinity-view-toolbar .upaep__trinity-btn`. Escrito más arriba, `__sm`
   ganaba y la fila seguía descuadrada. Es el mismo criterio con el que `__sm`
   devuelve `--upaep-btn-width` a `auto` frente al core. */
/*? ── RANURA DE CONTROL: cómo una región Trinity aloja un campo del kit ─────
   Trinity reparte una fila de controles con `gap` y espera que todos midan lo
   mismo. Los campos del kit no llegan así, y no por error suyo: `.upaep__input`
   y `.upaep__select` traen `margin: 0 0 .5rem 0` —pensada para un formulario
   APILADO, donde ESA margen es la separación entre campos— y una altura `auto`
   que depende de su contenido. Metidos en una fila que ya separa con `gap`, la
   margen es un hueco de más debajo de cada campo y la altura variable deja el
   buscador cinco píxeles por encima del botón de al lado.

   Se corrige donde toca —el contenedor, que es quien impone la fila— y por
   TOKEN, no pisando selectores ajenos: es exactamente lo que `.upaep__trinity-btn`
   ya hace con `--upaep-btn-*`. Vale para cualquier campo del kit, presente o
   futuro, que publique sus tokens con esa convención: la regla no nombra ninguna
   pieza concreta ni depende de cómo esté construida por dentro. */
.upaep__trinity-view-toolbar,
.upaep__trinity-filters,
.upaep__trinity-bulkbar {

    & .upaep__input,
    & .upaep__select {
        --upaep-input-margin: 0;
        --upaep-select-margin: 0;
    }

    /*? El selectpicker no entra por la regla de arriba: su margen no es
       `0 0 .5rem` como el de los campos sino `.5rem 0` —arriba Y abajo—, y
       publica su token con otro nombre. Mismo problema, un token más. */
    & .upaep__selectpicker {
        --upaep-selectpicker-margin: 0;
    }
}

/*? Y donde además COMPARTEN FILA con los botones de la región, una sola altura
   para todos. `.__sm` conserva su tipografía y su relleno compactos —que es lo
   que pide quien lo escribe— pero no su caja: un botón compacto junto a un campo
   de altura completa no es densidad, es una fila descuadrada. */
.upaep__trinity-view-toolbar,
.upaep__trinity-filters,
.upaep__trinity-filters-actions {

    & .upaep__input,
    & .upaep__select {
        --upaep-input-height: var(--upaep-trinity-control-h);
        --upaep-select-height: var(--upaep-trinity-control-h);
    }

    & .upaep__trinity-btn {
        --upaep-btn-height: var(--upaep-trinity-control-h);
    }

    /*? Solo-icono sigue siendo CUADRADO: su ancho es el alto de la fila. */
    & .upaep__trinity-btn.__icon {
        --upaep-btn-width: var(--upaep-trinity-control-h);
    }

    /*? El selectpicker mide 3px MÁS que la fila, y —medido— no es culpa de su
       botón: ese ya da 39px, los mismos que un input. Los tres píxeles salen de
       un `min-height: 2.65rem` (42.4px) que el core fija en el ENVOLTORIO.

       Es la única regla de este bloque que toca una propiedad en vez de un token,
       y es a propósito: ese mínimo está escrito como literal en `style.css` y no
       hay token que remapear. `--upaep-selectpicker-height` no sirve —es `auto`
       y el mínimo le gana—, así que se rebaja el mínimo y la caja la sigue
       poniendo su contenido. Si el core llega a publicarlo, esta regla se cambia
       por el token y se borra el comentario.

       Como la fila alinea por el SUELO, esos tres píxeles se leían como un
       escalón contra el campo de al lado. */
    & .upaep__selectpicker {
        min-height: var(--upaep-trinity-control-h);
    }
}



/*? ═══════════════════════════════════════════════════════════════════════════
   5. PANEL — contenedor base de contenido
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-panel {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--upaep-trinity-panel-bg);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-panel-radius);
    box-shadow: var(--upaep-trinity-panel-shadow);

    /*? Sin caja: solo separa contenido (útil dentro de otro panel) */
    &.__flat {
        --upaep-trinity-panel-bg: transparent;
        border: 0;
        border-radius: 0;
    }

    /*? Barra de acento arriba, para destacar un panel sin recargarlo */
    &.__accent {
        border-top: 2px solid var(--upaep-trinity-view-accent);
    }

    /*? El contenido manda (tablas, mapas): el body pierde su padding */
    &.__bleed .upaep__trinity-panel-body {
        padding: 0;
        overflow: hidden;
    }
}

/*? El mismo blindaje que la cabecera de vista: estas dos partes suelen marcarse
   con <header>/<footer> y style.css estiliza esos tags a nivel global. */
.upaep__trinity-panel-header,
.upaep__trinity-panel-footer {
    float: none;
    position: static;
    width: auto;
    height: auto;
    z-index: auto;
    background: transparent;
}

.upaep__trinity-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--upaep-trinity-view-gap);
    flex-wrap: wrap;
    padding: var(--upaep-trinity-panel-padding);
    border-bottom: 1px solid var(--upaep-trinity-panel-border);
}

.upaep__trinity-panel-heading {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-gap-tight, .25rem);
    min-width: 0;
}

/*? EL NIVEL DEL ENCABEZADO DECIDE EL TAMAÑO. Antes `.upaep__trinity-panel-title`
   medía 15 px fuera cual fuera su etiqueta, así que un `<h2>`, un `<h3>` y un
   `<h4>` salían idénticos: el marcado declaraba una jerarquía que la página no
   enseñaba, y el lector de pantalla y el ojo veían cosas distintas. La clase da
   el ROL —título de panel— y la etiqueta da el NIVEL. */
.upaep__trinity-panel-title {
    margin: 0;
    font-size: var(--upaep-trinity-fz-h3, 1rem);
    font-weight: var(--upaep-trinity-fw-heading, 600);
    line-height: var(--upaep-trinity-lh-heading, 1.25);
    color: var(--upaep-trinity-view-text-strong);
}

h2.upaep__trinity-panel-title { font-size: var(--upaep-trinity-fz-h2, 1.25rem); }
h4.upaep__trinity-panel-title,
h5.upaep__trinity-panel-title,
h6.upaep__trinity-panel-title { font-size: var(--upaep-trinity-fz-h4, .875rem); }

.upaep__trinity-panel-subtitle {
    margin: 0;
    /*? Rol `meta`: acompaña al título, nunca compite con él. */
    font-size: var(--upaep-trinity-fz-meta, .75rem);
    font-weight: var(--upaep-trinity-fw-body, 400);
    line-height: var(--upaep-trinity-lh-body, 1.45);
    color: var(--upaep-trinity-view-text-muted);
}

.upaep__trinity-panel-actions {
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-gap-tight, .25rem);
    flex-shrink: 0;
}

.upaep__trinity-panel-body {
    padding: var(--upaep-trinity-panel-padding);
    min-width: 0;
    /*? Las piezas hermanas del cuerpo (una barra de conteo y una tabla, un texto y un formulario…) se
       separan 1rem SIN que cada demo ponga su margen: antes quedaban pegadas o a ojo (~10px). */
    display: grid;
    align-content: start;
    gap: var(--upaep-trinity-view-gap);

    & > * { min-width: 0; }

    /*? el gap de la rejilla ya separa: sin sumarle el margen propio de estas piezas */
    & > :is(.upaep__trinity-bulkbar, .upaep__trinity-view-toolbar) { margin-top: 0; }
}

.upaep__trinity-panel-footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--upaep-trinity-gap-inline, .5rem);
    flex-wrap: wrap;
    padding: var(--upaep-trinity-panel-padding);
    border-top: 1px solid var(--upaep-trinity-panel-border);
    font-size: var(--upaep-trinity-fz-meta, .75rem);
    color: var(--upaep-trinity-view-text-muted);

    /*? Barra de acciones que no se pierde en formularios largos: queda pegada al
       borde inferior mientras el panel sigue en pantalla. Sin JS. */
    &.__sticky {
        position: sticky;
        bottom: 0;
        z-index: 1;
        background: var(--upaep-trinity-panel-bg);
        border-radius: 0 0 calc(var(--upaep-trinity-panel-radius) - 1px) calc(var(--upaep-trinity-panel-radius) - 1px);
    }
}

/*? ── Panel colapsable (lo alterna trinity-views.js) ───────────────────────
   Acordeón por grid-template-rows: no necesita saber el alto del contenido. */
.upaep__trinity-panel.__collapsible {
    /*? La cabecera es un rectángulo con fondo propio (el hover) dentro de un
       panel redondeado y SIN `overflow: hidden` —no lo puede tener: recortaría
       el pie `__sticky` y las sombras—. Sin radio propio, ese fondo pintaba
       las esquinas superiores en cuadrado justo al pasar el puntero. Se le da
       el radio del panel menos el hairline del borde, como ya hace `__sticky`
       con las inferiores; y plegado, cuando la cabecera ES el panel entero,
       las cuatro. */
    & > .upaep__trinity-panel-header {
        cursor: pointer;
        user-select: none;
        border-radius:
            calc(var(--upaep-trinity-panel-radius) - 1px)
            calc(var(--upaep-trinity-panel-radius) - 1px) 0 0;

        &:hover { background: var(--upaep-trinity-view-hover-bg); }
    }

    &:not(.is-open) > .upaep__trinity-panel-header {
        border-radius: calc(var(--upaep-trinity-panel-radius) - 1px);
    }

    & .upaep__trinity-panel-chevron {
        display: grid;
        place-items: center;
        width: .9rem;
        height: .9rem;
        color: var(--upaep-trinity-view-text-muted);
        transition: transform var(--upaep-trinity-view-transition);

        & svg {
            width: 100%;
            height: 100%;
            fill: none;
            stroke: currentColor;
            stroke-width: 2.2;
            stroke-linecap: round;
            stroke-linejoin: round;
        }
    }

    & > .upaep__trinity-panel-collapse {
        display: grid;
        grid-template-rows: minmax(0, 0fr);
        transition: grid-template-rows var(--upaep-trinity-view-transition);

        /*? El recorte va TAMBIÉN en el envoltorio, no solo en el hijo. Con la fila
           en 0fr el envoltorio mide 0, pero el hijo se seguía pintando a su alto de
           contenido y se salía de la caja del panel (visible al usar un panel
           plegable dentro del aside: el cuerpo plegado aparecía por debajo del
           borde). Con `overflow: hidden` aquí, el alto de la fila manda siempre. */
        overflow: hidden;

        & > * {
            min-height: 0;
            overflow: hidden;
        }
    }

    &:not(.is-open) > .upaep__trinity-panel-header {
        border-bottom-color: transparent;
    }

    &.is-open {
        & > .upaep__trinity-panel-collapse { grid-template-rows: minmax(0, 1fr); }
        & .upaep__trinity-panel-chevron { transform: rotate(180deg); }
    }

    /*? ── Abierto Y QUIETO: se suelta el recorte ────────────────────────────
       El `overflow: hidden` de arriba es del ACORDEÓN, no del panel: hace falta
       mientras la fila va de 0fr a 1fr, y deja de hacer falta en cuanto la
       transición termina —abierta, la fila mide justo lo que el contenido—.
       Manteniéndolo también en reposo recortaba a todo lo que FLOTA desde
       dentro: el menú de un `.upaep__selectpicker`, el del dropdown, el
       datepicker, el autocomplete. Todos son `position: absolute` dentro de su
       propio componente, así que el primer ancestro que recorta se los come.
       El caso que lo destapó: una fila de `.upaep__trinity-filters` con un
       selectpicker dentro de un panel plegable — el menú se cortaba en el borde
       inferior del panel.

       `is-expanded` la escribe trinity-views.js cuando la transición TERMINA (y
       de entrada en los paneles que ya nacen abiertos); la retira ANTES de
       empezar a plegar, así que el recorte cubre la animación entera y el
       desborde que describe el comentario de `-panel-collapse` sigue cubierto. */
    &.is-open.is-expanded > .upaep__trinity-panel-collapse {
        overflow: visible;

        & > * { overflow: visible; }
    }
}

/*? …salvo en `__bleed`, donde el recorte del cuerpo NO es del acordeón: es lo
   que le da al contenido a sangre (una tabla, un mapa) el radio del panel. Ahí
   se conserva, y quien necesite que algo flote desde dentro usa la escotilla
   `.__overflow-visible` de abajo. */
.upaep__trinity-panel.__collapsible.__bleed.is-open.is-expanded
    > .upaep__trinity-panel-collapse
    > .upaep__trinity-panel-body {
    overflow: hidden;
}

/*? ── Escotilla: esta caja deja de recortar ────────────────────────────────
   Las tres cajas de abajo recortan a propósito y no se puede quitar por defecto
   sin romper para qué están (el scroll de la tabla, la rejilla del bento, el
   radio del contenido a sangre). Pero todas pueden contener un control cuyo
   overlay flota —un selectpicker, un dropdown, un datepicker— y entonces el
   recorte se lo come.

   `.__overflow-visible` es el permiso explícito de quien monta la página: «esta
   tabla cabe, no necesita scroll» o «esta tarjeta del bento no desborda». No se
   pone a ciegas: en una tabla que SÍ scrollea, quitarle el recorte la deja
   empujar la página a lo ancho. */
.upaep__trinity-table-wrap.__overflow-visible,
.upaep__trinity-bento > .__overflow-visible,
.upaep__trinity-panel.__bleed .upaep__trinity-panel-body.__overflow-visible {
    overflow: visible;
}


/*? ═══════════════════════════════════════════════════════════════════════════
   6. DATOS CLAVE–VALOR
   La pieza que más se repite en vistas de detalle y en el aside. Se marca con
   una <dl> real (semántica de lista de descripción), con los pares envueltos:

     <dl class="upaep__trinity-facts">
       <div class="upaep__trinity-facts-row">
         <dt>Solicitante</dt><dd>Mario Montes Gómez</dd>
       </div>
     </dl>

   Variantes: `.__stacked` (etiqueta encima) · `.__flush` (sin hairlines)
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-facts {
    margin: 0;
    font-size: var(--upaep-trinity-fz-body, .875rem);

    & .upaep__trinity-facts-row {
        display: grid;
        /*? El 38% es una PROPORCION, y una proporcion no tiene tope: en el aside
           (320px) daba una etiqueta de 121px —bien—, pero en un panel a ancho
           completo daba 432px y el valor quedaba tan lejos de su etiqueta que la
           vista dejaba de emparejarlos. Se acota con `min()`: la proporcion sigue
           mandando donde la columna es estrecha, y donde es ancha manda la
           medida legible. */
        grid-template-columns: minmax(0, min(38%, 15rem)) minmax(0, 1fr);
        align-items: baseline;
        gap: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-3, .75rem);
        padding: var(--upaep-trinity-sp-2, .5rem) 0;
        border-bottom: 1px solid var(--upaep-trinity-view-divider);

        &:last-child { border-bottom: 0; }
    }

    /*? JERARQUÍA DEL PAR, que estaba invertida. `<dt>` hereda `font-weight: bold`
       de la hoja del navegador y Trinity nunca lo neutralizó, así que la ETIQUETA
       salía en 700 y el VALOR en 500: la lista se leía como una columna de
       rótulos con datos al lado, cuando el dato es justamente lo que se viene a
       leer. Ahora la etiqueta es texto secundario y el valor lleva el peso. */
    & dt {
        margin: 0;
        font-weight: var(--upaep-trinity-fw-body, 400);
        color: var(--upaep-trinity-view-text-muted);
        text-wrap: pretty;
    }

    & dd {
        margin: 0;
        color: var(--upaep-trinity-view-text-strong);
        font-weight: var(--upaep-trinity-fw-value, 600);
        min-width: 0;
        overflow-wrap: break-word;
    }

    /*? Etiqueta encima del valor: para columnas estrechas */
    &.__stacked .upaep__trinity-facts-row {
        grid-template-columns: minmax(0, 1fr);
        gap: var(--upaep-trinity-sp-1, .25rem);
    }

    /*? Sin separadores */
    &.__flush .upaep__trinity-facts-row {
        border-bottom: 0;
        padding: var(--upaep-trinity-sp-1, .25rem) 0;
    }
}


/*? ═══════════════════════════════════════════════════════════════════════════
   7. ESTADO VACÍO
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    padding: var(--upaep-trinity-sp-6, 2rem);
    text-align: center;

    /*? Compacto, para dentro de un panel pequeño */
    &.__compact {
        padding: var(--upaep-trinity-sp-4, 1rem);

        & .upaep__trinity-empty-icon {
            width: 2.25rem;
            height: 2.25rem;
        }
    }

    /*? ── Con ilustración en vez de icono ──────────────────────────────────
       El vacío es el sitio donde la marca puede hablar: en vez del icono en su
       disco gris, la ilustración de Brumildo (`maskIcons-sigil/brumildos/`, o
       cualquier `<img>`). El hueco es el MISMO —`-empty-icon`—, así que se
       cambia sin tocar el resto del markup.

       El disco y el recorte se retiran: una ilustración con su propio fondo
       dentro de un círculo gris se ve pegada. Tamaño por token para poder
       ajustarlo por caso (`--upaep-trinity-empty-art`). */
    &.__illustration {
        --upaep-trinity-empty-art: 170px;

        & .upaep__trinity-empty-icon {
            width: auto;
            height: auto;
            max-width: min(var(--upaep-trinity-empty-art), 100%);
            margin-bottom: var(--upaep-trinity-sp-2, .5rem);
            background: none;
            border-radius: 0;
            color: inherit;

            & :is(img, svg) {
                width: 100%;
                height: auto;
                display: block;
            }
        }
    }

    /*? Compacta + ilustración: la mitad de arte, para un panel o un aside */
    &.__illustration.__compact {
        --upaep-trinity-empty-art: 110px;
    }

    /*? ── Falla de carga ───────────────────────────────────────────────────
       Distinto del vacío, y conviene que se distinga: «no hay datos» es un
       resultado válido, «no se pudo cargar» es un error con reintento. Mismo
       esqueleto de markup, icono y acento en peligro. */
    &.__error .upaep__trinity-empty-icon {
        color: var(--upaep-trinity-view-danger);
        background: var(--upaep-trinity-view-danger-subtle);
    }

    /*? Detalle técnico plegado: útil para soporte, invisible para el resto */
    & .upaep__trinity-empty-detail {
        max-width: 46ch;
        margin: var(--upaep-trinity-sp-1, .25rem) 0 0;
        font-size: var(--upaep-trinity-fz-label, .75rem);
        font-family: var(--upaep-ff-space-mono, ui-monospace, monospace);
        color: var(--upaep-trinity-view-text-muted);
        word-break: break-word;

        & summary {
            cursor: pointer;
            list-style: none;
            text-decoration: underline dotted;
        }

        & summary::-webkit-details-marker { display: none; }
    }
}


/*? ── Pasos de un formulario largo ──────────────────────────────────────────
   La barra de pasos es la del core (`.upaep__steps` + `new Steps(el, {progressbar:true})`,
   que mueve `is-active`/`is-complete`). Aquí solo van los PANELES: se muestra el
   del paso vigente y se ocultan los demás, con el mismo contrato `is-active` que
   usan las vistas. */
.upaep__trinity-wizard-step {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-view-gap);

    &:not(.is-active) { display: none; }

    &.is-active { animation: a_trinityStepIn .25s ease; }
}

@keyframes a_trinityStepIn {
    from {
        opacity: 0;
        transform: translateY(.4rem);
    }
}

/*? Pie de navegación del asistente: atrás a la izquierda, avance a la derecha */
.upaep__trinity-wizard-nav {
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    flex-wrap: wrap;

    & .upaep__trinity-wizard-nav-hint {
        margin: 0 auto 0 0;
        font-size: var(--upaep-fz12, .75rem);
        color: var(--upaep-trinity-view-text-muted);
    }
}

.upaep__trinity-empty-icon {
    display: grid;
    place-items: center;
    width: 3rem;
    height: 3rem;
    margin-bottom: var(--upaep-trinity-sp-1, .25rem);
    color: var(--upaep-trinity-view-text-muted);
    background: var(--upaep-trinity-view-surface-2);
    border-radius: 50%;

    & svg {
        width: 50%;
        height: 50%;
        fill: none;
        stroke: currentColor;
        stroke-width: 1.8;
        stroke-linecap: round;
        stroke-linejoin: round;
    }
}

.upaep__trinity-empty-title {
    margin: 0;
    font-size: var(--upaep-trinity-fz-h3, 1rem);
    font-weight: var(--upaep-fw-semibold, 600);
    color: var(--upaep-trinity-view-text-strong);
}

.upaep__trinity-empty-text {
    max-width: 46ch;
    margin: 0;
    font-size: var(--upaep-trinity-fz-meta, .75rem);
    line-height: 1.5;
    color: var(--upaep-trinity-view-text-muted);
    text-wrap: pretty;
}

.upaep__trinity-empty-actions {
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    flex-wrap: wrap;
    justify-content: center;
    margin-top: var(--upaep-trinity-sp-3, .75rem);
}


/*? ═══════════════════════════════════════════════════════════════════════════
   8. ESQUELETO DE CARGA
   Marcado mínimo: <span class="upaep__trinity-skeleton __text"></span>
   Variantes de forma: __title · __text · __circle · __block · __btn
   Repetición: [data-lines="3"] pinta 3 renglones (la última al 60%).
════════════════════════════════════════════════════════════════════════════ */
.upaep__trinity-skeleton {
    display: block;
    width: 100%;
    height: .8rem;
    background: var(--upaep-trinity-skeleton-bg);
    border-radius: var(--upaep-trinity-skeleton-radius);
    background-image: linear-gradient(90deg,
            transparent 0%,
            var(--upaep-trinity-skeleton-shine) 50%,
            transparent 100%);
    background-repeat: no-repeat;
    background-size: 220px 100%;
    background-position: -220px 0;
    animation: a_trinitySkeleton var(--upaep-trinity-skeleton-duration) ease-in-out infinite;

    /*? El esqueleto no se anuncia como contenido */
    &:not([aria-hidden="false"]) { user-select: none; }

    &.__title {
        height: 1.15rem;
        max-width: 16rem;
    }

    &.__text {
        height: .75rem;
        margin-block: var(--upaep-trinity-sp-1, .25rem);
    }

    &.__circle {
        width: 2.5rem;
        height: 2.5rem;
        border-radius: 50%;
        flex-shrink: 0;
    }

    &.__btn {
        width: 7rem;
        height: 2rem;
        border-radius: var(--upaep-trinity-radius-md, 8px);
    }

    &.__block {
        height: 8rem;
        border-radius: var(--upaep-trinity-view-radius);
    }
}

/*? ── Animación del esqueleto: tres modos ───────────────────────────────────
   El defecto es el BARRIDO (un brillo que recorre la pieza de izquierda a
   derecha). Se puede cambiar por pieza o, mejor, en el contenedor: la clase
   también se lee desde cualquier ancestro, así una sección entera cambia de
   modo con una sola clase.

     · barrido (defecto) — sugiere «viene contenido de allá»; es el que mejor
       lee cuando hay muchas piezas: todas barren a la vez y el bloque completo
       se percibe como una sola cosa cargando.
     · `__pulse` — late en opacidad, sin desplazamiento. Para UNA pieza suelta o
       para superficies muy pequeñas, donde el barrido no alcanza a leerse.
     · `__static` — sin animación. Para capturas, PDF, pruebas visuales y
       cualquier sitio donde el movimiento estorbe.

   El barrido necesita un `background-image` propio; al apagarlo hay que
   retirarlo o queda el brillo congelado a media pieza.

   Los selectores NO usan un `.__pulse` a secas: ese modificador es genérico y
   lo usan otros componentes. Se ancla a la pieza, al contenedor destino (el que
   lleva `data-trinity-defer` / `data-trinity-skeleton`, que es de quien consume)
   o a la capa que pinta el runtime. */
.upaep__trinity-skeleton.__pulse,
[data-trinity-skeleton].__pulse .upaep__trinity-skeleton,
.upaep__trinity-skeleton-holder.__pulse .upaep__trinity-skeleton {
    background-image: none;
    animation: a_trinitySkeletonPulse var(--upaep-trinity-skeleton-duration) ease-in-out infinite;
}

.upaep__trinity-skeleton.__static,
[data-trinity-skeleton].__static .upaep__trinity-skeleton,
.upaep__trinity-skeleton-holder.__static .upaep__trinity-skeleton {
    background-image: none;
    animation: none;
}

@keyframes a_trinitySkeletonPulse {
    0%, 100% { opacity: 1; }
    50% { opacity: .45; }
}

/*? ── Capa de esqueleto ────────────────────────────────────────────────────
   El esqueleto se añade como una capa HERMANA del contenido real y este se
   oculta, en vez de reemplazar el `innerHTML`. Reemplazarlo destruye los nodos:
   se pierden las referencias que ya tenga el JS de la página y los listeners
   puestos sobre ellos (en la demo, el asistente por pasos dejaba de existir). */
[data-trinity-skeleton] > *:not(.upaep__trinity-skeleton-holder) {
    display: none !important;
}

/*? Bloque de renglones: `data-lines` repite el patrón sin ensuciar el markup */
.upaep__trinity-skeleton-lines {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-sp-2, .5rem);

    & > .upaep__trinity-skeleton:last-child { max-width: 60%; }
}

/*? ── Plantillas de esqueleto por forma ────────────────────────────────────
   Un esqueleto sirve si se PARECE a lo que va a llegar. Estas tres cubren los
   casos del catálogo; las pinta trinity-views.js con `preset`. */
.upaep__trinity-skeleton-table {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-view-gap);

    & .upaep__trinity-skeleton-row {
        display: grid;
        /*? Las pistas fijas suman 19rem + 3 huecos: por debajo de ~340px eso es un
           mínimo duro que desbordaba el documento en móvil. Con `minmax(0, …)`
           el ancho máximo es el mismo —el aspecto en escritorio no cambia— pero
           las pistas pueden encogerse en vez de empujar el layout. */
        grid-template-columns: minmax(0, 5rem) minmax(0, 1fr) minmax(0, 8rem) minmax(0, 6rem);
        gap: var(--upaep-trinity-view-gap);
        align-items: center;
    }
}

.upaep__trinity-skeleton-form {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr));
    gap: var(--upaep-trinity-view-gap);

    & .upaep__trinity-skeleton-field {
        display: flex;
        flex-direction: column;
        gap: var(--upaep-trinity-sp-2, .5rem);

        /*? etiqueta corta + control alto, como un campo real */
        & > .upaep__trinity-skeleton:first-child { max-width: 7rem; }
        & > .upaep__trinity-skeleton:last-child { height: 2.4rem; }
    }
}

@keyframes a_trinitySkeleton {
    to { background-position: calc(100% + 220px) 0; }
}


/*? ═══════════════════════════════════════════════════════════════════════════
   8b. CARGA EN CURSO — barra, spinner y capa de ocupado
   Tres piezas para que se entienda que algo está pasando, en vez del spinner
   centrado de siempre:

     · `.upaep__trinity-progress`  barra indeterminada bajo el header (lo que hace
                                   GitHub/YouTube): avisa sin tapar nada
     · `.upaep__trinity-spinner`   anillo con degradado cónico y máscara — trazo
                                   fino, sin el borde grueso del spinner clásico;
                                   variante `__dots` de tres puntos
     · `.upaep__trinity-busy`      capa sobre un contenedor que ya tiene contenido:
                                   lo atenúa y centra el spinner, sin perder el
                                   layout ni el scroll

   Regla de uso: primera carga → esqueleto (mantiene la forma). Recarga de algo
   que ya está en pantalla → `busy`. Navegación entre vistas → barra.
════════════════════════════════════════════════════════════════════════════ */

/*? ── Barra de progreso indeterminada ────────────────────────────────────── */
.upaep__trinity-progress {
    position: fixed;
    /*? `-progress-top` y `-progress-color` los remapea la piel trinity: bajo un
       header rojo el acento (rojo) desaparece. Ver trinity-shell.css, seccion 2B. */
    inset: var(--upaep-trinity-progress-top, var(--upaep-trinity-header-height, 56px)) 0 auto 0;
    z-index: calc(var(--upaep-trinity-zindex-header, 1099) + 1);
    height: 3px;
    overflow: hidden;
    background: var(--upaep-trinity-progress-track, color-mix(in srgb, var(--upaep-trinity-view-accent) 18%, transparent));
    opacity: 0;
    transition: opacity .2s ease;
    pointer-events: none;

    &::before {
        content: "";
        position: absolute;
        inset: 0 auto 0 0;
        width: 40%;
        background: linear-gradient(90deg,
                transparent,
                var(--upaep-trinity-progress-color, var(--upaep-trinity-view-accent)),
                transparent);
        animation: a_trinityProgress 1.1s ease-in-out infinite;
    }

    &.is-show { opacity: 1; }
}

@keyframes a_trinityProgress {
    from { transform: translateX(-100%); }
    to { transform: translateX(350%); }
}

/*? ── Progreso de lectura (scroll) ──────────────────────────────────────────
   Barra que se llena según lo que llevas leído de la vista. Misma posición que
   la de carga, pero determinada: no compiten porque nunca están juntas.

   Se resuelve con animación por scroll (`animation-timeline: scroll()`), o sea
   SIN JavaScript en el hilo del scroll. Donde no haya soporte, trinity-views.js
   escribe `--upaep-trinity-scrolled` y el ancho sale de ahí. */
.upaep__trinity-scroll-progress {
    position: fixed;
    inset: var(--upaep-trinity-progress-top, var(--upaep-trinity-header-height, 56px)) 0 auto 0;
    z-index: calc(var(--upaep-trinity-zindex-header, 1099) + 1);
    height: 3px;
    pointer-events: none;
    background: transparent;

    &::before {
        content: "";
        display: block;
        height: 100%;
        width: calc(var(--upaep-trinity-scrolled, 0) * 100%);
        background: linear-gradient(90deg,
                var(--upaep-trinity-progress-color, var(--upaep-trinity-view-accent)),
                color-mix(in srgb, var(--upaep-trinity-progress-color, var(--upaep-trinity-view-accent)) 55%, white));
        transform-origin: 0 50%;
    }
}

@supports (animation-timeline: scroll()) {
    .upaep__trinity-scroll-progress::before {
        width: 100%;
        transform: scaleX(0);
        animation: a_trinityScrolled linear;
        animation-timeline: scroll(root block);
    }
}

@keyframes a_trinityScrolled {
    to { transform: scaleX(1); }
}


/*? ── Volver arriba ─────────────────────────────────────────────────────────
   Aparece solo cuando ya scrolleaste; el runtime le pone `.is-show`. */
.upaep__trinity-totop {
    position: fixed;
    inset: auto 1.25rem 1.25rem auto;
    z-index: calc(var(--upaep-zindex-offcanvas, 1055) - 2);
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: 50%;
    background: var(--upaep-trinity-panel-bg);
    color: var(--upaep-trinity-view-text);
    box-shadow: var(--upaep-trinity-raised-shadow, 0 6px 16px -4px rgb(16 20 28 / .18));
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transform: translateY(.5rem);
    transition: opacity .2s ease, visibility .2s ease, transform .2s ease,
        box-shadow .2s ease, background .2s ease;

    & svg {
        width: 1.15rem;
        height: 1.15rem;
        fill: none;
        stroke: currentColor;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    &.is-show {
        opacity: 1;
        visibility: visible;
        transform: none;
    }

    &:hover {
        background: var(--upaep-trinity-view-hover-bg);
        color: var(--upaep-trinity-view-text-strong);
        box-shadow: var(--upaep-trinity-raised-shadow-hover, 0 14px 28px -8px rgb(16 20 28 / .24));
        transform: translateY(-2px);
    }

    &:focus-visible {
        outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
        outline-offset: var(--ui-focus-ring-offset, 2px);
    }

    /*? En móvil se aparta del pie y de los pulgares */
    @media (width <=640px) {
        inset: auto .85rem .85rem auto;
        width: 2.5rem;
        height: 2.5rem;
    }
}

/*? ── Spinner de anillo ──────────────────────────────────────────────────── */
.upaep__trinity-spinner {
    --upaep-trinity-spinner-size: 2rem;
    --upaep-trinity-spinner-thickness: 3px;
    --upaep-trinity-spinner-color: var(--upaep-trinity-view-accent);

    display: inline-block;
    flex-shrink: 0;
    width: var(--upaep-trinity-spinner-size);
    height: var(--upaep-trinity-spinner-size);
    border-radius: 50%;
    /*? El degradado cónico da la cola difuminada; la máscara deja solo el aro, así
       que el grosor se controla con una variable y no con `border`. */
    background: conic-gradient(from 0deg,
            transparent 0deg,
            color-mix(in srgb, var(--upaep-trinity-spinner-color) 35%, transparent) 140deg,
            var(--upaep-trinity-spinner-color) 340deg,
            transparent 360deg);
    mask: radial-gradient(farthest-side,
            transparent calc(100% - var(--upaep-trinity-spinner-thickness)),
            #000 calc(100% - var(--upaep-trinity-spinner-thickness) + .5px));
    -webkit-mask: radial-gradient(farthest-side,
            transparent calc(100% - var(--upaep-trinity-spinner-thickness)),
            #000 calc(100% - var(--upaep-trinity-spinner-thickness) + .5px));
    animation: a_trinitySpin .75s linear infinite;

    &.__sm {
        --upaep-trinity-spinner-size: 1.15rem;
        --upaep-trinity-spinner-thickness: 2px;
    }

    &.__lg {
        --upaep-trinity-spinner-size: 3rem;
        --upaep-trinity-spinner-thickness: 4px;
    }

    /*? Sobre fondos de color (botón primario, barra de marca) */
    &.__on-accent {
        --upaep-trinity-spinner-color: var(--upaep-trinity-view-on-accent);
    }

    &.__muted {
        --upaep-trinity-spinner-color: var(--upaep-trinity-view-text-muted);
    }
}

@keyframes a_trinitySpin {
    to { transform: rotate(1turn); }
}

/*? ── Spinner orbital (para el bloqueo por etapas) ──────────────────────────
   Dos arcos girando en sentidos opuestos a distinta velocidad, con un núcleo que
   late. Se lee como «hay varias cosas ocurriendo», que es justo lo que pasa en un
   proceso por etapas — a diferencia del anillo simple, que solo dice «espera». */
.upaep__trinity-orbit {
    --upaep-trinity-orbit-size: 3.25rem;
    --upaep-trinity-orbit-color: var(--upaep-trinity-view-accent);

    position: relative;
    display: inline-grid;
    place-items: center;
    width: var(--upaep-trinity-orbit-size);
    height: var(--upaep-trinity-orbit-size);
    flex-shrink: 0;

    /*? Arco exterior */
    &::before,
    &::after {
        content: "";
        position: absolute;
        border-radius: 50%;
        border: 2px solid transparent;
    }

    &::before {
        inset: 0;
        border-top-color: var(--upaep-trinity-orbit-color);
        border-right-color: color-mix(in srgb, var(--upaep-trinity-orbit-color) 35%, transparent);
        animation: a_trinitySpin 1.1s linear infinite;
    }

    /*? Arco interior, al revés y más lento */
    &::after {
        inset: 22%;
        border-bottom-color: var(--upaep-trinity-orbit-color);
        border-left-color: color-mix(in srgb, var(--upaep-trinity-orbit-color) 30%, transparent);
        animation: a_trinitySpin 1.6s linear infinite reverse;
    }

    /*? Núcleo que late */
    & > span {
        width: 22%;
        height: 22%;
        border-radius: 50%;
        background: var(--upaep-trinity-orbit-color);
        animation: a_trinityCore 1.4s ease-in-out infinite;
    }

    &.__sm { --upaep-trinity-orbit-size: 2rem; }
    &.__on-accent { --upaep-trinity-orbit-color: var(--upaep-trinity-view-on-accent); }
}

@keyframes a_trinityCore {
    0%, 100% { transform: scale(.7); opacity: .55; }
    50% { transform: scale(1); opacity: 1; }
}


/*? ── Tres puntos ────────────────────────────────────────────────────────── */
.upaep__trinity-dots {
    display: inline-flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-1, .25rem);

    & > span {
        width: .45rem;
        height: .45rem;
        border-radius: 50%;
        background: var(--upaep-trinity-view-accent);
        animation: a_trinityDots 1s ease-in-out infinite;

        &:nth-child(2) { animation-delay: .15s; }
        &:nth-child(3) { animation-delay: .3s; }
    }

    &.__muted > span { background: var(--upaep-trinity-view-text-muted); }
}

@keyframes a_trinityDots {
    0%, 80%, 100% { opacity: .25; transform: translateY(0); }
    40% { opacity: 1; transform: translateY(-.2rem); }
}

/*? ── Carga bloqueante (modal) ─────────────────────────────────────────────
   Para procesos que NO se pueden interrumpir: guardar, cambiar de institución,
   generar un reporte. Backdrop + tarjeta centrada con spinner y texto. Es la
   única de las cuatro que bloquea; si el usuario puede seguir trabajando, usa
   `busy` o la barra. */
.upaep__trinity-loading {
    position: fixed;
    inset: 0;
    z-index: var(--upaep-zindex-modal, 1060);
    display: grid;
    place-items: center;
    padding: var(--upaep-trinity-sp-4, 1rem);
    background: rgb(15 18 25 / .5);
    backdrop-filter: blur(2px);
    opacity: 0;
    visibility: hidden;
    transition: opacity .2s ease, visibility .2s ease;

    &.is-show {
        opacity: 1;
        visibility: visible;
    }

    & .upaep__trinity-loading-box {
        display: grid;
        justify-items: center;
        gap: var(--upaep-trinity-view-gap);
        min-width: min(19rem, 100%);
        max-width: 26rem;
        padding: var(--upaep-trinity-sp-5, 1.5rem) var(--upaep-trinity-sp-5, 1.5rem);
        text-align: center;
        background: var(--upaep-trinity-panel-bg);
        border: 1px solid var(--upaep-trinity-panel-border);
        border-radius: var(--upaep-trinity-view-radius);
        box-shadow: 0 18px 48px rgb(0 0 0 / .28);
        transform: translateY(.5rem) scale(.98);
        transition: transform .2s ease;
    }

    &.is-show .upaep__trinity-loading-box {
        transform: none;
    }

    & .upaep__trinity-loading-title {
        margin: 0;
        font-size: var(--upaep-fz15, .9375rem);
        font-weight: var(--upaep-fw-semibold, 600);
        color: var(--upaep-trinity-view-text-strong);
        text-wrap: balance;
    }

    & .upaep__trinity-loading-text {
        margin: 0;
        font-size: var(--upaep-trinity-fz-meta, .75rem);
        line-height: 1.45;
        color: var(--upaep-trinity-view-text-muted);
        text-wrap: pretty;
    }
}

/*? ── Capa de ocupado sobre un contenedor ─────────────────────────────────
   Se activa con `aria-busy="true"` — el mismo atributo que ya se usa para
   accesibilidad, así no hay dos fuentes de verdad. El contenido se atenúa y
   deja de ser interactivo, pero conserva su alto: la página no salta. */
.upaep__trinity-busy {
    position: relative;

    &[aria-busy="true"] {
        pointer-events: none;

        & > *:not(.upaep__trinity-busy-layer) {
            opacity: .4;
            filter: saturate(.6);
            transition: opacity .2s ease;
        }

        & > .upaep__trinity-busy-layer { display: grid; }
    }

    /*? La capa la inyecta trinity-views.js si no está en el markup */
    & > .upaep__trinity-busy-layer {
        display: none;
        position: absolute;
        inset: 0;
        z-index: 2;
        place-items: center;
        align-content: center;
        gap: var(--upaep-trinity-sp-2, .5rem);
        justify-items: center;
        background: color-mix(in srgb, var(--upaep-trinity-panel-bg) 55%, transparent);
        backdrop-filter: blur(1px);
        border-radius: inherit;

        & .upaep__trinity-busy-text {
            margin: 0;
            font-size: var(--upaep-fz12, .75rem);
            color: var(--upaep-trinity-view-text-muted);
        }
    }
}


/*? ═══════════════════════════════════════════════════════════════════════════
   9. LAYOUTS DE CONTENIDO
   Solo lo que el core NO tiene. Ya existen y se REUTILIZAN:
     · `.upaep__row` + `.upaep__col` / `__col-*`  → rejilla de 12 columnas manual
     · `.upaep__masonry` + `.upaep__masonry-item` → mampostería por `columns` CSS
   Lo que faltaba es la rejilla AUTOMÁTICA (no declaras columnas, se ajustan al
   ancho) y el bento (celdas de distinto tamaño).
════════════════════════════════════════════════════════════════════════════ */

/*? ── Rejilla automática de tarjetas ───────────────────────────────────────
   Para colecciones DINÁMICAS: no se declara el número de columnas, se declara el
   ancho mínimo de tarjeta y el navegador decide cuántas caben. */
.upaep__trinity-cards {
    --upaep-trinity-cards-min: 260px;
    --upaep-trinity-cards-gap: var(--upaep-trinity-view-gap);

    /*? ── U-25 · POR QUE `auto-fit` Y NO `auto-fill` ────────────────────────
       Era `auto-fill`, y el propio comentario de esta regla decia que `auto-fit`
       "es lo que quieres casi siempre" — pero habia que pedirlo con una clase.
       Un valor por omision que casi nunca es el correcto no es un valor por
       omision, es una trampa.

       Lo que producia, medido con cinco tarjetas equivalentes: a 1440px la ultima
       fila quedaba con UNA tarjeta de 276px y un hueco de 876px, es decir tres
       columnas vacias a su derecha; a 1920px, 912px. Ese hueco no se lee como
       espacio, se lee como contenido que falta.

       `auto-fit` colapsa las pistas vacias y reparte el sobrante entre las
       tarjetas que hay: siguen siendo hermanas del mismo rol y siguen midiendo lo
       mismo entre si. El comportamiento anterior no se pierde, se pide:
       `.__fill` cuando de verdad interese conservar el ancho de tarjeta y dejar
       el hueco (una rejilla que se va a completar, por ejemplo). */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(var(--upaep-trinity-cards-min), 100%), 1fr));
    gap: var(--upaep-trinity-cards-gap);
    align-items: start;

    &.__sm { --upaep-trinity-cards-min: 190px; }
    &.__lg { --upaep-trinity-cards-min: 330px; }

    /*? ── Cómo se reparte el sobrante ───────────────────────────────────────
       `.__fill`  (auto-fill) — las tarjetas conservan su ancho minimo y el
                  sobrante queda como hueco a la derecha. Solo para rejillas que
                  se van a completar.
       `.__fit`   (auto-fit) — es lo que hace la rejilla por omision; la clase se
                  conserva porque esta escrita en paginas publicadas y decirlo
                  explicitamente no molesta.
       `.__count` — las columnas son exactamente el número de tarjetas (hasta el
                  máximo que quepa). Lo calcula trinity-views.js contando hijos,
                  y sirve cuando 3 tarjetas deben verse como 3 columnas iguales y
                  no como 3 estiradas dentro de una rejilla de 4. */
    &.__fill {
        grid-template-columns: repeat(auto-fill, minmax(min(var(--upaep-trinity-cards-min), 100%), 1fr));
    }

    &.__fit {
        grid-template-columns: repeat(auto-fit, minmax(min(var(--upaep-trinity-cards-min), 100%), 1fr));
    }

    &.__count {
        --upaep-trinity-cards-count: 4;
    }

    /*? El reparto lo escribe el runtime —mira ancho disponible Y numero de piezas—
       y lo anuncia con `data-trinity-count`. Hasta que llega, manda el `auto-fit`
       de arriba: la rejilla nunca se queda sin columnas por esperar al JS. */
    &[data-trinity-count] {
        grid-template-columns: repeat(var(--upaep-trinity-cards-count), minmax(0, 1fr));
    }

    /*? Tarjetas de la misma altura dentro de su fila.
       Por defecto la rejilla es `align-items: start`, así que cada tarjeta mide lo
       que mide su contenido. Consecuencia que despista: un `height: 100%` en la
       tarjeta NO hace nada sin este modificador —se resuelve contra una fila de
       altura automática—, y el efecto es que se ignora en silencio. Si quieres
       alturas iguales, esto es lo que hay que pedir. */
    /*? ── Todas las tarjetas del mismo alto ────────────────────────────────
       Por defecto la rejilla es `align-items: start`: cada tarjeta mide lo que
       su contenido, y una fila con una tarjeta de tres líneas y otra de una se
       ve escalonada. `__even` las estira a la altura de la más alta.

       Estirar la CELDA no basta: la tarjeta de dentro sigue midiendo su
       contenido, así que se le da `height: 100%` y, al cuerpo del panel, que
       crezca — con eso el pie (acciones, enlaces) queda alineado en toda la
       fila, que es lo que de verdad se nota. */
    &.__even {
        align-items: stretch;

        & > * { height: 100%; }

        & > .upaep__trinity-panel > .upaep__trinity-panel-body,
        & > .upaep__trinity-tile > .upaep__trinity-tile-text {
            flex: 1 1 auto;
        }
    }
}

/*? U-25 · RED DE SEGURIDAD, NO REGLA PRINCIPAL.
   Con `__count` y sin runtime, cuatro columnas de 80px no le sirven a nadie, asi
   que por debajo del cascaron movil manda el ancho minimo. Pero cuando el runtime
   SI esta, esta regla era el problema y no la solucion: pisaba el recuento con un
   `auto-fit` que reparte por ancho y no mira cuantas piezas hay —cuatro tarjetas a
   600px daban tres columnas y una huerfana con 390px de hueco al lado—.

   `[data-trinity-count]` lo escribe el runtime al gobernar la rejilla: mientras no
   este, manda esto; en cuanto esta, manda el recuento, que ya mira ancho Y numero
   de piezas. */
@media (width <=780px) {
    .upaep__trinity-cards.__count:not([data-trinity-count]) {
        grid-template-columns: repeat(auto-fit, minmax(min(var(--upaep-trinity-cards-min), 100%), 1fr));
    }
}

/*? ── Dos columnas asimétricas que SÍ se encogen ───────────────────────────
   `grid-template-columns: 5fr 7fr` parece equivalente, pero NO lo es: el mínimo
   de una pista `fr` es `auto`, o sea min-content. Si dentro hay algo ancho (una
   tabla, un tracker cuyos pasos miden 200px cada uno) la columna crece y arrastra
   la página entera al desborde horizontal — con su `overflow-x: auto` intacto,
   porque un scroller solo scrollea si su ancho está limitado.

   `minmax(0, Nfr)` pone ese mínimo en 0: las columnas ceden y lo de dentro
   scrollea. Es el mismo problema que la cadena de `min-width: 0` en las tabs.

     <div class="upaep__trinity-split" style="--upaep-trinity-split-a: 5fr; --upaep-trinity-split-b: 7fr"> */
.upaep__trinity-split {
    --upaep-trinity-split-a: 1fr;
    --upaep-trinity-split-b: 1fr;

    display: grid;
    grid-template-columns:
        minmax(0, var(--upaep-trinity-split-a))
        minmax(0, var(--upaep-trinity-split-b));
    gap: var(--upaep-trinity-view-gap);
    align-items: start;

    & > * { min-width: 0; }

    @media (width <=780px) {
        grid-template-columns: minmax(0, 1fr);
    }
}


/*? ── Bento ────────────────────────────────────────────────────────────────
   Rejilla de celdas desiguales. `grid-auto-flow: dense` rellena los huecos que
   dejan las piezas grandes, así no hacen falta posiciones explícitas. */
.upaep__trinity-bento {
    --upaep-trinity-bento-cols: 4;
    --upaep-trinity-bento-row: 150px;

    display: grid;
    grid-template-columns: repeat(var(--upaep-trinity-bento-cols), minmax(0, 1fr));
    grid-auto-rows: var(--upaep-trinity-bento-row);
    grid-auto-flow: dense;
    gap: var(--upaep-trinity-view-gap);

    & > * {
        min-width: 0;
        overflow: hidden;
    }

    /*? Modificadores de tamaño sobre cualquier hijo */
    & > .__wide { grid-column: span 2; }
    & > .__wide-3 { grid-column: span 3; }
    & > .__full { grid-column: 1 / -1; }
    & > .__tall { grid-row: span 2; }
    & > .__big {
        grid-column: span 2;
        grid-row: span 2;
    }

    @media (width <=1200px) {
        --upaep-trinity-bento-cols: 2;

        & > .__wide-3 { grid-column: span 2; }
    }

    @media (width <=640px) {
        --upaep-trinity-bento-cols: 1;

        & > .__wide,
        & > .__wide-3,
        & > .__big { grid-column: span 1; }
        & > .__big,
        & > .__tall { grid-row: span 1; }
    }
}

/*? ── Sección de formulario ────────────────────────────────────────────────
   Agrupa campos con un título y un hairline. Dentro se usa el agrupamiento
   canónico del uikit (`upaep__label` fuera + `upaep__input-container`) y la
   rejilla `__row __row-gap` + `__col-*`; esta capa NO reinventa los campos. */
.upaep__trinity-fieldset {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-view-gap);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;

    &:not(:last-child) {
        padding-bottom: var(--upaep-trinity-view-gap);
        border-bottom: 1px solid var(--upaep-trinity-view-divider);
    }

    & .upaep__trinity-fieldset-legend {
        display: flex;
        align-items: baseline;
        gap: var(--upaep-trinity-sp-2, .5rem);
        flex-wrap: wrap;
        width: 100%;
        padding: 0;
        margin: 0;
        font-size: var(--upaep-fz14, .875rem);
        font-weight: var(--upaep-fw-semibold, 600);
        color: var(--upaep-trinity-view-text-strong);

        & small {
            font-size: var(--upaep-fz12, .75rem);
            font-weight: var(--upaep-fw-regular, 400);
            color: var(--upaep-trinity-view-text-muted);
        }
    }
}

/*? ── Envoltorio de tabla ──────────────────────────────────────────────────
   Las tablas anchas scrollean DENTRO de su caja, nunca empujan la página.
   Sirve igual para una tabla normal o para `.upaep__customtable`. */
.upaep__trinity-table-wrap {
    width: 100%;
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: var(--upaep-trinity-scrollbar-width, thin);
    scrollbar-color: var(--upaep-trinity-scrollbar-thumb) var(--upaep-trinity-scrollbar-track);

    /*? La tabla necesita CAJA. Sin filo ni superficie propia, y con el mismo
       fondo que el panel, no se veía dónde empieza y dónde acaba: las filas
       flotaban sobre el fondo y la última se perdía. El radio se recorta con el
       propio `overflow`, así que la esquina de la primera y la última celda
       quedan dentro. */
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-radius-md, 8px);
    background: var(--upaep-trinity-view-surface);
}

/*? …salvo dentro de un panel `__bleed`, donde la caja YA es el panel: ahí el
   filo propio de la tabla se sumaba al del panel y salía una línea doble. */
.upaep__trinity-panel.__bleed .upaep__trinity-table-wrap {
    border: 0;
    border-radius: 0;
    background: transparent;
}

/*? ── Barra de acciones en lote ─────────────────────────────────────────────
   En cuanto se puede marcar más de un elemento, las acciones dejan de ser por
   elemento y pasan a ser sobre el conjunto. Aparece solo cuando hay algo marcado
   y lo primero que dice es CUÁNTOS — sin eso nadie sabe sobre qué va a actuar.

   NO es solo para tablas: sirve igual sobre una rejilla de tarjetas, una lista de
   archivos adjuntos o un árbol. La pieza no lee casillas ni mantiene selección:
   quién está marcado lo sabe la vista, que enciende `is-show` y escribe la cuenta.

   DOS POSICIONES, y la diferencia importa:
     · por defecto es `sticky` al fondo de SU contenedor → la barra pertenece al
       panel y desaparece al salir de él (varias listas en una pantalla, cada una
       con su barra);
     · `.__fixed` (alias histórico `.__floating`) la ancla a la VENTANA, centrada
       abajo → una sola selección por pantalla, para listas muy largas.
   Sticky es el defecto porque no tapa nada del contenido de al lado. */
.upaep__trinity-bulkbar {
    position: sticky;
    bottom: 0;
    z-index: 3;
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-view-gap);
    flex-wrap: wrap;
    margin-top: var(--upaep-trinity-view-gap);
    padding: var(--upaep-trinity-pad-bar, .75rem);
    background: var(--upaep-trinity-view-surface-2);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-radius-md, 8px);
    opacity: 0;
    visibility: hidden;
    transform: translateY(.4rem);
    transition: opacity .18s ease, transform .18s ease, visibility .18s ease;

    &.is-show {
        opacity: 1;
        visibility: visible;
        transform: none;
    }

    /*? Los botones van con caja. La barra ya vive sobre `surface-2`, así que un
       botón fantasma —sin filo ni fondo— se leía como texto suelto: no parecía
       pulsable. Se les da la superficie y un filo, y el destructivo conserva su
       color de texto (el relleno rojo llega solo al pasar). */
    & .upaep__trinity-btn:not(.__primary) {
        --upaep-btn-bg: var(--upaep-trinity-view-surface);
        --upaep-btn-border-color: var(--upaep-trinity-view-divider);
        --upaep-btn-hover-border-color: var(--upaep-trinity-view-text-muted);
    }

    & .upaep__trinity-bulkbar-count {
        margin: 0 auto 0 0;
        font-size: var(--upaep-trinity-fz-meta, .75rem);
        color: var(--upaep-trinity-view-text);

        & b {
            font-weight: var(--upaep-fw-semibold, 600);
            color: var(--upaep-trinity-view-text-strong);
            font-variant-numeric: tabular-nums;
        }
    }

    /*? Anclada a la ventana, para listas muy largas. `__floating` era el nombre
       original y se conserva para no romper el markup que ya lo usa. */
    &.__fixed,
    &.__floating {
        position: fixed;
        inset: auto auto 1.25rem 50%;
        translate: -50% 0;
        width: min(44rem, calc(100vw - 2rem));
        margin: 0;
        background: var(--upaep-trinity-panel-bg);
        box-shadow: var(--upaep-trinity-raised-shadow, 0 8px 24px rgb(16 20 28 / .18));

        &.is-show { transform: none; }
    }

    @media (width <=640px) {
        & .upaep__trinity-bulkbar-count {
            flex: 1 0 100%;
            margin-bottom: var(--upaep-trinity-sp-1, .25rem);
        }
    }
}

/*? Tabla simple en el lenguaje nuevo (hairlines, cabecera tenue, sin cebra) */
.upaep__trinity-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--upaep-trinity-fz-meta, .75rem);

    /*? Centrado POR DEFECTO. En una tabla de dashboard la mayoría de las
       columnas son cortas (folios, fechas, estados, importes) y a la izquierda
       dejaban ríos de blanco a la derecha de cada celda. Para volver a la
       izquierda: `.__align-start` en la tabla (toda) o `.__start` en la celda
       (una columna). Las numéricas siguen a la derecha con `.__num`. */
    & th,
    & td {
        padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
        text-align: center;
        vertical-align: middle;
        border-bottom: 1px solid var(--upaep-trinity-view-divider);
        white-space: nowrap;
    }

    /*? Alineación GENERAL de la tabla */
    &.__align-start :is(th, td) { text-align: left; }
    &.__align-end :is(th, td) { text-align: right; }

    /*? …y por columna, que gana sobre la general */
    & :is(th, td).__start { text-align: left; }
    & :is(th, td).__center { text-align: center; }
    & :is(th, td).__end { text-align: right; }

    & thead th {
        position: sticky;
        top: 0;
        z-index: 1;
        font-size: var(--upaep-trinity-fz-label, .75rem);
        font-weight: var(--upaep-fw-semibold, 600);
        letter-spacing: .06em;
        text-transform: uppercase;
        color: var(--upaep-trinity-view-text-muted);
        /*? franja de cabecera: un escalón sobre la superficie de la tabla, para
           que se separe del cuerpo aunque no haya líneas verticales */
        background: var(--upaep-trinity-view-surface-2);
    }

    & tbody tr:last-child :is(th, td) { border-bottom: 0; }

    & tbody tr:hover { background: var(--upaep-trinity-view-hover-bg); }

    /*? Columnas numéricas alineadas a la derecha y con cifras tabulares */
    & .__num {
        text-align: right;
        font-variant-numeric: tabular-nums;
    }

    /*? Celda que sí puede envolver (descripciones) */
    & .__wrap { white-space: normal; min-width: 16rem; }
}

/*? ── Panel de gráfica ─────────────────────────────────────────────────────
   Marco y estados; el dibujo lo pone quien lo use (SVG propio, Chart.js, lo que
   sea). `.__expanded` lo saca a pantalla casi completa sin mover el DOM: útil
   para mirar una gráfica de cerca y volver. Lo alterna trinity-views.js. */
.upaep__trinity-chart {
    --upaep-trinity-chart-height: 200px;

    position: relative;
    width: 100%;
    height: var(--upaep-trinity-chart-height);
    transition: height var(--upaep-trinity-view-transition);

    & > svg {
        display: block;
        width: 100%;
        height: 100%;
        overflow: visible;
    }

    /*? EL MARCO CEDE LA ALTURA a lo que aloja cuando lo que aloja NO es un SVG
       suelto. Un componente de visualización del inventario trae su propia caja
       y su propia razón de aspecto; con la altura fija del marco quedaba
       recortado —o estirado— y el consumidor acababa corrigiéndolo con CSS
       propio, que es justo lo que el sistema prohíbe. La altura declarada pasa a
       ser el SUELO del hueco, para que el marco siga reservando sitio mientras la
       gráfica carga y no haya salto de layout. Sigue siendo genérico: no nombra
       ninguna pieza, sólo distingue «dibujo mío» de «componente alojado». */
    &:not(:has(> svg)) {
        height: auto;
        min-height: var(--upaep-trinity-chart-height);
    }

    /*? Piezas del dibujo, para que un SVG a mano herede el lenguaje visual */
    & .upaep__trinity-chart-grid line {
        stroke: var(--upaep-trinity-view-divider);
        stroke-width: 1;
    }

    & .upaep__trinity-chart-bar {
        fill: var(--upaep-trinity-view-accent);
        opacity: .85;
    }

    & .upaep__trinity-chart-line {
        fill: none;
        stroke: var(--upaep-trinity-view-accent);
        stroke-width: 2;
        stroke-linejoin: round;
        stroke-linecap: round;
    }

    & .upaep__trinity-chart-area {
        fill: var(--upaep-trinity-view-accent);
        opacity: .12;
    }

    & .upaep__trinity-chart-label {
        fill: var(--upaep-trinity-view-text-muted);
        font-size: 10px;
        font-family: inherit;
    }
}

/*? Panel expandido: ocupa la pantalla, con scrim aparte pintado por el runtime.
   La ANIMACIÓN se resuelve de dos maneras según el navegador:

   1. Con View Transitions (`document.startViewTransition`, Chrome/Edge 111+): el
      runtime le pone un `view-transition-name` y el navegador MORFA el panel de su
      sitio a pantalla completa. Es la única forma de animar de verdad un cambio
      de `position: static` a `fixed`, porque interpola los dos estados reales.
   2. Sin soporte: un zoom de entrada, que al menos hace legible el cambio. */
.upaep__trinity-panel.__expanded {
    position: fixed;
    inset: 1.5rem;
    z-index: var(--upaep-zindex-modal, 1060);
    overflow: auto;
    box-shadow: 0 18px 48px rgb(0 0 0 / .28);
    animation: a_trinityExpand .22s cubic-bezier(.2, .8, .3, 1);

    & .upaep__trinity-chart {
        --upaep-trinity-chart-height: min(62dvh, 520px);
    }
}

@keyframes a_trinityExpand {
    from {
        opacity: .4;
        transform: scale(.96);
    }
}

/*? Con View Transitions el navegador ya morfa: la animación propia estorbaría.
   El runtime pone el nombre en el atributo `style`, así que se detecta por ahí. */
.upaep__trinity-panel.__expanded[style*="view-transition-name"] {
    animation: none;
}

/*? ── Afinado de la transición: que CREZCA desde su sitio ──────────────────
   Por defecto View Transitions hace dos cosas a la vez: interpola la caja (eso es
   lo que queremos) y ADEMÁS funde en cruz la foto vieja con la nueva. El fundido
   es lo que hacía que se viera «mushy» en vez de un objeto que se agranda.

   Aquí se apaga el fundido (`animation: none` en old/new, y que ambas fotos
   llenen la caja con `object-fit: fill`) y se deja solo la interpolación del
   grupo. Resultado: la caja sale de su posición real y crece hasta el centro, y
   al cerrar vuelve a su hueco — el efecto de «expandir tarjeta» de iOS.

   La curva es la clave del carácter: `cubic-bezier(.32,.72,0,1)` arranca rápido y
   frena largo, que es lo que se siente «físico». */
::view-transition-group(trinity-chart) {
    z-index: 20;
    animation-duration: .46s;
    animation-timing-function: cubic-bezier(.32, .72, 0, 1);
}

/*? sin aislamiento no hay mezcla, y sin mezcla no hay fundido cruzado */
::view-transition-image-pair(trinity-chart) {
    isolation: auto;
}

::view-transition-old(trinity-chart),
::view-transition-new(trinity-chart) {
    animation: none;
    mix-blend-mode: normal;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: fill;
}

/*? El scrim sí se funde, para que el fondo no aparezca de golpe */
::view-transition-group(root) {
    animation-duration: .46s;
    animation-timing-function: cubic-bezier(.32, .72, 0, 1);
}

@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(trinity-chart) {
        animation-duration: .01ms;
    }
}

@media (width <=640px) {
    .upaep__trinity-panel.__expanded {
        inset: .5rem;
    }
}


/*? ── Rejilla de filtros ───────────────────────────────────────────────────
   Los filtros son el caso donde el número de campos cambia por pantalla (y a
   veces en caliente, según el rol). Por eso es rejilla AUTOMÁTICA por ancho
   mínimo y no columnas fijas: se agregan o quitan campos sin tocar el CSS.
   Dentro van los campos del core tal cual. */
.upaep__trinity-filters {
    --upaep-trinity-filters-min: 200px;

    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(var(--upaep-trinity-filters-min), 100%), 1fr));
    gap: var(--upaep-trinity-view-gap);
    align-items: end;

    &.__sm { --upaep-trinity-filters-min: 150px; }
    &.__lg { --upaep-trinity-filters-min: 260px; }

    /*? ── Aclaración sobre a qué se adapta ─────────────────────────────────
       La rejilla se adapta al ancho del CONTENEDOR, no al del control de dentro:
       decide cuántas columnas caben con `--upaep-trinity-filters-min` y cada
       campo llena su celda. Es lo que se quiere casi siempre (columnas alineadas
       aunque los controles midan distinto).

       Cuando NO se quiere —un select de «Todos/Sí/No» estirado a 260px se ve
       raro— está `.__fit-content`: cada campo ocupa lo que necesita y la fila se
       llena de izquierda a derecha. `.__wide` sigue funcionando en ambos modos. */
    &.__fit-content {
        grid-template-columns: none;
        display: flex;
        flex-wrap: wrap;

        & .upaep__trinity-filter {
            flex: 0 1 auto;
            min-width: 9rem;

            &.__wide { flex: 1 1 18rem; }
            &.__grow { flex: 1 1 12rem; }
        }

        & .upaep__trinity-filters-actions { flex: 0 0 auto; }
    }

    /*? ── Alineación vertical de las celdas ────────────────────────────────
       El defecto es `end`: los controles quedan alineados entre sí aunque unos
       campos lleven etiqueta y otros no, que es lo que se quiere en una fila de
       filtros. Los otros dos existen para los casos en que no:
         · `__align-start`  todo arriba — cuando cada campo lleva su propia
                            ayuda debajo y el bloque se lee de arriba a abajo;
         · `__align-center` centrado — filas de una sola línea sin etiquetas
                            (chips, switches, un buscador y dos botones);
         · `__align-stretch` las celdas miden lo mismo — para envolver cajas,
                            no controles. */
    &.__align-start { align-items: start; }
    &.__align-center { align-items: center; }
    &.__align-stretch { align-items: stretch; }

    /*? Un campo: etiqueta + control, ocupando su celda */
    & .upaep__trinity-filter {
        display: flex;
        flex-direction: column;
        gap: var(--upaep-trinity-sp-1, .25rem);
        min-width: 0;

        /*? El `.upaep__label` del core trae `margin: 0 0 .25rem`, que aquí se
           SUMA al `gap` de la columna: la etiqueta quedaba al doble de
           distancia de su control que en el resto del kit. El hueco lo pone el
           gap, así que el margen se retira. Igual con el `.upaep__feedback`,
           que cuelga del control y no necesita margen propio. */
        & > .upaep__label,
        & .upaep__input-container > .upaep__label {
            --upaep-label-margin: 0;
        }

        & .upaep__feedback { --upaep-feedback-margin: 0; }

        /*? que los controles del core llenen la celda */
        & .upaep__input,
        & .upaep__select,
        & .upaep__selectpicker {
            width: 100%;
        }

        /*? un filtro puede ocupar dos celdas (rango de fechas, búsqueda ancha) */
        &.__wide { grid-column: span 2; }
    }

    /*? ── Filtros que NO son un campo de texto ──────────────────────────────
       Un grupo de radios o una casilla suelta también son filtros, y hasta aquí
       la rejilla solo sabía de campos: los grupos caían al fondo de su celda (la
       rejilla alinea a `end`) y quedaban desalineados respecto a los controles
       de al lado, que son mucho más altos.

       La banda contra la que se alinean es `--upaep-trinity-control-h`, la misma
       altura de fila que ya llevan los campos y los botones. */

    /*? Un `<fieldset>` es lo correcto para atar varias opciones a un rótulo
       común, pero llega con borde, relleno y un `min-width: min-content` del
       navegador que le impide encogerse. Se neutraliza. */
    & fieldset.upaep__trinity-filter {
        min-width: 0;
        margin: 0;
        padding: 0;
        border: 0;
    }

    /*? El `<legend>` se ve como un `.upaep__label` cualquiera: si no, el rótulo
       de un grupo de radios canta al lado del de un campo.

       Se copia su CAJA, no solo su tipografía: `.upaep__label` es un flex con
       `align-items: center`, y eso le da una altura de línea distinta a la de un
       bloque normal (21px frente a 17px con la misma fuente). Como la fila alinea
       por el suelo, esos 4px de diferencia bajaban la leyenda respecto al resto
       de rótulos — se veía torcida sin que nada estuviera «mal».

       Y el hueco va como MARGEN, no por el `gap` del campo: aunque el fieldset
       sea flex, el navegador saca al `<legend>` de su flujo (es la «leyenda
       renderizada», que se dibuja sobre el borde) y por eso no cuenta como item
       flex ni recibe el `gap`. */
    & .upaep__trinity-filter-legend {
        display: flex;
        align-items: center;
        padding: 0;
        margin: 0 0 var(--upaep-trinity-sp-1, .25rem);
        float: none;
        font-size: var(--upaep-label-fz, var(--upaep-trinity-fz-body, .875rem));
        font-weight: var(--upaep-label-fw, var(--upaep-fw-medium, 500));
        color: var(--upaep-label-color, var(--upaep-trinity-view-text));
    }

    /*? La fila de opciones ocupa la misma banda que un control, y las opciones
       van CENTRADAS en ella: así los círculos de los radios quedan a la altura
       del texto de los campos vecinos, no pegados a su borde inferior. */
    & .upaep__trinity-filter-options {
        display: flex;
        align-items: center;
        gap: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-4, 1rem);
        flex-wrap: wrap;
        min-height: var(--upaep-trinity-control-h);
    }

    /*? Una casilla o un switch SUELTOS no llevan rótulo encima —su etiqueta ya
       dice qué hacen—, así que el bloque mide lo que la casilla y se iba al
       fondo de la celda. `__inline` le da la banda del control y lo centra en
       ella, que es lo que lo pone en línea con los campos de al lado. */
    & .upaep__trinity-filter.__inline {
        justify-content: center;
        min-height: var(--upaep-trinity-control-h);

        /*? con rótulo (el grupo de radios) la banda la pone `-filter-options`,
           que ya está arriba: aquí solo se centra lo que no lo tiene */
        &:has(> .upaep__trinity-filter-legend) {
            justify-content: flex-end;
            min-height: 0;
        }
    }

    /*? Los grupos del core traen su propio margen, pensado para un formulario
       apilado. En una fila de filtros ese margen descuadra la alineación. */
    & :is(.upaep__radio-group, .upaep__checkbox-group) {
        margin: 0;
        align-items: center;
    }

    /*? Los botones se alinean con los controles, no con las etiquetas */
    & .upaep__trinity-filters-actions {
        display: flex;
        align-items: center;
        gap: var(--upaep-trinity-sp-2, .5rem);
        flex-wrap: wrap;
        min-height: var(--upaep-trinity-control-h);

        /*? empujado al final de la fila cuando cabe.
           `justify-content` NO basta: en `__fit-content` el grupo es un item
           flex de ancho automático, o sea exactamente el de sus botones, y
           repartir dentro de esa caja no lo mueve de sitio. Lo que lo empuja al
           final de la fila es el margen automático. En modo rejilla el grupo SÍ
           llena su celda, así que ahí hacen falta los dos. */
        &.__end {
            justify-content: flex-end;
            margin-inline-start: auto;
        }
    }

    @media (width <=640px) {
        & .upaep__trinity-filter.__wide { grid-column: span 1; }
    }
}

/*? ── Modificador de buscador ──────────────────────────────────────────────
   `.upaep__input.__search` pinta la lupa dentro del campo SIN markup extra: es
   una máscara CSS sobre el fondo, así hereda el color del texto y el tema.
   (El core tiene `upaep__input-wrapper` + `upaep__input-icon` para iconos en
   general, pero exige envolver el campo; para el caso «esto es un buscador»,
   que es constante, un modificador sale más limpio.)
   Con `type="search"` además se oculta la X nativa de WebKit, que rompe el
   alineado y duplica el «limpiar» cuando ya hay chips de filtros. */
:root {
    --upaep-trinity-search-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-4.6-4.6"/></svg>');
}

/*? El campo solo cede el hueco y limpia la X nativa de WebKit (que rompe el
   alineado y duplica el «limpiar» cuando ya hay chips de filtros). */
.upaep__input.__search {
    --upaep-trinity-search-size: 1.05rem;
    --upaep-trinity-search-gap: var(--upaep-trinity-sp-3, .75rem);

    padding-left: calc(var(--upaep-trinity-search-size) + var(--upaep-trinity-search-gap) * 2);

    &::-webkit-search-cancel-button {
        -webkit-appearance: none;
        appearance: none;
    }
}

/*? La lupa la pinta el CONTENEDOR: un input no puede tener pseudoelementos, así
   que el icono va en el `::after` del envoltorio. Funciona con el contenedor de
   filtros, con el del uikit (`upaep__input-container`) o con `-search-field`.

   Centrado: el `::after` NO se centra respecto al bloque (que puede llevar
   etiqueta encima y feedback debajo), sino respecto al CAMPO. Se consigue
   estirándolo a la altura del campo desde su borde inferior y centrando la
   máscara dentro; así queda alineado aunque el input cambie de alto. */
/*? `.upaep__trinity-search-field` NO pinta lupa, a propósito.
   La pintaba con `:has(.upaep__input.__search)` —descendiente, sin el `>` que sí
   llevan los otros dos—, así que envolviendo con él un `.upaep__input-container`
   coincidían LOS DOS y salían dos lupas. El icono ahí era decorativo, así que se
   retira en vez de afinar el selector: quien quiera lupa usa uno de los dos
   envoltorios de abajo, que la resuelven sin ambigüedad. */
.upaep__trinity-search-field {
    display: block;
    min-width: 0;
}

.upaep__trinity-filter:has(> .upaep__input.__search),
.upaep__input-container:has(> .upaep__input.__search) {
    --upaep-trinity-search-size: 1.05rem;
    --upaep-trinity-search-gap: var(--upaep-trinity-sp-3, .75rem);
    /*? EL ALTO DEL CAMPO, no un literal. Era `2.6rem` (41.6 px) escrito a mano, y
       el campo mide 39: la lupa quedaba 1.3 px por encima del centro del campo,
       que es justo el defecto que se ve como «la lupa está descentrada». Un
       número copiado de otro sitio no puede seguir a lo que copia; el token de
       altura de fila sí, y es el mismo que usan el campo y el botón de al lado. */
    --upaep-trinity-search-field-h: var(--upaep-trinity-control-h, 2.4375rem);

    position: relative;

    &::after {
        content: "";
        position: absolute;
        left: var(--upaep-trinity-search-gap);
        bottom: 0;
        width: var(--upaep-trinity-search-size);
        height: var(--upaep-trinity-search-field-h);
        background: var(--upaep-trinity-view-text-muted);
        mask: var(--upaep-trinity-search-icon) center / var(--upaep-trinity-search-size) no-repeat;
        -webkit-mask: var(--upaep-trinity-search-icon) center / var(--upaep-trinity-search-size) no-repeat;
        pointer-events: none;
        opacity: .9;
        transition: background .15s ease, opacity .15s ease;
    }

    /*? Al enfocar el campo la lupa se aviva: refuerza que ahí se escribe */
    &:focus-within::after {
        background: var(--upaep-trinity-view-accent);
        opacity: 1;
    }
}

/*? El contenedor del kit NO lleva etiqueta dentro —la etiqueta va fuera, es su
   contrato— así que su alto ES el del campo y el ancla puede ser EXACTA, sin
   depender de ningún token ni de cuánto mida el campo. Donde no se puede es en
   `.upaep__trinity-filter`, que sí envuelve la etiqueta: ahí manda el token. */
.upaep__input-container:not(.is-feedback):has(> .upaep__input.__search)::after {
    inset-block: 0;
    height: auto;
}

/*? Si hay `.upaep__feedback` debajo, el campo ya no toca el borde inferior del
   bloque: el ancla se sube lo que mide el mensaje. */
.upaep__input-container.is-feedback:has(> .upaep__input.__search)::after {
    bottom: 1.15rem;
}

/*? Filtros aplicados, como chips retirables */
/*? EL SEPARADOR ES DE LA PAREJA, no de los chips. Los chips son el ESTADO de los
   filtros —qué está aplicado ahora y cómo quitarlo— y ese estado tiene que verse
   aunque el panel de filtros esté plegado, que es el caso normal en un listado:
   lo raro es tener el panel abierto. Cuando la fila de chips vive fuera del panel
   no hay nada de lo que separarla, y el `border-top` incondicional dejaba una
   línea suelta flotando entre dos paneles. Pegada a la rejilla —dentro del
   panel— el separador sigue exactamente igual que antes. */
.upaep__trinity-filters + .upaep__trinity-filters-chips {
    margin-top: var(--upaep-trinity-sp-3, .75rem);
    padding-top: var(--upaep-trinity-sp-3, .75rem);
    border-top: 1px solid var(--upaep-trinity-view-divider);
}

.upaep__trinity-filters-chips {
    display: flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    flex-wrap: wrap;

    &:empty { display: none; }

    & .upaep__trinity-chip {
        display: inline-flex;
        align-items: center;
        gap: var(--upaep-trinity-sp-1, .25rem);
        padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-2, .5rem);
        font-size: var(--upaep-fz12, .75rem);
        color: var(--upaep-trinity-view-text);
        background: var(--upaep-trinity-view-surface-2);
        border: 1px solid var(--upaep-trinity-view-divider);
        border-radius: var(--upaep-trinity-radius-pill, 999px);

        & b {
            font-weight: var(--upaep-fw-semibold, 600);
            color: var(--upaep-trinity-view-text-strong);
        }

        & button {
            display: grid;
            place-items: center;
            width: 1.1rem;
            height: 1.1rem;
            padding: 0;
            border: 0;
            border-radius: 50%;
            background: transparent;
            color: var(--upaep-trinity-view-text-muted);
            cursor: pointer;

            & svg {
                width: .7rem;
                height: .7rem;
                fill: none;
                stroke: currentColor;
                stroke-width: 2.5;
                stroke-linecap: round;
            }

            &:hover {
                background: var(--upaep-trinity-view-hover-page);
                color: var(--upaep-trinity-view-text-strong);
            }
        }
    }
}


/*? ── Tarjeta de acceso (icono + título + botón) ────────────────────────────
   La tarjeta de "atajo" de siempre: un icono, un título con descripción y una
   acción. Se combina con `.upaep__trinity-cards` para la rejilla. */
.upaep__trinity-tile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--upaep-trinity-sp-2, .5rem);
    height: 100%;
    padding: var(--upaep-trinity-panel-padding);
    background: var(--upaep-trinity-panel-bg);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-panel-radius);
    transition: border-color .15s ease, transform .15s ease;

    & .upaep__trinity-tile-icon {
        display: grid;
        place-items: center;
        width: 2.5rem;
        height: 2.5rem;
        color: var(--upaep-trinity-view-accent);
        background: color-mix(in srgb, var(--upaep-trinity-view-accent) 12%, transparent);
        border-radius: var(--upaep-trinity-radius-md, 8px);

        & svg {
            width: 1.25rem;
            height: 1.25rem;
            fill: none;
            stroke: currentColor;
            stroke-width: 1.8;
            stroke-linecap: round;
            stroke-linejoin: round;
        }
    }

    & .upaep__trinity-tile-title {
        margin: 0;
        font-size: var(--upaep-fz15, .9375rem);
        font-weight: var(--upaep-fw-semibold, 600);
        line-height: 1.3;
        color: var(--upaep-trinity-view-text-strong);
    }

    & .upaep__trinity-tile-text {
        margin: 0;
        font-size: var(--upaep-trinity-fz-meta, .75rem);
        line-height: 1.45;
        color: var(--upaep-trinity-view-text-muted);
        text-wrap: pretty;
    }

    /*? La acción se pega abajo, así las tarjetas de una fila alinean sus botones */
    & .upaep__trinity-tile-action {
        margin-top: auto;
        padding-top: var(--upaep-trinity-sp-1, .25rem);
    }

    /*? Toda la tarjeta clicable (envuélvela en <a>): se realza al pasar.
       Solo con puntero — en táctil el `:hover` se queda pegado tras el toque. */
    @media (hover: hover) {
        &:is(.__linked, .__interactive):hover {
            border-color: var(--upaep-trinity-view-accent);
            transform: translateY(-1px);
        }
    }

    /*? Si la tarjeta se puede pulsar, que se note y se pueda enfocar.
       Antes `__interactive` solo aparecía en reglas de `:hover`: daba la elevación
       y nada más. Sobre algo que no fuera un <button> el cursor no cambiaba y no
       había indicador de foco, así que la promesa de interactividad se rompía para
       quien usa teclado. El rol y el `tabindex` no salen del CSS: los pone el
       runtime al hidratar (ver trinity-views.js), igual que ya hace con la cabecera
       del panel colapsable. */
    &:is(.__linked, .__interactive) {
        cursor: pointer;

        &:focus-visible {
            outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
            outline-offset: var(--ui-focus-ring-offset, 2px);
        }
    }

    /*? Tarjeta-enlace: el tile entero es un <a>. Hay que neutralizar el estilo de
       enlace del core (style.css pinta `a` en azul y subrayado), o la tarjeta sale
       azul y con el título subrayado. Se hereda el color del contexto, que es lo
       que ya usan el resto de piezas de trinity. */
    &.__linked {
        color: inherit;
        text-decoration: none;

        &:hover,
        &:focus-visible {
            text-decoration: none;
        }
    }

    /*? Icono en colores de estado */
    &.__success .upaep__trinity-tile-icon {
        color: var(--ui-success, #2e9e4b);
        background: color-mix(in srgb, var(--ui-success, #2e9e4b) 12%, transparent);
    }

    &.__info .upaep__trinity-tile-icon {
        color: var(--ui-info, #2f6fed);
        background: color-mix(in srgb, var(--ui-info, #2f6fed) 12%, transparent);
    }

    &.__neutral .upaep__trinity-tile-icon {
        color: var(--upaep-trinity-view-text-muted);
        background: var(--upaep-trinity-view-surface-2);
    }

    /*? Las tres que faltaban para igualar la paleta de `.upaep__trinity-stat`.
       Que dos piezas hermanas admitieran juegos de estados distintos obligaba a
       recordar cuál acepta qué, y se notaba en cada consumidor. Mismos tokens y
       misma mezcla al 12 % que las de arriba. */
    &.__accent .upaep__trinity-tile-icon {
        color: var(--upaep-trinity-view-accent);
        background: color-mix(in srgb, var(--upaep-trinity-view-accent) 12%, transparent);
    }

    &.__warning .upaep__trinity-tile-icon {
        color: var(--ui-warning, #e0a800);
        background: color-mix(in srgb, var(--ui-warning, #e0a800) 12%, transparent);
    }

    &.__danger .upaep__trinity-tile-icon {
        color: var(--upaep-trinity-view-danger);
        background: color-mix(in srgb, var(--upaep-trinity-view-danger) 12%, transparent);
    }
}


/*? ── Aviso (alert) ─────────────────────────────────────────────────────────
   `.upaep__trinity-alert` NO es un alert nuevo: es una CAPA sobre
   `.upaep__alert` del core, igual que `.upaep__trinity-btn` sobre `.upaep__btn`.
   Se usa junto a él —

       <div class="upaep__alert upaep__trinity-alert __warning">…</div>

   — pero declara su propio relleno, filo y radio, así que TAMBIÉN funciona sola
   sobre otra pieza (un toast de Herald, por ejemplo, vía `customClass`). Ver
   «Reutilizar la piel del aviso» al final de esta sección.

   ── LAS DOS VERSIONES (relleno) ──────────────────────────────────────────────
   Son las del core y se conservan tal cual:
     · BASE    relleno SÓLIDO del color   (`--ui-warning` + `--ui-on-warning`)
     · LIGHT   relleno TENUE del color    (`--ui-warning-subtle` + `--ui-on-warning-subtle`)
   Las dos parejas están remapeadas para `data-theme="dark"`, así que ninguna
   versión necesita reglas de modo oscuro.

   ── LAS DOS FORMAS (reparto del color) ───────────────────────────────────────
   Encima de la versión hay dos formas, que es lo que de verdad distingue un
   aviso de dashboard de un cartel. Las dos llevan la BARRA lateral del color
   —identifica el aviso de un vistazo, sin leer el icono— y se diferencian en
   cuánto más se tiñe:
     · `.__tinted`  relleno tenue + chip teñido: el color inunda (fondo, barra,
                    texto e icono). Para el aviso que pertenece a la vista.
     · `.__clean`   relleno de SUPERFICIE (blanco/panel) + chip SÓLIDO. Fuera de
                    la barra, el color vive **solo en el icono** —el mismo
                    criterio que `.upaep__trinity-tile`— y el texto es el de la
                    capa. Es la forma que se lee como notificación: la misma piel
                    que un toast.
   Cada forma calcula su relleno del tono, así que no dependen de la pareja
   `-subtle` del core y valen para los cinco tipos, incluido el que el core no
   tiene (`__question`).

   ── LOS CINCO TIPOS ──────────────────────────────────────────────────────────
   `__success` `__error` `__warning` `__info` `__question` — los mismos cinco
   iconos que Herald, para que un aviso y un diálogo hablen igual. Cada tipo
   declara DOS tokens y nada más:
     --upaep-trinity-alert-hue      el color puro (barra, chip sólido)
     --upaep-trinity-alert-on-hue   el glifo sobre el chip sólido
   La TINTA del texto sobre relleno tenue (`--upaep-trinity-alert-ink`) no es un
   token por tipo: se DERIVA del tono mezclándolo con el texto fuerte de la capa,
   así que sale legible en los cinco tipos y en los dos temas con una sola línea.
   Por eso un tipo nuevo es una regla de dos líneas, no un bloque por forma.

   Ojo con la pareja `on-*-subtle` del core: está calculada para sus rellenos
   `-subtle`, y en `warning` da 2.9:1 — por debajo de AA. La versión `__light` a
   secas es la del core y lo hereda; `__tinted` calcula su tinta y no.

   Es aditiva: si el core adopta esta piel, esta sección se borra y nada cambia. */
.upaep__trinity-alert {
    /*? ── Remapeo de los tokens del core ── */
    --upaep-alert-margin: 0;
    --upaep-alert-padding: var(--upaep-trinity-sp-3, .75rem) var(--upaep-trinity-sp-4, 1rem);
    --upaep-alert-border-radius: var(--upaep-trinity-radius-md, 8px);
    --upaep-alert-fz: var(--upaep-trinity-fz-meta, .75rem);
    --upaep-alert-fw: var(--upaep-fw-regular, 400);
    /*? Sin tipo el aviso es neutro: la superficie de la capa. Con `__warning`,
       `__error`… gana el core (2 clases) y el relleno sólido de la versión base
       se queda intacto, que es lo que se quiere. */
    --upaep-alert-bg: var(--upaep-trinity-view-surface-2);
    --upaep-alert-border-color: var(--upaep-trinity-view-divider);
    --upaep-alert-color: var(--upaep-trinity-view-text);

    /*? ── Lo propio de esta capa ── */
    --upaep-trinity-alert-hue: var(--upaep-trinity-view-text-muted);
    --upaep-trinity-alert-on-hue: var(--upaep-trinity-view-surface);
    /*? la tinta se deriva en `__tinted`; aquí queda el valor neutro, y se puede
       fijar a mano para un caso concreto */
    --upaep-trinity-alert-ink: var(--upaep-trinity-view-text);
    --upaep-trinity-alert-icon-size: 1.75rem;
    /*? grosor de la barra lateral; la llevan las dos formas, y 0 la quita */
    --upaep-trinity-alert-rail: 4px;

    /*? Se declara la caja, no solo los tokens: así la clase vale también suelta
       sobre otra pieza (un toast), donde `.upaep__alert` no está para leerlos. */
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: start;
    gap: var(--upaep-trinity-sp-3, .75rem);
    padding: var(--upaep-alert-padding);
    margin: var(--upaep-alert-margin);
    border: 1px solid var(--upaep-alert-border-color);
    border-radius: var(--upaep-alert-border-radius);
    background: var(--upaep-alert-bg);
    color: var(--upaep-alert-color);
    font-size: var(--upaep-alert-fz);
    font-weight: var(--upaep-alert-fw);
    line-height: 1.45;

    /*? ── Con y sin cada parte ──────────────────────────────────────────────
       Las tres columnas se reparten según lo que HAYA dentro, no según una clase
       que haya que recordar: un aviso sin icono no queda sangrado a media caja y
       uno sin botón de cerrar no reserva su hueco. */
    &:not(:has(> .upaep__trinity-alert-icon)) {
        grid-template-columns: 1fr auto;
    }

    &:not(:has(> .upaep__trinity-alert-close, > .__close)) {
        grid-template-columns: auto 1fr;
    }

    &:not(:has(> .upaep__trinity-alert-icon)):not(:has(> .upaep__trinity-alert-close, > .__close)) {
        grid-template-columns: 1fr;
    }

    /*? El core reserva 3rem a la derecha para su botón absoluto. Aquí el botón
       es una CELDA de la rejilla, así que ese hueco sobra: `__dismissible` deja
       de hacer falta (se respeta si viene, para no romper markup existente). */
    &.__dismissible {
        --upaep-alert-padding: var(--upaep-trinity-sp-3, .75rem) var(--upaep-trinity-sp-4, 1rem);
    }
}

/*? Icono: chip circular. En `__tinted` el chip es el tono al 30 % y el glifo la
   tinta legible; en `__clean` el chip es el tono SÓLIDO y el glifo su color de
   contraste. Sin forma —versiones base y light a secas— el chip sale de
   `currentColor`, así que la misma regla sirve para las dos. */
.upaep__trinity-alert-icon {
    display: grid;
    place-items: center;
    box-sizing: border-box;
    width: var(--upaep-trinity-alert-icon-size);
    height: var(--upaep-trinity-alert-icon-size);
    /*? un icono NUNCA se encoge: con texto largo al lado, el `auto` de la
       columna cedería */
    flex: 0 0 auto;
    border-radius: 50%;
    /*? ANILLO de 2px al 45 % (glifo dentro de un círculo): el MISMO icono que el toast y Herald. Las formas
       `__tinted` y `__clean` rellenan el chip y apagan el anillo (abajo). */
    border: 2px solid color-mix(in srgb, currentColor 45%, transparent);
    background: transparent;
}

.upaep__trinity-alert-icon > svg {
    width: 1rem;
    height: 1rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/*? ── El icono base del tipo ─────────────────────────────────────────────────
   Un chip VACÍO (`<span class="upaep__trinity-alert-icon"></span>`) pinta el glifo del tipo, el MISMO que
   el toast, el aviso del core y Herald (`--upaep-alert-icon-content`, que fija `.upaep__alert.__tipo`):
   un aviso, un toast y un diálogo del mismo tipo se ven iguales y no hay que dibujar un SVG por uso. Un
   SVG propio dentro del chip sigue mandando (excepción, no la regla). `:has()` y no `:empty`: un salto de
   línea dentro del span no debe apagar el glifo. */
.upaep__trinity-alert-icon:not(:has(> *))::before {
    content: var(--upaep-alert-icon-content, none);
    font-family: var(--upaep-ff-fontawesome);
    font-size: var(--upaep-alert-icon-fz, var(--upaep-fz14));
    font-weight: var(--upaep-fw-regular);
    line-height: 1;
}

/*? `min-width: 0`: sin él una palabra larga (un folio, una URL) ensancha la
   columna y desborda el panel. */
.upaep__trinity-alert-body {
    min-width: 0;
}

.upaep__trinity-alert-title {
    margin: 0;
    font-size: var(--upaep-fz14, .875rem);
    font-weight: var(--upaep-fw-semibold, 600);
    line-height: 1.35;
}

/*? La línea de apoyo va en el MISMO tono, un paso atrás. Con opacidad y no con
   un color propio: así funciona igual en las dos versiones y en los dos temas,
   sin un token por tipo. */
.upaep__trinity-alert-text {
    margin: var(--upaep-trinity-sp-1, .25rem) 0 0;
    font-size: var(--upaep-trinity-fz-meta, .75rem);
    opacity: .85;
}

.upaep__trinity-alert-title + .upaep__trinity-alert-text {
    margin-top: var(--upaep-trinity-sp-1, .25rem);
}

/*? ── Botón de cerrar, estandarizado ────────────────────────────────────────
   UNA clase, no cuatro. Antes había que escribir
   `class="__close upaep__btn upaep__trinity-btn __icon __sm"` y encima el
   resultado quedaba descentrado (el `.__close` del core es absoluto y su padding
   está calculado para el relleno del core) y con el color del texto de la vista,
   casi invisible sobre un relleno sólido.

   Ahora es una CELDA de la rejilla, centrada en vertical, y conserva el efecto
   de los botones de Trinity: fantasma en reposo, círculo relleno al pasar. El
   color sale de `currentColor`, así que acierta en las dos versiones y en las
   dos formas sin una regla por caso.

   El glifo estándar es la ✕ EN CÍRCULO (`<circle>` + dos trazos): el aspa suelta
   se confunde con el icono de error del propio aviso.

   SIRVE PARA LOS DOS AVISOS, que es el punto de estandarizarlo:
     · dentro de `.upaep__trinity-alert` es una CELDA de la rejilla, centrada;
     · en un `.upaep__alert` del core se le añade su `.__close`, que lo coloca
       absoluto en la esquina — dos clases en vez de las cinco de antes:
       `class="__close upaep__trinity-alert-close"`.
   El selector incluye `.upaep__alert .upaep__trinity-alert-close` a propósito:
   el core pone `padding: 1.25rem 1rem` a su `.__close` con dos clases, y sin
   empatarle la especificidad ese padding deforma el botón.

   Se acepta también el `.__close` a secas dentro del aviso de Trinity, por el
   markup que ya existe. */
.upaep__trinity-alert-close,
.upaep__alert .upaep__trinity-alert-close,
.upaep__trinity-alert > .__close,
.upaep__trinity-tag-close {
    /*? inerte cuando es celda (un elemento `static` ignora `inset`); solo actúa
       cuando lo coloca el `.__close` del core */
    inset: .5rem .5rem auto auto;
    display: grid;
    place-items: center;
    width: 1.65rem;
    height: 1.65rem;
    padding: 0;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    color: inherit;
    background: transparent;
    opacity: .75;
    transition: background .15s ease, opacity .15s ease;

    /*? por si llega con las clases de botón del kit */
    --upaep-btn-bg: transparent;
    --upaep-btn-color: currentColor;
    --upaep-btn-hover-color: currentColor;
    --upaep-btn-hover-bg: transparent;
    --upaep-btn-padding: 0;
    --upaep-btn-height: 1.65rem;
    --upaep-btn-width: 1.65rem;
    --upaep-btn-margin: 0;

    &:hover {
        opacity: 1;
        background: color-mix(in srgb, currentColor 16%, transparent);
    }

    &:focus-visible {
        outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
        outline-offset: var(--ui-focus-ring-offset, 2px);
    }

    & > svg {
        width: 1.05rem;
        height: 1.05rem;
        fill: none;
        stroke: currentColor;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
    }
}

/*? Dentro del aviso de Trinity el cierre es una CELDA, no un absoluto: así no
   hace falta reservarle padding (`__dismissible`) y queda centrado en vertical
   por sí solo, sea el aviso de una línea o de tres. */
.upaep__trinity-alert > :is(.upaep__trinity-alert-close, .__close) {
    position: static;
    align-self: center;
}

/*? ── Los cinco tipos ───────────────────────────────────────────────────────
   Tres tokens por tipo. `ink` sale de la pareja `on-*-subtle` del core, que ya
   está remapeada para oscuro; `on-hue`, de la pareja `on-*` (en `warning` es
   NEGRO: un glifo blanco sobre ámbar no se lee). */
.upaep__trinity-alert {
    &.__success {
        --upaep-trinity-alert-hue: var(--ui-success, #28a745);
        --upaep-trinity-alert-on-hue: var(--ui-on-success, #ffffff);
    }

    &.__error {
        --upaep-trinity-alert-hue: var(--ui-danger, #dc3545);
        --upaep-trinity-alert-on-hue: var(--ui-on-danger, #ffffff);
    }

    &.__warning {
        --upaep-trinity-alert-hue: var(--ui-warning, #ffc107);
        --upaep-trinity-alert-on-hue: var(--ui-on-warning, #000000);
    }

    &.__info {
        --upaep-trinity-alert-hue: var(--ui-info, #437eea);
        --upaep-trinity-alert-on-hue: var(--ui-on-info, #ffffff);
    }

    /*? El QUINTO tipo, que el core no tiene: pregunta. Se pinta en gris —igual
       que el glifo `?` de Herald— porque una pregunta no es un estado: no debe
       competir con los cuatro que sí informan. Al no existir en el core, la capa
       le da también los rellenos de las dos versiones, para que el tipo esté
       completo también sin forma. */
    &.__question {
        --upaep-trinity-alert-hue: var(--upaep-trinity-view-text-muted);
        --upaep-trinity-alert-on-hue: var(--upaep-trinity-view-surface);

        --upaep-alert-bg: var(--upaep-trinity-view-surface-2);
        --upaep-alert-color: var(--upaep-trinity-view-text);
        --upaep-alert-border-color: var(--upaep-trinity-view-divider);

        &.__light {
            --upaep-alert-bg: var(--upaep-trinity-view-surface);
        }
    }

    /*? Versión LIGHT sin forma: el relleno y el texto los pone el core; aquí
       solo el filo, que el core deja en el color fuerte y sobre un relleno tenue
       se ve como un recuadro de más. */
    &.__light {
        --upaep-alert-border-color: color-mix(in srgb, var(--upaep-trinity-alert-hue) 35%, transparent);
    }
}

/*? ── LA BARRA LATERAL, rasgo de las dos formas ─────────────────────────────
   Un filo de color a la izquierda: es lo que hace que el aviso se identifique
   de un vistazo sin tener que leer el icono, y funciona igual sobre relleno
   tenue que sobre superficie. Por eso NO es de una forma: la llevan las dos.

   Es un `border-left`, no un `::before`: así respeta el radio de la caja sin un
   segundo radio que mantener y no se cuela por encima del texto.

   Para quitarla en un caso concreto: `--upaep-trinity-alert-rail: 0`. */
.upaep__trinity-alert:is(.__tinted, .__clean) {
    border-left: var(--upaep-trinity-alert-rail) solid var(--upaep-trinity-alert-hue);
    /*? el borde izquierdo ya no es hairline: se compensa el relleno para que el
       texto no se desplace respecto a un aviso sin barra */
    padding-left: calc(1rem - var(--upaep-trinity-alert-rail) + 1px);
}

/*? ── FORMA 1: teñida (`.__tinted`) ─────────────────────────────────────────
   Relleno tenue calculado del tono (no de la pareja `-subtle`, así vale para los
   cinco tipos y mezcla contra la superficie, que es lo que la hace funcionar
   igual en claro y en oscuro) y chip teñido. El texto va en la tinta legible del
   tipo: el color INUNDA el aviso. Mismo nombre y misma idea que
   `.upaep__trinity-stat.__tinted`. */
.upaep__trinity-alert.__tinted {
    /*? La tinta, DERIVADA del tono: 40 % de color sobre el texto fuerte de la
       capa. Se ve teñida —es lo que hace que el aviso se lea «de un color»— y
       pasa AA sobre el relleno del 14 % en los dos temas: en claro el texto
       fuerte es casi negro y la mezcla oscurece; en oscuro es casi blanco y
       aclara. Medido en claro: warning 5.6:1 y success 7.5:1, cuando con la
       pareja `on-*-subtle` del core eran 2.9:1 y 4.4:1. */
    --upaep-trinity-alert-ink: color-mix(in srgb, var(--upaep-trinity-alert-hue) 40%, var(--upaep-trinity-view-text-strong));
    --upaep-alert-bg: color-mix(in srgb, var(--upaep-trinity-alert-hue) 14%, var(--upaep-trinity-view-surface));
    --upaep-alert-color: var(--upaep-trinity-alert-ink);
    --upaep-alert-border-color: color-mix(in srgb, var(--upaep-trinity-alert-hue) 26%, transparent);

    & .upaep__trinity-alert-icon {
        color: var(--upaep-trinity-alert-ink);
        background: color-mix(in srgb, var(--upaep-trinity-alert-hue) 30%, transparent);
        border-color: transparent;
    }
}

/*? ── FORMA 2: limpia (`.__clean`) ──────────────────────────────────────────
   Superficie de la capa, hairline, y —fuera de la barra— el color SOLO en el
   icono, que pasa a ser un chip sólido. El texto es el de la capa (título en
   fuerte, apoyo en tenue) porque aquí el color no informa desde el fondo, lo
   hace desde el icono. Es la forma que se lee como notificación y la que se
   reutiliza en un toast.

   Se llamó `__card` un rato y era mal nombre: en esta capa «card» ya significa
   otra cosa (`.upaep__trinity-cards` es una rejilla, `.upaep__card` del core es
   una tarjeta de contenido), y la forma no añade una tarjeta: le QUITA color al
   aviso. `__clean` dice eso. */
.upaep__trinity-alert.__clean {
    --upaep-alert-bg: var(--upaep-trinity-view-surface);
    --upaep-alert-color: var(--upaep-trinity-view-text);
    --upaep-alert-border-color: var(--upaep-trinity-view-divider);

    & .upaep__trinity-alert-icon {
        color: var(--upaep-trinity-alert-on-hue);
        background: var(--upaep-trinity-alert-hue);
        border-color: transparent;
    }

    & .upaep__trinity-alert-title {
        color: var(--upaep-trinity-view-text-strong);
    }

    & .upaep__trinity-alert-text {
        color: var(--upaep-trinity-view-text-muted);
        opacity: 1;
    }

    /*? El cierre no lleva el color del tipo: en esta forma el texto es neutro y
       un aspa de color sería el único elemento gritando. */
    & > :is(.upaep__trinity-alert-close, .__close) {
        color: var(--upaep-trinity-view-text-muted);
    }

    /*? Sombra solo cuando el aviso FLOTA (un toast). Dentro de la vista la capa
       no usa sombras: hairlines. */
    /*? La sombra es la del design system (`--ui-shadow`, la misma que usa el
       toast del core), no una propia: un aviso flotante y un toast deben
       levantarse igual. */
    &.__floating {
        border-color: transparent;
        box-shadow: var(--ui-shadow, var(--upaep-box-shadow, 0 6px 24px rgb(0 0 0 / .12)));
    }
}

/*? ── Reutilizar la piel del aviso en un toast o en Herald ──────────────────
   Herald acepta `customClass`, así que la forma `__clean` se puede pedir en un
   toast:

       Herald.toast({ icon: 'success', title: '…', text: '…',
                      customClass: 'upaep__trinity-alert __clean __floating' });

   Y funciona con CERO reglas por tipo, que es el premio de haber puesto el color
   en tokens: el toast lleva su propio `__success`/`__error`/… en la lista de
   clases, así que las reglas de tipo de arriba ya le calculan `hue` y `on-hue`.

   OJO — dos cosas que cuestan depuración:

   1. Una pieza anfitriona trae SU PROPIA CAJA, y hay que devolvérsela. Este
      bloque declara relleno, fondo y filo directos (para poder viajar suelto),
      y encima de un toast eso le pisaba el fondo: `.upaep__notification.__success`
      pinta verde oscuro con TEXTO BLANCO, así que al volver el fondo blanco el
      texto desaparecía — un toast en blanco, literalmente. La piel se pide
      REMAPEANDO SUS TOKENS, no pisándole la caja.

   2. El diálogo de Herald no es su propia tarjeta: `.upaep__herald` es el
      envoltorio y la caja visible es la `.upaep__modal-content` del core. Ahí la
      clase no puede repintar la superficie; lo que sí se puede llevar es el
      ICONO, que es donde vive el color en esta forma. */
.upaep__notification.upaep__trinity-alert {
    /*? se le devuelve su caja: la de aquí no aplica */
    display: block;
    padding: var(--upaep-notification-padding);
    border: var(--upaep-notification-border-size) var(--upaep-notification-border-style) var(--upaep-notification-border-color);
    border-radius: var(--upaep-notification-border-radius);
    background: var(--upaep-notification-bg);
    color: inherit;
    font-size: inherit;
    font-weight: inherit;
}

/*? La BARRA lateral, también en el toast. Va aparte porque el bloque de arriba
   le devuelve su `border` completo al toast y con ello se llevaba la barra: hace
   falta volver a ponerla con más especificidad. El relleno del toast es 0 (padean
   sus hijos), así que aquí no hay que compensar nada. */
.upaep__notification.upaep__trinity-alert:is(.__tinted, .__clean) {
    border-left: var(--upaep-trinity-alert-rail) solid var(--upaep-trinity-alert-hue);
}

/*? `__tinted` sobre un toast: relleno tenue del tono y TODO el texto en la tinta
   derivada —título, apoyo, icono y aspa—, que es lo que hace que el aviso se lea
   «de un color». Los tokens del tipo ya están resueltos: el toast lleva su propio
   `__success`/`__error`/… en la lista de clases. */
.upaep__notification.upaep__trinity-alert.__tinted {
    --upaep-notification-bg: color-mix(in srgb, var(--upaep-trinity-alert-hue) 14%, var(--upaep-trinity-view-surface));
    --upaep-notification-border-color: color-mix(in srgb, var(--upaep-trinity-alert-hue) 26%, transparent);
    --upaep-notification-header-title-color: var(--upaep-trinity-alert-ink);
    --upaep-notification-content-text-color: var(--upaep-trinity-alert-ink);
    --upaep-notification-content-text-fw: var(--upaep-fw-regular, 400);
    --upaep-notification-content-icon-color: var(--upaep-trinity-alert-ink);
    --upaep-notification-close-color: var(--upaep-trinity-alert-ink);
    --upaep-notification-close-hover-color: var(--upaep-trinity-alert-ink);
    --upaep-notification-header-border-bottom-color: transparent;
}

/*? El anillo del icono pasa a chip TEÑIDO (el sólido es de `__clean`) */
.upaep__notification.upaep__trinity-alert.__tinted .upaep__notification-icons .upaep__notification-icon::before {
    border-color: transparent;
    background: color-mix(in srgb, var(--upaep-trinity-alert-hue) 30%, transparent);
    color: var(--upaep-trinity-alert-ink);
}

/*? `__clean` sobre un toast: superficie, título fuerte, texto tenue, aspa
   discreta y el color SOLO en el icono — la misma regla de reparto que en el
   aviso, dicha con los tokens del toast. */
.upaep__notification.upaep__trinity-alert.__clean {
    --upaep-notification-bg: var(--upaep-trinity-view-surface);
    --upaep-notification-header-title-color: var(--upaep-trinity-view-text-strong);
    --upaep-notification-content-text-color: var(--upaep-trinity-view-text-muted);
    --upaep-notification-content-text-fw: var(--upaep-fw-regular, 400);
    --upaep-notification-close-color: var(--upaep-trinity-view-text-muted);
    --upaep-notification-close-hover-color: var(--upaep-trinity-view-text-strong);
    --upaep-notification-header-border-bottom-color: transparent;
    /*? el icono recupera el color del tipo (el toast sólido lo había puesto en
       blanco para leerse sobre su relleno) */
    --upaep-notification-content-icon-color: var(--upaep-trinity-alert-hue);
}

.upaep__notification.upaep__trinity-alert.__clean.__floating {
    --upaep-notification-border-color: transparent;
}

/*? El anillo del icono pasa a chip SÓLIDO. El glifo no puede heredar el color
   —sería del mismo tono que su fondo—: se le pasa el color de contraste. */
.upaep__notification.upaep__trinity-alert.__clean .upaep__notification-icons .upaep__notification-icon::before {
    border-color: transparent;
    background: var(--upaep-trinity-alert-hue);
    color: var(--upaep-trinity-alert-on-hue);
}

/*? Herald: el envoltorio no pinta nada, así que se le quita la caja de aquí y
   se le lleva solo el icono — de anillo a chip sólido. El tono sale del token
   del propio Herald, que es quien lleva el tipo en el icono. */
.upaep__herald.upaep__trinity-alert {
    display: block;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font-size: inherit;
    font-weight: inherit;
}

.upaep__herald.upaep__trinity-alert.__clean .upaep__herald-icon {
    border-color: transparent;
    background: var(--upaep-herald-icon-color);
}

.upaep__herald.upaep__trinity-alert.__clean .upaep__herald-icon::before {
    color: var(--upaep-trinity-view-surface);
}

/*? Y la forma teñida: el mismo anillo relleno al 30 %, con el glifo en el color
   del tipo. El tono sale de `--upaep-herald-icon-color` y no del token del
   aviso, porque en un diálogo el tipo lo lleva el ICONO, no el envoltorio. */
.upaep__herald.upaep__trinity-alert.__tinted .upaep__herald-icon {
    border-color: transparent;
    background: color-mix(in srgb, var(--upaep-herald-icon-color) 30%, transparent);
}


/*? ── Etiquetas (tags) ──────────────────────────────────────────────────────
   Una etiqueta NO es una insignia, y conviene no mezclarlas: el badge dice un
   ESTADO del elemento que lo lleva («activo», «4 sin leer»); la etiqueta dice
   que el elemento PERTENECE a algo (una categoría, un periodo, una persona),
   suele venir en grupo y a menudo se puede quitar. De ahí las dos diferencias
   de forma: la etiqueta lleva filo —para que se lea como una unidad separable—
   y admite ✕; el badge es un rótulo macizo y no se cierra.

   Comparten PROPORCIONES a propósito (mismo alto, mismo texto, mismo aire que
   `.upaep__badge` con los valores de esta capa), para que un badge y una
   etiqueta en la misma fila no se vean de dos tamaños distintos.

   El tono cuelga de un token, como en el aviso: una variante nueva es una línea.
   Y el ✕ es el MISMO botón estándar del aviso, un escalón más pequeño. */
.upaep__trinity-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    min-width: 0;
}

.upaep__trinity-tag {
    --upaep-trinity-tag-hue: var(--upaep-trinity-view-text-muted);
    --upaep-trinity-tag-bg: var(--upaep-trinity-view-surface-2);
    --upaep-trinity-tag-color: var(--upaep-trinity-view-text);

    display: inline-flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    max-width: 100%;
    padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
    border: 1px solid color-mix(in srgb, var(--upaep-trinity-tag-hue) 30%, transparent);
    border-radius: var(--upaep-trinity-radius-sm, 6px);
    background: var(--upaep-trinity-tag-bg);
    color: var(--upaep-trinity-tag-color);
    font-size: var(--upaep-trinity-fz-meta, .75rem);
    font-weight: var(--upaep-fw-medium, 500);
    line-height: 1.2;
    /*? el rótulo no envuelve: una etiqueta de dos líneas deja de ser etiqueta */
    white-space: nowrap;

    /*? Con nombres largos (un correo, un nombre completo) se recorta en vez de
       ensanchar la fila; el título completo lo pone el autor en `title`. */
    & > span {
        overflow: hidden;
        text-overflow: ellipsis;
    }

    & > svg {
        flex: 0 0 auto;
        width: .9rem;
        height: .9rem;
        fill: none;
        stroke: currentColor;
        stroke-width: 2;
        stroke-linecap: round;
        stroke-linejoin: round;
        color: var(--upaep-trinity-tag-hue);
    }

    /*? Píldora, para grupos largos de categorías */
    &.__rounded {
        border-radius: var(--upaep-trinity-radius-pill, 999px);
        padding-inline: var(--upaep-trinity-sp-3, .75rem);
    }

    /*? Pulsable (un filtro que se aplica al hacer clic). Solo cuando de verdad
       hace algo: si no, el puntero promete y no cumple. */
    &.__interactive {
        cursor: pointer;
        transition: background .15s ease, border-color .15s ease;

        &:hover {
            background: color-mix(in srgb, var(--upaep-trinity-tag-hue) 14%, var(--upaep-trinity-tag-bg));
            border-color: color-mix(in srgb, var(--upaep-trinity-tag-hue) 55%, transparent);
        }
    }

    &:focus-visible {
        outline: var(--ui-focus-ring-width, 2px) solid var(--ui-focus-ring-color, currentColor);
        outline-offset: var(--ui-focus-ring-offset, 2px);
    }

    /*? ── El tono, una línea por variante ── */
    &.__accent {
        --upaep-trinity-tag-hue: var(--upaep-trinity-view-accent);
        --upaep-trinity-tag-color: var(--upaep-trinity-view-accent);
        --upaep-trinity-tag-bg: color-mix(in srgb, var(--upaep-trinity-view-accent) 10%, var(--upaep-trinity-view-surface));
    }

    &.__success {
        --upaep-trinity-tag-hue: var(--ui-success, #28a745);
        --upaep-trinity-tag-color: var(--ui-on-success-subtle, #1f7634);
        --upaep-trinity-tag-bg: var(--ui-success-subtle, #d8f0de);
    }

    &.__warning {
        --upaep-trinity-tag-hue: var(--ui-warning, #ffc107);
        --upaep-trinity-tag-color: var(--ui-on-warning-subtle, #8a5a00);
        --upaep-trinity-tag-bg: var(--ui-warning-subtle, #fdf0cd);
    }

    &.__danger {
        --upaep-trinity-tag-hue: var(--ui-danger, #dc3545);
        --upaep-trinity-tag-color: var(--ui-on-danger-subtle, #b7192c);
        --upaep-trinity-tag-bg: var(--ui-danger-subtle, #fadfe2);
    }

    &.__info {
        --upaep-trinity-tag-hue: var(--ui-info, #437eea);
        --upaep-trinity-tag-color: var(--ui-on-info-subtle, #2950cc);
        --upaep-trinity-tag-bg: var(--ui-info-subtle, #dbe7fd);
    }

    &.__neutral {
        --upaep-trinity-tag-hue: var(--upaep-trinity-view-text-muted);
        --upaep-trinity-tag-bg: var(--upaep-trinity-view-surface-2);
        --upaep-trinity-tag-color: var(--upaep-trinity-view-text);
    }

    /*? ── El RELLENO va DESPUÉS del tono, y no es un detalle ────────────────
       `__solid` y `__outline` cambian lo mismo que las variantes de tono
       (`--upaep-trinity-tag-bg`) y con la misma especificidad —dos clases—, así
       que gana la última que aparezca en la hoja. Escritas antes, un
       `__solid __accent` salía TENUE: el tono repintaba el fondo que el relleno
       acababa de poner. Van al final para que el relleno mande sobre el tono,
       que es lo que se espera al combinarlos. */

    /*? Rellena del tono: para la etiqueta que hay que ver primero. Una por
       grupo, o el grupo entero pierde el sentido. */
    &.__solid {
        --upaep-trinity-tag-bg: var(--upaep-trinity-tag-hue);
        --upaep-trinity-tag-color: var(--upaep-trinity-view-surface);
        border-color: transparent;

        & > svg { color: currentColor; }
    }

    /*? Solo filo, sin relleno: la más callada, para metadatos */
    &.__outline {
        --upaep-trinity-tag-bg: transparent;
    }
}

/*? El ✕ de la etiqueta: el mismo botón estándar del aviso, más pequeño y sin
   celda de rejilla (aquí es un hijo en línea). */
.upaep__trinity-tag-close {
    width: 1.15rem;
    height: 1.15rem;
    margin-right: -.15rem;

    & > svg {
        width: .8rem;
        height: .8rem;
    }
}


/*? ── Variantes semánticas de badge ────────────────────────────────────────
   El `.upaep__badge` del core solo trae `__rounded` y `__status`: el color se
   pasa por token, así que un badge de estado obliga a `style="--upaep-badge-bg:…"`
   en cada uso. Aquí se nombran las cinco variantes de siempre remapeando esos
   mismos tokens (no se reimplementa el badge). Relleno TENUE, que es lo que pide
   el lenguaje minimalista y lo único que funciona igual en claro y en oscuro.
   Son aditivas: si algún día pasan al core, esta sección se borra y nada cambia. */
.upaep__badge {
    /*? GEOMETRIA SOBRE LA ESCALA. El badge acumulaba tres juegos de medidas: el
       del core (`.35rem .5rem`, radio 5), el de esta capa (`.4rem .7rem`, radio
       6) y el de la variante `__status` del core (`.75rem 1rem`, radio 4) que,
       por llevar dos clases, ganaba a las otras dos — lo que se veia en pantalla
       era una capsula de 12x16 px con esquinas de 4, o sea ninguna de las tres
       intenciones declaradas. Ahora hay UNA: relleno de escala y el radio
       canonico de capsula. Color y semantica no se tocan. */
    --upaep-badge-padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-3, .75rem);
    --upaep-badge-fz: var(--upaep-trinity-fz-meta, .75rem);
    --upaep-badge-border-radius: var(--upaep-trinity-radius-pill, 999px);
    line-height: var(--upaep-trinity-lh-control, 1.2);
}

/*? MISMA ESPECIFICIDAD que la variante del core, o no gana. El core viste el badge
   de estado con `.upaep__badge.__status:not(:is(.__secondary, .__tertiary))`, que
   pesa (0,3,0): un bloque de una sola clase —o incluso de dos— pierde contra el, y
   lo que se veia en pantalla (12x16 px, esquinas de 4) no era ninguna de las tres
   geometrias declaradas. Se repite aqui la MISMA forma de selector, guarda incluida,
   para no pisar de paso las dos variantes que el core trata aparte a proposito. Se
   remapea solo la CAJA: el color y la semantica siguen siendo del core. */
.upaep__badge.__status:not(:is(.__secondary, .__tertiary)),
.upaep__badge.__rounded,
.upaep__badge.__status.__rounded {
    --upaep-badge-padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-3, .75rem);
    --upaep-badge-border-radius: var(--upaep-trinity-radius-pill, 999px);

    &.__success {
        --upaep-trinity-tag-hue: var(--ui-success, #28a745);
        --upaep-badge-bg: var(--ui-success-subtle, #d8f0de);
        --upaep-badge-color: var(--ui-on-success-subtle, #1f7634);
    }

    &.__warning {
        --upaep-trinity-tag-hue: var(--ui-warning, #ffc107);
        --upaep-badge-bg: var(--ui-warning-subtle, #fdf0cd);
        --upaep-badge-color: var(--ui-on-warning-subtle, #8a5a00);
    }

    &.__danger {
        --upaep-trinity-tag-hue: var(--ui-danger, #dc3545);
        --upaep-badge-bg: var(--ui-danger-subtle, #fadfe2);
        --upaep-badge-color: var(--ui-on-danger-subtle, #b7192c);
    }

    &.__info {
        --upaep-trinity-tag-hue: var(--ui-info, #437eea);
        --upaep-badge-bg: var(--ui-info-subtle, #dbe7fd);
        --upaep-badge-color: var(--ui-on-info-subtle, #2950cc);
    }

    &.__neutral {
        --upaep-trinity-tag-hue: var(--upaep-trinity-view-text-muted);
        --upaep-badge-bg: var(--upaep-trinity-view-surface-2);
        --upaep-badge-color: var(--upaep-trinity-view-text);
    }

    /*? ── Flotante (contador anclado a un icono) ───────────────────────────
       El badge del core es `position: relative` por defecto: con solo pasarle
       `--upaep-badge-inset` NO flota, se queda en el flujo y empuja el layout
       (aparecía debajo de la campana). Hay que cambiar también su `position`.
       El contenedor debe ser posicionado — los botones fantasma del cascarón y
       `.upaep__btn` ya lo son. */
    &.__floating {
        --upaep-badge-position: absolute;
        --upaep-badge-inset: -.3rem -.3rem auto auto;
        --upaep-badge-padding: 0 var(--upaep-trinity-sp-1, .25rem);
        --upaep-badge-fz: var(--upaep-fz10, .625rem);
        --upaep-badge-fw: var(--upaep-fw-semibold, 600);
        --upaep-badge-border-radius: var(--upaep-trinity-radius-pill, 999px);

        display: grid;
        place-items: center;
        min-width: 1.05rem;
        height: 1.05rem;
        z-index: 1;
        /*? anillo del color de la barra: separa el contador del icono */
        box-shadow: 0 0 0 2px var(--upaep-trinity-surface, #fff);
        pointer-events: none;
    }
}


/*? ── Una sola píldora: etiqueta y badge se ven igual ─────────────────────────
   En una tabla (o en cualquier fila) conviven `.upaep__trinity-tag` y `.upaep__badge`, y se
   escribían como dos estilos: la etiqueta con filo y esquinas de 6px, el badge macizo y sin filo,
   y encima `__rounded` en uno sí y en otro no. Aquí se igualan en FORMA (alto, aire, filo, radio,
   texto) y en COLOR (los mismos cinco tonos, que ya salen de `--ui-*-subtle`): lo único que las
   distingue es el significado —estado vs. pertenencia—, no la apariencia.
   El badge flotante (contador sobre un icono) NO entra: es otra pieza. */
:is(.upaep__trinity-tag, .upaep__badge:not(.__floating)) {
    --_hue: var(--upaep-trinity-tag-hue, var(--upaep-trinity-view-text-muted));
    display: inline-flex;
    align-items: center;
    gap: var(--upaep-trinity-sp-2, .5rem);
    box-sizing: border-box;
    min-block-size: 1.75rem;
    padding: var(--upaep-trinity-sp-1, .25rem) var(--upaep-trinity-sp-3, .75rem);
    border: 1px solid color-mix(in srgb, var(--_hue) 30%, transparent);
    border-radius: 6px;
    font-size: var(--upaep-fz13, .8125rem);
    font-weight: var(--upaep-fw-medium, 500);
    line-height: 1.2;
    white-space: nowrap;

    &.__rounded {
        border-radius: 999px;
    }
}

/*? Un badge sin tono es neutro, como una etiqueta sin tono */
.upaep__badge:not(.__floating, .__success, .__warning, .__danger, .__info, .__neutral, .__status) {
    --upaep-badge-bg: var(--upaep-trinity-view-surface-2);
    --upaep-badge-color: var(--upaep-trinity-view-text);
}

/*? ── Herald / offcanvas dentro de Trinity ───────────────────────────────────
   Mismo ritmo que el panel: relleno IGUAL en los cuatro lados y 1rem entre las piezas del contenido
   (una ficha de datos, sus etiquetas, un formulario…). Los tokens son los del Offcanvas del kit. */
.upaep__offcanvas.upaep__herald {
    --upaep-offcanvas-header-padding: var(--upaep-trinity-panel-padding);
    --upaep-offcanvas-content-padding: var(--upaep-trinity-panel-padding);
    --upaep-offcanvas-footer-padding: var(--upaep-trinity-panel-padding);
    --upaep-herald-html-gap: var(--upaep-trinity-view-gap);
    --upaep-herald-html-align: start;
}

/*? ── Tabs a todo el ancho ──────────────────────────────────────────────────
   Las tabs del core reparten con `space-around` y cada pestaña mide `auto`. Con
   `.__stretch` las cuatro ocupan por igual todo el ancho disponible: solo se
   remapean sus tokens, la estructura y el runtime siguen siendo del core. */
.upaep__tab.__stretch {
    --upaep-tab-tabs-width: 100%;
    --upaep-tab-tabsitem-justify: stretch;
    /*? `auto`, NO 100%: el reparto lo hace `flex-grow`. Con `width: 100%` y sin
       poder encogerse (hace falta `flex-shrink: 0` para que el scroll funcione),
       la primera pestaña se comía la fila entera y las demás caían al scroll. */
    --upaep-tab-tabitem-width: auto;
    --upaep-tab-tabitem-max-width: none;

    & .upaep__tabs-item {
        width: 100%;
        display: flex;
    }
}

/*? ── Tabs de subrayado (nivel secundario) ──────────────────────────────────
   Las pestañas «carpeta» rojas del core pesan mucho para un SEGUNDO nivel: dos
   niveles de bloques sólidos compiten entre sí. Con `.__underline` el nivel de
   dentro se vuelve texto con una barra de acento debajo del activo —lo que se ve
   en el ejemplo—, y el de fuera se queda como está para marcar la jerarquía.
   Todo por tokens: la estructura y el runtime siguen siendo del core. */
.upaep__tab.__underline {
    --upaep-tab-boxshadow: none;
    --upaep-tab-padding: 0;
    --upaep-tab-border-radius: 0;
    --upaep-tab-tabs-bg: transparent;

    /*? el riel inferior es un hairline, no una barra de 3px de acento */
    --upaep-tab-tabs-border-bottom-size: 1px;
    --upaep-tab-tabs-border-bottom-color: var(--upaep-trinity-view-divider);

    --upaep-tab-tabitem-minheight: 2.25rem;
    --upaep-tab-tabitem-min-width: 0;
    --upaep-tab-tabitem-max-width: none;
    --upaep-tab-tabitem-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
    --upaep-tab-tabitem-fz: var(--upaep-trinity-fz-meta, .75rem);
    --upaep-tab-tabitem-fw: var(--upaep-fw-medium, 500);
    --upaep-tab-tabitem-color: var(--upaep-trinity-view-text-muted);
    --upaep-tab-tabitem-bg: transparent;
    --upaep-tab-tabitem-border-radius: var(--upaep-trinity-radius-sm, 6px) var(--upaep-trinity-radius-sm, 6px) 0 0;

    --upaep-tab-tabitem-active-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
    --upaep-tab-tabitem-active-fz: var(--upaep-trinity-fz-meta, .75rem);
    --upaep-tab-tabitem-active-fw: var(--upaep-fw-semibold, 600);
    --upaep-tab-tabitem-active-color: var(--upaep-trinity-view-text-strong);
    --upaep-tab-tabitem-active-bg: transparent;
    --upaep-tab-tabitem-active-border-radius: var(--upaep-trinity-radius-sm, 6px) var(--upaep-trinity-radius-sm, 6px) 0 0;

    --upaep-tab-tabitem-hover-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
    --upaep-tab-tabitem-hover-fz: var(--upaep-trinity-fz-meta, .75rem);
    --upaep-tab-tabitem-hover-fw: var(--upaep-fw-medium, 500);
    --upaep-tab-tabitem-hover-color: var(--upaep-trinity-view-text-strong);
    --upaep-tab-tabitem-hover-bg: var(--upaep-trinity-view-hover-bg);
    --upaep-tab-tabitem-hover-border-radius: var(--upaep-trinity-radius-sm, 6px) var(--upaep-trinity-radius-sm, 6px) 0 0;

    /*? La pestaña ACTIVA no debe cambiar al pasar el puntero: ya está elegida, y
       si baja de peso o cambia de color parece que se «desactiva». El core tiene
       tokens propios para ese estado, así que se igualan a los de activo. */
    --upaep-tab-tabitem-active-hover-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
    --upaep-tab-tabitem-active-hover-fz: var(--upaep-trinity-fz-meta, .75rem);
    --upaep-tab-tabitem-active-hover-fw: var(--upaep-fw-semibold, 600);
    --upaep-tab-tabitem-active-hover-color: var(--upaep-trinity-view-text-strong);
    --upaep-tab-tabitem-active-hover-bg: transparent;
    --upaep-tab-tabitem-active-hover-border-radius: var(--upaep-trinity-radius-sm, 6px) var(--upaep-trinity-radius-sm, 6px) 0 0;

    /*? Las pestañas se agrupan a la izquierda, no repartidas */
    --upaep-tab-tabsitem-justify: flex-start;

    & .upaep__tab-item {
        position: relative;
        transition: color .15s ease;

        /*? El subrayado del activo: pseudoelemento propio, para poder animarlo */
        &::after {
            content: "";
            position: absolute;
            inset: auto 0 -1px 0;
            height: 2px;
            background: var(--upaep-trinity-view-accent);
            transform: scaleX(0);
            transform-origin: center;
            transition: transform .18s ease;
        }

        &.is-active::after { transform: scaleX(1); }
    }

    /*? Contenido sin caja: en el segundo nivel la caja ya la pone el panel */
    & .upaep__tabs-content {
        background: transparent;
        padding-top: var(--upaep-trinity-sp-4, 1rem);
    }
}

/*? ── Subrayado que SIGUE a la pestaña ──────────────────────────────────────
   Una sola barra que se desliza en vez de una por pestaña que se apaga y se
   enciende. El ojo sigue el movimiento y entiende de dónde a dónde fue.

   ESTO NO ES UNA PIEZA NUEVA: el core YA tiene la línea deslizante y su runtime
   (inyecta `.upaep__tab-line`, la mide contra la activa y la reposiciona en el
   `load`, al pulsar y al redimensionar). Aquí solo se combina con `__underline`
   y se le quita el peso: la del core es una barra de 5px con radio 10px y
   margen inferior, que junto al lenguaje de hairlines de esta capa canta.

       <div class="upaep__tab __underline tab-line">

   OJO CON EL NOMBRE: es `tab-line`, SIN el prefijo `__`. El runtime del core
   solo actúa con ese nombre; `.__line` existe en su CSS pero no recibe el nodo
   inyectado, así que con `__line` no aparece nada.

   Y lo que de verdad hacía falta arreglar: el `.__underline` del CORE ya pinta
   un `::before` bajo cada pestaña, y lo enciende en `.is-active` Y en `:hover`.
   Con la línea deslizante encima se veían las dos y el deslizamiento no se
   notaba. Se apaga aquí — hay que igualar la profundidad de su selector
   (0,4,0) y sumarle una clase más para ganarle. */
.upaep__tab.__underline.tab-line {
    & .upaep__tabs {
        position: relative;
    }

    /*? el subrayado por pestaña se retira: la barra deslizante lo sustituye.
       Se listan los dos pseudoelementos: el `::before` es del core y el
       `::after` es el de esta hoja (bloque `.__underline` de arriba). */
    & .upaep__tabs .upaep__tab-item::before,
    & .upaep__tabs .upaep__tab-item::after,
    & .upaep__tabs .upaep__tab-item.is-active::before,
    & .upaep__tabs .upaep__tab-item:hover::before {
        content: none;
    }

    /*? La barra del core, en clave de hairline. El core la declara con
       literales (no expone tokens para ella), así que se redeclaran sus
       propiedades — pero SOLO dentro de esta variante de Trinity, no global. */
    & .upaep__tab-line {
        height: 2px;
        margin: 0;
        /*? El radio ES el grosor de la barra: redondea sus dos extremos y nada
           mas. No pertenece a la escala de radios de superficie —una capsula de
           2 px no existe— asi que se declara como excepcion en
           `src/site/data/scales.decisions.json`. */
        border-radius: 2px 2px 0 0;
        background-color: var(--upaep-trinity-view-accent);
        transition: left .24s cubic-bezier(.32, .72, 0, 1),
                    width .24s cubic-bezier(.32, .72, 0, 1);
    }

    /*? Con movimiento reducido salta en vez de deslizarse: se sigue viendo
       dónde está el activo, que es lo que informa. */
    @media (prefers-reduced-motion: reduce) {
        & .upaep__tab-line { transition: none; }
    }
}


/*? ── Muchas pestañas: SCROLL horizontal, nunca apilar ──────────────────────
   Cuando no caben hay dos salidas y no son equivalentes:

     apilar en varias filas → deja de leerse como una fila de pestañas, y el alto
       de la cabecera cambia según el ancho, así que el contenido «salta» de sitio
       al girar el teléfono;
     scroll horizontal → conserva el patrón, el alto es fijo y es lo que hacen
       iOS, Android y Material.

   El comportamiento es el MISMO A CUALQUIER ANCHO, no solo en móvil: `flex: 1 0 auto`
   hace que las pestañas crezcan a partes iguales cuando caben, y que conserven su
   ancho de contenido cuando no — momento en el que la fila se arrastra. O sea: se
   reparten si sobra sitio, scrollean si falta, sin depender de un breakpoint. */
.upaep__tab.__stretch,
.upaep__tab.__underline,
.upaep__tab.__scroll {
    /*? CADENA DE `min-width: 0` — esto es lo que faltaba y por lo que la fila
       desbordaba en vez de scrollear. `.upaep__tabs-item` es un ITEM FLEX de
       `.upaep__tabs`, y un item flex no se encoge por debajo de su contenido
       mínimo salvo que se le diga: como sus pestañas llevan `min-width: max-content`,
       ese mínimo era la suma de todas y empujaba al contenedor a lo ancho.
       Con `min-width: 0` el scroller sí puede quedarse más angosto que su
       contenido, que es la condición para que `overflow-x` scrollee. */
    min-width: 0;

    /*? EL MARGEN ES LA OTRA MITAD DEL DESBORDE. El core le da a la fila
       `--upaep-tab-tabs-margin: 0 1rem` para que se vea despegada de los bordes;
       combinado con el `width: 100%` que necesita el scroll, el resultado mide
       100% + 2rem y se sale 32px. El aire lo pone el padding del contenedor, no
       un margen que se suma al ancho. */
    --upaep-tab-tabs-margin: 0;

    & .upaep__tabs {
        min-width: 0;
        max-width: 100%;
    }

    & .upaep__tabs-item {
        min-width: 0;
        max-width: 100%;
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        overscroll-behavior-x: contain;
        scroll-snap-type: x proximity;
        /*? la barra se oculta: la fila ya se entiende por el corte de la última */
        scrollbar-width: none;
        -ms-overflow-style: none;
    }

    & .upaep__tabs-item::-webkit-scrollbar {
        display: none;
    }

    & .upaep__tab-item {
        scroll-snap-align: start;
    }
}

/*? `__underline` también necesita que su fila mida el 100%: sin esto el
   contenedor crece con el contenido y no hay nada que scrollear. */
.upaep__tab.__underline {
    --upaep-tab-tabs-width: 100%;
}


/*? ── Variante segmentada (bloques unidos) ──────────────────────────────────
   Las pestañas se pegan entre sí y forman un único control: activa en acento,
   inactivas en gris, esquinas redondeadas solo en los extremos.

   CUÁNDO USARLA: con POCAS opciones (2–4) que SIEMPRE quepan. Un control
   segmentado promete «estas son todas las opciones, de un vistazo»; si hay que
   scrollear para ver el resto, la promesa se rompe y además los bloques grises
   pesan tanto como el activo, compitiendo con el contenido.

   CUÁNDO NO: en el nivel principal del catálogo (6 pestañas que scrollean) y en
   cualquier segundo nivel — ahí siguen siendo mejores las pestañas del core y
   `.__underline` respectivamente. Por eso es una variante y no el estilo base.

   Combínala con `.__stretch` para que ocupe todo el ancho. */
.upaep__tab.__segmented {
    --upaep-tab-boxshadow: none;
    --upaep-tab-padding: 0;
    --upaep-tab-tabs-margin: 0;
    --upaep-tab-tabs-border-bottom-size: 2px;
    --upaep-tab-tabs-border-bottom-color: var(--upaep-trinity-view-accent);

    --upaep-tab-tabitem-minheight: 2.4rem;
    --upaep-tab-tabitem-min-width: 0;
    --upaep-tab-tabitem-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-4, 1rem);
    --upaep-tab-tabitem-fz: var(--upaep-fz14, .875rem);
    --upaep-tab-tabitem-fw: var(--upaep-fw-semibold, 600);
    --upaep-tab-tabitem-border-radius: 0;
    --upaep-tab-tabitem-bg: var(--upaep-trinity-view-surface-3, #eee);
    --upaep-tab-tabitem-color: var(--upaep-trinity-view-text);

    --upaep-tab-tabitem-active-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-4, 1rem);
    --upaep-tab-tabitem-active-fz: var(--upaep-fz14, .875rem);
    --upaep-tab-tabitem-active-fw: var(--upaep-fw-semibold, 600);
    --upaep-tab-tabitem-active-border-radius: 0;
    --upaep-tab-tabitem-active-bg: var(--upaep-trinity-view-accent);
    --upaep-tab-tabitem-active-color: var(--upaep-trinity-view-on-accent);

    --upaep-tab-tabitem-hover-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-4, 1rem);
    --upaep-tab-tabitem-hover-fz: var(--upaep-fz14, .875rem);
    --upaep-tab-tabitem-hover-fw: var(--upaep-fw-semibold, 600);
    --upaep-tab-tabitem-hover-border-radius: 0;
    --upaep-tab-tabitem-hover-bg: var(--upaep-trinity-view-hover-page);
    --upaep-tab-tabitem-hover-color: var(--upaep-trinity-view-text-strong);

    /*? la activa no reacciona al hover (ya está elegida) */
    --upaep-tab-tabitem-active-hover-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-4, 1rem);
    --upaep-tab-tabitem-active-hover-fz: var(--upaep-fz14, .875rem);
    --upaep-tab-tabitem-active-hover-fw: var(--upaep-fw-semibold, 600);
    --upaep-tab-tabitem-active-hover-border-radius: 0;
    --upaep-tab-tabitem-active-hover-bg: var(--upaep-trinity-view-accent);
    --upaep-tab-tabitem-active-hover-color: var(--upaep-trinity-view-on-accent);

    /*? El redondeo va SOLO en los extremos: por dentro los bloques se tocan */
    & .upaep__tab-item {
        transition: background .15s ease, color .15s ease;

        &:first-child {
            border-start-start-radius: 8px;
        }

        &:last-child {
            border-start-end-radius: 8px;
        }
    }
}


/*? ── El padding del contenido se cuenta DOS veces ──────────────────────────
   El core lo aplica en dos capas: `.upaep__tabs-content` (el contenedor de todos
   los paneles) y ADEMÁS cada `.upaep__tab-content`. Son 1.5rem por lado cada una,
   o sea 3rem por lado antes de que empiece el panel. Aquí el aire lo pone el
   contenedor y el panel individual se queda a cero. */
.upaep__trinity-views .upaep__tab {
    --upaep-tab-tabcontent-padding: 0;
    --upaep-tab-tabcontent-second-padding: 0;
}


/*? ── Tabs ANIDADAS: el segundo nivel no vuelve a poner caja ────────────────
   A CUALQUIER ancho, no solo en móvil. El `.upaep__tab` de fuera ya aporta el
   padding, el fondo y el radio del contenido; si el de dentro los repite se
   acumulan. También se anula su sombra, que dibujaba una caja dentro de otra.
   Se cubren las variables normales y las `-second-`, que son las que usa el core
   cuando el contenido lleva la clase `.__second`. */
.upaep__tab .upaep__tab {
    --upaep-tab-padding: 0;
    --upaep-tab-margin: 0;
    --upaep-tab-bg: transparent;
    --upaep-tab-boxshadow: none;

    --upaep-tab-tabscontent-padding: 0;
    --upaep-tab-tabscontent-bg: transparent;
    --upaep-tab-tabscontent-border-radius: 0;
    --upaep-tab-tabscontent-boxshadow: none;

    --upaep-tab-tabscontent-second-padding: 0;
    --upaep-tab-tabscontent-second-bg: transparent;
    --upaep-tab-tabscontent-second-boxshadow: none;

    min-width: 0;
}

/*? Reparto EQUITATIVO cuando caben, ancho propio cuando no.
   `flex-basis: 0` + `grow: 1` reparte todo el espacio a partes iguales, así que las
   pestañas miden lo mismo aunque sus rótulos midan distinto (con `flex-basis: auto`
   cada una crecía desde su texto y quedaban desiguales: «Paneles» más angosta que
   «Formulario»). El suelo `min-width: max-content` impide que el texto se estruje:
   cuando la suma de esos mínimos supera el ancho, la fila desborda y scrollea.

   La ruta larga (`.upaep__tabs .upaep__tabs-item .upaep__tab-item`) no es adorno:
   el core anida a esa profundidad y con un selector corto su `flex: 0 0 auto`
   ganaba, dejando las pestañas del ancho de su texto en vez de repartidas. */
.upaep__tab.__stretch .upaep__tabs .upaep__tabs-item .upaep__tab-item {
    flex: 1 1 0;
    min-width: max-content;
}

/*? En `__underline` las pestañas nunca se estiran: se agrupan a la izquierda */
.upaep__tab.__underline .upaep__tab-item,
.upaep__tab.__scroll .upaep__tab-item {
    flex: 0 0 auto;
}

/*? ── Ficha de indicador (stat) ────────────────────────────────────────────
   Complemento habitual de los dashboards: rótulo tenue, cifra grande y una
   variación opcional. Se combina con `.upaep__trinity-cards` para la rejilla. */
.upaep__trinity-stat {
    display: flex;
    flex-direction: column;
    gap: var(--upaep-trinity-sp-1, .25rem);
    padding: var(--upaep-trinity-panel-padding);
    background: var(--upaep-trinity-panel-bg);
    border: 1px solid var(--upaep-trinity-panel-border);
    border-radius: var(--upaep-trinity-panel-radius);

    & .upaep__trinity-stat-label {
        margin: 0;
        /*? Rol `label`: 11 px quedaba por debajo del suelo del sistema. */
        font-size: var(--upaep-trinity-fz-label, .75rem);
        font-weight: var(--upaep-trinity-fw-label, 600);
        letter-spacing: .06em;
        text-transform: uppercase;
        color: var(--upaep-trinity-view-text-muted);
    }

    & .upaep__trinity-stat-value {
        margin: 0;
        /*? Rol `stat`. Medía 1.6rem (25.6 px) contra un `<h1>` de 20: la cifra de
           un indicador pesaba MÁS que el título de la página que la contiene, y
           además 1.6rem no era ningún peldaño de ninguna escala. Ahora va por
           debajo del h1 y se distingue por PESO, que es lo que hace que una cifra
           se lea como cifra sin robarle el primer plano al título. */
        font-size: var(--upaep-trinity-fz-stat, 1.375rem);
        font-weight: var(--upaep-trinity-fw-stat, 700);
        line-height: var(--upaep-trinity-lh-heading, 1.25);
        color: var(--upaep-trinity-view-text-strong);
        font-variant-numeric: tabular-nums;
    }

    & .upaep__trinity-stat-hint {
        margin: 0;
        font-size: var(--upaep-trinity-fz-meta, .75rem);
        color: var(--upaep-trinity-view-text-muted);
    }

    /*? ── Barra lateral de color ────────────────────────────────────────────
       Categoriza el indicador sin recargarlo. Todas las fichas llevan barra;
       lo que cambia es el color, así se leen como un conjunto y no como «una
       destacada y tres normales». */
    &.__accent,
    &.__success,
    &.__info,
    &.__warning,
    &.__danger,
    &.__neutral {
        border-left: 3px solid var(--upaep-trinity-stat-bar, var(--upaep-trinity-view-accent));
    }

    /*? U-56 · QUE SIGNIFICA CADA BARRA. `__accent` es MARCA: acota la ficha como
       "de la institucion", y por eso NO debe usarse para un contador neutro, un
       icono informativo ni una cifra sin estado — pintado de rojo institucional,
       un dato sin significado se lee como alerta. Para un dato neutro esta
       `__neutral`; para un estado, la familia semantica que corresponda. La cifra
       en si NUNCA toma el color de la barra salvo con `__tinted`, que es opt-in y
       solo tiene sentido cuando el dato ES el estado. */
    &.__success {
        --upaep-trinity-stat-bar: var(--ui-success, #2e9e4b);
        --upaep-trinity-stat-glyph: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Cpath d="M6.2 12.3 2 8.1l1.5-1.5 2.7 2.7 6.3-6.3L14 4.5z"/%3E%3C/svg%3E');
    }

    &.__info {
        --upaep-trinity-stat-bar: var(--ui-info, #2f6fed);
        --upaep-trinity-stat-glyph: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Cpath d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm1 11H7V7h2zm0-6.5H7v-2h2z"/%3E%3C/svg%3E');
    }

    &.__warning {
        --upaep-trinity-stat-bar: var(--ui-warning, #e0a800);
        --upaep-trinity-stat-glyph: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Cpath d="M8 1 15.5 14.5H.5zM7 6h2v4H7zm0 5h2v2H7z"/%3E%3C/svg%3E');
    }

    &.__danger {
        --upaep-trinity-stat-bar: var(--upaep-trinity-view-danger);
        --upaep-trinity-stat-glyph: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Cpath d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm3 8.6L9.6 11 8 9.4 6.4 11 5 9.6 6.6 8 5 6.4 6.4 5 8 6.6 9.6 5 11 6.4 9.4 8z"/%3E%3C/svg%3E');
    }

    &.__neutral { --upaep-trinity-stat-bar: var(--upaep-trinity-view-text-muted); }

    /*? U-09 · LA BARRA NO PUEDE SER LA UNICA SENAL.
       Cuatro fichas iguales, cuatro barras de colores distintos: quien no separa
       verde de ambar de rojo ve cuatro fichas iguales. La categoria pasa a tener
       tambien FORMA —paloma, i, triangulo, aspa—, que se distingue sin depender
       del tono (WCAG 1.4.1).

       El glifo va con `mask` y no como imagen de fondo: asi lo pinta
       `--upaep-trinity-stat-bar` y sigue al modo, a la marca y a cualquier
       redefinicion del rol sin tener que reescribir cuatro SVG. Es decorativo —
       redundante con el rotulo, que es quien lleva el significado para quien
       escucha— y por eso se dibuja en un pseudoelemento, que ningun lector anuncia.

       `__neutral` y `__accent` no llevan glifo a proposito: no son estados. */
    &:is(.__success, .__info, .__warning, .__danger) .upaep__trinity-stat-label {
        display: flex;
        align-items: center;
        gap: var(--upaep-trinity-sp-1, .25rem);

        &::before {
            content: "";
            flex: none;
            width: 1em;
            height: 1em;
            background-color: var(--upaep-trinity-stat-bar);
            mask: var(--upaep-trinity-stat-glyph) center / contain no-repeat;
            -webkit-mask: var(--upaep-trinity-stat-glyph) center / contain no-repeat;
        }
    }

    /*? La cifra puede tomar el color de la barra cuando el dato ES el estado */
    &.__tinted .upaep__trinity-stat-value {
        color: var(--upaep-trinity-stat-bar, var(--upaep-trinity-view-accent));
    }
}


/*? ═══════════════════════════════════════════════════════════════════════════
   VARIANTE ELEVADA — sombra en capas
   `.__raised` sobre cualquier pieza (panel, tile, stat, aside, toolbar), o sobre
   `.upaep__trinity-views` para toda la vista de golpe. Sombra en DOS capas (una
   corta y cerrada, otra larga y difusa) porque una sola se ve plana; y el borde
   se aclara para que la sombra haga el trabajo de separar.
════════════════════════════════════════════════════════════════════════════ */
/*? UNA capa, no dos. La sombra en dos capas —una corta y pegada, otra larga y
   difusa— se leía como DOS tarjetas apiladas: la de abajo asomando por el borde
   inferior. Una sola sombra a Y 4 con 6px de desenfoque separa la pieza del
   fondo sin duplicarla, y al ser corta no se sale del radio de la caja. */
:root {
    --upaep-trinity-raised-shadow: 0 4px 6px rgb(16 20 28 / .12);
    --upaep-trinity-raised-shadow-hover: 0 4px 10px rgb(16 20 28 / .18);
}

[data-theme="dark"] {
    --upaep-trinity-raised-shadow: 0 4px 6px rgb(0 0 0 / .55);
    --upaep-trinity-raised-shadow-hover: 0 4px 10px rgb(0 0 0 / .65);
}

@media (prefers-color-scheme: dark) {
    [data-theme="auto"] {
        --upaep-trinity-raised-shadow: 0 4px 6px rgb(0 0 0 / .55);
        --upaep-trinity-raised-shadow-hover: 0 4px 10px rgb(0 0 0 / .65);
    }
}

/*? Piezas elevadas, una por una… */
.upaep__trinity-panel.__raised,
.upaep__trinity-tile.__raised,
.upaep__trinity-stat.__raised,
.upaep__trinity-view-aside.__raised,
.upaep__trinity-view-toolbar.__boxed.__raised,
/*? …o toda la vista de una vez */
.upaep__trinity-views.__raised :is(.upaep__trinity-panel,
    .upaep__trinity-tile,
    .upaep__trinity-stat,
    .upaep__trinity-view-aside) {
    border-color: color-mix(in srgb, var(--upaep-trinity-panel-border) 55%, transparent);
    box-shadow: var(--upaep-trinity-raised-shadow);
}

/*? La transición de la sombra va aparte y EXCLUYE al aside a propósito.
   `transition` es una declaración de reemplazo, no aditiva: al aplicarla aquí se
   borraba la que el aside necesita para abrirse —`translate` en modo cajón,
   `flex-basis`/`width` en columna colapsable—. Síntoma: con `__raised` puesto (o
   con `raised: true` en Atrium, que lo pone en el contenedor) el cajón aparecía y
   desaparecía de golpe, sin deslizamiento. El aside conserva su sombra elevada;
   lo único que no anima es esa sombra, que es lo de menos frente a su apertura. */
.upaep__trinity-panel.__raised,
.upaep__trinity-tile.__raised,
.upaep__trinity-stat.__raised,
.upaep__trinity-view-toolbar.__boxed.__raised,
.upaep__trinity-views.__raised :is(.upaep__trinity-panel,
    .upaep__trinity-tile,
    .upaep__trinity-stat) {
    transition: box-shadow .18s ease, transform .18s ease;
}

/*? ── Realce al pasar: SOLO en lo que de verdad se puede pulsar ─────────────
   El hover es una promesa de interactividad. Si una tarjeta que no hace nada se
   eleva, el usuario la pulsa y no pasa nada — así que el efecto se pide con
   `__linked` (la tarjeta entera es un enlace) o `__interactive` (el panel
   responde al clic), nunca por el mero hecho de tener sombra.

   Va dentro de `@media (hover: hover)` porque en pantallas táctiles no hay
   puntero: ahí el `:hover` se «pega» tras el toque y la tarjeta se queda elevada
   hasta que tocas otra cosa.

   Se anima `transform` (compositable) y `box-shadow`; si algún día se nota el
   coste de repintar la sombra, el truco es moverla a un pseudoelemento y animar
   su `opacity`. */
@media (hover: hover) {

    .upaep__trinity-tile:is(.__linked, .__interactive).__raised:hover,
    .upaep__trinity-panel.__raised.__interactive:hover,
    .upaep__trinity-views.__raised :is(.upaep__trinity-tile:is(.__linked, .__interactive),
        .upaep__trinity-panel.__interactive):hover {
        box-shadow: var(--upaep-trinity-raised-shadow-hover);
        transform: translateY(-2px);
    }
}

/*? Un panel expandido nunca «flota» dos veces */
.upaep__trinity-panel.__expanded {
    transform: none !important;
}

/*? La barra lateral de color SOBREVIVE a `__raised`.
   El bloque de arriba usa el atajo `border-color`, que pisa los cuatro lados y se
   llevaba por delante la barra de las fichas de indicador (se quedaban sin línea al
   activar la sombra). Se vuelve a fijar solo el lado izquierdo, después y con
   especificidad suficiente para ganar también al selector de vista completa. */
.upaep__trinity-stat:is(.__accent, .__success, .__info, .__warning, .__danger, .__neutral),
.upaep__trinity-views.__raised .upaep__trinity-stat:is(.__accent, .__success, .__info, .__warning, .__danger, .__neutral) {
    border-left-color: var(--upaep-trinity-stat-bar, var(--upaep-trinity-view-accent));
}


/*? ═══════════════════════════════════════════════════════════════════════════
   10. RESPONSIVE Y MOVIMIENTO REDUCIDO
════════════════════════════════════════════════════════════════════════════ */
/*? Bajo 1024px la columna lateral se apila; bajo 780px pasa a cajón */
@media (width <=1200px) {
    .upaep__trinity-view-body {
        flex-direction: column;
        /*? CRÍTICO al apilar: el bloque base lleva `align-items: flex-start`
           porque en fila evita que la columna lateral se estire al alto de la
           principal. Al girar el eje a vertical, ese mismo valor pasa a ser la
           alineación HORIZONTAL y los hijos dejan de estirarse: cada uno toma
           el ancho de su CONTENIDO. Con una tabla dentro (que aporta un
           min-content de varios cientos de px) la columna principal se quedaba
           más ancha que la pantalla y arrastraba la página al scroll
           horizontal. Apilados sí tienen que llenar el ancho. */
        align-items: stretch;
    }

    .upaep__trinity-view-aside:not(.__offcanvas) {
        flex: 1 1 auto;
        width: 100%;
    }
}

@media (width <=780px) {
    :root {
        /*? el gap ya es 1rem en escritorio: aquí solo se recorta el relleno */
        --upaep-trinity-panel-padding: var(--upaep-trinity-sp-3, .75rem);
    }

    /*? ── Tabs en móvil ────────────────────────────────────────────────────
       El core reserva `--upaep-tab-padding: 2rem` alrededor del contenido y
       35px de alto mínimo por pestaña: en un teléfono eso es casi todo padding.
       Aquí se recorta y las pestañas pasan a scroll horizontal en vez de
       estrujarse a 60px cada una (ver el bloque `.__scroll`). */
    .upaep__tab {
        --upaep-tab-padding: 0;
        --upaep-tab-tabitem-minheight: 2.1rem;
        --upaep-tab-tabitem-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
        --upaep-tab-tabitem-active-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
        --upaep-tab-tabitem-hover-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
        --upaep-tab-tabitem-active-hover-padding: var(--upaep-trinity-sp-2, .5rem) var(--upaep-trinity-sp-3, .75rem);
        --upaep-tab-tabitem-min-width: 0;
        --upaep-tab-tabitem-fz: var(--upaep-trinity-fz-meta, .75rem);
        --upaep-tab-tabitem-active-fz: var(--upaep-trinity-fz-meta, .75rem);
        --upaep-tab-tabitem-hover-fz: var(--upaep-trinity-fz-meta, .75rem);
        --upaep-tab-tabitem-active-hover-fz: var(--upaep-trinity-fz-meta, .75rem);
    }

    /*? El scroll ya lo hacen a cualquier ancho (ver el bloque de `__stretch` /
       `__underline` / `__scroll`); aquí solo se afina el contenido. */
    .upaep__tab.__underline .upaep__tabs-content {
        padding-top: var(--upaep-trinity-sp-3, .75rem);
    }

    /*? El padding del contenido baja a la mitad: `--upaep-tab-tabscontent-padding`
       son 1.5rem por lado, que en un teléfono es demasiado aire. (Las anidadas ya
       lo tienen anulado a cualquier ancho, ver el bloque de tabs anidadas.) */
    .upaep__tab {
        --upaep-tab-tabscontent-padding: var(--upaep-trinity-sp-3, .75rem);
    }

    .upaep__trinity-view-header .upaep__trinity-view-header-row {
        align-items: flex-start;
        flex-direction: column;
        gap: var(--upaep-trinity-view-gap);
    }

    .upaep__trinity-view-actions {
        width: 100%;
    }

    /*? En móvil la columna lateral es SIEMPRE cajón. Se listan las dos formas
       (con y sin `__collapsible`) para que el `transition: translate` gane a la
       transición de ancho del colapso en línea, que es más específica y dejaba el
       cajón apareciendo de golpe, sin deslizamiento. */
    .upaep__trinity-view-aside,
    .upaep__trinity-view-aside.__collapsible {
        position: fixed;
        inset: 0 0 0 auto;
        z-index: var(--upaep-zindex-offcanvas, 1055);
        width: min(var(--upaep-trinity-aside-width), 92vw);
        height: 100dvh;
        border: 0;
        border-left: 1px solid var(--upaep-trinity-panel-border);
        border-radius: 0;
        overflow-y: auto;
        translate: 100% 0;
        transition: translate var(--upaep-trinity-view-transition);

        & .upaep__trinity-view-aside-close { display: grid; }
        &.is-show { translate: 0 0; }
    }

    /*? El colapso en línea no aplica en móvil: se deshacen sus efectos para que el
       cajón sea un cajón normal. */
    .upaep__trinity-view-aside.__collapsible {
        & > * {
            width: 100%;
        }

        /*? …incluido el soltar del recorte. Aquí la columna es un CAJÓN y su
           `overflow-y: auto` es lo que le da scroll propio; el `visible` del
           estado asentado gana por especificidad aunque esté fuera de la consulta
           —las media queries no suman— así que se devuelve a mano. */
        &.is-expanded:not(.is-hidden) { overflow-y: auto; }

        &.is-hidden {
            flex-basis: auto;
            width: min(var(--upaep-trinity-aside-width), 92vw);
            padding-inline: var(--upaep-trinity-panel-padding);
            opacity: 1;
            pointer-events: auto;
        }
    }
}

/*? Muy angosto: se bajan los mínimos de las rejillas para que dos columnas
   quepan sin desbordar, y filtros y tarjetas grandes pasan a una sola. */
@media (width <=640px) {
    .upaep__trinity-cards {
        --upaep-trinity-cards-min: 150px;
        --upaep-trinity-cards-gap: var(--upaep-trinity-sp-3, .75rem);

        &.__sm { --upaep-trinity-cards-min: 130px; }
        &.__lg { --upaep-trinity-cards-min: 100%; }
    }

    .upaep__trinity-filters {
        --upaep-trinity-filters-min: 100%;
    }

    .upaep__trinity-view-actions {
        width: 100%;
    }
}

@media (prefers-reduced-motion: reduce) {

    .upaep__trinity-view-aside,
    .upaep__trinity-view-scrim,
    .upaep__trinity-panel.__collapsible > .upaep__trinity-panel-collapse,
    .upaep__trinity-panel-chevron {
        transition-duration: .01ms !important;
    }

    .upaep__trinity-skeleton {
        animation: none;
        background-image: none;
    }
}
