﻿/* ============================================================================
   layout.css — LAYOUT y COMPONENTES de la UI del BackEnd.

   Consume los tokens de tokens.css (cargar ese primero). Cubre:
     1. Shell: sidebar + main (topbar / content / footer)
     2. Sidebar: módulos y opciones, estado actual, rail colapsado con flyout, offcanvas móvil
     3. Topbar: título, breadcrumb, acciones, usuario
     4. Paneles: filtros / resultados / formulario (headers sticky, botonera sticky)
     5. Compatibilidad con el markup de las páginas (card-header-sm, card-body-sm, labels…)
     6. Grilla + paginador (PagerControl: clases apf-pager-*)
     7. Botones, modales, SWAL, toasts, utilidades

   Convenciones:
     - Prefijo `ui-` para todo lo propio. Sin !important (la especificidad la da `.ui-shell`).
     - Los ids que el JS existente necesita NO cambian: #filtersPanel, #btnFilter,
       #btnSearch, #btnShowFields, #btnAyuda, #toast-container, #helpModal.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   1. SHELL
   --------------------------------------------------------------------------- */
html, body.ui-shell { height: 100%; }

body.ui-shell {
    margin: 0;
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
    font-size: var(--bs-body-font-size);
    line-height: var(--bs-body-line-height);
    -webkit-font-smoothing: antialiased;
}

body.ui-shell > form { min-height: 100%; }

/* Skip-link (accesibilidad): primer elemento del body; invisible salvo con foco de teclado, y flotante
   para no empujar el shell. */
body.ui-shell > .visually-hidden-focusable:focus {
    position: absolute;
    top: .5rem;
    left: .5rem;
    z-index: 2000;
    padding: .4rem .8rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    box-shadow: var(--ui-surface-shadow);
    text-decoration: none;
}

.ui-app {
    display: flex;
    min-height: 100vh;
    min-height: 100dvh;
}

.ui-main {
    flex: 1 1 auto;
    min-width: 0;            /* deja que las grillas anchas scrolleen dentro, no la página */
    display: flex;
    flex-direction: column;
}

.ui-content {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: var(--ui-content-gap);
    padding: var(--ui-content-pad);
}

/* ---------------------------------------------------------------------------
   2. SIDEBAR

   Hay DOS menús sobre los mismos estilos de shell (marca, pie, colores):

     a) UNA COLUMNA (los 3 portales: MiClub, Gaceteros, WebTrackeos) — módulos con
        monograma que abren su lista con Bootstrap collapse. Es el menú histórico.
     b) RAIL + PANEL (el BackEnd, desde 2026-09-22) — el <aside> lleva
        data-ui-nav="rail": una columna de íconos siempre visible (uno por módulo) y,
        al lado, el panel del módulo activo con sus grupos colapsables. Contraído
        queda el rail solo y el panel sale como flyout a toda altura.

   Todo lo que es exclusivo de (b) va bajo .ui-sidebar-shell[data-ui-nav="rail"], así
   los portales conservan (a) sin tocar su markup.
   --------------------------------------------------------------------------- */
/* El <aside> es el "shell" (offcanvas-lg de Bootstrap: en desktop fuerza background
   transparent !important, por eso el fondo va en el .ui-sidebar interno, no en el aside). */
.ui-sidebar-shell {
    --bs-offcanvas-width: var(--ui-sidebar-w);
    --bs-offcanvas-bg:    var(--ui-sidebar-bg);
    --bs-offcanvas-color: var(--ui-sidebar-fg);
    flex: 0 0 auto;
    width: var(--ui-sidebar-w);
    transition: width .18s ease;
}
.ui-sidebar-shell[data-ui-nav="rail"] {
    --bs-offcanvas-width: var(--ui-sidebar-rail-total);
    width: var(--ui-sidebar-rail-total);
}
/* El flyout del rail contraído se posiciona contra el shell: el ancla es el position: sticky
   del desktop (más abajo). OJO: no poner position: relative acá — por especificidad le gana al
   sticky y el sidebar se va con el scroll de la página. */

.ui-sidebar {
    height: 100%;
    background-color: var(--ui-sidebar-bg);
    color: var(--ui-sidebar-fg);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, .25) transparent;
}
/* En rail + panel el scroll es del PANEL, no de la columna entera: así el rail y el
   título del módulo nunca se van de la vista. */
.ui-sidebar-shell[data-ui-nav="rail"] .ui-sidebar { overflow: hidden; }

@media (min-width: 992px) {
    .ui-sidebar-shell {
        position: sticky;
        top: 0;
        height: 100vh;
        height: 100dvh;
        z-index: 1030;   /* sobre la topbar (1020) y los headers sticky: el flyout del rail no queda debajo del contenido */
    }
}

/* Marca */
.ui-sidebar-brand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    height: var(--ui-topbar-h);
    padding: 0 .75rem 0 .9rem;
    border-bottom: 1px solid var(--ui-sidebar-divider);
    flex: 0 0 auto;
}

.ui-brand-link {
    display: flex;
    align-items: center;
    gap: .6rem;
    min-width: 0;
    color: #fff;
    text-decoration: none;
    font-weight: 600;
    font-size: .95rem;
    letter-spacing: .01em;
}
.ui-brand-link:hover { color: #fff; }

.ui-brand-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    flex: 0 0 30px;
    border-radius: var(--bs-border-radius);
    background-color: rgba(255, 255, 255, .12);
    font-size: .9rem;
}

.ui-brand-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Contexto de sesión bajo la marca (portales: credencial y tipo de socio, razón social del canillita,
   sucursal/comercio logueado). Se oculta en el rail colapsado. */
.ui-sidebar-context {
    padding: .35rem .9rem .1rem;
    font-size: .72rem;
    color: var(--ui-sidebar-fg-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
html[data-ui-sidebar="collapsed"] .ui-sidebar-context { display: none; }

/* Navegación */
.ui-sidebar-nav {
    flex: 1 1 auto;
    padding: .5rem 0;
}

.ui-nav,
.ui-nav-items {
    list-style: none;
    margin: 0;
    padding: 0;
}

.ui-nav-module { position: relative; }

.ui-nav-module-btn {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    padding: .5rem .75rem .5rem .9rem;
    background: transparent;
    border: 0;
    color: var(--ui-sidebar-fg-muted);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: left;
    cursor: pointer;
}
.ui-nav-module-btn:hover { color: var(--ui-sidebar-fg); background-color: var(--ui-sidebar-hover-bg); }
.ui-nav-module-btn:focus-visible { outline: 2px solid var(--ui-sidebar-active-bar); outline-offset: -2px; }

.ui-nav-mono {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex: 0 0 26px;
    border-radius: var(--bs-border-radius-sm);
    background-color: rgba(255, 255, 255, .10);
    color: var(--ui-sidebar-fg);
    font-size: .6rem;
    font-weight: 700;
    letter-spacing: .01em;
}

.ui-nav-module-name {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ui-nav-caret {
    font-size: .6rem;
    transition: transform .18s ease;
    opacity: .7;
}
.ui-nav-module-btn[aria-expanded="true"] .ui-nav-caret { transform: rotate(90deg); }

/* Módulo de la pantalla actual (clase que pone wucECMSNavigationBarMenuModule) */
.ui-nav-module.menu-module-current > .ui-nav-module-btn { color: #fff; }
.ui-nav-module.menu-module-current > .ui-nav-module-btn .ui-nav-mono {
    background-color: var(--ui-sidebar-active-bar);
    color: var(--ui-sidebar-bg);
}

.ui-nav-items { padding: .1rem 0 .4rem; }
/* Un ítem que oculta su link (separador) deja el <li> del template vacío */
.ui-nav-items > li:empty { display: none; }

.ui-nav-link {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .38rem .75rem .38rem calc(.9rem + 26px + .6rem);
    color: var(--ui-sidebar-fg);
    font-size: .84rem;
    text-decoration: none;
    border-left: 3px solid transparent;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ui-nav-link:hover,
.ui-nav-link:focus-visible {
    color: #fff;
    background-color: var(--ui-sidebar-hover-bg);
    outline: none;
}
.ui-nav-link.menu-item-current {
    color: #fff;
    font-weight: 600;
    background-color: var(--ui-sidebar-active-bg);
    border-left-color: var(--ui-sidebar-active-bar);
}
.ui-nav-link.disabled {
    opacity: .45;
    pointer-events: none;
}
/* Texto del item, en su propio span (layout.js lo envuelve): truncado con "…" y, al pasar el
   mouse por un item que no entra, circula como marquesina: layout.js agrega una segunda copia
   del texto y la pista se desplaza en loop exactamente una copia (+ separación) por ciclo, así
   el empalme es continuo. Al salir vuelve al truncado. Si el texto entra entero no pasa nada.
   Ídem para el nombre del módulo, el título de un grupo y el título del panel. */
.ui-nav-text {
    display: block;
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
.ui-nav-marquee-track { white-space: nowrap; }
.ui-nav-text.is-marquee,
.ui-nav-module-name.is-marquee,
.ui-nav-group-name.is-marquee,
.ui-nav-panel-title-text.is-marquee { text-overflow: clip; }
.is-marquee > .ui-nav-marquee-track {
    display: inline-block;
    animation: ui-nav-marquee var(--ui-marquee-dur, 6s) linear .25s infinite;
}
.is-marquee .ui-nav-marquee-copy { padding-right: 2.5rem; }   /* separación entre las dos copias */
@keyframes ui-nav-marquee {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }   /* la pista son 2 copias: -50% = una copia exacta */
}
@media (prefers-reduced-motion: reduce) {
    .is-marquee > .ui-nav-marquee-track { animation: none; }
}

/* Estrella "novedad" que inyecta el code-behind (ms-auto blink-soft) */
.ui-nav-link .blink-soft {
    margin-left: auto;
    color: #ffd166;
    font-size: .7rem;
}

.ui-nav-divider {
    height: 1px;
    margin: .3rem .75rem .3rem calc(.9rem + 26px + .6rem);
    background-color: var(--ui-sidebar-divider);
}

.ui-nav-flyout-title { display: none; }

/* Pie del sidebar: botón contraer/expandir */
.ui-sidebar-foot {
    flex: 0 0 auto;
    border-top: 1px solid var(--ui-sidebar-divider);
    padding: .35rem .5rem;
}

.ui-sidebar-collapse-btn {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    padding: .45rem .4rem .45rem .5rem;
    background: transparent;
    border: 0;
    border-radius: var(--bs-border-radius);
    color: var(--ui-sidebar-fg-muted);
    font-size: .78rem;
    cursor: pointer;
}
.ui-sidebar-collapse-btn:hover { color: #fff; background-color: var(--ui-sidebar-hover-bg); }
.ui-collapse-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    flex: 0 0 26px;
    transition: transform .18s ease;
}

/* ---- Rail colapsado del menú de UNA COLUMNA (portales): iconos + flyout al pasar el mouse ---- */
@media (min-width: 992px) {
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) { width: var(--ui-sidebar-w-collapsed); }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-brand-text,
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-module-name,
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-caret { display: none; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-sidebar-brand { padding: 0; justify-content: center; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-module-btn { padding: .5rem 0; justify-content: center; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-collapse-icon { transform: rotate(180deg); }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-sidebar-collapse-btn { justify-content: center; padding-left: 0; padding-right: 0; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-sidebar { overflow: visible; }   /* el flyout sale del rail */
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-sidebar-nav { overflow: visible; }

    /* Las opciones del módulo se vuelven un flyout a la derecha del rail */
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-items {
        display: none;
        position: absolute;
        left: 100%;
        top: 0;
        min-width: 230px;
        max-width: 320px;
        padding: .35rem 0;
        background-color: var(--ui-sidebar-bg);
        border: 1px solid var(--ui-sidebar-divider);
        border-left: 0;
        border-radius: 0 var(--bs-border-radius) var(--bs-border-radius) 0;
        box-shadow: 8px 8px 24px -10px rgba(0, 0, 0, .45);
        z-index: 1030;
    }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-module:hover > .ui-nav-items,
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-module:focus-within > .ui-nav-items { display: block; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-flyout-title {
        display: block;
        padding: .3rem .9rem .4rem;
        color: var(--ui-sidebar-fg-muted);
        font-size: .68rem;
        font-weight: 600;
        letter-spacing: .06em;
        text-transform: uppercase;
    }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-link { padding-left: .9rem; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell:not([data-ui-nav="rail"]) .ui-nav-divider { margin-left: .9rem; }
}

/* ===========================================================================
   2b. RAIL + PANEL (BackEnd) — .ui-sidebar-shell[data-ui-nav="rail"]
   =========================================================================== */
.ui-sidebar-shell[data-ui-nav="rail"] .ui-sidebar-nav {
    display: flex;
    min-height: 0;
    padding: 0;
    position: relative;
}

/* ---- El rail: un ícono por módulo, nunca scrollea ---- */
.ui-nav-rail {
    list-style: none;
    margin: 0;
    padding: .35rem 0;
    width: var(--ui-rail-w);
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    background-color: var(--ui-rail-shade);
    /* NO lleva overflow:hidden. El tooltip del icono arranca en x=58 y el rail mide 56:
       recortando aca no se veia nunca (y no era el z-index). El alto lo sigue conteniendo
       .ui-sidebar, que en modo rail si recorta. */
    overflow: visible;
}
.ui-nav-rail > li { display: flex; justify-content: center; }
.ui-nav-rail > li.ui-rail-spacer { flex: 1 1 auto; }

.ui-rail-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin: 1px 0;
    border: 0;
    border-radius: var(--bs-border-radius);
    background: transparent;
    color: var(--ui-sidebar-fg-muted);
    font-size: 1rem;
    cursor: pointer;
}
.ui-rail-btn .ui-nav-mono { width: auto; height: auto; background: transparent; font-size: .6rem; }
.ui-rail-btn:hover { color: #fff; background-color: var(--ui-sidebar-hover-bg); }
.ui-rail-btn:focus-visible { outline: 2px solid var(--ui-sidebar-active-bar); outline-offset: -2px; }
.ui-rail-btn.is-active { color: #fff; background-color: var(--ui-sidebar-active-bg); }
.ui-rail-btn.is-active::before {
    content: '';
    position: absolute;
    left: calc((var(--ui-rail-w) - 40px) / -2);
    top: 9px;
    bottom: 9px;
    width: 3px;
    background-color: var(--ui-sidebar-active-bar);
    border-radius: 0 2px 2px 0;
}

/* Tooltip del ícono (expandido; contraído lo reemplaza el flyout, que ya dice el nombre) */
.ui-rail-btn[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    left: calc(100% + 10px);
    top: 50%;
    transform: translateY(-50%);
    padding: .25rem .55rem;
    border-radius: var(--bs-border-radius-sm);
    background-color: #111827;
    color: #fff;
    font-size: .74rem;
    white-space: nowrap;
    box-shadow: 0 4px 12px rgba(0, 0, 0, .3);
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s ease;
    z-index: 1040;
}
.ui-rail-btn[data-tip]:hover::after { opacity: 1; transition-delay: var(--ui-rail-tip-delay, .35s); }
/* Recorriendo el rail: una vez que salio el primero, los vecinos no vuelven a esperar.
   Sin esto, pasar el mouse por los iconos para saber que es cada uno no muestra NADA:
   cada icono reinicia la demora. La clase la pone/saca initRail() en layout.js. */
.ui-nav-rail.is-tip-warm .ui-rail-btn[data-tip]:hover::after { transition-delay: 0s; }
html[data-ui-sidebar="collapsed"] .ui-rail-btn[data-tip]::after { content: none; }

/* Estrella de novedad sobre el ícono: el módulo tiene una pantalla nueva */
.ui-rail-new {
    position: absolute;
    top: 3px;
    right: 5px;
    margin: 0;
    font-size: .45rem;
    line-height: 1;
    color: #ffd166;
    filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .55));
}

/* ---- El panel: el módulo activo, con su propio scroll ---- */
.ui-nav-panel {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    border-left: 1px solid var(--ui-sidebar-divider);
}
.ui-nav-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 0 0 .5rem;
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, .25) transparent;
}

/* Un panel por módulo; se muestra el activo (y, buscando, todos los que tienen resultados) */
.ui-nav-module-panel { display: none; }
.ui-nav-module-panel.is-active { display: block; }
.ui-sidebar-nav.is-filtering .ui-nav-module-panel { display: block; }
.ui-sidebar-nav.is-filtering .ui-nav-module-panel.ui-nav-hidden { display: none; }

.ui-nav-panel-title {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    padding: .5rem .9rem .4rem;
    background-color: var(--ui-sidebar-bg);
    border-bottom: 1px solid var(--ui-sidebar-divider);
    color: #fff;
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
}
.ui-nav-panel-title > i { flex: 0 0 auto; color: var(--ui-sidebar-active-bar); font-size: .8rem; }
.ui-nav-panel-title .ui-nav-mono { flex: 0 0 auto; }
.ui-nav-panel-title-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* ---- Grupos colapsables (los dividers CON nombre) ---- */
.ui-nav-group { border-top: 1px solid var(--ui-sidebar-divider); }
.ui-nav-group:first-child,
.ui-nav-group.is-flat { border-top: 0; }

.ui-nav-group-btn {
    display: flex;
    align-items: center;
    gap: .45rem;
    width: 100%;
    padding: .45rem .75rem .45rem .85rem;
    background: transparent;
    border: 0;
    text-align: left;
    cursor: pointer;
    color: var(--ui-sidebar-fg-muted);
    font-size: .68rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.ui-nav-group-btn:hover { color: #fff; background-color: var(--ui-sidebar-hover-bg); }
.ui-nav-group-btn:focus-visible { outline: 2px solid var(--ui-sidebar-active-bar); outline-offset: -2px; }
.ui-nav-group-btn .ui-nav-caret { flex: 0 0 auto; font-size: .55rem; opacity: .75; }
.ui-nav-group-name {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ui-nav-group-n { flex: 0 0 auto; font-size: .62rem; font-weight: 500; letter-spacing: 0; opacity: .6; }
.ui-nav-group.is-open > .ui-nav-group-btn { color: #fff; }
.ui-nav-group.is-open > .ui-nav-group-btn .ui-nav-caret { transform: rotate(90deg); }

/* Punto: la pantalla actual está en este grupo (solo con el grupo cerrado). Es un elemento
   propio y no un ::after del título porque el título puede volverse marquesina. */
.ui-nav-group-dot {
    width: 5px;
    height: 5px;
    flex: 0 0 auto;
    border-radius: 50%;
    background-color: var(--ui-sidebar-active-bar);
    transition: opacity .18s ease, transform .18s ease;
}
/* Estrella: hay novedades adentro del grupo (se apaga al abrirlo: ahí se ven las de cada opción) */
.ui-nav-group-new {
    margin: 0;
    flex: 0 0 auto;
    color: #ffd166;
    font-size: .6rem;
    transition: opacity .18s ease, transform .18s ease;
}
.ui-nav-group.is-open > .ui-nav-group-btn .ui-nav-group-dot,
.ui-nav-group.is-open > .ui-nav-group-btn .ui-nav-group-new { opacity: 0; transform: scale(.6); }

/* Apertura / cierre: el alto se interpola con grid-template-rows (0fr ↔ 1fr, sin medir en JS)
   y los ítems entran escalonados (--i = índice, lo pone layout.js al preparar el panel). */
.ui-nav-items-wrap {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .22s cubic-bezier(.4, 0, .2, 1);
}
.ui-nav-group.is-open > .ui-nav-items-wrap,
.ui-nav-group.is-flat > .ui-nav-items-wrap { grid-template-rows: 1fr; }
.ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-items { padding: 0; min-height: 0; overflow: hidden; }
.ui-nav-group.is-open > .ui-nav-items-wrap > .ui-nav-items { padding-bottom: .3rem; }
.ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-items > li {
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity .16s ease, transform .16s ease;
}
.ui-nav-group.is-open > .ui-nav-items-wrap > .ui-nav-items > li,
.ui-nav-group.is-flat > .ui-nav-items-wrap > .ui-nav-items > li {
    opacity: 1;
    transform: none;
    transition-delay: calc(var(--i, 0) * 22ms);
}

.ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-link { padding-left: 1.4rem; }
.ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-group.is-flat .ui-nav-link { padding-left: .9rem; }
.ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-divider { margin-left: 1.4rem; }

/* Cambio de módulo: el panel entra con un desplazamiento mínimo */
@keyframes ui-nav-panel-in {
    from { opacity: 0; transform: translateX(-5px); }
    to   { opacity: 1; transform: none; }
}
.ui-nav-module-panel.is-swap { animation: ui-nav-panel-in .2s ease both; }

@media (prefers-reduced-motion: reduce) {
    .ui-nav-items-wrap,
    .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-items > li,
    .ui-nav-group-dot,
    .ui-nav-group-new { transition: none; }
    .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-items > li { opacity: 1; transform: none; }
    .ui-nav-module-panel.is-swap { animation: none; }
}

/* ---- Rail contraído: queda el rail solo y el panel sale como flyout a TODA ALTURA ---- */
@media (min-width: 992px) {
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] { width: var(--ui-rail-w); }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-brand-text,
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-module-name { display: none; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-sidebar-brand { padding: 0; justify-content: center; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-sidebar-collapse-btn { justify-content: center; padding-left: 0; padding-right: 0; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-collapse-icon { transform: rotate(180deg); }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-rail { background-color: transparent; }

    /* Contraído el panel sale como flyout (position:absolute, left:100%), así que tiene que poder
       SALIRSE de la columna: sin esto queda recortado contra los 56px del rail y el menú parece
       vacío. El overflow:hidden de más arriba es para el estado expandido (que scrollee el panel
       y no la columna entera). Misma regla que ya tenía el sidebar de una sola columna. */
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-sidebar,
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-sidebar-nav { overflow: visible; }

    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-panel {
        position: absolute;
        left: 100%;
        top: 0;
        bottom: 0;
        width: var(--ui-panel-w);
        background-color: var(--ui-sidebar-bg);
        border-left: 1px solid var(--ui-sidebar-divider);
        border-right: 1px solid var(--ui-sidebar-divider);
        border-radius: 0 var(--bs-border-radius-lg) var(--bs-border-radius-lg) 0;
        box-shadow: 10px 0 28px -12px rgba(0, 0, 0, .55);
        opacity: 0;
        transform: translateX(-6px);
        pointer-events: none;
        transition: opacity .14s ease, transform .14s ease;
        z-index: 1035;
    }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"].is-flyout-open .ui-nav-panel {
        opacity: 1;
        transform: none;
        pointer-events: auto;
    }
    /* El buscador vive en el panel: contraído se ve cuando el flyout está abierto */
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-search-wrap { display: block; }
}

/* ---- Móvil / tablet: Bootstrap offcanvas-lg hace el trabajo; solo el look ---- */
@media (max-width: 991.98px) {
    .ui-sidebar-shell { border-right: 1px solid var(--ui-sidebar-divider); }
    /* En el offcanvas entran las dos columnas: el rail no se contrae */
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] { width: var(--ui-sidebar-rail-total); }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-nav-panel { position: static; opacity: 1; transform: none; pointer-events: auto; }
    html[data-ui-sidebar="collapsed"] .ui-sidebar-shell[data-ui-nav="rail"] .ui-brand-text { display: block; }
}

/* ---------------------------------------------------------------------------
   3. TOPBAR
   --------------------------------------------------------------------------- */
.ui-topbar {
    position: sticky;
    top: 0;
    z-index: 1020;
    display: flex;
    align-items: center;
    gap: .75rem;
    height: var(--ui-topbar-h);
    padding: 0 var(--ui-content-pad);
    background-color: var(--ui-surface);
    border-bottom: 1px solid var(--ui-surface-border);
    box-shadow: var(--ui-surface-shadow);
}

.ui-topbar-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 1px solid var(--ui-surface-border);
    border-radius: var(--bs-border-radius);
    background: var(--ui-surface);
    color: var(--bs-body-color);
}

.ui-title {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    line-height: 1.2;
}

.ui-breadcrumb {
    display: flex;
    align-items: center;
    gap: .35rem;
    font-size: .72rem;
    color: var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: .05em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ui-breadcrumb i { font-size: .55rem; opacity: .7; }

.ui-title h1 {
    margin: 0;
    font-size: 1.02rem;
    font-weight: 600;
    letter-spacing: .01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* litContentDescription llega envuelto en <h6> desde el code-behind */
.ui-title h1 h6 { display: none; }

/* "Se actualiza cada N min": cartel de cache de tableros (bajo el título) y de widgets (pie de card).
   El texto sale de CacheBP.TTL_* vía MensajesBP.Dashboard: cambiar la constante cambia el cartel. */
.ui-cache-info {
    font-size: .7rem;
    color: var(--bs-secondary-color);
    white-space: nowrap;
}
.ui-cache-info i { font-size: .65rem; opacity: .7; }

/* Badge "se actualiza sola cada N s" (pantallas que se auto-refrescan, ej. Usuarios conectados).
   Gris cuando el operador pausa el refresh. El texto sale de MensajesBP, no del CSS. */
.ui-autorefresh-badge {
    font-size: .7rem;
    font-weight: 500;
    color: var(--bs-success-text-emphasis, #0a3622);
    background-color: var(--bs-success-bg-subtle, #d1e7dd);
    border: 1px solid var(--bs-success-border-subtle, #a3cfbb);
}
.ui-autorefresh-badge i { font-size: .65rem; opacity: .8; }
.ui-autorefresh-badge.is-paused {
    color: var(--bs-secondary-color);
    background-color: var(--bs-secondary-bg);
    border-color: var(--ui-surface-border);
}
.ui-autorefresh-badge.is-paused i { opacity: .5; }

/* Barra de progreso del intervalo: se llena en los N s que faltan para el proximo refresh.
   La duracion la pone el JS (window.setInterval y animation comparten el mismo N). */
.ui-autorefresh-badge { position: relative; overflow: hidden; }
.ui-autorefresh-progress {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background-color: rgba(0, 0, 0, .08);
}
.ui-autorefresh-progress-fill {
    display: block;
    height: 100%;
    width: 0;
    background-color: currentColor;
    opacity: .55;
    animation: ui-autorefresh-fill linear infinite;
}
.ui-autorefresh-badge.is-paused .ui-autorefresh-progress { display: none; }
@keyframes ui-autorefresh-fill { from { width: 0; } to { width: 100%; } }
@media (prefers-reduced-motion: reduce) {
    .ui-autorefresh-progress-fill { animation: none; width: 100%; opacity: .25; }
}

.ui-topbar-actions {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex: 0 0 auto;
}
/* Rol bajo el nombre en el menú de usuario (WebTrackeos: Sucursal / Comercio / Administrador) */
.ui-user-role { display: block; font-size: .68rem; line-height: 1.1; color: var(--bs-secondary-color); text-transform: uppercase; letter-spacing: .04em; }

/* Usuario */
.ui-user-btn {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    height: 36px;
    padding: 0 .5rem 0 .25rem;
    border: 1px solid transparent;
    border-radius: var(--bs-border-radius-pill);
    background: transparent;
    color: var(--bs-body-color);
    font-size: .8125rem;
    cursor: pointer;
}
.ui-user-btn:hover { background-color: var(--ui-surface-muted); border-color: var(--ui-surface-border); }

.ui-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background-color: var(--ui-brand);
    color: var(--ui-brand-contrast);
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .02em;
}

.ui-user-name { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ui-user-caret { font-size: .6rem; opacity: .6; }

.ui-user .dropdown-menu { min-width: 220px; font-size: .8125rem; }
.ui-user .dropdown-item { display: flex; align-items: center; gap: .5rem; }

/* Selector de densidad: 3 estados no entran en un boton que cicla (no se ven las opciones
   y volver un paso cuesta dos clicks), asi que va como grupo de botones dentro del menu. */
.ui-density-picker { padding: .15rem 1rem .4rem; }
.ui-density-picker .dropdown-header { display: flex; align-items: center; gap: .5rem; padding: .25rem 0; }
.ui-density-picker .btn-group { width: 100%; }
.ui-density-picker .btn { font-size: .72rem; padding: .2rem .35rem; }

/* ---------------------------------------------------------------------------
   4. PANELES (filtros / resultados / formulario)
   --------------------------------------------------------------------------- */
.ui-panel {
    background-color: var(--ui-surface);
    border: 1px solid var(--ui-surface-border);
    border-radius: var(--bs-border-radius-lg);
    box-shadow: var(--ui-surface-shadow);
}

.ui-panel-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
    padding: var(--ui-panel-head-py) var(--ui-panel-px);
    border-bottom: 1px solid var(--ui-surface-border);
    border-radius: var(--bs-border-radius-lg) var(--bs-border-radius-lg) 0 0;
    background-color: var(--ui-surface);
}

.ui-panel-title {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    font-size: var(--ui-section-title-fs);
    font-weight: 600;
    letter-spacing: var(--ui-section-title-ls);
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}

.ui-panel-tools {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .35rem;
    margin-left: auto;
}

.ui-panel-body { padding: var(--ui-panel-body-py) var(--ui-panel-px); }

.ui-panel-foot {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
    padding: .6rem var(--ui-panel-px);
    border-top: 1px solid var(--ui-surface-border);
    border-radius: 0 0 var(--bs-border-radius-lg) var(--bs-border-radius-lg);
    background-color: var(--ui-surface);
}

/* Sticky: el header de resultados queda pegado bajo la topbar; la botonera del form, abajo */
.ui-panel-head.ui-sticky {
    position: sticky;
    top: var(--ui-topbar-h);
    z-index: 5;
}
.ui-panel-head.ui-sticky.is-stuck { box-shadow: var(--ui-sticky-shadow); }

.ui-panel-foot.ui-sticky {
    position: sticky;
    bottom: 0;
    z-index: 5;
}
.ui-panel-foot.ui-sticky.is-stuck { box-shadow: 0 -4px 12px -6px rgba(16, 24, 40, .18); }

/* Filtros: menos aire (el panel de filtros es lo primero que se ve, y suele ser 1-2 filas) */
.ui-filters .ui-panel-body { padding-top: .35rem; padding-bottom: .05rem; }
/* Secciones (h6.card-header + .card-body) dentro del panel de filtros: sin padding horizontal propio (lo pone
   el body del panel) y la primera sin línea arriba (el head del panel ya la separa) */
.ui-filters .card-header, .ui-filters .card-body, .ui-filters .card-body-sm { padding-left: 0; padding-right: 0; }
.ui-filters .ui-panel-body > .card-header:first-child,
.ui-filters [class*="col-"] > .card-header:first-child { border-top: 0; padding-top: .15rem; }
.ui-filters .card { border: 0; background: transparent; box-shadow: none; }
.ui-filters .mb-2 { margin-bottom: .4rem; }
.ui-filters .form-text { margin-top: .05rem; }

/* Badge contador de registros */
.ui-count {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .1rem .5rem;
    border-radius: var(--bs-border-radius-pill);
    background-color: var(--ui-brand-subtle);
    color: var(--ui-brand-text);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: 0;
    text-transform: none;
}

/* El panel de filtros abre por default en la UI (ver FiltersPanelPrefs: la preferencia del
   usuario sigue mandando); el botón "Filtros" rota el ícono según estado */
#btnFilter[aria-expanded="true"] .ui-filter-caret { transform: rotate(180deg); }
.ui-filter-caret { font-size: .6rem; transition: transform .18s ease; margin-left: .25rem; }

/* ---------------------------------------------------------------------------
   5. COMPATIBILIDAD CON EL MARKUP DE LAS PÁGINAS
   Las páginas siguen escribiendo <h6 class="card-header card-header-sm">SECCIÓN</h6> +
   <div class="card-body card-body-sm"> dentro de cphFields (y wucECMSAuditoria hace lo
   mismo). Fuera de un .card de Bootstrap esas clases no tienen estilo, así que acá se
   les da el look de "sección" del formulario.
   --------------------------------------------------------------------------- */
.ui-panel .card-header {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    padding: .5rem var(--ui-panel-px) .15rem;
    background: transparent;
    border: 0;
    border-top: 1px solid var(--ui-surface-border);
    font-size: var(--ui-section-title-fs);
    font-weight: 600;
    letter-spacing: var(--ui-section-title-ls);
    text-transform: uppercase;
    color: var(--bs-secondary-color);
}
/* La primera sección del form (sin header propio) no lleva línea arriba */
.ui-panel > .ui-form-sections > .card-body:first-child { padding-top: var(--ui-panel-body-py); }
.ui-panel .card-header .badge { font-size: .66rem; }

.ui-panel .card-body,
.ui-panel .card-body-sm {
    padding: .5rem var(--ui-panel-px) .35rem;
}
.ui-panel .card-footer,
.ui-panel .card-footer-sm { padding: .6rem var(--ui-panel-px); }
/* Cards apiladas DENTRO de un panel (historial de incidencias/pedidos, listas de un tab): un solo aire */
.ui-panel .card + .card { margin-top: var(--ui-content-gap); }

/* Formularios: labels compactos, controles chicos, ayuda discreta */
.ui-shell .form-label {
    margin-bottom: .15rem;
    font-size: var(--ui-label-font-size);
    font-weight: 500;
    color: var(--bs-secondary-color);
}
.ui-shell .form-label .text-danger { font-weight: 700; }

.ui-shell .form-control,
.ui-shell .form-select,
.ui-shell .input-group-text {
    font-size: var(--ui-control-font-size);
    background-color: var(--ui-surface);
    border-color: var(--ui-surface-border);
    color: var(--bs-body-color);
}
.ui-shell .form-control::placeholder { color: var(--bs-tertiary-color); opacity: 1; }
.ui-shell .form-control-sm,
.ui-shell .form-select-sm { min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; }
.ui-shell .form-control:disabled,
.ui-shell .form-control[readonly],
.ui-shell .form-control.readonly,
.ui-shell .form-select:disabled { background-color: var(--ui-surface-muted); color: var(--bs-secondary-color); }
.ui-shell .form-control.is-invalid,
.ui-shell .form-select.is-invalid { background-color: rgba(220, 53, 69, .06); }
.ui-shell .input-group-text { background-color: var(--ui-surface-muted); }
.ui-shell .input-group .rounded-0 { border-radius: var(--bs-border-radius-sm) 0 0 var(--bs-border-radius-sm); }

.ui-shell small.form-text,
.ui-shell .form-text { font-size: .72rem; color: var(--bs-tertiary-color); }

.ui-shell .form-check-input { margin-top: .15em; border-color: var(--ui-brand-border); }
/* Checkbox estándar del BackOffice (ui-patterns §5.4): el .form-check.form-switch ocupa la misma altura que
   un .form-control-sm y centra el switch verticalmente, para que quede alineado con los inputs de las
   columnas vecinas (sin esto el switch queda pegado al label). Flex en vez del float de Bootstrap: el
   padding-left/margin-left negativo se reemplaza por gap, así el label a la derecha (variante inline)
   queda a la misma distancia. */
.ui-shell .form-check.form-switch {
    display: flex;
    align-items: center;
    gap: .5em;
    min-height: calc(1.5 * var(--ui-control-font-size) + .5rem + 2px);
    padding-left: 0;
    margin-bottom: 0;
}
.ui-shell .form-check.form-switch .form-check-input { float: none; flex: 0 0 auto; margin: 0; }
.ui-shell .form-check.form-switch .form-check-label { font-size: var(--ui-control-font-size); }
.ui-shell .mb-2 { margin-bottom: .6rem; }

/* Textareas del form: no dejan redimensionar horizontalmente (rompe la grilla de columnas) */
.ui-shell textarea.form-control { resize: vertical; }

/* ---------------------------------------------------------------------------
   6. GRILLA + PAGINADOR
   --------------------------------------------------------------------------- */
.ui-shell .table-responsive {
    border: 1px solid var(--ui-grid-border);
    border-radius: var(--bs-border-radius);
    scrollbar-width: thin;
    scrollbar-color: rgba(127, 127, 127, .35) transparent;
}
.ui-shell .table-responsive::-webkit-scrollbar { height: 6px; }
.ui-shell .table-responsive::-webkit-scrollbar-thumb { background: rgba(127, 127, 127, .35); border-radius: 3px; }

.ui-shell .table {
    --bs-table-bg:            transparent;
    --bs-table-color:         var(--bs-body-color);
    --bs-table-border-color:  var(--ui-grid-border);
    --bs-table-striped-bg:    var(--ui-grid-striped-bg);
    --bs-table-striped-color: var(--bs-body-color);
    --bs-table-hover-bg:      var(--ui-grid-hover-bg);
    --bs-table-hover-color:   var(--bs-body-color);
    margin-bottom: 0;
    font-size: var(--ui-table-font-size);
}
.ui-shell .table > :not(caption) > * > * {
    padding: var(--ui-table-cell-py) var(--ui-table-cell-px);
    vertical-align: middle;
    border-bottom-width: 1px;
}
.ui-shell .table-bordered > :not(caption) > * > * { border-width: 0 1px 1px 0; }
.ui-shell .table-bordered > :not(caption) > * > *:last-child { border-right-width: 0; }
.ui-shell .table > tbody > tr:last-child > * { border-bottom-width: 0; }

/* Header: ABMHelper.SetStyleToHeaderRow pone class="table-dark" en la fila; el markup
   además pone color:White inline en cada <th>. Se le da el color de marca. */
.ui-shell .table .table-dark,
.ui-shell .table thead {
    --bs-table-bg:           var(--ui-grid-head-bg);
    --bs-table-color:        var(--ui-grid-head-fg);
    --bs-table-border-color: var(--ui-grid-head-border);
    --bs-table-striped-bg:   var(--ui-grid-head-bg);
    --bs-table-hover-bg:     var(--ui-grid-head-bg);
}
/* Pie de totales (tfoot): fondo de superficie apagada, respeta el tema (table-light era fijo claro). */
.ui-shell .table > .ui-table-foot { --bs-table-bg: var(--ui-surface-muted); }

.ui-shell .table th {
    font-weight: 600;
    /* Del token, no literal: si no, las celdas crecen con la densidad y los encabezados no. */
    font-size: var(--ui-table-head-fs);
    letter-spacing: .03em;
    text-transform: uppercase;
    white-space: nowrap;
}
.ui-shell .table th a { color: inherit; text-decoration: none; white-space: nowrap; }
.ui-shell .table th a:hover { text-decoration: underline; }
/* Indicador de orden (port de ParkQR, CSS puro): todo header ORDENABLE (el GridView lo renderiza como <a>)
   lleva SIEMPRE un sufijo — la doble flecha neutra tenue (fa-sort, ::after) cuando no es la columna
   activa, y la flecha ↑/↓ (el <i> que inyecta SetArrowToHeaderCell) cuando lo es — con el MISMO ancho
   fijo, así el header no cambia de ancho al ordenar. La columna activa se resalta con un fondo "foco".
   Los headers no ordenables son texto plano (sin <a>) y no llevan nada. */
.ui-shell .table th a::after,
.ui-shell .table th a > i {
    display: inline-block;
    width: 1.25em;
    margin-left: .3em;
    text-align: center;
    font-size: .95em;
    vertical-align: -.05em;
}
.ui-shell .table th a:not(:has(i))::after {
    content: "\f0dc";                 /* fa-sort (ALWAYS_INCLUDE en Generate-FontAwesomeSubset.py) */
    font-family: "Font Awesome 7 Free";
    font-weight: 900;
    opacity: .45;
}
.ui-shell .table th a:hover::after { opacity: .9; }
.ui-shell .table th a:has(i) {
    padding: .1rem .4rem;
    margin: -.1rem -.4rem;
    border-radius: var(--bs-border-radius-sm);
    background-color: rgba(255, 255, 255, .18);
}
.ui-shell .table th a:has(i):hover { text-decoration: none; background-color: rgba(255, 255, 255, .28); }

/* Botón de fila (Editar/Ver) */
.ui-shell .table .btn-sm { padding: .15rem .5rem; font-size: .74rem; }

/* Recetario de columnas (ver ui-patterns.md §9.5): alineación, ancho y nowrap salen del markup
   (FormatBoundField / HeaderStyle) con utilidades Bootstrap. Acá queda sólo lo que Bootstrap NO
   expresa. Las clases semánticas ui-col-date / ui-col-num / ui-col-money se borraron el 2026-09-22:
   tenían 0 usos y duplicaban text-nowrap / text-end / pe-3. */
.ui-shell .table .ui-col-actions { text-align: center; white-space: nowrap; }
.ui-shell .table .ui-col-actions .btn { min-width: 0; }
.ui-shell .table td.text-nowrap { white-space: nowrap; }
/* Dígitos de ancho fijo: una celda alineada a la derecha en una grilla es, por definición, un
   número. Sin esto las cifras no alinean columna a columna (Bootstrap no tiene utilidad para
   font-variant-numeric). Evita tener que etiquetar cada columna de monto. */
.ui-shell .table .text-end { font-variant-numeric: tabular-nums; }

/* Link "abrir en otra pestania" de apf:LinkBoundField. El icono queda tenue hasta el hover para
   que no compita con el texto de la celda (hay uno por fila). */
.ui-link-external { color: inherit; transition: color .15s; }
.ui-link-external .fa-arrow-up-right-from-square {
    font-size: .7rem;
    opacity: .4;
    margin-left: 3px;
    vertical-align: middle;
    transition: opacity .15s;
}
.ui-link-external:hover { color: var(--bs-primary); }
.ui-link-external:hover .fa-arrow-up-right-from-square { opacity: 1; }

/* Estado vacío (wucGridEmptyResults): ícono + título + texto */
.ui-shell .table .alert { margin: 0; border: 0; border-radius: 0; padding: 1.4rem 1rem; background-color: var(--ui-surface-muted); }
.ui-shell .table .alert::before {
    content: "\f002";
    display: block;
    margin-bottom: .4rem;
    font-family: "Font Awesome 7 Free";
    font-weight: 900;
    font-size: 1.4rem;
    color: var(--bs-tertiary-color);
}
.ui-shell .table .alert .fw-bold { font-size: .9rem; }

/* Estado inicial de una consulta (todavía no se buscó: la grilla no se bindeó) */
.ui-results .table-responsive:not(:has(table)) {
    min-height: 120px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--ui-surface-muted);
    color: var(--bs-tertiary-color);
    font-size: .85rem;
}
.ui-results .table-responsive:not(:has(table))::before {
    content: "Definí los filtros y presioná Buscar (o Enter) para ver resultados.";
}
.ui-results .table-responsive:not(:has(table)) + .apf-pager { display: none; }

/* Grilla + paginador se leen como una sola pieza */
.ui-shell .table-responsive:has(+ .apf-pager) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 0; }

.ui-shell .apf-pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    min-height: 42px;
    padding: .35rem .5rem;
    margin: 0;
    background-color: var(--ui-surface-muted);
    border: 1px solid var(--ui-grid-border);
    border-top: 0;
    border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius);
    box-sizing: border-box;
}
.ui-shell .apf-pager-left,
.ui-shell .apf-pager-right { display: flex; align-items: center; gap: .5rem; }
.ui-shell .apf-pager-list { display: inline-flex; gap: 2px; list-style: none; margin: 0; padding: 0; }
.ui-shell .apf-pager-item > .apf-pager-link {
    display: block;
    min-width: 30px;
    padding: .2rem .45rem;
    text-align: center;
    font-size: .78rem;
    line-height: 1.4;
    color: var(--bs-body-color);
    background-color: var(--ui-surface);
    border: 1px solid var(--ui-surface-border);
    border-radius: var(--bs-border-radius-sm);
    text-decoration: none;
}
.ui-shell .apf-pager-item.active > .apf-pager-link {
    color: var(--ui-brand-contrast);
    background-color: var(--ui-brand);
    border-color: var(--ui-brand);
    cursor: default;
}
.ui-shell .apf-pager-item.disabled > .apf-pager-link { opacity: .45; pointer-events: none; }
.ui-shell .apf-pager-item:not(.disabled):not(.active) > .apf-pager-link:hover,
.ui-shell .apf-pager-item:not(.disabled):not(.active) > .apf-pager-link:focus-visible {
    color: var(--ui-brand-text);
    background-color: var(--ui-brand-subtle);
    border-color: var(--ui-brand-border);
}
.ui-shell .apf-pager-indicator,
.ui-shell .apf-pager-show-label { font-size: .76rem; color: var(--bs-secondary-color); white-space: nowrap; }
.ui-shell .apf-pager-pagesize { width: auto; font-size: .76rem; padding: .2rem 2rem .2rem .6rem; background-position: right .6rem center; }
.ui-shell .apf-pager-right { gap: .6rem; }

/* ---------------------------------------------------------------------------
   7. BOTONES, MODALES, SWAL, TOASTS, UTILIDADES
   --------------------------------------------------------------------------- */
/* Reemplaza el style="min-width:100px" repetido en cada botón de la botonera */
.ui-btn-action { min-width: 100px; }

.ui-shell .btn { --bs-btn-font-weight: 500; }
.ui-shell .btn-sm { --bs-btn-padding-y: .3rem; --bs-btn-padding-x: .7rem; --bs-btn-font-size: .78rem; }
.ui-shell .btn .fa-solid,
.ui-shell .btn .fa-regular { font-size: .9em; }

/* Botonera del formulario: Confirmar/Actualizar a la derecha, destructivos a la izquierda */
.ui-botonera { display: flex; flex-wrap: wrap; gap: .5rem; width: 100%; }
.ui-botonera .btn-success { margin-left: auto; }
.ui-botonera .btn-success + .btn-success { margin-left: 0; }

/* Modales */
.ui-shell .modal-content { background-color: var(--ui-surface); border-color: var(--ui-surface-border); }
.ui-shell .modal-header.bg-custom { border-bottom: 0; }
.ui-shell .modal-footer.bg-light { background-color: var(--ui-surface-muted); border-top-color: var(--ui-surface-border); }

/* SweetAlert2: mismos ganchos que swal.custom.css, con radios del tema */
.swal2-popup { background: var(--ui-surface); color: var(--bs-body-color); border-radius: var(--bs-border-radius-lg); font-size: .95rem; }
.swal2-modal pre { color: var(--ui-brand-text); font-size: 1.1rem; font-weight: 700; border: 0; text-transform: uppercase; background: transparent; }
.swal2-btn-uniform,
.swal2-custom-confirm,
.swal2-custom-cancel { min-width: 100px; height: 40px; font-size: .9rem; font-weight: 600; border-radius: var(--bs-border-radius); }
.swal-error-popup    { border: 2px solid #dc3545; box-shadow: 0 0 15px rgba(220, 53, 69, .35); }
.swal-success-popup  { border: 2px solid #28a745; box-shadow: 0 0 15px rgba(40, 167, 69, .35); }
.swal-warning-popup  { border: 2px solid #ffc107; box-shadow: 0 0 15px rgba(255, 193, 7, .35); }
.swal-info-popup     { border: 2px solid #17a2b8; box-shadow: 0 0 15px rgba(23, 162, 184, .35); }
.swal-question-popup { border: 2px solid #6610f2; box-shadow: 0 0 15px rgba(102, 16, 242, .35); }

/* Toasts: el contenedor lo posiciona toast-helper.css (fixed, centrado arriba) */
#toast-container .alert { border-radius: var(--bs-border-radius); font-size: .85rem; }

/* Modal de ayuda: el body tiene min-height inline; el texto hereda el tema */
#helpModalContent { color: var(--bs-body-color); }

/* Footer */
.ui-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin-top: auto;
    padding: .4rem var(--ui-content-pad);
    border-top: 1px solid var(--ui-surface-border);
    background-color: var(--ui-surface);
    color: var(--bs-tertiary-color);
    font-size: .7rem;
}
.ui-footer-left,
.ui-footer-right { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Estrella "novedad" del menú (keyframe vivía en estilos-custom-*.css) */
@keyframes blink-soft {
    0%, 100% { opacity: 1; }
    50%      { opacity: .2; }
}
.blink-soft { animation: blink-soft 2.5s ease-in-out infinite; }

/* Botón de tema: muestra el ícono del modo al que se va a cambiar */
[data-ui-theme-toggle] .ui-icon-dark  { display: none; }
[data-ui-theme-toggle] .ui-icon-light { display: inline-block; }
html[data-bs-theme="dark"] [data-ui-theme-toggle] .ui-icon-dark  { display: inline-block; }
html[data-bs-theme="dark"] [data-ui-theme-toggle] .ui-icon-light { display: none; }

/* ---------------------------------------------------------------------------
   8. TABS — UNA sola definición para toda la UI (formularios ABM, tableros, utilidades).
   Markup = convención de ParkQR. En un ABM va dentro de .ui-tabs-container (tabs.js
   persiste el tab activo en hfActiveTab y UITabs navega a los errores); en un tablero alcanza
   con <ul class="nav nav-tabs"> + <div class="tab-content">: el look es el mismo.
   Clases de estado (las pone tabs.js / UITabs): .ui-tab-error, .disabled, .d-none (tab vacío).
   Sin colores fijos: ui-tabs.css de PQ y los estilos por página (.dash-tabs, .nav-tabs-integraciones) NO se usan.
   --------------------------------------------------------------------------- */
.ui-shell .nav-tabs {
    --bs-nav-tabs-border-width: 0;
    --bs-nav-link-padding-y: .55rem;
    --bs-nav-link-padding-x: .8rem;   /* 9 tabs (Beneficios) tienen que entrar a 1366px sin scroll */
    --bs-nav-link-font-size: .8rem;
    --bs-nav-link-font-weight: 500;
    --bs-nav-link-color: var(--bs-secondary-color);
    --bs-nav-link-hover-color: var(--ui-brand-text);
    --bs-nav-tabs-link-active-color: var(--ui-brand-text);
    --bs-nav-tabs-link-active-bg: transparent;
    --bs-nav-tabs-link-active-border-color: transparent;
    gap: .15rem;
    margin: 0;
    padding: 0 var(--ui-panel-px);
    border: 1px solid var(--ui-surface-border);
    border-radius: var(--bs-border-radius-lg) var(--bs-border-radius-lg) 0 0;
    background-color: var(--ui-surface-muted);
    overflow-x: auto;
    overflow-y: hidden;   /* overflow-x:auto arrastra overflow-y a auto: sin esto aparecía un mini scroll vertical */
    flex-wrap: nowrap;
    scrollbar-width: thin;
}
.ui-shell .nav-tabs .nav-item { flex: 0 0 auto; }
.ui-shell .nav-tabs .nav-link {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    border: 0;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    margin-bottom: 0;
    white-space: nowrap;
    background: transparent;
}
.ui-shell .nav-tabs .nav-link i { font-size: .85em; opacity: .8; }
.ui-shell .nav-tabs .nav-link:hover { border-bottom-color: var(--ui-brand-border); }
.ui-shell .nav-tabs .nav-link.active { font-weight: 600; border-bottom-color: var(--ui-brand); }
.ui-shell .nav-tabs .nav-link.active i { opacity: 1; }
.ui-shell .nav-tabs .nav-link.disabled,
.ui-shell .nav-tabs .nav-link:disabled { color: var(--bs-tertiary-color); opacity: .6; cursor: not-allowed; pointer-events: none; }
/* Tab con campos inválidos: punto rojo + texto en rojo (lo marca UITabs / uiTabs.setTabErrors) */
.ui-shell .nav-tabs .nav-link.ui-tab-error { color: var(--bs-danger); }
.ui-shell .nav-tabs .nav-link.ui-tab-error::after {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background-color: var(--bs-danger);
    box-shadow: 0 0 0 2px var(--ui-surface);
}
.ui-shell .nav-tabs .nav-link.active.ui-tab-error { border-bottom-color: var(--bs-danger); }

/* Tab content: en un tablero, el contenido de los tabs sigue al nav como un panel */
.ui-shell .tab-content { padding-top: .25rem; }
/* `~` y no `+`: entre el <ul> y el .tab-content va el HiddenField hfActiveTab (tabs.js) */
.ui-content-free .nav-tabs ~ .tab-content {
    padding: var(--ui-panel-body-py) var(--ui-panel-px);
    margin-top: calc(-1 * var(--ui-content-gap));   /* pegado al nav (el flex gap lo separaría) */
    border: 1px solid var(--ui-surface-border);
    border-top: 0;
    border-radius: 0 0 var(--bs-border-radius-lg) var(--bs-border-radius-lg);
    background-color: var(--ui-surface);
}

/* Dentro de un panel de ABM el nav no lleva bordes propios: los da el panel */
.ui-panel .ui-tabs-container { padding: 0; }
.ui-panel .ui-tabs-container > .nav-tabs { border-width: 0 0 1px; border-radius: 0; }
.ui-panel .ui-tabs-container > .tab-content > .tab-pane { padding-top: .25rem; }
.ui-panel .tab-pane > .card-header:first-child { border-top: 0; }

/* Contexto de la entidad (uiTabs.setContext), a la derecha de los tabs */
/* Contexto "#Id · nombre": PQ lo pone primero, en la UI va al final. Es ELÁSTICO: ocupa el espacio que
   sobra (hasta 40%) y se recorta con "…" — nunca empuja a los tabs ni fuerza el scroll horizontal
   (la barra es nowrap + overflow-x:auto; un nombre largo hacía aparecer el scroll al tipear). */
.ui-shell .nav-tabs .ui-tab-context-item { order: 99; margin-left: auto; align-self: center; padding: 0 .25rem; flex: 1 1 auto; min-width: 0; max-width: 40%; justify-content: flex-end; }
.ui-shell .ui-tab-context { font-size: .78rem; color: var(--bs-secondary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }

/* Ocultar botones de acción salvo "volver" en tabs operativos (data-hide-buttons, contrato PQ) */
.ui-botonera.ui-hide-action-buttons > *:not(.ui-btn-back) { display: none; }

@media (prefers-reduced-motion: reduce) {
    .ui-shell .tab-pane.fade { transition: none; }
}

/* ---------------------------------------------------------------------------
   9. GRILLA CON SCROLL INTERNO + HEADER STICKY — <html data-ui-grid="scroll"> (default;
   toggle en el menú de usuario → "page" = la grilla crece y scrollea la página).
   El GridView de WebForms no emite <thead>: el header es la <tr class="table-dark"> que pone
   ABMHelper.SetStyleToHeaderRow, por eso el sticky va sobre sus <th>.
   --------------------------------------------------------------------------- */
html[data-ui-grid="scroll"] .ui-results .table-responsive:has(table) {
    max-height: var(--ui-grid-max-h);
    overflow: auto;
}
html[data-ui-grid="scroll"] .ui-shell .table .table-dark > th,
html[data-ui-grid="scroll"] .ui-shell .table thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    box-shadow: inset 0 -1px 0 var(--ui-grid-head-border);
}
/* En modo página el header de la grilla NO es sticky: .table-responsive es un contenedor de
   scroll (overflow-x: auto para las grillas anchas) y un th sticky se pegaría ADENTRO de la
   tabla, desplazado (bug 2026-09-18). Lo que queda pegado en ese modo es el header del panel
   de Resultados (.ui-panel-head.ui-sticky). */

/* ---------------------------------------------------------------------------
   10. REFRESH PARCIAL DE GRILLA — overlay "Actualizando…" (grid-partial-refresh.js)
   --------------------------------------------------------------------------- */
.ui-grid-overlay-host { position: relative; }
.ui-grid-overlay {
    position: absolute;
    inset: 0;
    z-index: 20;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    background-color: rgba(var(--bs-body-bg-rgb), .55);
    backdrop-filter: blur(1px);
    cursor: progress;
}
/* El cartel es STICKY: en modo "grilla a lo largo de la pagina" el contenedor mide lo que la grilla
   (miles de px), asi que anclado al borde superior quedaba FUERA de la pantalla al paginar desde abajo
   (bug 2026-09-22). Con sticky se mantiene a la vista, debajo de la topbar y del header del panel, y
   queda acotado por el propio overlay: al pasar el final de la grilla se va con ella. En modo scroll
   interno el contenedor entra en el viewport y se ve igual que antes. */
.ui-grid-overlay-content {
    display: flex;
    align-items: center;
    gap: .5rem;
    position: sticky;
    top: calc(var(--ui-topbar-h) + 3.25rem);
    margin-top: 90px;
    padding: .5rem 1rem;
    border-radius: var(--bs-border-radius);
    background-color: var(--ui-surface);
    border: 1px solid var(--ui-surface-border);
    box-shadow: var(--ui-sticky-shadow);
    font-size: .85rem;
    color: var(--bs-body-color);
}
.ui-grid-overlay-sm { align-items: center; }
.ui-grid-overlay-sm .ui-grid-overlay-content { margin-top: 0; }

/* ---------------------------------------------------------------------------
   11. BUSCADOR DEL MENÚ (Ctrl+K) — filtra los módulos/opciones del sidebar en vivo
   --------------------------------------------------------------------------- */
.ui-nav-search-wrap { padding: .5rem .6rem .25rem; position: relative; }
.ui-nav-search {
    width: 100%;
    padding: .38rem .6rem .38rem 1.9rem;
    border: 1px solid var(--ui-sidebar-divider);
    border-radius: var(--bs-border-radius);
    background-color: rgba(255, 255, 255, .07);
    color: #fff;
    font-size: .8rem;
    outline: none;
}
.ui-nav-search::placeholder { color: var(--ui-sidebar-fg-muted); }
.ui-nav-search:focus { border-color: var(--ui-sidebar-active-bar); background-color: rgba(255, 255, 255, .1); }
.ui-nav-search-wrap > i { position: absolute; left: 1.2rem; top: 50%; transform: translateY(-40%); font-size: .75rem; color: var(--ui-sidebar-fg-muted); pointer-events: none; }
.ui-nav-search-kbd { position: absolute; right: 1.1rem; top: 50%; transform: translateY(-40%); font-size: .6rem; color: var(--ui-sidebar-fg-muted); border: 1px solid var(--ui-sidebar-divider); border-radius: 3px; padding: 0 .3rem; pointer-events: none; }
.ui-nav-search-wrap.is-active .ui-nav-search-kbd { display: none; }
.ui-nav-module.ui-nav-hidden, .ui-nav-items li.ui-nav-hidden, .ui-nav-group.ui-nav-hidden { display: none; }
.ui-nav-link.ui-nav-match { background-color: var(--ui-sidebar-hover-bg); }
.ui-nav-empty { padding: .75rem .9rem; color: var(--ui-sidebar-fg-muted); font-size: .8rem; display: none; }
.ui-sidebar-nav.is-filtering .ui-nav-empty.show { display: block; }
html[data-ui-sidebar="collapsed"] .ui-nav-search-wrap { display: none; }

/* ---------------------------------------------------------------------------
   12. BANNER SIN CONEXIÓN (offline-banner.js) y CONTEXTO DE TABS
   --------------------------------------------------------------------------- */
.ui-offline-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2000;
    padding: .45rem 1rem;
    text-align: center;
    font-size: .85rem;
    font-weight: 500;
    background-color: var(--bs-warning-bg-subtle);
    color: var(--bs-warning-text-emphasis);
    border-bottom: 2px solid var(--bs-warning);
}

/* ---------------------------------------------------------------------------
   13. CONTENIDO LIBRE (LayoutLogued / LayoutLoguedDashBoard): las páginas traen sus
   propias cards de Bootstrap; se les da superficie y borde del tema (dark incluido).
   --------------------------------------------------------------------------- */
.ui-content-free .card {
    --bs-card-bg: var(--ui-surface);
    --bs-card-border-color: var(--ui-surface-border);
    --bs-card-cap-bg: var(--ui-surface-muted);
    --bs-card-cap-color: var(--bs-body-color);
}
.ui-content-free .card {
    box-shadow: var(--ui-surface-shadow);
    border-radius: var(--bs-border-radius-lg);
}
/* .card-header-sm / .card-body-sm / .card-footer-sm: mismas medidas que los paneles de ABM/RPT
   (estilos.css lo hacía con !important; acá con tokens) */
.ui-content-free .card > .card-header,
.ui-content-free .card > .card-header-sm {
    padding: var(--ui-panel-head-py) var(--ui-panel-px);
    font-size: var(--ui-section-title-fs);
    font-weight: 600;
    letter-spacing: var(--ui-section-title-ls);
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    background-color: var(--ui-surface);
    border-bottom: 1px solid var(--ui-surface-border);
}
.ui-content-free .card > .card-body,
.ui-content-free .card > .card-body-sm { padding: var(--ui-panel-body-py) var(--ui-panel-px); }
.ui-content-free .card > .card-footer,
.ui-content-free .card > .card-footer-sm { padding: .45rem var(--ui-panel-px); background-color: var(--ui-surface-muted); border-top: 1px solid var(--ui-surface-border); }
/* Widgets KPI de color de marca (Dashboard-Responsive) */
.ui-content-free .card.bg-custom { border-color: transparent; }
.ui-content-free .card.bg-custom .p-2 { padding: .6rem .8rem; }
.ui-content-free .card.bg-custom h2 { font-size: 1.6rem; font-weight: 700; margin-bottom: .2rem; }
.ui-content-free .card.bg-custom p { margin-bottom: 0; font-size: .82rem; }
/* Imagen de un QR dentro de un modal / card (WebTrackeos Trackear, ABMSucursales): responsive, con
   borde redondeado y sombra, mismo look que ViewQR de ParkQR. */
.ui-shell .qr-image { width: 100%; max-width: 320px; height: auto; border-radius: var(--bs-border-radius); box-shadow: 0 4px 12px rgba(0, 0, 0, .15); }
@media (max-width: 576px) { .ui-shell .qr-image { max-width: 260px; } }
/* Utilidades de layout que vivían en estilos.css (equal-height / vertilcal-scroll) */
.ui-content-free .row.equal-height { display: flex; flex-wrap: wrap; }
.ui-content-free .row.equal-height > [class*='col-'] { display: flex; flex-direction: column; }
.ui-content-free .row.equal-height .card { flex: 1; }
.ui-content-free .vertilcal-scroll { flex: 1 1 0; display: flex; flex-direction: column; overflow-y: auto; scrollbar-width: thin; }
/* Cuerpo con scroll de un widget de lista del tablero (Novedades, Vencimientos del mes): alto acotado por
   token, sin min-height/max-height inline en el markup. */
.ui-shell .ui-widget-scroll { max-height: var(--ui-widget-scroll-max); overflow-y: auto; scrollbar-width: thin; }
/* Cuerpo de un widget de lista del tablero: se estira entre header y footer (footer pegado abajo) PERO
   acotado por el token de alto (--ui-widget-scroll-max) para que un widget con miles de registros
   (Novedades) no infle la fila entera; si el contenido excede, scrollea. flex fija el piso (llena el
   hueco aunque haya pocos ítems), max-height fija el techo, min-height:0 habilita el scroll del flex item. */
.ui-shell .ui-widget-fill { flex: 1 1 auto; min-height: 0; max-height: var(--ui-widget-scroll-max); overflow-y: auto; scrollbar-width: thin; }
/* El footer SIEMPRE pegado al fondo de la card. Sin esto, cuando el cuerpo esta topeado por
   --ui-widget-scroll-max y la card la estira `equal-height`/`h-100`, el sobrante de alto quedaba
   DESPUES del footer (es el ultimo flex item) y se veia una franja blanca debajo del fondo gris,
   tapando ademas el radio inferior. Pasaba solo en los widgets cuyo footer es unos px mas bajo:
   `.form-text` suma margin-top .25rem y `.metric-link` no (bug 2026-09-22, Busquedas sin resultado).
   Con margin-top:auto el sobrante queda ARRIBA del footer, donde es invisible (el cuerpo no pinta fondo). */
.ui-shell .card > .card-footer:last-child { margin-top: auto; }
/* Un asp:UpdatePanel envuelve su contenido en un <div> sin clase que corta la cadena de alto
   (col h-100 → [div del UpdatePanel] → card h-100 quedaba a la altura del contenido). Este hook, en el
   wrapper del UpdatePanel, hace que ese div ocupe el 100% y la card recupere el alto de la fila. */
.ui-shell .ui-upnl-fill, .ui-shell .ui-upnl-fill > div { height: 100%; }
/* UN solo aire entre widgets (--ui-content-gap = G), UNA sola fuente (este bloque), en cualquier nivel.
   Cómo se arma, porque Bootstrap mete su propia matemática:
     - una .row lleva margin-top: -G (su gutter-y) y CADA columna margin-top: +G → la primera línea de
       columnas arranca a ras (−G + G = 0) y las líneas siguientes quedan a G.
     - hijos DIRECTOS de main.ui-content: los separa el flex gap (G). Una fila que sigue conserva su −G y
       sus columnas ponen +G → G neto. Ahí NO va ningún margen extra.
     - anidados (dentro de un tab-pane, una card, un UpdatePanel, una columna) no hay flex gap: una fila
       que sigue a otra fila o a una card lleva margin-top 0 (sus columnas ya ponen +G → G neto); una card
       que sigue a una fila o a otra card lleva margin-top G.
   Los mt/mb-N que las páginas pongan en filas, columnas y cards NO cuentan (Clean-Markup.py los saca;
   las guardas de abajo cubren lo que se escape). Los mb-* DENTRO del contenido de una card sí valen. */
.ui-content-free .row { --bs-gutter-x: var(--ui-content-gap); --bs-gutter-y: var(--ui-content-gap); }
.ui-content-free .row > * { margin-bottom: 0 !important; }
.ui-content-free .row.mb-1, .ui-content-free .row.mb-2, .ui-content-free .row.mb-3,
.ui-content-free .card.mb-1, .ui-content-free .card.mb-2, .ui-content-free .card.mb-3 { margin-bottom: 0 !important; }
/* anidados */
.ui-content-free .row + .row,  .ui-content-free .card + .row  { margin-top: 0; }
.ui-content-free .card + .card, .ui-content-free .row + .card { margin-top: var(--ui-content-gap); }
/* hijos directos de main.ui-content: el flex gap ya separa; la fila vuelve a su −G de Bootstrap */
.ui-content-free > .row + .row,  .ui-content-free > .card + .row  { margin-top: calc(-1 * var(--bs-gutter-y)); }
.ui-content-free > .card + .card, .ui-content-free > .row + .card { margin-top: 0; }
/* guardas: un mt-N que se escape no rompe el aire (las utilidades de Bootstrap llevan !important) */
.ui-content-free .row + .row.mt-1, .ui-content-free .row + .row.mt-2, .ui-content-free .row + .row.mt-3 { margin-top: 0 !important; }
.ui-content-free > .row + .row.mt-1, .ui-content-free > .row + .row.mt-2, .ui-content-free > .row + .row.mt-3 { margin-top: calc(-1 * var(--bs-gutter-y)) !important; }
/* un alert (aviso de un tab) antes de un widget: mismo aire */
.ui-content-free .alert + .row, .ui-content-free .alert + .card { margin-top: 0; }
.ui-content-free .tab-pane > .alert { margin-bottom: var(--ui-content-gap); }
/* Separadores dentro de una card: un solo margen (las páginas traían hr con estilos inline distintos) */
.ui-content-free .card hr { margin: .4rem 0; opacity: .15; }
.ui-content-free .card .list-group-flush .list-group-item { padding: .35rem var(--ui-panel-px); }
/* Filtros de los tableros: los 5 con fechas son un panel ui-filters como los de ABM/RPT (head con
   período + presets + Aplicar, body con los campos). wucDashboardPeriodo (Programa) conserva su
   .dash-filter propio: acá se le da el mismo panel. */
.ui-content-free .dash-filter:not(.ui-panel) {
    padding: .45rem var(--ui-panel-px);
    border: 1px solid var(--ui-surface-border);
    border-radius: var(--bs-border-radius-lg);
    background-color: var(--ui-surface);
    box-shadow: var(--ui-surface-shadow);
}
.ui-content-free .dash-filter .fs-5 { font-size: .95rem !important; }   /* el período del wucDashboardPeriodo: Bootstrap lo fija con !important */
/* Los campos de los filtros de un tablero son EXACTAMENTE los de cphBlockFilters de un ABM:
   row > col-xl-3 col-lg-3 col-md-6 col-sm-6 > div.mb-2 > label.form-label + control (o el
   wucDateFlatPickr, que ya trae esa estructura). Por eso la fila de filtros recupera el gutter
   estándar de Bootstrap (el gutter reducido de .ui-content-free es para los widgets, no para
   los campos) y no hay reglas propias de label/select: son las de §6. */
.ui-content-free .ui-filters .row { --bs-gutter-x: 1.5rem; --bs-gutter-y: 0; }
.ui-content-free .ui-filters .btn-group .btn { font-size: .78rem; }
/* Tarjetas métricas (wucMetricCard) */
.ui-content-free .metric-card .card-body { padding: .7rem .85rem; }
.ui-content-free .metric-lbl, .ui-content-free .metric-deltatxt { color: var(--bs-secondary-color); }
/* Presentación base de las metric cards. Antes vivía en dashboards.css (que solo carga la master de
   tableros), pero las metric cards se usan también FUERA de tableros (widgets de Gaceteros/portales),
   así que van en el layout —que se carga en todas las páginas y lo copia Sync-Assets-to-Portals.cmd—. */
.ui-shell .metric-card .card-body { padding: .9rem 1rem; }
.ui-shell .metric-ico { width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; font-size: 20px; flex: 0 0 auto; background: color-mix(in srgb, currentColor 13%, transparent); }
.ui-shell .metric-val { font-size: 1.7rem; font-weight: 750; line-height: 1.05; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.ui-shell .metric-foot { font-size: .75rem; }
.ui-shell .metric-delta { font-weight: 700; }
.ui-content-free .apex-grid .card .card-header { border-bottom: 0; }
/* ApexCharts pinta textos con fill/color propios (inline): en dark mode quedarían negros. Es el
   único lugar de la UI con !important, y solo para el color de texto de los gráficos. */
.ui-shell .apexcharts-text tspan { fill: var(--bs-body-color); }
.ui-shell .apexcharts-legend-text { color: var(--bs-body-color) !important; }
.ui-shell .apexcharts-gridline { stroke: var(--ui-grid-border); }
.ui-shell .apexcharts-tooltip { background: var(--ui-surface) !important; color: var(--bs-body-color) !important; border-color: var(--ui-surface-border) !important; }

/* ---------------------------------------------------------------------------
   14. LAYOUT PÚBLICO (LayoutPublic: login, recuperación de clave, páginas de error)
   --------------------------------------------------------------------------- */
.ui-public { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
/* Topbar pública = cabecera del sidebar (marca, fondo oscuro) + topbar logueada (título, usuario) en una sola banda */
.ui-public-topbar {
    position: sticky;
    top: 0;
    z-index: 1020;
    display: flex;
    align-items: stretch;
    gap: .75rem;
    height: var(--ui-topbar-h);
    background-color: var(--ui-surface);
    border-bottom: 1px solid var(--ui-surface-border);
    box-shadow: var(--ui-surface-shadow);
}
.ui-public-topbar .ui-brand-link { flex: 0 0 auto; padding: 0 1rem 0 .9rem; background-color: var(--ui-sidebar-bg); color: #fff; }
.ui-public-topbar .ui-title { padding-left: .25rem; }
.ui-public-topbar .ui-topbar-actions { margin-left: auto; padding-right: var(--ui-content-pad); }
.ui-public-content { flex: 1 1 auto; width: 100%; max-width: 1140px; margin: 0 auto; padding: 1.5rem var(--ui-content-pad); }
.ui-public-content .card { --bs-card-bg: var(--ui-surface); --bs-card-border-color: var(--ui-surface-border); box-shadow: var(--ui-surface-shadow); }
.ui-public-content .card-header { background-color: var(--ui-surface-muted); font-weight: 600; }

/* ---------------------------------------------------------------------------
   15. POPOVERS (Bootstrap; los inicializa custom.js) — botón ⓘ discreto + popover con tokens
   --------------------------------------------------------------------------- */
.ui-info-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    margin-left: .1rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--bs-tertiary-color);
    font-size: .85rem;
    text-transform: none;
    letter-spacing: 0;
    cursor: help;
}
.ui-info-btn:hover, .ui-info-btn:focus { color: var(--ui-brand-text); background-color: var(--ui-brand-subtle); outline: none; }
.ui-shell .popover {
    --bs-popover-bg: var(--ui-surface);
    --bs-popover-border-color: var(--ui-surface-border);
    --bs-popover-header-bg: var(--ui-surface-muted);
    --bs-popover-header-color: var(--bs-body-color);
    --bs-popover-body-color: var(--bs-body-color);
    --bs-popover-font-size: .8rem;
    --bs-popover-border-radius: var(--bs-border-radius-lg);
    box-shadow: var(--ui-sticky-shadow);
}
.ui-shell .popover-header { font-size: var(--ui-section-title-fs); font-weight: 600; letter-spacing: var(--ui-section-title-ls); text-transform: uppercase; }
/* Teclas: <code> (Bootstrap sanea el contenido del popover y <kbd> no está en su allowList) */
.ui-shell .popover code { font-size: .72rem; padding: .1rem .35rem; border-radius: var(--bs-border-radius-sm); background-color: var(--bs-body-color); color: var(--bs-body-bg); font-family: inherit; font-weight: 600; }

/* Accesibilidad / impresión */
@media (prefers-reduced-motion: reduce) {
    .ui-sidebar, .ui-nav-caret, .ui-collapse-icon, .ui-filter-caret { transition: none; }
    .blink-soft { animation: none; }
}

@media print {
    .ui-sidebar, .ui-topbar, .ui-footer, .ui-panel-foot, .ui-filters { display: none; }
    .ui-content { padding: 0; }
    .ui-panel { border: 0; box-shadow: none; }
}
