/* ==========================================================================
   Parqueo — admin design system (shell, sidebar, topbar, cards, table, forms).
   Global stylesheet consumed by the Razor components of the staff site.
   Recreates design_handoff_panel_admin with the DECR-005 owner overrides.
   Fully responsive: desktop collapsible sidebar (256<->74px) + mobile drawer.
   ========================================================================== */

/* ---------------------------------------------------------------- Shell --- */
.admin-shell {
    display: flex;
    height: 100vh;
    height: 100dvh;
    width: 100%;
    overflow: hidden;
    background: var(--app-bg);
}

.admin-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--app-bg);
}

.admin-content {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--content-pad);
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* -------------------------------------------------------------- Sidebar --- */
.sidebar {
    width: var(--side-w);
    flex: none;
    background: var(--side-bg);
    color: var(--side-text);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
    border-right: 1px solid var(--side-border);
    transition: width var(--motion), transform var(--motion);
    z-index: 40;
}

.sidebar.is-collapsed {
    width: var(--side-w-collapsed);
}

.sidebar__logo {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 20px 18px 18px;
    min-height: var(--topbar-h);
}

.sidebar__logo-mark {
    width: 34px;
    height: 34px;
    flex: none;
    border-radius: 9px;
    background: var(--accent);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-accent);
}

.sidebar__logo-mark .msi {
    font-size: 21px;
}

.sidebar__brand-wrap {
    overflow: hidden;
}

.sidebar__brand {
    font-weight: 800;
    font-size: 16px;
    letter-spacing: -.3px;
    white-space: nowrap;
}

.sidebar__brand em {
    font-style: normal;
    color: var(--accent);
}

.sidebar__brand-sub {
    font-size: 10.5px;
    color: var(--side-muted);
    font-weight: 600;
    letter-spacing: .4px;
    white-space: nowrap;
}

.sidebar__section {
    padding: 2px 22px 10px;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .9px;
    color: var(--side-muted);
}

/* Live menu search (replaces the "PRINCIPAL" label) */
.sidebar__search {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 12px 8px;
    padding: 0 10px;
    height: 38px;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    background: var(--side-hover);
    transition: border-color .15s, background .15s;
}

.sidebar__search:focus-within {
    border-color: var(--accent);
    background: var(--card-bg);
}

.sidebar__search .msi {
    font-size: 19px;
    color: var(--side-muted);
    flex: none;
}

.sidebar__search input {
    border: none;
    background: transparent;
    outline: none;
    width: 100%;
    font-family: inherit;
    font-size: 13px;
    color: var(--text);
}

.sidebar__search input::placeholder {
    color: var(--side-muted);
}

.sidebar__no-results {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 16px;
    font-size: 12.5px;
    color: var(--side-muted);
}

.sidebar__no-results .msi {
    font-size: 18px;
}

.sidebar__nav {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 4px 12px 12px;
}

/* Nav buttons (single item + group header).
   The ink is --side-text, the sidebar's OWN text token, and not a hardcoded slate. The
   hardcoded #475569 was overriding the colour .sidebar already sets on itself, and in dark
   it measured 2.36:1 on --side-bg across the WHOLE level 1 of the DECR-049 two-level menu
   (Panel, Seguridad, Clientes, Parqueo, Contabilidad, Integraciones, Reportes) -- label,
   icon and chevron, on every screen. Removing the override restores the inherited ink:
   17.85:1 light, 14.52:1 dark.
   --side-text and NOT --text-2: the sub-items (.nav-sub__item) are already on --text-2, so
   --text-2 here would paint the group header exactly like the children it contains and
   flatten the two levels. Parent gets primary ink, child secondary, active child accent. */
.nav-btn {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    min-height: 42px;
    border: none;
    border-radius: var(--r-menu);
    cursor: pointer;
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--side-text);
    background: transparent;
    text-align: left;
    transition: background .15s;
}

.nav-btn:hover {
    background: var(--side-hover);
}

.nav-btn.is-active {
    background: var(--accent);
    color: #fff;
    box-shadow: var(--shadow-accent);
}

.nav-btn.is-active:hover {
    background: var(--accent);
}

.nav-btn.is-disabled {
    color: var(--text-3);
    cursor: not-allowed;
    opacity: .65;
}

.nav-btn.is-disabled:hover {
    background: transparent;
}

.nav-btn__icon {
    font-size: 21px;
    width: 24px;
    text-align: center;
    flex: none;
}

.nav-btn__label {
    flex: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.nav-btn__tag {
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: .3px;
    text-transform: uppercase;
    color: var(--text-3);
    background: var(--off-bg);
    padding: 2px 6px;
    border-radius: var(--r-pill);
    flex: none;
}

.nav-btn__chevron {
    font-size: 19px;
    opacity: .75;
    flex: none;
    transition: transform .2s;
}

.nav-btn.is-open .nav-btn__chevron {
    transform: rotate(180deg);
}

.nav-btn__dot {
    display: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
    flex: none;
}

/* Sub-items */
.nav-sub {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin: 2px 0 4px;
    padding-left: 14px;
    overflow: hidden;
    animation: fadeUp .18s ease;
}

.nav-sub__item {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 12px 9px 14px;
    min-height: 40px;
    border: none;
    border-radius: var(--r-sub);
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-2);
    background: transparent;
    text-align: left;
    transition: background .15s;
}

.nav-sub__item:hover {
    background: var(--border-softer);
}

.nav-sub__item.is-active {
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 700;
}

.nav-sub__item.is-disabled {
    color: var(--text-3);
    cursor: not-allowed;
    opacity: .7;
}

.nav-sub__item.is-disabled:hover {
    background: transparent;
}

.nav-sub__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #cbd5e1;
    flex: none;
}

.nav-sub__item.is-active .nav-sub__dot {
    background: var(--accent);
}

.nav-sub__label {
    flex: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.nav-sub__tag {
    font-size: 9px;
    font-weight: 700;
    color: var(--text-3);
    flex: none;
}

/* Reports: second-level category sub-accordion (nested inside .nav-sub) */
.nav-subgroup__head {
    font-weight: 600;
}

.nav-subgroup__chevron {
    font-size: 17px;
    opacity: .7;
    flex: none;
    margin-left: auto;
    transition: transform .2s;
}

.nav-subgroup__head.is-open .nav-subgroup__chevron {
    transform: rotate(180deg);
}

.nav-subsub {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin: 1px 0 3px;
    padding-left: 16px;
    overflow: hidden;
    animation: fadeUp .18s ease;
}

/* User card */
.sidebar__usercard {
    margin-top: auto;
    padding: 12px;
}

.sidebar__user {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 11px;
    border-radius: 12px;
    background: rgba(148, 163, 184, .12);
    width: 100%;
    border: none;
    cursor: pointer;
    font-family: inherit;
    text-align: left;
    color: inherit;
}

.sidebar__user-text {
    overflow: hidden;
    flex: 1;
}

.sidebar__user-name {
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar__user-role {
    font-size: 11px;
    color: var(--side-muted);
    white-space: nowrap;
}

.sidebar__user .msi {
    font-size: 19px;
    color: var(--side-muted);
}

/* Drawer backdrop (mobile only) */
.sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, .45);
    z-index: 35;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--motion), visibility var(--motion);
}

/* Collapsed (desktop mini rail) — only applies on wide viewports */
@media (min-width: 900px) {
    .sidebar.is-collapsed .sidebar__brand-wrap,
    .sidebar.is-collapsed .sidebar__section,
    .sidebar.is-collapsed .sidebar__search,
    .sidebar.is-collapsed .nav-btn__label,
    .sidebar.is-collapsed .nav-btn__tag,
    .sidebar.is-collapsed .nav-btn__chevron,
    .sidebar.is-collapsed .nav-sub,
    .sidebar.is-collapsed .sidebar__user-text,
    .sidebar.is-collapsed .sidebar__user .msi {
        display: none;
    }

    .sidebar.is-collapsed .nav-btn,
    .sidebar.is-collapsed .sidebar__user {
        justify-content: center;
        gap: 0;
    }

    .sidebar.is-collapsed .nav-btn.has-active-child .nav-btn__dot {
        display: block;
        position: absolute;
        margin-top: -14px;
        margin-left: 14px;
    }

    .sidebar.is-collapsed .nav-btn {
        position: relative;
    }

    .sidebar.is-collapsed .sidebar__logo {
        justify-content: center;
        padding-left: 0;
        padding-right: 0;
    }
}

/* --------------------------------------------------------------- Topbar --- */
.topbar {
    height: var(--topbar-h);
    flex: none;
    background: #fff;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 0 18px;
}

.topbar__menu {
    width: 38px;
    height: 38px;
    flex: none;
    border: none;
    background: transparent;
    border-radius: 9px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #475569;
    transition: background .15s;
}

.topbar__menu:hover {
    background: #f1f5f9;
}

.topbar__menu .msi {
    font-size: 23px;
}

.topbar__menu--mobile {
    display: none;
}

.topbar__heading {
    min-width: 0;
}

.topbar__crumb {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 11.5px;
    color: var(--side-muted);
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.topbar__title {
    font-size: 17px;
    font-weight: 800;
    letter-spacing: -.3px;
    line-height: 1.1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.topbar__spacer {
    flex: 1;
}

/* Surface and ink come from TOKENS, not from #fff / #334155. The hardcoded pair
   only ever resolved for the light theme: the dark remap further down repaints the
   background but never the ink, so #334155 on --card-bg (#111a2b) measured 1.63:1,
   far under the 4.5:1 AA floor -- and under the SYSTEM dark preference (no
   data-theme attribute at all) that remap does not even apply, leaving a white pill
   on the dark bar. --text on --card-bg is 15.9:1 light and 15.3:1 dark. */
.topbar__chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 34px;
    padding: 0 10px 0 12px;
    border: 1px solid var(--border);
    border-radius: var(--r-pill);
    background: var(--card-bg);
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text);
    cursor: pointer;
    white-space: nowrap;
}

/* Read-only variant: the chip REPORTS the parking scope, it does not open anything,
   so it must not offer the affordance of one. Long parking names are clipped rather
   than allowed to push the search box and the account menu off the bar. */
.topbar__chip--static {
    cursor: default;
    max-width: 260px;
}

.topbar__chip--static > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.topbar__chip .msi {
    font-size: 18px;
    color: var(--text-3);
    flex: none;
}

.topbar__search {
    display: flex;
    align-items: center;
    gap: 8px;
    background: #f1f5f9;
    border: 1px solid var(--border);
    border-radius: 11px;
    padding: 0 13px;
    height: 40px;
    width: 280px;
    max-width: 32vw;
}

.topbar__search:focus-within {
    border-color: var(--accent);
    background: #fff;
}

.topbar__search .msi {
    font-size: 20px;
    color: var(--side-muted);
}

.topbar__search input {
    border: none;
    background: transparent;
    outline: none;
    font-family: inherit;
    font-size: 13.5px;
    width: 100%;
    color: var(--text);
}

.topbar__search-btn {
    display: none;
}

.topbar__icon-btn {
    position: relative;
    width: 40px;
    height: 40px;
    flex: none;
    border: 1px solid var(--border);
    background: #fff;
    border-radius: 11px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #475569;
    transition: background .15s;
}

.topbar__icon-btn:hover {
    background: #f8fafc;
}

.topbar__icon-btn .msi {
    font-size: 21px;
}

.topbar__bell-dot {
    position: absolute;
    top: 8px;
    right: 9px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #ef4444;
    border: 2px solid #fff;
}

.topbar__avatar {
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: 11px;
    background: var(--avatar-gradient);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-weight: 700;
    font-size: 14px;
    cursor: pointer;
}

/* ---------------------------------------------------------- Section head -- */
.section-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}

.section-head__title {
    font-size: 22px;
    font-weight: 800;
    letter-spacing: -.5px;
}

.section-head__subtitle {
    font-size: 13.5px;
    color: var(--text-2);
    margin-top: 3px;
}

/* --- 026/T104: el subtítulo de sección en tema CLARO -------------------------------------
   Este subtítulo NO vive sobre una tarjeta: .section-head es lo primero de .admin-content, o
   sea que se dibuja sobre --app-bg (#eef2f7), no sobre --card-bg (#ffffff). Ahí está la
   trampa: --text-2 fue calibrado contra el BLANCO y sobre blanco cumple, pero sobre la página
   pierde el margen. Medido con la fórmula WCAG sobre los colores computados:

     ANTES  --text-2 #64748b sobre --app-bg #eef2f7 = 4,23:1  ❌  (sobre #ffffff daría 4,76:1)
     AHORA  #475569          sobre --app-bg #eef2f7 = 6,73:1  ✅

   13,5 px con peso normal no es "texto grande", así que el piso es 4,5:1 y 4,23 no alcanza.

   ⛔ NO SE MUEVE EL TOKEN: --text-2 pinta encabezados de tabla, etiquetas de formulario, glosas
   y metadatos en decenas de pantallas, y en TODAS ésas ya cumple. El defecto es de esta
   variante, que es la única que apoya --text-2 sobre --app-bg. #475569 no es un color nuevo:
   es la tinta secundaria que este mismo archivo ya usa en .topbar__icon-btn y en el botón del
   drawer, así que la familia no se ensancha.

   🔑 EN OSCURO NO HAY DEFECTO: --text-2 (#a3b0c4) sobre --app-bg (#0b1220) mide 8,53:1 -- no
   8,45:1: esa cifra estaba corrida en 0,08 y de acá la copiaron de largo (026/T726, recalculada
   canal por canal y confirmada por 026/T716 en RestrictedScopeState.razor). Por eso la
   corrección va acotada a los dos estados claros con el mismo par de selectores que usan
   .alert--ok/--warn y .badge--ok/--warn (el tercer estado, :root sin data-theme con el SO en
   oscuro, quedaría mal pintado por un :not([data-theme="dark"]) a secas). */
:root[data-theme="light"] .section-head__subtitle {
    color: #475569;
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .section-head__subtitle {
        color: #475569;
    }
}

.section-head__actions {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}

/* -------------------------------------------------------------- Buttons --- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    height: 40px;
    min-height: 40px;
    padding: 0 15px;
    border-radius: var(--r-control);
    border: 1px solid var(--border);
    background: #fff;
    color: #334155;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: background .15s, border-color .15s, box-shadow .15s;
}

.btn:hover {
    background: #f8fafc;
}

.btn .msi {
    font-size: 19px;
}

.btn--primary {
    background: var(--accent);
    border-color: transparent;
    color: #fff;
    font-weight: 700;
    box-shadow: var(--shadow-accent);
}

.btn--primary:hover {
    background: var(--accent-dark);
}

.btn--ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-2);
}

.btn--ghost:hover {
    background: #f1f5f9;
}

.btn--danger {
    background: var(--danger-text);
    border-color: transparent;
    color: #fff;
    font-weight: 700;
}

.btn--danger:hover {
    background: #be123c;
}

/* ---- 026/T101 (DECR-258 R4): el botón que CONFIRMA una liquidación ------------------------
   Se usa en un solo lugar — el pie del diálogo de confirmación de Settlements.razor — y existe
   porque ahí el botón llevaba .btn--danger: registrar la transferencia es la acción correcta y
   esperada del contador, no una destructiva, y el rojo le decía lo contrario de lo que pasa.

   ⛔ NO se toca .btn--primary, y ésa es la decisión, no una precaución: el primario en tema
   oscuro mide 3,68:1 contra su tinta blanca —bajo el piso AA— y arreglarlo en la clase global
   cambiaría el tono de reposo de TODOS los botones primarios del sitio, en decenas de
   pantallas. Ese defecto está diferido a propósito (P-064 punto 2). Por eso esto es un
   modificador ACOTADO que se pone ENCIMA de .btn--primary: el botón sigue siendo primario para
   los remaps de tema oscuro (que excluyen .btn--primary por nombre) y sólo cambia su relleno.

   MEDIDO con la fórmula WCAG sobre los colores computados, tinta #fff, no estimado:
     antes  .btn--danger   claro 4,70:1        oscuro 6,96:1
     ahora  --accent-dark  claro 10,85:1 ✅    oscuro 5,17:1 ✅   (#193490 / #2563eb)
     hover  15 % de negro  claro 12,55:1 ✅    oscuro 6,64:1 ✅   (#152C7A / #1F54C8)
   ⛔ NO se inventó ningún color: --accent-dark ya es el hover de todo botón primario del sitio.
   Lo único nuevo es usarlo como BASE en vez de como hover, que es la combinación que los dos
   análisis anteriores no habían probado (probaron blanco sobre --accent y tinta oscura sobre
   --accent, y las dos fallan en un extremo).

   🔑 EL HOVER OSCURECE EL FONDO, NO EL BOTÓN, y la diferencia está medida. El mecanismo obvio
   —copiar el `filter: brightness()` que .btn--danger usa en oscuro— aplica el filtro al
   ELEMENTO, así que oscurece también la tinta: el blanco cae a #d9d9d9 y el hover mide 4,70:1,
   no 6,64:1. Un gradiente plano de negro al 15 % es exactamente brightness(.85) aplicado sólo
   a la capa de fondo, así que reproduce el color medido y deja la tinta intacta. El hover
   repite background-color porque .btn:hover es una abreviatura y, si no, pisaría el relleno. */
.btn--settle-confirm {
    background-color: var(--accent-dark);
    background-image: none;
}

.btn--settle-confirm:hover {
    background-color: var(--accent-dark);
    background-image: linear-gradient(rgba(0, 0, 0, .15), rgba(0, 0, 0, .15));
}

.btn:disabled {
    opacity: .55;
    cursor: not-allowed;
    box-shadow: none;
}

/* ---------------------------------------------------------------- Cards --- */
.card {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: var(--r-card);
    box-shadow: var(--shadow-card);
}

.card--pad {
    padding: var(--card-pad);
}

/* ----------------------------------------------------------------- KPIs --- */
.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--grid-gap);
    margin-bottom: 20px;
}

.kpi {
    padding: var(--card-pad);
}

.kpi__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.kpi__icon {
    width: 42px;
    height: 42px;
    border-radius: var(--r-kpi-icon);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-soft);
    color: var(--accent);
}

.kpi__icon .msi {
    font-size: 22px;
}

.kpi__icon--ok {
    background: #ecfdf5;
    color: var(--ok-text);
}

.kpi__icon--muted {
    background: var(--off-bg);
    color: var(--text-3);
}

.kpi__icon--danger {
    background: var(--danger-bg);
    color: var(--danger-text);
}

/* 023/T051 -- el catálogo fiscal necesita un cuarto tono para "cuántos conceptos tienen el
   CABYS pendiente de confirmar": ni ok, ni mute, ni error. --warn-bg/--warn-text ya traen sus
   propios valores de tema oscuro (app.css), así que esto no necesita un bloque dark aparte,
   igual que .badge--warn. */
.kpi__icon--warn {
    background: var(--warn-bg);
    color: var(--warn-text);
}

/* ------------------------------------------------------- Occupancy banner --- */
/* Prominent aforo status banner (US4): red when full, green when there is room. */
.occ-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 26px 20px;
    border-radius: var(--r-card);
    margin-bottom: 20px;
    border: 1px solid transparent;
}

.occ-banner .msi {
    font-size: 40px;
}

.occ-banner__text {
    font-size: 30px;
    font-weight: 800;
    letter-spacing: -.5px;
}

.occ-banner--full {
    background: var(--danger-bg);
    color: var(--danger-text);
    border-color: var(--danger-text);
}

.occ-banner--free {
    background: var(--ok-bg);
    color: var(--ok-text);
    border-color: var(--ok-text);
}

/* ------------------------------------------------------ Monitor board --- */
/* Real-time supervisor monitor (US6, MON-01): coloured tickets in process.
   Green = automatic (QuickPass), Red = manual / not integrated, Blue = contracts.
   Rows carry a coloured left rail; the colour comes from the semantic tokens so
   both light and dark themes are covered. */
.mon-row--green td:first-child {
    box-shadow: inset 4px 0 0 0 var(--ok-text);
}

.mon-row--red td:first-child {
    box-shadow: inset 4px 0 0 0 var(--danger-text);
}

.mon-row--blue td:first-child {
    box-shadow: inset 4px 0 0 0 var(--accent);
}

/* Colour badges reused on the board (green reuses .badge--ok). */
.badge--danger {
    color: var(--danger-text);
    background: var(--danger-bg);
}

.badge--info {
    color: var(--accent);
    background: var(--accent-soft);
}

.kpi__value {
    font-size: 27px;
    font-weight: 800;
    letter-spacing: -.6px;
    margin-top: 14px;
}

.kpi__value--muted {
    color: var(--text-3);
}

.kpi__label {
    font-size: 13px;
    color: var(--text-2);
    font-weight: 500;
    margin-top: 2px;
}

/* ---------------------------------------------------------------- Table --- */
.table-card {
    overflow: hidden;
}

.table-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 18px 20px;
    border-bottom: 1px solid var(--border-soft);
}

.table-card__title {
    font-size: 15px;
    font-weight: 800;
}

.table-toolbar {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    padding: 14px 20px;
    border-bottom: 1px solid var(--border-soft);
}

.table-scroll {
    overflow-x: auto;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

/* --text-2 and NOT --text-3, measured on THIS surface and not on the card: #94a3b8 on the
   header band (#fafbfc) is 2.47:1, under the 4.5:1 AA floor; --text-2 (#64748b) measures
   4.59:1. In dark the band is repainted to --border-soft (#223049), where --text-3 (#74839c)
   is 3.45:1 -- also failing -- and --text-2 (#a3b0c4) is 6.03:1, so both themes clear AA.
   A column header is the table's CONTENT, not its metadata ink: the cell below it means
   nothing without it. Same species as .detail-grid__label (11px, 700, uppercase, tracked),
   which already reads at --text-2; this closes the divergence that fix left open. */
.data-table th {
    text-align: left;
    padding: 11px 20px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .5px;
    color: var(--text-2);
    text-transform: uppercase;
    background: #fafbfc;
    border-bottom: 1px solid var(--border-soft);
    white-space: nowrap;
}

.data-table td {
    padding: 13px 20px;
    border-bottom: 1px solid var(--border-softer);
    white-space: nowrap;
    vertical-align: middle;
}

/* --- 026/T105: ENCABEZADO DE FILA, como pieza del sistema de diseño ----------------------
   Una celda marcada <th scope="row"> es lo único que asocia los números de una fila con el
   sujeto del que hablan: sin ella un lector de pantalla recorre una tabla de dinero diciendo
   "6.102,00" sin decir de quién es. Pero el <th> arrastra el estilo de .data-table th —11 px,
   mayúsculas, tracking y banda gris—, que es el de un encabezado de COLUMNA y en medio del
   cuerpo queda fuera de lugar. Esta clase lo devuelve a apariencia de celda de datos sin
   tocar la semántica.

   🔴 LA TRAMPA QUE ESTO CIERRA — está MEDIDA y es de especificidad, no de color:
   la banda del encabezado en oscuro la impone `:root[data-theme="dark"] .data-table th`, que
   es (0,3,1) — :root (1) + [data-theme] (1) + .data-table (1) + th (1 elemento). Cualquier
   regla de row-head con MENOS de tres clases pierde contra ella, y la celda se pinta con la
   banda del encabezado SÓLO EN TEMA OSCURO: la variante se ve bien en claro y rota en oscuro.
   Por eso la solución NO es subirle la especificidad a la excepción sino EXCLUIR el row-head
   de la regla que causa el daño, con el `:not(.cell-row-head)` que lleva el remap oscuro más
   abajo. Así esta regla base (0,2,1) alcanza y no hay carrera de pesos.
   ⚠️ Ojo con el CSS scoped: Blazor agrega el atributo al ÚLTIMO selector simple, así que
   `.data-table .cell-row-head` en un .razor.css compila a (0,3,0) y TAMPOCO le gana a (0,3,1).
   Cualquier pantalla que se estilice por su cuenta hereda esta misma trampa. */
.data-table th.cell-row-head {
    padding: 13px 20px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: normal;
    text-transform: none;
    color: inherit;
    background: transparent;
    border-bottom: 1px solid var(--border-softer);
    vertical-align: middle;
}

/* Un encabezado de fila que ADEMÁS es el identificador conserva la tinta y el peso de
   .cell-id. La regla de arriba escribe `color: inherit` y `font-weight: 600` para neutralizar
   el gris chico del encabezado de columna, y por ser (0,2,1) le ganaba a .cell-id (0,1,0):
   el "#1234" perdía el acento y quedaba como texto común. Esto lo devuelve sin tocar .cell-id,
   que sigue sirviendo igual a las celdas normales. */
.data-table th.cell-row-head.cell-id {
    color: var(--accent);
    font-weight: 700;
}

.data-table tbody tr {
    transition: background .12s;
}

.data-table tbody tr:hover {
    background: #fafbfc;
}

/* --- 026/T967 (b): frozen first column ----------------------------------------------------
   UnpostedCommissions.razor and UnaccountedPayments.razor are both wide (12 columns plus an
   optional selection checkbox) and the last column ("Qué pasó" / "Qué pasa") sits off-screen —
   scrolling sideways to read it used to lose track of WHICH row it belonged to, since nothing
   stayed put. The row's identifying column ("Cobro") now sticks to the left edge of the
   scroll container (`.grid__scroll`), and the selection checkbox column sticks right beside it
   when the screen renders one, so the two travel together with no gap between them.

   `left` itself is set per usage (0 for the checkbox and for the id column when there is no
   checkbox; the checkbox's own 44px width when there is one) -- this class only carries what
   both spots share: the sticky mechanics, an opaque background so scrolled-past columns do
   not show through, and a hairline right edge marking where the frozen zone ends.

   🔴 `.data-table th.cell-row-head` (above) sets `background: transparent` ON PURPOSE, so the
   row-head cell shows the row's own background instead of the column-header band -- exactly
   wrong for a STICKY cell, which needs to occlude whatever scrolls underneath it. The extra
   selector below targets that one combination without touching the transparent default
   anywhere else the class is used. */
.cell-frozen {
    position: sticky;
    z-index: 2;
    background: var(--card-bg);
    box-shadow: 1px 0 0 var(--border-softer);
}

.data-table th.cell-row-head.cell-frozen {
    background: var(--card-bg);
}

.data-table tbody tr:hover .cell-frozen {
    background: #fafbfc;
}

.cell-id {
    font-family: ui-monospace, 'Cascadia Code', 'Consolas', monospace;
    font-weight: 700;
    color: var(--accent);
}

.cell-name {
    display: flex;
    align-items: center;
    gap: 10px;
}

.cell-name__text {
    font-weight: 600;
}

.cell-actions {
    text-align: right;
}

.cell-actions__wrap {
    display: inline-flex;
    gap: 4px;
    justify-content: flex-end;
}

/* 018/T305: nota de "sólo el personal técnico la puede cambiar" en la columna Acciones de
   PlantillasDePosteo.razor. .data-table td fija white-space: nowrap (~línea 1052), así que
   sin esto el texto libre empujaba la columna a un único ancho fijo (405px) en TODAS las
   filas, incluso las que sólo tienen el botón "Editar". display: inline-block + max-width
   deja que el texto rompa línea (dos-tres líneas) sin ensanchar la columna para el resto.

   🔴 SEGUNDA PASADA DEL ux-reviewer (2026-09-22): la tabla usa table-layout: auto, así que
   sin un MÍNIMO el navegador comprime la columna Acciones a ~60px entre 1568 y 1200px de
   ancho y la nota cae a 7-8 renglones de una palabra. min-width fija el piso que el
   algoritmo de auto-layout respeta antes de repartir el resto: la columna Acciones no baja
   de ~150px y la que cede es la que ya tiene overflow-wrap para texto libre (Descripción).
   Filas normales (sólo el ícono "Editar") no llevan este <span> y no se ven afectadas: la
   columna Acciones angosta de siempre sigue angosta cuando ninguna fila tiene la nota. */
.cell-actions__note {
    display: inline-block;
    min-width: 150px;
    max-width: 170px;
    white-space: normal;
    overflow-wrap: break-word;
    word-break: break-word;
    text-align: left;
    font-size: 12px;
}

/* Numeric / money cells: right-aligned with tabular figures, so a column of amounts
   lines up digit under digit and the eye can compare magnitudes down the column.
   DECR-230 R2 asks for ONE money presentation across the site, and alignment is half
   of it — ₡5.000 above ₡178,77 only reads as "bigger" when the units share a column.

   🔴 THIS RULE IS NOT NEW. It lived, byte for byte, inside the SCOPED css of exactly two
   Reports screens (EjecutarReporte / HistorialReportes), which is why Mayor and Diario
   could not reuse it even if they had wanted to: outside those two components the class
   simply did not exist, so `CellClass = "cell-num"` would have been a no-op. It was
   promoted here — and DELETED from both .razor.css files, so there is one definition and
   not three. Do not copy it back into a component's scoped sheet.

   `.cell-num` goes on the <td> (PagedGrid's GridColumn.CellClass), `.cell-num__text` on an
   inner <span> when the cell wraps its value in one. Alignment inherits either way; both
   names are kept because both are already in use.

   ⚠️ It deliberately does NOT touch <th>: `.data-table th` sets `text-align:left` with a
   higher specificity (0,1,1) than this class (0,1,0), so a header would need its own rule.
   Headers stay left-aligned, which is what the Reports grids already look like. */
.cell-num,
.cell-num__text {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* --- 026/T992: celda de TEXTO LIBRE, hallazgo del ux-reviewer ---------------------------
   `.data-table td` fija `white-space: nowrap` sin excepción (arriba, ~línea 1052): correcto
   para las celdas cortas del sitio, pero una fila con un motivo de anulación o una nota real
   (hasta 400 caracteres, CutDepositRequest.Notes / la anulación de VoidCutDepositAsync) no
   tiene forma de cortar línea y empuja la celda y la tabla entera a un ancho absurdo en vez
   de crecer hacia abajo. No existía una clase para esto en el sistema de diseño -- sólo
   .cell-num, .cell-actions y .cell-id, ninguna pensada para texto libre -- así que nace acá,
   con el mismo patrón de nombre que esas tres. Sólo se aplica donde una columna admite texto
   libre de verdad (CutDeposits.razor: "Anulación" y "Nota"); no es el default de .data-table
   td porque la mayoría de columnas del sitio SÍ quiere quedarse en una línea.

   🔴 SEGUNDA PASADA DEL ux-reviewer (2026-09-22): `.cell-text` sola es (0,1,0) y pierde contra
   `.data-table td` (0,1,1) -- la MISMA trampa de especificidad que el comentario de
   `.data-table th.cell-row-head` ya documentó arriba (~línea 1064), medida ahí con
   `getComputedStyle` dando `nowrap`. Se agrega el calificador `.data-table td` para llegar a
   (0,2,1) y ganar la cascada; no se toca `.data-table td` en general porque el resto de
   columnas del sitio sigue queriendo `nowrap`. */
.data-table td.cell-text {
    white-space: normal;
    max-width: 320px;
    overflow-wrap: break-word;
    word-break: break-word;
}

/* --- 018/T305, cuarta pasada: topes ACOTADOS de PlantillasDePosteo.razor -----------------
   El ux-reviewer midió, en la tercera pasada, que Débito y Crédito quedan en 268px cada una y
   que Tipo de movimiento + Activa + Acciones NO cambian con este trabajo (201 + 82 + 190 =
   473px). Con eso solo, aplicar `cell-text` (tope compartido de 320px, ~línea 1230) a
   Descripción no bastaba: 473 + 268 + 268 + 320 = 1329px contra 1252px de `clientWidth` a
   1568px, 77px de sobra — «Editar» seguía fuera de vista. La cuenta débito/crédito ("código —
   nombre" en una sola línea, AccountLabel) es la otra columna de texto libre, así que las tres
   ceden con su PROPIO tope, más angosto que el de `cell-text` porque ese lo usan otras
   pantallas (CutDeposits.razor) y no se toca acá. Cada regla va DESPUÉS de
   `.data-table td.cell-text` con la MISMA especificidad (0,2,1) — misma trampa documentada
   arriba (~línea 1224) — así que el orden de aparición decide el `max-width`; el resto de las
   propiedades (white-space, overflow-wrap, word-break) las siguen heredando de `cell-text`, que
   ambas columnas también llevan en `CellClass`. Con los topes nuevos, Descripción 250 + Débito
   225 + Crédito 225 = 700px, más el fijo 473px = 1173px contra 1252px: 79px de margen.
   Estimación del `frontend-programmer`, no medición: pendiente la pasada del `ux-reviewer`. */
.data-table td.posting-template-description {
    max-width: 250px;
}

.data-table td.posting-template-account {
    max-width: 225px;
}

.text-muted {
    color: var(--text-2);
}

/* --- 026/T685: modificador para el .text-muted que vive DIRECTO sobre --app-bg (nunca
   dentro de una .card) ------------------------------------------------------------------
   MISMA causa que 026/T104 ya corrigió en .section-head__subtitle, y MISMO arreglo: --text-2
   se calibró contra --card-bg (#ffffff) y sobre --app-bg (#eef2f7) pierde el piso WCAG.
   Medido 2026-09-06, misma fórmula:

     ANTES  --text-2 #64748b sobre --app-bg #eef2f7 = 4,23:1  ❌
     AHORA  #475569          sobre --app-bg #eef2f7 = 6,73:1  ✅

   Es la MISMA tinta que .section-head__subtitle ya reusa (y que .topbar__icon-btn y el botón
   del drawer ya traían antes de eso) -- la familia no se ensancha.

   ⛔ NO SE TOCA EL TOKEN NI LA CLASE BASE: .text-muted tiene 300+ usos en el sitio, casi todos
   dentro de una .card (--card-bg), donde --text-2 YA cumple. Este modificador es SÓLO para las
   seis notas, en tres pantallas, medidas 2026-09-06 con el mismo defecto -- el aviso de
   auto-refresco de Monitor.razor (026/T677) y las de "este parqueo dejó de estar
   disponible"/"todo en orden"/"el más antiguo se liquidó" de Settlements.razor y
   MissingParkingInvoices.razor (026/T613) -- que por eso comparten UN modificador en vez de
   repetir este mismo bloque de comentario y de números tres (o seis) veces; sigue el mismo
   patrón base+modificador que .alert/.alert--error y .badge/.badge--ok ya usan en este archivo.

   🔑 EN OSCURO NO HAY DEFECTO: mismo cálculo que .section-head__subtitle (8,53:1, no 8,45:1 --
   ver la nota de esa regla), así que la
   corrección va acotada a los dos estados claros con el mismo par de selectores que usa
   .alert--ok/--warn y .badge--ok/--warn (el tercer estado, :root sin data-theme con el SO en
   oscuro, quedaría mal pintado por un :not([data-theme="dark"]) a secas). */
:root[data-theme="light"] .text-muted--onpage {
    color: #475569;
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .text-muted--onpage {
        color: #475569;
    }
}

/* -------------------------------------------------------------- Avatar ---- */
.avatar {
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    color: #fff;
    background: var(--avatar-gradient);
    flex: none;
    width: 30px;
    height: 30px;
    font-size: 11px;
    line-height: 1;
}

.avatar--md {
    width: 36px;
    height: 36px;
    font-size: 13px;
}

/* --------------------------------------------------------------- Badges --- */
.badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border-radius: var(--r-pill);
    font-size: 11.5px;
    font-weight: 700;
    white-space: nowrap;
}

.badge__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex: none;
}

/* 029/T040: decorative glyph shown when the badge has a hint (NeverSynced/NotResidential/
   NoGate). currentColor, so it never carries information by color alone. */
.badge__hint {
    font-size: 14px;
    color: currentColor;
    flex: none;
}

/* 029/T040, second pass: the hint used to live ONLY in the badge's native `title`, unreachable
   by keyboard/touch/screen reader. Stacks the hint as real text below the badge instead; only
   rendered for the 3 actionable states, so the other states keep a single badge, unchanged. */
.cell-stack {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}

.cell-stack__hint {
    display: block;
    max-width: 220px;
    white-space: normal;
    overflow-wrap: break-word;
    word-break: break-word;
    font-size: 11.5px;
    line-height: 1.35;
    color: var(--text-2);
}

.badge--ok {
    color: var(--ok-text);
    background: var(--ok-bg);
}

.badge--off {
    color: var(--off-text);
    background: var(--off-bg);
}

.badge--warn {
    color: var(--warn-text);
    background: var(--warn-bg);
}

/* --- 026/T104: .badge--ok y .badge--warn en tema CLARO -----------------------------------
   EL MISMO defecto de clase que ya se corrigió en .alert--ok y .alert--warn más abajo, sobre
   los MISMOS pares de color, y por eso las cifras son idénticas (verificadas con la fórmula
   WCAG sobre los colores computados, no estimadas):

     .badge--warn  --warn-text #d97706 sobre --warn-bg #fef3c7 = 2,86:1  ❌   (17 pantallas)
     .badge--ok    --ok-text   #16a34a sobre --ok-bg   #dcfce7 = 3,00:1  ❌   (32 pantallas)
   ⚠️ Los conteos de la casilla venian cruzados (33 para el aviso, 18 para el bien); medidos
   hoy son 17 archivos / 24 usos para --warn y 32 archivos / 44 usos para --ok. No cambia nada
   del arreglo, pero el que reparte trabajo merece el numero correcto.
     después       #92400e     sobre #fef3c7            = 6,37:1  ✅
                   #166534     sobre #dcfce7            = 6,49:1  ✅

   ⚠️ EL PISO ES 4,5:1, NO 3:1: la insignia se dibuja a 11,5 px con peso 700, y "texto grande"
   empieza en 18,66 px en negrita. El peso no la exime.

   🔑 LO CONTRAINTUITIVO: en OSCURO estas dos miden 8,64:1 y 8,14:1 — perfectas. El defecto es
   sólo del claro, y quien mirara únicamente la columna oscura no lo vería. ⛔ NO se "baja" el
   oscuro para emparejar: el oscuro está bien.

   ⛔ POR QUÉ NO SE TOCAN LOS TOKENS: --ok-text y --warn-text también pintan KPIs, chips, íconos
   y bordes por todo el sitio. Se corrige LA VARIANTE, por alcance y sin !important, que es el
   precedente que este archivo ya fijó tres veces (--error, --info, y .alert--ok/--warn).

   🔴 POR QUÉ DOS SELECTORES Y NO UNO: el tema tiene TRES estados, y el tercero es el :root SIN
   data-theme que ve cualquiera antes de que theme.js estampe (y otra vez tras cada navegación
   mejorada, P-065); ahí manda prefers-color-scheme. Un `:not([data-theme="dark"])` a secas
   pintaría verde oscuro sobre fondo oscuro en ese estado. Los dos selectores describen EXACTA-
   MENTE los dos estados claros y ningún otro. Es la misma forma que usa .alert--ok/--warn.
   ⭐ Consecuencia: en TODO estado oscuro estas reglas no matchean y el color sigue saliendo de
   var(--ok-text)/var(--warn-text) — el oscuro NO SE PUEDE MOVER desde acá.

   ⚠️ .badge--off queda AFUERA a propósito, y NO porque pase: medida al hacer esto, --off-text
   #64748b sobre --off-bg #f1f5f9 da 4,34:1 en claro — también bajo el piso, aunque por poco
   (en oscuro mide 6,79:1 y está bien). No estaba en la casilla T104 y cambiarla movería la
   insignia de "inactivo" en todo el sitio, así que se REPORTA en vez de arreglarse de paso.

   🔴 023/T051, hallazgo del ux-reviewer (2026-09-05): .kpi__icon--warn (el cuarto tono del KPI,
   ver el comentario junto a su definición) pinta EL MISMO PAR --warn-text sobre --warn-bg, así
   que hereda exactamente el mismo defecto y la misma cifra: 2,86:1 contra el piso de 3:1 (ícono
   no textual, WCAG 1.4.11). Se suma al MISMO patrón en vez de abrir uno nuevo: mismo color de
   reemplazo (#92400e), misma cifra después (6,37:1), mismo par de selectores. */
:root[data-theme="light"] .badge--ok {
    color: #166534;
}

:root[data-theme="light"] .badge--warn,
:root[data-theme="light"] .kpi__icon--warn {
    color: #92400e;
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .badge--ok {
        color: #166534;
    }

    :root:not([data-theme="dark"]) .badge--warn,
    :root:not([data-theme="dark"]) .kpi__icon--warn {
        color: #92400e;
    }
}

/* --- 026/T274: .badge--danger en tema CLARO -------------------------------------------
   El mismo defecto de clase que 026/T104 ya corrigió arriba en .badge--ok y .badge--warn,
   ahora en el distintivo que más urge: el que marca la situación que NINGÚN otro reporte
   deja ver (Cobro anulado, Falló, cobro automático pendiente de regularizar, Red — incluido
   el tablero de Monitor, que reusa esta misma clase). MEDIDO con la fórmula WCAG sobre los
   colores computados, no estimado:
     antes  --danger-text #e11d48 sobre --danger-bg #ffe4e6 = 3,91:1  ❌ bajo el piso AA
     ahora  #9f1239              sobre --danger-bg #ffe4e6 = 6,68:1  ✅
   La insignia se dibuja a 11,5 px con peso 700 -- "texto grande" en negrita empieza en
   18,66 px, así que el piso es 4,5:1 y no 3:1, la misma trampa que 026/T104 documentó.

   ⭐ En OSCURO ya estaba bien y NO se toca: --danger-text (#fb7185) sobre --danger-bg
   (#3a1620) mide 5,93:1 (mismo par que mide el comentario de .icon-action--danger, más
   abajo en este archivo).

   ⛔ POR QUÉ NO SE TOCA EL TOKEN: --danger-text también pinta el riel de fila roja del
   Monitor, .icon-action--danger, .qr-error y .setting-row__error; moverlo ahí arreglaría
   esto y correría media docena de cosas más. Se corrige LA VARIANTE, el mismo alcance que
   ya fijaron --error, --info, --ok y --warn en este archivo.

   ⭐ #9f1239 no es un tono nuevo elegido a ojo -- que es como nació este defecto --: es el
   mismo color que .alert--error ya usa sobre este MISMO fondo (--danger-bg), medido más
   arriba en este archivo en 6,68:1. Se reusa el par ya vetado.

   🔴 Dos selectores, no uno: el tema tiene TRES estados y el tercero es el :root SIN
   data-theme (antes de que theme.js estampe, y de nuevo tras cada navegación mejorada,
   P-065), donde manda prefers-color-scheme. Misma forma que .badge--ok/.badge--warn. */
:root[data-theme="light"] .badge--danger {
    color: #9f1239;
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .badge--danger {
        color: #9f1239;
    }
}

/* --- 023/T060: .badge--info en tema OSCURO ------------------------------------------------
   Al revés que .badge--ok/.badge--warn/.badge--danger arriba (esas fallan en claro): ésta
   falla en OSCURO. Es la MISMA trampa que .book-picker__label ya documentó más abajo en este
   archivo (026/T104): .badge--info pinta color: var(--accent); background: var(--accent-soft)
   -- el idéntico par de tokens, no uno parecido. MEDIDO con la fórmula WCAG sobre los colores
   computados:
     claro  --accent #1e40af sobre --accent-soft #eff4ff = 7,91:1  ✅  (no se toca)
     antes  --accent #3b82f6 sobre --accent-soft #1d2a44 = 3,89:1  ❌  (piso 4,5:1)
     ahora  #93c5fd          sobre --accent-soft #1d2a44 = 7,93:1  ✅

   La insignia se dibuja a 11,5 px con peso 700 (.badge, más arriba) -- "texto grande" en
   negrita empieza en 18,66 px, así que el piso es 4,5:1 y no 3:1.

   ⭐ #93c5fd no es un tono nuevo elegido a ojo: es EXACTAMENTE el mismo color que
   .book-picker__label ya reusa sobre este MISMO fondo (--accent-soft oscuro), para que las
   dos superficies de acento sobre ese fondo lean igual.

   ⛔ POR QUÉ NO SE TOCA --accent: pinta enlaces, foco, .cell-id y el borde de .book-picker en
   todo el sitio. Se corrige LA VARIANTE, el mismo alcance que ya fijaron --ok/--warn/--danger
   arriba y --info/--book-picker__label en sus propios bloques.

   🔴 Dos selectores, no uno: el tema tiene TRES estados y el tercero es el :root SIN
   data-theme (antes de que theme.js estampe, y de nuevo tras cada navegación mejorada,
   P-065), donde manda prefers-color-scheme. Misma forma que los bloques anteriores. */
:root[data-theme="dark"] .badge--info {
    color: #93c5fd;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .badge--info {
        color: #93c5fd;
    }
}

/* ---------------------------------------------------------- Icon action --- */
/* --text-2 and NOT --text-3. This is a BUTTON WITH NO TEXT -- the five per row on every
   listing -- so the glyph IS the control and the applicable floor is the 3:1 of SC 1.4.11
   (non-text contrast), not a decorative-ink allowance. Measured on /security/users:
   --text-3 was 2.56:1 in light over 402 buttons on one screen, while the SAME control read
   4.53:1 in dark and passed -- a one-theme defect, the shape this file has now hit four
   times. --text-2 is 4.76:1 light / 7.93:1 dark, which is the criterion .detail-grid
   already applies a few rules below. */
.icon-action {
    width: 32px;
    height: 32px;
    min-width: 32px;
    border: none;
    background: transparent;
    border-radius: 8px;
    cursor: pointer;
    color: var(--text-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background .12s, color .12s;
}

.icon-action:hover {
    background: #f1f5f9;
    color: var(--text-2);
}

/* 🔴 The destructive action must be distinguishable AT REST, not only on hover (ux-reviewer,
   2026-08-24). Until now .icon-action--danger had a rule ONLY on :hover, so in four listings
   — Mantenimiento Parqueos, Usuarios, Roles y permisos, Tickets — "Desactivar" was pixel for
   pixel the same mute glyph as "Editar" until the mouse happened to reach it. Same token the
   hover already used, so both themes are covered by construction (app.css defines its dark
   variant) and nothing that already worked changes. */
.icon-action--danger {
    color: var(--danger-text);
}

.icon-action--danger:hover {
    background: var(--danger-bg);
    color: var(--danger-text);
}

/* And the destructive action is SEPARATED from the ones that only edit the record. The cluster
   is grouped by intention: "editar este registro" (lápiz, ajustes) on one side, "cambiar su
   estado" on the other. Done purely in CSS — .icon-action--danger already marks which one it
   is, so no grid's markup is touched. The divider is drawn by a pseudo-element centred in the
   added gap (4px of the wrap's gap + 10px of margin = 14px, so -7px is its middle) instead of
   as a border on the 32px box, which would have hugged the glyph and collided with the hover
   fill. :not(:first-child) covers the grids where the destructive action is the only one. */
.cell-actions__wrap .icon-action--danger:not(:first-child) {
    position: relative;
    margin-left: 10px;
}

.cell-actions__wrap .icon-action--danger:not(:first-child)::before {
    content: "";
    position: absolute;
    left: -7px;
    top: 6px;
    bottom: 6px;
    width: 1px;
    background: var(--border);
}

.icon-action .msi {
    font-size: 19px;
}

/* ----------------------------------------------------------- DateField --- */
/* 022/T059, DECR-409: the calendar the owner asked for, built from tokens only (R4 --
   no third-party library), so it needs no dark-theme override block of its own: every
   color below is a variable that app.css already remaps for [data-theme="dark"]. */
.date-field {
    position: relative;
    display: block;
}

/* Feriados.razor keeps its date narrower (CssClass="input hol__date", max-width:190px in
   Feriados.razor.css) inside a modal built with .field (display:flex; flex-direction:column).
   A flex item's default align-items:stretch would still stretch THIS WRAPPER to the field's
   full width even though the <input> inside caps itself at 190px, leaving the calendar toggle
   button (position:absolute; right:4px) floating far past the visibly narrow input. Kept in
   sync with that 190px on purpose -- if Feriados' own max-width ever changes, this one has to
   move with it. */
.date-field:has(.hol__date) {
    align-self: flex-start;
    width: 190px;
}

/* Room for the toggle button so typed text (up to "13/09/2026") never runs under it. */
.date-field .input {
    padding-right: 38px;
}

.date-field__toggle {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 32px;
    height: 32px;
    /* Above ANY DateField's own backdrop (z-index 85, position:fixed inset:0) -- with two
       instances side by side (Desde/Hasta), Desde's backdrop paints over Hasta's toggle
       button while Desde's popup is open (same reasoning as the comment on .date-field__
       backdrop: position:relative alone does not give this button a stacking context, so it
       sat at the implicit z-index:0 of the root context, under the fixed backdrop). Without
       this, the first click on Hasta's icon only closed Desde's popup (022/T059 ux-reviewer
       round 2) and a second click was needed to actually open Hasta's. Still well under any
       modal (z-index 200+). */
    z-index: 87;
}

/* Same click-outside-closes idiom as .topbar__backdrop (Topbar.razor): a transparent
   full-viewport catcher UNDER the popup. Below the topbar's own z-index (91) is fine --
   this popup never has to compete with the account menu, and staying under it means a
   date field inside some future dialog still closes correctly if that dialog's own
   backdrop is also on the stack. */
.date-field__backdrop {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 85;
}

.date-field__popup {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 86;
    width: 264px;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    box-shadow: var(--shadow-card);
    padding: 10px;
    animation: fadeUp .16s ease;
}

.date-field__cal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
}

.date-field__cal-title {
    font-size: 13px;
    font-weight: 700;
    color: var(--text);
}

.date-field__cal-weekdays {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    margin-bottom: 2px;
}

.date-field__cal-weekdays span {
    text-align: center;
    font-size: 11px;
    font-weight: 700;
    color: var(--text-3);
    padding: 4px 0;
}

.date-field__cal-row {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
}

.date-field__cal-day {
    width: 100%;
    aspect-ratio: 1 / 1;
    border: 1px solid transparent;
    background: transparent;
    border-radius: var(--r-sub);
    cursor: pointer;
    font-family: inherit;
    font-size: 12.5px;
    color: var(--text);
    transition: background .12s, color .12s;
}

.date-field__cal-day:hover {
    background: var(--border-softer);
}

.date-field__cal-day--muted {
    color: var(--text-3);
}

.date-field__cal-day--today {
    border-color: var(--accent);
    font-weight: 700;
}

.date-field__cal-day--selected {
    background: var(--accent);
    color: #fff;
    font-weight: 700;
}

.date-field__cal-day--selected:hover {
    background: var(--accent-dark);
}

/* --------------------------------------------------------------- Fields --- */
.field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 14px;
}

.field__label {
    font-size: 12.5px;
    font-weight: 700;
    color: #334155;
}

/* --text-2 and NOT --text-3: measured, #94a3b8 on the card is 2.56:1, under the 4.5:1 AA floor.
   --text-2 (#64748b) measures 4.76:1 on light and 7.93:1 on dark (up from 4.53:1), so both
   themes clear AA.
   Field hints carry the safety information next to an input (units, stored format), which is
   why the per-page copies of this rule -- .vat-editor__hint, .commission-editor__hint -- were
   already moved to --text-2. This is the shared rule they diverged from. */
.field__hint {
    font-size: 11.5px;
    color: var(--text-2);
}

/* 022/T054 ux-reviewer MEDIO: when a DateField's own invalid-state hint lands next to a
   host's static field__hint (Tarifas "Vigencia desde", ListaAcceso "Desde"), both share
   class, size and color and read as one run-on line. A divider is the least invasive fix
   that needs no host markup change and touches no text: it separates the two WITHOUT
   picking a wording -- that call (replace vs. keep both) stays with the dueño. */
.field__hint + .field__hint {
    margin-top: 2px;
    padding-top: 4px;
    border-top: 1px dashed var(--border);
}

/* ---- 016-a1/T059 (ux-reviewer MEDIO): el selector de archivo -------------------------------
   El <input type="file"> nativo trae su propio botón de sistema ("Choose File"/"No file
   chosen", en el idioma del navegador) que NO sigue --text/--border/etc. de este sitio y no
   se puede traducir por CSS (es texto del navegador, no contenido nuestro). En vez de pelear
   con ::file-selector-button, se oculta el input de forma ACCESIBLE -- técnica estándar:
   dimensión de 1px y clip, nunca display:none ni visibility:hidden, que sí lo saca del árbol
   de foco -- y el <label for> de LocationProfileDialog.razor hace de botón visible, con el
   look ya temizado de .btn. El nombre del archivo elegido lo pinta el propio .razor en
   español (@_pendingPhotoName ?? "Ninguna foto elegida"), nunca el navegador. */
.file-picker {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.file-picker__input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.file-picker__input:focus-visible + .file-picker__name,
.file-picker:has(.file-picker__input:focus-visible) label.btn {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.file-picker__name {
    font-size: 13px;
    color: var(--text-2);
}

.input {
    height: 40px;
    width: 100%;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    padding: 0 12px;
    font-family: inherit;
    font-size: 13.5px;
    color: var(--text);
    background: #fff;
    transition: border-color .15s, box-shadow .15s;
}

/* NO `outline: none` here. app.css defines the site's keyboard ring once, on :focus-visible
   (2px solid var(--accent), offset 2px). This rule's specificity is (0,2,0) against that
   (0,1,0), so suppressing the outline here silently removed the ring from EVERY field in the
   site, PagedGrid's filter row included, and left form navigation with a different focus
   indicator than every other control -- a consistency defect, since the border did still
   turn accent-coloured (8.72:1 in light).
   The border tint stays on :focus (mouse and keyboard alike) as an affordance; the ring is
   the global one again. The old halo, rgba(30,64,175,.12), is gone with it: it was a fixed
   light-blue wash that is invisible against the dark theme's surfaces, so it was decoration
   in one theme only and the outline now does that job in both. */
.input:focus {
    border-color: var(--accent);
}

.check {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 600;
    color: #334155;
    cursor: pointer;
    user-select: none;
    min-height: 40px;
}

.check input {
    width: 17px;
    height: 17px;
    accent-color: var(--accent);
    cursor: pointer;
}

.form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 4px;
}

/* --------------------------------------------------------------- Alerts --- */
.alert {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding: 12px 14px;
    border-radius: var(--r-control);
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 16px;
}

.alert .msi {
    font-size: 20px;
    flex: none;
}

.alert--error {
    background: var(--danger-bg);
    color: #9f1239;
    border: 1px solid #fecdd3;
}

.alert--info {
    background: var(--accent-soft);
    color: var(--accent-dark);
    border: 1px solid #dbe4ff;
}

.alert--ok {
    background: var(--ok-bg);
    color: var(--ok-text);
    border: 1px solid var(--ok-bg);
}

.alert--warn {
    background: var(--warn-bg);
    color: var(--warn-text);
    border: 1px solid var(--warn-bg);
}

/* --- .alert--ok y .alert--warn en tema CLARO: el espejo del defecto de --info ------------
   MEDIDO en el navegador con getComputedStyle sobre elementos reales, no calculado:
     .alert--ok    --ok-text   #16a34a sobre --ok-bg   #dcfce7 = 3.00:1  ❌
     .alert--warn  --warn-text #d97706 sobre --warn-bg #fef3c7 = 2.86:1  ❌
   Las dos bajo el piso AA de 4.5:1, y las dos INVISIBLES para quien sólo mirara la columna
   oscura — que es donde miden 8.64:1 y 8.14:1, perfectas. El defecto de clase apareció tres
   veces en este archivo (--error, --info y ahora estas dos): la lección es que una variante
   se mide en LOS DOS temas o no se midió.

   ⛔ POR QUÉ NO SE TOCAN LOS TOKENS: --ok-text y --warn-text también pintan badges, chips y
   KPIs por todo el sitio. Corregirlos ahí arreglaría el aviso y movería veinte cosas más.
   Se corrige LA VARIANTE DEL AVISO, por alcance y sin !important, que es el precedente que
   ya existe en este archivo para --error (dark) y para --info (dark).

   🔴 POR QUÉ DOS SELECTORES Y NO UNO — acá es donde esto se rompe en silencio. El tema tiene
   TRES estados, no dos, y el tercero es el que ve cualquiera antes de que theme.js estampe
   (y de nuevo tras cada navegación mejorada, P-065): el :root SIN data-theme, cuyo color lo
   decide prefers-color-scheme. Un `:root:not([data-theme="dark"])` a secas cubriría también
   ese estado con el SO en oscuro, y ahí pintaría verde oscuro sobre fondo oscuro. Los dos
   selectores de abajo describen EXACTAMENTE los dos estados claros y ningún otro:
     1) el claro elegido a mano  → :root[data-theme="light"]
     2) el SO en claro sin estampar → @media (prefers-color-scheme: light) + :not(dark)
   Es la forma inversa y simétrica de la que app.css ya usa para la rama oscura.

   ⭐ CONSECUENCIA QUE IMPORTA: en TODO estado oscuro estas reglas no matchean, así que el
   color sigue saliendo de var(--ok-text)/var(--warn-text) y el oscuro NO SE PUEDE MOVER.
   Medido después del cambio para probarlo: 8.64:1 y 8.14:1, idénticos.

   Los colores elegidos (#166534 = 6.49:1, #92400e = 6.37:1) caen en la banda de la familia
   en claro, junto a --error (6.68:1); los inmediatamente más claros —#15803d (4.57:1) y
   #b45309 (4.51:1)— pasaban raspando y se descartaron por eso.
   El BORDE no se toca: en esta familia borde = color de fondo, y eso ya está bien. */
:root[data-theme="light"] .alert--ok {
    color: #166534;
}

:root[data-theme="light"] .alert--warn {
    color: #92400e;
}

@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .alert--ok {
        color: #166534;
    }

    :root:not([data-theme="dark"]) .alert--warn {
        color: #92400e;
    }
}

/* Read-only key/value grid used by the booth-charge detail and quote cards.
   The <span> is the field LABEL, so it gets content ink:
   --text-2 and NOT --text-3: measured, #94a3b8 on the card is 2.56:1, under the 4.5:1 AA floor.
   --text-2 (#64748b) measures 4.76:1 on light and 7.93:1 on dark (up from 4.53:1), so both
   themes clear AA.
   Without the label the value beside it means nothing, so this is the grid's content, not
   its metadata ink. Shared on purpose -- CobroCaseta, CobroManual, Horario, Turnos,
   OperationTrace, LocationSettings, DiagnosticMaintenance and DiagnosticFailureDetail all
   read through this one rule. */
.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 12px 18px;
}

.detail-grid > div > span {
    display: block;
    font-size: 11.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--text-2);
    margin-bottom: 2px;
}

.detail-grid > div > div {
    font-size: 14px;
    font-weight: 600;
}

/* --------------------------------------------------------------- States --- */
.state {
    padding: 44px 20px;
    text-align: center;
    color: var(--text-2);
    font-size: 13.5px;
}

.state .msi {
    font-size: 34px;
    color: var(--text-3);
    display: block;
    margin: 0 auto 10px;
}

/* "No signed-in subject" state of PagedGrid. It has to be READ as a different
   thing from an empty listing, so it carries a bold headline the empty state
   does not; the hint below says outright that nothing was queried.
   Colours come from tokens only, so both themes follow: --text and --text-2 are
   4.76:1 light / 7.93:1 dark on the card, --text-3 would be 2.56:1 and is used
   only by the decorative icon. */
.grid__no-session {
    /* Stable hook so a review can assert THIS state and not confuse it with the
       empty listing, which uses the bare .state block. */
    display: block;
}

.grid__no-session-title {
    color: var(--text);
    font-size: 14px;
    font-weight: 700;
    margin-bottom: 6px;
}

.grid__no-session-hint {
    color: var(--text-2);
    max-width: 44ch;
    margin: 0 auto;
    line-height: 1.5;
}

.spinner {
    width: 22px;
    height: 22px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    display: inline-block;
    animation: spin .7s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* ---------------------------------------------------------------- Modal --- */
.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, .5);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    z-index: 60;
    animation: fadeUp .15s ease;
}

.modal {
    background: #fff;
    border-radius: var(--r-card);
    box-shadow: 0 20px 60px rgba(15, 23, 42, .25);
    width: 100%;
    max-width: 460px;
    max-height: 90vh;
    overflow-y: auto;
    animation: fadeUp .18s ease;
}

/* Wide variant for two-panel layouts (e.g. the permission dual-list) */
.modal--wide {
    max-width: 880px;
}

/* Opt-in variant for a TALL dialog: the head and the foot stay put and the BODY scrolls.
   Without it the head, the body and the foot are all children of .modal — the only element
   with overflow-y — so scrolling a dialog that does not fit takes the title AND the "Cerrar"
   button off screen at the same time, and the backdrop click becomes the only way out.
   Precedent inside this same file: the role-permission dual list, where the scroll lives in
   .dual-list__items and that is why its head never moves. This does the same thing for a body
   that is not a fixed pair of panels. Opt-in on purpose: the short dialogs never scroll, and
   changing .modal for everybody would change every dialog of the site at once. */
.modal--scroll-body {
    display: flex;
    flex-direction: column;
    /* The panel itself must stop scrolling, or the body and the panel would both scroll. */
    overflow: hidden;
}

.modal--scroll-body > .modal__head {
    padding-bottom: 14px;
    border-bottom: 1px solid var(--border-soft);
    /* A flex child shrinks by default; the chrome must not. */
    flex: 0 0 auto;
}

.modal--scroll-body > .modal__body {
    overflow-y: auto;
    padding-top: 16px;
    /* min-height:auto is the flex default and refuses to shrink below the content, which would
       make the overflow-y above do nothing at all. This one line is what makes it scroll. */
    min-height: 0;
    flex: 1 1 auto;
}

.modal--scroll-body > .modal__foot {
    border-top: 1px solid var(--border-soft);
    padding-top: 14px;
    flex: 0 0 auto;
}

.modal__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding: 20px 22px 6px;
}

.modal__title {
    font-size: 17px;
    font-weight: 800;
    letter-spacing: -.2px;
}

.modal__subtitle {
    font-size: 12.5px;
    color: var(--text-2);
    margin-top: 3px;
}

.modal__body {
    padding: 14px 22px 4px;
}

.modal__foot {
    padding: 12px 22px 20px;
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    flex-wrap: wrap;
}

/* Read-only context row at the very top of a dialog's body — or of a long form card: states
   WHICH scope the record being created belongs to, because the dialog covers the selector
   that chose it (and a tall form scrolls it out of sight). */
.modal-context {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 12px;
    margin-bottom: 14px;
    border: 1px solid var(--card-border);
    border-left: 3px solid var(--accent);
    border-radius: var(--r-control);
    background: var(--accent-soft);
    font-size: 13px;
}

.modal-context .msi {
    font-size: 18px;
    color: var(--accent);
}

.modal-context__label {
    font-weight: 700;
    color: var(--text-2);
}

.modal-context__value {
    font-weight: 800;
    color: var(--text);
}

/* ------------------------------------------- Accounting book picker (013) --- */
/* The book selector is NOT one more filter: accounting is per parking (DECR-069), so the
   chosen book redefines every row the screen shows. It is deliberately heavier than the
   secondary controls beside it — accent frame, uppercase label, its own surface. */
.book-picker {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 12px 6px 10px;
    border: 1px solid var(--accent);
    border-radius: var(--r-control);
    background: var(--accent-soft);
}

.book-picker__label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    font-weight: 800;
    letter-spacing: .5px;
    text-transform: uppercase;
    color: var(--accent);
    white-space: nowrap;
}

.book-picker__label .msi {
    font-size: 18px;
}

/* --- 026/T104: la etiqueta del selector de libro en tema OSCURO --------------------------
   Ésta falla AL REVÉS que las otras tres de la casilla: pasa en claro y falla en oscuro.
   Medido con la fórmula WCAG sobre los colores computados:

     claro  --accent #1e40af sobre --accent-soft #eff4ff = 7,91:1  ✅  (no se toca)
     ANTES  --accent #3b82f6 sobre --accent-soft #1d2a44 = 3,89:1  ❌
     AHORA  #93c5fd          sobre --accent-soft #1d2a44 = 7,93:1  ✅

   12 px con peso 800 y mayúsculas NO es "texto grande" (eso empieza en 18,66 px en negrita),
   así que el piso es 4,5:1 y 3,89 no alcanza — aunque "se vea bien".

   ⭐ ES EXACTAMENTE LA TRAMPA QUE .alert--info ya documenta más arriba: sobre este fondo el
   token de acento del propio tema oscuro (#3b82f6) mide 3,89:1. Se reusa el MISMO #93c5fd que
   aquella corrección eligió y midió, para que las dos superficies de acento sobre
   --accent-soft lean igual. El ícono va incluido: hereda color de la etiqueta.

   ⛔ NO se toca --accent: pinta enlaces, foco, .cell-id y el borde del propio .book-picker.

   🔑 ACÁ SÍ VAN LOS DOS ESTADOS OSCUROS, y no sólo el estampado: --accent-soft se vuelve
   #1d2a44 también por prefers-color-scheme (app.css), así que en el :root sin data-theme con
   el SO en oscuro —el que se ve antes de que theme.js estampe, y otra vez tras cada navegación
   mejorada (P-065)— el defecto ocurre igual. Es el espejo de los pares claros de este archivo.
   ⚠️ HALLAZGO DERIVADO: el override de .alert--info sólo cubre el estado estampado, así que en
   ese tercer estado sigue midiendo 2,77:1. No es esta casilla; se reporta. */
:root[data-theme="dark"] .book-picker__label {
    color: #93c5fd;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]):not([data-theme="dark"]) .book-picker__label {
        color: #93c5fd;
    }
}

.book-picker__select {
    height: 36px;
    min-width: 240px;
    max-width: 320px;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    padding: 0 10px;
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text);
    background: var(--card-bg);
    cursor: pointer;
}

/* Same defect and same remedy as .input:focus above: `outline: none` on a (0,2,0) selector
   was cancelling the site's global :focus-visible ring for the accounting book selector, so
   this one control announced keyboard focus differently from the rest of the site. Found
   while fixing .input -- fixing only the reported one would leave the identical bug here.
   The halo stays: unlike .input's, it is already on var(--accent-shadow), so it is
   theme-aware and visible in both themes. */
.book-picker__select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-shadow);
}

/* -------------------------------------------------- User roles (Users.razor) */
.role-chips {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* The surface comes from --card-bg, NOT a hardcoded #fff. Measured in the "Roles del
   usuario" modal of /security/users, dark theme: a #fff pill under --text (#e8eef7) read
   1.17:1 -- the role name was not readable at all, and the .icon-action that removes the
   role sat on the same white pill at 2.69:1. This is the SAME defect the .topbar__chip
   note at the top of this file already describes and already fixed ("deja una pildora
   blanca"), so it takes the same remedy rather than a new one.
   Token, not a dark-scoped override: --card-bg is #ffffff in light, so light is byte-for-byte
   unchanged (17.85:1 before and after), and dark stops painting a light surface at all
   (14.92:1). The border was already on --border and needed nothing. */
.role-chip {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    background: var(--card-bg);
}

.role-chip__body {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 1;
    min-width: 0;
    flex-wrap: wrap;
}

.role-chip__name {
    font-weight: 700;
    font-size: 13px;
    color: var(--text);
}

/* Heading that separates the two panels of a dual-list ("Roles asignados" / "Agregar un
   rol"), the pattern the owner set as the general shape for assigning X to Y. The ink is
   --text and NOT a hardcoded slate: measured in dark, #334155 on --card-bg read 1.68:1, so
   the two panels were there with no readable way to tell which was which. --text is
   10.35:1 in light (unchanged: #0f172a replaces #334155 on white, both far over AA) and
   14.92:1 in dark. It is a heading, so it takes heading ink, not metadata ink. */
.subsection-title {
    font-size: 12.5px;
    font-weight: 700;
    color: var(--text);
    margin: 18px 0 10px;
}

.add-row {
    display: flex;
    gap: 10px;
    align-items: stretch;
}

.add-row .input,
.add-row select.input {
    flex: 1;
    min-width: 0;
}

select.input {
    appearance: auto;
    padding-right: 8px;
    cursor: pointer;
}

/* ---------------------------------------------- Dual list (Roles.razor perms) */
.dual-list {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px;
}

/* Panel body on the CARD token, not on a hardcoded #fff. --card-bg resolves to
   #ffffff in light (byte-identical to the #fff it replaces, so light is
   untouched) and to #111a2b in dark, which is also what .modal remaps to -- the
   dual list always lives inside a dialog, so the panel now sits flush with the
   dialog it is drawn on instead of being a white island on it. Using the token in
   the BASE rule, rather than adding a dark override, is what makes the panel
   follow the theme by itself. */
.dual-list__panel {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    overflow: hidden;
    min-width: 0;
    background: var(--card-bg);
}

/* Header band: same species AND now the same treatment as .data-table th --
   #fafbfc in light, remapped to --border-soft in dark down in the dark section.
   No token equals #fafbfc, so the light value stays literal exactly as the table
   header does; inventing a token for it would change every band on the site. */
.dual-list__panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 10px 14px;
    background: #fafbfc;
    border-bottom: 1px solid var(--border-soft);
}

/* The surface came first (see .dual-list__panel-head above); now the ink follows,
   which is what the previous note in this spot said had to happen and why it was
   left on --text-3 until then. Measured on the band, both themes: --text-3 was
   2.47:1 light / 3.71:1 dark; --text-2 is 4.59:1 light / 6.03:1 dark. Same ink as
   .data-table th, which is the rule this band mirrors. */
.dual-list__panel-title {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .5px;
    text-transform: uppercase;
    color: var(--text-2);
}

/* --text and NOT --text-2: this chip carries its OWN surface (--off-bg), so the
   panel fix does not reach it, and --text-2 on --off-bg measures 4.34:1 in light
   -- under the 4.5:1 floor at 11px bold, which is not large text. --text reads
   16.30:1 light / 12.76:1 dark. It is also the right token by meaning: the chip is
   a COUNT, that is data, not a metadata label. Shared with Reportes.razor, where
   the chip sits in .table-card__head; it brings its own background there too, so
   the same numbers hold. */
.dual-list__count {
    font-size: 11px;
    font-weight: 700;
    color: var(--text);
    background: var(--off-bg);
    padding: 2px 9px;
    border-radius: var(--r-pill);
}

.dual-list__items {
    list-style: none;
    margin: 0;
    padding: 6px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow-y: auto;
    max-height: 46vh;
    min-height: 220px;
}

/* --text-2 and NOT --text-3, and the rule is unchanged -- but the note that came
   with it was measured on the CARD while this text actually sits on the PANEL,
   which back then hardcoded #fff in both themes. On the real surface --text-2 read
   2.20:1 in dark, WORSE than the 3.84:1 it replaced. The panel now resolves to
   --card-bg, so the card numbers finally describe the card this text is on:
   --text-2 is 4.76:1 light / 7.93:1 dark, and --text-3 would be 2.56:1 / 4.53:1.
   Empty-state copy, same as .state above, which already reads at --text-2. */
.dual-list__empty {
    padding: 32px 16px;
    text-align: center;
    color: var(--text-2);
    font-size: 12.5px;
}

.dual-list__item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 8px 10px;
    border: none;
    background: transparent;
    border-radius: 8px;
    cursor: pointer;
    text-align: left;
    font-family: inherit;
    color: var(--text);
    transition: background .12s;
}

.dual-list__item:hover:not(:disabled) {
    background: var(--accent-soft);
}

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

.dual-list__item-body {
    flex: 1;
    min-width: 0;
}

.dual-list__item-code {
    display: block;
    font-family: ui-monospace, 'Cascadia Code', 'Consolas', monospace;
    font-weight: 700;
    font-size: 12.5px;
    color: var(--accent);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A HUMAN NAME (amenity, spot feature) is not a code: no monospace, no accent
   color, no bold -- that look is reserved for real codes (permission code,
   placeholder token). Same weight as .cell-name__text, the equivalent label in
   a data-table row (016-a2/T320). */
.dual-list__item-name {
    display: block;
    font-weight: 600;
    font-size: 13px;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dual-list__item-desc {
    display: block;
    font-size: 11.5px;
    color: var(--text-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-top: 1px;
}

.dual-list__item-arrow {
    flex: none;
    color: var(--text-3);
    display: inline-flex;
}

.dual-list__item-arrow .msi {
    font-size: 18px;
}

.dual-list__item:hover:not(:disabled) .dual-list__item-arrow {
    color: var(--accent);
}

@media (max-width: 639.98px) {
    .dual-list {
        grid-template-columns: 1fr;
    }
}

/* --------------------------------------------------------- Placeholder ---- */
.placeholder {
    max-width: 560px;
    margin: 8px 0;
    padding: 34px 30px;
    text-align: center;
}

.placeholder__icon {
    width: 56px;
    height: 56px;
    border-radius: 16px;
    background: var(--accent-soft);
    color: var(--accent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 14px;
}

.placeholder__icon .msi {
    font-size: 28px;
}

.placeholder__title {
    font-size: 17px;
    font-weight: 800;
}

.placeholder__text {
    font-size: 13.5px;
    color: var(--text-2);
    margin-top: 6px;
}

.placeholder__tag {
    display: inline-block;
    margin-top: 14px;
    font-size: 11.5px;
    font-weight: 700;
    color: var(--accent);
    background: var(--accent-soft);
    padding: 5px 12px;
    border-radius: var(--r-pill);
}

/* ----------------------------------------------------------- Topbar user -- */
.topbar__user {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    flex: none;
    position: relative;
}

/* The avatar is a <button> now; reset the native button chrome. */
.topbar__avatar {
    border: none;
    font-family: inherit;
    padding: 0;
}

/* Transparent full-screen catcher: any click outside the menu closes it. */
.topbar__backdrop {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 90;
}

/* Account dropdown, anchored under the avatar and aligned to its right edge so it
   never overflows the viewport. */
.topbar__menu-pop {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 91;
    min-width: 224px;
    max-width: calc(100vw - 24px);
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    box-shadow: var(--shadow-card);
    padding: 6px;
    animation: fadeUp .16s ease;
}

.topbar__menu-head {
    padding: 10px 12px 11px;
    border-bottom: 1px solid var(--border-soft);
    margin-bottom: 6px;
}

.topbar__menu-name {
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.topbar__menu-login {
    font-size: 11.5px;
    color: var(--text-3);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-top: 2px;
}

.topbar__menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 9px 12px;
    border: none;
    background: transparent;
    border-radius: var(--r-sub);
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-2);
    text-align: left;
    text-decoration: none;
    transition: background .15s;
}

.topbar__menu-item:hover {
    background: var(--border-softer);
}

.topbar__menu-item .msi {
    font-size: 19px;
    flex: none;
}

.topbar__menu-item--danger {
    color: var(--danger-text);
}

.topbar__menu-item:disabled {
    opacity: .65;
    cursor: not-allowed;
}

/* ---------------------------------------------------------------- Auth ----- */
.auth-shell {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: var(--app-bg);
}

.auth-card {
    width: 100%;
    max-width: 380px;
    padding: 30px 28px;
}

.auth-brand {
    display: flex;
    align-items: center;
    gap: 11px;
    margin-bottom: 22px;
}

.auth-brand__mark {
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: 10px;
    background: var(--accent);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-accent);
}

.auth-brand__mark .msi {
    font-size: 24px;
}

.auth-brand__name {
    font-size: 18px;
    font-weight: 800;
    letter-spacing: -.3px;
}

.auth-brand__sub {
    font-size: 11px;
    color: var(--side-muted);
    font-weight: 600;
    letter-spacing: .4px;
}

.auth-title {
    font-size: 16px;
    font-weight: 800;
    margin-bottom: 2px;
}

.auth-subtitle {
    font-size: 13px;
    color: var(--text-2);
    margin-bottom: 18px;
}

/* Secondary links under an auth form (forgot password, back, etc.) */
.auth-links {
    display: flex;
    justify-content: center;
    gap: 14px;
    margin-top: 16px;
    font-size: 13px;
}

.auth-links a {
    color: var(--accent);
    font-weight: 700;
    text-decoration: none;
}

.auth-links a:hover {
    text-decoration: underline;
}

.auth-linkbtn {
    background: none;
    border: none;
    padding: 0;
    color: var(--accent);
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
}

.auth-linkbtn:hover:not(:disabled) {
    text-decoration: underline;
}

.auth-linkbtn:disabled {
    color: var(--text-3);
    cursor: default;
}

/* One-time-code / TOTP input: large, spaced, centered digits */
.input--otp {
    text-align: center;
    letter-spacing: .5em;
    font-size: 20px;
    font-weight: 700;
    padding-left: .5em;
}

/* Success note (e.g. after a completed password reset) */
.auth-success {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding: 12px 14px;
    border-radius: var(--r-control);
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 16px;
    background: var(--ok-bg, #ecfdf5);
    color: var(--ok-text, #047857);
    border: 1px solid #a7f3d0;
}

.auth-success .msi {
    font-size: 20px;
    flex: none;
}

/* --------------------------------------------- Read-only field + enroll --- */
.readonly-value {
    font-size: 13.5px;
    color: var(--text);
    padding: 6px 0;
}

/* TOTP enrollment dialog: QR beside the manual key/URI */
.enroll {
    display: flex;
    gap: 18px;
    align-items: flex-start;
    flex-wrap: wrap;
}

.enroll__qr {
    flex: none;
    background: #fff;
    border: 1px solid var(--border);
    border-radius: var(--r-card);
    padding: 10px;
    line-height: 0;
}

.enroll__manual {
    flex: 1 1 220px;
    min-width: 200px;
}

.enroll__secret {
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    font-size: 15px;
    font-weight: 700;
    letter-spacing: .08em;
    word-break: break-all;
    user-select: all;
    background: var(--accent-soft);
    color: var(--accent-dark);
    border: 1px solid #dbe4ff;
    border-radius: var(--r-control);
    padding: 10px 12px;
}

.enroll__uri {
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    font-size: 11.5px;
    color: var(--text-2);
    word-break: break-all;
    user-select: all;
    background: #f8fafc;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    padding: 8px 10px;
}

.qr-error {
    font-size: 13px;
    color: #9f1239;
}

/* -------------------------------------------------------- System settings --- */
.settings-list {
    display: flex;
    flex-direction: column;
}

.setting-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    padding: 16px 18px;
    border-top: 1px solid var(--border);
}

.setting-row:first-child {
    border-top: none;
}

.setting-row__info {
    flex: 1 1 260px;
    min-width: 220px;
}

.setting-row__key {
    font-size: 14px;
    font-weight: 700;
    color: var(--text);
}

.setting-row__desc {
    font-size: 12.5px;
    color: var(--text-2);
    margin-top: 2px;
}

.setting-row__meta {
    font-size: 11.5px;
    color: var(--text-3);
    margin-top: 4px;
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    align-items: center;
}

.setting-row__error {
    font-size: 12.5px;
    color: #9f1239;
    font-weight: 600;
    margin-top: 6px;
}

.setting-row__editor {
    flex: none;
}

.setting-row__inline {
    display: flex;
    gap: 8px;
    align-items: center;
}

.setting-row__input {
    width: 120px;
}

/* Toggle switch (bool settings) */
.switch {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    user-select: none;
}

.switch:has(input:disabled) {
    cursor: not-allowed;
}

.switch__lock {
    font-size: 16px;
    color: var(--text-2);
}

.switch input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.switch__track {
    position: relative;
    width: 42px;
    height: 24px;
    flex: none;
    border-radius: 999px;
    background: var(--border);
    transition: background .15s;
}

.switch__thumb {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgba(15, 23, 42, .3);
    transition: transform .15s;
}

.switch input:checked + .switch__track {
    background: var(--accent);
}

.switch input:checked + .switch__track .switch__thumb {
    transform: translateX(18px);
}

.switch input:focus-visible + .switch__track {
    box-shadow: 0 0 0 3px rgba(30, 64, 175, .25);
}

.switch input:disabled + .switch__track {
    opacity: .55;
}

.switch__text {
    font-size: 13px;
    font-weight: 600;
    color: #334155;
    min-width: 76px;
}

/* =============================================================== Responsive */

/* Tablet and below: sidebar becomes an off-canvas drawer */
@media (max-width: 899.98px) {
    .sidebar {
        position: fixed;
        top: 0;
        left: 0;
        height: 100%;
        height: 100dvh;
        width: min(84vw, 300px) !important;
        transform: translateX(-100%);
        box-shadow: 0 10px 40px rgba(15, 23, 42, .18);
    }

    .sidebar.is-open {
        transform: none;
    }

    .sidebar-backdrop {
        display: block;
    }

    .sidebar-backdrop.is-visible {
        opacity: 1;
        visibility: visible;
    }

    .topbar__menu--desktop {
        display: none;
    }

    .topbar__menu--mobile {
        display: inline-flex;
    }

    .topbar__chip {
        display: none;
    }
}

/* Phone: tighten spacing, collapse the search box to an icon */
@media (max-width: 639.98px) {
    .admin-content {
        padding: 16px;
    }

    .topbar {
        gap: 10px;
        padding: 0 12px;
    }

    .topbar__search {
        display: none;
    }

    .topbar__search-btn {
        display: inline-flex;
    }

    .topbar__crumb {
        display: none;
    }

    .topbar__help {
        display: none;
    }

    .section-head__title {
        font-size: 20px;
    }

    .data-table th {
        padding: 10px 14px;
    }

    .data-table td {
        padding: 12px 14px;
    }

    .kpi-grid {
        grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    }
}

/* ============================================================ Theme toggle */
.topbar__theme .msi {
    font-size: 21px;
}

/* ==================================================== Paged grid (DECR-012/13) */
.grid__toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding: 14px 20px;
    border-bottom: 1px solid var(--border-soft);
}

.grid__toolbar-left,
.grid__toolbar-right {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

.grid__scroll {
    overflow-x: auto;
}

/* Column header: label + sort control */
.grid__th-inner {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

/* --text-2 and NOT --text-3, same reasoning as .icon-action: the sort control is a button
   whose only content is a glyph, so 3:1 (SC 1.4.11) applies. Measured on the header band
   (--border-soft, which resolves to #fafbfc here): --text-3 was 2.47:1 in light. --text-2
   clears it in both themes. */
.grid__sort {
    border: none;
    background: transparent;
    cursor: pointer;
    color: var(--text-2);
    display: inline-flex;
    align-items: center;
    padding: 2px;
    border-radius: 6px;
    font: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
}

.grid__sort:hover {
    color: var(--accent);
    background: var(--border-soft);
}

.grid__sort .msi {
    font-size: 16px;
}

.grid__sort.is-active {
    color: var(--accent);
}

/* Filter row */
.grid__filters th {
    padding: 8px 12px;
    background: var(--card-bg);
    border-bottom: 1px solid var(--border-soft);
    vertical-align: top;
}

.grid__filter {
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-width: 120px;
}

.grid__filter .input,
.grid__filter select.input {
    height: 32px;
    font-size: 12px;
    padding: 0 8px;
    text-transform: none;
    letter-spacing: normal;
    font-weight: 500;
}

.grid__filter-actions {
    display: flex;
    gap: 8px;
    align-items: center;
}

/* Footer: total + page size + pager */
.grid__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    flex-wrap: wrap;
    padding: 14px 20px;
    border-top: 1px solid var(--border-soft);
}

.grid__footer-left {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 12.5px;
    color: var(--text-2);
}

.grid__pagesize {
    height: 34px;
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    padding: 0 8px;
    font-family: inherit;
    font-size: 13px;
    color: var(--text);
    background: var(--card-bg);
}

.grid__pager {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    color: var(--text-2);
    font-weight: 600;
}

.grid__pager-btn {
    width: 34px;
    height: 34px;
    flex: none;
    border: 1px solid var(--border);
    background: var(--card-bg);
    color: var(--text-2);
    border-radius: var(--r-control);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background .12s, color .12s;
}

.grid__pager-btn:hover:not(:disabled) {
    background: var(--side-hover);
    color: var(--accent);
}

.grid__pager-btn:disabled {
    opacity: .45;
    cursor: not-allowed;
}

/* ================================================= Dark theme surface remaps */
:root[data-theme="dark"] .topbar {
    background: var(--card-bg);
}

:root[data-theme="dark"] .topbar__menu,
:root[data-theme="dark"] .topbar__icon-btn,
:root[data-theme="dark"] .btn:not(.btn--primary):not(.btn--danger) {
    color: var(--text);
}

:root[data-theme="dark"] .topbar__chip,
:root[data-theme="dark"] .topbar__icon-btn,
:root[data-theme="dark"] .btn:not(.btn--primary):not(.btn--danger) {
    background: var(--card-bg);
}

:root[data-theme="dark"] .topbar__menu:hover,
:root[data-theme="dark"] .topbar__icon-btn:hover,
:root[data-theme="dark"] .btn:not(.btn--primary):not(.btn--danger):hover,
:root[data-theme="dark"] .btn--ghost:hover {
    background: var(--side-hover);
}

/* The three remaps above are for NEUTRAL buttons only: .btn--primary and
   .btn--danger keep their own fill in dark, exactly as they do in light.
   The ink of the destructive fill is flipped because --danger-text is a
   LIGHT rose on dark (#fb7185): white on it resolves to 2.7:1, while
   --app-bg gives 7.0:1. Hover brightens instead of darkening, which is the
   right direction on a dark surface and keeps the ink readable. */
:root[data-theme="dark"] .btn--danger {
    color: var(--app-bg);
}

:root[data-theme="dark"] .btn--danger:hover {
    background: var(--danger-text);
    filter: brightness(1.12);
}

:root[data-theme="dark"] .topbar__search {
    background: var(--side-hover);
}

:root[data-theme="dark"] .topbar__search:focus-within {
    background: var(--card-bg);
}

:root[data-theme="dark"] .topbar__bell-dot {
    border-color: var(--card-bg);
}

:root[data-theme="dark"] .input {
    background: var(--card-bg);
    color: var(--text);
}

/* The dual-list header band is the same species as the table header and gets the
   same remap. Its PANEL needs no rule here: it uses --card-bg in its base
   declaration, so it follows the theme without an override. */
/* 026/T105 — el `:not(.cell-row-head)` NO es adorno. Sin él esta regla, que es (0,3,1), le
   gana a cualquier excepción de encabezado de FILA escrita con dos clases (y también a la
   scoped `.data-table .cell-row-head[b-xxx]`, que compila a (0,3,0)): la celda del sujeto se
   pinta con la banda del encabezado de columna, y sólo en tema oscuro. Excluirla acá arregla
   de una vez la clase compartida y toda pantalla que la use. La banda sigue siendo de los
   encabezados de COLUMNA, que es lo que esta regla siempre quiso decir. */
:root[data-theme="dark"] .data-table th:not(.cell-row-head),
:root[data-theme="dark"] .dual-list__panel-head {
    background: var(--border-soft);
}

:root[data-theme="dark"] .data-table tbody tr:hover {
    background: var(--side-hover);
}

/* 026/T967 (b), dark theme half of the hover pair right above: the frozen cell is opaque
   (see .cell-frozen's own comment) so it needs the SAME hover repaint the rest of the row
   gets in dark, not just the light-theme #fafbfc the base rule already covers. */
:root[data-theme="dark"] .data-table tbody tr:hover .cell-frozen {
    background: var(--side-hover);
}

/* 026/T102 — EL DIÁLOGO NO TENÍA LÍMITE EN TEMA OSCURO.
   El panel se repinta a --card-bg (#111a2b), que es EXACTAMENTE el mismo color de la
   tarjeta que queda detrás, y la única separación era la sombra base
   `0 20px 60px rgba(15,23,42,.25)` — azul marino sobre un fondo casi negro, es decir
   nada. Medido con la fórmula WCAG sobre los colores computados, no estimado:

     ANTES  panel #111a2b  vs  página velada por el backdrop (#0d1425) = 1,05:1  ❌
            panel #111a2b  vs  tarjeta de atrás (#111a2b)              = 1,00:1  ❌
            borde: ninguno
     AHORA  borde #74839c  vs  panel #111a2b                           = 4,53:1  ✅
            borde #74839c  vs  backdrop oscurecido (#040709)           = 5,25:1  ✅

   El piso de SC 1.4.11 para el LÍMITE de un componente es 3:1, así que el borde lo pasa
   con holgura por los dos lados. ⭐ Importa acá más que en cualquier otro de los 28
   diálogos del sitio (73 instancias de <ModalDialog> en 30 pantallas, medido) porque éste es el
   que corta el paso a un asiento irreversible: si no
   se percibe como una capa que se interpone, no cumple su función de freno.

   ⛔ POR QUÉ NO --border NI --card-border: son los hairlines del tema y miden 1,48:1 y
   menos sobre el panel — el borde "de regla" NO alcanza el mínimo acá. --text-3 (#74839c)
   es el color MÁS APAGADO que la hoja ya define que sí pasa 3:1; los intermedios que
   existen (#33415e, el pulgar de la barra) miden 1,71:1 y se descartaron por medidos.

   ⛔ POR QUÉ NO SE SUBE UN ESCALÓN EL FONDO DEL PANEL: pasarlo a --side-hover (#1b2740)
   lo deja igual en 1,23:1 contra el backdrop —sigue lejos de 3:1— y encima rompe algo que
   este archivo hizo A PROPÓSITO: .dual-list__panel usa --card-bg justamente para quedar al
   ras del diálogo (ver el comentario de ese bloque). Se corrige el LÍMITE, que es lo que
   la norma pide, y no la superficie.

   El tema CLARO no se toca: ahí la misma sombra sí lee sobre #eef2f7 y el panel blanco ya
   se distingue del fondo. Por eso esto vive en el bloque oscuro y en ningún otro lado. */
:root[data-theme="dark"] .modal {
    background: var(--card-bg);
    border: 1px solid var(--text-3);
    /* Negro y no azul marino: es el mismo patrón que --shadow-card ya usa en oscuro
       (rgba(0,0,0,.45)); una sombra navy sobre #0b1220 es invisible por definición. */
    box-shadow: 0 24px 60px rgba(0, 0, 0, .6);
}

/* El velo también: rgba(15,23,42,.5) sobre --app-bg #0b1220 casi no oscurece (queda
   #0d1425), así que el "hay algo encima" se perdía. Con negro al 60 % la página baja a
   #040709 y el salto al panel se nota; el borde de arriba es el que carga con el 3:1. */
:root[data-theme="dark"] .modal-backdrop {
    background: rgba(0, 0, 0, .6);
}

:root[data-theme="dark"] .field__label,
:root[data-theme="dark"] .check,
:root[data-theme="dark"] .switch__text,
:root[data-theme="dark"] .btn:not(.btn--primary):not(.btn--danger) {
    color: var(--text);
}

:root[data-theme="dark"] .kpi__icon--ok {
    background: var(--ok-bg);
}

:root[data-theme="dark"] .alert--error {
    color: var(--danger-text);
    border-color: var(--danger-bg);
}

/* The same class-wide defect that .alert--error above fixes, on the ONLY variant that
   was left without its dark override — and it is the one used the most (83 usos en 46
   componentes, medido 2026-08-30).
   MEDIDO con la fórmula WCAG sobre los colores computados, no estimado:
     antes  --accent-dark #2563eb sobre --accent-soft #1d2a44 = 2.77:1  ❌ bajo el piso AA
     ahora  #93c5fd            sobre --accent-soft #1d2a44 = 7.94:1  ✅
   Los hermanos en oscuro miden 8.14:1 (--warn), 8.64:1 (--ok) y 5.93:1 (--error), así que
   7.94:1 deja a la familia pareja.
   ⛔ NO se usa var(--accent) (#3b82f6): mide 3.89:1 sobre este fondo — mejora, pero SIGUE
   bajo 4.5:1. Es la trampa exacta de este bloque: el token que "se ve bien" no basta.
   El borde: la regla base escribe #dbe4ff a mano, sin token, y en oscuro eso da 13.72:1
   contra la tarjeta cuando TODOS los demás hairlines miden 1.48:1 — un marco brillante
   alrededor del texto más apagado de la pantalla. Vuelve al patrón de la familia, que es
   borde = color de fondo (lo que ya hacen --ok, --warn y el override de --error).
   El tema claro NO se mueve: ahí .alert--info mide 9.85:1 y está bien. */
:root[data-theme="dark"] .alert--info {
    color: #93c5fd;
    border-color: var(--accent-soft);
}

:root[data-theme="dark"] .auth-success {
    border-color: var(--ok-bg);
}

:root[data-theme="dark"] .enroll__uri {
    background: var(--side-hover);
}

:root[data-theme="dark"] .enroll__secret {
    color: #93c5fd;
    border-color: var(--accent-soft);
}

:root[data-theme="dark"] .qr-error,
:root[data-theme="dark"] .setting-row__error {
    color: var(--danger-text);
}

/* Neutral icon actions only: .icon-action--danger keeps its own rose hover in
   dark, exactly as it does in light. Scoped with :not(), not with weight — the
   same fix already applied to .btn above. Unlike the destructive BUTTON, the ink
   is NOT flipped here: the icon hover fills with --danger-bg, which in dark is a
   deep maroon (#3a1620), so --danger-text (#fb7185) reads on it at 5,93:1. */
:root[data-theme="dark"] .icon-action:not(.icon-action--danger):hover {
    background: var(--side-hover);
    color: var(--text-2);
}

:root[data-theme="dark"] .scroll::-webkit-scrollbar-thumb {
    background: #33415e;
    background-clip: content-box;
}

/* Theme-persistence error toast (topbar) */
.topbar__toast {
    position: fixed;
    top: 74px;
    right: 20px;
    z-index: 80;
    max-width: 380px;
    margin: 0;
    align-items: center;
    box-shadow: 0 10px 30px rgba(15, 23, 42, .2);
}

.topbar__toast .icon-action {
    margin-left: auto;
}

/* -------------------------------------------------- Account / password --- */

/* The avatar and sidebar user card are now links to "Mi cuenta"; keep them
   looking like the previous button (no underline, inherited color). */
.topbar__avatar {
    text-decoration: none;
}

.sidebar__user {
    text-decoration: none;
}

.sidebar__user:hover {
    background: rgba(148, 163, 184, .2);
}

/* Card heading reused by the "Mi cuenta" cards. */
.card-title {
    font-size: 15px;
    font-weight: 800;
    margin-bottom: 14px;
}

/* "Mi cuenta" responsive layout: profile + contact side by side, password full. */
.account-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: var(--grid-gap);
    align-items: start;
}

.account-grid__full {
    grid-column: 1 / -1;
}

/* Change-password: fields beside the live rules checklist, wraps on narrow. */
.account-pwd {
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
    align-items: flex-start;
    margin-bottom: 4px;
}

.account-pwd__fields {
    flex: 1 1 280px;
    min-width: 240px;
}

.account-pwd__rules {
    flex: 1 1 260px;
    min-width: 220px;
}

/* Live password-policy checklist (PasswordRules.razor). */
.pwd-rules {
    background: var(--accent-soft);
    border: 1px solid var(--border);
    border-radius: var(--r-control);
    padding: 12px 14px;
}

.pwd-rules__title {
    font-size: 12px;
    font-weight: 700;
    color: var(--text-2);
    margin-bottom: 8px;
}

.pwd-rules__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* --text-2 and NOT --text-3: measured, #94a3b8 on the card is 2.56:1, under the 4.5:1 AA floor.
   --text-2 (#64748b) measures 4.76:1 on light and 7.93:1 on dark (up from 4.53:1), so both
   themes clear AA.
   The unmet rules are the ones the user is reading while typing the password; .pwd-rule--ok
   already jumps to --text, so --text-3 was dimming exactly the state that has to be read.
   The paired .msi moves with it: it is a state marker, not a watermark, and at 2.56:1 it also
   missed the 3:1 non-text floor. */
.pwd-rule {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12.5px;
    color: var(--text-2);
    transition: color .15s;
}

.pwd-rule .msi {
    font-size: 18px;
    color: var(--text-2);
    transition: color .15s;
}

.pwd-rule--ok {
    color: var(--text);
}

.pwd-rule--ok .msi {
    color: var(--ok-text);
}

/* ------------------------------------------- Report catalogue (009-reports) */
/* One card per module (reuses .card/.table-card__head) holding a list of report
   links. Only the row itself is new: everything else (card, head, count pill,
   badge, arrow) reuses existing design-system classes. */
.report-group {
    margin-bottom: var(--grid-gap);
}

.report-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px;
}

.report-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 12px;
    border-radius: var(--r-control);
    text-decoration: none;
    color: var(--text);
    transition: background .12s;
}

.report-item:hover {
    background: var(--accent-soft);
}

.report-item:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.report-item:hover .dual-list__item-arrow {
    color: var(--accent);
}

.report-item__icon {
    flex: none;
    width: 40px;
    height: 40px;
    border-radius: var(--r-kpi-icon);
    background: var(--accent-soft);
    color: var(--accent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.report-item:hover .report-item__icon {
    background: var(--card-bg);
}

.report-item__icon .msi {
    font-size: 21px;
}

.report-item__body {
    flex: 1;
    min-width: 0;
}

.report-item__name {
    display: block;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--text);
}

.report-item__desc {
    display: block;
    font-size: 12.5px;
    color: var(--text-2);
    margin-top: 2px;
}

.report-item__badge {
    flex: none;
}

@media (max-width: 639.98px) {
    .report-item__badge {
        display: none;
    }
}

/* ------------------------------------------------------- Quick Search panel */
/* A right-side drawer opened from the topbar. Backdrop shades the page; the panel
   slides in from the right. Both use a visibility transition so they animate in AND
   out and leave nothing focusable while closed. */
.quicksearch__backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, .5);
    z-index: 200;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--motion), visibility var(--motion);
}

.quicksearch.is-open .quicksearch__backdrop {
    opacity: 1;
    visibility: visible;
}

.quicksearch__panel {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    height: 100dvh;
    width: 400px;
    max-width: 100vw;
    z-index: 201;
    display: flex;
    flex-direction: column;
    background: var(--card-bg);
    border-left: 1px solid var(--border);
    box-shadow: -14px 0 40px rgba(15, 23, 42, .22);
    transform: translateX(100%);
    visibility: hidden;
    transition: transform .28s cubic-bezier(.4, 0, .2, 1), visibility .28s;
}

.quicksearch.is-open .quicksearch__panel {
    transform: translateX(0);
    visibility: visible;
}

.quicksearch__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 15px 18px;
    border-bottom: 1px solid var(--border-soft);
    flex: none;
}

.quicksearch__title {
    font-size: 16px;
    font-weight: 800;
    letter-spacing: -.3px;
}

.quicksearch__body {
    flex: 1;
    overflow-y: auto;
    padding: 18px;
}

.quicksearch__submit {
    width: 100%;
    margin-top: 6px;
}

.quicksearch__results {
    margin-top: 20px;
    border-top: 1px solid var(--border-soft);
    padding-top: 16px;
}

/* --text-2 and NOT --text-3: measured, #94a3b8 on the card is 2.56:1, under the 4.5:1 AA floor.
   --text-2 (#64748b) measures 4.76:1 on light and 7.93:1 on dark (up from 4.53:1), so both
   themes clear AA. The panel paints itself with --card-bg in both themes, so the card numbers
   are the right ones here.
   Same species as .data-table th and .detail-grid__label, and it is the only thing naming what
   the list under it contains -- content, not metadata ink. */
.quicksearch__results-title {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .5px;
    color: var(--text-2);
    margin-bottom: 10px;
}

/* --text-2 and NOT --text-3: measured, #94a3b8 on the card is 2.56:1, under the 4.5:1 AA floor.
   --text-2 (#64748b) measures 4.76:1 on light and 7.93:1 on dark (up from 4.53:1), so both
   themes clear AA.
   Empty-state copy tells the staffer WHY there is nothing here, and the site's canonical empty
   state (.state) already reads at --text-2. Same species, same ink. */
.quicksearch__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    text-align: center;
    padding: 26px 12px;
    color: var(--text-2);
    font-size: 13px;
}

.quicksearch__empty .msi {
    font-size: 26px;
    opacity: .8;
}

.quicksearch__result {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 12px;
    border-radius: var(--r-sub);
    color: var(--text-2);
    text-decoration: none;
    transition: background .15s;
}

.quicksearch__result:hover {
    background: var(--border-softer);
}

.quicksearch__result-icon {
    width: 34px;
    height: 34px;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--r-sub);
    background: var(--accent-soft);
    color: var(--accent);
}

.quicksearch__result-body {
    flex: 1;
    min-width: 0;
}

.quicksearch__result-title {
    display: block;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --text-2 and NOT --text-3: measured, #94a3b8 on the card is 2.56:1, under the 4.5:1 AA floor.
   --text-2 (#64748b) measures 4.76:1 on light and 7.93:1 on dark (up from 4.53:1), so both
   themes clear AA.
   This subtitle is not decoration: two results can share a title, and the subtitle is the only
   thing that tells them apart before the staffer clicks one. */
.quicksearch__result-subtitle {
    display: block;
    font-size: 12px;
    color: var(--text-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.quicksearch__result-arrow {
    flex: none;
    color: var(--text-3);
}
