/* ═══════════════════════════════════════════════════════════════════════
 * main.bundle.css — GENERADO POR App\Support\CssBundler — NO EDITAR A MANO
 * Generado: 2026-10-29 22:47:42
 * Fuente:   public/assets/css/main.css (+ @imports recursivos)
 * Archivos: ["main.css","tokens.css","../fonts/fonts.css","reset.css","composition.css","utilities.css","blocks/links.css","blocks/button.css","blocks/card.css","blocks/form.css","blocks/table.css","blocks/badge.css","blocks/alert.css","blocks/toast.css","blocks/modal.css","blocks/dropdown.css","blocks/avatar.css","blocks/spinner.css","blocks/empty-state.css","blocks/nav-sidebar.css","blocks/nav-bottom.css","blocks/app-header.css","blocks/error-page.css","blocks/datepicker.css","blocks/filter-bar.css","blocks/page-header.css","blocks/stat-card.css","blocks/tab-filter.css","blocks/list-toolbar.css","blocks/form-footer.css","blocks/socio-row.css","blocks/pagination.css","blocks/drawer.css","blocks/lock-screen.css","blocks/app-footer.css","blocks/progress-bar.css","blocks/nav-submenu.css","blocks/notifications.css","blocks/tabs.css","blocks/file-upload.css","blocks/layout-cliente.css","blocks/descriptions.css","blocks/switch.css","blocks/impersonation-banner.css","blocks/banner-2fa-gracia.css","blocks/login-admin.css","blocks/logo-marca.css","blocks/diagnostico.css","blocks/cron-comando.css","blocks/combo.css","blocks/pull-refresh.css","blocks/offline-banner.css","blocks/pagos.css","blocks/casos.css","blocks/avisos-admin.css","blocks/avisos-cliente.css","blocks/ajustes.css","blocks/codigo.css","blocks/doble-factor.css","blocks/requisitos-clave.css","blocks/permisos.css","blocks/importar.css","blocks/dashboard.css","blocks/barra.css","blocks/ayuda.css","blocks/transicion-paginas.css","theme-dark.css","proyecto.css","blocks/selector-explotacion.css","blocks/tarjetas-jaula.css","blocks/historia-animal.css","blocks/plan-trabajo.css","blocks/indices.css","blocks/licencia.css","blocks/documento-salida.css","blocks/libro-registro.css"]
 * ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   main.css — Entry point CSS
   Importa todas las capas en el orden correcto de CUBE CSS.
   El orden es crítico — no alterar sin revisar las implicaciones de cascada.

   Orden: tokens → reset → composition → utilities → blocks
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1. Design tokens — custom properties base del sistema */

/* ── tokens.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   tokens.css — Design tokens del proyecto
   Basado en Ant Design v5 (Daybreak Blue + Sunset Orange).
   ═══════════════════════════════════════════════════════════════════════════ */


/* ── fonts.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   fonts.css — Inter auto-hospedado (fuente única, sans-serif)
   Subsets: latin + latin-ext. Pesos: 400, 500, 600, 700.
   Origen: Google Fonts. Auto-hospedado para CSP estricto y offline en Capacitor.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/inter-400-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/inter-400-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../fonts/inter-500-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url('../fonts/inter-500-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../fonts/inter-600-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('../fonts/inter-600-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../fonts/inter-700-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('../fonts/inter-700-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}



:root {

    /* Los controles del navegador (calendario de las fechas, barras de
       desplazamiento, casillas, menús de un select) en claro; el tema
       oscuro lo cambia a `dark` (theme-dark.css). */
    color-scheme: light;

    /* ── Paleta — Ant Design v5 ──────────────────────────────────────────── */

    /* Primary — Daybreak Blue. La marca es #1677FF; --color-primary es su
       tono «fuerte», el que cumple AA (4,5:1) con texto blanco encima y como
       texto sobre las superficies claras (#1677FF daba 4,1:1). Lo calcula
       App\Support\ColorDeMarca y _base.html.twig lo vuelve a fijar desde el
       ajuste branding.color_primary: estos son los de #1677FF. Hover y activo,
       más oscuros, para que el texto blanco siga en AA. */
    --color-primary:        #1369E0;
    --color-primary-hover:  #115CC5;
    --color-primary-active: #0E50AA;
    --color-primary-soft:   #E6F4FF;    /* blue-1 */
    --color-primary-border: #91CAFF;    /* blue-3 */
    /* Texto (y iconos) en el color de marca: el fuerte en claro; en oscuro,
       uno claro (theme-dark.css y _base.html.twig). */
    --color-primary-text:   var(--color-primary);

    /* Estados semánticos — paleta AntD. El color base, para lo que no es
       texto (iconos, puntos, bordes); -text para el texto (4,5:1 sobre el
       blanco, el gris de fondo y su -soft) y -strong de fondo con texto blanco
       (botones, etiquetas sólidas). El tono AntD como texto se quedaba en
       1,5-3,3:1. */
    --color-success:        #52C41A;
    --color-success-hover:  #73D13D;
    --color-success-soft:   #F6FFED;
    --color-success-text:   #237804;    /* green-8 */
    --color-success-strong: #237804;
    --color-success-strong-hover: #135200;

    --color-warning:        #FAAD14;
    --color-warning-hover:  #FFC53D;
    --color-warning-soft:   #FFFBE6;
    --color-warning-text:   #9A5B00;    /* entre gold-8 y gold-9 */

    --color-danger:         #FF4D4F;
    --color-danger-hover:   #FF7875;
    --color-danger-soft:    #FFF1F0;
    --color-danger-text:    #CF1322;    /* red-7 */
    --color-danger-strong:  #CF1322;
    --color-danger-strong-hover: #A8071A;

    --color-info:           var(--color-primary);
    --color-info-soft:      var(--color-primary-soft);
    --color-info-text:      var(--color-primary-text);

    /* Fondos / superficies */
    --color-bg:             #FFFFFF;
    --color-bg-alt:         #FAFAFA;     /* gris-1 AntD */
    --color-bg-layout:      #F0F2F5;     /* fondo del body — AntD Pro Classic (más contraste con cards) */
    --color-bg-elevated:    #FFFFFF;

    /* Sidebar dark — AntD Pro */
    --color-sidebar-bg:         #001529;
    --color-sidebar-bg-darker:  #000C17;
    --color-sidebar-text:       rgba(255, 255, 255, 0.88);
    --color-sidebar-text-muted: rgba(255, 255, 255, 0.55);
    --color-sidebar-border:     rgba(255, 255, 255, 0.08);
    --color-sidebar-hover:      rgba(255, 255, 255, 0.08);
    --color-sidebar-active:     var(--color-primary);

    /* Bordes */
    --color-border:         #D9D9D9;
    --color-border-subtle:  #E8E8E8;
    --color-border-strong:  #BFBFBF;
    /* Borde de un control (campo, select, casilla, interruptor apagado): lo
       que lo distingue de su fondo, 3:1 (WCAG 1.4.11) sobre el blanco y el
       gris de fondo. --color-border (1,4:1) queda para separadores y tarjetas. */
    --color-border-control: #858585;

    /* Texto — alpha-on-black (patrón AntD, modo oscuro friendly) */
    --color-text:           rgba(0, 0, 0, 0.88);    /* primary */
    --color-text-muted:     rgba(0, 0, 0, 0.65);    /* secondary */
    --color-text-subtle:    rgba(0, 0, 0, 0.45);    /* placeholders, disabled e iconos: 3,4:1, no para texto */
    --color-text-on-dark:   #FFFFFF;

    /* ── Tipografía — Inter (fuente única sans) ──────────────────────────── */
    --font-sans:
        'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
        'Helvetica Neue', Arial, sans-serif;

    --font-body:    var(--font-sans);
    --font-heading: var(--font-sans);

    /* Escala de tamaños */
    --font-size-xs:    0.75rem;    /* 12px */
    --font-size-sm:    0.875rem;   /* 14px */
    --font-size-base:  0.875rem;   /* 14px — AntD usa 14 como base */
    --font-size-md:    1rem;       /* 16px */
    --font-size-lg:    1.125rem;   /* 18px */
    --font-size-xl:    1.25rem;    /* 20px — AntD heading-3 */
    --font-size-2xl:   1.5rem;     /* 24px — AntD heading-2 */
    --font-size-3xl:   1.875rem;   /* 30px — AntD heading-1 */

    /* Pesos */
    --font-weight-regular:  400;
    --font-weight-medium:   500;
    --font-weight-semibold: 600;
    --font-weight-bold:     700;

    /* Line heights */
    --line-height-tight:   1.3;
    --line-height-base:    1.5715;   /* line-height de AntD */

    --letter-spacing-wide: 0.05em;

    /* ── Espaciados ──────────────────────────────────────────────────────── */
    --space-xs:   4px;
    --space-sm:   8px;
    --space-md:   16px;
    --space-lg:   24px;
    --space-xl:   32px;
    --space-2xl:  48px;
    --space-3xl:  64px;

    /* ── Radius — AntD v5 (sutiles, menos generosos) ─────────────────────── */
    --radius-sm:  4px;      /* chips, badges */
    --radius-md:  6px;      /* inputs, botones */
    --radius-lg:  8px;      /* cards, modales */
    --radius-xl:  12px;     /* elementos grandes */
    --radius-full: 999px;   /* pills, avatares */

    /* ── Sombras — AntD v5 (neutras, no tintadas) ────────────────────────── */
    --shadow-sm:
        0 1px 2px 0 rgba(0, 0, 0, 0.05),
        0 1px 6px -1px rgba(0, 0, 0, 0.04),
        0 2px 4px 0 rgba(0, 0, 0, 0.03);

    --shadow-md:
        0 6px 16px 0 rgba(0, 0, 0, 0.08),
        0 3px 6px -4px rgba(0, 0, 0, 0.12),
        0 9px 28px 8px rgba(0, 0, 0, 0.05);

    --shadow-lg:
        0 6px 16px 0 rgba(0, 0, 0, 0.08),
        0 9px 28px 8px rgba(0, 0, 0, 0.05),
        0 12px 48px 16px rgba(0, 0, 0, 0.03);

    --shadow-xl: var(--shadow-lg);

    /* Focus ring — AntD usa 2px solid primary con soft outline */
    --focus-ring: 0 0 0 2px var(--color-primary-soft);

    /* ── Transiciones ────────────────────────────────────────────────────── */
    --transition-fast:   0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
    --transition-normal: 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
    --transition-slow:   0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
    /* Curva de entrada (AntD motionEaseOut): rápida al principio, suave al
       final. La usa el cambio de página del panel (transicion-paginas.css). */
    --ease-out:          cubic-bezier(0.215, 0.61, 0.355, 1);

    /* ── Layout ──────────────────────────────────────────────────────────── */
    --header-height:           56px;
    --bottom-nav-height:       64px;
    --footer-height:           40px;  /* aprox del .app-footer — usado por overlay de modal/drawer */
    --sidebar-width:           240px;
    --sidebar-width-collapsed: 64px;
    --content-max-width:       1280px;
    --form-max-width:          480px;

    --container-xs:            28rem;
    --container-sm:            32rem;
    --container-md:            40rem;
    --container-lg:            48rem;
    --container-xl:            var(--content-max-width);

    /* Breakpoints. Desde el ancho, con min-width (768px ya es md). Lo que
       es solo para debajo, con max-width un pelo menos: 47.99rem, no 48rem,
       que a 768px exactos aplicaría a la vez lo de móvil y lo de escritorio
       (el panel se quedaba con la cabecera y el contenido en la columna de
       64px del menú plegado). */
    --bp-sm:                   40rem;
    --bp-md:                   48rem;
    --bp-lg:                   64rem;
    --bp-xl:                   80rem;

    /* ── Capas (z-index) ─────────────────────────────────────────────────── */
    /* De abajo arriba (diseno.md §«Capas (z-index)»). Un bloque usa la capa de su
       papel, nunca un número suelto (salvo -1, 0 o 1 dentro de su propia
       caja). El menú del móvil, los modales y los cajones, por encima de los
       avisos fijos de arriba: en el móvil, el aviso de la 2FA tapaba el título
       y la X del cajón de la ayuda, y la marca del menú abierto. */
    --z-pegado:           20;  /* lo que se queda pegado al desplazar el contenido: las pestañas */
    --z-franja-segura:    50;  /* la franja de la zona segura de abajo (móvil), bajo el pie fijo */
    --z-pie-fijo:         60;  /* el pie fijo de un formulario (móvil) */
    --z-desplegable:      70;  /* menús desplegables y combos, sobre el pie fijo */
    --z-franja-estado:   100;  /* la franja de la barra de estado del portal (app), sobre su contenido */
    --z-aviso-fijo:      400;  /* los avisos fijos de arriba: la 2FA en su plazo; la suplantación, una más */
    --z-menu-lateral:    450;  /* el menú lateral del móvil (su fondo, una menos): un cajón, sobre los avisos */
    --z-modal:           500;
    --z-cajon:           600;
    --z-modal-encima:    700;  /* sobre otros modales y cajones: la confirmación global, la vista previa de un aviso */
    --z-sin-conexion:   1000;  /* «Sin conexión», sobre todo lo anterior: lo que se haga no llegará */
    --z-toast:          1100;
    --z-bloqueo:        1200;  /* la pantalla de bloqueo de la app: no se usa nada sin pasar por ella */
    --z-progreso:       9999;  /* la barra de carga global y «tirar para recargar» */
}

/* Fondo del body — gris muy suave tipo AntD Pro */
body {
    background-color: var(--color-bg-layout);
}



/* 2. Reset — normalización mínima del navegador */

/* ── reset.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   reset.css — Reset mínimo y estilos base
   Solo corrige comportamientos del navegador, sin opinión de diseño.
   ═══════════════════════════════════════════════════════════════════════════ */

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

* {
    margin: 0;
    padding: 0;
}

html {
    /* Escala fluida de tipografía base */
    font-size: 100%;
    -webkit-text-size-adjust: 100%;
    hanging-punctuation: first last;
    -webkit-tap-highlight-color: transparent;   /* quita el flash azul al tocar en Android */
}

/* Scroll lock — el body no scrollea cuando hay un modal/drawer abierto.
   Se aplica via JS añadiendo la clase cuando un overlay se hace visible. */
html.is-scroll-locked,
html.is-scroll-locked body {
    overflow: hidden;
    touch-action: none;
    overscroll-behavior: contain;
}

/* Accesibilidad — respeta la preferencia del sistema operativo de reducir animaciones */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Focus-visible consistente en todos los controles interactivos.
   Solo aparece con navegación por teclado (no con clicks de ratón). */
:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Anti-flash en restauración de scroll tras F5 o atrás/adelante.
   El atributo lo pone el script previo al pintado (_base.html.twig) si hay
   una posición que restaurar, y lo quita __restaurarScroll() en cuanto el
   layout ha leído el main y lo ha recolocado: normalmente antes del primer
   pintado, así que no llega a verse. Solo cubre que el navegador pinte el
   main a medio leer, en la posición equivocada. */
html[data-scroll-restoring] .layout-app__main,
html[data-scroll-restoring] .layout-cliente__main {
    visibility: hidden;
}

/* (Revisado) Page fade-in entre navegaciones — descartado.
   Causaba un flash desagradable porque el sidebar/header se pintaban primero
   con el main aún en opacity:0. AntD Pro v5+, Linear, GitHub no animan
   transiciones de página justamente por esto. La velocidad percibida gana. */

body {
    font-family: var(--font-body);
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-regular);
    color: var(--color-text);
    background-color: var(--color-bg);
    line-height: var(--line-height-base);
    /* min-height emparejado con el layout: svh (smallest viewport).
       Con `100dvh` (dinámico), cuando la URL bar del browser se retraía,
       dvh crecía y body se hacía más alto que .layout-app (que ahora es
       100svh fijo). Resultado: body overflowea → scroll root activo →
       el usuario scrolleaba todo el body arriba/abajo, el header "se
       escondía". Con 100svh emparejado, body y layout son exactamente
       del mismo tamaño → no hay overflow root → solo scrollea main. */
    min-height: 100dvh;   /* fallback */
    min-height: 100svh;   /* moderno, estable */
    overflow: hidden;     /* blindaje extra: nunca scroll a nivel root */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    /* Safe-area strip en mobile — ver body::after abajo */
}

@media (max-width: 47.99rem) {
    body::after {
        content: '';
        position: fixed;
        inset-inline: 0;
        inset-block-end: 0;
        block-size: env(safe-area-inset-bottom, 0);
        background-color: var(--color-bg-elevated);
        pointer-events: none;
        z-index: var(--z-franja-segura);
    }

    /* Dentro de Capacitor WebView nativo, la NavigationBar del plugin
       Capacitor maneja el color de la zona inferior. La strip web sería
       redundante (y podría no coincidir en color con la nav nativa), así
       que la ocultamos. Se mantiene el safe-area respect en layouts
       (calc(svh - env)) — solo el pintado visual delegado al plugin. */
    html[data-capacitor="true"] body::after {
        display: none;
    }
}

/* Patrón genérico: elementos marcados con `data-capacitor-hide` no se
   renderizan dentro de la APK nativa (cookie capacitor=1 + atributo
   data-capacitor en <html>). Uso típico: enlaces a áreas que la app móvil
   no cubre (ej. "Ir al panel admin" — la APK es solo para cliente). */
html[data-capacitor="true"] [data-capacitor-hide] {
    display: none;
}

/* La regla de anti-flash cold start APK (html[data-capacitor] azul hasta
   data-alpine-ready) vive INLINE en _base.html.twig <style>. Moverla al
   bundle dejaba una ventana donde el body pintaba con el color del CSS
   inline (#F0F2F5 gris pálido) entre splash y bundle-load → flash
   blanco perceptible en cold start Android. Inline gana esa carrera. */

/* ── Jerarquía de títulos — semántica acorde a docs/diseno.md punto 2 ── */
/* Jerarquía AntD — todos los títulos en Inter semibold.
   Escala más compacta que antes, base 14px. */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-semibold);
    line-height: var(--line-height-tight);
    color: var(--color-text);
}

h1 { font-size: var(--font-size-3xl); }   /* 30px */
h2 { font-size: var(--font-size-2xl); }   /* 24px */
h3 { font-size: var(--font-size-xl); }    /* 20px */
h4 { font-size: var(--font-size-lg); }    /* 18px */
h5 { font-size: var(--font-size-md); }    /* 16px */
h6 { font-size: var(--font-size-base); }  /* 14px */

/* Texto pequeño semántico */
small {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* Code inline */
code, kbd, samp {
    font-family: var(--font-mono, ui-monospace, 'SF Mono', 'Cascadia Mono', 'Roboto Mono', Consolas, monospace);
    font-size: 0.9em;
}

/* Imágenes y medios responsivos por defecto */
img,
picture,
video,
canvas,
svg {
    display: block;
    max-width: 100%;
}

/* Formularios heredan tipografía */
input,
button,
textarea,
select {
    font: inherit;
}

/* Evitar desbordamiento de texto en elementos de bloque */
p,
h1,
h2,
h3,
h4,
h5,
h6 {
    overflow-wrap: break-word;
}

/* Eliminar estilos de lista cuando se usan con role="list" */
ul[role='list'],
ol[role='list'] {
    list-style: none;
}

/* ── Focus visible global — accesibilidad WCAG ─────────────────────────────
   Outline solo cuando el usuario navega con teclado (no con click de ratón).
   Color del primary para mantener coherencia con la marca.                  */
:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}



/* 3. Composition — layouts y estructuras espaciales */

/* ── composition.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   composition.css — CUBE CSS: Composition
   Define estructuras de layout reutilizables.
   No aplica colores, tipografía ni estética — solo disposición espacial.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Contenedor centrado ─────────────────────────────────────────────────── */
.container {
    max-width: none;
    margin-inline: auto;
    padding-inline: var(--space-md);
}

@media (min-width: 48rem) {
    .container {
        padding-inline: var(--space-lg);
    }
}

/* Anchos canónicos — solo se aplican si la página los pide explícitamente
   (auth, perfil, confirmaciones). Por defecto el container es full-width
   estilo AntD Pro (aprovecha todo el ancho entre sidebar y borde). */

/* Anchos canónicos del container */
.container[data-width="xs"] { max-width: var(--container-xs); }
.container[data-width="sm"] { max-width: var(--container-sm); }
.container[data-width="md"] { max-width: var(--container-md); }
.container[data-width="lg"] { max-width: var(--container-lg); }
.container[data-width="xl"] { max-width: var(--container-xl); }

/* Padding vertical del container — para páginas que lo necesiten sin hijos adicionales */
.container[data-pad-y="md"]  { padding-block: var(--space-md); }
.container[data-pad-y="lg"]  { padding-block: var(--space-lg); }
.container[data-pad-y="xl"]  { padding-block: var(--space-xl); }
.container[data-pad-y="2xl"] { padding-block: var(--space-2xl); }

/* ── Layout de aplicación con sidebar ───────────────────────────────────── */
.layout-app {
    display: grid;
    grid-template-columns: var(--sidebar-width) 1fr;
    grid-template-rows: var(--header-height) 1fr auto;
    grid-template-areas:
        "sidebar header"
        "sidebar main"
        "sidebar footer";
    /* Layout altura estable + respeta safe-area inferior.
         · svh (no dvh): el layout NO se resizea cuando Chrome Mobile /
           iOS Safari esconden la URL bar durante el scroll. Sticky/fixed
           estables, sin jitter.
         · calc(100svh - env(safe-area-inset-bottom)): con viewport-fit=
           cover, svh incluye la zona bajo la barra Android/iOS. Si no
           restamos, contenido al final del layout queda bajo la barra
           (letras con descenders cortadas, etc.). Restando, el layout
           termina SOBRE la barra; el gap inferior lo cubre body::after
           strip en color elevated.
       Fallback chain: dvh → svh → svh con resta (browsers nuevos). */
    height: 100dvh;
    height: 100svh;
    height: calc(100svh - env(safe-area-inset-bottom, 0));
    overflow: hidden;     /* Previene doble scroll si algún hijo se desborda */
    transition: grid-template-columns var(--transition-normal);
}

.layout-app__footer { grid-area: footer; }

/* Collapse solo aplica en desktop — en móvil el sidebar es drawer.
   Se lee del <html> (seteado por script síncrono en _base.html.twig)
   para que aplique ANTES de que Alpine bootee — evita flash de expandido. */
@media (min-width: 48rem) {
    html[data-sidebar-collapsed="true"] .layout-app,
    .layout-app[data-sidebar-collapsed="true"] {
        grid-template-columns: var(--sidebar-width-collapsed) 1fr;
    }
}

.layout-app__sidebar { grid-area: sidebar; }
.layout-app__header  { grid-area: header; }
.layout-app__main    {
    grid-area: main;
    overflow-y: auto;
    /* Desactiva el rubber-band / overscroll bounce nativo al llegar al
       borde del scroll. Sin esto, el browser anima un "rebote elástico"
       en Chrome Android / iOS Safari — durante ese rebote los sticky
       interiores (form-footer) parecen "saltar" un par de pixels
       arriba/abajo. `contain` también bloquea el scroll-chaining hacia
       el body/html (defensa en profundidad ante cualquier overflow). */
    overscroll-behavior: contain;
    /* Scrollbar fino estilo AntD Pro — default del browser es muy
       visible (especialmente Chromium/Windows, ~17px con flechas).
       Mismo patrón que .modal__body (blocks/modal.css). */
    scrollbar-width: thin;
    scrollbar-color: var(--color-border-strong) transparent;
}

.layout-app__main::-webkit-scrollbar {
    width: 10px;
}
.layout-app__main::-webkit-scrollbar-track {
    background: transparent;
}
.layout-app__main::-webkit-scrollbar-thumb {
    background-color: var(--color-border-strong);
    border-radius: var(--radius-md);
    border: 2px solid transparent;
    background-clip: padding-box;
}
.layout-app__main::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-text-muted);
}

/* Un instante, al cerrar un modal o un cajón: app.js la pone y la quita
   para que el navegador táctil vuelva a enganchar el scroll del main (truco
   de body-scroll-lock: un cambio que no se ve y fuerza un reflow). */
.layout-app__main.is-repintando,
.layout-cliente__main.is-repintando {
    transform: translateZ(0);
}

/* En móvil (admin): sidebar como drawer, SIN bottom-nav.
   BP: por debajo de --bp-md (48rem / 768px) — ver tokens.css: 47.99rem para
   que a 768px exactos no se sume al grid de escritorio de arriba. */
@media (max-width: 47.99rem) {  /* --bp-md */
    .layout-app {
        grid-template-columns: 1fr;
        grid-template-rows: var(--header-height) 1fr;
        grid-template-areas:
            "header"
            "main";
    }

    .layout-app__sidebar {
        position: fixed;
        inset-block: 0;
        inset-inline-start: 0;
        z-index: var(--z-menu-lateral);   /* como un cajón: también sobre los avisos fijos de arriba */
        transform: translateX(-100%);
        transition: transform var(--transition-normal);
        max-width: 280px;            /* deja parte del contenido visible detrás */
    }

    .layout-app__sidebar[data-open="true"] {
        transform: translateX(0);
    }

    /* Backdrop — se muestra al abrir el drawer en móvil. Click en él cierra
       el sidebar (patrón estándar AntD Pro / Material).                      */
    .layout-app__backdrop {
        position: fixed;
        inset: 0;
        background-color: rgba(0, 0, 0, 0.45);
        z-index: calc(var(--z-menu-lateral) - 1);   /* justo debajo del menú */
        opacity: 0;
        pointer-events: none;
        transition: opacity var(--transition-normal);
    }

    .layout-app__backdrop[data-open="true"] {
        opacity: 1;
        pointer-events: auto;
    }

    .layout-app__bottom { grid-area: bottom; }
}

/* En desktop el backdrop nunca aparece — sidebar siempre integrado en grid */
@media (min-width: 48rem) {
    .layout-app__backdrop {
        display: none;
    }
}

/* ── Flow — espaciado vertical entre hijos ──────────────────────────────── */
.flow > * + * {
    margin-block-start: var(--flow-space, var(--space-md));
}

/* Variantes de separación del flow */
.flow[data-space="xs"]  { --flow-space: var(--space-xs); }
.flow[data-space="sm"]  { --flow-space: var(--space-sm); }
.flow[data-space="md"]  { --flow-space: var(--space-md); }
.flow[data-space="lg"]  { --flow-space: var(--space-lg); }
.flow[data-space="xl"]  { --flow-space: var(--space-xl); }

/* ── Cluster — grupo de elementos con espacio entre ellos ───────────────── */
.cluster {
    display: flex;
    flex-wrap: wrap;
    gap: var(--cluster-gap, var(--space-sm));
    align-items: var(--cluster-align, center);
    justify-content: var(--cluster-justify, flex-start);
}

/* Variantes del cluster */
.cluster[data-gap="xs"]  { --cluster-gap: var(--space-xs); }
.cluster[data-gap="sm"]  { --cluster-gap: var(--space-sm); }
.cluster[data-gap="md"]  { --cluster-gap: var(--space-md); }
.cluster[data-gap="lg"]  { --cluster-gap: var(--space-lg); }

.cluster[data-justify="start"]   { --cluster-justify: flex-start; }
.cluster[data-justify="center"]  { --cluster-justify: center; }
.cluster[data-justify="end"]     { --cluster-justify: flex-end; }
.cluster[data-justify="between"] { --cluster-justify: space-between; }

.cluster[data-align="start"]    { --cluster-align: flex-start; }
.cluster[data-align="center"]   { --cluster-align: center; }
.cluster[data-align="end"]      { --cluster-align: flex-end; }
.cluster[data-align="stretch"]  { --cluster-align: stretch; }

/* ── Stack — columna con espacio entre hijos ────────────────────────────── */
.stack {
    display: flex;
    flex-direction: column;
    gap: var(--stack-gap, var(--space-md));
}

/* Variantes del stack */
.stack[data-gap="xs"]  { --stack-gap: var(--space-xs); }
.stack[data-gap="sm"]  { --stack-gap: var(--space-sm); }
.stack[data-gap="md"]  { --stack-gap: var(--space-md); }
.stack[data-gap="lg"]  { --stack-gap: var(--space-lg); }
.stack[data-gap="xl"]  { --stack-gap: var(--space-xl); }

/* ── Grid automático responsive ─────────────────────────────────────────────
   Tantas columnas de al menos `--auto-grid-min` como quepan; con menos sitio,
   una debajo de otra (el `min(…, 100%)` evita que una columna mínima más
   ancha que el contenedor dé scroll horizontal). El mínimo se elige con
   `data-min` según lo que lleven las celdas (docs/diseno.md «Rejillas,
   paneles y tablas»): una tabla o una matriz necesita `xl`.
   Cada celda puede encoger hasta su columna (`min-width: 0`): lo que no
   encoge (una tabla) va en un contenedor con scroll propio
   (`.data-table-wrapper`), nunca saliéndose de la celda.                   */
.auto-grid {
    display: grid;
    grid-template-columns: repeat(
        auto-fill,
        minmax(min(var(--auto-grid-min, 16rem), 100%), 1fr)
    );
    gap: var(--space-md);
}

.auto-grid > * {
    min-width: 0;
}

.auto-grid[data-min="xs"] { --auto-grid-min: 8rem; }
.auto-grid[data-min="sm"] { --auto-grid-min: 12rem; }
.auto-grid[data-min="md"] { --auto-grid-min: 16rem; }
.auto-grid[data-min="lg"] { --auto-grid-min: 22rem; }
.auto-grid[data-min="xl"] { --auto-grid-min: 32rem; }

/* ── Sidebar layout (contenido + aside) ─────────────────────────────────── */
.with-sidebar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
}

.with-sidebar > :first-child {
    flex-basis: var(--sidebar-width);
    flex-grow: 1;
}

.with-sidebar > :last-child {
    flex-basis: 0;
    flex-grow: 999;
    min-inline-size: 50%;
}



/* 4. Utilities — clases de un propósito */

/* ── utilities.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   utilities.css — CUBE CSS: Utilities
   Clases de un único propósito. Selectivas — no generar utilidades para todo.
   Si necesitas más de 3 utilidades juntas en un elemento, crea un bloque.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Alpine — ocultar hasta que Alpine inicialice ────────────────────────── */
/* Reemplaza `style="display: none"` en elementos con x-show para evitar
   flash de contenido sin estilar antes de que Alpine se cargue.            */
[x-cloak] { display: none !important; }

/* ── Margen reset — formularios sueltos dentro de flex/grid ──────────────── */
.no-margin { margin: 0; }

/* ── Anchos máximos — centran el contenido horizontalmente ──────────────── */
/* Útil cuando el elemento NO es el container principal (que ya se ocupa
   del max-width + padding-inline). Solo restringen el ancho.               */
.max-w-xs { max-width: var(--container-xs); margin-inline: auto; }
.max-w-sm { max-width: var(--container-sm); margin-inline: auto; }
.max-w-md { max-width: var(--container-md); margin-inline: auto; }
.max-w-lg { max-width: var(--container-lg); margin-inline: auto; }

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

/* ── Visibilidad ─────────────────────────────────────────────────────────── */
.hidden  { display: none; }

/* ── Alineación de texto ─────────────────────────────────────────────────── */
.text-center { text-align: center; }
.text-right  { text-align: right; }
.text-left   { text-align: left; }

/* ── Tamaño de texto ─────────────────────────────────────────────────────── */
.text-xs   { font-size: var(--font-size-xs); }
.text-sm   { font-size: var(--font-size-sm); }
.text-base { font-size: var(--font-size-base); }
.text-lg   { font-size: var(--font-size-lg); }
.text-xl   { font-size: var(--font-size-xl); }
.text-2xl  { font-size: var(--font-size-2xl); }
.text-3xl  { font-size: var(--font-size-3xl); }
.text-cifra { font-size: 2rem; }   /* una cifra suelta que destaca en su tarjeta */

/* ── Peso de texto ───────────────────────────────────────────────────────── */
.font-bold     { font-weight: var(--font-weight-bold); }
.font-semibold { font-weight: var(--font-weight-semibold); }
.font-medium   { font-weight: var(--font-weight-medium); }
.font-regular  { font-weight: var(--font-weight-regular); }

/* ── Familia de texto ────────────────────────────────────────────────────── */
/* Códigos, claves, comandos: de ancho fijo. */
.font-mono { font-family: var(--font-mono, monospace); }

/* ── Saltos de línea ─────────────────────────────────────────────────────── */
.whitespace-pre-line { white-space: pre-line; }   /* respeta los saltos del texto */
.break-all           { word-break: break-all; }   /* un valor sin espacios (agente de usuario, token) */

/* ── Estilo de texto ─────────────────────────────────────────────────────── */
.text-uppercase {
    text-transform: uppercase;
    letter-spacing: var(--letter-spacing-wide);
}

/* ── Colores de texto ────────────────────────────────────────────────────── */
.text-muted   { color: var(--color-text-muted); }
.text-subtle  { color: var(--color-text-subtle); }
.text-danger  { color: var(--color-danger-text); }
.text-success { color: var(--color-success-text); }
.text-warning { color: var(--color-warning-text); }
.text-primary { color: var(--color-primary-text); }
.text-info    { color: var(--color-info-text); }

/* ── Colores de fondo ────────────────────────────────────────────────────── */
.bg-alt           { background-color: var(--color-bg-alt); }
.bg-elevated      { background-color: var(--color-bg-elevated); }
.bg-primary-soft  { background-color: var(--color-primary-soft); }
.bg-success-soft  { background-color: var(--color-success-soft); }
.bg-warning-soft  { background-color: var(--color-warning-soft); }
.bg-danger-soft   { background-color: var(--color-danger-soft); }

/* ── Márgenes selectivos ─────────────────────────────────────────────────── */
.mt-auto { margin-block-start: auto; }
.mb-auto { margin-block-end: auto; }
.ms-auto { margin-inline-start: auto; }
.me-auto { margin-inline-end: auto; }
.m-auto  { margin: auto; }

/* Márgenes por escala de espaciado canónica (tokens.css). Cero
   `style="margin-top: var(...)"` inline en templates Twig: usar estas
   clases en su lugar. */
.mt-xs { margin-block-start: var(--space-xs); }
.mt-sm { margin-block-start: var(--space-sm); }
.mt-md { margin-block-start: var(--space-md); }
.mt-lg { margin-block-start: var(--space-lg); }
.mt-xl { margin-block-start: var(--space-xl); }

.mb-xs { margin-block-end: var(--space-xs); }
.mb-sm { margin-block-end: var(--space-sm); }
.mb-md { margin-block-end: var(--space-md); }
.mb-lg { margin-block-end: var(--space-lg); }
.mb-xl { margin-block-end: var(--space-xl); }

.m-xs { margin: var(--space-xs); }
.m-sm { margin: var(--space-sm); }
.m-md { margin: var(--space-md); }
.m-lg { margin: var(--space-lg); }

/* Separación junto a lo que va delante en la línea (una etiqueta tras un
   nombre). */
.ms-xs { margin-inline-start: var(--space-xs); }
.ms-sm { margin-inline-start: var(--space-sm); }

/* ── Hijos de una fila flex (.cluster, .stack) ───────────────────────────── */
/* El que se lleva el sitio que sobra y, para que su texto baje de línea en
   vez de salirse, puede encoger por debajo de su contenido (docs/diseno.md
   §7, «Filas»). */
.flex-1      { flex: 1; }
.min-w-0     { min-width: 0; }
.shrink-0    { flex-shrink: 0; }
.self-center { align-self: center; }

/* ── Caja ────────────────────────────────────────────────────────────────── */
.d-block { display: block; }
.w-full  { width: 100%; }

/* ── Separador: una línea encima, con aire a los dos lados ────────────────── */
.separado-arriba {
    margin-top: var(--space-md);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border-subtle);
}

/* ── Cursor ──────────────────────────────────────────────────────────────── */
/* Lo que se pulsa sin ser un botón ni un enlace (una etiqueta con su casilla,
   un código que se copia). */
.cursor-pointer { cursor: pointer; }

/* ── Truncado de texto ───────────────────────────────────────────────────── */
.truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Iconos (Lucide) — SVG inline desde resources/views/icons/ ───────────── */
/* Por defecto el SVG hereda color (currentColor) y tamaño (1em).
   Variantes con clase modificadora para tamaños fijos.                       */
.icon {
    display: inline-block;
    width: 1em;
    height: 1em;
    flex-shrink: 0;
    vertical-align: -0.125em;       /* alinea con el baseline del texto */
}

.icon--sm { width: 14px; height: 14px; }
.icon--md { width: 18px; height: 18px; }
.icon--lg { width: 24px; height: 24px; }
.icon--xl { width: 32px; height: 32px; }

/* ── Indicador de carga HTMX ─────────────────────────────────────────────── */
.htmx-indicator {
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
    opacity: 1;
}

/* ── Line clamp — truncar texto a N líneas ──────────────────────────── */
.line-clamp-1 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; }
.line-clamp-2 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
.line-clamp-3 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }



/* 5. Blocks — componentes (un archivo por bloque).
   links.css va el primero: es el estilo base de <a> y los bloques que vienen
   después lo sobrescriben (alert.css gana con la misma especificidad). */

/* ── links.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/links.css — Estilo global de hyperlinks `<a>`

   Regla baseline para anclas que NO están dentro de un bloque con styling
   propio (alert, botones, navegación, tabs, notifications, app-footer).
   Los bloques específicos siguen ganando por especificidad CSS o por ir
   después en main.css — esta regla solo cubre las anclas "huérfanas" en
   stat-cards, tablas data-table, page-headers, etc.

   Razón: sin esto, un `<a>` dentro de un stat-card o tabla heredaba el
   color del padre o caía al default del browser (azul oscuro casi
   invisible en dark mode).

   Si un bloque nuevo es un <a> con aspecto de botón/chip, debe anular el
   subrayado del :hover (`text-decoration: none`), como hacen .btn,
   .nav-sidebar__link, .tab-filter__item y .tabs__tab.

   Tokens usados (definidos en tokens.css):
     - --color-primary       — color base del enlace
     - --color-primary-hover — variante hover (más saturada)

   Hover/focus accesibles + transiciones suaves alineadas con el resto
   de interactivos del sistema (buttons, tabs, badges).
   ═══════════════════════════════════════════════════════════════════════════ */

a {
    color: var(--color-primary-text);
    text-decoration: none;
    transition: color var(--transition-fast, 120ms ease);
}

a:hover {
    color: var(--color-primary-text);
    text-decoration: underline;
    text-underline-offset: 2px;
}

a:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

a:active {
    /* Feedback de tap, sin saltar a otro color drásticamente */
    opacity: 0.85;
}

/* Variante "muted" — anclas en footers, captions, breadcrumbs */
a[data-variant="muted"] {
    color: var(--color-text-muted);
}

a[data-variant="muted"]:hover {
    color: var(--color-text);
}

/* Anclas que envuelven un bloque (ej. card-link entera).
   Sin underline, color del texto del padre, hover destaca con bg sutil. */
a.block-link,
a[data-variant="block"] {
    color: inherit;
    text-decoration: none;
}

a.block-link:hover,
a[data-variant="block"]:hover {
    color: inherit;
    text-decoration: none;
}



/* ── button.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/button.css — Bloque: botón
   Variantes mediante data-variant y data-size (CUBE CSS Exceptions).
   ═══════════════════════════════════════════════════════════════════════════ */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    padding: 0.625rem var(--space-md);   /* 10px / 16px — algo más alto que el default */
    border: 1px solid transparent;
    border-radius: var(--radius-md);     /* 10px — estilo referencia */
    font-family: var(--font-body);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    line-height: 1.2;
    cursor: pointer;
    text-decoration: none;
    transition:
        background var(--transition-fast),
        border-color var(--transition-fast),
        color var(--transition-fast),
        box-shadow var(--transition-fast),
        transform var(--transition-fast);
    user-select: none;
}

/* Cuando el .btn es un <a>, links.css aplica `text-decoration: underline`
   en :hover/:focus por defecto (regla genérica para anclas huérfanas).
   Aquí lo anulamos — un botón nunca debe mostrar subrayado, aunque sea
   un hyperlink semántico. Excepción: data-variant="link" sí lo quiere
   (regla más específica ya definida abajo). */
.btn:hover,
.btn:focus,
.btn:focus-visible {
    text-decoration: none;
}

.btn:active:not(:disabled) {
    transform: translateY(1px);
}

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

/* ── Variantes (data-variant) ────────────────────────────────────────────── */

.btn[data-variant="primary"] {
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    box-shadow: var(--shadow-sm);
}

/* :hover de todos los variants va prefijado con `html:not(.is-touch)` para
   evitar sticky-hover tras tap en móviles. `html.is-touch` se marca desde
   app.js al primer touchstart o sincronamente si UA es pointer:coarse.
   Patrón AntD Mobile canónico. Ver docs/estandar-ux.md §9.4. */

html:not(.is-touch) .btn[data-variant="primary"]:hover:not(:disabled) {
    background-color: var(--color-primary-hover);
}

.btn[data-variant="primary"]:active:not(:disabled) {
    background-color: var(--color-primary-active);
}

.btn[data-variant="danger"] {
    background-color: var(--color-danger-strong);
    color: var(--color-text-on-dark);
}

html:not(.is-touch) .btn[data-variant="danger"]:hover:not(:disabled) {
    background-color: var(--color-danger-strong-hover);
}

.btn[data-variant="success"] {
    background-color: var(--color-success-strong);
    color: var(--color-text-on-dark);
}

html:not(.is-touch) .btn[data-variant="success"]:hover:not(:disabled) {
    background-color: var(--color-success-strong-hover);
}

.btn[data-variant="ghost"] {
    background-color: var(--color-bg-elevated);
    border-color: var(--color-border);
    color: var(--color-text);
}

html:not(.is-touch) .btn[data-variant="ghost"]:hover:not(:disabled) {
    background-color: var(--color-bg-alt);
    border-color: var(--color-border-strong);
}

/* Ghost activo — para botones tipo "Filtros (3)" cuando hay estado aplicado.
   Borde + texto en primary, fondo soft. Indica que está "encendido". */
.btn[data-variant="ghost-active"] {
    background-color: var(--color-primary-soft);
    border-color: var(--color-primary);
    color: var(--color-primary-text);
}

html:not(.is-touch) .btn[data-variant="ghost-active"]:hover:not(:disabled) {
    background-color: var(--color-primary-soft);
    border-color: var(--color-primary-hover);
    color: var(--color-primary-text);
}

.btn[data-variant="link"] {
    background-color: transparent;
    color: var(--color-primary-text);
    padding-inline: 0;
    border-radius: 0;
}

html:not(.is-touch) .btn[data-variant="link"]:hover:not(:disabled) {
    text-decoration: underline;
    color: var(--color-primary-text);
}

/* ── Tamaños (data-size) ─────────────────────────────────────────────────── */

.btn[data-size="sm"] {
    padding: 0.375rem var(--space-sm);
    font-size: var(--font-size-xs);
    border-radius: var(--radius-sm);
}

.btn[data-size="lg"] {
    padding: 0.875rem var(--space-lg);
    font-size: var(--font-size-base);
}

/* Botón circular — solo icono */
.btn[data-variant="icon"] {
    background-color: transparent;
    color: var(--color-text-muted);
    padding: var(--space-sm);
    border-radius: var(--radius-full);
    width: 2.5rem;
    height: 2.5rem;
}

html:not(.is-touch) .btn[data-variant="icon"]:hover:not(:disabled) {
    background-color: var(--color-bg-alt);
    color: var(--color-text);
}



/* ── card.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/card.css — Bloque: tarjeta de contenido
   Radius generoso (14px) y sombra suave — diferenciador visual de la
   referencia. Sin borde por defecto; la sombra y el radius hacen el trabajo.
   ═══════════════════════════════════════════════════════════════════════════ */

.card {
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-lg);         /* 8px AntD */
    padding: var(--space-lg);
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--transition-normal);
}

/* :hover solo en devices con cursor real — evita sticky-hover tras tap
   en touch. `html.is-touch` lo marca app.js al primer touchstart (patrón
   AntD Mobile canónico, ver gotcha "revert !important" en docs/gotchas.md). */
html:not(.is-touch) .card[data-interactive="true"]:hover {
    box-shadow: var(--shadow-md);
}

/* El título y sus acciones bajan de línea si no caben juntos (a 360px el
   selector de rango de una tarjeta se salía de ella). */
.card__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    margin-block-end: var(--space-md);
    padding-block-end: var(--space-md);
    border-block-end: 1px solid var(--color-border);
}

.card__title {
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
}

.card__body {
    /* Sin estilos propios — el contenido define su layout */
}

.card__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-sm);
    margin-block-start: var(--space-md);
    padding-block-start: var(--space-md);
    border-block-start: 1px solid var(--color-border);
}

/* ── Variantes ───────────────────────────────────────────────────────────── */

.card[data-variant="destacado"] {
    border-color: var(--color-primary);
    box-shadow: var(--shadow-md);
}

.card[data-variant="flat"] {
    box-shadow: none;
}

.card[data-variant="elevated"] {
    border-color: transparent;
    box-shadow: var(--shadow-lg);
}

/* Ancho de un formulario corto (contraseña, preferencias), sin centrar. */
.card[data-ancho="formulario"] {
    max-width: 560px;
}

/* En móvil: padding interno más compacto para aprovechar mejor el ancho útil.
   AntD Pro mobile sigue este patrón. */
@media (max-width: 47.99rem) {
    .card {
        padding: var(--space-md);
    }
}



/* ── form.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/form.css — Bloque: formularios

   Visual cuidado tipo Slack: inputs con buen padding, focus con ring suave
   en lugar de outline plano, estados error/disabled bien diferenciados,
   labels claramente jerárquicos.
   ═══════════════════════════════════════════════════════════════════════════ */

.form {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

/* ── Campo de formulario ─────────────────────────────────────────────────── */
.form__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    /* Contexto de posicionamiento para desplegables anclados al campo
       (el combobox del compositor...). */
    position: relative;
}

/* Reset cuando .form__field se aplica a <fieldset> — grupos de checkboxes
   semánticamente agrupados (ej. multi-select de roles). Sin este reset,
   el user-agent aplica border + padding + margin nativos que rompen la
   rejilla del form-grid. Ver a11y: fieldset+legend es el patrón canónico
   para grupos de campos que comparten un significado común. */
fieldset.form__field {
    border: 0;
    margin: 0;
    padding: 0;
    min-inline-size: 0;  /* evita blowout en grid/flex children */
}

/* Variante en fila (label a la izquierda, input a la derecha) — desktop only */
.form__field[data-layout="row"] {
    display: grid;
    grid-template-columns: minmax(8rem, 1fr) 2fr;
    align-items: center;
    gap: var(--space-md);
}

@media (max-width: 47.99rem) {  /* --bp-md */
    .form__field[data-layout="row"] {
        display: flex;
        flex-direction: column;
    }
}

/* ── Label ───────────────────────────────────────────────────────────────── */
.form__label {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);    /* AntD Pro: medium (500) — no semibold */
    color: var(--color-text);
}

.form__label[data-required]::after {
    content: ' *';
    color: var(--color-danger-text);
}

/* Reset cuando .form__label se aplica a <legend> — el user-agent aplica
   padding + display específicos al legend. Queremos que se comporte
   exactamente como un <label> normal dentro del flex column del field. */
legend.form__label {
    padding: 0;
    display: block;          /* legend default es inline-block con reglas raras */
    float: none;             /* override algunos resets globales */
    inline-size: 100%;
}

/* ── Inputs / Selects / Textarea — base común ────────────────────────────── */
.form__input,
.form__select,
.form__textarea {
    width: 100%;
    padding: 0.625rem 0.875rem;                  /* 10px 14px */
    font-size: var(--font-size-sm);
    font-family: inherit;
    line-height: 1.4;
    color: var(--color-text);
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-control);   /* 3:1 con su fondo (WCAG 1.4.11) */
    border-radius: var(--radius-md);
    transition:
        border-color var(--transition-fast),
        box-shadow   var(--transition-fast),
        background-color var(--transition-fast);
}

.form__input::placeholder,
.form__textarea::placeholder {
    color: var(--color-text-subtle);
}

/* Texto de ancho fijo: Markdown, una plantilla, una clave técnica. */
.form__input[data-fuente="mono"] {
    font-family: var(--font-mono, monospace);
}

/* Un código de un solo uso (2FA, recuperación): grande, de ancho fijo y
   espaciado, para leerlo y teclearlo carácter a carácter. `cifras`, el de
   seis cifras de la app de autenticación, con más aire entre ellas. */
.form__input[data-tipo="codigo"],
.form__input[data-tipo="cifras"] {
    font-family: var(--font-mono, monospace);
    font-size: var(--font-size-lg);
    letter-spacing: 0.1em;
}

.form__input[data-tipo="cifras"] {
    letter-spacing: 0.2em;
}

/* ── Hover — borde del color de marca, como AntD (el gris claro de antes
      bajaba del 3:1 del borde normal) ──────────────────────────────────── */
.form__input:hover:not(:disabled):not(:focus),
.form__select:hover:not(:disabled):not(:focus),
.form__textarea:hover:not(:disabled):not(:focus) {
    border-color: var(--color-primary);
}

/* ── Focus — ring suave en lugar de outline plano ────────────────────────── */
.form__input:focus,
.form__select:focus,
.form__textarea:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: var(--focus-ring);
}

/* ── Estado error ────────────────────────────────────────────────────────── */
.form__input[data-error="true"],
.form__select[data-error="true"],
.form__textarea[data-error="true"],
.form__input[aria-invalid="true"],
.form__select[aria-invalid="true"],
.form__textarea[aria-invalid="true"] {
    border-color: var(--color-danger);
}

.form__input[data-error="true"]:focus,
.form__select[data-error="true"]:focus,
.form__textarea[data-error="true"]:focus {
    box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.15);
}

/* ── Estado disabled ─────────────────────────────────────────────────────── */
.form__input:disabled,
.form__select:disabled,
.form__textarea:disabled {
    background-color: var(--color-bg-alt);
    border-color: var(--color-border);
    color: var(--color-text-subtle);
    cursor: not-allowed;
}

/* ── Tipos específicos ───────────────────────────────────────────────────── */
.form__textarea {
    resize: vertical;
    min-height: 6rem;
    line-height: var(--line-height-base);
}

.form__select {
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23616061' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0.6rem center;
    background-size: 16px;
    padding-inline-end: 2rem;
}

/* Checkboxes y radios — alinear con texto */
.form__check {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    cursor: pointer;
    user-select: none;
}

.form__check input[type="checkbox"],
.form__check input[type="radio"] {
    width: 1rem;
    height: 1rem;
    accent-color: var(--color-primary);
    cursor: pointer;
    flex-shrink: 0;
}

/* Checkbox con título + descripción (AntD Checkbox con hint) */
.form__check:has(.form__check-content) {
    align-items: flex-start;
}

.form__check-content {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;                 /* en una columna estrecha, la etiqueta baja de línea */
    overflow-wrap: break-word;    /* en vez de salirse y pisar la casilla de al lado */
    line-height: 1.3;
}

.form__check-title {
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
}

.form__check-desc {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* ── Mensajes de error y ayuda ───────────────────────────────────────────── */
.form__error {
    font-size: var(--font-size-sm);
    color: var(--color-danger-text);
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-height: 21px;                   /* reserva altura para no desplazar layout */
    line-height: 1.4;
}

/* ── Grid de 12 columnas para formularios (AntD Row/Col) ─────────────────── */
.form-grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--space-md);
    align-items: start;                 /* nunca estirar — error no arrastra hermanos */
}

.form-grid > .form__field {
    grid-column: span 12;               /* full width por defecto */
    min-width: 0;
}

/* Helpers por columnas — `data-cols="N"` en el .form__field */
.form-grid > .form__field[data-cols="2"]  { grid-column: span 2; }
.form-grid > .form__field[data-cols="3"]  { grid-column: span 3; }
.form-grid > .form__field[data-cols="4"]  { grid-column: span 4; }
.form-grid > .form__field[data-cols="5"]  { grid-column: span 5; }
.form-grid > .form__field[data-cols="6"]  { grid-column: span 6; }
.form-grid > .form__field[data-cols="7"]  { grid-column: span 7; }
.form-grid > .form__field[data-cols="8"]  { grid-column: span 8; }
.form-grid > .form__field[data-cols="9"]  { grid-column: span 9; }
.form-grid > .form__field[data-cols="10"] { grid-column: span 10; }
.form-grid > .form__field[data-cols="12"] { grid-column: span 12; }

/* Mobile: todo a ancho completo para legibilidad */
@media (max-width: 47.99rem) {
    .form-grid > .form__field[data-cols] {
        grid-column: span 12;
    }
}

/* ── Sub-sección dentro de una tab de formulario ─────────────────────────── */
.form-subsection {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding-block-start: var(--space-sm);
}

.form-subsection + .form-subsection {
    padding-block-start: var(--space-md);
}

/* Sobre un <fieldset>, que por defecto mide lo que su contenido
   (min-inline-size: min-content): con una tabla dentro, en móvil se salía de
   la tarjeta en vez de dejar que la tabla se desplace en su wrapper. */
fieldset.form-subsection {
    min-inline-size: 0;
}

.form-subsection__title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
    padding-inline-start: var(--space-sm);
    border-inline-start: 2px solid var(--color-primary);
    line-height: 1.2;
}

/* ── Subgrupo dentro de un form-grid ──────────────────────────────────────
   Header full-width que parte un form-grid en secciones. Se inyecta como
   hijo del grid con data-cols="12" y visualmente separa bloques de campos
   (ej: "Alta por invitación" vs "Verificación en 2 pasos" en Configuración). */
.form-grid > .form__subgroup[data-cols="12"] {
    grid-column: span 12;
    padding-block-start: var(--space-md);
    margin-block-start: var(--space-xs);
    border-block-start: 1px solid var(--color-border-subtle);
}

/* El primer subgrupo no necesita separador superior — ya viene el padding
   del contenedor de la sección. Mantiene proporciones limpias. */
.form-grid > .form__subgroup[data-cols="12"]:first-child {
    padding-block-start: 0;
    margin-block-start: 0;
    border-block-start: none;
}

.form__subgroup-title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
    padding-inline-start: var(--space-sm);
    border-inline-start: 2px solid var(--color-primary);
    line-height: 1.2;
}

.form__hint {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* ── Acciones del formulario ─────────────────────────────────────────────── */
.form__actions {
    display: flex;
    gap: var(--space-sm);
    justify-content: flex-end;
    margin-block-start: var(--space-sm);
    padding-block-start: var(--space-md);
    border-block-start: 1px solid var(--color-border);
}

.form__actions[data-justify="between"] { justify-content: space-between; }
.form__actions[data-justify="start"]   { justify-content: flex-start; }
.form__actions[data-justify="center"]  { justify-content: center; }

/* Móvil: botones apilados full-width con primary arriba (AntD Mobile).
   Mismo patrón canónico que .modal__footer en modal.css. Sin esto,
   botones largos (ej. 'Guardar borrador' + 'Programar envío') rompen
   línea individualmente y quedan con ancho desigual — feo. */
@media (max-width: 39.99rem) {
    .form__actions {
        flex-direction: column-reverse;
        gap: var(--space-xs);
    }
    .form__actions .btn {
        width: 100%;
        justify-content: center;
    }
}

/* ── Input con icono interno ─────────────────────────────────────────────── */
.form__input-group {
    position: relative;
    display: flex;
    align-items: center;
}

/* Variantes de ancho fijo — para searches en barras de filtros */
.form__input-group[data-width="sm"] { flex: 0 0 240px; max-width: 240px; }
.form__input-group[data-width="md"] { flex: 0 0 320px; max-width: 320px; }
.form__input-group[data-width="lg"] { flex: 0 0 400px; max-width: 400px; }

/* En móvil los anchos fijos de filtro pasan a 100% — caben bien y evitan
   cortes extraños en viewports estrechos. */
@media (max-width: 47.99rem) {
    .form__input-group[data-width="sm"],
    .form__input-group[data-width="md"],
    .form__input-group[data-width="lg"] {
        flex: 1 1 100%;
        max-width: 100%;
    }
}

.form__input-group .form__input {
    padding-inline-start: 2.25rem;
}

.form__input-group__icon {
    position: absolute;
    inset-inline-start: 0.75rem;
    color: var(--color-text-subtle);
    pointer-events: none;
    width: 16px;
    height: 16px;
}

.form__input-group .form__input:focus + .form__input-group__icon,
.form__input-group:focus-within .form__input-group__icon {
    color: var(--color-primary-text);
}

/* ── Fieldset / Legend ───────────────────────────────────────────────────────
   Grupos visuales dentro de un campo (ej: roles por área en el form de user).
   Se resetea el estilo nativo del navegador y se aplica el propio del sistema. */
.form__fieldset {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-sm) var(--space-md) var(--space-md);
    margin: 0;
    background: var(--color-bg-alt);
}

.form__fieldset > legend {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
}



/* ── table.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/table.css — Bloque: tabla de datos

   Pensada para listados de gestión: header sticky, hover de fila, paddings
   generosos en densidad comfortable, columnas alineables, indicador de
   ordenación opcional, scroll horizontal en móvil.

   Variantes:
       data-density="compact"   → padding reducido para mostrar más filas
       data-bordered="true"     → bordes verticales entre celdas
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Wrapper para scroll horizontal en móvil + sticky header ─────────────── */
/* position: relative lo hace el bloque contenedor de lo que va en posición
   absoluta dentro de la tabla (las etiquetas .sr-only de los campos de una
   celda): sin él, se colocan respecto a algo de fuera, no se desplazan con la
   tabla y estiran la página (65px de scroll horizontal a 360px en la semana
   tipo de un plan de pago de TRW). */
.data-table-wrapper {
    position: relative;
    overflow-x: auto;
    overflow-y: visible;
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}

/* Con alto máximo: una lista larga dentro de un modal se desplaza también
   en vertical, dentro de su marco (`md` 300px, `lg` 400px). */
.data-table-wrapper[data-alto="md"],
.data-table-wrapper[data-alto="lg"] {
    overflow-y: auto;
}

.data-table-wrapper[data-alto="md"] { max-height: 300px; }
.data-table-wrapper[data-alto="lg"] { max-height: 400px; }

.data-table {
    width: 100%;
    border-collapse: collapse;
    background-color: var(--color-bg-elevated);
    font-size: var(--font-size-sm);
}

/* Si la tabla NO está dentro del wrapper, mantener bordes y radio en la propia tabla */
.data-table:not(.data-table-wrapper > .data-table) {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
}

/* ── Cabecera ────────────────────────────────────────────────────────────── */
.data-table thead {
    background-color: var(--color-bg-alt);
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
}

/* La alineación por defecto, sin especificidad de clase: así una cabecera con
   `.text-right` / `.text-center` (o `data-align`) se alinea como sus celdas.
   Con `.data-table th { text-align: start }` la utilidad perdía y las
   cabeceras de las columnas de cifras quedaban a la izquierda de sus datos. */
:where(.data-table) th {
    text-align: start;
}

.data-table th {
    padding: var(--space-md);
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-sm);
    color: var(--color-text);
    border-block-end: 1px solid var(--color-border-subtle);
    white-space: nowrap;
    background-color: var(--color-bg-alt);
}

/* Header ordenable — clickable */
.data-table th[data-sortable] {
    cursor: pointer;
    user-select: none;
}

.data-table th[data-sortable]:hover {
    color: var(--color-text);
    background-color: var(--color-border);
}

.data-table th[data-sort-active]::after {
    content: '';
    display: inline-block;
    margin-inline-start: var(--space-xs);
    border: 4px solid transparent;
    vertical-align: middle;
}

.data-table th[data-sort-active="asc"]::after {
    border-block-end-color: currentColor;
    border-block-start: none;
    margin-block-end: 2px;
}

.data-table th[data-sort-active="desc"]::after {
    border-block-start-color: currentColor;
    border-block-end: none;
    margin-block-start: 2px;
}

/* ── Filas y celdas ──────────────────────────────────────────────────────── */
.data-table td {
    padding: var(--space-md);
    color: var(--color-text);
    border-block-end: 1px solid var(--color-border-subtle);
    vertical-align: middle;
}

.data-table tbody tr:last-child td {
    border-block-end: none;
}

.data-table tbody tr {
    transition: background-color var(--transition-fast),
                opacity 200ms ease,
                transform 200ms ease;
}

/* HTMX añade .htmx-swapping durante el swap-out de la fila eliminada.
   Fade + colapso suave (~200ms) en lugar de desaparición brusca. */
.data-table tbody tr.htmx-swapping {
    opacity: 0;
    transform: translateX(-12px);
    background-color: var(--color-danger-soft, rgba(255, 77, 79, 0.08));
}

.data-table tbody tr:hover {
    background-color: var(--color-bg-alt);
}

.data-table tbody tr[data-selected="true"] {
    background-color: var(--color-primary-soft);
}

/* ── Acciones (última columna) ───────────────────────────────────────────── */
.data-table__actions {
    display: flex;
    gap: var(--space-xs);
    justify-content: flex-end;
    flex-wrap: nowrap;
}

/* La columna de acciones va a la derecha, como su cabecera («Acciones» con
   `data-align="end"` o `.text-right`): los `.row-actions` se pegan al borde. */
.data-table td:has(> .row-actions) {
    text-align: end;
}

/* ── Alineación por columna — `data-align` en th + td ────────────────────────
   La forma canónica: el atributo en AMBAS celdas (th del thead + td del
   tbody), para que la cabecera vaya alineada con su dato. Las cifras, a la
   derecha (docs/estandar-crud.md §5 «Tablas»):
       <th data-align="end">Total</th>
       <td data-align="end">{{ row.total }}</td>
   `.text-right` / `.text-center` en las dos celdas también funciona: la
   alineación por defecto de la cabecera (arriba) no les gana.
   ─────────────────────────────────────────────────────────────────────────── */
.data-table th[data-align="end"],
.data-table td[data-align="end"] {
    text-align: end;
}

/* Cifras de ancho fijo: las unidades, decenas y decimales caen una bajo otra. */
.data-table td[data-align="end"] {
    font-variant-numeric: tabular-nums;
}

.data-table th[data-align="center"],
.data-table td[data-align="center"] {
    text-align: center;
}

.data-table th[data-align="start"],
.data-table td[data-align="start"] {
    text-align: start;
}

/* ── Columna de texto largo — `data-ancho="texto"` en th + td ────────────────
   Descripción, justificación, efecto: en una tabla más ancha que su tarjeta
   cada columna baja a su ancho mínimo, y la de texto (que parte) quedaba en
   dos o tres palabras por línea mientras las de etiquetas y botones (que no
   parten) conservaban su sitio. Con el atributo no baja de 14rem; la tabla se
   desplaza en su wrapper.
   ─────────────────────────────────────────────────────────────────────────── */
.data-table th[data-ancho="texto"],
.data-table td[data-ancho="texto"] {
    min-width: 14rem;
}

/* ── Celda que se corta — `data-recorte="sm|md|lg"` en el td (o dentro) ─────
   Un valor largo que no hace falta leer entero en la tabla (una URL, un
   mensaje de error, la salida de una tarea): `sm` y `md`, una línea con
   puntos suspensivos hasta ese ancho, con `title` con el texto entero
   (docs/diseno.md §7, «Texto»); `lg` solo pone el ancho máximo y el texto
   baja de línea.
   ─────────────────────────────────────────────────────────────────────────── */
.data-table [data-recorte="sm"],
.data-table [data-recorte="md"] {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.data-table [data-recorte="sm"] { max-width: 250px; }
.data-table [data-recorte="md"] { max-width: 300px; }
.data-table [data-recorte="lg"] { max-width: 400px; }

/* ── Variantes ───────────────────────────────────────────────────────────── */

/* Densidad compacta — más filas en pantalla */
.data-table[data-density="compact"] td {
    padding: var(--space-sm) var(--space-md);
    font-size: var(--font-size-sm);
}

.data-table[data-density="compact"] th {
    padding: var(--space-xs) var(--space-md);
}

/* Bordes verticales entre celdas */
.data-table[data-bordered="true"] th,
.data-table[data-bordered="true"] td {
    border-inline-end: 1px solid var(--color-border);
}

.data-table[data-bordered="true"] th:last-child,
.data-table[data-bordered="true"] td:last-child {
    border-inline-end: none;
}

/* ── Mensaje cuando la tabla está vacía (legacy — preferir empty-state) ──── */
.data-table__empty {
    text-align: center;
    padding: var(--space-2xl);
    color: var(--color-text-muted);
}



/* ── badge.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/badge.css — Bloque: etiqueta corta de estado o categoría

   Estilo Slack: usa fondo "soft" del color con texto del color saturado.
   Variante por defecto = neutra. Variantes con data-variant.
   ═══════════════════════════════════════════════════════════════════════════ */

.badge {
    display: inline-block;
    padding: 0.2rem var(--space-sm);
    border-radius: var(--radius-full);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
    line-height: 1.4;
    background-color: var(--color-bg-alt);
    color: var(--color-text-muted);
}

/* ── Variantes "soft" — estilo Slack ─────────────────────────────────────── */

.badge[data-variant="primary"] {
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.badge[data-variant="success"] {
    background-color: var(--color-success-soft);
    color: var(--color-success-text);
}

.badge[data-variant="warning"] {
    background-color: var(--color-warning-soft);
    color: var(--color-warning-text);
}

.badge[data-variant="danger"] {
    background-color: var(--color-danger-soft);
    color: var(--color-danger-text);
}

.badge[data-variant="info"] {
    background-color: var(--color-info-soft);
    color: var(--color-info-text);
}


/* ── Variante "solid" — fondo saturado, texto blanco ─────────────────────── */

.badge[data-variant="primary-solid"] {
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
}

.badge[data-variant="danger-solid"] {
    background-color: var(--color-danger-strong);
    color: var(--color-text-on-dark);
}

/* ── Estado de una invitación bajo el nombre del usuario
      (partials/_invitacion_chip): pegado a la línea de arriba. ─────────── */

.invitacion-chip {
    margin-top: 2px;
}



/* ── alert.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/alert.css — Bloque: alerta inline en página

   Distinto del toast (flotante temporal). El alert ocupa espacio en el flujo
   normal del documento y persiste hasta que el usuario lo descarta o navega.

   Variantes con data-variant: success | warning | danger | info
   ═══════════════════════════════════════════════════════════════════════════ */

.alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-md);
    border-radius: var(--radius-lg);
    border: 1px solid transparent;
    border-inline-start-width: 4px;
    background-color: var(--color-bg-alt);
    color: var(--color-text);
}

.alert__icon {
    flex-shrink: 0;
    margin-block-start: 0.1em;          /* alinea con la primera línea de texto */
}

.alert__body {
    flex: 1;
    min-width: 0;                       /* permite truncar texto largo */
}

.alert__title {
    display: block;
    font-weight: var(--font-weight-medium);
    margin-block-end: var(--space-xs);
}

.alert__close {
    flex-shrink: 0;
    background: transparent;
    border: none;
    padding: var(--space-xs);
    margin: calc(var(--space-xs) * -1);
    cursor: pointer;
    color: inherit;
    border-radius: var(--radius-sm);
}

.alert__close:hover {
    background-color: rgba(0, 0, 0, 0.05);
}

/* ── Variantes — fondo soft + borde + icono coloreado ───────────────────── */

.alert[data-variant="success"] {
    background-color: var(--color-success-soft);
    border-color: var(--color-success);
    color: var(--color-success-text);
}

.alert[data-variant="warning"] {
    background-color: var(--color-warning-soft);
    border-color: var(--color-warning);
    color: var(--color-warning-text);
}

.alert[data-variant="danger"] {
    background-color: var(--color-danger-soft);
    border-color: var(--color-danger);
    color: var(--color-danger-text);
}

.alert[data-variant="info"] {
    background-color: var(--color-info-soft);
    border-color: var(--color-info);
    color: var(--color-info-text);
}

/* Links dentro de un alert: heredan el color del propio alert
   (success-hover, info, etc.) — el azul-link por defecto se confunde
   con el fondo info-soft en dark, quedando ilegible. Subrayado para
   que se diferencien sin depender solo del color. */
.alert a {
    color: currentColor;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.alert a:hover {
    text-decoration-thickness: 2px;
}



/* ── toast.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/toast.css — AntD Message style

   Feedbacks cortos flotantes arriba-centro, auto-descartables a los 3s.
   Sin botón de cerrar (el usuario no interactúa, solo lee).
   Variantes con data-variant: success | warning | danger | info

   Canonical AntD Message: https://ant.design/components/message
   ═══════════════════════════════════════════════════════════════════════════ */

.toast-stack {
    position: fixed;
    inset-block-start: var(--space-lg);
    inset-inline: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
    z-index: var(--z-toast);          /* por encima de modales y cajones */
    pointer-events: none;
    padding-inline: var(--space-md);
}

/* ── Mobile: Snackbar-style bottom (Material Design / AntD Mobile) ──────────
   En desktop el toast top-center (AntD Pro) funciona bien — la vista es
   amplia, el header ocupa poco porcentaje, y el patrón AntD Pro es top.

   En móvil el toast pegado al edge superior queda casi encima del header /
   status bar y es poco perceptible — más aún en WebView Capacitor sin
   overlaysWebView donde el safe-area-inset-top devuelve 0. El patrón
   Material (Android nativo) y AntD Mobile usa bottom con margen.

   Ofset: elevar por encima del bottom-nav del portal cliente (64px) +
   safe-area-inset-bottom del notch/gesture-bar. Si el layout no tiene
   bottom-nav (admin mobile, login), queda simplemente a `--space-lg` +
   safe-area — perfecto, no hay nada que esquivar. */
@media (max-width: 767.98px) {
    .toast-stack {
        inset-block-start: auto;
        inset-block-end: calc(var(--bottom-nav-height, 0px) + var(--space-lg) + env(safe-area-inset-bottom, 0px));
        flex-direction: column-reverse;  /* nuevos toasts aparecen arriba del stack */
    }

    .toast {
        /* Sombra reforzada + ancho mínimo más generoso — estilo Snackbar */
        padding: 12px 20px;
        font-size: 15px;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18),
                    0 4px 12px -2px rgba(0, 0, 0, 0.14);
        max-width: min(92vw, 420px);
    }
}

/* Layouts sin bottom-nav (admin mobile, login, páginas auth): cuando el
   --bottom-nav-height no aplica al contexto, el navegador usa 0 del
   fallback — el cálculo queda en --space-lg + safe-area, pegado al
   edge inferior sin solapar nada. No necesita override explícito. */

.toast {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 10px 16px;
    background-color: var(--color-bg-elevated);
    border-radius: var(--radius-md);
    box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
                0 3px 6px -4px rgba(0, 0, 0, 0.12),
                0 9px 28px 8px rgba(0, 0, 0, 0.05);
    color: var(--color-text);
    font-size: var(--font-size-sm);
    max-width: min(90vw, 480px);
}

/* Icono circular coloreado a la izquierda (AntD) */
.toast__icon {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    display: inline-grid;
    place-items: center;
}

.toast__icon .icon {
    width: 16px;
    height: 16px;
}

.toast__body {
    flex: 1;
    min-width: 0;
    line-height: 1.4;
}

.toast__title {
    display: block;
    font-weight: var(--font-weight-medium);
    margin-block-end: 2px;
}

/* ── Colores por variante — icon-only (AntD no colorea ni fondo ni borde) ─── */
.toast[data-variant="success"] .toast__icon { color: var(--color-success); }
.toast[data-variant="warning"] .toast__icon { color: var(--color-warning); }
.toast[data-variant="danger"]  .toast__icon { color: var(--color-danger);  }
.toast[data-variant="info"]    .toast__icon { color: var(--color-primary-text); }



/* ── modal.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/modal.css — Bloque: diálogo modal centrado

   Estructura HTML típica (con Alpine.js para apertura/cierre):

   <div x-data="{ open: false }">
       <button @click="open = true">Abrir</button>

       <div x-show="open" x-transition.opacity
            @keydown.escape.window="open = false"
            @click="open = false"
            class="modal__overlay">
           <div class="modal" @click.stop role="dialog" aria-modal="true">
               <header class="modal__header">
                   <h2 class="modal__title">Título</h2>
                   <button @click="open = false" class="modal__close" aria-label="Cerrar">
                       {% include 'icons/x.svg.twig' %}
                   </button>
               </header>
               <div class="modal__body">...</div>
               <footer class="modal__footer">
                   <button class="btn" data-variant="ghost" @click="open = false">Cancelar</button>
                   <button class="btn" data-variant="primary">Confirmar</button>
               </footer>
           </div>
       </div>
   </div>
   ═══════════════════════════════════════════════════════════════════════════ */

.modal__overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: grid;
    place-items: center;
    z-index: var(--z-modal);

    /* Padding del overlay = espacio que el modal NO puede ocupar. Mantiene el
       modal dentro del área de contenido principal, sin pisar sidebar/header/
       footer. El overlay sigue dimando todo el viewport para mantener foco.
       Derecha usa space-xl (32px) para compensar visualmente que a la
       izquierda el modal tiene el "cuerpo visual" del sidebar al lado.     */
    padding-block-start:  calc(var(--header-height) + var(--space-md));
    padding-block-end:    calc(var(--footer-height) + var(--space-md));
    padding-inline-start: calc(var(--sidebar-width) + var(--space-md));
    padding-inline-end:   var(--space-xl);
}

/* Sidebar colapsado: el modal gana espacio a la izquierda.
   SOLO desktop — en mobile el sidebar es un drawer fuera del flow,
   no hay espacio que compensar. Sin el @media, el atributo
   data-sidebar-collapsed (persistido en localStorage desde sesión
   desktop previa) filtraba al mobile → modal con padding-inline-start
   de 80px vs padding-inline-end de 16px → modal desplazado a la derecha. */
@media (min-width: 48rem) {
    html[data-sidebar-collapsed="true"] .modal__overlay {
        padding-inline-start: calc(var(--sidebar-width-collapsed) + var(--space-md));
    }
}

/* Móvil (≤768px) — overlay con padding asimétrico DELIBERADO para centrar
   el modal en la zona VISIBLE del viewport (arriba de la barra Android/iOS).

   Con viewport-fit=cover, el viewport se extiende bajo la barra del
   sistema. Si usamos padding simétrico, el modal queda centrado en
   "viewport/2" = centro TÉCNICO del viewport, que cae parcialmente
   detrás de la barra → el usuario lo percibe como "desplazado hacia
   abajo".

   Compensación: padding-block-end = padding-block-start + env(safe-area).
   Eso desplaza el centro del modal hacia ARRIBA exactamente por la
   altura de la barra → el modal queda en el centro del área visible.

   Los md/lg full-screen pasan a padding: 0 más abajo mediante selectores
   scoped — para ellos el centrado no importa (cubren el viewport). */
@media (max-width: 47.99rem) {
    .modal__overlay {
        padding-inline-start: var(--space-md);
        padding-inline-end:   var(--space-md);
        padding-block-start:  var(--space-md);
        padding-block-end:    calc(var(--space-md) + env(safe-area-inset-bottom, 0));
    }
}

/* Layer superior — para modales que deben aparecer SOBRE otros modales/drawers.
   Uso canónico: modal de confirmación global (confirm destructivo) que puede
   dispararse desde cualquier contexto, incluido otro modal abierto.         */
.modal__overlay[data-layer="top"] {
    z-index: var(--z-modal-encima);
}

.modal {
    background-color: var(--color-bg-elevated);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xl);
    width: 100%;
    max-width: var(--container-sm);
    max-height: calc(100dvh - var(--space-2xl));
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.modal__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-block-end: 1px solid var(--color-border);
}

.modal__title {
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
}

/* Botón de cerrar — área clickable 40x40 (AntD ~46x56, WCAG 2.1 AA mínimo 44).
   Hover con fondo suave redondeado estilo AntD. Icono centrado a 16x16.      */
.modal__close {
    display: inline-grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    margin: calc(-1 * var(--space-xs));   /* compensa visualmente para alinear con el título */
    background: transparent;
    border: none;
    cursor: pointer;
    color: var(--color-text-muted);
    border-radius: var(--radius-md);
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.modal__close .icon {
    width: 16px;
    height: 16px;
}

.modal__close:hover {
    background-color: var(--color-bg-alt);
    color: var(--color-text);
}

.modal__close:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* Cuando header/body/footer están envueltos en un wrapper (patrón habitual
   con Alpine: <form>, <div x-data>, etc.), el wrapper debe heredar el
   layout flex-column de .modal para que el body scrollee dentro y el
   footer quede fijo. Sin esto, un body con mucho contenido (o con un
   <details>/disclosure expandido) empuja el footer fuera del viewport.

   Se aplica a cualquier hijo directo de .modal que NO sea header/body/
   footer (los tres elementos "conocidos") — cubre <form>, <div x-data>,
   y cualquier otro wrapper futuro. */
.modal > form,
.modal > [x-data] {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.modal__body {
    padding: var(--space-lg);
    overflow-y: auto;
    flex: 1;
    min-height: 0;
}

/* Modales con tabs dentro — anclar altura para que el cambio de tab no salte
   el layout (patrón AntD Pro). La tab más alta define el alto; las cortas
   dejan aire debajo pero el footer queda fijo.

   SOLO en desktop: en mobile el modal ya es full-screen (height: 100dvh) y
   el body con flex:1 gestiona el layout. Aplicar min-height: 500px aquí
   sumaba con header (~60px) + footer apilado (~140px) + safe-areas,
   excediendo 100dvh en viewports típicos (640-720px) y empujando el
   footer (Cancelar/Guardar) fuera del viewport. Es el síntoma "el botón
   Cancelar se corta" que solo pasaba en modales con tabs (form empleado).*/
@media (min-width: 48rem) {
    .modal > form:has(.tabs) .modal__body,
    .modal:has(> .tabs) .modal__body,
    /* Modales de detalle (no form) que envuelven en <div x-data> y tienen
       tabs internas. Mismo problema que el form — sin min-height las tabs
       cortas hacen saltar la altura. */
    .modal > [x-data]:has(.tabs) .modal__body {
        min-height: 500px;
    }
}

/* Scrollbar fino estilo AntD — visible pero discreto.
   Firefox usa scrollbar-width, Chromium usa ::-webkit-scrollbar. */
.modal__body {
    scrollbar-width: thin;
    scrollbar-color: var(--color-border-strong) transparent;
}

.modal__body::-webkit-scrollbar {
    width: 6px;
}
.modal__body::-webkit-scrollbar-track {
    background: transparent;
}
.modal__body::-webkit-scrollbar-thumb {
    background-color: var(--color-border);
    border-radius: 3px;
}
.modal__body::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-border-strong);
}

/* Modificador: cuerpo sin padding interno.
   Útil cuando el contenido (ej. galería con tabs) gestiona su propio padding.
   Mantiene el overflow-y: auto por defecto del body — el scroll es único en
   el cuerpo, no anidado en hijos. */
.modal__body[data-padding="none"] {
    padding: 0;
}

/* Alert como primer hijo del modal body — distancia compacta a ambos lados.
   Sin esto el alert quedaba con `space-lg` arriba (padding del body) y
   `space-md` abajo (lobotomized owl de .flow) → flotante en el centro.

   La regla del primer hijo aplica un margin negativo que reduce la
   distancia con la cabecera de `space-lg` a `space-sm`. La regla del
   hermano siguiente fija `space-sm` (anulando el `space-md` del .flow)
   para que los tabs queden cerca pero con respiración. */
.modal__body > .alert:first-child {
    margin-block-start: calc(var(--space-sm) - var(--space-lg));
}
.modal__body > .alert:first-child + * {
    margin-block-start: var(--space-sm);
}

.modal__footer {
    display: flex;
    flex-wrap: wrap;   /* con muchas acciones (Cerrar, Despublicar, Archivar, Eliminar, Editar) bajan
                          de línea; sin wrap, justify-content: flex-end empujaba la primera fuera del modal */
    justify-content: flex-end;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
    border-block-start: 1px solid var(--color-border-subtle);
    background-color: var(--color-bg-elevated);
    flex-shrink: 0;    /* el body es quien scrolea; footer siempre visible */
    /* Respeta teclado virtual en móvil — el footer queda por encima del keyboard.
       Fallback a 0 para browsers sin env() — así el max() siempre evalúa a
       var(--space-md) como mínimo, nunca se invalida la propiedad. */
    padding-block-end: max(var(--space-md), env(safe-area-inset-bottom, 0));
}

/* ── Móvil: full-screen + footer apilado (AntD móvil) ────────────────────── */
@media (max-width: 39.99rem) {
    /* Si el modal va a ir full-screen (NO size="sm"), el overlay no
       debe tener padding — si no, el modal de `height: 100dvh` centrado
       con grid se sale del área padded y se recorta ~80px por abajo
       (el síntoma "no se ve centrado / cortado"). Con `:has()` lo
       aplicamos solo cuando corresponde; size="sm" sigue siendo pill
       centrado con su padding. */
    .modal__overlay:has(> .modal:not([data-size="sm"])) {
        padding: 0;
        place-items: stretch;
    }

    /* modales md/sm → full width/height (excepto size="sm" que mantiene pill).
       100dvh ya se adapta al viewport dinámico (browser UI), pero con
       viewport-fit=cover + Android gesture bar, el viewport "cubre" la barra.
       Restamos env(safe-area-inset-*) para que el modal no quede DEBAJO
       de ella — el footer con sus botones queda siempre dentro del área
       visible.

       Defensiva con `, 0` como fallback:
         - Navegadores modernos sin safe-areas (dispositivo plano): env=0,
           modal = 100dvh edge-to-edge. Funciona.
         - Navegadores muy antiguos que NO soportan env(): el fallback 0
           evita invalidar la propiedad entera (que haría revertir a las
           reglas desktop de max-width: 520px → modal tiny en mobile).
         - iOS con home indicator / Android con gesture bar (con
           viewport-fit=cover): env reporta el inset real, el modal se
           acorta y el footer queda visible. */
    .modal:not([data-size="sm"]):not([data-size="full"]) {
        max-width: 100%;
        max-height: calc(100dvh - env(safe-area-inset-top, 0) - env(safe-area-inset-bottom, 0));
        width: 100%;
        height: calc(100dvh - env(safe-area-inset-top, 0) - env(safe-area-inset-bottom, 0));
        margin-block-start: env(safe-area-inset-top, 0);
        margin-block-end:   env(safe-area-inset-bottom, 0);
        border-radius: 0;
    }

    /* Footer en columna, botones full-width, submit arriba (pulgar accesible) */
    .modal__footer {
        flex-direction: column-reverse;
        gap: var(--space-xs);
    }
    .modal__footer .btn {
        width: 100%;
        justify-content: center;
    }
}

/* ── Variantes de tamaño ─────────────────────────────────────────────────── */

.modal[data-size="sm"]  { max-width: 24rem; }
.modal[data-size="md"]  { max-width: 520px; }   /* AntD Modal default */
.modal[data-size="lg"]  { max-width: var(--container-lg); }

/* full = ocupa todo el área de contenido disponible (ya limitada por el
   padding del overlay). Mantiene radius y sombra — no es edge-to-edge.     */
.modal[data-size="full"] {
    max-width: 100%;
    max-height: 100%;
    width: 100%;
    height: 100%;
}



/* ── dropdown.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/dropdown.css — Bloque: menú desplegable contextual

   Estructura HTML típica (con Alpine.js):

   <div x-data="{ open: false }" @click.outside="open = false" class="dropdown">
       <button @click="open = !open" :aria-expanded="open" class="dropdown__trigger">
           Acciones
           {% include 'icons/chevron-down.svg.twig' %}
       </button>

       <ul x-show="open" x-transition class="dropdown__menu" role="menu">
           <li><a class="dropdown__item" href="#" role="menuitem">Editar</a></li>
           <li><a class="dropdown__item" href="#" role="menuitem">Duplicar</a></li>
           <li class="dropdown__separator" role="separator"></li>
           <li><a class="dropdown__item" data-variant="danger" href="#" role="menuitem">Eliminar</a></li>
       </ul>
   </div>
   ═══════════════════════════════════════════════════════════════════════════ */

.dropdown {
    position: relative;
    display: inline-block;
}

.dropdown__trigger {
    /* No tiene estilos propios — suele combinarse con .btn */
    cursor: pointer;
}

.dropdown__menu {
    position: absolute;
    inset-block-start: calc(100% + var(--space-xs));
    inset-inline-end: 0;
    /* AntD Pro user avatar dropdown: 220px típico para acomodar email
       sin wrap. Usamos 14rem (224px) como estándar canónico compartido
       por todos los dropdowns de usuario (admin + cliente). */
    min-width: 14rem;
    padding: var(--space-xs);
    margin: 0;
    list-style: none;
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    z-index: var(--z-desplegable);
}

/* Variante: alinear a la izquierda en lugar de derecha */
.dropdown__menu[data-align="start"] {
    inset-inline-end: auto;
    inset-inline-start: 0;
}

.dropdown__item {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    width: 100%;
    padding: var(--space-sm) var(--space-md);
    font-size: var(--font-size-sm);
    color: var(--color-text);
    text-decoration: none;
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    text-align: start;
}

/* color + text-decoration: un <a class="dropdown__item"> es un item de menú;
   sin esto se llevaría el color y el subrayado de a:hover (links.css). Las
   variantes de abajo (danger, disabled) siguen ganando por ir después. */
.dropdown__item:hover,
.dropdown__item:focus-visible {
    background-color: var(--color-bg-alt);
    color: var(--color-text);
    text-decoration: none;
    outline: none;
}

.dropdown__item[data-variant="danger"] {
    color: var(--color-danger-text);
}

.dropdown__item[data-variant="danger"]:hover {
    background-color: var(--color-danger-soft);
}

.dropdown__item:disabled,
.dropdown__item[aria-disabled="true"] {
    color: var(--color-text-subtle);
    cursor: not-allowed;
}

.dropdown__separator {
    height: 1px;
    margin-block: var(--space-xs);
    background-color: var(--color-border-subtle);
}

/* Header del dropdown — info usuario, no clickable */
.dropdown__header {
    display: flex;
    flex-direction: column;
    padding: var(--space-sm) var(--space-md);
    line-height: 1.3;
}

.dropdown__header strong {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
}

.dropdown__header-sub {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Item tipo submit-form — sin fondo de botón */
.dropdown__item[type="submit"],
button.dropdown__item {
    width: 100%;
    font: inherit;
}



/* ── avatar.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/avatar.css — Bloque: avatar circular

   Acepta texto (iniciales) o imagen como hijo. Variantes de tamaño con
   data-size: sm | md (default) | lg | xl

   Uso:
       <span class="avatar" aria-label="Juan Pérez">JP</span>
       <img class="avatar" data-size="lg" src="..." alt="Juan Pérez">
   ═══════════════════════════════════════════════════════════════════════════ */

.avatar {
    /* Tamaño base — md */
    --_avatar-size: 36px;
    --_avatar-font: var(--font-size-sm);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--_avatar-size);
    height: var(--_avatar-size);
    border-radius: var(--radius-full);
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
    font-size: var(--_avatar-font);
    font-weight: var(--font-weight-medium);
    line-height: 1;
    object-fit: cover;
    flex-shrink: 0;
    text-transform: uppercase;
    user-select: none;
}

.avatar[data-size="sm"] {
    --_avatar-size: 24px;
    --_avatar-font: var(--font-size-xs);
}

.avatar[data-size="lg"] {
    --_avatar-size: 48px;
    --_avatar-font: var(--font-size-base);
}

.avatar[data-size="xl"] {
    --_avatar-size: 64px;
    --_avatar-font: var(--font-size-lg);
}

/* Stack de avatares solapados — útil para listas de participantes */
.avatar-stack {
    display: inline-flex;
}

.avatar-stack > .avatar {
    border: 2px solid var(--color-bg);
    margin-inline-start: -8px;
}

.avatar-stack > .avatar:first-child {
    margin-inline-start: 0;
}



/* ── spinner.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/spinner.css — Bloque: indicador de carga circular

   Animación CSS pura, sin JS. Hereda color del padre via currentColor.
   Variantes con data-size: sm | md (default) | lg

   Uso:
       <span class="spinner" aria-label="Cargando"></span>
       <span class="spinner" data-size="lg"></span>

   Combinable con HTMX — añadir clase "htmx-indicator" para que solo aparezca
   durante peticiones activas:
       <span class="spinner htmx-indicator"></span>
   ═══════════════════════════════════════════════════════════════════════════ */

.spinner {
    --_spinner-size: 18px;
    --_spinner-thickness: 2px;

    display: inline-block;
    width: var(--_spinner-size);
    height: var(--_spinner-size);
    border: var(--_spinner-thickness) solid currentColor;
    border-block-end-color: transparent;
    border-radius: var(--radius-full);
    animation: spinner-rotate 0.8s linear infinite;
    flex-shrink: 0;
    vertical-align: -0.2em;
}

.spinner[data-size="sm"] {
    --_spinner-size: 14px;
    --_spinner-thickness: 2px;
}

.spinner[data-size="lg"] {
    --_spinner-size: 32px;
    --_spinner-thickness: 3px;
}

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

/* Respeta usuarios con prefers-reduced-motion: hace pulsar en lugar de rotar */
@media (prefers-reduced-motion: reduce) {
    .spinner {
        animation: spinner-pulse 1.5s ease-in-out infinite;
    }

    @keyframes spinner-pulse {
        0%, 100% { opacity: 1; }
        50%      { opacity: 0.4; }
    }
}



/* ── empty-state.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/empty-state.css — Bloque: estado vacío

   Patrón unificado para "no hay datos aún" / "sin resultados".
   Componer con icono grande + título + descripción + CTA opcional.

   Estructura HTML:

   <div class="empty-state">
       <div class="empty-state__icon">
           {% include 'icons/users.svg.twig' with { class: 'icon--xl' } %}
       </div>
       <h2 class="empty-state__title">Sin socios aún</h2>
       <p class="empty-state__desc">Crea el primer socio para empezar.</p>
       <div class="empty-state__actions">
           <a class="btn" data-variant="primary">Nuevo socio</a>
       </div>
   </div>
   ═══════════════════════════════════════════════════════════════════════════ */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: var(--space-sm);
    padding: calc(var(--space-2xl) * 1.5) var(--space-lg);
    color: var(--color-text-muted);
}

/* Ilustración — círculo grande con icono centrado. AntD Empty usa una
   ilustración simple + gradiente suave. Aquí combinamos icono + fondo. */
.empty-state__icon {
    display: inline-grid;
    place-items: center;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--color-primary-soft) 0%, var(--color-bg-alt) 100%);
    color: var(--color-primary-text);
    margin-block-end: var(--space-sm);
}

.empty-state__icon .icon,
.empty-state__icon svg {
    width: 40px;
    height: 40px;
    opacity: 0.85;
}

.empty-state__title {
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
}

.empty-state__desc {
    max-width: 360px;
    margin: 0;
    line-height: 1.5;
}

.empty-state__actions {
    display: flex;
    gap: var(--space-sm);
    margin-block-start: var(--space-md);
    flex-wrap: wrap;
    justify-content: center;
}

/* Variante compacta — para cuando el empty está dentro de un modal/card pequeño */
.empty-state[data-size="sm"] {
    padding: var(--space-xl) var(--space-md);
}
.empty-state[data-size="sm"] .empty-state__icon {
    width: 64px;
    height: 64px;
}
.empty-state[data-size="sm"] .empty-state__icon .icon,
.empty-state[data-size="sm"] .empty-state__icon svg {
    width: 28px;
    height: 28px;
}



/* ── nav-sidebar.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/nav-sidebar.css — Sidebar dark estilo Ant Design Pro
   Fondo navy oscuro `#001529`. Link activo con bg primary saturado.
   ═══════════════════════════════════════════════════════════════════════════ */

.nav-sidebar {
    background-color: var(--color-sidebar-bg);
    color: var(--color-sidebar-text);
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}

/* ── Estado colapsado — solo iconos (64px) ──────────────────────────────
   Los textos NO cambian de layout. Siempre renderizados (no display:none)
   para que estén "ya ahí" cuando el sidebar se expande — el propio overflow
   del sidebar los revela progresivamente al ensanchar.
   Doble selector: html[...] activa antes de Alpine (evita flash en F5);
   .nav-sidebar[...] mantiene sync cuando Alpine togglea en caliente.

   Solo aplica en desktop. En móvil la sidebar es un drawer full-slide,
   el estado colapsado del desktop no tiene sentido (desencadenaba que
   al abrir el drawer solo se vieran las cabeceras si el user había
   colapsado la sidebar en una sesión desktop previa). */
@media (min-width: 48rem) {
    /* Transición sincronizada con grid-template-columns del layout —
       el texto aparece/desaparece gradualmente mientras el sidebar se
       ensancha/estrecha. Sin esto el label iba de opacity 0→1 INSTANTE
       al hacer click (data-sidebar-collapsed flip) mientras el ancho
       tardaba ~300ms: el usuario veía "texto pop-in primero, ancho
       después" como un redibujado al acabar. */
    .nav-sidebar__brand-text,
    .nav-sidebar__section-title,
    .nav-sidebar__link span,
    .nav-sidebar__group-trigger > span {
        transition: opacity var(--transition-normal);
    }

    html[data-sidebar-collapsed="true"] .nav-sidebar__brand-text,
    html[data-sidebar-collapsed="true"] .nav-sidebar__section-title,
    html[data-sidebar-collapsed="true"] .nav-sidebar__link span,
    html[data-sidebar-collapsed="true"] .nav-sidebar__group-trigger > span,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__brand-text,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__section-title,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__link span,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__group-trigger > span {
        opacity: 0;
        pointer-events: none;
    }
}

/* ── Brand (arriba) ───────────────────────────────────────────────────────
   Es un <a href="/app/dashboard"> — click navega al dashboard.
   Reset de estilos link + transición sutil de hover.                         */
.nav-sidebar__brand {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-md);
    height: var(--header-height);
    background-color: var(--color-sidebar-bg-darker);
    flex-shrink: 0;
    text-decoration: none;
    color: inherit;
    transition: background-color var(--transition-fast);
}

.nav-sidebar__brand:hover {
    background-color: rgba(255, 255, 255, 0.04);
    /* Sin underline en hover (links.css global aplica underline a TODO
       <a>:hover). El brand es una pill compacta, el underline rompe la
       estética del nav dark. */
    text-decoration: none;
}

.nav-sidebar__brand-logo {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-md);
    background-color: var(--color-primary);
    display: grid;
    place-items: center;
    color: var(--color-text-on-dark);
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size-md);
    flex-shrink: 0;
}

.nav-sidebar__brand-text {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
    min-width: 0;
    white-space: nowrap;
}

.nav-sidebar__brand-name {
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-on-dark);
    font-family: var(--font-heading);
}

.nav-sidebar__brand-sub {
    font-size: var(--font-size-xs);
    color: var(--color-sidebar-text-muted);
}

/* ── Nav (scrollable, expande para empujar user abajo) ─────────────────── */
.nav-sidebar__nav {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-md) var(--space-sm);
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    /* Scrollbar fino estilo AntD — mismo patrón canónico que .modal__body
       y .layout-cliente__main pero adaptado al fondo oscuro del sidebar.
       Firefox usa scrollbar-width/color, Chromium usa ::-webkit-scrollbar. */
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.nav-sidebar__nav::-webkit-scrollbar {
    width: 6px;
}
.nav-sidebar__nav::-webkit-scrollbar-track {
    background: transparent;
}
.nav-sidebar__nav::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, 0.18);
    border-radius: 3px;
}
.nav-sidebar__nav::-webkit-scrollbar-thumb:hover {
    background-color: rgba(255, 255, 255, 0.32);
}

/* Sección — grupo de links con heading */
.nav-sidebar__section {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.nav-sidebar__section-title {
    padding: var(--space-xs) var(--space-md);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    letter-spacing: var(--letter-spacing-wide);
    text-transform: uppercase;
    color: var(--color-sidebar-text-muted);
    margin-block-end: var(--space-xs);
    white-space: nowrap;
}

.nav-sidebar__link {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 0.5rem var(--space-md);
    color: var(--color-sidebar-text);
    text-decoration: none;
    border-radius: var(--radius-md);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-regular);
    /* Wrap a 2 líneas si el label no cabe — en todos los breakpoints.
       Patrón AntD Pro: truncar con "..." en nav items pierde contexto
       (el label ES la información principal, no hay columna/metadata
       adyacente como en celdas de tabla). Mejor 2 líneas que ellipsis.
       Aplicado también en desktop tras pregunta user 2026-04-22: labels
       como "Dispositivos conectados" rozaban el borde de la sidebar. */
    white-space: normal;
    line-height: 1.25;
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

/* Estado colapsado (solo iconos) — fuerza nowrap en el contenedor
   para que no se vea un segundo renglón durante el fade-out. Los
   textos ya tienen opacity:0 por la regla de arriba; esto solo
   asegura altura constante del item mientras se colapsa. */
@media (min-width: 48rem) {
    html[data-sidebar-collapsed="true"] .nav-sidebar__link,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__link {
        white-space: nowrap;
    }
}

/* Durante la animación de colapsar/expandir (grid-template-columns
   transition del layout), forzar nowrap en los links del sidebar.
   Sin esto, en anchos intermedios el texto wrappea a 2 líneas y los
   items crecen/encogen verticalmente → efecto "texto aparece desde
   abajo, iconos se mueven". El flag is-sidebar-animating lo añade/
   quita app.js al escuchar transitionstart/end. */
html.is-sidebar-animating .nav-sidebar__link,
html.is-sidebar-animating .nav-sidebar__group-trigger {
    white-space: nowrap;
}

.nav-sidebar__link .icon {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    opacity: 0.85;
}

.nav-sidebar__link:hover {
    background-color: var(--color-sidebar-hover);
    color: var(--color-text-on-dark);
    /* Sin underline en hover (links.css global aplica underline a TODO
       <a>:hover). Los items del nav son botones visuales, no enlaces
       en prosa — el underline pierde sentido y rompe la estética. */
    text-decoration: none;
}

.nav-sidebar__link[aria-current="page"] {
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    transition: none;
}

.nav-sidebar__link[aria-current="page"] .icon {
    opacity: 1;
    color: var(--color-text-on-dark);
}




/* ── nav-bottom.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/nav-bottom.css — Bloque: navegación inferior móvil

   Estilo Material Design / Slack móvil. Solo visible en mobile.
   En desktop (breakpoint 48rem) queda oculto y se usa la sidebar.

   Estructura HTML:

   <nav class="nav-bottom" aria-label="Navegación principal">
       <a href="..." class="nav-bottom__link" data-active="true">
           {% include 'icons/layout-dashboard.svg.twig' %}
           <span class="nav-bottom__label">Inicio</span>
       </a>
       ...
   </nav>
   ═══════════════════════════════════════════════════════════════════════════ */

.nav-bottom {
    display: flex;
    justify-content: space-around;
    align-items: stretch;
    height: var(--bottom-nav-height);
    background-color: var(--color-bg-elevated);
    border-block-start: 1px solid var(--color-border);
    box-shadow: 0 -2px 8px rgba(26, 95, 122, 0.06);
    /* El safe-area bottom lo gestiona ahora `body::after` strip + layout
       a 100svh (el nav queda SOBRE la barra automáticamente). Añadir
       padding-block-end: env() aquí aplastaría los iconos dentro de los
       64px del grid row — síntoma que reportó el user: "nav cortado". */
}

@media (min-width: 48rem) {  /* --bp-md */
    /* En admin el bottom-nav se oculta (admin usa drawer). En cliente SÍ se
       mantiene visible en desktop — es el patrón "app móvil en canvas".     */
    .layout-app .nav-bottom {
        display: none;
    }
}

.nav-bottom__link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    flex: 1;
    /* Padding suficiente para tap target ≥ 44px (WCAG 2.1 AA) */
    padding: var(--space-sm);
    color: var(--color-text-muted);
    text-decoration: none;
    transition: color var(--transition-fast);
    min-width: 0;
    position: relative;
}

/* Anula el color y el subrayado de a:hover (links.css), que también se
   quedaría pegado tras un tap. Va antes del hover con cursor y de
   [data-active] para que esos sigan ganando. */
.nav-bottom__link:hover {
    color: var(--color-text-muted);
    text-decoration: none;
}

/* :hover solo en cursor real — el bottom-nav es mobile-only pero por
   desktop responsive puede verse; evita sticky-hover. focus-visible
   siempre (accesibilidad teclado). */
html:not(.is-touch) .nav-bottom__link:hover {
    color: var(--color-text);
    outline: none;
}
.nav-bottom__link:focus-visible {
    color: var(--color-text);
    outline: none;
}

.nav-bottom__link[data-active="true"] {
    color: var(--color-primary-text);
}

.nav-bottom__link .icon {
    width: 22px;
    height: 22px;
}

.nav-bottom__label {
    font-size: var(--font-size-xs);
    line-height: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}

/* Badge de contador (notificaciones sin leer). Se posiciona sobre el
   icono del link. Mínimo 20×20px para visibilidad en móvil. */
.nav-bottom__badge {
    position: absolute;
    top: 4px;
    right: 50%;
    margin-right: -20px;
    background: var(--color-danger, #ff4d4f);
    color: white;
    min-width: 20px;
    height: 20px;
    border-radius: 10px;
    padding: 0 5px;
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

/* Respetar el atributo `hidden` del HTML. Sin esto, el `display: inline-flex`
   de la clase sobreescribe el default del browser para [hidden] y el badge
   se pinta brevemente como círculo rojo vacío al cargar la página, aunque
   el server haya renderizado `<span hidden>` por tener 0 notificaciones.
   Síntoma reportado: "pestañeo del icono de notificaciones al entrar al
   portal del cliente". */
.nav-bottom__badge[hidden] {
    display: none;
}



/* ── app-header.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/app-header.css — Bloque: cabecera de la zona autenticada
   ═══════════════════════════════════════════════════════════════════════════ */

.app-header {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 0 var(--space-sm);
    background-color: var(--color-bg-elevated);
    border-bottom: 1px solid var(--color-border-subtle);
}

.app-header__slot {
    flex: 1;
    min-width: 0;
}

/* Botón genérico del header — cuadrado 40x40, hover bg-alt */
.app-header__action {
    position: relative;
    display: inline-grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    border-radius: var(--radius-md);
    transition: background-color var(--transition-fast), color var(--transition-fast);
    text-decoration: none;
}

.app-header__action:hover {
    background-color: var(--color-bg-alt);
    color: var(--color-text);
    text-decoration: none;               /* el <a> no se subraya (links.css) */
}

.app-header__action .icon {
    width: 18px;
    height: 18px;
}

/* Con su nombre al lado del icono («Ayuda»). En el móvil, solo el icono: el
   nombre sigue en su aria-label. */
.app-header__texto {
    display: none;
}

@media (min-width: 48rem) {
    .app-header__action[data-con-texto] {
        width: auto;
        grid-auto-flow: column;
        gap: var(--space-xs);
        padding-inline: var(--space-sm);
        font-size: var(--font-size-sm);
        font-weight: var(--font-weight-medium);
    }

    .app-header__action[data-con-texto] .app-header__texto {
        display: inline;
    }
}

/* Badge numérico sobre el icono (notificaciones) */
.app-header__badge {
    position: absolute;
    inset-block-start: 4px;
    inset-inline-end: 4px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    font-size: 10px;
    font-weight: var(--font-weight-semibold);
    line-height: 16px;
    text-align: center;
    color: var(--color-text-on-dark);
    background-color: var(--color-danger-strong);
    border-radius: var(--radius-full);
    border: 2px solid var(--color-bg-elevated);
    box-sizing: content-box;
}

/* Trigger del dropdown de usuario — avatar + nombre + chevron */
.app-header__user {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 0 var(--space-sm) 0 4px;
    height: 40px;
    border: none;
    background: transparent;
    color: var(--color-text);
    cursor: pointer;
    border-radius: var(--radius-md);
    transition: background-color var(--transition-fast);
}

.app-header__user:hover {
    background-color: var(--color-bg-alt);
}

.app-header__user-name {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    max-width: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-header__user-chevron {
    width: 14px;
    height: 14px;
    color: var(--color-text-muted);
}

@media (max-width: 47.99rem) {
    .app-header__user-name,
    .app-header__user-chevron {
        display: none;
    }
    .app-header__user {
        padding: 0;
        width: 40px;
        justify-content: center;
    }
}



/* ── error-page.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/error-page.css — Bloque: páginas de error (404, 403, 500, etc.)
   ═══════════════════════════════════════════════════════════════════════════ */

.error-page {
    min-height: 100dvh;   /* fallback */
    min-height: 100svh;   /* estable ante retracción de URL bar en mobile */
    display: grid;
    place-items: center;
    padding: var(--space-lg);
    background-color: var(--color-bg-alt);
}

.error-page__inner {
    max-width: var(--container-sm);
    text-align: center;
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-xl) var(--space-lg);
    box-shadow: var(--shadow-md);
}

.error-page__code {
    font-size: 6rem;
    font-weight: var(--font-weight-bold);
    line-height: 1;
    color: var(--color-primary-text);
    letter-spacing: -0.05em;
}

.error-page__title {
    font-size: var(--font-size-xl);
    color: var(--color-text);
}

.error-page__mensaje {
    color: var(--color-text-muted);
    max-width: var(--container-xs);
    margin-inline: auto;
}

.error-page__acciones {
    margin-block-start: var(--space-lg);
}



/* ── datepicker.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/datepicker.css — Campos de fecha y hora (controles nativos)

   Las fechas y horas son los controles del navegador:
       <input type="date"            class="form__input">   → Y-m-d
       <input type="time"            class="form__input">   → H:i
       <input type="datetime-local"  class="form__input">   → Y-m-d\TH:i

   Por qué nativos (antes, flatpickr):
     - El calendario es una capa del navegador: ningún modal ni cajón lo
       recorta ni lo tapa, y no hay capas ni z-index que ajustar.
     - Sigue el tema: `color-scheme` (tokens.css y theme-dark.css) pinta el
       calendario, el icono y los menús en claro u oscuro.
     - En el móvil abre el selector del sistema; con teclado se escribe por
       partes (día, mes, año); los lectores de pantalla lo conocen.
     - Envía siempre ISO, sea cual sea el idioma de quien lo usa, y muestra
       la fecha con el formato de su idioma (dd/mm/aaaa en español).

   Aquí solo se iguala la caja al resto de inputs (.form__input ya pone
   borde, radio, foco y error) y se cuida el icono del calendario.
   `min` / `max` / `step` son atributos HTML normales.
   ═══════════════════════════════════════════════════════════════════════════ */

.form__input:is([type="date"], [type="time"], [type="datetime-local"], [type="month"], [type="week"]) {
    /* Misma altura que un input de texto (línea de 1.4 + relleno + borde):
       Chrome la crece con el icono y Safari la encoge si está vacío. */
    block-size: calc(1.4em + 1.25rem + 2px);
    cursor: text;
}

/* Las partes (día, mes, año) sin relleno extra: el texto queda alineado
   con el de los demás campos. */
.form__input::-webkit-datetime-edit,
.form__input::-webkit-datetime-edit-fields-wrapper {
    padding: 0;
}

.form__input::-webkit-date-and-time-value {
    text-align: start;
}

/* El icono que abre el calendario: discreto en reposo, claro al pasar o
   con foco. Su color lo decide el navegador según `color-scheme`. */
.form__input::-webkit-calendar-picker-indicator {
    cursor: pointer;
    opacity: 0.55;
    padding: 2px;
    margin-inline-start: var(--space-xs);
    border-radius: var(--radius-sm);
    transition: opacity var(--transition-fast), background-color var(--transition-fast);
}

.form__input:focus::-webkit-calendar-picker-indicator,
html:not(.is-touch) .form__input:hover::-webkit-calendar-picker-indicator {
    opacity: 1;
}

html:not(.is-touch) .form__input::-webkit-calendar-picker-indicator:hover {
    background-color: var(--color-bg-alt);
}

.form__input:disabled::-webkit-calendar-picker-indicator {
    cursor: not-allowed;
    opacity: 0.3;
}



/* ── filter-bar.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/filter-bar.css — Bloque: barra de filtros sobre listados

   Patrón consistente para listados: barra de filtros + chips de filtros
   activos + acciones (limpiar todos, exportar, etc.). Diseñado para HTMX:
   cada filtro dispara un GET al endpoint que devuelve el listado actualizado.

   Estructura típica:

   <div class="filter-bar">
       <div class="filter-bar__main">
           <input class="form__input" type="search" hx-get="..." hx-trigger="...">
           <select class="form__select" hx-get="..." hx-trigger="change">...</select>
       </div>

       <div class="filter-bar__chips">
           <span class="filter-chip">
               Categoría: general
               <button class="filter-chip__remove" hx-get="...">×</button>
           </span>
           <button class="filter-chip__clear">Limpiar filtros</button>
       </div>
   </div>
   ═══════════════════════════════════════════════════════════════════════════ */

.filter-bar {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

/* ── Fila principal de filtros ──────────────────────────────────────────── */
.filter-bar__main {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    align-items: center;
}

.filter-bar__main > * {
    flex: 0 1 auto;
}

/* El primer input/search ocupa el espacio sobrante */
.filter-bar__main > input[type="search"],
.filter-bar__main > .filter-bar__search {
    flex: 1 1 16rem;
    min-width: 12rem;
}

/* Selects más estrechos en la barra de filtros */
.filter-bar__main .form__select {
    flex: 0 0 auto;
    width: auto;
    min-width: 10rem;
}

/* ── Chips de filtros activos ────────────────────────────────────────────── */
.filter-bar__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    align-items: center;
    min-height: 1.75rem;        /* reserva espacio para evitar layout shift */
}

.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 0.2rem var(--space-sm);
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
    border-radius: var(--radius-full);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
}

.filter-chip__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    padding: 0;
    border: none;
    background: transparent;
    color: inherit;
    cursor: pointer;
    border-radius: var(--radius-full);
    opacity: 0.7;
}

.filter-chip__remove:hover {
    background-color: var(--color-primary);
    color: var(--color-text-on-dark);
    opacity: 1;
}

.filter-chip__clear {
    background: transparent;
    border: none;
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    cursor: pointer;
    padding: 0.2rem var(--space-sm);
    text-decoration: underline;
}

.filter-chip__clear:hover {
    color: var(--color-text);
}



/* ── page-header.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/page-header.css — Bloque: cabecera de página

   Patrón: icono/avatar + título + subtítulo a la izquierda, acciones a la
   derecha. Uso típico arriba de listados (ej. "Gestión de Socios").

   Estructura:
     <div class="page-header">
       <div class="page-header__title-group">
         <span class="page-header__icon">{icon}</span>
         <div>
           <h1 class="page-header__title">Gestión de Socios</h1>
           <p class="page-header__subtitle">Gestiona la base de socios</p>
         </div>
       </div>
       <div class="page-header__actions">
         <button class="btn" data-variant="ghost">Importar</button>
         <button class="btn" data-variant="secondary">+ Nuevo</button>
       </div>
     </div>
   ═══════════════════════════════════════════════════════════════════════════ */

.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-lg);
    flex-wrap: wrap;
}

.page-header__title-group {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
}

.page-header__icon {
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md);
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
    display: grid;
    place-items: center;
}

.page-header__icon .icon {
    width: 22px;
    height: 22px;
}

.page-header__title {
    font-size: var(--font-size-2xl);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    line-height: 1.2;
    margin: 0;
}

.page-header__subtitle {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin: 0;
    margin-block-start: 2px;
}

.page-header__actions {
    display: flex;
    gap: var(--space-sm);
    flex-wrap: wrap;
    align-items: center;
}

@media (max-width: 47.99rem) {
    .page-header__actions {
        width: 100%;
    }
    .page-header__actions .btn {
        flex: 1;
    }

    /* Título y icono más compactos en móvil — sin perder jerarquía visual.
       AntD Pro baja al siguiente tamaño en la escala cuando el viewport
       es estrecho. */
    .page-header__title {
        font-size: var(--font-size-xl);   /* 20px en vez de 24px */
    }
    .page-header__icon {
        width: 36px;
        height: 36px;
    }
    .page-header__icon .icon {
        width: 18px;
        height: 18px;
    }
}



/* ── stat-card.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/stat-card.css — Bloque: tarjeta de métrica (KPI)

   Uso: fila de 4 cards con métricas arriba de un listado.
   Cada card: icono coloreado + número grande + label pequeño.
   Variantes via data-variant: primary (default) | success | warning | danger
   ═══════════════════════════════════════════════════════════════════════════ */

.stat-grid {
    --stat-grid-gap: var(--space-md);
    --stat-grid-min: 13rem;   /* lo mínimo que ocupa una tarjeta sin partir su etiqueta en tres líneas */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
    gap: var(--stat-grid-gap);
}

/* De cuatro a nueve tarjetas, sin una suelta debajo de las demás (cuatro en
   3 + 1, cinco en 4 + 1, nueve en 4 + 4 + 1). Todas en una fila si caben
   (n); si no, en filas de m (4 → 2 + 2; 5 → 3 + 2; 6 → 3 + 3; 7 → 4 + 3;
   8 → 4 + 4; 9 → 3 + 3 + 3); si no, de dos; si no, una por fila. La
   columna mínima salta de un tamaño a otro con clamp(): el «* 999» convierte
   cada umbral en un interruptor y el «- 0.1px» evita que el redondeo deje
   una columna menos. Con `data-cols` manda el máximo que pide la vista. */
.stat-grid:not([data-cols]):has(> :nth-child(4):last-child) { --_n: 4; --_m: 2; }
.stat-grid:not([data-cols]):has(> :nth-child(5):last-child) { --_n: 5; --_m: 3; }
.stat-grid:not([data-cols]):has(> :nth-child(6):last-child) { --_n: 6; --_m: 3; }
.stat-grid:not([data-cols]):has(> :nth-child(7):last-child) { --_n: 7; --_m: 4; }
.stat-grid:not([data-cols]):has(> :nth-child(8):last-child) { --_n: 8; --_m: 4; }
.stat-grid:not([data-cols]):has(> :nth-child(9):last-child) { --_n: 9; --_m: 3; }

.stat-grid:not([data-cols]):is(
    :has(> :nth-child(4):last-child), :has(> :nth-child(5):last-child), :has(> :nth-child(6):last-child),
    :has(> :nth-child(7):last-child), :has(> :nth-child(8):last-child), :has(> :nth-child(9):last-child)
) {
    --_g: var(--stat-grid-gap);
    --_ancho-n: calc(var(--_n) * var(--stat-grid-min) + (var(--_n) - 1) * var(--_g));
    --_ancho-m: calc(var(--_m) * var(--stat-grid-min) + (var(--_m) - 1) * var(--_g));
    --_ancho-2: calc(2 * var(--stat-grid-min) + var(--_g));
    grid-template-columns: repeat(auto-fit, minmax(clamp(
        clamp(
            clamp(
                (100% - (var(--_n) - 1) * var(--_g)) / var(--_n) - 0.1px,
                (var(--_ancho-n) - 100%) * 999,
                (100% - (var(--_m) - 1) * var(--_g)) / var(--_m) - 0.1px
            ),
            (var(--_ancho-m) - 100%) * 999,
            (100% - var(--_g)) / 2 - 0.1px
        ),
        (var(--_ancho-2) - 100%) * 999,
        100%
    ), 1fr));
}

/* `data-cols="2|3|4"`: como mucho esas tarjetas por fila (muchas tarjetas con
   texto, como las comprobaciones de Diagnóstico); menos si no caben a 14rem.
   Hasta ahora el atributo se usaba sin que nada lo leyera. */
.stat-grid[data-cols="2"] { --_cols: 2; }
.stat-grid[data-cols="3"] { --_cols: 3; }
.stat-grid[data-cols="4"] { --_cols: 4; }

.stat-grid[data-cols] {
    grid-template-columns: repeat(auto-fit, minmax(
        min(100%, max(14rem, (100% - (var(--_cols) - 1) * var(--stat-grid-gap)) / var(--_cols) - 0.1px)),
        1fr
    ));
}

/* En filas de tres, la última que se quedaría sola en su fila (34 tarjetas:
   11 filas y una suelta) la ocupa entera: cierra la rejilla. Solo cuando de
   verdad hay tres columnas, que es cuando caben tres de 14rem con sus dos
   huecos (44rem con el hueco de 1rem de --space-md): a dos columnas la
   cuenta es otra. La rejilla es contenedor de consultas para eso. */
.stat-grid[data-cols="3"] {
    container-type: inline-size;
}

@container (min-width: 44rem) {
    .stat-grid[data-cols="3"] > :last-child:nth-child(3n + 1) {
        grid-column: 1 / -1;
    }
}

.stat-card {
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-lg);
    padding: var(--space-lg);
    box-shadow: var(--shadow-sm);
    display: flex;
    align-items: center;
    gap: var(--space-md);
    transition: box-shadow var(--transition-normal), transform var(--transition-normal);
}

/* Cuando la grid tiene contenido de altura variable (ej: cards de cron
   con textos de longitud distinta), alinear al top evita que las cards
   más cortas centren su contenido dejando gaps visuales. */
.stat-grid[data-align="start"] .stat-card {
    align-items: flex-start;
}

/* :hover solo en cursor real — evita sticky-hover tras tap en mobile. */
html:not(.is-touch) .stat-card:hover {
    box-shadow: var(--shadow-md);
    transform: translateY(-1px);
}

.stat-card__icon {
    flex-shrink: 0;
    width: 48px;
    height: 48px;
    border-radius: var(--radius-md);
    display: grid;
    place-items: center;
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.stat-card__icon .icon {
    width: 22px;
    height: 22px;
}

.stat-card__body {
    display: flex;
    flex-direction: column;
    min-width: 0;
    /* Texto técnico largo (URLs, hostnames, mensajes de error) se parte en
       cualquier carácter — evita que overflow horizontal el card. */
    overflow-wrap: anywhere;
    word-break: break-word;
}

.stat-card__value {
    font-size: var(--font-size-2xl);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    line-height: 1;
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-xs);
}

/* Un valor que es texto (un nombre, un estado), no una cifra: a tamaño de
   texto y partiendo solo entre palabras. A 2xl, en una tarjeta de un
   cuarto de fila a 1024, «sostenido» no cabía y se partía en «sostenid / o». */
.stat-card__value[data-tipo="texto"] {
    display: block;
    font-size: var(--font-size-base);
    line-height: var(--line-height-tight);
    overflow-wrap: break-word;
    word-break: normal;
}

/* Indicador de tendencia vs periodo anterior (AntD Pro) */
.stat-card__trend {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    line-height: 1;
}

.stat-card__trend[data-trend="up"]   { color: var(--color-success-text); }
.stat-card__trend[data-trend="down"] { color: var(--color-danger-text); }

.stat-card__label {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    font-weight: var(--font-weight-regular);
    margin-block-start: 4px;
}

/* ── Variantes — color del icono y fondo del icono ──────────────────────── */

.stat-card[data-variant="success"] .stat-card__icon {
    background-color: var(--color-success-soft);
    color: var(--color-success);
}

.stat-card[data-variant="warning"] .stat-card__icon {
    background-color: var(--color-warning-soft);
    color: var(--color-warning);
}

.stat-card[data-variant="danger"] .stat-card__icon {
    background-color: var(--color-danger-soft);
    color: var(--color-danger);
}

/* ── data-state — pista de estado para stat-cards que ya son block-link ──
   Usar cuando la card es un <a class="stat-card" data-variant="block">
   (block-link sin underline) y queremos además colorear el icono según
   estado del valor. `data-variant` está ocupado por "block", por eso
   usamos `data-state` separado. Mismo set de variantes/colores. */
.stat-card[data-state="success"] .stat-card__icon {
    background-color: var(--color-success-soft);
    color: var(--color-success);
}

.stat-card[data-state="warning"] .stat-card__icon {
    background-color: var(--color-warning-soft);
    color: var(--color-warning);
}

.stat-card[data-state="danger"] .stat-card__icon {
    background-color: var(--color-danger-soft);
    color: var(--color-danger);
}

/* ── Responsive móvil ────────────────────────────────────────────────── */

@media (max-width: 480px) {
    .stat-card {
        padding: var(--space-md);
    }

    .stat-card__icon {
        width: 40px;
        height: 40px;
    }

    .stat-card__icon .icon {
        width: 18px;
        height: 18px;
    }

    .stat-card__value {
        font-size: var(--font-size-xl);
    }
}




/* ── tab-filter.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/tab-filter.css — Bloque: tabs tipo segmented control

   Uso: filtrar listados por categorías predefinidas. Los tabs disparan HTMX.
   data-active="true" marca el tab seleccionado.
   ═══════════════════════════════════════════════════════════════════════════ */

.tab-filter {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 4px;
    background-color: var(--color-bg-alt);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

/* Variante "nowrap" — para tab-filters dentro de card headers o
   contenedores estrechos donde 3+ chips deben caber en una línea
   siempre. Default sigue siendo wrap para listados anchos. */
.tab-filter[data-wrap="nowrap"] {
    flex-wrap: nowrap;
}

.tab-filter__item {
    padding: 0.375rem var(--space-md);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-text-muted);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    text-decoration: none;
    transition: background-color var(--transition-fast), color var(--transition-fast);
    white-space: nowrap;
}

.tab-filter__item:hover {
    color: var(--color-text);
    /* Sin underline en hover — la regla global de links.css (a:hover) aplica
       underline a TODO <a>; aquí los tab-items son chips/botones visuales,
       no enlaces en prosa. Mismo patrón que .nav-sidebar__link. */
    text-decoration: none;
}

.tab-filter__item[data-active="true"] {
    background-color: var(--color-bg-elevated);
    color: var(--color-primary-text);
    box-shadow: var(--shadow-sm);
}

.tab-filter__count {
    display: inline-block;
    margin-inline-start: 6px;
    padding: 1px 8px;
    font-size: var(--font-size-xs);
    background-color: var(--color-bg-alt);
    border-radius: var(--radius-full);
    color: var(--color-text-muted);
}

.tab-filter__item[data-active="true"] .tab-filter__count {
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
}

/* Variantes del contador para señalar urgencia operativa.
   Ponerlo solo cuando el contador > 0. Permite que tabs como "Pendientes
   de revisar" o "Sin actividad en 30 días" destaquen visualmente cuando
   tienen casos que atender. */
.tab-filter__count[data-variant="success"] {
    background-color: var(--color-success-soft);
    color: var(--color-success-text);
}
.tab-filter__count[data-variant="warning"] {
    background-color: var(--color-warning-soft);
    color: var(--color-warning-text);
}
.tab-filter__count[data-variant="danger"] {
    background-color: var(--color-danger-soft);
    color: var(--color-danger-text);
}
/* En tab activo, mantenemos el énfasis primary para no romper la jerarquía
   visual del tab seleccionado. */
.tab-filter__item[data-active="true"] .tab-filter__count[data-variant] {
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
}



/* ── list-toolbar.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/list-toolbar.css — Toolbar de listado estilo AntD Pro

   Patrón canónico AntD Pro para list pages: una "card header" que agrupa
   búsqueda + tabs rápidos + acciones (primary + secundarias) arriba de la
   tabla. Reemplaza el antipatrón de meter acciones en el page-header, que
   en mobile quedaba desordenado (filtros/exportar separados del CTA).

   Estructura Twig:
     <div class="list-toolbar">
       <div class="list-toolbar__row">
         <div class="list-toolbar__search"> <input type=search/> </div>
         <div class="list-toolbar__actions">
           <button data-variant="primary">+ Nuevo</button>
           <button>Filtros</button>
           <div class="dropdown">Exportar ▾</div>
         </div>
       </div>
       <div class="list-toolbar__row list-toolbar__row--tabs">
         {% include 'partials/_tabs_xxx.html.twig' %}
       </div>
     </div>

   Desktop (>=768px):
     Row 1: [search 1fr] [actions auto] — search estira, actions a la derecha.
     Row 2: tabs a lo largo de toda la fila.

   Mobile (<768px):
     Row 1 wrap → search arriba full-width, actions abajo full-width en grid.
     Row 2 tabs con scroll horizontal (evita wrap a 2 líneas).
   ═══════════════════════════════════════════════════════════════════════════ */

.list-toolbar {
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-lg);
    padding: var(--space-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

.list-toolbar__row {
    display: flex;
    gap: var(--space-sm);
    align-items: center;
    flex-wrap: wrap;
}

/* Separador sutil entre row 1 (search+actions) y row 2 (tabs). Solo si
   existe la segunda fila — se aplica al --tabs para evitar ruido visual
   cuando el toolbar no tiene tabs. */
.list-toolbar__row--tabs {
    border-block-start: 1px solid var(--color-border-subtle);
    padding-block-start: var(--space-sm);
    margin-block-start: 0;
}

/* Buscador — se estira al máximo disponible, min 240px para que el input
   no quede enano cuando hay muchas acciones al lado. */
.list-toolbar__search {
    flex: 1 1 240px;
    min-width: 0;
}

.list-toolbar__search .form__input-group {
    width: 100%;
}

/* Acciones — agrupadas a la derecha (margin-inline-start: auto). En mobile
   pasan a ocupar fila completa con grid equidistante. */
.list-toolbar__actions {
    display: flex;
    gap: var(--space-sm);
    margin-inline-start: auto;
    flex-wrap: wrap;
    align-items: center;
}

/* Tabs — ocupan la fila completa. Overflow-x auto para scroll horizontal
   en mobile cuando hay muchos. */
.list-toolbar__tabs-slot {
    width: 100%;
    overflow-x: auto;
    scrollbar-width: thin;
}

/* Los .tab-filter dentro del slot no deben wrapear — scroll horizontal */
.list-toolbar__tabs-slot > .tab-filter {
    flex-wrap: nowrap;
    min-width: max-content;
}

@media (max-width: 47.99rem) {
    .list-toolbar {
        padding: var(--space-sm);
    }

    .list-toolbar__search {
        flex: 1 1 100%;
    }

    /*
       Layout móvil de las acciones — patrón AntD Pro:
         · Primary (+ Nuevo): ocupa el ancho disponible con label + icon.
         · Secundarias (Filtros/Exportar/Importar): icon-only, compactas.

       El usuario ve qué debería hacer (CTA primario prominente) y las
       acciones auxiliares quedan accesibles sin robar protagonismo ni
       hacer wrap a múltiples líneas de texto.
    */
    .list-toolbar__actions {
        width: 100%;
        margin-inline-start: 0;
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-sm);
        align-items: center;
    }

    /* Primary — se lleva el sitio que dejan las secundarias y, si su
       etiqueta entera no cabe a su lado ("Nuevo empleado" a 360px), baja a
       su propia fila a lo ancho. Antes se cortaba con "…": en un móvil no hay
       title que enseñe el resto (docs/diseno.md §7, «Texto»). */
    .list-toolbar__actions > .btn[data-variant="primary"] {
        flex: 1 0 auto;
        white-space: nowrap;
    }

    /* Secundarias — tamaño fijo cuadrado (target touch 44x44 mínimo) */
    .list-toolbar__actions > .btn:not([data-variant="primary"]),
    .list-toolbar__actions > .dropdown {
        flex: 0 0 auto;
    }

    .list-toolbar__actions > .btn:not([data-variant="primary"]),
    .list-toolbar__actions > .dropdown > .btn {
        min-width: 2.75rem;
        padding-inline: var(--space-sm);
    }

    /* Ocultar label de texto en secundarias — usar sr-only para mantener
       accesibilidad (screen readers siguen anunciando el propósito). */
    .list-toolbar__actions > .btn:not([data-variant="primary"]) > span,
    .list-toolbar__actions > .dropdown > .btn > span {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    /* Dropdown menu (Exportar ▾) en mobile — flip a alineación izquierda.
       Por defecto el menu tiene inset-inline-end: 0 (ancla a la derecha
       del botón). En desktop los botones son anchos, así que el menu
       queda ancla y extiende hacia la izquierda dentro del viewport.
       En mobile el botón es icon-only 44px y vive en medio del row —
       si el menu de 12rem se ancla a la derecha del botón, su borde
       izquierdo sale del viewport ~80px. Flip: ancla a la izquierda
       del botón, extiende hacia la derecha (fits en viewport porque
       los botones están en el lado izquierdo del row junto al search). */
    .list-toolbar__actions > .dropdown > .dropdown__menu {
        inset-inline-end: auto;
        inset-inline-start: 0;
    }
}



/* ── form-footer.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/form-footer.css — Footer de acciones canónico para forms de página

   Patrón AntD Pro "Settings page footer":
     - Desktop: cluster justify-end al final del form, separador sutil arriba
     - Mobile (<768px): fixed al bottom del viewport con backdrop, botones
       full-width apilados, primary arriba (column-reverse), respeta
       safe-area-inset-bottom

   Por qué sticky mobile: en forms largos (configuración, compositor de
   avisos, detalle de empleado) forzar al user a scrollear hasta el final
   para encontrar "Guardar" es fricción. Mismo motivo por el que GitHub,
   Slack, Google Settings tienen botón sticky.

   Complementa .modal__footer (forms dentro de modal) y .form__actions
   (acciones cortas al final sin necesidad de stick mobile).
   ═══════════════════════════════════════════════════════════════════════════ */

.form-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-sm);
    margin-block-start: var(--space-md);
}

@media (max-width: 47.99rem) {
    /* Fixed (no sticky) — sticky reaccionaba a micro-cambios de layout y
       env() durante scroll, dando efecto "se desplaza al llegar abajo".
       Fixed pinza al viewport y no toca nada durante scroll. */
    .form-footer {
        position: fixed;
        inset-inline: 0;
        inset-block-end: 0;
        z-index: var(--z-pie-fijo);

        background-color: var(--color-bg-elevated);
        border-block-start: 1px solid var(--color-border);
        padding: var(--space-md);
        padding-block-end: calc(var(--space-md) + env(safe-area-inset-bottom, 0));

        /* Fixed ya ancla al viewport — resetear margin del desktop. */
        margin: 0;

        /* Stack primary arriba (column-reverse porque el DOM tiene primary
           al final) + gap pequeño. Patrón AntD Mobile. */
        flex-direction: column-reverse;
        gap: var(--space-xs);
    }

    .form-footer .btn {
        width: 100%;
        min-height: 2.75rem;   /* target touch 44px AntD Mobile */
        justify-content: center;
    }

    /* Con el footer fijo el último contenido del form queda oculto. Un
       spacer al final del form reserva la altura aproximada del footer
       + safe-area. Usa el helper `.form-footer-spacer` justo antes del
       footer en el HTML. */
    .form-footer-spacer {
        block-size: calc(4.5rem + env(safe-area-inset-bottom, 0));
    }
}

/* Desktop: el spacer no ocupa espacio (el footer es normal flow) */
.form-footer-spacer {
    display: none;
}
@media (max-width: 47.99rem) {
    .form-footer-spacer {
        display: block;
    }
}



/* ── socio-row.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/socio-row.css — Componentes de celdas para tabla de socios

   Patrón reutilizable para cualquier tabla con: avatar + texto multi-línea,
   dot de estado, botones de acción compactos. Aplicable a futuras entidades.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Celda: avatar + nombre bold + sub-texto muted */
.user-cell {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-width: 0;
}

.user-cell__avatar {
    position: relative;
    flex-shrink: 0;
}

/* Thumbnail de entidades visuales (fotos, portadas, logos…) — el
   <img> sin restricción se renderizaba a tamaño nativo (cientos de px de
   alto). Lo encajamos en un cuadrado consistente con AntD List/Table:
   48×48px, recortado con object-fit. */
.user-cell__avatar img {
    width: 48px;
    height: 48px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    display: block;
}

/* Dot de estado sobre la esquina del avatar */
.user-cell__dot {
    position: absolute;
    inset-block-end: -2px;
    inset-inline-end: -2px;
    width: 12px;
    height: 12px;
    border-radius: var(--radius-full);
    background-color: var(--color-text-subtle);
    border: 2px solid var(--color-bg-elevated);
}

.user-cell__dot[data-status="active"]   { background-color: var(--color-success); }
.user-cell__dot[data-status="inactive"] { background-color: var(--color-text-subtle); }
.user-cell__dot[data-status="pending"]  { background-color: var(--color-warning); }

.user-cell__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}

/* Nada de la celda se corta con puntos suspensivos: lo que no cabe baja de
   línea (la tabla tiene su scroll dentro del wrapper). Cortado, el título se
   llevaba por delante lo que va a su lado (una etiqueta de plan, un número)
   y el correo o las etiquetas de la segunda línea no se podían leer. Un
   correo no parte (no tiene espacios): marca el ancho mínimo de la columna. */
.user-cell__title {
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    overflow-wrap: break-word;
}

.user-cell__sub {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    overflow-wrap: break-word;
}

/* En una tabla ancha, cada columna baja a su ancho mínimo y la del sujeto
   (que parte en líneas) acababa en una palabra por línea mientras las de
   etiquetas y acciones, que no parten, se quedaban su sitio. Un mínimo para
   la celda de usuario: el título cabe en dos o tres líneas legibles; si la
   tabla no cabe, se desplaza en su wrapper. */
.data-table .user-cell {
    min-width: 14rem;
}

/* Grupo de botones de acción en la última columna */
.row-actions {
    display: inline-flex;
    gap: 4px;
    justify-content: flex-end;
}

.row-action {
    display: inline-grid;
    place-items: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background-color: var(--color-bg-elevated);
    color: var(--color-text-muted);
    cursor: pointer;
    text-decoration: none;
    transition:
        background-color var(--transition-fast),
        border-color var(--transition-fast),
        color var(--transition-fast);
}

/* Un <a class="row-action"> no toma el color ni el subrayado de a:hover
   (links.css), que en táctil se quedaría pegado tras el tap. Las variantes
   y el hover con cursor, más abajo, siguen ganando. */
.row-action:hover {
    color: var(--color-text-muted);
    text-decoration: none;
}

/* Hit target 44x44 en dispositivos táctiles (WCAG AA + iOS HIG) */
@media (pointer: coarse) {
    .row-action {
        width: 44px;
        height: 44px;
    }
}

.row-action:active {
    background-color: var(--color-primary-soft);
    transform: scale(0.96);
}

.row-action .icon {
    width: 16px;
    height: 16px;
}

/* :hover solo en cursor real — evita sticky-hover tras tap. Los 3
   variants (default / danger / warning) van envueltos. */
html:not(.is-touch) .row-action:hover {
    background-color: var(--color-primary-soft);
    border-color: var(--color-primary-border);
    color: var(--color-primary-text);
}

html:not(.is-touch) .row-action[data-variant="danger"]:hover {
    background-color: var(--color-danger-soft);
    border-color: var(--color-danger);
    color: var(--color-danger-text);
}

/* Warning — acciones semi-destructivas (revocan seguridad pero no eliminan).
   Color naranja idle para distinguirse visualmente del danger (gris→rojo).
   Patrón canónico: revocar biometría, desactivar 2FA, etc. */
.row-action[data-variant="warning"] {
    color: var(--color-warning-text);
}
html:not(.is-touch) .row-action[data-variant="warning"]:hover {
    background-color: var(--color-warning-soft);
    border-color: var(--color-warning);
    color: var(--color-warning-text);
}



/* ── pagination.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/pagination.css — Bloque: paginación AntD

   Estructura:
     .pagination
       .pagination__info    — "Mostrando X-Y de Z"
       .pagination__pages   — números de página + prev/next
       .pagination__size    — selector de resultados por página
   ═══════════════════════════════════════════════════════════════════════════ */

.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    flex-wrap: wrap;
    padding: var(--space-md) 0;
}

.pagination__info {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* ── Bloque de números de página + prev/next ───────────────────────────── */
.pagination__pages {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    list-style: none;
    margin: 0;
    padding: 0;
}

.pagination__item {
    display: inline-grid;
    place-items: center;
    min-width: 32px;
    height: 32px;
    padding: 0 6px;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-regular);
    color: var(--color-text);
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    cursor: pointer;
    text-decoration: none;
    transition:
        color var(--transition-fast),
        border-color var(--transition-fast),
        background-color var(--transition-fast);
}

/* Sin el subrayado de a:hover (links.css): son botones, no enlaces en prosa. */
.pagination__item:hover {
    text-decoration: none;
}

.pagination__item:hover:not([aria-disabled="true"]):not([aria-current="true"]) {
    color: var(--color-primary-text);
    border-color: var(--color-primary);
}

.pagination__item[aria-current="true"] {
    color: var(--color-primary-text);
    border-color: var(--color-primary);
    font-weight: var(--font-weight-medium);
    cursor: default;
}

.pagination__item[aria-disabled="true"] {
    color: var(--color-text-subtle);
    cursor: not-allowed;
    background-color: var(--color-bg-alt);
}

/* Prev/Next — solo icono, sin borde visible (estilo AntD) */
.pagination__item[data-variant="nav"] {
    border-color: transparent;
    background: transparent;
}

.pagination__item[data-variant="nav"]:hover:not([aria-disabled="true"]) {
    background-color: var(--color-bg-alt);
    border-color: transparent;
    color: var(--color-primary-text);
}

.pagination__item[data-variant="nav"] .icon {
    width: 14px;
    height: 14px;
}

/* Ellipsis — no interactivo */
.pagination__ellipsis {
    display: inline-grid;
    place-items: center;
    min-width: 32px;
    height: 32px;
    color: var(--color-text-subtle);
    font-size: var(--font-size-sm);
    cursor: default;
    user-select: none;
}

/* ── Selector de tamaño de página ──────────────────────────────────────── */
.pagination__size {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.pagination__size select {
    padding: 4px var(--space-sm);
    padding-inline-end: 1.75rem;
    font-size: var(--font-size-sm);
    line-height: 1.4;
    color: var(--color-text);
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-control);   /* 3:1 con su fondo (WCAG 1.4.11) */
    border-radius: var(--radius-md);
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23595959' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0.5rem center;
    background-size: 14px;
    transition: border-color var(--transition-fast);
}

.pagination__size select:hover {
    border-color: var(--color-primary);
}

.pagination__size select:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: var(--focus-ring);
}

/* En móvil: apilar verticalmente, centrar */
@media (max-width: 47.99rem) {
    .pagination {
        justify-content: center;
        gap: var(--space-sm);
    }
    .pagination__info,
    .pagination__size {
        width: 100%;
        justify-content: center;
    }
}



/* ── drawer.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/drawer.css — Bloque: panel deslizante lateral (AntD Drawer)

   Se abre desde el borde (derecho por defecto) con backdrop semi-transparente.
   Uso típico: filtros avanzados, formularios largos, detalles sin salir de la
   lista actual. Estructura similar al modal pero anclado al borde.

   Patrón Alpine + x-teleport="body" (mismo que modal, escapa del grid).
   ═══════════════════════════════════════════════════════════════════════════ */

.drawer__overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.45);
    z-index: var(--z-cajon);
    display: flex;
    justify-content: flex-end;

    /* Misma política que el modal: dim fullscreen pero el drawer se ancla
       al área de contenido, respetando sidebar/header/footer.               */
    padding-block-start:  var(--header-height);
    padding-block-end:    var(--footer-height);
}

html[data-sidebar-collapsed="true"] .drawer__overlay {
    /* El drawer sale desde la derecha — el sidebar izquierdo no le afecta */
}

@media (max-width: 47.99rem) {
    .drawer__overlay {
        padding-block-end: var(--bottom-nav-height);
    }
}

.drawer {
    background-color: var(--color-bg-elevated);
    box-shadow: var(--shadow-lg);
    width: 100%;
    max-width: 400px;
    /* height: 100% del overlay (que tiene padding por header+footer globales).
       NO usar 100dvh — desbordaría por debajo y ocultaría el footer del drawer. */
    height: 100%;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* Ancho: un editor que no cabe en 400px (los tipos de notificación). */
.drawer[data-size="lg"] {
    max-width: min(880px, 95vw);
}

/* Un texto para leer (la ayuda de la página): líneas de unas 70 letras. */
.drawer[data-size="md"] {
    max-width: 35rem;
}

/* Un cajón que recibe el foco al abrirse (tabindex="-1", la ayuda): el anillo
   por dentro, que su overflow recorta lo que sobresale. */
.drawer:focus-visible {
    outline-offset: -2px;
}

/* El <form> dentro del drawer envuelve .drawer__body + .drawer__footer.
   Sin estas reglas, el form queda con display:block normal → su .drawer__body
   (flex:1) NO funciona porque el padre (form) no es flex container → el body
   crece sin límite y el footer se sale del viewport. Síntoma: botones
   Limpiar/Aplicar cortados por debajo cuando el body tiene muchos grupos.
   Replicamos el comportamiento flex del drawer en el form. */
.drawer > form {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;              /* crítico para permitir scroll en hijos flex */
    overflow: hidden;
}

/* Variante desde la izquierda (por si algún día) */
.drawer__overlay[data-placement="left"] {
    justify-content: flex-start;
}

/* ── Header ───────────────────────────────────────────────────────────── */
.drawer__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-block-end: 1px solid var(--color-border-subtle);
    flex-shrink: 0;
}

.drawer__title {
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
}

.drawer__close {
    display: inline-grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    margin: calc(-1 * var(--space-xs));
    background: transparent;
    border: none;
    cursor: pointer;
    color: var(--color-text-muted);
    border-radius: var(--radius-md);
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.drawer__close .icon {
    width: 16px;
    height: 16px;
}

.drawer__close:hover {
    background-color: var(--color-bg-alt);
    color: var(--color-text);
}

.drawer__close:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* ── Body (scrollable) ────────────────────────────────────────────────── */
.drawer__body {
    padding: var(--space-lg);
    overflow-y: auto;
    flex: 1;
}

/* ── Footer (sticky abajo con acciones) ───────────────────────────────── */
.drawer__footer {
    display: flex;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-lg);
    border-block-start: 1px solid var(--color-border-subtle);
    background-color: var(--color-bg-elevated);
    flex-shrink: 0;
}

/* El aviso de un cajón sin contenido («Sin filtros avanzados configurados»). */
.drawer__vacio {
    padding: var(--space-md);
}

/* Todas las acciones a la derecha (sin «Limpiar» a la izquierda). */
.drawer__footer[data-justify="end"] {
    justify-content: flex-end;
}

/* ── Grupos de campos dentro del drawer (con separadores sutiles) ─────── */
.drawer__group {
    padding-block-end: var(--space-lg);
    margin-block-end: var(--space-lg);
    border-block-end: 1px solid var(--color-border-subtle);
}

.drawer__group:last-child {
    padding-block-end: 0;
    margin-block-end: 0;
    border-block-end: none;
}

.drawer__group-title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0 0 var(--space-sm) 0;
}

/* Rango de 2 inputs en línea (min/max, desde/hasta) */
.drawer__range {
    display: grid;
    /* minmax(0, …): una fecha nativa no baja de su ancho propio y, con 1fr
       a secas, a 360px sacaba la segunda del cajón. */
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    gap: var(--space-sm);
    align-items: center;
}

.drawer__range > .form__input {
    min-width: 0;
}

.drawer__range-sep {
    color: var(--color-text-subtle);
    font-size: var(--font-size-sm);
}

/* Lista vertical de checkboxes/radios — queda más AntD que multi-select */
.drawer__options {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}



/* ── lock-screen.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/lock-screen.css — Overlay bloqueante de soft-lock biométrico

   Uso: APK Capacitor con setting `biometria_al_abrir_activado` + user opt-in
   (localStorage) u obligatorio por rol. Aparece sobre TODO el contenido al
   abrir la app con sesión activa — el user debe autenticar con biometría
   para desbloquear y ver el contenido.

   Capa --z-bloqueo (tokens.css): por encima de modales, cajones y toasts.
   El user literalmente NO puede interactuar con la app sin pasar por aquí.
   ═══════════════════════════════════════════════════════════════════════════ */

.lock-screen {
    position: fixed;
    inset: 0;
    z-index: var(--z-bloqueo);
    display: grid;
    place-items: center;
    background-color: var(--color-bg);
    padding: var(--space-lg);

    /* Safe area inset — evita que el contenido quede debajo del notch / barra */
    padding-block-start: max(var(--space-lg), env(safe-area-inset-top, 0));
    padding-block-end:   max(var(--space-lg), env(safe-area-inset-bottom, 0));
}

.lock-screen__content {
    text-align: center;
    max-width: 320px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-md);
}

.lock-screen__icon {
    display: grid;
    place-items: center;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
    margin-block-end: var(--space-sm);
}

.lock-screen__icon .icon {
    width: 40px;
    height: 40px;
}

.lock-screen__title {
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
}

.lock-screen__subtitle {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin: 0;
    line-height: 1.5;
}

.lock-screen__actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    width: 100%;
    margin-block-start: var(--space-md);
}

.lock-screen__actions .btn {
    width: 100%;
    justify-content: center;
}



/* ── app-footer.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/app-footer.css — Footer del shell autenticado.
   Vive en la fila final del grid `.layout-app`. Discreto, tipo AntD Pro Waterline.
   ═══════════════════════════════════════════════════════════════════════════ */

.app-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-sm) var(--space-lg);
    background-color: var(--color-bg-elevated);
    border-block-start: 1px solid var(--color-border-subtle);
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    flex-wrap: wrap;
}

.app-footer__links {
    display: inline-flex;
    gap: var(--space-md);
    list-style: none;
    margin: 0;
    padding: 0;
}

.app-footer__links a {
    color: inherit;
    text-decoration: none;
    transition: color var(--transition-fast);
}

.app-footer__links a:hover {
    color: var(--color-primary-text);
}

/* En móvil el bottom-nav ocupa el lugar del footer — escondemos éste */
@media (max-width: 47.99rem) {
    .app-footer {
        display: none;
    }
}



/* ── progress-bar.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/progress-bar.css — Barra de progreso global tipo NProgress
   (como usa AntD Pro). Aparece arriba durante peticiones HTMX iniciadas
   por el usuario. Lógica en app.js; polls de fondo la saltan. El avance lo
   pone app.js en --progreso (sin ella, 0).

   Anatomía NProgress:
     - Barra de 2px de alto en el color primario del sistema.
     - "Peg": banda inclinada ~3° al final de la barra que hace glow solo
       en el borde que avanza. Da sensación de "brocha dejando rastro".
   ═══════════════════════════════════════════════════════════════════════════ */

.progress-bar {
    position: fixed;
    inset-block-start: 0;
    inset-inline-start: 0;
    height: 2px;
    width: var(--progreso, 0);
    background-color: var(--color-primary);
    z-index: var(--z-progreso);
    pointer-events: none;
    opacity: 0;
    transition: width 0.2s ease-out, opacity 0.2s ease-out 0.15s;
}

.progress-bar[data-active="true"] {
    opacity: 1;
    transition: width 0.4s cubic-bezier(0.1, 0.5, 0.2, 1), opacity 0.1s;
}

/* Peg — glow direccional sobre el borde que avanza. Solo visible mientras
   la barra está activa (si no, arrastra un resplandor extraño fuera de ella). */
.progress-bar[data-active="true"]::after {
    content: '';
    position: absolute;
    inset-inline-end: 0;
    inset-block-start: 0;
    width: 100px;
    height: 100%;
    box-shadow: 0 0 10px var(--color-primary),
                0 0 5px  var(--color-primary);
    transform: rotate(3deg) translate(0, -4px);
    opacity: 1;
}



/* ── nav-submenu.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/nav-submenu.css — Submenús multi-nivel del sidebar AntD Pro.

   Estructura:
     <div class="nav-sidebar__group">
       <button class="nav-sidebar__group-trigger" @click="toggle">
         <icon> · <span>Texto</span> · <chevron>
       </button>
       <ul class="nav-sidebar__group-children">
         <li><a class="nav-sidebar__link nav-sidebar__link--child">...</a></li>
       </ul>
     </div>
   ═══════════════════════════════════════════════════════════════════════════ */

.nav-sidebar__group {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.nav-sidebar__group-trigger {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 0.5rem var(--space-md);
    background: transparent;
    border: none;
    color: var(--color-sidebar-text);
    text-align: start;
    cursor: pointer;
    border-radius: var(--radius-md);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-regular);
    font-family: inherit;
    /* Wrap a 2 líneas si el label no cabe — en todos los breakpoints.
       Mismo patrón que .nav-sidebar__link (ver nav-sidebar.css). No
       truncamos con "..." porque el label ES la información principal
       del item de navegación. */
    white-space: normal;
    line-height: 1.25;
    transition: background-color var(--transition-fast), color var(--transition-fast);
    width: 100%;
}

.nav-sidebar__group-trigger:hover {
    background-color: var(--color-sidebar-hover);
    color: var(--color-text-on-dark);
}

.nav-sidebar__group-trigger .icon {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    opacity: 0.85;
}

/* La etiqueta se lleva el sitio; la flecha, sus 14px a la derecha. Sin el
   :not(), la flecha también crecía (flex: 1 gana a su flex-shrink) y partían
   la fila a medias: la flecha quedaba en el centro y una etiqueta larga
   («Administración») se salía de su mitad y pisaba la flecha. */
.nav-sidebar__group-trigger > span:not(.nav-sidebar__group-chevron) {
    flex: 1;
    min-width: 0;
}

/* Estado colapsado desktop — nowrap en el trigger para altura constante
   durante el fade-out del span (ver nav-sidebar.css). */
@media (min-width: 48rem) {
    html[data-sidebar-collapsed="true"] .nav-sidebar__group-trigger,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__group-trigger {
        white-space: nowrap;
    }
}

.nav-sidebar__group-chevron {
    display: inline-grid;
    place-items: center;
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    opacity: 0.6;
    transition: transform var(--transition-fast);
    transform-origin: 50% 50%;
}

/* El SVG interno hereda tamaño del wrapper — así rota sobre su propio centro
   en vez de desbordar y moverse al aplicar el transform. */
.nav-sidebar__group-chevron .icon {
    width: 12px;
    height: 12px;
    display: block;
}

/* Abierto (también antes de que Alpine ponga data-open, como los hijos de
   abajo): flecha hacia arriba. Sin esto, la flecha giraba en cada carga. */
.nav-sidebar__group:not([data-open="false"]) .nav-sidebar__group-chevron {
    transform: rotate(180deg);
}

/* Default: ABIERTO (max-height: 800px). Así el HTML recién renderizado
   (sin `data-open` aún, Alpine no ha corrido) muestra los grupos expandidos
   — es la preferencia por defecto del localStorage — evitando el flash de
   expansión al cargar la página. Alpine luego aplicará data-open="false"
   a los grupos que el usuario había colapsado. */
.nav-sidebar__group-children {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow: hidden;
    max-height: 800px;          /* suficiente para listas razonables */
    transition: max-height var(--transition-normal);
}

.nav-sidebar__group[data-open="false"] .nav-sidebar__group-children {
    max-height: 0;
}

/* Hasta que Alpine marca el documento como listo, NO animar — así la
   aplicación inicial de data-open="false" (para grupos colapsados por
   el usuario) pasa al instante, sin transición visible. Tras alpine:initialized
   el listener en alpine-data.js añade `data-alpine-ready` al <html> y las
   transiciones vuelven a estar activas para clicks posteriores. */
html:not([data-alpine-ready]) .nav-sidebar__group-children,
html:not([data-alpine-ready]) .nav-sidebar__group-chevron {
    transition: none;
}

/* Items hijos — indentación y barra lateral */
.nav-sidebar__link--child {
    padding-inline-start: calc(var(--space-md) + 16px + var(--space-sm));
    font-size: var(--font-size-sm);
}

/* Cuando el sidebar está colapsado (SOLO desktop), ocultamos la pestaña
   children + chevron y el grupo se comporta como un link plano (solo icono).
   En móvil el sidebar es drawer, no rail — el estado colapsado no aplica.

   Usamos max-height+opacity+visibility en lugar de display:none para que la
   aparición/desaparición sea transicionable y sincronice con el grid
   width del layout. Con display:none los children + chevron popeaban
   instantáneos al expandir/colapsar, desentonando con el fade del texto. */
@media (min-width: 48rem) {
    .nav-sidebar__group-chevron {
        /* Añadimos opacity a la transición existente (transform) */
        transition: opacity var(--transition-normal), transform var(--transition-fast);
    }

    /* El bloque arriba ya declara transition: max-height; añadimos opacity */
    .nav-sidebar__group-children {
        transition: max-height var(--transition-normal), opacity var(--transition-normal);
    }

    html[data-sidebar-collapsed="true"] .nav-sidebar__group-children,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__group-children {
        max-height: 0;
        opacity: 0;
        pointer-events: none;
    }

    html[data-sidebar-collapsed="true"] .nav-sidebar__group-chevron,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__group-chevron {
        opacity: 0;
        pointer-events: none;
    }

    /* Group trigger en estado colapsado: igual de estrecho que un link normal */
    html[data-sidebar-collapsed="true"] .nav-sidebar__group-trigger,
    .nav-sidebar[data-collapsed="true"] .nav-sidebar__group-trigger {
        padding-inline: var(--space-md);
    }
}



/* ── notifications.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/notifications.css — Dropdown de notificaciones AntD Pro.
   Estructura: dropdown__menu pero más ancho + items con contenido rico.
   ═══════════════════════════════════════════════════════════════════════════ */

.notif-dropdown {
    width: 360px;
    padding: 0;
    overflow: hidden;
}

/* En móvil el dropdown de 360px se salía del viewport por la izquierda
   (está anclado al borde derecho del botón de la campana). Lo pasamos a
   `position: fixed` anclado a ambos bordes del viewport con un margen
   pequeño — queda centrado debajo del header sin recortarse. */
@media (max-width: 47.99rem) {
    .dropdown__menu.notif-dropdown {
        position: fixed;
        inset-block-start: calc(var(--header-height) + 4px);
        inset-inline-start: var(--space-sm);
        inset-inline-end: var(--space-sm);
        width: auto;
        max-width: 420px;
        margin-inline: auto;
    }
}

.notif-dropdown__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    border-block-end: 1px solid var(--color-border-subtle);
    background-color: var(--color-bg-alt);
}

.notif-dropdown__title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
}

.notif-dropdown__action {
    background: transparent;
    border: none;
    color: var(--color-primary-text);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
    cursor: pointer;
    padding: 4px 8px;
    border-radius: var(--radius-sm);
}

.notif-dropdown__action:hover {
    background-color: var(--color-primary-soft);
}

.notif-dropdown__list {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 360px;
    overflow-y: auto;
}

.notif-item {
    border-block-end: 1px solid var(--color-border-subtle);
    transition: background-color var(--transition-fast);
}

.notif-item:last-child {
    border-block-end: none;
}

/* Toda la fila es un <a> — el link ocupa todo el área clickable. */
.notif-item__link {
    display: flex;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    color: inherit;
    text-decoration: none;
    cursor: pointer;
}

/* color + text-decoration: anulan el color y el subrayado de a:hover
   (links.css) — la fila entera es el enlace, no un enlace en prosa. */
.notif-item__link:hover,
.notif-item__link:focus-visible {
    background-color: var(--color-bg-alt);
    color: inherit;
    text-decoration: none;
    outline: none;
}

.notif-item__icon {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: var(--radius-full);
    display: grid;
    place-items: center;
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.notif-item__icon[data-variant="success"] {
    background-color: var(--color-success-soft);
    color: var(--color-success);
}
.notif-item__icon[data-variant="warning"] {
    background-color: var(--color-warning-soft);
    color: var(--color-warning);
}
.notif-item__icon[data-variant="danger"] {
    background-color: var(--color-danger-soft);
    color: var(--color-danger);
}

.notif-item__icon .icon {
    width: 16px;
    height: 16px;
}

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

.notif-item__title {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    line-height: 1.3;
}

.notif-item__desc {
    display: block;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    margin-block-start: 2px;
    line-height: 1.4;
}

.notif-item__time {
    display: block;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);   /* texto de 12px: subtle no llega a 4,5:1 */
    margin-block-start: 4px;
}

/* Punto azul para "no leídas" */
.notif-item[data-unread="true"] {
    background-color: var(--color-primary-soft);
}
.notif-item[data-unread="true"] .notif-item__title::after {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background-color: var(--color-primary);
    margin-inline-start: var(--space-xs);
    vertical-align: middle;
}

/* Footer del dropdown */
.notif-dropdown__footer {
    padding: var(--space-sm) var(--space-md);
    text-align: center;
    border-block-start: 1px solid var(--color-border-subtle);
    background-color: var(--color-bg-alt);
}

.notif-dropdown__footer a {
    color: var(--color-primary-text);
    font-size: var(--font-size-sm);
    text-decoration: none;
    font-weight: var(--font-weight-medium);
}

.notif-dropdown__footer a:hover {
    text-decoration: underline;
}

/* Empty state */
.notif-dropdown__empty {
    padding: var(--space-2xl) var(--space-md);
    text-align: center;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}



/* ── tabs.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/tabs.css — Tabs horizontales estilo AntD.

   Estructura: la pintan las macros de partials/_pestanas.html.twig, con la
   pestaña abierta marcada por el servidor, y la mueve el componente Alpine
   `pestanas` (docs/estandar-ux.md §«Pestañas de una página»):
     <div x-data="pestanas" data-pestanas="g">
       <div class="tabs">
         <div class="tabs__barra" x-data="barraPestanas" x-bind="barra">
           <button type="button" class="tabs__flecha" data-lado="inicio" x-bind="flecha" aria-label="…" hidden>‹</button>
           <div class="tabs__nav" role="tablist">
             <button class="tabs__tab" role="tab" id="g-pestana-uno" aria-controls="g-panel-uno"
                     data-pestana="uno" aria-selected="true" data-active="true" tabindex="0"
                     x-bind="pestana">Uno</button>
             <button class="tabs__tab" role="tab" … data-active="false" tabindex="-1" …>Dos</button>
           </div>
           <button type="button" class="tabs__flecha" data-lado="fin" x-bind="flecha" aria-label="…" hidden>›</button>
         </div>
       </div>
       <section role="tabpanel" id="g-panel-uno" data-panel="uno" x-bind="panel">…</section>
       <section role="tabpanel" id="g-panel-dos" data-panel="dos" x-bind="panel" hidden>…</section>
     </div>

   `.tabs__barra` (con sus flechas) lo pone la macro barra(); una barra escrita
   a mano puede ir sin él (un `.tabs__nav` suelto): sin flechas, pero se
   desplaza igual. Con él, cuando las pestañas no caben, el componente
   `barraPestanas` (alpine-data.js) enseña la flecha de cada lado por el que
   hay más y marca `data-mas-inicio` / `data-mas-fin`, que difuminan ese borde.

   Distinto de `.tab-filter` (segmented control para filtros). Esta es
   navegación entre secciones de contenido (como AntD Tabs).
   ═══════════════════════════════════════════════════════════════════════════ */

.tabs {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* Lo que el servidor oculta con `hidden` dentro de unas pestañas (los paneles
   que no son el abierto, lo que solo se ve con otra pestaña) no se ve aunque
   su bloque fije `display` (una tarjeta, una rejilla de campos, un aviso). */
[data-pestanas] [hidden] {
    display: none;
}

/* Modificador: las tabs se pegan al borde superior del contenedor (útil
   justo debajo del modal__header para no generar un hueco grande). Compensa
   el padding-top del modal body. */
.tabs[data-flush="top"] {
    margin-block-start: calc(var(--space-lg) * -1);
}

.tabs[data-flush="top"] .tabs__nav {
    margin-block-end: var(--space-md);
    padding-block-start: var(--space-sm);
}

/* Modificador: rellena el contenedor padre con padding propio (útil dentro
   de un modal donde el body no tiene padding). El nav queda sticky arriba
   — así al scrollear el body del modal sigue visible. */
.tabs[data-fill="true"] {
    padding: var(--space-lg);
}

.tabs[data-fill="true"] .tabs__nav {
    position: sticky;
    inset-block-start: 0;
    background-color: var(--color-bg-elevated);
    z-index: var(--z-pegado);     /* sobre lo que se desplaza por debajo */
    margin-block-start: calc(-1 * var(--space-lg));
    padding-block-start: var(--space-lg);
}

/* Las dos variantes con la barra de flechas (.tabs__barra, abajo): lo que
   hacían con el .tabs__nav lo hace su envoltorio. */
.tabs[data-flush="top"] .tabs__barra {
    margin-block-end: var(--space-md);
    padding-block-start: var(--space-sm);
}

.tabs[data-fill="true"] .tabs__barra {
    position: sticky;
    inset-block-start: 0;
    background-color: var(--color-bg-elevated);
    z-index: var(--z-pegado);
    margin-block-start: calc(-1 * var(--space-lg));
    padding-block-start: var(--space-lg);
}

.tabs[data-flush="top"] .tabs__barra > .tabs__nav,
.tabs[data-fill="true"] .tabs__barra > .tabs__nav {
    position: static;
    margin-block: 0;
    padding-block-start: 0;
}

.tabs__nav {
    display: flex;
    /* 32px entre textos (AntD v5): el hueco más el relleno de cada pestaña. */
    gap: calc(var(--space-xl) - 2 * var(--space-xs));
    border-block-end: 1px solid var(--color-border-subtle);
    margin-block-end: var(--space-lg);

    /* overflow-x: auto + overflow-y: visible provoca que el navegador
       active scroll vertical "fantasma" también. Forzar hidden en Y lo
       evita. X queda auto para scroll horizontal cuando hay muchas tabs. */
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    flex-shrink: 0;
}

.tabs__nav::-webkit-scrollbar {
    height: 0;
    display: none;
}

/* ── Barra que no cabe: flechas y borde difuminado ───────────────────────────
   La línea inferior y el margen pasan del .tabs__nav a su envoltorio, que
   reparte el ancho entre la lista y las flechas (una a cada lado, solo si hay
   más pestañas por ese lado: `hidden` las quita). */
.tabs__barra {
    display: flex;
    align-items: stretch;
    min-inline-size: 0;
    border-block-end: 1px solid var(--color-border-subtle);
    margin-block-end: var(--space-lg);
    flex-shrink: 0;
}

.tabs__barra > .tabs__nav {
    flex: 1 1 auto;
    min-inline-size: 0;
    border-block-end: 0;
    margin-block-end: 0;
}

.tabs__flecha {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--space-xl);
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-muted);
    font: inherit;
    cursor: pointer;
    transition: color var(--transition-fast), background-color var(--transition-fast);
}

.tabs__flecha:hover {
    color: var(--color-primary-text);
    background-color: var(--color-bg-alt);
}

.tabs__flecha:focus-visible {
    outline-offset: -2px;
}

.tabs__flecha[hidden] {
    display: none;
}

.tabs__flecha .icon {
    inline-size: 16px;
    block-size: 16px;
}

/* En una pantalla táctil, la flecha, del tamaño de un dedo (§5 de estandar-ux). */
@media (pointer: coarse) {
    .tabs__flecha {
        inline-size: 2.75rem;
    }
}

/* El borde por el que hay más pestañas se difumina: una máscara (solo cuenta
   su transparencia, no su color) que funde lo que queda debajo, sea cual sea
   el fondo (página, tarjeta o modal, en claro y en oscuro). */
.tabs__barra[data-mas-inicio] > .tabs__nav {
    mask-image: linear-gradient(to right, transparent, black var(--space-xl));
}

.tabs__barra[data-mas-fin] > .tabs__nav {
    mask-image: linear-gradient(to left, transparent, black var(--space-xl));
}

.tabs__barra[data-mas-inicio][data-mas-fin] > .tabs__nav {
    mask-image: linear-gradient(to right, transparent, black var(--space-xl), black calc(100% - var(--space-xl)), transparent);
}

.tabs__tab {
    position: relative;
    /* Un poco de aire a los lados: el anillo de foco va por dentro (abajo). */
    padding: 0.75rem var(--space-xs);           /* 12px — match AntD v5 */
    background: transparent;
    border: none;
    font-family: inherit;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-regular);
    color: var(--color-text-muted);
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;                       /* aplica a <a class="tabs__tab"> */
    transition: color var(--transition-fast);
}

.tabs__tab:hover {
    color: var(--color-primary-text);
    text-decoration: none;
}

.tabs__tab[data-active="true"] {
    color: var(--color-primary-text);
    font-weight: var(--font-weight-medium);
}

/* El anillo de foco, dentro de la pestaña. El global (reset.css: 2px a 2px por
   fuera) lo recortaba la barra, que no deja salir nada por arriba ni por abajo
   (overflow): tras pulsar una pestaña y una tecla (F5) se veían solo sus dos
   lados, como dos franjas azules. */
.tabs__tab:focus-visible {
    outline-offset: -2px;
}

/* Punto rojo que avisa de errores en la tab (AntD pattern) */
.tabs__tab[data-has-error="true"]::before {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--color-danger);
    margin-inline-end: 6px;
    vertical-align: 1px;
}

/* Línea inferior que marca la tab activa — 2px solid primary,
   anclada al borde inferior del nav para tapar parte del border-block-end. */
.tabs__tab[data-active="true"]::after {
    content: '';
    position: absolute;
    inset-inline: var(--space-xs);              /* del ancho del texto, sin el relleno */
    inset-block-end: -1px;
    height: 2px;
    background-color: var(--color-primary);
    border-radius: 2px 2px 0 0;
}

/* Iconos opcionales dentro de una tab */
.tabs__tab .icon {
    width: 14px;
    height: 14px;
    margin-inline-end: 6px;
    vertical-align: -2px;
}

/* Panel — por defecto se expande pero NO scrolea por sí mismo. El contenedor
   padre (modal body, página, card) gestiona el scroll. Evita scrollbars
   anidados feos. */
.tabs__panel {
    flex: 1;
    min-height: 0;
}

/* Variante "pills" — tabs con fondo suave en activo (alternativa AntD card) */
.tabs[data-variant="pills"] .tabs__nav {
    border-block-end: none;
    gap: var(--space-xs);
    padding: 4px;
    background-color: var(--color-bg-alt);
    border-radius: var(--radius-md);
}

.tabs[data-variant="pills"] .tabs__tab {
    padding: 0.375rem var(--space-md);
    border-radius: var(--radius-sm);
}

.tabs[data-variant="pills"] .tabs__tab[data-active="true"] {
    background-color: var(--color-bg-elevated);
    box-shadow: var(--shadow-sm);
}

.tabs[data-variant="pills"] .tabs__tab[data-active="true"]::after {
    display: none;
}

.tabs[data-variant="pills"] .tabs__barra {
    border-block-end: 0;
}



/* ── file-upload.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/file-upload.css — Componentes de subida de ficheros AntD-style.

   Dos variantes:

   A) `.form__file`        — botón compacto "Seleccionar archivo" con lista
                             de archivos elegidos debajo. Para 1-2 archivos.

   B) `.file-dropzone`     — área drag-and-drop con icono, título y descripción.
                             Para múltiples archivos o UX más visible.

   Ambas esconden el <input type="file"> nativo (feo + inconsistente entre
   navegadores) y exponen un elemento estilado. El input sigue siendo funcional
   para accesibilidad (click y teclado funcionan vía <label>).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── A) Botón compacto ──────────────────────────────────────────────────── */

.form__file {
    display: inline-flex;
    flex-direction: column;
    gap: var(--space-sm);
    align-items: flex-start;
}

.form__file input[type="file"] {
    /* Oculta visualmente pero sigue siendo clickable via label + accesible */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.form__file-label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 0.625rem var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background-color: var(--color-bg-elevated);
    color: var(--color-text);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    cursor: pointer;
    transition:
        border-color var(--transition-fast),
        background-color var(--transition-fast),
        color var(--transition-fast);
}

.form__file-label:hover,
.form__file input[type="file"]:focus-visible + .form__file-label {
    border-color: var(--color-primary);
    color: var(--color-primary-text);
}

.form__file-label .icon {
    width: 16px;
    height: 16px;
}

/* Lista de archivos elegidos (poblada por JS o servidor) */
.form__file-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    list-style: none;
    margin: 0;
    padding: 0;
}

.form__file-item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 4px var(--space-sm);
    background-color: var(--color-bg-alt);
    border-radius: var(--radius-sm);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.form__file-item .icon {
    width: 14px;
    height: 14px;
    color: var(--color-text-subtle);
}

.form__file-item__remove {
    margin-inline-start: auto;
    background: none;
    border: none;
    padding: 2px;
    cursor: pointer;
    color: var(--color-text-subtle);
    border-radius: var(--radius-sm);
    display: inline-grid;
    place-items: center;
}

.form__file-item__remove:hover {
    color: var(--color-danger-text);
}

.form__file-item__remove .icon {
    width: 12px;
    height: 12px;
}

/* ─── B) Dropzone drag-and-drop ──────────────────────────────────────────── */

.file-dropzone {
    position: relative;
    display: block;
    padding: var(--space-xl) var(--space-lg);
    border: 1px dashed var(--color-border-strong);
    border-radius: var(--radius-lg);
    background-color: var(--color-bg-alt);
    text-align: center;
    cursor: pointer;
    transition:
        border-color var(--transition-fast),
        background-color var(--transition-fast);
}

.file-dropzone:hover,
.file-dropzone:focus-within {
    border-color: var(--color-primary);
    background-color: var(--color-primary-soft);
}

.file-dropzone[data-dragover="true"] {
    border-color: var(--color-primary);
    background-color: var(--color-primary-soft);
    border-style: solid;
}

.file-dropzone input[type="file"] {
    /* Oculto pero funcional */
    position: absolute;
    width: 100%;
    height: 100%;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

.file-dropzone__icon {
    display: inline-grid;
    place-items: center;
    width: 48px;
    height: 48px;
    border-radius: var(--radius-full);
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
    margin-block-end: var(--space-sm);
}

.file-dropzone__icon .icon {
    width: 24px;
    height: 24px;
}

.file-dropzone__title {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    margin-block-end: 4px;
}

.file-dropzone__desc {
    display: block;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
}



/* ── layout-cliente.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/layout-cliente.css — Shell del área cliente (empleado).

   Responsive:
     - Móvil (< 1024px): header + main + bottom-nav. Sin sidebar.
     - Desktop (≥ 1024px): sidebar colapsable + header + main. Sin bottom-nav.

   Patrón AntD Pro:
     - Logo solo en sidebar (desktop) o header (móvil, sin sidebar)
     - Header: collapse toggle + slot + avatar dropdown
     - Móvil: bottom-nav con 4 tabs, sin hamburger ni drawer
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Variables ────────────────────────────────────────────────────────── */
.layout-cliente {
    --sidebar-width: 240px;
    --sidebar-width-collapsed: 64px;
}

/* ── Móvil (default) ─────────────────────────────────────────────────── */

.layout-cliente {
    display: grid;
    /* MOBILE (<1024px): header ELIMINADO — patrón nativo moderno (Instagram,
       Revolut, WhatsApp Messenger web, Gmail mobile). El branding se sirve
       inline en /cliente/inicio via hero + saludo, y las acciones del header
       antiguo (Mi perfil, Ir a admin, logout) viven en bottom-nav + ajustes.
       Grid pasa de 3 filas (header/main/bottom) a 2 (main/bottom). El
       safe-area-inset-top queda respetado por padding-block-start del main
       — así el contenido no se mete bajo el notch/status bar.
       Desktop (≥1024px, más abajo) mantiene el header completo con logo,
       notificaciones, admin-switch y avatar. */
    grid-template-rows: 1fr var(--bottom-nav-height);
    grid-template-columns: 1fr;
    grid-template-areas:
        "main"
        "bottom";
    /* svh (no dvh) para evitar jitter con la URL bar del browser.
       Y restamos env(safe-area-inset-bottom) porque con viewport-fit=
       cover, svh incluye la zona bajo la barra Android / home indicator
       — sin restar, el nav-bottom queda con su parte inferior bajo la
       barra y las letras con descenders (Avisos, página) se cortan.
       Fallback: dvh + svh sin resta para browsers sin soporte env(). */
    height: 100dvh;
    height: 100svh;
    height: calc(100svh - env(safe-area-inset-bottom, 0));
    overflow: hidden;
}

/* Backdrop opaco sobre la status-bar en APK Capacitor (viewport-fit=cover).
   Sin esto, el contenido scrolleado se ve DETRÁS de la status bar translúcida.
   El padding-block-start del main crea hueco en scrollTop=0, pero al
   scrollear el contenido entra en esa zona y asoma bajo la status bar.
   Fix canónico: una capa fija opaca cubriendo env(safe-area-inset-top),
   por encima del scroll. En desktop o web sin safe-area es 0px → invisible.
   Color = var(--color-bg-layout) (#F0F2F5 light / #000 dark) para matchear
   el body y no introducir un cambio de color visible. */
.layout-cliente::before {
    content: "";
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    height: env(safe-area-inset-top, 0);
    background-color: var(--color-bg-layout);
    z-index: var(--z-franja-estado);
    pointer-events: none;
}

/* Header oculto en mobile — ver comentario arriba. */
.layout-cliente__header  {
    display: none;
}
.layout-cliente__main    {
    grid-area: main;
    overflow-y: auto;
    /* Padding-top respeta status bar nativa (notch iPhone / Android) sin
       header intermedio. Sin esto, el contenido de /cliente/inicio se
       metería bajo la status bar. */
    padding-block-start: max(var(--space-md), env(safe-area-inset-top, 0));
    /* Desactiva el rubber-band / overscroll bounce nativo — mismo
       patrón que .layout-app__main. Evita el "salto" de sticky interiors
       al hacer bounce en los límites del scroll en mobile. */
    overscroll-behavior: contain;
    /* Scrollbar fino estilo AntD Pro (solo afecta desktop — móvil usa
       overlay del SO). Mismo patrón que .modal__body y .layout-app__main. */
    scrollbar-width: thin;
    scrollbar-color: var(--color-border-strong) transparent;
}

.layout-cliente__main::-webkit-scrollbar {
    width: 10px;
}
.layout-cliente__main::-webkit-scrollbar-track {
    background: transparent;
}
.layout-cliente__main::-webkit-scrollbar-thumb {
    background-color: var(--color-border-strong);
    border-radius: var(--radius-md);
    border: 2px solid transparent;
    background-clip: padding-box;
}
.layout-cliente__main::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-text-muted);
}
.layout-cliente__bottom  { grid-area: bottom; }
.layout-cliente__sidebar { display: none; }

/* Elementos solo desktop */
.layout-cliente__desktop-only { display: none; }

/* Marca en header — solo visible en móvil (desktop la tiene en sidebar) */
.layout-cliente__mobile-brand { display: flex; }

/* Contenido centrado en móvil */
.layout-cliente__main > .container {
    max-width: var(--container-md);
    margin-inline: auto;
    padding-inline: var(--space-md);
    padding-block: var(--space-md);
}

.layout-cliente__logo {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    object-fit: contain;
}

/* ── Desktop (≥ 1024px) ──────────────────────────────────────────────── */

@media (min-width: 1024px) {
    .layout-cliente {
        grid-template-rows: var(--header-height) 1fr;
        grid-template-columns: var(--sidebar-width) 1fr;
        grid-template-areas:
            "sidebar header"
            "sidebar main";
    }

    .layout-cliente[data-sidebar-collapsed="true"] {
        grid-template-columns: var(--sidebar-width-collapsed) 1fr;
    }

    /* Header vuelve a mostrarse en desktop (mobile lo oculta con display:none).
       Contiene notificaciones, botón "Ir al panel admin" y avatar dropdown —
       esenciales en desktop web donde no hay bottom-nav. */
    .layout-cliente__header {
        display: flex;
        grid-area: header;
    }
    .layout-cliente__header.app-header { padding-inline: var(--space-md); }

    /* En desktop el padding extra del status-bar safe-area no aplica —
       resetear el padding-block-start del main al canónico del desktop. */
    .layout-cliente__main {
        padding-block-start: 0;
    }

    .layout-cliente__sidebar {
        grid-area: sidebar;
        display: block;
    }

    .layout-cliente__sidebar-nav {
        display: flex;
        flex-direction: column;
        height: 100%;
        padding: var(--space-md);
        background: var(--color-bg-elevated);
        border-right: 1px solid var(--color-border-subtle);
        overflow-y: auto;
        overflow-x: hidden;
    }

    .layout-cliente__bottom { display: none; }
    .layout-cliente__desktop-only { display: inline-flex; }
    .layout-cliente__mobile-brand { display: none; }


    .layout-cliente__main > .container {
        max-width: var(--container-xl, 1200px);
        padding-inline: var(--space-xl);
        padding-block: var(--space-lg);
    }
}

/* ── Sidebar — collapse ──────────────────────────────────────────────── */

.layout-cliente[data-sidebar-collapsed="true"] .layout-cliente__sidebar-text,
.layout-cliente[data-sidebar-collapsed="true"] .nav-sidebar__link span,
.layout-cliente[data-sidebar-collapsed="true"] .nav-sidebar__link .badge {
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-fast);
}

.layout-cliente[data-sidebar-collapsed="true"] .layout-cliente__sidebar-nav {
    padding: var(--space-sm);
}

.layout-cliente[data-sidebar-collapsed="true"] .layout-cliente__sidebar-footer {
    justify-content: center;
}

/* ── Sidebar — navigation ────────────────────────────────────────────── */

.layout-cliente__nav-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
}

.layout-cliente__sidebar-brand {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-xs);
    margin-bottom: var(--space-md);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    white-space: nowrap;
    overflow: hidden;
}

/* Eslogan del portal (branding_cliente.app_slogan) bajo el nombre. */
.layout-cliente__sidebar-slogan {
    display: block;
    font-size: var(--font-size-xs);
    font-weight: 400;
    line-height: 1.3;
    color: var(--color-text-muted);
}

.layout-cliente__sidebar-footer {
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border-subtle);
    margin-top: auto;
    overflow: hidden;
    white-space: nowrap;
}

/* ── Overrides .nav-sidebar__link para sidebar claro ─────────────────── */

.layout-cliente .nav-sidebar__link {
    color: var(--color-text);
}

.layout-cliente .nav-sidebar__link:hover {
    background: var(--color-bg-alt);
    color: var(--color-text);
}

.layout-cliente .nav-sidebar__link[aria-current="page"] {
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
    font-weight: var(--font-weight-semibold);
}

.layout-cliente .nav-sidebar__link .icon {
    opacity: 0.65;
}

.layout-cliente .nav-sidebar__link[aria-current="page"] .icon {
    opacity: 1;
    color: var(--color-primary-text);
}

/* ── Avatar del header (comparte bloque .dropdown canónico) ──────────────
   El dropdown de usuario usa blocks/dropdown.css — mismo que admin.
   Aquí solo queda el estilo del círculo del avatar. */

.avatar--header {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
}

/* ── Transición fade entre páginas (hx-boost) ────────────────────────── */
/* HTMX añade .htmx-swapping durante el swap-out y .htmx-settling tras
   el swap-in. Usamos opacity para un fade suave tipo SPA (100ms).
   El <section id="main-content"> de los dos layouts: por su clase, no por
   el id (docs/frontend.md, «Reglas CSS — resumen estricto»; lo vigila
   ReglasDeCssTest). */

.layout-cliente__main,
.layout-app__main {
    transition: opacity 0.1s ease;
}

.layout-cliente__main.htmx-swapping,
.layout-app__main.htmx-swapping {
    opacity: 0;
}

/* ── Viewport muy pequeño (iPhone SE / 320px) ────────────────────────── */

@media (max-width: 375px) {
    .layout-cliente__main > .container {
        padding-inline: var(--space-sm);
    }
}

/* ── Safe areas iOS (notch + home indicator) ─────────────────────────── */

@supports (padding: env(safe-area-inset-top)) {
    .layout-cliente__header {
        /* padding-block-start compensa el notch (iPhone X+) y la status bar
           en Android cuando el WebView extiende por detrás (defensivo contra
           OEMs que ignoran `overlaysWebView: false` del config Capacitor). */
        padding-block-start: env(safe-area-inset-top, 0);
        padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
    }

    .layout-cliente__main > .container {
        padding-left: max(var(--space-md), env(safe-area-inset-left));
        padding-right: max(var(--space-md), env(safe-area-inset-right));
    }
}



/* ── descriptions.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/descriptions.css — AntD Descriptions

   Dos variantes:
     Default (plain)     → sin bordes, label pequeño gris arriba + valor abajo.
                           Apta para modales "ver" y páginas de detalle modernas.
     data-bordered="true"→ con bordes tipo tabla. Para cards de dashboard cuando
                           se prefiere lectura tabular compacta.

   Columnas (data-columns): 1 | 2 | 3 (default 2 desktop, 1 móvil).
   ═══════════════════════════════════════════════════════════════════════════ */

.descriptions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-md) var(--space-xl);
    margin: 0;
    padding: 0;
}

.descriptions[data-columns="1"] { grid-template-columns: 1fr; }
.descriptions[data-columns="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* ── Item default (plain) ────────────────────────────────────────────────── */
.descriptions__item {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

/* Un dato que ocupa la fila entera (una fecha con su explicación). */
.descriptions__item[data-ancho="completo"] {
    grid-column: 1 / -1;
}

.descriptions__label {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    font-weight: var(--font-weight-regular);
    margin: 0;
    letter-spacing: 0.01em;
}

.descriptions__value {
    font-size: var(--font-size-base);
    color: var(--color-text);
    font-weight: var(--font-weight-medium);    /* 500 — destaca sobre el label */
    margin: 0;
    line-height: 1.5;
}

/* Un valor sin espacios (una URL, un identificador) parte donde haga falta
   en vez de salirse de su columna y pisar la de al lado. También con
   <dt>/<dd> sueltos, sin las clases del bloque. */
.descriptions__value,
.descriptions dd {
    margin: 0;
    overflow-wrap: anywhere;
}

.descriptions__value strong {
    font-weight: var(--font-weight-semibold);
}

/* ── Variante bordered (tabla estilo AntD) ───────────────────────────────── */
.descriptions[data-bordered="true"] {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
    gap: 0;
}

.descriptions[data-bordered="true"] .descriptions__item {
    flex-direction: row;
    gap: 0;
    border-bottom: 1px solid var(--color-border);
}

.descriptions[data-bordered="true"]:not([data-columns="1"]) .descriptions__item:nth-last-child(-n+2) {
    border-bottom: none;
}

.descriptions[data-bordered="true"][data-columns="1"] .descriptions__item:last-child {
    border-bottom: none;
}

.descriptions[data-bordered="true"] .descriptions__label {
    width: 140px;
    flex-shrink: 0;
    padding: var(--space-sm) var(--space-md);
    background-color: var(--color-bg-alt);
    border-right: 1px solid var(--color-border);
}

.descriptions[data-bordered="true"] .descriptions__value {
    flex: 1;
    padding: var(--space-sm) var(--space-md);
}

/* ── Responsive: 1 columna en móvil ──────────────────────────────────────── */
@media (max-width: 47.99rem) {
    .descriptions,
    .descriptions[data-columns="3"] {
        grid-template-columns: 1fr;
    }

    .descriptions[data-bordered="true"] .descriptions__item {
        flex-direction: column;
    }

    .descriptions[data-bordered="true"] .descriptions__label {
        width: auto;
        border-right: none;
        border-bottom: 1px solid var(--color-border);
    }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Detail View — hero + secciones con título (AntD Pro pattern)
   Usado en modales "ver" para dar jerarquía visual sin abusar de bordes.
   ═══════════════════════════════════════════════════════════════════════════ */

.detail-hero {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding-block-end: var(--space-md);
    border-block-end: 1px solid var(--color-border-subtle);
}

.detail-hero__avatar {
    flex-shrink: 0;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
    display: inline-grid;
    place-items: center;
    font-size: 20px;
    font-weight: var(--font-weight-semibold);
    position: relative;
}

.detail-hero__dot {
    position: absolute;
    inset-block-end: 2px;
    inset-inline-end: 2px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid var(--color-bg-elevated);
}
.detail-hero__dot[data-status="active"]   { background-color: var(--color-success); }
.detail-hero__dot[data-status="inactive"] { background-color: var(--color-text-muted); }

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

.detail-hero__title {
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
    line-height: 1.3;
}

.detail-hero__subtitle {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin: 0;
}

.detail-hero__badges {
    display: flex;
    gap: var(--space-xs);
    flex-wrap: wrap;
    margin-block-start: 4px;
}

/* Sección dentro del detalle — título destacado con accent bar + contenido */
.detail-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding-block: var(--space-sm);
}

.detail-section + .detail-section {
    border-block-start: 1px solid var(--color-border-subtle);
    padding-block-start: var(--space-lg);
}

.detail-section__title {
    font-size: var(--font-size-md);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
    padding-inline-start: var(--space-sm);
    border-inline-start: 3px solid var(--color-primary);
    line-height: 1.3;
}



/* ── switch.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/switch.css — Patrón AntD Switch: toggle on/off visual para estados
   binarios (activo/inactivo, visible/oculto, etc.).

   Uso:
     <label class="switch">
         <input type="checkbox" name="active" value="1" checked>
         <span class="switch__track"></span>
         <span class="switch__label">Cuenta activa</span>
     </label>
   ═══════════════════════════════════════════════════════════════════════════ */

.switch {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    cursor: pointer;
    user-select: none;
}

.switch input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}

.switch__track {
    position: relative;
    display: inline-block;
    width: 44px;
    height: 22px;
    background-color: var(--color-border-control);   /* apagado: 3:1 con su fondo (WCAG 1.4.11), en claro y en oscuro */
    border-radius: 11px;
    transition: background-color var(--transition-fast);
    flex-shrink: 0;
}

.switch__track::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 18px;
    height: 18px;
    background: #fff;
    border-radius: 50%;
    box-shadow: 0 2px 4px rgba(0, 35, 11, 0.2);
    transition: left var(--transition-fast), background var(--transition-fast);
}

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

.switch input:checked + .switch__track::after {
    left: 24px;
}

.switch input:focus-visible + .switch__track {
    box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
}

.switch input:disabled + .switch__track {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Bloqueado por una política (no por el formulario): toda la etiqueta. */
.switch.switch--disabled {
    cursor: not-allowed;
}

.switch__label {
    font-size: var(--font-size-base);
    color: var(--color-text);
}



/* ── impersonation-banner.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/impersonation-banner.css — banner persistente durante impersonation

   Se muestra arriba de TODAS las páginas mientras un admin está viendo la
   app como otro usuario. Color rojo intenso para que sea imposible olvidar
   que se está en este modo.
   ═══════════════════════════════════════════════════════════════════════════ */

.impersonation-banner {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-sm) var(--space-lg);
    background: #dc2626;           /* rojo AntD error */
    color: white;
    font-weight: 500;
    font-size: 0.9rem;
    position: sticky;
    top: 0;
    z-index: calc(var(--z-aviso-fijo) + 1);   /* sobre la cabecera y el aviso de la 2FA; bajo el menú del móvil, modales y cajones */
    box-shadow: 0 2px 4px rgb(0 0 0 / 0.15);
}

.impersonation-banner__icon {
    font-size: 1.2rem;
}

.impersonation-banner__text {
    flex: 1;
}

.impersonation-banner__btn {
    background: white;
    color: #dc2626;
    border: none;
    padding: var(--space-xs) var(--space-md);
    border-radius: var(--radius-sm);
    font-weight: 600;
    font-size: 0.85rem;
    cursor: pointer;
    transition: background 0.15s;
}

.impersonation-banner__btn:hover {
    background: #fef2f2;
}

.impersonation-banner__btn:focus-visible {
    outline: 2px solid white;
    outline-offset: 2px;
}

/* Empuja el header/sidebar hacia abajo para que el banner no los tape */
body:has(.impersonation-banner) .layout-app__header {
    top: var(--impersonation-banner-height, 44px);
}



/* ── banner-2fa-gracia.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/banner-2fa-gracia.css — banner countdown para 2FA obligatoria
   en periodo de gracia.

   Se muestra cuando el user está sujeto a política de 2FA obligatoria, no
   la tiene activa aún, pero todavía está dentro del plazo. Color ámbar
   (warning) — sin ser tan agresivo como el de impersonation (rojo).
   Dismissible por sesión (sessionStorage, no cookie).
   ═══════════════════════════════════════════════════════════════════════════ */

.banner-2fa-gracia {
    display: flex;
    flex-wrap: wrap;                      /* en móvil, el botón baja bajo el texto */
    align-items: center;
    gap: var(--space-sm) var(--space-md);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-warning-soft);
    border-bottom: 1px solid var(--color-warning);
    color: var(--color-warning-text);     /* #ad6800 se quedaba en 4,2:1 */
    font-size: 0.9rem;
    position: sticky;
    top: 0;
    z-index: var(--z-aviso-fijo);         /* bajo el de la suplantación, el menú del móvil, modales y cajones */
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.06);
}

/* Cerrado en esta sesión: lo marca el script previo al pintado
   (_base.html.twig), antes de que Alpine lo oculte con x-show. */
html[data-banner-2fa-oculto] .banner-2fa-gracia {
    display: none;
}

.banner-2fa-gracia__icon {
    font-size: 1.1rem;
}

/* Al menos 12rem de texto: a 360px, con el botón al lado, quedaba una
   columna de dos palabras por línea. */
.banner-2fa-gracia__text {
    flex: 1 1 12rem;
}

@media (min-width: 48rem) {
    .banner-2fa-gracia {
        padding-inline: var(--space-lg);
    }
}

.banner-2fa-gracia__btn {
    background: var(--color-primary);
    color: var(--color-text-on-dark);
    text-decoration: none;
    padding: var(--space-xs) var(--space-md);
    border-radius: var(--radius-sm);
    font-weight: 600;
    font-size: 0.85rem;
    transition: background 0.15s;
}

/* color + text-decoration: sin ellos a:hover (links.css) pinta el texto del
   color primary-hover, el mismo que el fondo, y el botón se queda en blanco. */
.banner-2fa-gracia__btn:hover {
    background: var(--color-primary-hover);
    color: var(--color-text-on-dark);
    text-decoration: none;
}

/* A la derecha también cuando baja de línea con el botón (móvil). */
.banner-2fa-gracia__close {
    margin-inline-start: auto;
    background: transparent;
    border: 0;
    color: inherit;
    font-size: 1.25rem;
    line-height: 1;
    padding: 0 var(--space-xs);
    cursor: pointer;
    opacity: 0.7;
}

.banner-2fa-gracia__close:hover {
    opacity: 1;
}



/* ── login-admin.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/login-admin.css — Pantalla de login backoffice (split-screen AntD Pro)

   Layout 2 columnas en desktop: izquierda panel de marca oscuro, derecha
   formulario compacto. En móvil el panel de marca se oculta.
   ═══════════════════════════════════════════════════════════════════════════ */

.login-admin-body {
    background: var(--color-bg);
    margin: 0;
    min-height: 100dvh;   /* fallback */
    min-height: 100svh;   /* estable ante retracción de URL bar en mobile */
}

.login-admin {
    display: grid;
    grid-template-columns: 1fr 1fr;
    min-height: 100dvh;   /* fallback */
    min-height: 100svh;   /* estable ante retracción de URL bar en mobile */
}

@media (max-width: 47.99rem) {
    .login-admin {
        grid-template-columns: 1fr;
    }
}

/* ── Panel izquierdo — marca ────────────────────────────────────────────── */
.login-admin__brand {
    background: linear-gradient(135deg, #001529 0%, #003a8c 100%);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-xl);
    position: relative;
    overflow: hidden;
}

@media (max-width: 47.99rem) {
    .login-admin__brand { display: none; }
}

.login-admin__brand::before {
    content: '';
    position: absolute;
    top: -50%;
    right: -30%;
    width: 80%;
    height: 200%;
    background: radial-gradient(circle, rgb(22 119 255 / 0.15) 0%, transparent 70%);
    pointer-events: none;
}

.login-admin__brand-content {
    position: relative;
    max-width: 420px;
    width: 100%;
}

.login-admin__brand-logo {
    width: 64px;
    height: 64px;
    border-radius: var(--radius-md);
    background: rgb(255 255 255 / 0.1);
    display: grid;
    place-items: center;
    font-size: 1.8rem;
    font-weight: 700;
    margin-bottom: var(--space-lg);
    backdrop-filter: blur(10px);
}

.login-admin__brand-title {
    font-size: 2rem;
    margin: 0 0 var(--space-xs);
    color: white;
}

/* Eslogan (setting branding.app_slogan) entre el nombre y el subtítulo. */
.login-admin__brand-slogan {
    margin-block-start: var(--space-xs);
    font-size: var(--font-size-sm);
    opacity: 0.9;
}

.login-admin__brand-sub {
    font-size: 1rem;
    opacity: 0.8;
    margin: 0 0 var(--space-xl);
}

.login-admin__brand-features {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.login-admin__brand-features li {
    display: flex;
    gap: var(--space-sm);
    align-items: flex-start;
    font-size: 0.95rem;
    opacity: 0.9;
}

.login-admin__brand-bullet {
    color: #1677FF;
    font-weight: 700;
}

/* En el flujo, bajo la lista. Con position: absolute (abajo y a la izquierda)
   se medía desde el contenido, no desde el panel, y se montaba sobre las
   últimas líneas de la lista. */
.login-admin__brand-footer {
    margin-block-start: var(--space-2xl);
    font-size: 0.8rem;
    opacity: 0.5;
}

/* ── Panel derecho — form ───────────────────────────────────────────────── */
.login-admin__form-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-xl);
}

.login-admin__form {
    width: 100%;
    max-width: 400px;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.login-admin__form-header {
    margin-bottom: var(--space-md);
}

.login-admin__form-title {
    font-size: 1.5rem;
    margin: 0 0 var(--space-xs);
}

.login-admin__form-sub {
    color: var(--color-text-muted);
    font-size: 0.9rem;
    margin: 0;
}

/* El logo del proyecto, en vez de la letra y el nombre (partials/_logo_marca.html.twig). */
.login-admin__brand-title .logo-marca {
    --logo-marca-alto: 3.5rem;
}



/* ── logo-marca.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/logo-marca.css — el logo del proyecto en las páginas de entrar
   (partials/_logo_marca.html.twig; config/proyecto.php: logo, logo_oscuro).

   Apaisado y a una altura fija: el ancho lo da la imagen. Con un logo para
   cada tema, se ve el del tema de la página (html[data-theme], que pone el
   script previo al pintado: también con «el del sistema»). Un
   <picture> con prefers-color-scheme seguiría al sistema y no al tema
   elegido en la aplicación.
   ═══════════════════════════════════════════════════════════════════════════ */

.logo-marca {
    display: inline-block;
    vertical-align: top;              /* sin el hueco de los descendentes bajo la imagen */
    block-size: var(--logo-marca-alto, 3rem);
    inline-size: auto;
    max-inline-size: 100%;
    object-fit: contain;              /* si el sitio es más estrecho que el logo, encoge entero */
}

.logo-marca[data-tema="oscuro"],
html[data-theme="dark"] .logo-marca[data-tema="claro"] {
    display: none;
}

html[data-theme="dark"] .logo-marca[data-tema="oscuro"] {
    display: inline-block;
}



/* ── diagnostico.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/diagnostico.css — estilos del panel de diagnóstico

   Reutiliza .stat-card del estándar canónico. Solo añade:
   - Estado loading (spinner centrado)
   - Viewer de logs (monospace + scroll)
   ═══════════════════════════════════════════════════════════════════════════ */

.stat-card--loading {
    opacity: 0.7;
}

/* Transición suave en el swap HTMX (hx-swap="outerHTML transition:true").
   Evita el "pop" brusco al pasar de skeleton a card resuelta.
   Interpola background/border/opacity durante 150ms. */
.stat-card {
    transition:
        background-color 150ms ease,
        border-color     150ms ease,
        opacity          150ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .stat-card { transition: none; }
}

.stat-card__meta {
    margin-top: var(--space-sm);
}

.stat-card__meta summary {
    cursor: pointer;
    padding: var(--space-xs) 0;
}

.stat-card__meta[open] summary {
    margin-bottom: var(--space-xs);
}

/* ── Action toolbar ──────────────────────────────────────────────────────
   Agrupador de botones con título + hint, usado arriba del grid de cards.
   Separa acciones por intención (ej: Mantenimiento vs Pruebas de entrega)
   para que el usuario sepa qué hace cada grupo y —en las pruebas— a dónde
   llega el mensaje. */
.action-toolbar {
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-lg);
    padding: var(--space-md) var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

.action-toolbar__head {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.action-toolbar__title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    margin: 0;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.action-toolbar__hint {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin: 0;
}

.action-toolbar__buttons {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    align-items: stretch;
}

.action-toolbar__buttons[data-align="center"] {
    align-items: center;
}

.action-toolbar__buttons .btn {
    min-height: 2.5rem;
    /* Si el label se parte en dos líneas, centrar verticalmente el texto
       y el icono dentro del botón para que todo el grupo quede alineado. */
    white-space: normal;
    line-height: 1.2;
    text-align: start;
}

/* Un formulario en la fila (el push de prueba: buscador y botón): crece y su
   buscador encoge, en vez de fijarle un ancho mínimo que en móvil lo sacaba
   del panel. */
.action-toolbar__push {
    flex: 1 1 24rem;
    min-width: 0;
}

.action-toolbar__push .combo {
    flex: 1 1 16rem;
    min-width: 0;
}

/* En móvil: grid 2 columnas con altura uniforme — sin más wraps raros. */
@media (max-width: 47.99rem) {
    .action-toolbar__buttons {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--space-sm);
    }
    .action-toolbar__buttons .btn {
        width: 100%;
        min-height: 3rem;
    }
    .action-toolbar__push {
        grid-column: 1 / -1;
    }
}

/* Pantallas muy estrechas → 1 columna (botones más legibles). */
@media (max-width: 380px) {
    .action-toolbar__buttons {
        grid-template-columns: 1fr;
    }
}

/* ── Log viewer (modal de diagnóstico → Ver logs) ─────────────────────── */
.log-viewer {
    font-family: var(--font-mono, ui-monospace, 'Cascadia Mono', Consolas, monospace);
    font-size: 0.82rem;
    background: #0e1117;
    color: #e6edf3;
    padding: var(--space-md);
    border-radius: var(--radius-md);
    max-height: 60vh;
    overflow: auto;
    white-space: pre;
    line-height: 1.5;
    margin: 0;
}

/* Resaltar niveles típicos de Monolog */
.log-viewer {
    /* Fallback simple — el coloreado completo requeriría JS */
}



/* ── cron-comando.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/cron-comando.css — Input + botón "Copiar" alineados del panel cron

   Usado en cada card del setup para mostrar el comando a pegar en el
   sistema de crons del servidor. Input readonly + botón de copiar a la
   derecha con misma altura y sin gap entre ellos.
   ═══════════════════════════════════════════════════════════════════════════ */

.cron-comando {
    display: flex;
    width: 100%;
    margin-block: var(--space-sm);
    border: 1px solid var(--color-border-control);   /* es el borde del campo (WCAG 1.4.11) */
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-bg-alt, #f5f5f5);
}

.cron-comando__input {
    flex: 1;
    min-width: 0;                    /* permite que shrink correctamente */
    border: 0;
    padding: var(--space-sm) var(--space-md);
    font-family: var(--font-mono, ui-monospace, 'Cascadia Mono', Consolas, monospace);
    font-size: 0.82rem;
    color: var(--color-text);
    background: transparent;
    outline: none;
}

.cron-comando__input:focus {
    background: var(--color-bg);
}

.cron-comando__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--space-sm) var(--space-md);
    border: 0;
    border-inline-start: 1px solid var(--color-border);
    background: var(--color-bg-elevated, white);
    color: var(--color-text);
    font-size: 0.85rem;
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s;
}

.cron-comando__btn:hover {
    background: var(--color-primary);
    color: white;
}

.cron-comando__btn:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: -2px;
}

.cron-comando__btn svg {
    width: 1rem;
    height: 1rem;
}

/* ── Header del card de tag (título + badge cron en la misma línea) ─────── */
.cron-tag-card__header {
    display: flex;
    align-items: flex-start;         /* top-align para que si el título ocupa
                                        2 líneas, el badge quede arriba a la derecha */
    justify-content: space-between;
    flex-wrap: nowrap;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
    /* Reserva espacio para 2 líneas del título — así la sección siguiente
       (descripción Tag/Frecuencia) arranca a la misma altura en todas las
       cards del grid, incluso si un título ocupa 2 líneas. */
    min-height: calc(var(--font-size-lg) * 2 * 1.25);
}

.cron-tag-card__title {
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    line-height: 1.25;
    margin: 0;
    min-width: 0;                    /* evita que el título empuje al badge */
    /* Limita a 2 líneas con ellipsis si el nombre fuera muy largo */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.cron-tag-card__cron {
    flex-shrink: 0;                  /* el badge nunca se comprime ni baja */
    font-family: var(--font-mono, ui-monospace, monospace);
}



/* ── combo.css ───────────────────────────────── */
/* ── combo — searchable combobox ─────────────────────────────────────────
   Dropdown estilizado reemplazando la <datalist> nativa. Ver
   comboBuscador() en app.js para el comportamiento. */

.combo {
    position: relative;
}

.combo__dropdown {
    position: absolute;
    top: calc(100% + var(--space-xs));
    left: 0;
    right: 0;
    z-index: var(--z-desplegable);
    max-height: 18rem;
    overflow-y: auto;
    margin: 0;
    padding: var(--space-xs);
    list-style: none;
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
}

.combo__option {
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--font-size-sm);
    color: var(--color-text);
    transition: background-color 0.12s ease, color 0.12s ease;
    display: flex;
    flex-direction: column;
    gap: 2px;
    line-height: 1.4;
}

/* Hover y highlight (keyboard nav) — patrón AntD: fondo azul suave
   (blue-1) + texto primary. Contraste claro frente al fondo blanco. */
.combo__option:hover,
.combo__option.is-highlighted {
    background: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.combo__option:hover .combo__option-secondary,
.combo__option.is-highlighted .combo__option-secondary {
    color: var(--color-primary-text);
}

.combo__option.is-empty {
    color: var(--color-text-muted);
    font-style: italic;
    cursor: default;
}

.combo__option.is-empty:hover {
    background: transparent;
    color: var(--color-text-muted);
}

.combo__option-secondary {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
}



/* ── pull-refresh.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/pull-refresh.css — Indicador pull-to-refresh (Capacitor)

   Spinner circular tipo Material Design que aparece al arrastrar hacia
   abajo desde el top. Sigue el dedo proporcionalmente y rota al soltar.
   La posición y el giro los pone app.js en --pull-y y --pull-giro; sin
   ellas, queda plegado arriba (fuera de la pantalla).
   ═══════════════════════════════════════════════════════════════════════════ */

.pull-refresh {
    position: fixed;
    top: 0;
    left: 50%;
    transform: translateX(-50%) translateY(var(--pull-y, -60px)) rotate(var(--pull-giro, 0deg));
    z-index: var(--z-progreso);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--color-bg-elevated, #fff);
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
    display: grid;
    place-items: center;
    transition: transform 0.2s ease, opacity 0.2s ease;
    opacity: 0;
    pointer-events: none;
}

.pull-refresh[data-visible="true"] {
    opacity: 1;
}

.pull-refresh[data-refreshing="true"] .pull-refresh__spinner {
    animation: pull-refresh-spin 0.6s linear infinite;
}

/* AntD Spin — 4 puntos en rotación (patrón Ant Design) */
.pull-refresh__spinner {
    width: 22px;
    height: 22px;
    position: relative;
}

.pull-refresh__spinner::before,
.pull-refresh__spinner::after,
.pull-refresh__spinner i::before,
.pull-refresh__spinner i::after {
    content: '';
    position: absolute;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-primary, #1677ff);
    animation: pull-refresh-antd 1s ease-in-out infinite;
}

.pull-refresh__spinner::before  { top: 0;    left: 50%; margin-left: -3px; animation-delay: 0s; }
.pull-refresh__spinner::after   { right: 0;  top: 50%;  margin-top: -3px;  animation-delay: 0.25s; }
.pull-refresh__spinner i::before { bottom: 0; left: 50%; margin-left: -3px; animation-delay: 0.5s; }
.pull-refresh__spinner i::after  { left: 0;   top: 50%;  margin-top: -3px;  animation-delay: 0.75s; }

@keyframes pull-refresh-antd {
    0%, 40%, 100% { opacity: 0.3; transform: scale(0.7); }
    20%           { opacity: 1;   transform: scale(1); }
}



/* ── offline-banner.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/offline-banner.css — Banner "Sin conexión" persistente

   Aparece/desaparece automáticamente según el estado de la red.
   Se coloca justo debajo del header, empujando el contenido.
   ═══════════════════════════════════════════════════════════════════════════ */

.offline-banner {
    display: none;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    background: #fff2e8;
    color: #d4380d;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    text-align: center;
    border-bottom: 2px solid #ff7a45;
    min-height: 44px;
    /* Overlay fijo — evita empujar el layout. Se sitúa JUSTO debajo de la
       status bar (via `top: env(safe-area-inset-top)`) en vez de extender
       su fondo naranja hacia esa zona — así los iconos del sistema no se
       mezclan con el color del banner en iPhone con notch / Android con
       status bar overlay. Capa --z-sin-conexion (tokens.css): por encima
       de desplegables, modales y cajones, por debajo de los toasts. */
    position: fixed;
    top: env(safe-area-inset-top, 0);
    inset-inline: 0;
    z-index: var(--z-sin-conexion);
}

.offline-banner[data-visible="true"] {
    display: flex;
}

.offline-banner__spinner {
    width: 16px;
    height: 16px;
    border: 2px solid #ffbb96;
    border-top-color: #d4380d;
    border-radius: 50%;
    animation: offline-spin 0.8s linear infinite;
    flex-shrink: 0;
}

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

.offline-banner--reconnected {
    background: #f6ffed;
    color: #389e0d;
    border-color: #52c41a;
}

.offline-banner--reconnected .offline-banner__spinner {
    animation: none;
    border-color: #52c41a;
    border-top-color: #52c41a;
}



/* ── pagos.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/pagos.css — Módulo «pagos»: tarjetas de pasarelas, URLs para copiar,
   resumen del checkout, casillas legales y página de vuelta.
   Mobile-first; solo tokens.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Pasarelas: una tarjeta por driver ──────────────────────────────────── */
.pasarelas-grid {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 900px) {
    .pasarelas-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.pasarela .card__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
}

.pasarela__modo {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
}

/* ── Texto con botón «Copiar» (URL de notificación) ─────────────────────── */
.copiable {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
}

/* El botón no encoge: la URL (que parte en cualquier carácter) cede el sitio.
   Sin esto, «Copiar» quedaba en 41px partido en «Cop / iar». */
.copiable > .btn {
    flex-shrink: 0;
    white-space: nowrap;
}

.copiable__texto {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
    font-size: var(--font-size-sm);
    padding: var(--space-xs) var(--space-sm);
    background-color: var(--color-bg-alt);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-sm);
}

/* ── Checkout ───────────────────────────────────────────────────────────── */
.checkout-resumen {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
}

.checkout-resumen__importe {
    font-size: var(--font-size-2xl);
    font-weight: var(--font-weight-bold);
}

.checkout-medios {
    display: grid;
    gap: var(--space-sm);
}

.checkout-casillas {
    display: grid;
    gap: var(--space-sm);
    padding: var(--space-md);
    background-color: var(--color-bg-alt);
    border-radius: var(--radius-md);
}

.checkout-casillas .form__check[data-error="true"] {
    color: var(--color-danger-text);
}

/* ── Estado del pago (página de vuelta) ─────────────────────────────────── */
.estado-pago {
    display: grid;
    justify-items: center;
    gap: var(--space-sm);
    text-align: center;
}

.estado-pago__icono {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: var(--radius-full);
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
}

.estado-pago[data-estado="pagado"] .estado-pago__icono {
    background-color: var(--color-success-soft);
    color: var(--color-success);
}

.estado-pago[data-estado="fallido"] .estado-pago__icono,
.estado-pago[data-estado="cancelado"] .estado-pago__icono,
.estado-pago[data-estado="caducado"] .estado-pago__icono {
    background-color: var(--color-danger-soft);
    color: var(--color-danger);
}

.estado-pago[data-estado="pendiente"] .estado-pago__icono {
    background-color: var(--color-warning-soft);
    color: var(--color-warning);
}

/* ── Pasarela simulada: aviso visible de que nada es real ───────────────── */
.pasarela-simulada__aviso {
    padding: var(--space-sm) var(--space-md);
    border: 1px dashed var(--color-warning);
    border-radius: var(--radius-md);
    background-color: var(--color-warning-soft);
    text-align: center;
}



/* ── casos.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/casos.css — Módulo «casos»: celda de la bandeja, ficha del caso
   (dos columnas en escritorio), historial y «Mis consultas» del portal.
   Mobile-first; solo tokens.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Bandeja: título del caso con su tipo debajo ────────────────────────── */
.caso-celda {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.caso-celda__titulo {
    color: var(--color-text);
    font-weight: var(--font-weight-medium);
    text-decoration: none;
}

.caso-celda__titulo:hover,
.caso-celda__titulo:focus-visible {
    color: var(--color-primary-text);
    text-decoration: underline;
}

.caso-celda__sub {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}

/* ── Ficha: una columna en móvil, principal + lateral en escritorio ─────── */
.caso-ficha {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.caso-ficha__columnas {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-md);
    align-items: start;
}

@media (min-width: 1024px) {
    .caso-ficha__columnas {
        grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
    }
}

.caso-ficha__lateral .form__select,
.caso-ficha__lateral .form__textarea {
    width: 100%;
}

/* Reasignar y prioridad: el desplegable y su botón en una fila, para que el
   lateral no sea mucho más largo que la columna principal. Si no caben,
   el botón baja. */
.caso-ficha__en-linea {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
}

.caso-ficha__en-linea .form__select {
    flex: 1 1 10rem;
    width: auto;
    min-width: 0;
}

.caso-composer__para {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
}

/* Acciones del tipo: a todo el ancho del lateral. */
.caso-accion,
.caso-accion > summary {
    width: 100%;
}

.caso-accion > summary {
    list-style: none;
    cursor: pointer;
}

.caso-accion > summary::-webkit-details-marker {
    display: none;
}

.caso-accion[open] > summary {
    margin-block-end: var(--space-sm);
}

/* ── Historial (ficha y portal) ─────────────────────────────────────────── */
.historial {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

.historial__paso {
    position: relative;
    padding-inline-start: var(--space-md);
    border-inline-start: 2px solid var(--color-border);
}

.historial__paso[data-tipo="mensaje"],
.historial__paso[data-tipo="respuesta"],
.historial__paso[data-tipo="creado"] {
    border-inline-start-color: var(--color-primary);
}

.historial__paso[data-tipo="vencido"],
.historial__paso[data-tipo="escalado"] {
    border-inline-start-color: var(--color-danger);
}

.historial__cabecera {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-xs);
}

.historial__que {
    font-weight: var(--font-weight-medium);
}

.historial__quien {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}

.historial__texto {
    margin-block-start: var(--space-xs);
    padding: var(--space-sm);
    border-radius: var(--radius-md);
    background-color: var(--color-bg-alt);
    overflow-wrap: anywhere;
}

/* Lo que el solicitante no ve, marcado: que nadie lo confunda con un mensaje. */
.historial__texto[data-visible="interna"] {
    background-color: var(--color-warning-soft);
}

/* En el portal, lo suyo a un lado y lo del equipo al otro. */
.historial__paso[data-de="yo"] {
    border-inline-start-color: var(--color-border);
}

@media (min-width: 768px) {
    .historial__paso[data-de="yo"] {
        margin-inline-start: var(--space-xl);
    }

    .historial__paso[data-de="equipo"] {
        margin-inline-end: var(--space-xl);
    }
}

/* ── «Mis consultas» ────────────────────────────────────────────────────── */
.consultas-lista {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

.consultas-lista__item {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    color: var(--color-text);
    text-decoration: none;
}

.consultas-lista__item:hover,
.consultas-lista__item:focus-visible {
    border-color: var(--color-primary-border);
}

.consultas-lista__titulo {
    font-weight: var(--font-weight-medium);
}

.consultas-lista__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
}



/* ── avisos-admin.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/avisos-admin.css — Los avisos en el panel: el editor de tipos de
   notificación con su vista previa (partials/_editor_tipos_notificacion,
   en Notificaciones › Tipos y en el cajón de Configuración) y el compositor
   (Notificaciones › Enviar).

   Anatomía:
     .tipo-aviso__nombre      nombre y clave del tipo, en la fila compacta
     .tipo-aviso__urgencia    el select de urgencia
     .tipo-aviso__plantilla   el editor plegable, separado por una línea
     .tipo-aviso__email       la sección del correo propio, sobre gris
     .tipo-aviso__capa        overlay de la vista previa: por encima del
                              cajón de Configuración y de los avisos
     .tipo-aviso__modal       la vista previa (ancho propio, también en móvil)
     .tipo-aviso__pestanas    sus pestañas, con el margen del modal
     .tipo-aviso__cuerpo      su cuerpo, que se desplaza
     .aviso-muestra           la notificación de muestra (campana, correo, push)
     .compositor__clave       la clave de una variable personalizada
   ═══════════════════════════════════════════════════════════════════════════ */

.tipo-aviso__nombre {
    min-width: 220px;
}

.tipo-aviso__urgencia {
    min-width: 140px;
}

.tipo-aviso__plantilla {
    margin-top: var(--space-md);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
}

.tipo-aviso__email {
    margin-bottom: var(--space-md);
    padding: var(--space-sm);
    background: var(--color-bg-alt);
    border-radius: var(--radius-md);
}

.modal__overlay.tipo-aviso__capa {
    z-index: var(--z-modal-encima);
}

/* Con más peso que las reglas de móvil de modal.css (pantalla completa):
   la vista previa conserva su ancho en todos los tamaños. */
.modal__overlay > .modal.tipo-aviso__modal {
    width: min(720px, 92vw);
    max-height: 88vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.tipo-aviso__pestanas {
    padding: 0 var(--space-lg);
}

.tipo-aviso__cuerpo {
    flex: 1;
    overflow: auto;
}

/* ── Muestras: cómo se verá en la campana, el correo y el móvil ──────────── */

.aviso-muestra {
    background: white;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-md);
    max-width: 380px;
}

.aviso-muestra__icono {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--color-primary);
    color: white;
    display: grid;
    place-items: center;
    flex-shrink: 0;
}

.aviso-muestra__correo {
    width: 100%;
    height: 480px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: white;
}

/* El fondo de una pantalla de móvil con su banner de notificación. */
.aviso-muestra__pantalla {
    background: linear-gradient(180deg, #f0f2f5 0%, #dcdfe3 100%);
    padding: var(--space-lg);
    border-radius: var(--radius-lg);
}

.aviso-muestra__banner {
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(10px);
    border-radius: var(--radius-lg);
    padding: var(--space-sm) var(--space-md);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
    max-width: 360px;
    margin: 0 auto;
}

.aviso-muestra__app {
    width: 20px;
    height: 20px;
    border-radius: var(--radius-sm);
    background: var(--color-primary);
}

/* ── Compositor: una variable personalizada es clave (fija) + valor ─────── */

.compositor__clave {
    flex: 0 0 200px;
}



/* ── avisos-cliente.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/avisos-cliente.css — Los avisos en el área de cliente: la lista
   corta de Inicio (pages/cliente/inicio) y la página de Avisos
   (pages/cliente/notificaciones).

     .aviso-fila              un aviso de la lista de Inicio, con línea debajo
                              salvo el último
     .aviso-fila__marca       su marca (leído / nuevo), alineada con el título
     .aviso-tarjeta           un aviso de la página de Avisos; sin leer, con la
                              raya del color de marca a la izquierda
     .aviso-tarjeta__icono    el círculo con su icono
   ═══════════════════════════════════════════════════════════════════════════ */

.aviso-fila {
    padding: var(--space-xs) 0;
}

.aviso-fila:not(:last-child) {
    border-bottom: 1px solid var(--color-border-subtle);
}

.aviso-fila__marca {
    flex-shrink: 0;
    margin-top: 2px;
}

.aviso-tarjeta[data-unread="true"] {
    border-left: 3px solid var(--color-primary);
}

.aviso-tarjeta__icono {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--color-bg-alt);
    display: grid;
    place-items: center;
    flex-shrink: 0;
    font-size: 1.1rem;
}



/* ── ajustes.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/ajustes.css — Piezas propias de Configuración
   (pages/admin/configuracion/index.html.twig).

     .ajustes                    la página: navegación y contenido; según su
                                 ancho (container query), columna o desplegable
     .ajustes__marco             la rejilla: una columna o navegación + contenido
     .ajustes__nav               la navegación (<nav>)
     .ajustes__selector          el desplegable, en pantallas estrechas
     .ajustes__columna           la columna, en anchas: se queda a la vista al
                                 bajar y, si no cabe, se desplaza ella sola
     .ajustes__categoria         una categoría: su título y su lista
     .ajustes__categoria-titulo
     .ajustes__lista             sus grupos (role="tablist", vertical)
     .ajustes__pestana           un grupo (role="tab"); el abierto, data-active
     .ajustes__contenido         el grupo abierto y el formulario

     .ajuste-imagen        vista del logo subido (o «Sin logo»), 96×96
     .ajuste-imagen__img   el logo, entero dentro del cuadro
     .ajuste-prueba        el campo «Enviar test a» junto a su botón
     .ajuste-roles         un ajuste de tipo roles_list: una casilla por rol
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Navegación por categorías ───────────────────────────────────────────────
   Móvil primero: el desplegable encima del contenido. Desde 50rem de ancho
   de la página (no de la ventana: el menú lateral, abierto o plegado, cambia
   lo que queda), la columna a la izquierda. */
.ajustes {
    container: ajustes / inline-size;
}

.ajustes__marco {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-lg);
    align-items: start;
}

.ajustes__selector {
    display: grid;
    gap: var(--space-xs);
    max-inline-size: var(--container-xs);
}

/* El grupo abierto, arriba del todo, a la altura de la columna: delante solo
   tiene los campos ocultos del formulario, pero `.flow` le daba su margen. */
.ajustes__contenido > form > [role="tabpanel"] {
    margin-block-start: 0;
}

.ajustes__columna {
    display: none;
}

.ajustes__categoria {
    display: grid;
    gap: var(--space-xs);
}

.ajustes__categoria-titulo {
    margin: 0;
    padding-inline: var(--space-sm);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    line-height: var(--line-height-base);
    letter-spacing: var(--letter-spacing-wide);
    color: var(--color-text-muted);
}

.ajustes__lista {
    display: grid;
    gap: 2px;
}

.ajustes__pestana {
    position: relative;
    display: block;
    inline-size: 100%;
    padding: 6px var(--space-sm) 6px var(--space-md);
    border: 0;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text);
    font: inherit;
    font-size: var(--font-size-sm);
    line-height: var(--line-height-base);
    text-align: start;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.ajustes__pestana:hover {
    background-color: var(--color-bg-alt);
    color: var(--color-primary-text);
}

.ajustes__pestana[data-active="true"] {
    background-color: var(--color-primary-soft);
    color: var(--color-primary-text);
    font-weight: var(--font-weight-medium);
}

/* La marca del abierto, a la izquierda (como el subrayado de las horizontales). */
.ajustes__pestana[data-active="true"]::before {
    content: '';
    position: absolute;
    inset-block: 6px;
    inset-inline-start: 0;
    inline-size: 3px;
    border-radius: var(--radius-full);
    background-color: var(--color-primary);
}

/* El anillo de foco, por dentro: la columna recorta lo que sobresale. */
.ajustes__pestana:focus-visible {
    outline-offset: -2px;
}

@container ajustes (min-width: 50rem) {
    .ajustes__marco {
        grid-template-columns: 15rem minmax(0, 1fr);
        gap: var(--space-xl);
    }

    .ajustes__selector {
        display: none;
    }

    /* A la vista al bajar por un grupo largo; si las categorías no caben en
       la altura de la ventana, la columna se desplaza ella sola (con la barra
       fina del panel) y la abierta se asoma (data-pestanas-marco). */
    .ajustes__nav {
        position: sticky;
        inset-block-start: 0;
    }

    .ajustes__columna {
        display: grid;
        gap: var(--space-lg);
        max-block-size: calc(100dvh - var(--header-height) - var(--space-2xl));
        overflow-y: auto;
        overscroll-behavior: contain;
        scrollbar-width: thin;
        scrollbar-color: var(--color-border-strong) transparent;
        padding-block-end: var(--space-md);
    }
}

.ajuste-imagen {
    width: 96px;
    height: 96px;
    border-radius: var(--radius-md);
    border: 1px dashed var(--color-border);
    display: grid;
    place-items: center;
    overflow: hidden;
    background: var(--color-bg-alt);
}

.ajuste-imagen__img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.form__field.ajuste-prueba {
    margin-bottom: 0;
    min-width: 18rem;
}

/* Un ajuste de tipo `roles_list` (partials/_ajuste_roles_list): una casilla
   por rol, cada una en su línea, en un recuadro gris. */
.ajuste-roles {
    padding: var(--space-sm);
    background: var(--color-bg-alt);
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
}

.ajuste-roles > .form__check {
    display: flex;
}



/* ── codigo.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/codigo.css — Un valor técnico para leer o copiar entero (un
   secreto, un agente de usuario): de ancho fijo, en un recuadro gris, y
   partiendo donde haga falta.

     <code class="codigo-bloque">…</code>
     <code class="codigo-bloque" data-tamano="xs">…</code>
   ═══════════════════════════════════════════════════════════════════════════ */

.codigo-bloque {
    display: block;
    padding: var(--space-sm);
    background: var(--color-bg-alt);
    border-radius: var(--radius-sm);
    word-break: break-all;
}

.codigo-bloque[data-tamano="xs"] {
    font-size: var(--font-size-xs);
}



/* ── doble-factor.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/doble-factor.css — Activar la verificación en dos pasos
   (partials/_activacion_2fa_modal, partials/_recovery_codes_2fa).

     .doble-factor__qr-caja   columna del QR: no encoge
     .doble-factor__qr        el QR, 220×220 sobre blanco (se lee igual en oscuro)
     .doble-factor__manual    columna del código para teclearlo a mano
     .doble-factor__secreto   ese código (sobre .codigo-bloque), espaciado
     .doble-factor__codigos   la lista de códigos de recuperación, sobre gris
     .doble-factor__codigo    cada código de recuperación
   ═══════════════════════════════════════════════════════════════════════════ */

.doble-factor__qr-caja {
    flex: 0 0 auto;
}

.doble-factor__qr {
    width: 220px;
    height: 220px;
    display: block;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: white;
    padding: var(--space-xs);
}

.doble-factor__manual {
    flex: 1;
    min-width: 240px;
}

.doble-factor__secreto {
    font-size: 0.9rem;
    letter-spacing: 0.1em;
}

.doble-factor__codigos {
    background: var(--color-bg-alt);
    padding: var(--space-sm);
    border-radius: var(--radius-md);
}

.doble-factor__codigo {
    font-family: var(--font-mono, monospace);
    font-size: 0.95rem;
    letter-spacing: 0.05em;
    display: block;
}



/* ── requisitos-clave.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/requisitos-clave.css — Lo que pide una contraseña nueva, marcado en
   verde según se cumple al teclear (pages/perfil/password,
   pages/cliente/password).

     .requisitos-clave                       la lista, algo menor que el texto
     .requisitos-clave__item[data-cumple]    un requisito; "true", en verde
     .requisitos-clave__repite[data-estado]  «Coinciden» (si) o no (no)

   El estado lo pone Alpine en el atributo (:data-cumple, :data-estado); el
   color, esta hoja.
   ═══════════════════════════════════════════════════════════════════════════ */

.requisitos-clave {
    font-size: 13px;
}

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

.requisitos-clave__item[data-cumple="true"] {
    color: var(--color-success);
}

.requisitos-clave__repite[data-estado="si"] {
    color: var(--color-success);
}

.requisitos-clave__repite[data-estado="no"] {
    color: var(--color-danger);
}



/* ── permisos.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/permisos.css — Elegir los permisos de un rol
   (partials/_form_role_modal) y ver los de un usuario
   (partials/_usuario_detalle_modal).

     .permisos__area           «(área)» tras el nombre de un dominio
     .permisos__transversales  la sección de los permisos de sistema, separada
     .permisos__titulo         su título, con el icono delante
     .perm-group__body         los permisos de un área, sangrados
     .perm-group__permiso      cada permiso (una etiqueta), de ancho fijo
   ═══════════════════════════════════════════════════════════════════════════ */

.permisos__area {
    margin-left: 6px;
}

.form-subsection.permisos__transversales {
    margin-top: var(--space-lg);
    border-top: 1px solid var(--color-border);
    padding-top: var(--space-md);
}

.permisos__titulo {
    display: flex;
    align-items: center;
    gap: 6px;
}

.perm-group__body {
    margin-top: var(--space-xs);
    padding-left: var(--space-md);
}

.badge.perm-group__permiso {
    font-family: var(--font-mono, monospace);
    font-size: 11px;
}



/* ── importar.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/importar.css — El asistente de importación (partials/_import_modal,
   _import_preview, _import_resultado).

     .importar__pasos         la barra de pasos bajo la cabecera del modal
     .importar__avanzadas     las opciones avanzadas al desplegarlas
     .importar__cifra         una cifra del análisis (Total, OK…)
     .importar__cifra-final   una cifra del resultado (Creados…), algo más ancha
     .importar__mensajes      la lista de mensajes de una fila
     .importar__hecho         «Import completado», más compacto que un vacío
   ═══════════════════════════════════════════════════════════════════════════ */

.importar__pasos {
    padding: var(--space-md) var(--space-lg);
    border-bottom: 1px solid var(--color-border-subtle);
}

.importar__avanzadas {
    padding-top: var(--space-sm);
}

.stat-card.importar__cifra {
    min-width: 120px;
}

.stat-card.importar__cifra-final {
    min-width: 140px;
}

.importar__mensajes {
    margin: 0;
    padding-left: 1em;
}

.empty-state.importar__hecho {
    padding: var(--space-xl) var(--space-md);
}



/* ── dashboard.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/dashboard.css — El inicio del panel (pages/dashboard.html.twig).

     .dashboard-grid       rejilla de tarjetas: tantas columnas de 320px como
                           quepan (con menos sitio, una debajo de otra)
     .quick-actions        rejilla de accesos rápidos
     .quick-action         un acceso: icono, título y una línea de ayuda
   ═══════════════════════════════════════════════════════════════════════════ */

.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
    gap: var(--space-lg);
}

.quick-actions {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-sm);
}

.quick-action {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-bg-elevated);
    color: var(--color-text);
    text-decoration: none;
    transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}

.quick-action:hover {
    background: var(--color-primary-soft);
    border-color: var(--color-primary);
    transform: translateY(-1px);
}

.quick-action__icon {
    display: inline-grid;
    place-items: center;
    width: 40px;
    height: 40px;
    background: var(--color-primary-soft);
    color: var(--color-primary);
    border-radius: var(--radius-md);
    flex-shrink: 0;
}

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

.quick-action__text small {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}



/* ── barra.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/barra.css — Una proporción que solo se sabe al pintar (un avance,
   un uso, un paso de un embudo) sin `style="width: …%"`: un <progress> con
   su valor y su máximo, que el navegador dibuja a esa proporción exacta y
   los lectores de pantalla anuncian («37 %»).

     <progress class="barra" value="{{ pct }}" max="100">{{ pct }} %</progress>
     <progress class="barra" value="{{ hechos }}" max="{{ total }}">{{ hechos }} de {{ total }}</progress>

   El aspecto se cambia con variables, en el bloque que la usa (sin tocar
   este):

     --barra-alto    alto (0.5rem; 100% para llenar una caja de alto fijo)
     --barra-fondo   el carril (gris de fondo; transparent sobre una caja propia)
     --barra-color   lo que va lleno (el color de marca)
     --barra-radio   las esquinas (redondas)
     --barra-radio-lleno  las de lo lleno (las mismas; 0 si la barra son dos
                     partes que suman el todo, como «plan / libre»)

   Un texto encima (una cifra dentro de la barra) va en un hermano con
   position: absolute dentro de una caja con position: relative: un
   <progress> no muestra su contenido. docs/frontend.md §«Valores que solo
   se saben al pintar».
   ═══════════════════════════════════════════════════════════════════════════ */

.barra {
    appearance: none;
    display: block;
    inline-size: 100%;
    block-size: var(--barra-alto, 0.5rem);
    border: 0;
    border-radius: var(--barra-radio, var(--radius-full));
    background-color: var(--barra-fondo, var(--color-bg-layout));
    overflow: hidden;
}

/* Chromium y Safari: el carril y lo lleno son pseudoelementos. */
.barra::-webkit-progress-bar {
    background-color: transparent;
}

.barra::-webkit-progress-value {
    background-color: var(--barra-color, var(--color-primary));
    border-radius: var(--barra-radio-lleno, var(--barra-radio, var(--radius-full)));
    transition: inline-size var(--transition-slow);
}

/* Firefox. */
.barra::-moz-progress-bar {
    background-color: var(--barra-color, var(--color-primary));
    border-radius: var(--barra-radio-lleno, var(--barra-radio, var(--radius-full)));
}



/* ── ayuda.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/ayuda.css — Bloque: la ayuda de una página del panel

   El contenido del cajón que abre el botón «Ayuda» de la cabecera
   (partials/_ayuda_pagina.html.twig, docs/ayuda.md): el Markdown del proyecto
   pintado por App\View\MarkdownSeguro. Sus títulos llegan un nivel por debajo
   (`##` → <h3>): el <h2> es el título del cajón.

       <div class="drawer__body ayuda">…</div>

   Una cita (`> Ejemplo: …`) sale como un recuadro: es donde van los ejemplos.
   ═══════════════════════════════════════════════════════════════════════════ */

.ayuda-pagina {
    display: flex;
}

.ayuda {
    color: var(--color-text);
    line-height: var(--line-height-base);
}

.ayuda > * + * {
    margin-block-start: var(--space-md);
}

.ayuda > :first-child {
    margin-block-start: 0;
}

.ayuda h2,
.ayuda h3 {
    font-size: var(--font-size-md);
    margin-block-start: var(--space-xl);
}

.ayuda h4,
.ayuda h5,
.ayuda h6 {
    font-size: var(--font-size-base);
    margin-block-start: var(--space-lg);
}

.ayuda ul,
.ayuda ol {
    padding-inline-start: var(--space-lg);
}

.ayuda li + li,
.ayuda li > ul,
.ayuda li > ol {
    margin-block-start: var(--space-xs);
}

/* Los ejemplos. */
.ayuda blockquote {
    padding: var(--space-sm) var(--space-md);
    border-inline-start: 3px solid var(--color-primary-border);
    border-radius: var(--radius-md);
    background-color: var(--color-primary-soft);
}

.ayuda blockquote > * + * {
    margin-block-start: var(--space-sm);
}

.ayuda code {
    padding-inline: var(--space-xs);
    border-radius: var(--radius-sm);
    background-color: var(--color-bg-alt);
}

.ayuda hr {
    border: none;
    border-block-start: 1px solid var(--color-border-subtle);
}

.ayuda table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--font-size-sm);
}

.ayuda th,
.ayuda td {
    padding: var(--space-xs) var(--space-sm);
    border-block-end: 1px solid var(--color-border-subtle);
    text-align: start;
    vertical-align: top;
}

.ayuda img {
    height: auto;
    border-radius: var(--radius-md);
}



/* ── transicion-paginas.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   blocks/transicion-paginas.css — Cambio de página suave en el panel

   El panel es MPA: cada sección es una página completa. Con las View
   Transitions entre documentos (solo CSS, sin JavaScript), al ir de una
   sección a otra la barra lateral y la cabecera se quedan quietas y solo
   el contenido cambia, con un fundido corto y un desplazamiento mínimo.

   Cuándo hay transición:
     - Navegaciones del mismo origen entre dos páginas con esta hoja (clic en
       un enlace, formulario, atrás/adelante). Un F5 o una URL escrita a mano
       no tienen: es como el navegador lo define.
     - Solo anima si la página nueva es del panel (`body.layout-app-body`).
       Las demás páginas de esta hoja (acceso, portal de cliente, errores)
       cambian en el acto, como sin transición.
     - Nunca dentro de la app Capacitor (Modo A, html[data-capacitor]): ahí
       el gesto atrás del sistema y una transición se pisan (gotcha de
       docs/gotchas.md «View Transitions globales en hx-boost colisionan
       con back gesture»). Además, la APK no carga el panel
       (CapacitorAreaGuardMiddleware).
     - Con «reducir movimiento» del sistema, no hay transición.
     - Un navegador sin View Transitions entre documentos navega como siempre.

   No choca con HTMX: sus intercambios parciales son del mismo documento y
   no usan esto. El tipo `navegacion` solo lo llevan las navegaciones entre
   documentos, y los nombres de abajo solo existen mientras dura una de
   ellas: un `transition:true` de HTMX (el «tirar para recargar») sigue con
   su fundido de siempre, y fuera de la transición la barra lateral, la
   cabecera y el contenido no son contextos de apilamiento nuevos (un
   view-transition-name permanente lo sería y cambiaría las capas).
   ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: no-preference) {
    @view-transition {
        navigation: auto;
        types: navegacion;
    }
}

/* Por defecto, la transición entre documentos se resuelve en el acto: la
   página nueva aparece tal cual. El panel la anima abajo. */
html:active-view-transition-type(navegacion)::view-transition-group(*) {
    animation-duration: 0s;
}

/* Panel: tres piezas con nombre. La barra lateral y la cabecera heredan el
   0s de arriba (se quedan quietas: si cambia algo, como la entrada marcada
   del menú, cambia sin animación); el contenido tiene su propia animación. */
html:active-view-transition-type(navegacion):not([data-capacitor="true"]) .layout-app__sidebar {
    view-transition-name: panel-lateral;
}

html:active-view-transition-type(navegacion):not([data-capacitor="true"]) .layout-app__header {
    view-transition-name: panel-cabecera;
}

html:active-view-transition-type(navegacion):not([data-capacitor="true"]) .layout-app__main {
    view-transition-name: panel-contenido;
}

/* El contenido: el anterior se va rápido y el nuevo entra con un fundido y
   4px de subida. Entre los dos se ve un instante el fondo del panel, que es
   lo que da la sensación de «pasar» de una sección a otra. Solo si la
   página nueva es del panel: al salir de él (cerrar sesión, ir al portal)
   el contenido viejo también desaparece en el acto. */
html:active-view-transition-type(navegacion):has(> body.layout-app-body):not([data-capacitor="true"])::view-transition-old(panel-contenido) {
    animation: transicion-panel-salida 90ms var(--ease-out) both;
}

html:active-view-transition-type(navegacion):has(> body.layout-app-body):not([data-capacitor="true"])::view-transition-new(panel-contenido) {
    animation: transicion-panel-entrada 180ms var(--ease-out) 50ms both;
}

@keyframes transicion-panel-salida {
    to { opacity: 0; }
}

@keyframes transicion-panel-entrada {
    from {
        opacity: 0;
        transform: translateY(4px);
    }
}



/* 6. Theme overrides — se aplican al final para ganar en cascada sobre blocks */

/* ── theme-dark.css ───────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   theme-dark.css — Overrides para modo oscuro (AntD Dark Algorithm).
   Se activa con [data-theme="dark"] en <html>. Los componentes no cambian:
   solo los custom properties.
   ═══════════════════════════════════════════════════════════════════════════ */

html[data-theme="dark"] {

    /* Controles del navegador en oscuro: el calendario y el icono de los
       campos de fecha y hora, las barras de desplazamiento, las casillas
       y los menús de los select. */
    color-scheme: dark;

    /* ── Paleta primary — AntD Dark mantiene el mismo brand ──────────── */
    /* El primary queda igual; los "soft" y hover se invierten en luminancia */
    --color-primary-soft:   rgba(22, 119, 255, 0.15);
    --color-primary-border: #1668DC;
    /* Texto en el color de marca sobre los fondos oscuros: el de marca
       aclarado hasta 4,5:1 (ColorDeMarca, «claro»; _base.html.twig lo fija
       desde el ajuste de la marca). El fuerte se queda en 3,2:1 aquí. */
    --color-primary-text:   #4592FF;

    /* ── Estados semánticos — versiones "soft" oscuras ───────────────── */
    --color-success-soft:   rgba(82, 196, 26, 0.12);
    --color-warning-soft:   rgba(250, 173, 20, 0.12);
    --color-danger-soft:    rgba(255, 77, 79, 0.14);

    /* Texto de estado: sobre oscuro, los tonos claros (4,5:1 sobre los fondos
       y su -soft). Los -strong, con texto blanco, no cambian. */
    --color-success-text:   var(--color-success);
    --color-warning-text:   var(--color-warning);
    --color-danger-text:    #FF7875;

    /* ── Fondos / superficies — escala invertida ─────────────────────── */
    --color-bg:             #141414;
    --color-bg-alt:         #1F1F1F;
    --color-bg-layout:      #000000;
    --color-bg-elevated:    #1F1F1F;

    /* ── Bordes — tono oscuro pero visible sobre bg-elevated ─────────── */
    --color-border-subtle:  #303030;
    --color-border:         #424242;
    --color-border-strong:  #595959;
    --color-border-control: #737373;    /* 3:1 sobre #1F1F1F, #141414 y negro */

    /* ── Texto — alpha sobre blanco (patrón AntD dark) ───────────────── */
    --color-text:           rgba(255, 255, 255, 0.88);
    --color-text-muted:     rgba(255, 255, 255, 0.65);
    --color-text-subtle:    rgba(255, 255, 255, 0.45);

    /* ── Sombras — más marcadas sobre fondo oscuro ───────────────────── */
    --shadow-sm:
        0 1px 2px 0 rgba(0, 0, 0, 0.25),
        0 1px 6px -1px rgba(0, 0, 0, 0.20);
    --shadow-md:
        0 6px 16px 0 rgba(0, 0, 0, 0.35),
        0 3px 6px -4px rgba(0, 0, 0, 0.40);
    --shadow-lg:
        0 9px 28px 0 rgba(0, 0, 0, 0.45),
        0 12px 48px 16px rgba(0, 0, 0, 0.30);
    --shadow-xl: var(--shadow-lg);

    --focus-ring: 0 0 0 2px rgba(22, 119, 255, 0.35);

    /* Sidebar — en dark queda aún más oscuro */
    --color-sidebar-bg:         #000000;
    --color-sidebar-bg-darker:  #141414;
    --color-sidebar-border:     rgba(255, 255, 255, 0.06);
    --color-sidebar-hover:      rgba(255, 255, 255, 0.06);
}

/* Fondo del body en dark — tokens.css lo define con var(--color-bg-layout) */

/* Ajustes puntuales donde el valor rgba/alpha no basta y hace falta override */
/* Flecha del select — svg con stroke adaptado al modo */
html[data-theme="dark"] .form__select {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23bbbbbb' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
}

html[data-theme="dark"] .pagination__size select {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23bbbbbb' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
}



/* Lo propio del proyecto derivado — siempre lo último. */

/* ── proyecto.css ───────────────────────────────── */
/* Estilos propios de granjas. La plantilla no toca este archivo. */

/* ── selector-explotacion.css ───────────────────────────────── */
/* ── Selector de explotación (cabecera) ─────────────────────────────────────
   Con qué explotación se está trabajando, y el desplegable para cambiar.
   A diferencia del nombre de usuario, NO se oculta en móvil: saber en qué
   granja se está apuntando importa más cuanto más pequeña es la pantalla. */

.selector-explotacion {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    height: 40px;
    padding: 0 var(--space-sm);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
}

button.selector-explotacion {
    cursor: pointer;
    transition: background-color var(--transition-fast);
}

button.selector-explotacion:hover {
    background-color: var(--color-bg-alt);
}

.selector-explotacion__nombre {
    max-width: 14rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.selector-explotacion__chevron {
    width: 14px;
    height: 14px;
    color: var(--color-text-muted);
}

.selector-explotacion[data-sin-elegir] {
    border-color: var(--color-warning, var(--color-border));
}

/* El desplegable: nombre a la izquierda, especie a la derecha, cada
   explotación en una sola línea. Crece con el nombre más largo, sin
   salirse de la pantalla. */
.selector-explotacion__menu {
    width: max-content;
    max-width: min(24rem, calc(100vw - 2 * var(--space-md)));
}

.selector-explotacion__menu .dropdown__item {
    justify-content: space-between;
    gap: var(--space-md);
}

.selector-explotacion__menu .dropdown__item[aria-current="true"] {
    background-color: var(--color-bg-alt);
    font-weight: var(--font-weight-medium);
}

.selector-explotacion__opcion {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.selector-explotacion__especie {
    flex: none;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
}

@media (max-width: 48rem) {
    .selector-explotacion__nombre {
        max-width: 8rem;
    }
}



/* ── tarjetas-jaula.css ───────────────────────────────── */
/* ── Tarjetas de las jaulas (pages/admin/alojamientos/tarjetas) ───────────────
   En pantalla, una rejilla para revisarlas; al imprimir, una hoja A4 de
   3 × 8 tarjetas de 63,5 × 33,9 mm (el formato de las hojas de etiquetas
   habituales), con una línea fina para recortarlas. El QR (SVG) lo pinta
   tarjetas-qr.js dentro de .tarjeta-jaula__qr. */

.tarjetas-jaula {
    --tarjeta-ancho: 63.5mm;
    --tarjeta-alto: 33.9mm;
    --tarjeta-qr: 27mm;

    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    max-width: 1100px;
    margin: 0 auto;
    padding: var(--space-lg) var(--space-md);
}

.tarjetas-jaula__barra {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
}

.tarjetas-jaula__titulo {
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-semibold);
}

.tarjetas-jaula__hoja {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--tarjeta-ancho), 1fr));
    gap: var(--space-sm);
}

.tarjeta-jaula {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: var(--tarjeta-alto);
    padding: var(--space-sm);
    border: 1px dashed var(--color-border);
    border-radius: var(--radius-md);
    background: #fff;
    color: #000;
    break-inside: avoid;
}

.tarjeta-jaula__qr {
    flex: 0 0 var(--tarjeta-qr);
    width: var(--tarjeta-qr);
    height: var(--tarjeta-qr);
}

.tarjeta-jaula__qr svg {
    display: block;
    width: 100%;
    height: 100%;
}

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

.tarjeta-jaula__codigo {
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-semibold);
    line-height: 1.1;
    overflow-wrap: anywhere;
}

.tarjeta-jaula__dato {
    font-size: var(--font-size-xs);
}

.tarjeta-jaula__clave {
    font-family: ui-monospace, monospace;
    font-size: var(--font-size-xs);
    letter-spacing: 0.04em;
}

@media print {
    @page {
        size: A4;
        margin: 10mm 7mm;
    }

    .tarjetas-jaula__pagina {
        background: #fff;
    }

    .tarjetas-jaula {
        max-width: none;
        padding: 0;
        gap: 0;
    }

    .tarjetas-jaula__barra {
        display: none;
    }

    .tarjetas-jaula__hoja {
        grid-template-columns: repeat(3, var(--tarjeta-ancho));
        grid-auto-rows: var(--tarjeta-alto);
        gap: 0;
    }

    .tarjeta-jaula {
        min-height: 0;
        height: var(--tarjeta-alto);
        border-radius: 0;
        border-color: #bbb;
    }
}



/* ── historia-animal.css ───────────────────────────────── */
/* ── Historia de un animal (ficha, _animal_detalle_modal) ─────────────────────
   Usa el bloque .historial de la plantilla (blocks/casos.css). Aquí solo lo
   propio: un apunte deshecho sigue en la historia, tachado y apagado, para
   que se vea que pasó y que no cuenta. */

.historial__paso[data-anulado="true"] .historial__que,
.historial__paso[data-anulado="true"] .historial__texto {
    color: var(--color-text-muted);
    text-decoration: line-through;
}

.historial__paso[data-anulado="true"] .badge {
    text-decoration: none;
}



/* ── plan-trabajo.css ───────────────────────────────── */
/* ── El plan de trabajo y los apuntes en bloque (pages/admin/plan) ───────────
   En pantalla: la barra del día y la nave, una tarjeta por tarea y el parte
   en bloque con sus campos estrechos. Al imprimir (plan/imprimir): blanco y
   negro, sin botones, una tarea tras otra y una casilla por hembra para
   marcar a mano en la nave. */

.plan-barra {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-md);
}

.plan-barra__filtros {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-md);
}

.plan-barra__filtros .form__field {
    min-inline-size: 10rem;
}

.plan-tarea .card__title {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

/* La casilla para marcar a mano en la hoja impresa. */
.plan-tarea__marca {
    inline-size: 2.5rem;
}

td.plan-tarea__marca::before {
    content: "";
    display: inline-block;
    inline-size: 1rem;
    block-size: 1rem;
    border: 1px solid currentColor;
    border-radius: var(--radius-sm);
}

/* ── La hoja para imprimir ── */

.plan-imprimir {
    max-inline-size: 1100px;
    margin-inline: auto;
    padding: var(--space-lg) var(--space-md);
}

.plan-imprimir__barra {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
}

.plan-imprimir__titulo {
    margin: 0;
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-semibold);
}

/* ── El parte en bloque ── */

.apuntes-bloque {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.apuntes-bloque__numero {
    inline-size: 6rem;
    text-align: end;
}

.apuntes-bloque__eleccion {
    inline-size: 9rem;
}

.apuntes-bloque__codigo {
    inline-size: 10rem;
}

.apuntes-bloque__resultado {
    min-inline-size: 14rem;
}

.apuntes-bloque tr[data-estado="hecho"] {
    background-color: var(--color-success-soft);
}

.apuntes-bloque tr[data-estado="error"] {
    background-color: var(--color-danger-soft);
}

@media print {
    @page {
        size: A4;
        margin: 12mm 10mm;
    }

    .plan-imprimir__pagina {
        background: #fff;
        color: #000;
    }

    .plan-imprimir {
        max-inline-size: none;
        padding: 0;
    }

    .plan-imprimir__boton {
        display: none;
    }

    .plan-imprimir .card {
        border: 0;
        box-shadow: none;
        padding: 0;
        break-inside: auto;
    }

    .plan-imprimir .data-table-wrapper {
        overflow: visible;
        box-shadow: none;
    }

    .plan-imprimir .data-table th,
    .plan-imprimir .data-table td {
        color: #000;
        border-color: #999;
    }

    .plan-imprimir .badge {
        border: 1px solid #000;
        background: none;
        color: #000;
    }
}



/* ── indices.css ───────────────────────────────── */
/* ── El panel de índices (pages/admin/indices) ──────────────────────────────
   La barra del periodo (fechas y atajos) y las tablas de cada sección: el
   nombre del índice con su fórmula debajo, y la referencia con su fuente. */

.indices-periodo {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-md);
}

.indices-periodo__fechas {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-md);
}

.indices-periodo__fechas .form__field {
    min-inline-size: 10rem;
}

.indices-seccion__nombre,
.indices-seccion__formula,
.indices-seccion__fuente {
    display: block;
}

.indices-seccion__formula {
    font-weight: var(--font-weight-regular);
}

.indices-seccion__valor {
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
}

/* Las cuatro tablas, con las mismas columnas: el nombre se lleva lo que
   sobra y parte su fórmula; las cifras y la comparación, fijas. En el móvil,
   la tabla se desplaza en su envoltorio. */
.indices-seccion__tabla {
    table-layout: fixed;
    min-inline-size: 46rem;
}

/* El nombre del índice es la cabecera de su fila: parte (las de la tabla no). */
.indices-seccion__tabla th[scope="row"] {
    white-space: normal;
}

.indices-seccion__col-cifra {
    inline-size: 9rem;
}

.indices-seccion__col-comparacion {
    inline-size: 11rem;
}



/* ── licencia.css ───────────────────────────────── */
/* ── La licencia de una organización ────────────────────────────────────────
   El aviso de la cabecera (partials/_aviso_licencia), el resumen del
   listado de Organizaciones y la barra de uso de la página de licencia. En
   el móvil el aviso de la cabecera se queda en el icono: el texto pasa a
   ser solo para el lector de pantalla. */

.aviso-licencia {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-full);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    line-height: var(--line-height-tight);
    text-decoration: none;
    white-space: nowrap;
}

.aviso-licencia[hidden] { display: none; }

.aviso-licencia[data-variant="warning"] {
    color: var(--color-warning-text);
    background-color: var(--color-warning-soft);
}

.aviso-licencia[data-variant="danger"] {
    color: var(--color-danger-text);
    background-color: var(--color-danger-soft);
}

html:not(.is-touch) a.aviso-licencia:hover {
    text-decoration: underline;
}

@media (max-width: 47.99rem) {
    .aviso-licencia__texto {
        position: absolute;
        inline-size: 1px;
        block-size: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border-width: 0;
    }
}

.licencia-resumen {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
}

.licencia-uso {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
}

.licencia-uso .barra {
    flex: 1 1 12rem;
    --barra-alto: 0.75rem;
}

.licencia-resumen .badge {
    white-space: nowrap;
}

.licencia-reglas {
    margin: 0;
    padding-inline-start: var(--space-lg);
}



/* ── documento-salida.css ───────────────────────────────── */
/* documento-salida.css — El documento de salida a matadero de un lote
   (pages/admin/lotes/traslado.html.twig, docs/cunicultura.md §10): sus dos
   partes, los datos en dos columnas, los huecos para rellenar a mano y las
   cajas de las firmas. La hoja y su impresión en A4 son las del plan
   (.plan-imprimir, plan-trabajo.css). */

.documento-salida__seccion {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-md);
}

.documento-salida__titulo {
    margin: 0 0 var(--space-xs);
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
}

.documento-salida__apartado {
    margin: var(--space-md) 0 var(--space-xs);
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-semibold);
}

.documento-salida__nota,
.documento-salida__declaracion {
    margin: 0 0 var(--space-sm);
    font-size: var(--font-size-sm);
}

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

.documento-salida__declaracion {
    margin-block-start: var(--space-md);
}

.documento-salida__datos {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-xs) var(--space-md);
    margin: 0;
}

.documento-salida__datos dt {
    color: var(--color-text-muted);
}

.documento-salida__datos dd {
    margin: 0 0 var(--space-xs);
}

/* Lo que se rellena a mano: una línea de puntos. */
.documento-salida__hueco {
    display: inline-block;
    min-inline-size: 12rem;
    min-block-size: 1.2em;
    border-block-end: 1px dotted currentColor;
    vertical-align: bottom;
}

.documento-salida__hueco[data-ancho="corto"] {
    min-inline-size: 5rem;
}

.documento-salida__firmas {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-md);
    margin-block-start: var(--space-md);
}

.documento-salida__firma {
    min-block-size: 6rem;
    padding: var(--space-sm);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: var(--font-size-sm);
}

@media (min-width: 40rem) {
    .documento-salida__datos {
        grid-template-columns: minmax(10rem, 16rem) 1fr;
    }

    .documento-salida__datos dd {
        margin-block-end: 0;
    }

    .documento-salida__firmas {
        grid-template-columns: 1fr 1fr;
    }
}

@media print {
    .documento-salida__seccion {
        break-inside: avoid;
        border-color: #000;
    }

    .documento-salida__nota,
    .documento-salida__datos dt {
        color: #000;
    }

    .documento-salida__firma {
        border-color: #000;
    }

    .documento-salida__datos {
        grid-template-columns: 16rem 1fr;
    }

    .documento-salida__firmas {
        grid-template-columns: 1fr 1fr;
    }
}



/* ── libro-registro.css ───────────────────────────────── */
/* libro-registro.css — El libro de registro de la explotación
   (pages/admin/libro, docs/cunicultura.md §10): los selectores del año y
   de la forma, la apertura y el cierre del movimiento pecuario destacados,
   las filas anuladas tachadas y, al imprimir, las hojas anchas (el
   movimiento pecuario, la CU1, las visitas, los tratamientos) en hoja
   apaisada con la letra menor. La hoja es la del plan (.plan-imprimir). */

.libro-selector {
    inline-size: auto;
}

.libro-movimientos tr[data-fila] {
    background-color: var(--color-bg-alt);
    font-weight: var(--font-weight-semibold);
}

.libro-tabla tr[data-anulada] td {
    color: var(--color-text-muted);
    text-decoration: line-through;
}

@page apaisada {
    size: A4 landscape;
    margin: 10mm;
}

@media print {
    .libro-imprimir__hoja[data-apaisada] {
        page: apaisada;
        break-before: page;
    }

    /* Que quepan todas las columnas: celdas estrechas y cabeceras que parten. */
    .libro-movimientos,
    .libro-tabla {
        inline-size: 100%;
        font-size: var(--font-size-xs);
    }

    .libro-movimientos th,
    .libro-movimientos td,
    .libro-tabla th,
    .libro-tabla td {
        padding: var(--space-xs);
        font-size: var(--font-size-xs);
        white-space: normal;
    }

    .libro-movimientos tr[data-fila] {
        background-color: transparent;
    }
}




