/* =====================================================================
   Servia Lite — Sistema de diseño de la app del taller
   =====================================================================

   Hoja de la app que usa el CLIENTE. El panel super-admin tiene la suya
   aparte (`panel.css`) y no comparte nada con esta.

   Diferencia esencial con el panel: esta app es WHITE-LABEL. El acento no
   se decide aquí — lo inyecta `marcaCss()` (lib/marca.php) desde el color
   que cada taller guardó en `ajustes`, ya derivado en toda su familia
   (`--l-acento`, `-suave`, `-borde`, `-texto`, `-sobre`) y corregido por
   contraste para cada tema. Por eso en esta hoja NUNCA se escribe un color
   de marca literal: había siete azules de Bootstrap incrustados y un
   cliente con marca roja seguía viendo halos, degradados y un login azules.

   Principios (los mismos del resto del producto):
   - La jerarquía se marca con tamaño y peso de texto, no con sombras.
     La elevación se reserva para lo que de verdad flota: menús, modales,
     la barra lateral cuando se abre encima del contenido en móvil.
   - Superficies separadas por un borde de 1px, no por un halo gris.
   - Nunca un color literal en un componente: todo sale de un token.

   Los tokens se declaran dos veces —claro y oscuro— y además se reasignan
   las variables de Bootstrap, de modo que sus componentes de serie
   (modales, dropdowns, formularios, tablas) hereden este diseño sin tener
   que reescribirlos vista por vista. Antes el tema oscuro se parcheaba con
   ~120 líneas de `[data-bs-theme="dark"] .loquesea { … }` a base de colores
   escritos a mano; todo eso lo sustituye la tabla de tokens de abajo.

   Nota sobre `@layer`: sería lo natural para ganarle a Bootstrap sin
   especificidad, pero los estilos SIN capa siempre ganan a los que están
   EN una capa. Como Bootstrap se carga sin capa, meter esta hoja en un
   `@layer` la dejaría por debajo. Se mantiene el orden por carga, igual
   que hace `panel.css`.
   ===================================================================== */

/* ---------------------------------------------------------------------
   0. Tipografía autoalojada
   Sin CDN, igual que Bootstrap: si jsDelivr fallaba, la interfaz se veía
   rota — mal asunto en una PWA pensada para conexiones malas.
   El navegador sólo descarga `latin-ext` (85 KB) si la página realmente
   usa un glifo de ese rango; en español se queda en los 48 KB de `latin`.
   --------------------------------------------------------------------- */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('../vendor/inter/inter-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+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: 100 900;
    font-display: swap;
    src: url('../vendor/inter/inter-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;
}

/* ---------------------------------------------------------------------
   1. Tokens — tema claro
   --------------------------------------------------------------------- */
:root {
    /* Superficies, de atrás hacia adelante */
    --l-fondo:        #f6f7f9;  /* lienzo de la aplicación */
    --l-superficie:   #ffffff;  /* tarjetas, tablas, formularios */
    --l-superficie-2: #f1f3f6;  /* cabeceras de tabla, campos, estados vacíos */
    --l-flotante:     #ffffff;  /* menús y modales */

    /* Bordes: el hairline es la unidad estructural de esta interfaz */
    --l-borde:        #e4e7ec;
    --l-borde-fuerte: #d2d7df;

    /* Texto */
    --l-texto:        #15181d;
    --l-texto-2:      #636b79;  /* etiquetas, metadatos */
    --l-texto-3:      #8a919d;  /* deshabilitado, marcas de agua */

    /* Acento — lo REESCRIBE marcaCss() con el color del taller.
       Estos valores son sólo el azul por defecto, para que la hoja
       funcione sola donde no se inyecte la marca (impresión, errores). */
    --l-acento:        #2563eb;
    --l-acento-fuerte: #1f54c5;
    --l-acento-suave:  #eef3fd;
    --l-acento-borde:  #c8d8f8;
    --l-acento-texto:  #2563eb;
    --l-acento-sobre:  #ffffff;  /* tinta que se lee ENCIMA del acento */

    /* Semáforo de estado. `-suave` es el relleno de las píldoras y
       `-texto` su tinta: pareja verificada para contraste AA.
       Estos NO dependen de la marca: "pagado" es verde en todos los
       talleres, aunque el taller sea rojo. */
    --l-ok:            #16a34a;
    --l-ok-suave:      #e7f6ec;
    --l-ok-texto:      #14682f;

    --l-info:          #0284c7;
    --l-info-suave:    #e2f2fb;
    --l-info-texto:    #075e87;

    --l-aviso:         #d97706;
    --l-aviso-suave:   #fdf1de;
    --l-aviso-texto:   #92590a;

    --l-peligro:       #dc2626;
    --l-peligro-suave: #fdeaea;
    --l-peligro-texto: #a51b1b;

    --l-neutro:        #6b7280;
    --l-neutro-suave:  #eef0f3;
    --l-neutro-texto:  #4b5563;

    /* Barra lateral — superficie oscura propia en ambos temas */
    --l-nav-fondo:        #171c24;
    --l-nav-texto:        #b9c0cc;
    --l-nav-texto-activo: #ffffff;
    --l-nav-hover:        rgba(255,255,255,.07);
    --l-nav-borde:        rgba(255,255,255,.08);
    --l-nav-seccion:      rgba(255,255,255,.38);

    /* Geometría */
    --l-radio-sm:   6px;
    --l-radio:      10px;
    --l-radio-lg:   14px;
    --l-radio-full: 999px;

    /* Elevación — sólo para lo que flota de verdad */
    --l-sombra-sm: 0 1px 2px rgba(16,24,40,.05);
    --l-sombra:    0 6px 24px -6px rgba(16,24,40,.12), 0 2px 6px -2px rgba(16,24,40,.06);
    --l-sombra-lg: 0 24px 48px -12px rgba(16,24,40,.22);

    /* Tipografía */
    --l-texto-xs:   .75rem;
    --l-texto-sm:   .8125rem;
    --l-texto-md:   .875rem;
    --l-texto-base: .9375rem;
    --l-texto-lg:   1.0625rem;
    --l-texto-xl:   1.25rem;
    --l-cifra:      1.6rem;

    /* Estructura */
    --l-nav-ancho:     248px;
    --l-nav-ancho-min: 68px;
    --l-top-alto:      56px;
    --l-transicion:    .16s ease;

    /* ----- Alias heredados -----
       Nombres que ya usan `pwa.css` y algunas vistas. Se conservan
       apuntando al token nuevo para no romper nada mientras se migran. */
    --sidebar-width: var(--l-nav-ancho);
    --sidebar-bg: var(--l-nav-fondo);
    --sidebar-text: var(--l-nav-texto);
    --sidebar-text-active: var(--l-nav-texto-activo);
    --sidebar-hover: var(--l-nav-hover);
    --topbar-bg: var(--l-superficie);
    --topbar-border: var(--l-borde);
    --content-bg: var(--l-fondo);
    --transition: var(--l-transicion);
    --radio-sm: var(--l-radio-sm);
    --radio-md: var(--l-radio);
    --radio-lg: var(--l-radio-lg);
    --sombra-sm: var(--l-sombra-sm);
    --sombra-md: var(--l-sombra);
    --sombra-marca: var(--l-sombra);
    --texto-xs: var(--l-texto-xs);
    --texto-sm: var(--l-texto-sm);
    --texto-base: var(--l-texto-base);
    --texto-lg: var(--l-texto-lg);
    --texto-xl: var(--l-texto-xl);

    /* Fallback de marca. `marcaCss()` los sobrescribe con los del taller. */
    --brand: var(--l-acento);
    --brand-dark: var(--l-acento-fuerte);
    --brand-rgb: 37,99,235;
}

/* ---------------------------------------------------------------------
   2. Tokens — tema oscuro
   --------------------------------------------------------------------- */
[data-bs-theme="dark"] {
    --l-fondo:        #0d1117;
    --l-superficie:   #12161d;
    --l-superficie-2: #1a1f28;
    --l-flotante:     #1a1f28;

    --l-borde:        #262d39;
    --l-borde-fuerte: #354052;

    --l-texto:        #e7eaf0;
    --l-texto-2:      #939cad;
    --l-texto-3:      #6a7383;

    --l-ok:            #34d17a;
    --l-ok-suave:      #12291d;
    --l-ok-texto:      #6ee7a4;

    --l-info:          #38bdf8;
    --l-info-suave:    #102836;
    --l-info-texto:    #7dd3fc;

    --l-aviso:         #fbbf24;
    --l-aviso-suave:   #2c2210;
    --l-aviso-texto:   #fcd34d;

    --l-peligro:       #f87171;
    --l-peligro-suave: #301618;
    --l-peligro-texto: #fca5a5;

    --l-neutro:        #94a3b8;
    --l-neutro-suave:  #212836;
    --l-neutro-texto:  #a9b4c4;

    /* En oscuro la barra lateral se funde con el lienzo: separarla con un
       borde en vez de con un salto de color, que ahí resulta sucio. */
    --l-nav-fondo:    #0a0e14;
    --l-nav-texto:    #9aa3b2;
    --l-nav-hover:    rgba(255,255,255,.06);

    --l-sombra-sm: 0 1px 2px rgba(0,0,0,.4);
    --l-sombra:    0 6px 24px -6px rgba(0,0,0,.5), 0 2px 6px -2px rgba(0,0,0,.4);
    --l-sombra-lg: 0 24px 48px -12px rgba(0,0,0,.6);
}

/* ---------------------------------------------------------------------
   3. Puente con Bootstrap
   Reasignar sus variables es lo que hace que modales, dropdowns, campos
   y tablas de serie salgan ya con este diseño en ambos temas, sin tocar
   las vistas. Se listan los tres selectores porque Bootstrap 5.3 declara
   sus valores tanto en `:root` como en cada `[data-bs-theme]`.
   --------------------------------------------------------------------- */
:root,
[data-bs-theme="light"],
[data-bs-theme="dark"] {
    --bs-body-bg: var(--l-superficie);
    --bs-body-color: var(--l-texto);
    --bs-secondary-bg: var(--l-superficie-2);
    --bs-secondary-color: var(--l-texto-2);
    --bs-tertiary-bg: var(--l-superficie-2);
    --bs-tertiary-color: var(--l-texto-3);
    --bs-emphasis-color: var(--l-texto);
    --bs-heading-color: var(--l-texto);
    --bs-border-color: var(--l-borde);
    --bs-border-color-translucent: var(--l-borde);

    --bs-link-color: var(--l-acento-texto);
    --bs-link-hover-color: var(--l-acento-fuerte);
    --bs-link-color-rgb: var(--brand-rgb);

    --bs-primary: var(--l-acento);
    --bs-primary-rgb: var(--brand-rgb);
    --bs-primary-text-emphasis: var(--l-acento-texto);
    --bs-primary-bg-subtle: var(--l-acento-suave);
    --bs-primary-border-subtle: var(--l-acento-borde);

    --bs-border-radius: var(--l-radio);
    --bs-border-radius-sm: var(--l-radio-sm);
    --bs-border-radius-lg: var(--l-radio-lg);
    --bs-border-radius-pill: var(--l-radio-full);

    --bs-dropdown-bg: var(--l-flotante);
    --bs-dropdown-border-color: var(--l-borde);
    --bs-dropdown-link-color: var(--l-texto);
    --bs-dropdown-link-hover-bg: var(--l-superficie-2);
    --bs-dropdown-link-hover-color: var(--l-texto);
    --bs-dropdown-link-active-bg: var(--l-acento-suave);
    --bs-dropdown-link-active-color: var(--l-acento-texto);
    --bs-dropdown-divider-bg: var(--l-borde);
    --bs-dropdown-header-color: var(--l-texto-2);

    --bs-modal-bg: var(--l-flotante);
    --bs-modal-border-color: var(--l-borde);
    --bs-modal-header-border-color: var(--l-borde);
    --bs-modal-footer-border-color: var(--l-borde);

    --bs-card-bg: var(--l-superficie);
    --bs-card-border-color: var(--l-borde);
    --bs-card-cap-bg: transparent;

    --bs-table-bg: transparent;
    --bs-table-color: var(--l-texto);
    --bs-table-border-color: var(--l-borde);
    --bs-table-hover-bg: var(--l-superficie-2);
    --bs-table-hover-color: var(--l-texto);
    --bs-table-striped-bg: var(--l-superficie-2);

    --bs-list-group-bg: var(--l-superficie);
    --bs-list-group-border-color: var(--l-borde);
    --bs-list-group-color: var(--l-texto);
    --bs-list-group-action-hover-bg: var(--l-superficie-2);

    --bs-tooltip-bg: var(--l-texto);
    --bs-popover-bg: var(--l-flotante);
    --bs-popover-border-color: var(--l-borde);

    --bs-code-color: var(--l-acento-texto);
}

/* ---------------------------------------------------------------------
   4. Base
   --------------------------------------------------------------------- */
html, body { height: 100%; }

body {
    background: var(--l-fondo);
    color: var(--l-texto);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
                 'Helvetica Neue', Arial, sans-serif;
    font-size: var(--l-texto-base);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Inter trae los ajustes ópticos apagados por defecto; `cv11` da la "a" de
   una sola planta y `ss01`, comillas y puntuación mejor equilibradas. */
@supports (font-variation-settings: normal) {
    body { font-feature-settings: 'cv11', 'ss01'; }
}

h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
    color: var(--l-texto);
    font-weight: 650;
    letter-spacing: -.011em;
}

/* Cifras y columnas numéricas: ancho de dígito fijo, para que las cantidades
   se alineen en vertical y no “bailen” al actualizarse. Es lo que separa una
   tabla de dinero que se lee de una que hay que descifrar. */
.cifra, .tabular, td.num, th.num,
.kpi-value, .pagination {
    font-variant-numeric: tabular-nums;
}

/* Foco visible y coherente en toda la app. `:focus-visible` deja fuera el
   clic con ratón, así que no molesta a quien no navega con teclado. */
:focus-visible {
    outline: 2px solid var(--l-acento);
    outline-offset: 2px;
    border-radius: var(--l-radio-sm);
}
.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible {
    outline: 2px solid var(--l-acento);
    outline-offset: 1px;
}

/* Quien pide menos movimiento, lo obtiene: sin transiciones ni animaciones. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
        scroll-behavior: auto !important;
    }
}

/* ---------------------------------------------------------------------
   5. Armazón: lateral + barra superior + contenido
   --------------------------------------------------------------------- */
.layout {
    display: flex;
    min-height: 100vh;
}

.main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    margin-left: var(--l-nav-ancho);
    transition: margin-left var(--l-transicion);
}

@media (max-width: 991.98px) {
    .main { margin-left: 0; }
}

/* ----- Barra lateral ----- */
/* El que desplaza es la LISTA, no la barra entera.
   Con `overflow-y` en la barra, un menú que no cabe —un portátil de 720px, o
   un móvil— empujaba el pie por debajo del recorte: el número de versión sólo
   se veía desplazando el menú, y quien no lo intentaba daba por hecho que la
   barra de tareas se lo comía. Ahora la cabecera y el pie son fijos y el hueco
   de en medio es lo único que se mueve, así que el pie no puede desaparecer
   por largo que sea el menú.
   La altura va en `dvh` (con `vh` de respaldo para WebViews viejos) porque en
   móvil la barra del navegador se superpone al viewport de layout: con
   `bottom: 0` el pie quedaba justo debajo de ella. */
.sidebar {
    position: fixed;
    top: 0; left: 0;
    width: var(--l-nav-ancho);
    height: 100vh;
    height: 100dvh;
    background: var(--l-nav-fondo);
    color: var(--l-nav-texto);
    display: flex;
    flex-direction: column;
    z-index: 1040;
    transition: transform var(--l-transicion);
    overflow: hidden;
}

@media (max-width: 991.98px) {
    .sidebar { transform: translateX(-100%); box-shadow: var(--l-sombra-lg); }
    .sidebar.show { transform: translateX(0); }
}

.brand-link { color: #fff; font-weight: 600; }
.brand-link:hover { color: #fff; }
.brand-logo {
    width: 34px; height: 34px;
    background: var(--l-acento);
    color: var(--l-acento-sobre);
    border-radius: var(--l-radio-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
/* Logo del cliente: imagen limpia, sin recuadro ni fondo.
   Compacto a propósito: ahora comparte sitio con el nombre del taller, así
   que funciona como distintivo, no como cartel. Un logo apaisado se ajusta
   solo con `contain` sin deformarse. */
.brand-logo-img {
    height: 32px;
    max-width: 48px;
    object-fit: contain;
    display: block;
    flex-shrink: 0;
}

/* `min-height: 0` no es decorativo: sin él un hijo flex no baja de su tamaño
   de contenido y la lista, en vez de desplazarse, estira la barra y vuelve a
   empujar el pie fuera — el fallo que esto viene a arreglar. */
.sidebar-nav {
    flex: 1 1 auto;
    min-height: 0;
    padding: .5rem 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Fina y del color del lateral: la del sistema sale clara y ancha sobre
       un menú oscuro y se lee como un defecto. No se oculta del todo —al
       revés que en `.l-rail`— porque aquí sí importa saber que hay más menú
       debajo. Firefox usa `scrollbar-*`; WebKit, el pseudoelemento. */
    scrollbar-width: thin;
    scrollbar-color: var(--l-nav-borde) transparent;
}
.sidebar-nav::-webkit-scrollbar { width: 6px; }
.sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav::-webkit-scrollbar-thumb {
    background: var(--l-nav-borde);
    border-radius: 3px;
}
.nav-section {
    padding: 1rem .9rem .3rem;
    font-size: var(--l-texto-xs);
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--l-nav-seccion);
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .4rem;
}

/* ----- Grupos plegables del menú -----
   Operación y Administración se pliegan; Dashboard y Ayuda no, porque son una
   sola entrada y plegar algo para esconder una línea no ahorra nada.
   El estado vive en `localStorage['servia-nav-grupos']` y lo aplica el script
   del `<head>` ANTES del primer pintado (igual que el tema y el ancho del
   menú): si esperara a `app.js`, el menú se pintaría abierto y se cerraría a
   la vista. */
button.nav-section {
    width: 100%;
    background: none;
    border: 0;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: color var(--l-transicion);
}
button.nav-section:hover { color: var(--l-nav-texto-activo); }
button.nav-section:focus-visible {
    outline: 2px solid var(--l-acento);
    outline-offset: -2px;
    border-radius: var(--l-radio-sm);
}
.nav-section-chevron {
    font-size: .7rem;
    flex-shrink: 0;
    transition: transform var(--l-transicion);
}
.nav-grupo { margin: 0; padding: 0; list-style: none; }

/* Un grupo cerrado se oculta, salvo que contenga la página actual: plegar
   Administración y navegar luego a Ajustes no puede dejar la entrada activa
   escondida. Ese `data-activo` lo pone el servidor, que es quien sabe dónde
   estás.
   Hay una regla por grupo porque CSS no puede comparar el valor de dos
   atributos. Al añadir un grupo plegable nuevo hay que añadir la suya —
   `test_phase26` (3) falla si una de las dos listas se queda coja, que si no
   sería un botón que no hace nada y una página que carga igual. */
html[data-nav-cerrado~="operacion"] .nav-grupo[data-grupo="operacion"]:not([data-activo]) { display: none; }
html[data-nav-cerrado~="administracion"] .nav-grupo[data-grupo="administracion"]:not([data-activo]) { display: none; }
html[data-nav-cerrado~="operacion"] [data-plegar-seccion="operacion"] .nav-section-chevron,
html[data-nav-cerrado~="administracion"] [data-plegar-seccion="administracion"] .nav-section-chevron {
    transform: rotate(-90deg);
}
.sidebar .nav-link {
    color: var(--l-nav-texto);
    padding: .55rem .8rem;
    margin: .1rem .6rem;
    border-radius: var(--l-radio-sm);
    display: flex;
    align-items: center;
    gap: .7rem;
    font-size: var(--l-texto-md);
    font-weight: 500;
    transition: background var(--l-transicion), color var(--l-transicion);
}
.sidebar .nav-link:hover {
    background: var(--l-nav-hover);
    color: var(--l-nav-texto-activo);
}
/* El item activo se marca con el color de marca plano, no con un degradado
   con sombra: el degradado envejece y la sombra hacía flotar algo que no
   flota. La tinta la decide `--l-acento-sobre`, ya validada por contraste. */
.sidebar .nav-link.active {
    background: var(--l-acento);
    color: var(--l-acento-sobre);
    font-weight: 600;
}
.sidebar .nav-link.disabled {
    opacity: .45;
    cursor: not-allowed;
}
.sidebar .nav-link i { font-size: 1.05rem; width: 1.2em; text-align: center; flex-shrink: 0; }

.sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,.45);
    z-index: 1039;
}
.sidebar-backdrop.show { display: block; }

/* ----- Botones de la cabecera del lateral ----- */
.sidebar-brand {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .85rem .9rem;
    border-bottom: 1px solid var(--l-nav-borde);
    min-height: var(--l-top-alto);
}
.brand-link {
    display: flex;
    align-items: center;
    gap: .6rem;
    text-decoration: none;
    min-width: 0;
}
.brand-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.sidebar-btn {
    flex-shrink: 0;
    width: 30px; height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--l-radio-sm);
    background: transparent;
    color: var(--l-nav-texto);
    font-size: .85rem;
    cursor: pointer;
    transition: background var(--l-transicion), color var(--l-transicion);
}
.sidebar-btn:hover { background: var(--l-nav-hover); color: var(--l-nav-texto-activo); }

/* `flex-shrink: 0` para que la lista no lo comprima hasta hacerlo ilegible, y
   el área segura sumada al acolchado inferior: en un móvil con barra de
   gestos, `bottom: 0` deja el texto justo debajo de ella. */
.sidebar-footer {
    flex-shrink: 0;
    border-top: 1px solid var(--l-nav-borde);
    padding: .6rem .9rem calc(.6rem + env(safe-area-inset-bottom, 0px));
    color: var(--l-nav-seccion);
    font-size: var(--l-texto-xs);
    white-space: nowrap;
    overflow: hidden;
}

/* Hay una versión nueva instalada y esperando (lo marca `app.js`). El aviso
   de verdad es la banda del contenido; esto es la confirmación tranquila de
   que el número que se lee al lado no es todavía el que se está usando. */
.sidebar-version--nueva { color: var(--l-nav-texto-activo); }
.sidebar-version--nueva::after {
    content: "";
    display: inline-block;
    width: .45rem;
    height: .45rem;
    margin-left: .35rem;
    border-radius: 50%;
    background: var(--l-info);
    vertical-align: middle;
}

/* ----- Barra lateral plegada a iconos (escritorio) -----
   Un taller con el portátil en el mostrador agradece los ~180px extra de
   ancho para la tabla de órdenes. La preferencia se recuerda. */
@media (min-width: 992px) {
    .nav-min .sidebar { width: var(--l-nav-ancho-min); }
    .nav-min .main { margin-left: var(--l-nav-ancho-min); }

    .nav-min .sidebar-brand { justify-content: center; padding-inline: .5rem; }
    .nav-min .brand-text,
    .nav-min .sidebar-version,
    .nav-min .nav-section span { display: none; }
    .nav-min .brand-logo-img { max-width: 40px; }

    /* El encabezado de sección se convierte en un separador: sin él, los
       grupos se funden en una lista larga e indistinguible. */
    .nav-min .nav-section {
        padding: .6rem .9rem .3rem;
        margin-top: .3rem;
        border-top: 1px solid var(--l-nav-borde);
    }

    .nav-min .sidebar .nav-link {
        justify-content: center;
        padding-inline: 0;
        margin-inline: .6rem;
    }
    .nav-min .sidebar .nav-link span { display: none; }
    .nav-min .sidebar-footer { text-align: center; padding-inline: .4rem; }

    /* Plegada a iconos no hay etiquetas, así que un grupo cerrado sería un
       hueco mudo: no se vería qué falta ni cómo recuperarlo. Se fuerzan
       abiertos y el chevron desaparece — el gesto de plegar ya lo cubre el
       botón de la cabecera. `!important` porque las reglas de arriba llevan
       el atributo de <html> y ganan por especificidad.
       Ojo: sólo dentro de esta media query. La clase `.nav-min` sigue puesta
       en móvil (es una preferencia de escritorio recordada), y fuera de aquí
       le quitaría el plegado por categorías a quien más lo necesita. */
    .nav-min .nav-grupo { display: block !important; }
    .nav-min .nav-section-chevron { display: none; }
    .nav-min button.nav-section { cursor: default; }

    /* Globo con la etiqueta al pasar el ratón: sin esto, plegar la barra
       obliga a adivinar qué significa cada icono. */
    .nav-min .sidebar .nav-link::after {
        content: attr(data-etq);
        position: absolute;
        left: calc(100% + 8px);
        top: 50%;
        transform: translateY(-50%);
        background: var(--l-flotante);
        color: var(--l-texto);
        border: 1px solid var(--l-borde);
        box-shadow: var(--l-sombra);
        border-radius: var(--l-radio-sm);
        padding: .3rem .55rem;
        font-size: var(--l-texto-sm);
        white-space: nowrap;
        opacity: 0;
        pointer-events: none;
        transition: opacity var(--l-transicion);
        z-index: 1050;
    }
    .nav-min .sidebar .nav-link { position: relative; }
    .nav-min .sidebar .nav-link:hover::after,
    .nav-min .sidebar .nav-link:focus-visible::after { opacity: 1; }
}

/* ----- Barra superior ----- */
.topbar {
    background: var(--l-superficie);
    border-bottom: 1px solid var(--l-borde);
    position: sticky;
    top: 0;
    z-index: 100;
    min-height: var(--l-top-alto);
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .5rem .9rem;
}
/* `flex-shrink: 0` aquí era un error: con un título largo ("Manual de
   usuario") a 320px empujaba los botones de la derecha fuera de la pantalla
   —35px de desbordamiento medidos— y la elipsis nunca entraba en juego. El
   título es lo que cede; los iconos de acción, no. */
.topbar-title {
    font-weight: 600;
    font-size: var(--l-texto-lg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    flex: 0 1 auto;
}
.topbar-acciones {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin-left: auto;
    flex-shrink: 0;
}

/* Botón de icono: la unidad de la barra superior. Discreto en reposo —
   una fila de botones con borde competía con el contenido. */
.l-icono {
    width: 36px; height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--l-radio-sm);
    background: transparent;
    color: var(--l-texto-2);
    font-size: 1.05rem;
    cursor: pointer;
    transition: background var(--l-transicion), color var(--l-transicion);
}
.l-icono:hover { background: var(--l-superficie-2); color: var(--l-texto); }
.l-icono.show { background: var(--l-superficie-2); color: var(--l-texto); }

/* Contador de notificaciones: punto rojo con la cifra. */
.l-punto-notif {
    position: absolute;
    top: 3px; right: 3px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: var(--l-radio-full);
    background: var(--l-peligro);
    color: #fff;
    font-size: .65rem;
    font-weight: 700;
    line-height: 17px;
    text-align: center;
    border: 2px solid var(--l-superficie);
    box-sizing: content-box;
}
.l-notif-menu { min-width: 320px; max-width: 360px; }
.notif-list { max-height: 360px; overflow-y: auto; }

/* ----- Buscador global ----- */
.l-buscar {
    position: relative;
    align-items: center;
    flex: 1 1 auto;
    max-width: 420px;
    margin-left: .5rem;
}
.l-buscar-icono {
    position: absolute;
    left: .65rem;
    color: var(--l-texto-3);
    font-size: .9rem;
    pointer-events: none;
}
.l-buscar-campo {
    width: 100%;
    background: var(--l-superficie-2);
    border: 1px solid transparent;
    border-radius: var(--l-radio-full);
    padding: .42rem 2.2rem .42rem 2.1rem;
    font-size: var(--l-texto-md);
    color: var(--l-texto);
    transition: background var(--l-transicion), border-color var(--l-transicion);
}
.l-buscar-campo::placeholder { color: var(--l-texto-3); }
.l-buscar-campo:focus {
    outline: none;
    background: var(--l-superficie);
    border-color: var(--l-acento);
    box-shadow: 0 0 0 3px var(--l-acento-suave);
}
/* La tecla del atajo estorba en cuanto se empieza a escribir. */
.l-buscar-tecla {
    position: absolute;
    right: .55rem;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: 5px;
    color: var(--l-texto-3);
    font-size: .68rem;
    font-family: inherit;
    padding: 1px 6px;
    pointer-events: none;
    transition: opacity var(--l-transicion);
}
.l-buscar-campo:focus ~ .l-buscar-tecla { opacity: 0; }

/* ----- Botón de cuenta ----- */
.l-usuario {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    border: 0;
    background: transparent;
    border-radius: var(--l-radio-full);
    padding: .25rem .5rem .25rem .25rem;
    color: var(--l-texto);
    font-size: var(--l-texto-md);
    font-weight: 500;
    cursor: pointer;
    transition: background var(--l-transicion);
}
.l-usuario:hover, .l-usuario.show { background: var(--l-superficie-2); }
.l-avatar {
    width: 30px; height: 30px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--l-acento);
    color: var(--l-acento-sobre);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--l-texto-sm);
    font-weight: 700;
}
.l-usuario-flecha { font-size: .7rem; color: var(--l-texto-3); }

.content { flex: 1; }

/* ---------------------------------------------------------------------
   5b. Avisos flotantes (mensajes flash)
   Antes eran una banda sobre el contenido que empujaba la página hacia
   abajo justo después de guardar algo. Ahora flotan en una esquina y se
   van solos — salvo los errores, que esperan a que los cierres.
   --------------------------------------------------------------------- */
.l-avisos {
    position: fixed;
    top: calc(var(--l-top-alto) + .75rem);
    right: 1rem;
    z-index: 1090;
    display: flex;
    flex-direction: column;
    gap: .5rem;
    max-width: min(380px, calc(100vw - 2rem));
    pointer-events: none;
}
.l-aviso {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    background: var(--l-flotante);
    border: 1px solid var(--l-borde);
    border-left: 3px solid var(--l-neutro);
    border-radius: var(--l-radio);
    box-shadow: var(--l-sombra);
    padding: .7rem .8rem;
    font-size: var(--l-texto-md);
    color: var(--l-texto);
    pointer-events: auto;
    opacity: 0;
    transform: translateX(12px);
    transition: opacity .2s ease, transform .2s ease;
}
.l-aviso--visible { opacity: 1; transform: translateX(0); }
.l-aviso--saliendo { opacity: 0; transform: translateX(12px); }
.l-aviso-icono { font-size: 1rem; line-height: 1.35; flex-shrink: 0; }
.l-aviso-texto { flex: 1 1 auto; min-width: 0; line-height: 1.35; }
.l-aviso-cerrar {
    flex-shrink: 0;
    border: 0;
    background: transparent;
    color: var(--l-texto-3);
    font-size: .75rem;
    padding: .1rem .2rem;
    cursor: pointer;
    border-radius: var(--l-radio-sm);
}
.l-aviso-cerrar:hover { color: var(--l-texto); background: var(--l-superficie-2); }

.l-aviso--ok      { border-left-color: var(--l-ok); }
.l-aviso--ok      .l-aviso-icono { color: var(--l-ok); }
.l-aviso--info    { border-left-color: var(--l-info); }
.l-aviso--info    .l-aviso-icono { color: var(--l-info); }
.l-aviso--aviso   { border-left-color: var(--l-aviso); }
.l-aviso--aviso   .l-aviso-icono { color: var(--l-aviso); }
.l-aviso--peligro { border-left-color: var(--l-peligro); }
.l-aviso--peligro .l-aviso-icono { color: var(--l-peligro); }

/* En móvil ocupan el ancho y aparecen abajo, lejos del pulgar que acaba
   de pulsar "Guardar" pero dentro del campo de visión. */
@media (max-width: 575.98px) {
    .l-avisos {
        top: auto;
        bottom: calc(var(--pwa-tabbar-h, 0px) + 1rem);
        left: .75rem;
        right: .75rem;
        max-width: none;
    }
}

/* ---------------------------------------------------------------------
   5c. Banda de aviso (en el flujo, no flotante)
   Para lo que NO puede desaparecer solo a los cinco segundos: la
   suscripción por vencer, el modo soporte. Un aviso flotante sirve para
   confirmar algo que acabas de hacer; esto es una condición que sigue ahí
   aunque cambies de página, así que ocupa sitio en el contenido.
   --------------------------------------------------------------------- */
.l-banda {
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    padding: .75rem 1rem;
    margin-bottom: 1rem;
    border: 1px solid var(--l-borde);
    border-left: 3px solid var(--l-neutro);
    border-radius: var(--l-radio);
    background: var(--l-superficie);
    font-size: var(--l-texto-md);
    color: var(--l-texto);
}
.l-banda-icono { flex-shrink: 0; font-size: 1.1rem; line-height: 1.4; }
/* Base 0, no `auto`: con `flex-wrap` activo (móvil), una base automática hace
   que el texto se lleve a la línea siguiente entera y el icono se quede solo
   arriba. Con base 0 el cuerpo encoge y se queda al lado del icono. */
.l-banda-cuerpo { flex: 1 1 0; min-width: 0; }
.l-banda-tit { font-weight: 600; line-height: 1.35; }
.l-banda-txt { color: var(--l-texto-2); line-height: 1.4; }
.l-banda-acciones { flex-shrink: 0; display: flex; align-items: center; gap: .5rem; }
.l-banda-cerrar {
    flex-shrink: 0;
    border: 0;
    background: transparent;
    color: var(--l-texto-3);
    font-size: .75rem;
    padding: .2rem .3rem;
    cursor: pointer;
    border-radius: var(--l-radio-sm);
}
.l-banda-cerrar:hover { color: var(--l-texto); background: var(--l-superficie-2); }

.l-banda--aviso   { background: var(--l-aviso-suave);   border-color: var(--l-aviso);   border-left-color: var(--l-aviso); }
.l-banda--aviso   .l-banda-icono, .l-banda--aviso   .l-banda-tit { color: var(--l-aviso-texto); }
.l-banda--aviso   .l-banda-txt  { color: var(--l-aviso-texto); }
.l-banda--peligro { background: var(--l-peligro-suave); border-color: var(--l-peligro); border-left-color: var(--l-peligro); }
.l-banda--peligro .l-banda-icono, .l-banda--peligro .l-banda-tit { color: var(--l-peligro-texto); }
.l-banda--peligro .l-banda-txt  { color: var(--l-peligro-texto); }
.l-banda--info    { background: var(--l-info-suave);    border-color: var(--l-info);    border-left-color: var(--l-info); }
.l-banda--info    .l-banda-icono, .l-banda--info    .l-banda-tit { color: var(--l-info-texto); }
.l-banda--info    .l-banda-txt  { color: var(--l-info-texto); }

@media (max-width: 575.98px) {
    /* A 320px un texto largo más un botón al lado no caben en una línea. */
    .l-banda { flex-wrap: wrap; }
    .l-banda-acciones { width: 100%; padding-left: 1.85rem; }
}

/* ---------------------------------------------------------------------
   6. Componentes de la app
   --------------------------------------------------------------------- */

/* ----- Cabecera de bienvenida -----
   Era un bloque con degradado a todo color que competía con los datos por la
   atención. Se queda como encabezado sobrio: el color de marca aparece en una
   línea de acento, no ocupando un tercio de la pantalla. */
.welcome-card {
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-left: 3px solid var(--l-acento);
    border-radius: var(--l-radio);
    padding: 1.15rem 1.35rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.welcome-card h2 {
    font-size: var(--l-texto-xl);
    font-weight: 650;
    margin: 0;
}

/* ----- Tarjetas de indicador -----
   Son un enlace entero, no un `div` con un enlace dentro: el objetivo táctil
   pasa de un texto de 12px a toda la tarjeta, que es lo que se agradece en
   el móvil del mostrador. */
.kpi-card {
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    padding: 1rem;
    display: flex;
    align-items: center;
    gap: .85rem;
    height: 100%;
    color: inherit;
    transition: border-color var(--l-transicion);
}
/* Sin desplazamiento al pasar el ratón: en una rejilla de cuatro tarjetas, que
   salten resulta inquieto. Basta con marcar el borde. */
.kpi-card:hover { border-color: var(--l-borde-fuerte); color: inherit; }
.kpi-body { min-width: 0; }
.kpi-icon {
    width: 42px; height: 42px;
    border-radius: var(--l-radio-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    flex-shrink: 0;
}
.kpi-label {
    font-size: var(--l-texto-xs);
    color: var(--l-texto-2);
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: 600;
    /* A 320px la etiqueta ("ENTREGADAS HOY") no cabe en su caja de 56px y,
       al ser una sola palabra larga, no podía partirse: se salía y arrastraba
       al documento entero a desplazarse en horizontal. */
    overflow-wrap: break-word;
    min-width: 0;
}
.kpi-value {
    font-size: var(--l-cifra);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -.02em;   /* los números grandes respiran mejor algo apretados */
}

/* =====================================================================
   6b. Biblioteca de componentes `.l-*`

   Reemplaza el uso directo de las clases semánticas de Bootstrap
   (`bg-success`, `text-muted`, `badge bg-danger`…) en las vistas. Aquellas
   tienen dos problemas en este producto: no siguen el tema oscuro y su
   `primary` es el color de marca del taller, así que un estado de dominio
   como "Aprobado" cambiaba de color según el cliente.

   Regla al añadir un componente: usa un TOKEN de tono, nunca un color
   literal ni una clase de Bootstrap.
   ===================================================================== */

/* ----- Píldora de estado -----
   Relleno suave + tinta del mismo tono: la pareja está verificada para
   contraste AA en ambos temas. */
.l-pill {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .2rem .55rem;
    border-radius: var(--l-radio-full);
    font-size: var(--l-texto-xs);
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
    background: var(--l-neutro-suave);
    color: var(--l-neutro-texto);
}
.l-pill i { font-size: .85em; }
.l-pill--ok      { background: var(--l-ok-suave);      color: var(--l-ok-texto); }
.l-pill--info    { background: var(--l-info-suave);    color: var(--l-info-texto); }
.l-pill--aviso   { background: var(--l-aviso-suave);   color: var(--l-aviso-texto); }
.l-pill--peligro { background: var(--l-peligro-suave); color: var(--l-peligro-texto); }
.l-pill--neutro  { background: var(--l-neutro-suave);  color: var(--l-neutro-texto); }
.l-pill--acento  { background: var(--l-acento-suave);  color: var(--l-acento-texto); }

/* Punto de color, para cuando la píldora es demasiado ruido. */
.l-punto {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--l-neutro);
    flex-shrink: 0;
}
.l-punto--ok      { background: var(--l-ok); }
.l-punto--info    { background: var(--l-info); }
.l-punto--aviso   { background: var(--l-aviso); }
.l-punto--peligro { background: var(--l-peligro); }
.l-punto--acento  { background: var(--l-acento); }

/* ----- Tarjeta -----
   Superficie quieta: borde de 1px, sin sombra. */
.l-tarjeta {
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    overflow: hidden;
}
.l-tarjeta-cab {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .7rem .9rem;
    border-bottom: 1px solid var(--l-borde);
    font-weight: 600;
    font-size: var(--l-texto-md);
}
.l-tarjeta-cab h2, .l-tarjeta-cab h3 {
    font-size: var(--l-texto-md);
    font-weight: 600;
    margin: 0;
}
.l-tarjeta-cuerpo { padding: .9rem; }
.l-tarjeta-pie {
    padding: .6rem .9rem;
    border-top: 1px solid var(--l-borde);
    background: var(--l-superficie-2);
    font-size: var(--l-texto-sm);
}
/* Acento superior para tarjetas que avisan de algo. */
.l-tarjeta--aviso   { border-top: 2px solid var(--l-aviso); }
.l-tarjeta--peligro { border-top: 2px solid var(--l-peligro); }

/* ----- Cabecera de página -----
   Título + contador a la izquierda, acciones a la derecha, y se apila sola
   en móvil en vez de comprimir el botón hasta partir su texto. */
.l-cab-pagina {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: .75rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}
.l-cab-pagina h2, .l-cab-pagina h1 {
    font-size: var(--l-texto-xl);
    font-weight: 650;
    margin: 0;
    line-height: 1.2;
}
.l-cab-sub {
    font-size: var(--l-texto-sm);
    color: var(--l-texto-2);
    margin-top: .15rem;
}
.l-cab-acciones { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }

/* ----- Barra de filtros ----- */
.l-filtros {
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    padding: .75rem .9rem;
    margin-bottom: .9rem;
}
.l-filtros .form-label {
    font-size: var(--l-texto-xs);
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--l-texto-2);
    font-weight: 600;
    margin-bottom: .25rem;
}

/* ----- Tabla -----
   `.l-tabla-apila` es lo que hace usable la app en el móvil del taller:
   bajo 768px cada fila se convierte en una tarjeta con sus etiquetas, en
   vez de obligar a desplazar en horizontal. Las vistas ponen la etiqueta
   de cada celda en `data-etq` (o en un `.l-etq-movil`). */
.l-tabla { width: 100%; border-collapse: collapse; }
.l-tabla thead th {
    text-align: left;
    font-size: var(--l-texto-xs);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--l-texto-2);
    background: var(--l-superficie-2);
    padding: .55rem .7rem;
    white-space: nowrap;
    border-bottom: 1px solid var(--l-borde);
}
.l-tabla tbody td {
    padding: .6rem .7rem;
    border-bottom: 1px solid var(--l-borde);
    vertical-align: middle;
    font-size: var(--l-texto-md);
}
.l-tabla tbody tr:last-child td { border-bottom: 0; }
.l-tabla tbody tr:hover { background: var(--l-superficie-2); }
.l-tabla .num, .l-tabla .der { text-align: right; font-variant-numeric: tabular-nums; }
.l-etq-movil { display: none; }

@media (max-width: 767.98px) {
    /* La tabla deja de ser tabla. Sin esto, aunque las filas sean bloques el
       elemento `<table>` conserva su disposición de tabla y calcula un ancho
       mínimo a partir de las columnas: la página entera se desplazaba en
       horizontal y el contenido quedaba cortado por la derecha. Y como el
       envoltorio `.table-responsive` desplaza, el desbordamiento pasaba
       desapercibido en escritorio. */
    .l-tabla-apila,
    .l-tabla-apila tbody { display: block; width: 100%; }
    .l-tabla-apila thead { display: none; }
    .l-tabla-apila tbody tr {
        display: block;
        border: 1px solid var(--l-borde);
        border-radius: var(--l-radio);
        margin-bottom: .6rem;
        padding: .3rem .1rem;
        background: var(--l-superficie);
    }
    .l-tabla-apila tbody tr:hover { background: var(--l-superficie); }
    .l-tabla-apila tbody td {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        border-bottom: 0;
        padding: .3rem .75rem;
        text-align: left;
    }
    .l-tabla-apila .num, .l-tabla-apila .der { text-align: right; }
    /* La etiqueta de la columna, que en escritorio vive en el <thead>. */
    .l-tabla-apila .l-etq-movil {
        display: inline;
        font-size: var(--l-texto-xs);
        text-transform: uppercase;
        letter-spacing: .05em;
        color: var(--l-texto-2);
        font-weight: 600;
        flex-shrink: 0;
    }
    /* Celdas vacías (una acción sin contenido) no deben dejar un hueco. */
    .l-tabla-apila td:empty { display: none; }

    /* Ya no hay nada que desplazar: el envoltorio no debe recortar la sombra
       ni crear una barra horizontal fantasma. */
    .table-responsive:has(.l-tabla-apila) { overflow-x: visible; }

    /* El valor puede ser largo (un equipo con marca y modelo): que se ajuste
       en varias líneas en vez de empujar la celda a lo ancho. */
    .l-tabla-apila td > *:last-child { min-width: 0; text-align: right; }
}

/* Ninguna página debe desplazarse en horizontal. Si algo desborda, que se
   note en el elemento culpable y no en el documento entero. */
@media (max-width: 767.98px) {
    .l-cab-pagina, .l-cab-acciones { max-width: 100%; }
    .l-cab-acciones .btn { flex: 1 1 auto; }
}

/* ----- Métrica ----- */
.l-metrica {
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    padding: .9rem;
    height: 100%;
}
.l-metrica-etq {
    font-size: var(--l-texto-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--l-texto-2);
    font-weight: 600;
}
.l-metrica-valor {
    font-size: var(--l-cifra);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
}

/* ----- Estado vacío -----
   Un icono gigante y "No hay datos" no ayuda a nadie. Un estado vacío
   decente dice qué es esto y ofrece la acción para empezar. */
.l-vacio {
    text-align: center;
    padding: 2.5rem 1rem;
    color: var(--l-texto-2);
}
.l-vacio-icono {
    font-size: 1.75rem;
    color: var(--l-texto-3);
    margin-bottom: .5rem;
    display: block;
}
.l-vacio-titulo {
    font-weight: 600;
    color: var(--l-texto);
    font-size: var(--l-texto-base);
    margin-bottom: .2rem;
}
.l-vacio-texto { font-size: var(--l-texto-md); margin-bottom: .9rem; }

/* ----- Lista de datos (clave/valor) ----- */
.l-datos { margin: 0; }
.l-datos > div {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: .4rem 0;
    border-bottom: 1px solid var(--l-borde);
    font-size: var(--l-texto-md);
}
.l-datos > div:last-child { border-bottom: 0; }
.l-datos dt, .l-datos .l-datos-etq {
    color: var(--l-texto-2);
    font-weight: 500;
    flex-shrink: 0;
}
.l-datos dd, .l-datos .l-datos-val {
    margin: 0;
    text-align: right;
    font-weight: 500;
    min-width: 0;
}
.l-datos .num { font-variant-numeric: tabular-nums; }

/* ----- Bloque de sección en formularios ----- */
.l-seccion { margin-bottom: 1.25rem; }
.l-seccion-titulo {
    font-size: var(--l-texto-xs);
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--l-texto-2);
    font-weight: 700;
    padding-bottom: .4rem;
    margin-bottom: .75rem;
    border-bottom: 1px solid var(--l-borde);
}

/* ----- Cabecera de sección con identidad -----
   El problema de la pantalla de Ajustes no era falta de color: era que todas
   las tarjetas pesaban lo mismo y la cabecera se descolocaba. `.l-tarjeta-cab`
   reparte a lo ancho, y allí el icono, el título y la ayuda iban como tres
   hijos sueltos: el icono acababa en un extremo y la descripción en el otro.
   Aquí el icono y el texto van juntos, y cada sección lleva un distintivo de
   color que sirve de ancla para el ojo al recorrer una página larga. */
.l-cab-seccion {
    display: flex;
    align-items: center;
    gap: .7rem;
    min-width: 0;
}
.l-cab-icono {
    width: 34px; height: 34px;
    flex-shrink: 0;
    border-radius: var(--l-radio-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
}
.l-cab-textos { min-width: 0; }
.l-cab-titulo {
    font-weight: 650;
    font-size: var(--l-texto-base);
    line-height: 1.2;
    color: var(--l-texto);
}
.l-cab-ayuda {
    font-size: var(--l-texto-sm);
    color: var(--l-texto-2);
    line-height: 1.3;
}

/* Filo de color a la izquierda: identifica la sección sin gritar. */
.l-tarjeta--tono { border-left: 3px solid var(--l-neutro); }
.l-tarjeta--acento  { border-left-color: var(--l-acento); }
.l-tarjeta--ok      { border-left-color: var(--l-ok); }
.l-tarjeta--info    { border-left-color: var(--l-info); }
.l-tarjeta--aviso2  { border-left-color: var(--l-aviso); }

/* ----- Página larga: columna de navegación + contenido -----
   Una tira de píldoras leía como etiquetas, no como navegación. El patrón
   correcto para una pantalla larga es una columna lateral fija con la
   sección activa marcada — y de paso aprovecha el espacio vertical que
   quedaba vacío a la izquierda.
   Lo usan Ajustes y el Manual de usuario; el rail y su resaltado
   (`app.js`) enganchan por clase, no por el id de una página concreta. */
.l-ajustes, .l-manual {
    display: grid;
    grid-template-columns: 216px minmax(0, 1fr);
    gap: 1.75rem;
    align-items: start;
}
/* Los hijos directos pueden ENCOGERSE. Sin esto, un `min-width: auto` (el
   valor por defecto de un elemento de grid) deja que el contenido ancho
   —la tabla de secuencias fiscales de Ajustes— estire la columna y arrastre
   la página entera a desplazarse en horizontal, aunque la tabla vaya dentro
   de un `.table-responsive`: el contenedor no puede scrollear lo que no
   puede encogerse. Medido a 320 y 390 px; ver la nota de móvil en CLAUDE.md. */
.l-ajustes > *, .l-manual > * { min-width: 0; }

@media (max-width: 991.98px) {
    /* Sin sitio para dos columnas: la navegación pasa arriba, en una tira
       que se desplaza en horizontal (ella sola, no la página). */
    .l-ajustes, .l-manual { grid-template-columns: minmax(0, 1fr); gap: 0; }
    .l-rail {
        position: sticky;
        top: var(--l-top-alto);
        z-index: 20;
        display: flex;
        gap: .15rem;
        overflow-x: auto;
        padding: .5rem 0;
        margin-bottom: .75rem;
        background: var(--l-fondo);
        border-bottom: 1px solid var(--l-borde);
        scrollbar-width: none;
    }
    .l-rail::-webkit-scrollbar { display: none; }
    .l-rail a { white-space: nowrap; border-left: 0 !important; }
    .l-rail-titulo { display: none; }
}

@media (min-width: 992px) {
    .l-rail { position: sticky; top: calc(var(--l-top-alto) + 1rem); }
}
.l-rail-titulo {
    font-size: var(--l-texto-xs);
    text-transform: uppercase;
    letter-spacing: .07em;
    font-weight: 700;
    color: var(--l-texto-3);
    padding: 0 .75rem .5rem;
}
.l-rail a {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .5rem .75rem;
    border-radius: var(--l-radio-sm);
    border-left: 2px solid transparent;
    color: var(--l-texto-2);
    font-size: var(--l-texto-md);
    font-weight: 500;
    text-decoration: none;
    transition: background var(--l-transicion), color var(--l-transicion);
}
.l-rail a i { font-size: 1rem; width: 1.2em; text-align: center; flex-shrink: 0; color: var(--l-texto-3); }
.l-rail a:hover { background: var(--l-superficie-2); color: var(--l-texto); }
.l-rail a:hover i { color: var(--l-texto-2); }
/* La sección en la que estás: acento sobrio, sin relleno chillón. */
.l-rail a.activo {
    color: var(--l-acento-texto);
    border-left-color: var(--l-acento);
    background: var(--l-acento-suave);
    font-weight: 600;
}
.l-rail a.activo i { color: var(--l-acento-texto); }

/* Al saltar por anclaje, dejar aire bajo la barra y el índice. */
.l-tarjeta[id],
.l-doc-sec[id],
.accordion-item[id] { scroll-margin-top: calc(var(--l-top-alto) + 4rem); }

/* ----- Manual de usuario -----
   Es la única página del sistema que es texto largo, así que necesita
   medidas de lectura propias: ancho contenido, listas con aire y una
   caja de búsqueda — sin ella todo el contenido queda dentro de
   acordeones cerrados, invisible para el Ctrl+F del navegador. */
/* Ayuda contextual: el interrogante junto al título de cada módulo.
   Discreto en reposo, con el color de marca al pasar por encima; en pantalla
   táctil respeta el mínimo de 44px que exige el resto de la interfaz. */
.l-ayuda-ico {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    margin-left: .15rem;
    border-radius: 50%;
    color: var(--l-texto-3);
    font-size: .95rem;
    text-decoration: none;
    vertical-align: middle;
    transition: color var(--l-transicion), background var(--l-transicion);
}
.l-ayuda-ico:hover, .l-ayuda-ico:focus-visible {
    color: var(--l-acento-texto);
    background: var(--l-acento-suave);
}
@media (pointer: coarse) {
    .l-ayuda-ico { width: 44px; height: 44px; margin-left: 0; }
}

.l-doc-sec { margin-bottom: 1.5rem; }
.l-doc-titulo {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-size: var(--l-texto-lg);
    font-weight: 650;
    color: var(--l-texto);
    margin: 0 0 .6rem;
}
.l-doc-titulo i { color: var(--l-acento); }
.l-doc-intro {
    font-size: var(--l-texto-md);
    color: var(--l-texto-2);
    margin: -.35rem 0 .7rem;
}
/* Prosa: el interlineado de la app es demasiado apretado para leer párrafos. */
.l-doc p { line-height: 1.6; margin-bottom: .7rem; }
.l-doc p:last-child, .l-doc ul:last-child, .l-doc ol:last-child { margin-bottom: 0; }
.l-doc ul, .l-doc ol { padding-left: 1.15rem; margin-bottom: .7rem; }
.l-doc li { line-height: 1.6; margin-bottom: .35rem; }
.l-doc li:last-child { margin-bottom: 0; }
/* `.l-datos` nace para pares dato/valor cortos, alineados a la derecha para
   comparar cifras de un vistazo. Aquí los "valores" son frases de dos líneas:
   alineadas a la derecha se leen fatal. Se conserva el componente y sólo se
   cambia el reparto — en móvil, además, la descripción baja a su propia línea. */
.l-doc .l-datos > div { flex-wrap: wrap; align-items: baseline; }
.l-doc .l-datos dt { flex: 0 0 9.5rem; min-width: 0; }
.l-doc .l-datos dd { flex: 1 1 16rem; text-align: left; font-weight: 400; }
@media (max-width: 575.98px) {
    .l-doc .l-datos > div { gap: .15rem; }
    .l-doc .l-datos dt { flex-basis: 100%; }
}

/* Teclas: Bootstrap las pinta casi negras y en tema oscuro desaparecen. */
.l-doc kbd, .l-tecla {
    display: inline-block;
    padding: .05rem .35rem;
    border: 1px solid var(--l-borde-fuerte);
    border-bottom-width: 2px;
    border-radius: var(--l-radio-sm);
    background: var(--l-superficie-2);
    color: var(--l-texto);
    font-size: .85em;
    font-weight: 600;
    line-height: 1.4;
}

/* Buscador del manual: pegado bajo la barra para que siga a mano al
   desplazarse por una página que es, a propósito, larga. */
.l-doc-buscar {
    position: sticky;
    top: var(--l-top-alto);
    z-index: 15;
    padding: .6rem 0;
    background: var(--l-fondo);
    margin-bottom: .9rem;
}
.l-doc-buscar-fila { display: flex; gap: .5rem; align-items: center; }
.l-doc-conteo {
    font-size: var(--l-texto-sm);
    color: var(--l-texto-2);
    min-height: 1.2em;
    margin-top: .35rem;
}
@media (max-width: 991.98px) {
    /* En móvil el rail ya ocupa la banda pegajosa: el buscador va debajo,
       sin pegarse, o se apilan dos barras y no queda pantalla. */
    .l-doc-buscar { position: static; padding-top: 0; }
}

/* Barra de guardado pegada al pie: en un formulario de seis bloques, el botón
   al final obliga a recorrer toda la página para confirmar un cambio. */
.l-guardar {
    position: sticky;
    bottom: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .75rem;
    flex-wrap: wrap;
    padding: .75rem 0 1rem;
    margin-top: .5rem;
    background: linear-gradient(to top, var(--l-fondo) 65%, transparent);
}
@media (max-width: 575.98px) {
    /* En móvil compite con la barra de pestañas de la PWA. */
    .l-guardar { bottom: var(--pwa-tabbar-h, 0px); }
    .l-guardar .btn { flex: 1 1 auto; }
}

/* ----- Editor de plantillas de mensajes -----
   Los campos se insertan pulsando una etiqueta con su nombre real ("Nombre del
   cliente"), no escribiendo `{cliente}` de memoria. */
.l-chips { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.l-chip {
    border: 1px dashed var(--l-borde-fuerte);
    background: var(--l-superficie-2);
    color: var(--l-texto-2);
    border-radius: var(--l-radio-full);
    padding: .2rem .6rem;
    font-size: var(--l-texto-xs);
    font-weight: 500;
    cursor: pointer;
    transition: background var(--l-transicion), color var(--l-transicion), border-color var(--l-transicion);
}
.l-chip:hover {
    background: var(--l-acento-suave);
    color: var(--l-acento-texto);
    border-color: var(--l-acento-borde);
    border-style: solid;
}

/* Vista previa: lo que el cliente va a leer, con datos de ejemplo. */
.l-previa {
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    overflow: hidden;
    background: var(--l-superficie-2);
}
.l-previa-cab {
    padding: .35rem .7rem;
    font-size: var(--l-texto-xs);
    font-weight: 600;
    color: var(--l-texto-2);
    border-bottom: 1px solid var(--l-borde);
    background: var(--l-superficie);
}
.l-previa-nota { font-weight: 400; color: var(--l-texto-3); margin-left: .3rem; }
.l-previa-cuerpo {
    padding: .7rem;
    font-size: var(--l-texto-md);
    line-height: 1.5;
    white-space: pre-wrap;   /* respetar los saltos de línea del mensaje */
    overflow-wrap: anywhere; /* los enlaces son largos y no llevan espacios */
    min-height: 2.5rem;
    color: var(--l-texto);
}
/* Cuando se muestra el texto de reserva, se distingue del propio. */
.l-previa-cuerpo.l-previa--defecto { color: var(--l-texto-2); font-style: italic; }

.l-plantilla + hr { border-color: var(--l-borde); }

/* ----- Páginas de error -----
   El número gigante se queda como marca de agua discreta: lo que la persona
   necesita leer es qué pasó y cómo salir de ahí, no un "404" de 120px. */
.l-error-body {
    min-height: 100vh;
    margin: 0;
    display: grid;
    place-items: center;
    background: var(--l-fondo);
    padding: 2rem 1rem;
}
.l-error { text-align: center; max-width: 30rem; }
.l-error-icono {
    width: 56px; height: 56px;
    border-radius: var(--l-radio-lg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    margin-bottom: 1rem;
}
.l-error-codigo {
    font-size: var(--l-texto-sm);
    font-weight: 700;
    letter-spacing: .12em;
    color: var(--l-texto-3);
    font-variant-numeric: tabular-nums;
}
.l-error-rotulo {
    font-size: 1.5rem;
    font-weight: 650;
    margin: .15rem 0 .5rem;
}
.l-error-texto { color: var(--l-texto-2); margin-bottom: 1.5rem; }

/* ----- Texto monetario -----
   Positivo y negativo con tono, no con `text-success`/`text-danger`. */
.l-dinero      { font-variant-numeric: tabular-nums; font-weight: 600; }
.l-dinero--mas { color: var(--l-ok-texto); }
.l-dinero--menos { color: var(--l-peligro-texto); }

/* ----- Línea de tiempo (historial de órdenes) ----- */
.timeline { position: relative; margin: 0; padding: 0; }
.timeline-item {
    position: relative;
    display: flex;
    gap: .9rem;
    padding-bottom: 1.35rem;
}
.timeline-item:last-child { padding-bottom: 0; }
/* Línea vertical que conecta los puntos. */
.timeline-item::before {
    content: '';
    position: absolute;
    left: 17px;
    top: 8px;
    bottom: -8px;
    width: 2px;
    background: var(--l-borde);
    z-index: 0;
}
.timeline-item:last-child::before { display: none; }

.timeline-dot {
    position: relative;
    z-index: 1;
    width: 36px; height: 36px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 1rem;
    /* Anillo del color del fondo: corta limpiamente la línea en el punto. */
    box-shadow: 0 0 0 4px var(--l-superficie);
}

.timeline-body {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    padding: .55rem .8rem;
    transition: border-color var(--l-transicion);
}
.timeline-body:hover { border-color: var(--l-borde-fuerte); }
.timeline-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: .5rem;
}
.timeline-title { font-weight: 600; font-size: var(--l-texto-base); line-height: 1.2; }
.timeline-time { font-size: var(--l-texto-xs); color: var(--l-texto-2); white-space: nowrap; }
.timeline-desc { font-size: var(--l-texto-md); margin-top: .15rem; }
.timeline-meta { font-size: var(--l-texto-xs); color: var(--l-texto-2); margin-top: .2rem; }
.timeline-meta i { opacity: .7; }

/* ----- Bandeja de notificaciones ----- */
.notif-list a { color: var(--l-texto); }
.notif-list a:hover { background-color: var(--l-superficie-2); }

/* ----- Punto de venta -----
   Se usa de pie, con un cliente delante y a veces con un lector de códigos.
   Por eso el buscador y el total son más grandes que en el resto de la app:
   aquí la jerarquía la manda la distancia de lectura, no la densidad.
   Nada de colores literales: todo sale de los tokens. */
.l-pos-buscador { position: relative; }

.l-pos-resultados {
    position: absolute;
    z-index: 20;
    inset-inline: 0;
    top: calc(100% + .25rem);
    max-height: 320px;
    overflow-y: auto;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    box-shadow: var(--l-sombra);
}
.l-pos-resultado {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: .75rem;
    width: 100%;
    padding: .55rem .75rem;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--l-borde);
    text-align: left;
    color: var(--l-texto);
}
.l-pos-resultado:last-child { border-bottom: 0; }
.l-pos-resultado:hover,
.l-pos-resultado:focus-visible { background: var(--l-superficie-2); }
.l-pos-resultado-txt { font-weight: 500; }
.l-pos-resultado-num {
    font-size: var(--l-texto-sm);
    color: var(--l-texto-2);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Una línea recién añadida parpadea un momento: con el lector de códigos, sin
   esto no hay forma de saber si el disparo entró o si hay que repetirlo. */
.l-linea-nueva { animation: l-linea-entra .6s ease; }
@keyframes l-linea-entra {
    from { background: var(--l-acento-suave); }
    to   { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .l-linea-nueva { animation: none; }
}

.l-pos-cobro { position: sticky; top: 5rem; }
.l-pos-totales td { padding-block: .3rem; }
.l-pos-totales td:first-child { color: var(--l-texto-2); }
.l-pos-total td {
    font-weight: 700;
    font-size: 1.15rem;
    border-top: 1px solid var(--l-borde);
    padding-top: .5rem;
}
.l-pos-vuelta {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: .6rem .75rem;
    border-radius: var(--l-radio);
    background: var(--l-superficie-2);
    color: var(--l-texto-2);
    font-size: var(--l-texto-sm);
}
.l-pos-cambio {
    font-size: 1.25rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

/* ----- Fotos adjuntas: galería de una orden o de un producto -----
   Rejilla que se adapta sola con `auto-fill`: no hay que declarar cuántas
   columnas por tamaño de pantalla, y en un móvil de 320 px caben dos.
   Todo con tokens; ni un color literal. */
.l-fotos {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
    gap: .75rem;
}
.l-foto {
    margin: 0;
    position: relative;
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    overflow: hidden;
    background: var(--l-superficie-2);
}
.l-foto img {
    display: block;
    width: 100%;
    /* Cuadrada y recortada: con proporciones libres la rejilla queda dentada y
       parece rota. El original completo está a un clic. */
    aspect-ratio: 1 / 1;
    object-fit: cover;
    background: var(--l-superficie-2);
}
.l-foto-cinta {
    position: absolute;
    top: .35rem;
    left: .35rem;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .1rem .45rem;
    border-radius: var(--l-radio-full);
    background: var(--l-acento);
    color: var(--l-acento-sobre);
    font-size: var(--l-texto-xs);
    font-weight: 600;
}
.l-foto-pie {
    display: flex;
    justify-content: flex-end;
    gap: .25rem;
    padding: .35rem;
    border-top: 1px solid var(--l-borde);
    background: var(--l-superficie);
}
.l-foto-pie form { margin: 0; }

.l-fotos-subir { border-bottom: 1px solid var(--l-borde); padding-bottom: .9rem; }

/* ----- Miniatura del producto en el listado de inventario ----- */
.l-prod-linea { display: flex; align-items: flex-start; gap: .6rem; }
.l-prod-mini {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    border-radius: var(--l-radio-sm);
    border: 1px solid var(--l-borde);
    object-fit: cover;
    background: var(--l-superficie-2);
}
.l-prod-mini--vacia {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--l-texto-3);
}

/* ----- Catálogo visual del punto de venta -----
   Dos vistas sobre el mismo marcado: la clase del contenedor decide. Una
   tienda de 40 productos se maneja mejor en rejilla; un taller con 300
   repuestos, en lista. La elección se recuerda por equipo. */
.l-cat {
    display: grid;
    /* 150px y no menos: con «RD$ 2,000.00» una tarjeta más estrecha parte el
       importe en dos líneas y la rejilla se ve descuadrada. */
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: .6rem;
    max-height: 420px;
    /* La rejilla se desplaza POR DENTRO: si creciera sin límite, el carrito y
       el botón de cobrar quedarían fuera de la pantalla en cada búsqueda. */
    overflow-y: auto;
}
.l-cat-item {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: .4rem;
    padding: .5rem;
    text-align: left;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio);
    color: var(--l-texto);
    cursor: pointer;
    transition: border-color var(--l-transicion), background var(--l-transicion);
}
.l-cat-item:hover { border-color: var(--l-acento); background: var(--l-superficie-2); }
.l-cat-item:focus-visible { outline: 2px solid var(--l-acento); outline-offset: 2px; }
/* Ya está en la venta: se marca, no se bloquea. */
.l-cat-item--en { border-color: var(--l-acento); background: var(--l-acento-suave); }
/* Agotado: se atenúa, pero SE PUEDE PULSAR. Puede haber existencias sin
   registrar, y dejar al mostrador sin vender por un inventario desactualizado
   es peor que el descuadre. El servidor rechaza si de verdad no hay. */
.l-cat-item--agotado { opacity: .55; }

.l-cat-foto {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: var(--l-radio-sm);
    overflow: hidden;
    background: var(--l-superficie-2);
}
.l-cat-foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
.l-cat-foto--vacia {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--l-texto-3);
    font-size: 1.5rem;
}

.l-cat-txt { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.l-cat-nombre {
    font-size: var(--l-texto-sm);
    font-weight: 500;
    line-height: 1.25;
    /* Dos líneas y a partir de ahí, puntos suspensivos: sin esto un nombre
       largo estira su tarjeta y descuadra toda la fila. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.l-cat-pie { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: .15rem .4rem; }
.l-cat-precio { font-weight: 700; font-size: var(--l-texto-sm); font-variant-numeric: tabular-nums; white-space: nowrap; }
.l-cat-stock  { font-size: var(--l-texto-xs); color: var(--l-texto-2); white-space: nowrap; }

.l-cat-badge {
    position: absolute;
    top: -.4rem;
    right: -.4rem;
    min-width: 1.35rem;
    height: 1.35rem;
    padding: 0 .3rem;
    border-radius: var(--l-radio-full);
    background: var(--l-acento);
    color: var(--l-acento-sobre);
    font-size: var(--l-texto-xs);
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Vista de lista: la misma tarjeta en horizontal. */
.l-cat--lista { grid-template-columns: 1fr; gap: .35rem; }
.l-cat--lista .l-cat-item { flex-direction: row; align-items: center; gap: .6rem; }
.l-cat--lista .l-cat-foto { width: 44px; height: 44px; flex-shrink: 0; aspect-ratio: auto; }
.l-cat--lista .l-cat-txt  { flex: 1 1 auto; }
.l-cat--lista .l-cat-nombre { -webkit-line-clamp: 1; }
.l-cat--lista .l-cat-pie  { justify-content: flex-start; gap: .75rem; }
.l-cat--lista .l-cat-badge { position: static; margin-left: auto; }

.l-cat-vacio {
    padding: 1.25rem;
    text-align: center;
    color: var(--l-texto-2);
    font-size: var(--l-texto-sm);
}

/* Miniatura en los resultados del buscador. */
.l-pos-resultado-foto {
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    border-radius: var(--l-radio-sm);
    object-fit: cover;
    background: var(--l-superficie-2);
}

@media (prefers-reduced-motion: reduce) {
    .l-cat-item { transition: none; }
}

/* En móvil la columna de cobro va debajo, así que fijarla no aporta nada y
   además taparía el carrito al desplazarse. */
@media (max-width: 991.98px) {
    .l-pos-cobro { position: static; }
}
/* ---------------------------------------------------------------------
   7. Pantallas de acceso (login, recuperar, restablecer)

   OJO: en modo SaaS el login NO es white-label y no puede serlo — el
   dominio de acceso es único para todos los talleres y hasta que alguien
   no se autentica no hay tenant resuelto, así que `cfg()` no tiene de
   dónde leer la marca y devuelve la del producto. Esta pantalla se ve
   siempre con el acento por defecto; el color del taller entra a partir
   del dashboard.

   Aun así todo aquí va sobre tokens y no sobre los `#2563eb` que había
   escritos a mano: así el día que el acceso se sirva por subdominio de
   taller (o se recuerde el último tenant del navegador) la pantalla se
   tiñe sola, sin tocar una regla.

   RELIEVE (neumorfismo) — el porqué y el hasta dónde.
   La landing ya está construida sobre relieve (`--n-alto`, `--n-hundido`,
   `--n-chip` en `assets/landing/servia.css`), así que quien viene de la web
   entraba a un acceso que no se parecía a lo que le vendió. Aquí se repite
   ese lenguaje para coser las dos mitades, con la MISMA regla que rige allí:

     el relieve va en SUPERFICIES, nunca en los controles.

   Los azulejos, el chip del logo y la tarjeta del formulario se elevan; los
   campos y el botón conservan su borde de 1px y su contraste. Un campo
   hundido sin borde pierde el límite que exige AA y un botón neumórfico no
   se lee como pulsable — en la pantalla de acceso eso se paga en soporte.

   Y la sombra DERIVA de la superficie, no es negra: sobre estos fondos un
   negro puro se ve como suciedad, no como profundidad.
   --------------------------------------------------------------------- */

/* Tokens de relieve. Se declaran aquí y no en las secciones 1-2 porque hoy
   sólo los usan las pantallas de acceso; si otra pantalla adopta el relieve,
   promoverlos a la tabla global. Van en las dos versiones, claro y oscuro,
   como el resto de los tokens de esta hoja. */
/* Tokens de relieve. Se declaran aquí y no en las secciones 1-2 porque hoy
   sólo los usan las pantallas de acceso; si otra pantalla adopta el relieve,
   promoverlos a la tabla global. Van en las dos versiones, claro y oscuro,
   como el resto de los tokens de esta hoja. */
:root {
    --l-relieve-luz:    rgba(255,255,255,.95);
    --l-relieve-sombra: rgba(16,24,40,.13);
}
[data-bs-theme="dark"] {
    /* Azulada y no negra: sobre `--l-fondo` (#0d1117) el negro puro no
       levanta nada, sólo ensucia. Mismo criterio que `--n-luz` en la landing. */
    --l-relieve-luz:    rgba(148,197,255,.055);
    --l-relieve-sombra: rgba(2,6,15,.62);
}
:root,
[data-bs-theme="dark"] {
    --l-relieve:         -2px -2px 5px var(--l-relieve-luz), 6px 8px 20px var(--l-relieve-sombra);
    --l-relieve-alto:    -6px -6px 16px var(--l-relieve-luz), 14px 18px 42px var(--l-relieve-sombra);
    --l-relieve-chip:    -1px -1px 3px var(--l-relieve-luz), 3px 4px 10px var(--l-relieve-sombra);
    /* El pozo: es lo que da el neumorfismo pronunciado de los campos. */
    --l-relieve-hundido: inset 3px 4px 8px var(--l-relieve-sombra), inset -2px -2px 5px var(--l-relieve-luz);
    /* Elevación del botón: SÓLO la sombra, sin el reflejo blanco. Sobre un
       relleno de acento el reflejo se ve como una mancha, no como luz. */
    --l-relieve-boton:   8px 10px 24px var(--l-relieve-sombra);
}

.login-body {
    min-height: 100vh;
    min-height: 100dvh;
    margin: 0;
    display: grid;
    place-items: center;
    padding: clamp(.75rem, 2.5vh, 2rem) clamp(.75rem, 3vw, 2rem);
    background:
        radial-gradient(1100px 700px at 12% -10%, var(--l-acento-suave), transparent 60%),
        var(--l-fondo);
    color: var(--l-texto);
}

/* UNA sola pieza con dos zonas dentro, no dos mitades a sangre separadas por
   un filete con el formulario flotando aparte en medio del lienzo: aquello
   se leía como dos pantallas pegadas. Ahora el relieve es del conjunto y lo
   que distingue las zonas es el tono de su superficie, que integra en vez de
   partir. */
.login-shell {
    width: min(1060px, 100%);
    max-height: 100%;
    display: grid;
    grid-template-columns: 1fr;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: clamp(18px, 2.2vh, 26px);
    box-shadow: var(--l-relieve-alto);
    overflow: hidden;
}
@media (min-width: 992px) {
    .login-shell { grid-template-columns: 1.12fr .88fr; }
    /* Red de seguridad para una ventana muy baja: antes de que la PÁGINA se
       desplace, se desplaza la zona por dentro. El objetivo es que no haga
       falta nunca — de eso se encargan los `clamp()` de abajo. */
    .login-brand, .login-form-panel { overflow-y: auto; }
}

/* ----- Zona de marca -----
   Todas las medidas verticales van en `clamp(mínimo, vh, máximo)`: en un
   portátil de 768px de alto la pieza se comprime sola en vez de empujar la
   tercera función fuera de la pantalla, que es lo que pasaba. */
.login-brand {
    position: relative;
    display: none;
    color: var(--l-texto);
    padding: clamp(1rem, 2.8vh, 2.5rem) clamp(1.25rem, 2.4vw, 2.75rem);
    /* Degradado y no un tono plano: dos superficies planas que se tocan
       dejan una linea recta en el medio, y esa linea volvia a partir la
       pieza en dos justo lo que se queria evitar. Asi la zona de marca se
       funde con la del formulario y el corte desaparece. */
    background: linear-gradient(100deg, var(--l-superficie-2) 30%, var(--l-superficie) 100%);
    overflow: hidden;
}
/* Un solo halo de acento por zona, no uno por azulejo: uno por tarjeta
   produce bandas en cuanto el panel es malo, que es la mitad de los
   teléfonos de un taller. Misma lección que la landing. */
.login-brand::before {
    content: "";
    position: absolute; inset: 0;
    background: radial-gradient(680px 520px at 15% 0%, var(--l-acento-suave), transparent 66%);
    pointer-events: none;
}
@media (min-width: 992px) { .login-brand { display: flex; } }
.login-brand-inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: clamp(.6rem, 1.8vh, 1.5rem);
    width: 100%;
    max-width: 460px;
    margin: auto;
}
.login-brand-top { display: flex; align-items: center; gap: .85rem; }
/* La insignia del producto trae su propio fondo, su radio y su brillo, así
   que aquí sólo se fija el tamaño y el relieve que comparte con el resto de
   la pieza. Antes era un glifo de Bootstrap dentro de un chip de superficie:
   con una marca de verdad, ese chip sería una segunda caja alrededor. */
.login-brand-logo {
    width: 42px; height: 42px;
    border-radius: 12px;
    box-shadow: var(--l-relieve-chip);
    display: block;
}
.login-brand-name { font-weight: 700; font-size: 1.1rem; letter-spacing: .3px; }

/* El separador vertical lo pone el `gap` del contenedor. */
.login-brand-hero { margin: 0; }
/* El puente con Bootstrap fija `--bs-heading-color`; el titular de esta zona
   se declara igualmente para que no dependa de ese puente. Ya mordió una vez
   en el panel super-admin: mismo error, misma cura. */
.login-brand-title,
.login-brand h1, .login-brand h2 {
    color: var(--l-texto);
    font-size: clamp(1.35rem, 2.9vh, 2.15rem);
    font-weight: 800;
    line-height: 1.15;
    margin-bottom: clamp(.35rem, .9vh, .75rem);
}
.login-brand-subtitle {
    color: var(--l-texto-2);
    font-size: clamp(.9rem, 1.7vh, 1.05rem);
    margin-bottom: clamp(.6rem, 1.8vh, 1.5rem);
    line-height: 1.5;
}
.login-feature-list {
    list-style: none; padding: 0; margin: 0;
    display: grid;
    gap: clamp(.4rem, 1.1vh, .85rem);
}
.login-feature-list li {
    display: flex; align-items: flex-start; gap: .8rem;
    padding: clamp(.5rem, 1.3vh, .85rem) 1rem;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio-lg);
    box-shadow: var(--l-relieve-chip);
}
.login-feature-list strong { display: block; font-weight: 600; color: var(--l-texto); }
.login-feature-list small { color: var(--l-texto-2); font-size: var(--l-texto-sm); }
/* Chip dentro de un azulejo ya elevado: aquí el relieve se omite a
   propósito. Dos alturas en el mismo milímetro se ven sucias. */
.login-feature-icon {
    width: 34px; height: 34px;
    flex: 0 0 34px;
    background: var(--l-acento-suave);
    color: var(--l-acento-texto);
    border-radius: 9px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 1rem;
}
.login-brand-foot { color: var(--l-texto-2); }

/* ----- Zona del formulario -----
   Ya no lleva tarjeta propia: la tarjeta es la pieza entera. Dos cajas con
   sombra, una dentro de otra, es justo lo que hacía que el formulario
   pareciera un elemento suelto pegado al lado del armazón. */
.login-form-panel {
    background: var(--l-superficie);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(1.25rem, 3.4vh, 2.75rem) clamp(1rem, 2.4vw, 2.5rem);
}
.login-form-wrap { width: 100%; max-width: 380px; }
@media (max-width: 575.98px) {
    .login-form-panel { padding: 1.35rem 1rem; }
}
.login-form-head h1 { font-weight: 700; color: var(--l-texto); }
.login-logo {
    width: 56px; height: 56px;
    background: var(--l-acento);
    border-radius: var(--l-radio-lg);
    color: var(--l-acento-sobre);
    font-size: 1.6rem;
    display: inline-flex; align-items: center; justify-content: center;
}
/* Logo del taller: imagen limpia, sin recuadro ni fondo. Vivía en un
   `<style nonce>` dentro de `_acceso.php`, o sea que el navegador lo volvía
   a descargar en cada visita; aquí se cachea con la hoja. */
.login-brand-img { max-height: 84px; max-width: 280px; object-fit: contain; display: block; }
.login-logo-img  { max-height: 88px; max-width: 240px; object-fit: contain; }

/* ----- Los campos: pozo neumórfico CON borde -----
   El relieve hundido va ENCIMA del borde de 1px, nunca en su lugar. Sin ese
   filete el límite del control se queda en una sombra difusa, que es donde
   el neumorfismo incumple AA y donde la gente deja de saber dónde se escribe.
   El fondo del campo baja un tono respecto de la superficie: sin ese salto
   el pozo no se ve, y entonces la sombra sólo ensucia. */
.login-form .form-floating > label { color: var(--l-texto-2); }
.login-form .form-control {
    background-color: var(--l-fondo);
    border: 1px solid var(--l-borde-fuerte);
    border-radius: var(--l-radio);
    box-shadow: var(--l-relieve-hundido);
    transition: border-color .15s, box-shadow .15s, background-color .15s;
}
.login-form .form-control:focus {
    background-color: var(--l-superficie);
    border-color: var(--l-acento);
    box-shadow: var(--l-relieve-hundido), 0 0 0 .22rem var(--l-acento-suave);
}
.btn-toggle-pass {
    position: absolute;
    right: .5rem; top: 50%;
    transform: translateY(-50%);
    color: var(--l-texto-2);
    text-decoration: none;
    padding: .25rem .5rem;
    z-index: 5;
}
.btn-toggle-pass:hover { color: var(--l-acento-texto); }

/* ----- El botón: relieve alzado y hundido al pulsar -----
   Aquí el relieve SÍ añade affordance en vez de quitarla, porque el relleno
   sigue siendo el acento sólido: se ve pulsable por color y además se hunde
   al pulsarlo. Lo que no se hace nunca es dejarlo del color del fondo. */
.login-submit {
    background: var(--l-acento);
    color: var(--l-acento-sobre);
    border: none;
    font-weight: 600;
    padding-block: .85rem;
    border-radius: var(--l-radio);
    box-shadow: var(--l-relieve-boton);
    transition: background .15s ease, box-shadow .15s ease, transform .1s ease;
}
.login-submit:hover, .login-submit:focus {
    background: var(--l-acento-fuerte);
    color: var(--l-acento-sobre);
    box-shadow: var(--l-relieve-alto);
}
.login-submit:active {
    box-shadow: var(--l-relieve-hundido);
    transform: translateY(1px);
}

/* En inputs flotantes el label hace de placeholder: mantenerlo transparente
   para que el texto del placeholder no choque con el label. */
.form-floating > .form-control::placeholder { color: transparent; }

/* ---------------------------------------------------------------------
   7b. El robot del taller (views/auth/_robot.php)

   Ilustración animada: el robot aprieta un tornillo de un equipo sobre su
   banco. Es el oficio del taller, no un adorno genérico — se entiende sin
   leer nada, que es justo lo que hace falta en una pantalla donde quien
   mira todavía no sabe si está en el sistema correcto.

   Todo el movimiento está aquí y no dentro del SVG para que se cachee con
   la hoja y no necesite `nonce`. El SVG sólo lleva geometría y clases.

   Las animaciones son sólo `transform` y `opacity` — se componen en la GPU
   y no repintan. `prefers-reduced-motion` las congela con la regla global
   de la sección 4, y lo que queda es la pose de reposo del SVG.
   --------------------------------------------------------------------- */
.login-robot {
    position: relative;
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-radius: var(--l-radio-lg);
    box-shadow: var(--l-relieve);
    padding: clamp(.6rem, 1.4vh, 1rem) clamp(.7rem, 1.6vw, 1.15rem) clamp(.3rem, .8vh, .5rem);
}
.login-robot-svg { display: block; width: 100%; height: auto; }
/* El azulejo ocupa el ancho de la columna y el dibujo se limita por ALTO.
   Al revés -limitando el ancho- en un portátil bajo quedaba un cuadradito
   pequeño con media columna vacía al lado: el presupuesto escaso es el
   vertical, no el horizontal, y conviene gastar el que sobra. */
.login-robot--grande { width: 100%; margin-bottom: clamp(.6rem, 1.6vh, 1.5rem); }
.login-robot--grande .login-robot-svg {
    width: auto;
    max-width: 100%;
    height: clamp(112px, 19vh, 215px);
    margin-inline: auto;
}
.login-robot--mini   { max-width: 172px; margin: 0 auto 1rem; padding: .5rem .6rem .25rem; }

/* ----- Chapa, pantalla y herramienta ----- */
.rb-chapa {
    fill: var(--l-superficie);
    stroke: var(--l-borde-fuerte);
    stroke-width: 2;
}
.rb-visor  { fill: var(--l-nav-fondo); }
.rb-ojo    { fill: var(--l-acento); }
.rb-luz    { fill: var(--l-acento); }
.rb-antena { stroke: var(--l-neutro); stroke-width: 2.5; stroke-linecap: round; }
.rb-oreja  { fill: var(--l-neutro); }
.rb-panel  { fill: var(--l-acento-suave); stroke: var(--l-acento-borde); stroke-width: 1.5; }
.rb-linea  { stroke: var(--l-acento); stroke-width: 2.5; stroke-linecap: round; opacity: .5; }
/* Las extremidades van en `--l-neutro` y no en el gris de borde: con el
   tono del borde eran casi invisibles sobre la superficie clara, y en la
   pantalla lo que se veía era un robot sin brazos. */
.rb-brazo-linea {
    fill: none;
    stroke: var(--l-neutro);
    stroke-width: 7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.rb-mano  { fill: var(--l-neutro); }
.rb-banco { fill: var(--l-superficie-2); stroke: var(--l-neutro); stroke-width: 1.5; }
.rb-pata  { stroke: var(--l-neutro); stroke-width: 5; stroke-linecap: round; }
/* El equipo en reparación es la silueta oscura de la escena — la misma
   tinta del visor. En gris medio se confundía con el banco y con los brazos
   y la mitad derecha del dibujo se leía como una sola mancha. */
/* El filete lo sostiene en oscuro: ahí la tinta del equipo y la del
   azulejo casi coinciden y sin borde el aparato se disolvía en el fondo. */
.rb-equipo-cuerpo   { fill: var(--l-nav-fondo); stroke: var(--l-neutro); stroke-width: 1.5; }
.rb-equipo-pantalla { fill: var(--l-info); opacity: .8; }
.rb-tornillo { fill: var(--l-nav-texto); }
.rb-mango  { fill: var(--l-acento); }
.rb-virola { fill: var(--l-neutro); }
/* La punta va en la tinta oscura, no en gris: en gris medio sobre superficie
   clara no se veía y el destornillador parecía flotar sobre el equipo. */
.rb-punta  { fill: var(--l-nav-fondo); }
/* La chispa nace invisible: ésa es la pose de reposo, la que queda cuando
   se pide menos movimiento. Un destello congelado sería un defecto. */
.rb-chispa { fill: var(--l-aviso); opacity: 0; }

/* ----- Movimiento -----
   `transform-box: fill-box` hace que el origen se resuelva contra la caja
   del propio elemento; sin él cada navegador lo resuelve donde quiere y el
   destornillador acaba girando alrededor de la esquina del lienzo. */
.rb-robot,
.rb-brazo,
.rb-herramienta,
.rb-ojos,
.rb-equipo,
.rb-chispa { transform-box: fill-box; }

/* Respiración: el robot no se queda tieso mientras trabaja. */
.rb-robot {
    transform-origin: 50% 100%;
    animation: rb-respirar 4.2s ease-in-out infinite;
}
@keyframes rb-respirar {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-1.5px); }
}

/* El brazo baja a apretar dos veces por vuelta y después descansa. */
.rb-brazo { animation: rb-apretar 4s ease-in-out infinite; }
@keyframes rb-apretar {
    0%, 8%    { transform: translateY(0); }
    16%       { transform: translateY(2.5px); }
    26%       { transform: translateY(0); }
    36%       { transform: translateY(2.5px); }
    46%, 100% { transform: translateY(0); }
}

/* El destornillador vuelve sobre su punta: es el gesto que dice
   «atornillar». El origen es la punta (50% 100% de su caja), no el mango. */
.rb-herramienta {
    transform-origin: 50% 100%;
    animation: rb-atornillar 4s ease-in-out infinite;
}
@keyframes rb-atornillar {
    0%, 8%    { transform: rotate(0deg); }
    16%       { transform: rotate(-20deg); }
    22%       { transform: rotate(8deg); }
    28%       { transform: rotate(0deg); }
    36%       { transform: rotate(-20deg); }
    42%       { transform: rotate(8deg); }
    48%, 100% { transform: rotate(0deg); }
}

/* La chispa salta EN el golpe, no antes ni después. */
.rb-chispa {
    transform-origin: 50% 50%;
    animation: rb-chispear 4s linear infinite;
}
@keyframes rb-chispear {
    0%, 14%   { opacity: 0; transform: scale(.4); }
    17%       { opacity: 1; transform: scale(1); }
    21%       { opacity: 0; transform: scale(.5); }
    34%       { opacity: 0; transform: scale(.4); }
    37%       { opacity: 1; transform: scale(1); }
    41%, 100% { opacity: 0; transform: scale(.5); }
}

/* El brazo que sujeta se tensa cuando el otro aprieta. Sin esto era un
   grupo sin papel: la mano quedaba clavada mientras el equipo se movia
   bajo ella, que es exactamente lo que no hace una mano que agarra. */
.rb-brazo-sujeta {
    transform-box: fill-box;
    transform-origin: 0% 0%;
    animation: rb-sujetar 4s ease-in-out infinite;
}
@keyframes rb-sujetar {
    0%, 14%, 21%, 34%, 41%, 100% { transform: translateY(0); }
    17%, 37%                     { transform: translateY(.8px); }
}

/* El equipo acusa el apriete. Un movimiento mayor lo despegaría del banco
   y parecería que flota. */
.rb-equipo { animation: rb-acusar 4s ease-in-out infinite; }
@keyframes rb-acusar {
    0%, 14%, 21%, 34%, 41%, 100% { transform: translateY(0); }
    17%, 37%                     { transform: translateY(1px); }
}

/* Piloto de la antena: el latido que dice «esto está encendido». */
.rb-luz { animation: rb-latir 1.9s ease-in-out infinite; }
@keyframes rb-latir {
    0%, 100% { opacity: 1; }
    50%      { opacity: .28; }
}

/* Parpadeo. Sin él la cara se queda mirando fijo, que es lo que separa un
   personaje de un icono. */
.rb-ojos {
    transform-origin: 50% 50%;
    animation: rb-parpadear 5.5s ease-in-out infinite;
}
@keyframes rb-parpadear {
    0%, 92%, 100% { transform: scaleY(1); }
    95%           { transform: scaleY(.12); }
}

/* La variante de móvil anima IGUAL que la grande. Se probó a dejarle sólo
   el latido de la antena por no competir con el teclado abierto, y el efecto
   fue el contrario del buscado: en el teléfono —desde donde entra media
   plantilla de un taller— el robot se veía muerto, justo donde más se nota
   que una pantalla está viva. Quien pide menos movimiento lo sigue teniendo
   apagado por la regla global de la sección 4. */

/* ---------------------------------------------------------------------
   8. Ajuste de los componentes de Bootstrap
   El puente de la sección 3 hace el grueso; aquí sólo queda la forma.
   --------------------------------------------------------------------- */
.card {
    border-radius: var(--l-radio);
    border-color: var(--l-borde);
}
/* Superficies quietas: borde de 1px, sin sombra. La elevación se reserva
   para lo que flota (menús, modales, lateral en móvil). */
.card-header {
    font-weight: 600;
    border-color: var(--l-borde);
}

/* Cabeceras de tabla: versalitas discretas, para que la fila de datos pese
   más que su etiqueta. */
.table > thead th {
    font-weight: 600;
    font-size: var(--l-texto-xs);
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--l-texto-2);
    white-space: nowrap;
    background: var(--l-superficie-2);
    border-bottom-color: var(--l-borde);
}
.table-light {
    --bs-table-bg: var(--l-superficie-2);
    --bs-table-color: var(--l-texto-2);
}

/* Tablas más densas: un taller con 400 órdenes quiere ver más filas de una
   vez, no más aire entre ellas. */
.table > :not(caption) > * > * { padding: .55rem .65rem; }
.table > tbody > tr > td { vertical-align: middle; }

/* Formularios */
.form-control, .form-select {
    background-color: var(--l-superficie);
    border-color: var(--l-borde);
    color: var(--l-texto);
}
.form-control:focus, .form-select:focus {
    background-color: var(--l-superficie);
    border-color: var(--l-acento);
    color: var(--l-texto);
    box-shadow: 0 0 0 .2rem var(--l-acento-suave);
}
.form-control::placeholder { color: var(--l-texto-3); }
.form-control:disabled, .form-select:disabled {
    background-color: var(--l-superficie-2);
    color: var(--l-texto-3);
}
.input-group-text {
    background-color: var(--l-superficie-2);
    border-color: var(--l-borde);
    color: var(--l-texto-2);
}
.form-label { font-size: var(--l-texto-md); font-weight: 500; color: var(--l-texto); }
.form-check-input:checked {
    background-color: var(--l-acento);
    border-color: var(--l-acento);
}

/* Botones */
.btn {
    font-size: var(--l-texto-md);
    font-weight: 500;
    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.btn-primary {
    --bs-btn-bg: var(--l-acento);
    --bs-btn-border-color: var(--l-acento);
    --bs-btn-color: var(--l-acento-sobre);
    --bs-btn-hover-bg: var(--l-acento-fuerte);
    --bs-btn-hover-border-color: var(--l-acento-fuerte);
    --bs-btn-hover-color: var(--l-acento-sobre);
    --bs-btn-active-bg: var(--l-acento-fuerte);
    --bs-btn-active-border-color: var(--l-acento-fuerte);
    --bs-btn-active-color: var(--l-acento-sobre);
    --bs-btn-disabled-bg: var(--l-acento);
    --bs-btn-disabled-border-color: var(--l-acento);
    --bs-btn-disabled-color: var(--l-acento-sobre);
}
.btn-outline-primary {
    --bs-btn-color: var(--l-acento-texto);
    --bs-btn-border-color: var(--l-acento-borde);
    --bs-btn-hover-bg: var(--l-acento);
    --bs-btn-hover-border-color: var(--l-acento);
    --bs-btn-hover-color: var(--l-acento-sobre);
    --bs-btn-active-bg: var(--l-acento-fuerte);
    --bs-btn-active-border-color: var(--l-acento-fuerte);
    --bs-btn-active-color: var(--l-acento-sobre);
}
.btn-light {
    --bs-btn-bg: var(--l-superficie-2);
    --bs-btn-border-color: var(--l-borde);
    --bs-btn-color: var(--l-texto);
    --bs-btn-hover-bg: var(--l-borde);
    --bs-btn-hover-border-color: var(--l-borde-fuerte);
    --bs-btn-hover-color: var(--l-texto);
}
.btn-outline-secondary {
    --bs-btn-color: var(--l-texto-2);
    --bs-btn-border-color: var(--l-borde-fuerte);
    --bs-btn-hover-bg: var(--l-superficie-2);
    --bs-btn-hover-border-color: var(--l-borde-fuerte);
    --bs-btn-hover-color: var(--l-texto);
    --bs-btn-active-bg: var(--l-superficie-2);
    --bs-btn-active-color: var(--l-texto);
}
/* `btn-outline-dark` quedaba invisible en tema oscuro (texto y borde negros
   sobre fondo negro). Con tokens sigue el tema solo. */
.btn-outline-dark {
    --bs-btn-color: var(--l-texto);
    --bs-btn-border-color: var(--l-borde-fuerte);
    --bs-btn-hover-bg: var(--l-texto);
    --bs-btn-hover-border-color: var(--l-texto);
    --bs-btn-hover-color: var(--l-superficie);
}

/* Enlaces y textos primarios: usar el tono con contraste verificado, no el
   color plano de marca — un taller amarillo tendría enlaces ilegibles. */
.text-primary { color: var(--l-acento-texto) !important; }
.link-primary { color: var(--l-acento-texto) !important; }
.bg-primary { background-color: var(--l-acento) !important; color: var(--l-acento-sobre); }
.badge.bg-primary { background-color: var(--l-acento) !important; color: var(--l-acento-sobre); }
.border-primary { border-color: var(--l-acento) !important; }

/* Pestañas */
.nav-tabs { border-bottom-color: var(--l-borde); }
.nav-tabs .nav-link {
    color: var(--l-texto-2);
    border: none;
    border-bottom: 2px solid transparent;
    padding: .6rem 1rem;
    font-weight: 500;
}
.nav-tabs .nav-link:hover { color: var(--l-texto); border-bottom-color: var(--l-borde-fuerte); }
.nav-tabs .nav-link.active {
    color: var(--l-acento-texto);
    background: transparent;
    border-bottom-color: var(--l-acento);
    font-weight: 600;
}
.nav-tabs .nav-link.disabled { opacity: .5; }
.tab-content { background: transparent; }

/* Panel de pestañas: una superficie continua con la fila de pestañas, para
   que se lea como una sola pieza y no como una caja colgando de otra. */
.l-tabs-panel {
    background: var(--l-superficie);
    border: 1px solid var(--l-borde);
    border-top: 0;
    border-radius: 0 0 var(--l-radio) var(--l-radio);
    padding: 1rem;
}

/* Insignias y avisos */
.badge { font-weight: 600; letter-spacing: .01em; }

.alert { border-radius: var(--l-radio); border-width: 1px; }
.alert-success { background-color: var(--l-ok-suave);      color: var(--l-ok-texto);      border-color: var(--l-ok-suave); }
.alert-info    { background-color: var(--l-info-suave);    color: var(--l-info-texto);    border-color: var(--l-info-suave); }
.alert-warning { background-color: var(--l-aviso-suave);   color: var(--l-aviso-texto);   border-color: var(--l-aviso-suave); }
.alert-danger  { background-color: var(--l-peligro-suave); color: var(--l-peligro-texto); border-color: var(--l-peligro-suave); }
.alert-secondary { background-color: var(--l-neutro-suave); color: var(--l-neutro-texto); border-color: var(--l-neutro-suave); }

/* Grupos de lista */
.list-group-item {
    background: var(--l-superficie);
    border-color: var(--l-borde);
    color: var(--l-texto);
}

/* Menús desplegables: esto sí flota, así que lleva sombra. */
.dropdown-menu {
    border-radius: var(--l-radio);
    box-shadow: var(--l-sombra);
    padding-block: .35rem;
}
.dropdown-item { border-radius: var(--l-radio-sm); font-size: var(--l-texto-md); }

/* Paginación */
.pagination { gap: 3px; }
.page-link {
    border-radius: var(--l-radio-sm);
    color: var(--l-texto-2);
    background: var(--l-superficie);
    border-color: var(--l-borde);
}
.page-link:hover { background: var(--l-superficie-2); color: var(--l-texto); }
.page-item.active .page-link {
    background: var(--l-acento);
    border-color: var(--l-acento);
    color: var(--l-acento-sobre);
}
/* Los «…» y las flechas de los extremos. El marcado antiguo no tenía estados
   deshabilitados —los pintaba cada vista a su manera— así que esto faltaba y
   caían al gris de Bootstrap sobre fondo blanco: en tema oscuro, un parche
   claro en medio de la barra. */
.page-item.disabled .page-link {
    background: transparent;
    border-color: transparent;
    color: var(--l-texto-3);
}
/* Bootstrap redondea sólo los extremos y junta los botones con un margen
   negativo, porque asume un grupo pegado. Aquí van sueltos (`gap`), así que
   todos se redondean igual: si no, el primero y el último salen con dos
   esquinas de un radio y dos de otro. */
.pagination .page-item:first-child .page-link,
.pagination .page-item:last-child .page-link {
    border-radius: var(--l-radio-sm);
}
.pagination .page-item:not(:first-child) .page-link { margin-left: 0; }

/* Modales: la otra cosa que flota de verdad. */
.modal-content { border-radius: var(--l-radio-lg); box-shadow: var(--l-sombra-lg); }

/* Acordeón (manual de usuario) */
.accordion-item {
    background: var(--l-superficie);
    border-color: var(--l-borde);
}
.accordion-button {
    background: var(--l-superficie);
    color: var(--l-texto);
    font-weight: 600;
    /* Bootstrap lo hace flex sin envolver, así que un título con su píldora
       ("Caja · Solo administrador") se salía 30px de una pantalla de 320px. */
    flex-wrap: wrap;
    gap: .35rem;
}
.accordion-button:not(.collapsed) {
    background: var(--l-acento-suave);
    color: var(--l-acento-texto);
    box-shadow: none;
}
.accordion-button:focus { box-shadow: 0 0 0 3px var(--l-acento-suave); }
.accordion-body { background: var(--l-superficie); color: var(--l-texto); }

hr { border-color: var(--l-borde); opacity: 1; }

/* ---------------------------------------------------------------------
   8b. Puente de las utilidades semánticas de Bootstrap

   Las vistas usan `text-muted`, `text-success`, `bg-warning-subtle`… en 313
   sitios. Reescribir todos esos puntos de llamada sería mucho riesgo para
   poco beneficio, así que se REDIRIGEN a los tokens: la clase se queda donde
   está y pasa a seguir el tema y la paleta del producto.

   Por qué hacía falta: los colores de Bootstrap 5.3 no se recalculan en
   oscuro (un `text-success` verde oscuro sobre fondo negro es ilegible) y su
   `primary` es el color de marca del taller, así que teñía de la marca cosas
   que no son de marca.

   Llevan `!important` porque las utilidades de Bootstrap también lo llevan;
   esta hoja se carga después, así que gana.

   Al escribir una vista NUEVA, usa los componentes `.l-*` de la sección 6b,
   no estas clases: esto es una red de seguridad para el código existente.
   --------------------------------------------------------------------- */
.text-muted,
.text-secondary   { color: var(--l-texto-2) !important; }
.text-success     { color: var(--l-ok-texto) !important; }
.text-danger      { color: var(--l-peligro-texto) !important; }
.text-warning,
.text-warning-emphasis { color: var(--l-aviso-texto) !important; }
.text-info        { color: var(--l-info-texto) !important; }
.text-dark        { color: var(--l-texto) !important; }
.text-body        { color: var(--l-texto) !important; }

/* Rellenos suaves (fondos de tarjeta de aviso, iconos de indicador). */
.bg-primary-subtle   { background-color: var(--l-acento-suave) !important; }
.bg-success-subtle   { background-color: var(--l-ok-suave) !important; }
.bg-danger-subtle    { background-color: var(--l-peligro-suave) !important; }
.bg-warning-subtle   { background-color: var(--l-aviso-suave) !important; }
.bg-info-subtle      { background-color: var(--l-info-suave) !important; }
.bg-secondary-subtle { background-color: var(--l-neutro-suave) !important; }
.bg-light,
.bg-body-secondary   { background-color: var(--l-superficie-2) !important; }
.bg-body             { background-color: var(--l-superficie) !important; }

.border-primary-subtle   { border-color: var(--l-acento-borde) !important; }
.border-success-subtle   { border-color: var(--l-ok-suave) !important; }
.border-danger-subtle    { border-color: var(--l-peligro-suave) !important; }
.border-warning-subtle   { border-color: var(--l-aviso-suave) !important; }
.border-info-subtle      { border-color: var(--l-info-suave) !important; }

/* Rellenos sólidos. Se usan casi siempre en `badge`, así que se les da el
   mismo tratamiento que a una píldora: relleno suave y tinta del tono. Un
   badge negro (`bg-dark`) o amarillo saturado pesaba más que el dato al que
   acompañaba y ensuciaba la tabla. */
.badge.bg-success   { background-color: var(--l-ok-suave) !important;      color: var(--l-ok-texto) !important; }
.badge.bg-danger    { background-color: var(--l-peligro-suave) !important; color: var(--l-peligro-texto) !important; }
.badge.bg-warning   { background-color: var(--l-aviso-suave) !important;   color: var(--l-aviso-texto) !important; }
.badge.bg-info      { background-color: var(--l-info-suave) !important;    color: var(--l-info-texto) !important; }
.badge.bg-secondary { background-color: var(--l-neutro-suave) !important;  color: var(--l-neutro-texto) !important; }
.badge.bg-dark      { background-color: var(--l-neutro-suave) !important;  color: var(--l-neutro-texto) !important; }
.badge.bg-light     { background-color: var(--l-superficie-2) !important;  color: var(--l-texto-2) !important; }
/* `bg-warning text-dark` es un modismo muy repetido en las vistas: el
   `text-dark` no debe ganarle a la tinta del tono. */
.badge.bg-warning.text-dark { color: var(--l-aviso-texto) !important; }
.badge { padding: .28rem .55rem; border-radius: var(--l-radio-full); }

/* Fuera de un badge, el relleno sólido sí es sólido (barras, puntos). */
.bg-success   { background-color: var(--l-ok) !important; }
.bg-danger    { background-color: var(--l-peligro) !important; }
.bg-warning   { background-color: var(--l-aviso) !important; }
.bg-info      { background-color: var(--l-info) !important; }
.bg-secondary { background-color: var(--l-neutro) !important; }

/* Botones de contorno en tonos (Exportar CSV, acciones destructivas). */
.btn-outline-success {
    --bs-btn-color: var(--l-ok-texto);
    --bs-btn-border-color: var(--l-borde-fuerte);
    --bs-btn-hover-bg: var(--l-ok-suave);
    --bs-btn-hover-border-color: var(--l-ok);
    --bs-btn-hover-color: var(--l-ok-texto);
    --bs-btn-active-bg: var(--l-ok-suave);
    --bs-btn-active-color: var(--l-ok-texto);
    --bs-btn-active-border-color: var(--l-ok);
}
.btn-outline-danger {
    --bs-btn-color: var(--l-peligro-texto);
    --bs-btn-border-color: var(--l-borde-fuerte);
    --bs-btn-hover-bg: var(--l-peligro-suave);
    --bs-btn-hover-border-color: var(--l-peligro);
    --bs-btn-hover-color: var(--l-peligro-texto);
    --bs-btn-active-bg: var(--l-peligro-suave);
    --bs-btn-active-color: var(--l-peligro-texto);
    --bs-btn-active-border-color: var(--l-peligro);
}
.btn-danger {
    --bs-btn-bg: var(--l-peligro);
    --bs-btn-border-color: var(--l-peligro);
    --bs-btn-hover-bg: var(--l-peligro-texto);
    --bs-btn-hover-border-color: var(--l-peligro-texto);
}
.btn-success {
    --bs-btn-bg: var(--l-ok);
    --bs-btn-border-color: var(--l-ok);
    --bs-btn-hover-bg: var(--l-ok-texto);
    --bs-btn-hover-border-color: var(--l-ok-texto);
}

/* ---------------------------------------------------------------------
   9. Responsividad (móvil/tablet) — aplica en navegador y en PWA
   --------------------------------------------------------------------- */
@media (max-width: 575.98px) {
    /* Más aire útil: menos padding en el contenido. */
    .content.p-3, .content { padding: .85rem !important; }

    /* Tarjeta de bienvenida apilada y compacta. */
    .welcome-card { flex-direction: column; align-items: flex-start; gap: .5rem; padding: 1.15rem; }

    /* KPIs un poco más compactos para que quepan 2 por fila cómodos. */
    .kpi-card { padding: .8rem; gap: .6rem; }
    .kpi-icon { width: 38px; height: 38px; font-size: 1.1rem; }
    .kpi-value { font-size: 1.3rem; }
    .kpi-label { font-size: var(--l-texto-xs); }

    /* Tablas más densas en pantallas pequeñas. */
    .table { font-size: var(--l-texto-md); }
    .table > :not(caption) > * > * { padding: .5rem .5rem; }

    /* Títulos de sección sin desbordar. */
    h1, .h1 { font-size: 1.5rem; }
    h2, .h2 { font-size: 1.3rem; }

    /* Modales pegados a los bordes con un respiro. */
    .modal-dialog { margin: .5rem; }

    /* Botones agrupados que se envuelven en vez de cortarse. */
    .btn-group-wrap { flex-wrap: wrap; gap: .4rem; }
}

/* Objetivos táctiles.
   Los botones de sólo icono de las columnas de acciones medían 28x31px: por
   debajo del mínimo cómodo para un dedo, y son justo los que más se pulsan
   (ver, editar, imprimir, anular). La regla anterior apuntaba a `.table`, que
   dejó de existir al pasar las tablas a `.l-tabla` — de ahí que se hubieran
   quedado pequeños sin que se notara.
   Se aplica siempre, no sólo en `pointer: coarse`: un objetivo de 36px
   tampoco molesta con ratón, y así el tamaño no depende de que el navegador
   informe bien del tipo de puntero (los híbridos táctil+ratón mienten). */
.l-tabla .btn-sm,
.l-tabla-apila .btn-sm,
.l-cab-acciones .btn-sm {
    min-width: 34px;
    min-height: 34px;
}

@media (pointer: coarse) {
    /* 44px es el mínimo cómodo para un dedo. `btn-sm` se quedaba en 31px de
       alto: suficiente con ratón, incómodo con la mano grasienta de quien
       acaba de destripar una impresora. Se aplica a TODOS los botones, no
       sólo a los de las tablas — `padding-block` solo no llegaba a 44. */
    .btn, .btn-sm, .btn.btn-sm {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    /* Los botones de sólo icono también necesitan ancho. */
    .l-tabla .btn-sm, .l-tabla-apila .btn-sm, .l-cab-acciones .btn-sm {
        min-width: 44px;
    }
    /* Un botón que en realidad es un enlace de texto no debe inflarse. */
    .btn-link { min-height: 0; display: inline; }

    .l-icono { width: 44px; height: 44px; }
    .form-control, .form-select { min-height: 44px; }
    .form-control-sm, .form-select-sm { min-height: 40px; }
    .sidebar .nav-link { padding-block: .7rem; }
    /* El encabezado de sección pasa a ser pulsable: con el acolchado de
       escritorio se queda en ~34px, por debajo del mínimo cómodo para un
       dedo. No entra en la regla de `.btn` de arriba porque no es un `.btn`. */
    button.nav-section { min-height: 44px; }
}

/* ---------------------------------------------------------------------
   10. Impresión (factura/recibo A5)
   --------------------------------------------------------------------- */
@media print {
    @page { margin: 8mm; }
    body { background: #fff !important; color: #000 !important; }
    .sidebar, .topbar, .sidebar-backdrop, .no-print, .btn, nav.nav-tabs,
    .alert-dismissible, #dropdownNotif, .pwa-tabbar { display: none !important; }
    .main { margin-left: 0 !important; }
    .content { padding: 0 !important; }
    .card { border: 1px solid #ccc !important; box-shadow: none !important; page-break-inside: avoid; }
    a[href]:after { content: ""; }
}
