/*
 * Sistema de diseño propio de ServiceDesk IT (regla 44/45): sin frameworks CSS, sin librerías de
 * iconos externas. Variables (custom properties) para que cada tenant pueda personalizar color
 * primario/secundario sin duplicar hojas de estilo (regla 46): ver bloque <style> inline generado
 * por Views/layouts/base.php a partir de tenants.brand_color_primary/secondary.
 *
 * Paleta "Serio y empresarial" (regla 101, rediseño 2026-09-26): el propietario pidió una revisión
 * completa del modo claro por contraste ("hay cosas que no se ven bien, no me gustan los
 * contrastes"). Se generaron dos propuestas completas ("Serio y empresarial" / "Alegre y moderno")
 * como comparador interactivo (artefacto), con el contraste de cada par texto/fondo verificado
 * programáticamente contra WCAG AA (≥4.5:1 texto normal, ≥3:1 texto grande/componentes de UI) antes
 * de mostrarlas — el propietario eligió "Serio y empresarial". Sustituye la paleta "Minimalista
 * corporativo" del rediseño anterior (2026-09-07) — ver CLAUDE.md, sección "Paleta 'Serio y
 * empresarial' (modo claro)", para el detalle completo de cada valor y su verificación de contraste.
 * Cambio SOLO en el modo claro — el modo oscuro (`:root[data-theme="dark"]` y su `@media`
 * equivalente, más abajo) no cambia de aspecto, tal como pidió el propietario ("hablo del modo día no
 * del noche"), aunque sí gana dos líneas nuevas explicadas donde corresponde para blindarlo del
 * cambio en el sidebar (ver comentario junto a `--color-sidebar-bg` más abajo).
 */

:root {
    --color-primary: #1f4e8c;
    --color-primary-dark: #163a68;
    --color-secondary: #16202e;

    --color-bg: #eef1f6;
    --color-surface: #ffffff;
    --color-surface-muted: #f4f6f9;
    --color-text: #16202e;
    --color-text-muted: #5b6b82;
    --color-border: #d7dde6;
    --color-border-strong: #b7c2d1;

    --color-danger: #a3271f;
    --color-danger-bg: #f7e8e7;
    --color-success: #1f6b46;
    --color-success-bg: #e6f2ec;
    --color-warning: #8a5a12;
    --color-warning-bg: #f7eeda;

    /* Tercer escalón de aviso, deliberadamente amarillo y distinto de `--color-warning` (naranja):
       petición del propietario, 2026-10-03, para el aviso de vencimiento próximo en "Tareas"
       (`.task-row--due-soon`/`.badge[data-task-due="soon"]`) — pidió expresamente tres colores
       (amarillo/naranja/rojo), no dos. Contraste verificado ≥4.5:1 texto/fondo en claro (≈5.25:1) y
       en oscuro (≈7.16:1, ver bloque `:root[data-theme="dark"]`), mismo criterio de verificación que
       el resto de la paleta "Serio y empresarial" (regla 43/54). */
    --color-caution: #7a6500;
    --color-caution-bg: #fdf6d6;

    /* Fondo de FILA del aviso de vencimiento próximo en "Tareas" — tres valores exactos pedidos por
       el propietario (2026-10-03, segunda vuelta: "no me gustan los fondos" con los tonos suaves de
       `--color-caution-bg`/`--color-warning-bg`/`--color-danger-bg` de arriba, dio directamente estos
       tres: #ffee8a / #ffb836 / #ff5b50). Deliberadamente NO se reutilizan esos otros tokens: son
       compartidos por muchos badges de toda la aplicación (prioridad alta, bono bajo, etc. — mismo
       motivo que `--color-task-accent` más abajo) y cambiar su valor los habría afectado a todos.
       Deliberadamente tokens APARTE, definidos una sola vez aquí y SIN redefinir en los bloques de
       modo oscuro (`:root[data-theme="dark"]` y su `@media` equivalente): el propietario dio tres
       colores concretos, no un par claro/oscuro, así que se mantienen idénticos en ambos temas (una
       custom property no redefinida en el bloque oscuro conserva el valor de este `:root`). Por eso
       el texto/enlaces DENTRO de estas filas se fuerza a `#16202e` (ver `.task-row--due-*` más abajo)
       en vez de depender de `--color-text`/`--color-primary`: esos sí cambian con el tema y en modo
       oscuro quedan claros, ilegibles sobre un fondo amarillo/naranja/rojo que no cambia. Contraste
       de `#16202e` verificado ≥4.5:1 contra los tres fondos (el más exigente, #ff5b50, ≈5.36:1). */
    --color-task-due-soon-bg: #ffee8a;
    --color-task-due-warning-bg: #ffb836;
    --color-task-due-critical-bg: #ff5b50;

    /* Acento naranja EXCLUSIVO del borde de tareas (petición del propietario, 2026-09-25: "cámbiame
       el color del borde de tareas... a algo más naranja como #df7f24, ahora está en #8a6217 que es
       casi marrón"). Deliberadamente NO se reutiliza `--color-warning` para esto: ese token es
       compartido por muchos badges (prioridad alta, pendiente cliente, bono bajo, etc.) y cambiar su
       valor habría afectado a todos ellos, cuando el propietario solo pidió el borde de tareas.
       Se mantiene SIN CAMBIOS en este rediseño "Serio y empresarial" (2026-09-26): es una decisión
       explícita y reciente del propietario, independiente de la revisión general de contraste, y
       cambiarla ahora la habría revertido sin que lo pidiera. Ver `.dashboard-queue--tasks` más
       abajo. */
    --color-task-accent: #df7f24;

    --radius-sm: 6px;
    --radius-md: 8px;
    --shadow-sm: 0 1px 2px rgba(22, 24, 29, 0.05), 0 1px 1px rgba(22, 24, 29, 0.02);
    --shadow-md: 0 4px 16px rgba(22, 24, 29, 0.07);

    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* Dashboard (regla 101): sidebar + cabecera. La paleta "Serio y empresarial" (2026-09-26)
       cambia el sidebar de claro (fondo blanco, dirección "Minimalista corporativo" de 2026-09-07) a
       un panel azul-marino casi negro — más peso visual y jerarquía marcada, look de "consola de
       gestión" — SOLO en modo claro. Antes, `--color-sidebar-bg`/`--color-sidebar-text-strong` eran
       `var(--color-surface)`/`var(--color-text)` (se "recalculaban solos" con el tema activo, sin
       necesidad de redefinirlas en el bloque de modo oscuro); ahora que en modo claro llevan un
       valor propio distinto de `--color-surface`/`--color-text`, el modo oscuro necesita las mismas
       dos líneas explícitas para seguir viéndose exactamente igual que antes (ver
       `:root[data-theme="dark"]` y su equivalente `@media` más abajo) — cero cambio visual en oscuro,
       solo se hace explícito lo que antes era automático. `--color-sidebar-border` es una variable
       nueva (antes el sidebar tomaba prestado `--color-border` del resto de la página, que en modo
       claro pasa a ser un gris claro poco visible contra un fondo casi negro); en modo oscuro se
       define como `var(--color-border)`, exactamente el valor que ya usaba, para no cambiar nada
       ahí tampoco. */
    --sidebar-width: 240px;
    --sidebar-width-collapsed: 72px;
    --color-sidebar-bg: #101826;
    --color-sidebar-text: #a9b6cc;
    --color-sidebar-text-strong: #ffffff;
    --color-sidebar-hover-bg: #1b2536;
    --color-sidebar-border: #1f2b3f;
    --header-height: 64px;
}

* {
    box-sizing: border-box;
}

html, body {
    height: 100%;
}

/* Botones flotantes "ir al principio/final" (petición del propietario, 2026-09-10: incidencias muy
   largas). El desplazamiento suave es solo una mejora visual sobre los enlaces `#page-top`/
   `#page-bottom` — sin esta regla, los mismos enlaces siguen funcionando exactamente igual, solo que
   el salto es instantáneo en vez de animado (regla 47/48). Respeta la preferencia de movimiento
   reducido del sistema operativo. */
html {
    scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

body {
    margin: 0;
    font-family: var(--font-sans);
    color: var(--color-text);
    background: var(--color-bg);
    font-size: 15px;
    line-height: 1.5;
}

h1, h2, h3, p {
    margin: 0 0 0.5em;
}

a {
    color: var(--color-primary);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

img {
    max-width: 100%;
    display: block;
}

/* ---------- Formularios ---------- */

.field {
    margin-bottom: 1.1rem;
}

.field label {
    display: block;
    font-size: 0.875rem;
    font-weight: 600;
    margin-bottom: 0.35rem;
    color: var(--color-text);
}

/* Varios campos en la misma fila cuando el ancho lo permite (regla 43/101: formularios largos
   como "Nueva incidencia" resultaban excesivamente verticales con un campo por fila). auto-fit
   con minmax hace que el número de columnas se adapte solo al ancho disponible y al número de
   campos realmente visibles en cada momento — importante porque varios de estos campos son
   condicionales por tenant (site/department/affected_user/impact/category, ver
   TicketFieldSettingsService) y no siempre están todos presentes a la vez. Colapsa a una sola
   columna de forma natural en móvil, sin necesidad de un breakpoint explícito. */
.field-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0 1.25rem;
}

.field-row .field {
    min-width: 0;
}

/* Texto de ayuda bajo un campo (p. ej. límite de caracteres del asunto — petición del propietario
   2026-09-25). Deliberadamente estático y sin JS: un contador en vivo añadiría una dependencia de
   comportamiento innecesaria para un simple recordatorio del maxlength ya impuesto por el navegador
   y validado de nuevo en servidor (TicketService::MAX_SUBJECT_LENGTH). */
.field-hint {
    display: block;
    margin-top: 0.3rem;
    font-size: 0.78rem;
    color: var(--color-text-muted);
}

.input {
    width: 100%;
    padding: 0.6rem 0.75rem;
    font-size: 0.95rem;
    font-family: inherit;
    color: var(--color-text);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.input:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}

/* ---------- Botones ---------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    width: 100%;
    padding: 0.65rem 1rem;
    font-size: 0.95rem;
    font-weight: 600;
    font-family: inherit;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}

.btn-primary {
    background: var(--color-primary);
    color: #fff;
}

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

.btn-secondary {
    background: var(--color-surface);
    color: var(--color-text);
    border-color: var(--color-border);
}

.btn-secondary:hover {
    background: var(--color-bg);
}

.btn-danger {
    background: var(--color-danger-bg);
    color: var(--color-danger);
    border-color: color-mix(in srgb, var(--color-danger) 30%, transparent);
}

.btn-danger:hover {
    background: var(--color-danger);
    color: #fff;
}

.btn-link {
    width: auto;
    padding: 0;
    background: none;
    color: var(--color-primary);
    font-weight: 500;
}

/* ---------- Tarjetas / alertas ---------- */

.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: 1.75rem;
}

.alert {
    padding: 0.75rem 1rem;
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    margin-bottom: 1.1rem;
}

.alert-error {
    background: var(--color-danger-bg);
    color: var(--color-danger);
    border: 1px solid color-mix(in srgb, var(--color-danger) 25%, transparent);
}

.alert-success {
    background: var(--color-success-bg);
    color: var(--color-success);
    border: 1px solid color-mix(in srgb, var(--color-success) 25%, transparent);
}

.alert-warning {
    background: var(--color-warning-bg);
    color: var(--color-warning);
    border: 1px solid color-mix(in srgb, var(--color-warning) 25%, transparent);
}

/* ---------- Pantallas de autenticación (login/landing) ---------- */

.auth-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem 1rem;
}

.auth-shell__box {
    width: 100%;
    max-width: 380px;
}

.auth-shell__brand {
    text-align: center;
    margin-bottom: 1.5rem;
}

.auth-shell__brand img {
    max-height: 56px;
    margin: 0 auto 0.75rem;
}

/* Icono de la aplicación encima del logo del cliente, en columna y centrados (2026-10-16). Los
   márgenes de las imágenes los lleva el contenedor (gap), no cada imagen. */
.auth-shell__logos {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

.auth-shell__brand .auth-shell__logos img {
    margin: 0;
}

.auth-shell__appicon {
    width: 44px;
    height: 44px;
    flex: 0 0 auto;
    border-radius: 10px;
}

.auth-shell__tenant-logo {
    max-width: 100%;
    object-fit: contain;
}

.auth-shell__brand h1 {
    font-size: 1.15rem;
    font-weight: 700;
}

.auth-shell__welcome {
    color: var(--color-text-muted);
    font-size: 0.9rem;
    text-align: center;
    margin-bottom: 1.5rem;
}

.auth-shell__footer {
    text-align: center;
    margin-top: 1.25rem;
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

/* ---------- Dashboard shell: sidebar + cabecera + contenido (regla 101) ---------- */

.app-shell {
    display: flex;
    align-items: stretch;
    min-height: 100vh;
}

.sidebar-toggle-input {
    display: none;
}

.sidebar-backdrop {
    display: none;
}

.sidebar-collapse-input {
    display: none;
}

.sidebar {
    width: var(--sidebar-width);
    flex: 0 0 var(--sidebar-width);
    /* Fijo al hacer scroll (petición del propietario, 2026-10-01: "no tiene mucho sentido que, al
       descender, perdamos los elementos, si hay mucho espacio libre") — antes era `min-height: 100vh`
       sin `position`, así que al crecer `.app-content` más que la ventana, el sidebar entero (que se
       estira para igualar la altura de `.app-shell`, su hermano) se desplazaba junto con el resto de
       la página. `position: sticky` lo ancla al viewport mientras se hace scroll; `overflow-y: auto`
       es una red de seguridad para el caso (raro) de que el propio contenido del sidebar sea más alto
       que la ventana (muchos enlaces de menú), en vez de recortarlo. Solo aplica en escritorio: la
       media query de 960px de abajo lo sustituye por el patrón "cajón" (`position: fixed`,
       siempre fuera de pantalla salvo al abrirlo) ya existente, que no tiene este problema porque
       nunca se desplaza con el contenido. */
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    background: var(--color-sidebar-bg);
    color: var(--color-sidebar-text);
    border-right: 1px solid var(--color-sidebar-border);
    display: flex;
    flex-direction: column;
    padding: 1.25rem 0.9rem;
}

.sidebar__brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0.5rem 1.25rem;
    color: var(--color-sidebar-text-strong);
    font-weight: 700;
    font-size: 1rem;
}

.sidebar__brand:hover {
    text-decoration: none;
}

.sidebar__brand img {
    max-height: 28px;
    border-radius: var(--radius-sm);
}

.sidebar__brand-mark {
    display: inline-flex;
    width: 28px;
    height: 28px;
    color: var(--color-primary);
}

.sidebar__brand-mark svg, .sidebar__icon svg {
    width: 100%;
    height: 100%;
}

/* Colapsar sidebar en escritorio (regla 47/48/101, ampliación 2026-09-08): botón visible solo a
   partir de 961px — en móvil el sidebar ya se oculta/muestra por completo con el toggle existente
   (.sidebar-toggle-input), un colapso a iconos no aporta nada ahí. */
.sidebar__collapse-toggle {
    display: none;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0.7rem;
    margin-bottom: 0.6rem;
    border-radius: var(--radius-sm);
    color: var(--color-sidebar-text);
    font-size: 0.82rem;
    font-weight: 500;
    cursor: pointer;
}

.sidebar__collapse-toggle:hover {
    background: var(--color-sidebar-hover-bg);
    color: var(--color-sidebar-text-strong);
}

.sidebar__collapse-icon {
    display: inline-flex;
    width: 16px;
    height: 16px;
    flex: 0 0 16px;
    transition: transform 0.15s ease;
}

@media (min-width: 961px) {
    .sidebar__collapse-toggle {
        display: flex;
    }
}

.sidebar__nav {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    flex: 1 1 auto;
}

.sidebar__link {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.7rem;
    border-radius: var(--radius-sm);
    color: var(--color-sidebar-text);
    font-weight: 500;
    font-size: 0.92rem;
}

.sidebar__link:hover {
    background: var(--color-sidebar-hover-bg);
    color: var(--color-sidebar-text-strong);
    text-decoration: none;
}

.sidebar__link.is-active {
    background: var(--color-primary);
    color: #fff;
}

.sidebar__icon {
    display: inline-flex;
    width: 19px;
    height: 19px;
    flex: 0 0 19px;
}

.sidebar__identity {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.75rem 0.5rem 0.4rem;
    margin-top: 0.75rem;
    border-top: 1px solid var(--color-sidebar-border);
}

.sidebar__avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    border-radius: 50%;
    background: var(--color-primary);
    color: #fff;
    font-weight: 700;
    font-size: 0.9rem;
}

.sidebar__identity-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
    font-size: 0.82rem;
}

.sidebar__identity-text strong {
    color: var(--color-sidebar-text-strong);
    font-size: 0.85rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar__identity-text span {
    color: var(--color-sidebar-text);
}

.sidebar__logout {
    flex: 0 0 auto;
}

.sidebar__logout-btn {
    display: inline-flex;
    width: 32px;
    height: 32px;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-sidebar-text);
    cursor: pointer;
}

.sidebar__logout-btn:hover {
    background: var(--color-sidebar-hover-bg);
    color: var(--color-sidebar-text-strong);
}

.sidebar__logout-btn svg {
    width: 17px;
    height: 17px;
}

.app-content {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.app-header {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    min-height: var(--header-height);
    padding: 0.75rem 1.5rem;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    position: sticky;
    top: 0;
    z-index: 20;
}

.app-header__toggle {
    display: none;
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-sm);
    color: var(--color-text);
    cursor: pointer;
}

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

.app-header__toggle svg {
    width: 20px;
    height: 20px;
}

.app-header__search {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.app-header__search-icon {
    display: none;
}

.app-header__search-input {
    flex: 1 1 220px;
    min-width: 140px;
    padding: 0.55rem 1rem;
    font-size: 0.9rem;
    font-family: inherit;
    color: var(--color-text);
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 999px;
}

.app-header__search-input:focus {
    outline: none;
    border-color: var(--color-primary);
    background: var(--color-surface);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}

.app-header__filter {
    width: auto;
    flex: 0 0 auto;
}

.app-header__search-submit {
    flex: 0 0 auto;
}

/* "Quitar filtros" (regla 101, petición del propietario 2026-09-30) — enlace de texto, no botón, para
   no competir visualmente con "Buscar"; reutiliza .btn-link (ya existente, sin uso hasta ahora). */
.app-header__search-clear {
    flex: 0 0 auto;
    white-space: nowrap;
    font-size: 0.85rem;
}

.app-header__cta {
    flex: 0 0 auto;
    white-space: nowrap;
}

.app-header__cta svg {
    width: 16px;
    height: 16px;
}

.app-main {
    flex: 1 1 auto;
    width: 100%;
    max-width: 1400px;
    margin: 0 auto;
    padding: 1.75rem 2rem 3rem;
}

@media (max-width: 960px) {
    .sidebar {
        /* Vuelve a `top`/`bottom` en vez del `height: 100vh` fijo de escritorio (ver comentario de la
           regla base `.sidebar`): en algunos navegadores móviles `100vh` no descuenta la barra de
           direcciones, y aquí ya no hace falta — este patrón "cajón" siempre está fuera de pantalla
           salvo al abrirlo, nunca se desplaza con el scroll de la página. */
        height: auto;
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 100;
        transform: translateX(-100%);
        transition: transform 0.2s ease;
        box-shadow: var(--shadow-md);
    }

    .sidebar-toggle-input:checked ~ .sidebar {
        transform: translateX(0);
    }

    .sidebar-toggle-input:checked ~ .sidebar-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        background: rgba(15, 23, 42, 0.45);
        z-index: 90;
    }

    .app-header__toggle {
        display: inline-flex;
    }

    .app-header {
        padding: 0.75rem 1rem;
        flex-wrap: wrap;
    }

    .app-header__search {
        order: 3;
        flex-basis: 100%;
    }

    .app-main {
        padding: 1.25rem 1rem 2.5rem;
    }
}

/* 2026-11-13: antes el botón «Buscar» se ocultaba aquí (bastaba el Enter del teclado), pero con los
   filtros desplegables (estado, prioridad, fechas…) en móvil no había forma de aplicarlos. Ahora se
   muestra siempre, a todo el ancho y con altura táctil. */
@media (max-width: 620px) {
    .app-header__search-submit {
        flex: 1 1 100%;
        min-height: 2.75rem;
    }

    .app-header__search-clear {
        flex: 1 1 100%;
        text-align: center;
    }
}

/* Sidebar colapsado a iconos, solo escritorio (regla 47/48/101, ampliación 2026-09-08). Mismo
   patrón checkbox+label sin JavaScript ya usado por el toggle móvil (.sidebar-toggle-input) —
   .sidebar-collapse-input es hermano de .sidebar/.app-content dentro de .app-shell, así que el
   combinador ~ alcanza a ambos. Envuelto en min-width para no interferir nunca con el toggle móvil
   de 960px, que resuelve un problema distinto (mostrar/ocultar el sidebar entero, no reducirlo). */
@media (min-width: 961px) {
    .sidebar-collapse-input:checked ~ .sidebar {
        width: var(--sidebar-width-collapsed);
        flex: 0 0 var(--sidebar-width-collapsed);
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }

    .sidebar-collapse-input:checked ~ .sidebar .sidebar__brand-name,
    .sidebar-collapse-input:checked ~ .sidebar .sidebar__link span:not(.sidebar__icon),
    .sidebar-collapse-input:checked ~ .sidebar .sidebar__collapse-label,
    .sidebar-collapse-input:checked ~ .sidebar .sidebar__identity-text {
        display: none;
    }

    .sidebar-collapse-input:checked ~ .sidebar .sidebar__brand {
        justify-content: center;
        padding-left: 0;
        padding-right: 0;
    }

    .sidebar-collapse-input:checked ~ .sidebar .sidebar__link {
        justify-content: center;
        gap: 0;
    }

    .sidebar-collapse-input:checked ~ .sidebar .sidebar__collapse-toggle {
        justify-content: center;
    }

    .sidebar-collapse-input:checked ~ .sidebar .sidebar__collapse-icon {
        transform: rotate(180deg);
    }

    .sidebar-collapse-input:checked ~ .sidebar .sidebar__identity {
        flex-direction: column;
        gap: 0.5rem;
        padding-left: 0;
        padding-right: 0;
    }
}

/* ---------- Rejilla de tarjetas de navegación (dashboards, regla 101) ---------- */

.ticket-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1rem;
}

/* ---------- Listado de incidencias EN TABLA (regla 101, cambiado 2026-08-22: con muchas
   incidencias una rejilla de tarjetas no se lee bien — el propietario pidió lista). Reutiliza
   .table-wrap/.table (mismo componente que el resto de listados administrativos de la aplicación,
   ver p.ej. staff/admin/tenants/index.php) en vez de inventar un componente nuevo. ---------- */

.ticket-table__number {
    font-weight: 600;
    color: var(--color-text-muted);
    white-space: nowrap;
}

.ticket-table__subject {
    font-weight: 600;
    color: var(--color-text);
}

.ticket-table__number:hover,
.ticket-table__subject:hover {
    color: var(--color-primary);
    text-decoration: none;
}

/* Por debajo de este ancho la tabla de incidencias pasa a "tarjetas apiladas": cada <tr> se
   convierte en un bloque y cada celda muestra su etiqueta de columna (data-label) delante del
   valor, en vez de obligar a desplazar la tabla en horizontal en un móvil estrecho. */
@media (max-width: 720px) {
    .ticket-table thead {
        display: none;
    }

    .ticket-table, .ticket-table tbody, .ticket-table tr, .ticket-table td {
        display: block;
        width: 100%;
    }

    .ticket-table tr {
        padding: 0.75rem 1rem;
        border-bottom: 1px solid var(--color-border);
    }

    .ticket-table td {
        border-bottom: none;
        padding: 0.2rem 0;
        white-space: normal;
    }

    .ticket-table td[data-label]::before {
        content: attr(data-label) ": ";
        font-size: 0.72rem;
        text-transform: uppercase;
        letter-spacing: 0.02em;
        color: var(--color-text-muted);
        font-weight: 600;
    }

    .ticket-table td:first-child {
        padding-top: 0;
    }

    /* En la vista de tarjetas apiladas ya no hay una columna estrecha que proteger, y el móvil no
       tiene hover para el tooltip del title — así que aquí se deshace el truncado y se vuelve a
       envolver el texto completo (mismo criterio que .table-cell-wrap). */
    .ticket-table td.table-cell-truncate {
        max-width: none;
        overflow: visible;
        text-overflow: unset;
        white-space: normal;
    }
}

.badge {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.quick-link {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    color: var(--color-text);
    transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}

.quick-link:hover {
    text-decoration: none;
    box-shadow: var(--shadow-md);
    border-color: color-mix(in srgb, var(--color-primary) 30%, var(--color-border));
    transform: translateY(-1px);
}

.quick-link strong {
    font-size: 1rem;
    color: var(--color-text);
}

.quick-link span {
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

.empty-state {
    text-align: center;
    color: var(--color-text-muted);
    padding: 3rem 1rem;
}

/* ---------- Estados / prioridades de ticket ---------- */

.badge[data-status="open"] { color: #a13a3a; background: #fdf1f1; border-color: #f3d4d4; }
.badge[data-status="in_progress"] { color: #3e48a8; background: #eef0fb; border-color: #d8dcf5; }
.badge[data-status="pending_customer"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-status="resolved"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-status="closed"] { color: var(--color-text-muted); background: var(--color-bg); border-color: var(--color-border); }

.badge[data-priority="low"] { color: var(--color-text-muted); background: var(--color-bg); border-color: var(--color-border); }
.badge[data-priority="normal"] { color: #2f5fb0; background: #eef3fb; border-color: #d3e0f5; }
.badge[data-priority="high"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-priority="urgent"] { color: var(--color-danger); background: var(--color-danger-bg); border-color: color-mix(in srgb, var(--color-danger) 25%, transparent); }

/* ---------- Estados de lote de facturación ---------- */

.badge[data-billing-status="open"] { color: #2f5fb0; background: #eef3fb; border-color: #d3e0f5; }
.badge[data-billing-status="closed"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-billing-status="invoiced"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }

/* ---------- Estado de tenant (administración) ---------- */

.badge[data-tenant-status="active"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-tenant-status="suspended"] { color: var(--color-danger); background: var(--color-danger-bg); border-color: color-mix(in srgb, var(--color-danger) 25%, transparent); }

.badge[data-portal-user-status="active"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-portal-user-status="disabled"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }

.badge[data-category-status="active"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-category-status="inactive"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }

.badge[data-billing-policy-status="active"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-billing-policy-status="closed"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }

.badge[data-sla-policy-status="active"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-sla-policy-status="inactive"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }

/* ---------- Preincidencias (correo entrante) ---------- */

.badge[data-preincidencia-status="pending"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-preincidencia-status="converted"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-preincidencia-status="discarded"] { color: var(--color-text-muted); background: var(--color-bg); border-color: var(--color-border); }

.badge[data-preincidencia-method="reply_to_ticket"] { color: #2f5fb0; background: #eef3fb; border-color: #d3e0f5; }
.badge[data-preincidencia-method="known_user_email"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-preincidencia-method="manual_email"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-preincidencia-method="corporate_domain"] { color: #6b46a0; background: #f5f0fb; border-color: #e6d9f2; }

/* ---------- SLA e incumplimiento ---------- */

.badge[data-sla="breached"] { color: var(--color-danger); background: var(--color-danger-bg); border-color: color-mix(in srgb, var(--color-danger) 25%, transparent); }

/* ---------- Franja horaria de un registro de tiempo (recargo de facturación x2/x3) ---------- */

.badge[data-time-band="off_hours"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-time-band="holiday"] { color: var(--color-danger); background: var(--color-danger-bg); border-color: color-mix(in srgb, var(--color-danger) 25%, transparent); }
.badge[data-preincidencia-method="unidentified"] { color: var(--color-text-muted); background: var(--color-bg); border-color: var(--color-border); }

/* ---------- Previsión de bono de horas en el detalle de la incidencia (2026-09-09) ---------- */

.badge[data-billing-preview="included"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-billing-preview="excluded"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-billing-preview="no_bundle"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }

/* ---------- Aviso de vencimiento próximo en tareas, 3 escalones (2026-10-03) ---------- */
/* "En curso" ya se distingue por color reutilizando .badge[data-status="in_progress"] (compartido
   con incidencias, arriba). Sustituye al aviso binario `data-task-urgency="due_soon"/"overdue"` que
   existió aquí entre 2026-09-11 y 2026-10-03 (solo para la cola del dashboard, sin tinte de fila):
   petición del propietario primero para `staff/tasks/index.php`/`portal/tasks/index.php` ("al quedar
   5 días en amarillo, al quedar 3 en naranja y al quedar 2 o menos en rojo") y después, el mismo día,
   para unificar también la cola "Tus tareas"/"Tareas sin asignar" del dashboard
   (`staff/dashboard.php`) con estos mismos tres escalones — ya no hay ninguna vista que use
   `data-task-urgency`, así que esas dos reglas se retiraron en vez de dejarlas sin uso (regla 9).
   Mismos tres escalones que tiñen la fila completa (`.task-row--due-*` más abajo, junto a
   `.table tbody tr:hover`); esta insignia es el texto accesible equivalente (nunca depender solo del
   color, regla 43) y se calcula en la vista, no aquí — ver el docblock de `$renderTaskPanel`/
   `$renderTaskQueue` en esas vistas para los umbrales exactos. */

.badge[data-task-due="soon"] { color: var(--color-caution); background: var(--color-caution-bg); border-color: color-mix(in srgb, var(--color-caution) 30%, transparent); }
.badge[data-task-due="warning"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-task-due="critical"] { color: #fff; background: var(--color-danger); border-color: var(--color-danger); font-weight: 700; }

/* ---------- Estado de saldo de un bono de horas, 4 niveles (2026-09-13) ---------- */
/* Ya se usaba data-bundle-balance en la vista de bonos sin ninguna regla CSS definida (hueco
   preexistente, la insignia se renderizaba sin color) — "ok"/"low" ya existían con solo 3 estados;
   se añade "depleted" (saldo exactamente en 0, antes indistinguible de "low") y se refuerza
   "negative" (relleno, en negrita, mismo tratamiento que data-task-due="critical" arriba) para que
   el nivel más urgente destaque claramente frente a un simple aviso. */

.badge[data-bundle-balance="ok"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-bundle-balance="low"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-bundle-balance="depleted"] { color: var(--color-danger); background: var(--color-danger-bg); border-color: color-mix(in srgb, var(--color-danger) 25%, transparent); }
.badge[data-bundle-balance="negative"] { color: #fff; background: var(--color-danger); border-color: var(--color-danger); font-weight: 700; }

/* ---------- Estado (activo/cerrado) de un bono de horas (2026-09-22) ---------- */
/* Cierre definitivo de bonos: un bono "closed" ya no admite recargas ni reapertura (ver
   HourBundleService::closeBundle()). "active" reutiliza el mismo tratamiento que
   data-billing-preview="included" (éxito discreto); "closed" usa un gris neutro, nunca el rojo de
   data-bundle-balance="negative" — un cierre es un estado administrativo normal, no una alerta. */

.badge[data-bundle-status="active"] { color: var(--color-success); background: var(--color-success-bg); border-color: #c7e8d4; }
.badge[data-bundle-status="closed"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }

/* ---------- Caducidad de un servicio contratado, 3 niveles (2026-09-23) ---------- */
/* "soon" (dentro del mes pero más de 7 días) es un aviso ligero; "urgent" (7 días o menos, pero
   todavía sin caducar) refuerza el mismo aviso; "overdue" (ya caducado) usa el mismo tratamiento en
   relleno/negrita que data-bundle-balance="negative" arriba, para que lo más urgente destaque igual
   de claro que un bono en negativo. */

.badge[data-expiry-status="soon"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }
.badge[data-expiry-status="urgent"] { color: #fff; background: var(--color-warning); border-color: var(--color-warning); font-weight: 700; }
.badge[data-expiry-status="overdue"] { color: #fff; background: var(--color-danger); border-color: var(--color-danger); font-weight: 700; }

/* ---------- Motivo de no facturación de un registro de tiempo, ficha del ticket (2026-09-25) ---------- */
/* Dos estados de "no se factura" muy distintos, antes indistinguibles en la vista (ver CLAUDE.md,
   caso real de María): "bundle" = ya cubierto por el bono activo (normal, tono neutro, igual que
   data-billing-preview="no_bundle" — NO es una alerta); "excluded" = de verdad no se va a facturar
   por exclusión de técnico/empresa/tenant o marca manual, sin bono que lo cubra (aviso, mismo tono
   que data-billing-preview="excluded", para que llame la atención sin ser un error). */

.badge[data-invoiceable="bundle"] { color: var(--color-text-muted); background: var(--color-surface-muted); border-color: var(--color-border); }
.badge[data-invoiceable="excluded"] { color: var(--color-warning); background: var(--color-warning-bg); border-color: #f0e0b8; }

/* ==========================================================================
   MODO OSCURO (2026-09-25)
   ==========================================================================
   Petición directa del propietario ("¿es viable un modo oscuro?"), pospuesta explícitamente hasta
   después del arranque en producción y retomada el mismo día a petición suya ("empecemos ya"). Ver
   `servicedesk-estado.md` para el diseño acordado de antemano: variables CSS bajo un segundo tema,
   toggle sol/luna en el sidebar (ver `partials/shell_start.php`), preferencia en `localStorage` del
   navegador (por usuario, no por tenant — es presentación pura, no necesita servidor ni migración),
   y `prefers-color-scheme` del sistema operativo como valor inicial cuando el usuario no ha elegido
   todavía. Bajo riesgo técnico tal y como se prevé: todo el sistema de diseño ya estaba construido
   sobre variables CSS (regla 44/46, pensado originalmente para el theming por tenant), así que un
   segundo juego de valores es casi todo lo que hace falta.

   Se activa de dos formas, deliberadamente complementarias (regla 47/48, progresive enhancement):
   1) `:root[data-theme="dark"]` — el atributo lo fija el pequeño script inline en el <head> de
      `layouts/base.php` ANTES de pintar la página (evita el parpadeo de un instante en claro al
      cargar), leyendo la preferencia guardada en `localStorage` o, si el usuario nunca ha tocado el
      interruptor, `prefers-color-scheme`. El interruptor del sidebar solo cambia ese atributo y lo
      guarda — la lógica de qué colores usar vive entera aquí, en CSS.
   2) `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {...} }` — mismo bloque
      de valores, como red de seguridad si JavaScript estuviera desactivado: sin él, el interruptor no
      puede escribir `data-theme`, pero la página igualmente respeta el modo oscuro del sistema
      operativo en vez de quedarse forzosamente en claro.

   No se reescribe NINGUNA regla existente de arriba: cada elemento del sistema (tarjetas, tablas,
   formularios, sidebar, insignias de estado/prioridad/facturación...) ya lee su color de una variable
   `--color-*`, así que redefinir esas variables aquí basta para que todo el árbol cambie de tema sin
   tocar su CSS propio — exactamente el mismo mecanismo que ya usa el theming por tenant
   (`brand_color_primary`/`brand_color_secondary`, inyectado en `<style>` por `layouts/base.php`), que
   sigue funcionando igual en los dos temas porque solo toca `--color-primary`/`--color-secondary`.

   Única excepción real: un puñado de insignias de estado (`.badge[data-status="open"]`, `"in_progress"`,
   `data-priority="normal"`, `data-billing-status="open"`, dos variantes de `data-preincidencia-method`)
   usan colores pastel escritos directamente en vez de una variable — son combinaciones de color que no
   encajan en ningún token existente (ni éxito/aviso/error ni el azul primario). Se repiten aquí letra
   por letra con su equivalente oscuro. Lo mismo para el borde (nunca el fondo/texto, que sí son
   variables) de las insignias de éxito/aviso ya existentes, que usa un tono pastel fijo
   (`#c7e8d4`/`#f0e0b8`) en vez de derivarlo de la variable — en oscuro ese mismo pastel claro
   desentonaría contra un fondo ya oscuro, así que se sustituye por un tono oscuro de la misma familia.

   Límite conocido, aceptado deliberadamente (regla 5/91: simplicidad y mantenimiento antes que un
   ajuste perfecto de contraste): las insignias "en negrita, fondo relleno, texto blanco" (p. ej.
   `data-task-due="critical"`, `data-bundle-balance="negative"`, `data-expiry-status="urgent"`)
   reutilizan `--color-danger`/`--color-warning` tal cual, la misma variable que sirve como TEXTO sobre
   fondo oscuro en el resto de la aplicación — un color no puede ser a la vez suficientemente claro para
   leerse como texto sobre fondo oscuro y suficientemente oscuro para que el texto blanco se lea bien
   encima suyo. Quedan legibles pero con un contraste algo más ajustado que el resto en modo oscuro; si
   en el futuro se quiere un ajuste fino de accesibilidad, la solución sería introducir variables
   separadas "-text"/"-fill" para estos tres colores. */

:root[data-theme="dark"] {
    --color-primary: #7c86e8;
    --color-primary-dark: #6169c9;
    --color-secondary: #e6e8ec;

    --color-bg: #0f1115;
    --color-surface: #171a21;
    --color-surface-muted: #1c1f26;
    --color-text: #e6e8ec;
    --color-text-muted: #97a0ac;
    --color-border: #2a2f3a;
    --color-border-strong: #3a4150;

    --color-danger: #e5484d;
    --color-danger-bg: #3a1f22;
    --color-success: #3ecf8e;
    --color-success-bg: #16332a;
    --color-warning: #e0972e;
    --color-warning-bg: #3a2c14;
    --color-caution: #e3c23a;
    --color-caution-bg: #3a3414;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 1px rgba(0, 0, 0, 0.2);
    --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);

    /* Hasta el rediseño "Serio y empresarial" (2026-09-26), `--color-sidebar-bg`/
       `--color-sidebar-text-strong` no se redefinían aquí: en modo claro eran literalmente
       `var(--color-surface)`/`var(--color-text)`, así que heredaban el cambio de arriba
       automáticamente sin necesidad de nada más. Ahora que el modo claro les da un valor propio
       (sidebar azul-marino, independiente de `--color-surface`/`--color-text`), estas dos líneas
       son necesarias para que el modo oscuro seguir viéndose EXACTAMENTE igual que antes —
       recrean a mano la misma derivación que antes era automática. Lo mismo para
       `--color-sidebar-border` (variable nueva): aquí vale `var(--color-border)`, el mismo valor
       que el sidebar ya usaba como borde antes de que existiera esta variable. Cero cambio visual
       en modo oscuro por este rediseño. */
    --color-sidebar-bg: var(--color-surface);
    --color-sidebar-text: #9aa1ad;
    --color-sidebar-text-strong: var(--color-text);
    --color-sidebar-hover-bg: #20242d;
    --color-sidebar-border: var(--color-border);
}

:root[data-theme="dark"] .badge[data-status="open"] { color: #ff9a9a; background: #3a2020; border-color: #5c3232; }
:root[data-theme="dark"] .badge[data-status="in_progress"] { color: #9aa4f5; background: #232744; border-color: #38406b; }
:root[data-theme="dark"] .badge[data-priority="normal"],
:root[data-theme="dark"] .badge[data-billing-status="open"],
:root[data-theme="dark"] .badge[data-preincidencia-method="reply_to_ticket"] { color: #8fb3ef; background: #1c2740; border-color: #2f4568; }
:root[data-theme="dark"] .badge[data-preincidencia-method="corporate_domain"] { color: #cf9ef0; background: #2c2038; border-color: #45355c; }

:root[data-theme="dark"] .badge[data-status="resolved"],
:root[data-theme="dark"] .badge[data-billing-status="invoiced"],
:root[data-theme="dark"] .badge[data-tenant-status="active"],
:root[data-theme="dark"] .badge[data-portal-user-status="active"],
:root[data-theme="dark"] .badge[data-category-status="active"],
:root[data-theme="dark"] .badge[data-billing-policy-status="active"],
:root[data-theme="dark"] .badge[data-sla-policy-status="active"],
:root[data-theme="dark"] .badge[data-preincidencia-status="converted"],
:root[data-theme="dark"] .badge[data-preincidencia-method="known_user_email"],
:root[data-theme="dark"] .badge[data-preincidencia-method="manual_email"],
:root[data-theme="dark"] .badge[data-billing-preview="included"],
:root[data-theme="dark"] .badge[data-bundle-balance="ok"],
:root[data-theme="dark"] .badge[data-bundle-status="active"] { border-color: #2e5945; }

:root[data-theme="dark"] .badge[data-status="pending_customer"],
:root[data-theme="dark"] .badge[data-priority="high"],
:root[data-theme="dark"] .badge[data-billing-status="closed"],
:root[data-theme="dark"] .badge[data-preincidencia-status="pending"],
:root[data-theme="dark"] .badge[data-time-band="off_hours"],
:root[data-theme="dark"] .badge[data-task-due="warning"],
:root[data-theme="dark"] .badge[data-bundle-balance="low"],
:root[data-theme="dark"] .badge[data-expiry-status="soon"],
:root[data-theme="dark"] .badge[data-invoiceable="excluded"],
:root[data-theme="dark"] .timeline-item.is-internal { border-color: #5c471f; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --color-primary: #7c86e8;
        --color-primary-dark: #6169c9;
        --color-secondary: #e6e8ec;

        --color-bg: #0f1115;
        --color-surface: #171a21;
        --color-surface-muted: #1c1f26;
        --color-text: #e6e8ec;
        --color-text-muted: #97a0ac;
        --color-border: #2a2f3a;
        --color-border-strong: #3a4150;

        --color-danger: #e5484d;
        --color-danger-bg: #3a1f22;
        --color-success: #3ecf8e;
        --color-success-bg: #16332a;
        --color-warning: #e0972e;
        --color-warning-bg: #3a2c14;
        --color-caution: #e3c23a;
        --color-caution-bg: #3a3414;

        --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35), 0 1px 1px rgba(0, 0, 0, 0.2);
        --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);

        /* Mismo blindaje que en `:root[data-theme="dark"]` más arriba (rediseño "Serio y
           empresarial", 2026-09-26): cero cambio visual aquí, solo se hace explícito lo que antes
           era automático porque el modo claro ya no deriva `--color-sidebar-bg`/
           `--color-sidebar-text-strong` de `--color-surface`/`--color-text`. */
        --color-sidebar-bg: var(--color-surface);
        --color-sidebar-text: #9aa1ad;
        --color-sidebar-text-strong: var(--color-text);
        --color-sidebar-hover-bg: #20242d;
        --color-sidebar-border: var(--color-border);
    }

    :root:not([data-theme="light"]) .badge[data-status="open"] { color: #ff9a9a; background: #3a2020; border-color: #5c3232; }
    :root:not([data-theme="light"]) .badge[data-status="in_progress"] { color: #9aa4f5; background: #232744; border-color: #38406b; }
    :root:not([data-theme="light"]) .badge[data-priority="normal"],
    :root:not([data-theme="light"]) .badge[data-billing-status="open"],
    :root:not([data-theme="light"]) .badge[data-preincidencia-method="reply_to_ticket"] { color: #8fb3ef; background: #1c2740; border-color: #2f4568; }
    :root:not([data-theme="light"]) .badge[data-preincidencia-method="corporate_domain"] { color: #cf9ef0; background: #2c2038; border-color: #45355c; }

    :root:not([data-theme="light"]) .badge[data-status="resolved"],
    :root:not([data-theme="light"]) .badge[data-billing-status="invoiced"],
    :root:not([data-theme="light"]) .badge[data-tenant-status="active"],
    :root:not([data-theme="light"]) .badge[data-portal-user-status="active"],
    :root:not([data-theme="light"]) .badge[data-category-status="active"],
    :root:not([data-theme="light"]) .badge[data-billing-policy-status="active"],
    :root:not([data-theme="light"]) .badge[data-sla-policy-status="active"],
    :root:not([data-theme="light"]) .badge[data-preincidencia-status="converted"],
    :root:not([data-theme="light"]) .badge[data-preincidencia-method="known_user_email"],
    :root:not([data-theme="light"]) .badge[data-preincidencia-method="manual_email"],
    :root:not([data-theme="light"]) .badge[data-billing-preview="included"],
    :root:not([data-theme="light"]) .badge[data-bundle-balance="ok"],
    :root:not([data-theme="light"]) .badge[data-bundle-status="active"] { border-color: #2e5945; }

    :root:not([data-theme="light"]) .badge[data-status="pending_customer"],
    :root:not([data-theme="light"]) .badge[data-priority="high"],
    :root:not([data-theme="light"]) .badge[data-billing-status="closed"],
    :root:not([data-theme="light"]) .badge[data-preincidencia-status="pending"],
    :root:not([data-theme="light"]) .badge[data-time-band="off_hours"],
    :root:not([data-theme="light"]) .badge[data-task-due="warning"],
    :root:not([data-theme="light"]) .badge[data-bundle-balance="low"],
    :root:not([data-theme="light"]) .badge[data-expiry-status="soon"],
    :root:not([data-theme="light"]) .badge[data-invoiceable="excluded"],
    :root:not([data-theme="light"]) .timeline-item.is-internal { border-color: #5c471f; }
}

/* ---------- Selector de tema (sol/luna) en el sidebar ---------- */
/* Un solo botón con los dos iconos superpuestos (regla 47/48: sigue siendo un <button> normal sin
   JavaScript, simplemente no cambiaría nada al pulsarlo) — el script de `shell_start.php` es quien le
   da comportamiento; qué icono se ve en cada tema lo decide solo CSS, igual que el resto de este
   fichero. Mismo tratamiento visual que `.sidebar__logout-btn` (botón cuadrado, transparente, con
   halo suave al pasar el ratón), reutilizado literalmente para que ambos boones del pie del sidebar
   luzcan idénticos. */
.sidebar__theme-toggle {
    display: inline-flex;
    width: 32px;
    height: 32px;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-sidebar-text);
    cursor: pointer;
    flex: 0 0 auto;
}

.sidebar__theme-toggle:hover {
    background: var(--color-sidebar-hover-bg);
    color: var(--color-sidebar-text-strong);
}

.sidebar__theme-toggle svg {
    width: 17px;
    height: 17px;
}

.sidebar__theme-icon {
    display: inline-flex;
}

.sidebar__theme-icon--moon {
    display: none;
}

:root[data-theme="dark"] .sidebar__theme-icon--sun {
    display: none;
}

:root[data-theme="dark"] .sidebar__theme-icon--moon {
    display: inline-flex;
}

/* ---------- Tablas ---------- */

.table-wrap {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    overflow-x: auto;
}

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

.table th, .table td {
    text-align: left;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--color-border);
    white-space: nowrap;
}

.table th {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--color-text-muted);
    font-weight: 600;
}

.table tbody tr:last-child td {
    border-bottom: none;
}

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

/* Aviso de vencimiento próximo en el listado de "Tareas" (petición del propietario, 2026-10-03) —
   ver el bloque `data-task-due` más abajo para el detalle completo de los tres escalones. La clase
   la añade la vista (`staff/tasks/index.php`/`portal/tasks/index.php`) solo sobre las filas de tareas
   ACTIVAS cuya fecha de fin ya está cerca o superada; el histórico nunca lleva esta clase. Colocadas
   DESPUÉS de `.table tbody tr:hover` a propósito: misma especificidad CSS (dos clases + dos
   elementos), así que por orden de aparición el tinte de vencimiento se mantiene visible también al
   pasar el ratón por encima en vez de desaparecer bajo el gris del hover.

   `color: #16202e` forzado (y el mismo valor para los enlaces de la fila, que si no seguirían usando
   `var(--color-primary)` vía la regla global `a {...}`): ver el comentario junto a
   `--color-task-due-soon-bg` en el bloque `:root` — estos tres fondos son fijos en ambos temas, así
   que el texto también tiene que serlo para no quedar iluminado-sobre-iluminado en modo oscuro. No
   afecta a las insignias (`.badge`) de dentro de la fila: su propio `color` ya está declarado
   explícitamente en las reglas `.badge[data-status]`/`.badge[data-priority]`/`.badge[data-task-due]`
   de este fichero, y una declaración directa sobre un elemento siempre gana a un valor heredado del
   `<tr>`, sin que importe la especificidad de esta regla. */
.table tbody tr.task-row--due-soon { background: var(--color-task-due-soon-bg); color: #16202e; }
.table tbody tr.task-row--due-warning { background: var(--color-task-due-warning-bg); color: #16202e; }
.table tbody tr.task-row--due-critical { background: var(--color-task-due-critical-bg); color: #16202e; }

.table tbody tr.task-row--due-soon a,
.table tbody tr.task-row--due-warning a,
.table tbody tr.task-row--due-critical a { color: #16202e; }

.table td.table-cell-wrap {
    white-space: normal;
}

/* Asunto/título cortado con "…" en vistas de listado (petición del propietario 2026-09-25: asuntos
   muy largos —sobre todo antes de limitar tickets.subject a 100 caracteres en TicketService, y en
   preincidencias, cuyo asunto es el texto crudo del email y no tiene ese límite— rompían la fila,
   ya fuera desbordando la tabla (comportamiento por defecto, white-space: nowrap) o estirándola en
   vertical con .table-cell-wrap). max-width fijo + ellipsis funciona igual con table-layout: auto,
   sin necesidad de tocar el layout de toda la tabla. El texto completo se conserva siempre en el
   atributo title (tooltip nativo, sin JS) para no perder información. */
.table td.table-cell-truncate {
    max-width: 320px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------- Selects / textarea ---------- */

.select, .textarea {
    width: 100%;
    padding: 0.6rem 0.75rem;
    font-size: 0.95rem;
    font-family: inherit;
    color: var(--color-text);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
}

.select:focus, .textarea:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}

.textarea {
    resize: vertical;
    min-height: 6rem;
}

/* ---------- Select con búsqueda (autocompletado, regla 47/48) ---------- */
/* Mejora progresiva de un <select> con muchas opciones (categorías de incidencia) en un campo de
   texto con autocompletado — ver public/assets/js/searchable-select.js. El <select> real queda
   oculto SOLO visualmente (opacity + posición absoluta, nunca display:none/hidden) para que la
   validación nativa `required` del navegador lo siga encontrando y enfocando con normalidad. */

.searchable-select {
    position: relative;
}

.searchable-select__native {
    position: absolute;
    inset: 0;
    opacity: 0;
    z-index: 1;
    pointer-events: none;
}

.searchable-select__input {
    position: relative;
    z-index: 2;
}

.searchable-select__listbox {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    z-index: 30;
    margin: 0;
    padding: 0.35rem;
    list-style: none;
    max-height: 260px;
    overflow-y: auto;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}

.searchable-select__option {
    padding: 0.5rem 0.6rem;
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
    color: var(--color-text);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.searchable-select__option.is-active,
.searchable-select__option:hover {
    background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface));
}

.searchable-select__empty {
    padding: 0.5rem 0.6rem;
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

/* Filtros de la cabecera (Tenant / Empresa, petición del propietario 2026-10-07: "no se ve al ser la caja
   pequeña"): la caja gana ancho propio, y la LISTA desplegable puede ser más ancha que la caja (hasta
   28rem, o el ancho de la pantalla en móvil) para leer nombres largos enteros. */
.app-header__search .searchable-select {
    flex: 0 1 14rem;
    min-width: 9rem;
}

.app-header__search .searchable-select__listbox {
    right: auto;
    min-width: 100%;
    width: max-content;
    max-width: min(28rem, 90vw);
}

/* ---------- Botones flotantes "ir al principio / ir al final" (regla 101, petición del propietario
   2026-09-10: incidencias muy largas) ----------
   Son enlaces `<a href="#page-top">`/`<a href="#page-bottom">` normales — funcionan sin JavaScript
   (regla 47/48), con el `html { scroll-behavior: smooth }` de arriba como única mejora visual. */
.scroll-nav {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 40;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.scroll-nav__btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 999px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-sm);
    color: var(--color-text-muted);
    transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.scroll-nav__btn svg {
    width: 1.25rem;
    height: 1.25rem;
}

.scroll-nav__btn:hover,
.scroll-nav__btn:focus-visible {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: #ffffff;
    transform: translateY(-1px);
}

@media (max-width: 640px) {
    .scroll-nav {
        right: 0.75rem;
        bottom: 0.75rem;
    }

    .scroll-nav__btn {
        width: 2.5rem;
        height: 2.5rem;
    }
}

/* ---------- Filtros ---------- */

.filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: end;
    margin-bottom: 1.25rem;
}

.filters .field {
    margin-bottom: 0;
    min-width: 160px;
}

.filters .btn {
    width: auto;
}

/* ---------- Paginación ---------- */

.pagination {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    margin-top: 1.5rem;
    font-size: 0.9rem;
}

.pagination a, .pagination span {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    height: 2rem;
    padding: 0 0.5rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
}

.pagination a:hover {
    background: var(--color-bg);
    text-decoration: none;
}

.pagination .is-current {
    background: var(--color-primary);
    color: #fff;
    border-color: var(--color-primary);
}

/* ---------- Colas de trabajo del dashboard (sin cerrar asignadas al usuario / sin asignar) ---------- */

.dashboard-queues {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 1.5rem;
    margin-bottom: 1.5rem;
    align-items: start;
}

.dashboard-queues--stacked {
    grid-template-columns: 1fr;
}

.dashboard-queue {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: 1.5rem;
}

/* Distinción visual incidencias/tareas (2026-09-25, petición directa del propietario — su
   diseñadora sugirió un borde de color: "las incidencias en azul y las tareas en naranja").
   El azul reutiliza `--color-primary` (el mismo del resto de la app). El naranja de tareas usa su
   propio token dedicado, `--color-task-accent` (ver bloque `:root` arriba) — inicialmente reutilizaba
   `--color-warning`, pero el propietario pidió después un naranja más vivo específicamente para este
   borde ("ahora está en #8a6217 que es casi marrón"), y `--color-warning` es un token compartido por
   muchos badges que no debían cambiar de color. Sigue la recomendación de accesibilidad de preferir
   azul/naranja a azul/verde para que se distingan mejor con más tipos de daltonismo.

   Nacieron en `staff/dashboard.php` (las 4 tarjetas del dashboard eran visualmente idénticas y
   costaba distinguir de un vistazo cuál era cuál) y se extendieron el mismo día, por consistencia
   visual (petición explícita), al listado principal de incidencias (`staff/tickets/index.php`,
   reutilizando estas mismas clases sobre `.dashboard-queue`). El listado de tareas
   (`staff/tasks/index.php`) usó al principio un `.table-wrap` normal en vez de `.dashboard-queue` (de
   ahí las variantes `.table-wrap--tickets`/`.table-wrap--tasks` que existieron aquí), pero desde la
   separación en dos paneles activas/histórico (petición del propietario, 2026-09-30 — ver el docblock
   de `staff/tasks/index.php`) también usa `.dashboard-queue`/`.dashboard-queue--tasks` igual que
   incidencias, así que esas dos variantes de `.table-wrap` quedaron sin ningún uso y se retiraron
   (regla 86, evitar CSS muerto). Deliberadamente NO se aplica en `portal/tickets/index.php` ni en
   `portal/tasks/index.php` (listados del propio cliente en el portal, cada uno de un único tipo): ahí
   nunca existe ambigüedad entre incidencias y tareas, así que se dejan sin borde de color a propósito.
*/
.dashboard-queue--tickets {
    border-left: 4px solid var(--color-primary);
}

.dashboard-queue--tasks {
    border-left: 4px solid var(--color-task-accent);
}

.dashboard-queue__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 1rem;
}

.dashboard-queue__header h2 {
    font-size: 1.05rem;
    margin: 0;
}

/*
 * Filtro por categoría padre de las colas "sin asignar" (petición del propietario, 2026-09-25 — ver
 * `$renderQueue`/`$renderTaskQueue` en `staff/dashboard.php`). `margin-left: auto` lo empuja al
 * extremo derecho de `.dashboard-queue__header` (que ya es `display: flex`); en pantallas estrechas
 * `flex-wrap: wrap` del propio header lo baja a su propia línea sin más ajustes. `.select` normal es
 * `width: 100%`, aquí se sobrescribe a un ancho de contenido razonable para que no ocupe toda la
 * tarjeta.
 */
.dashboard-queue__filter {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
}

.dashboard-queue__filter .select {
    width: auto;
    min-width: 170px;
}

/* Oculta visualmente una etiqueta manteniéndola accesible para lectores de pantalla (regla 43: la
   propia app ya usa `<label>` con texto visible en todos los demás formularios; aquí el desplegable
   ya queda identificado por el título de la sección, así que la etiqueta solo hace falta para
   accesibilidad, no visualmente). Patrón estándar, sin dependencias externas (regla 45). */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/*
 * Listado de incidencias en dos paneles apilados (activas arriba / histórico abajo) — petición del
 * propietario, 2026-09-08. A diferencia de `.dashboard-queues` (grid de 2 columnas lado a lado, para
 * las colas del dashboard), aquí se pide expresamente "una ventana superior... y una inferior", así
 * que se apilan en vertical. Reutiliza `.dashboard-queue`/`.dashboard-queue__header` tal cual para
 * cada panel individual (regla 9).
 */
.ticket-list-stack {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}

/*
 * Ancla `#resultados` de los listados de Incidencias/Tareas del panel (2026-10-16, reportado por David:
 * al filtrar por técnico había que bajar para ver los datos filtrados). El formulario de filtros
 * envía a `/panel/...#resultados`, así que el navegador aterriza directamente en el listado.
 * `scroll-margin-top` deja sitio a la cabecera fija (`.app-header`, sticky) para que no tape el título
 * del primer panel; en móvil la cabecera se parte en varias líneas, de ahí el margen mayor.
 */
.list-results {
    scroll-margin-top: calc(var(--header-height, 4rem) + 1rem);
}

@media (max-width: 960px) {
    .list-results {
        scroll-margin-top: 9rem;
    }
}

/* Aviso de panel vacío compacto: no empuja hacia abajo el panel que sí tiene resultados. */
.dashboard-queue__empty {
    margin: 0;
}

/* ---------- Cabecera de página / acciones ---------- */

.page-header {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1.25rem;
}

.page-header h1 {
    font-size: 1.3rem;
}

.btn-inline {
    width: auto;
}

/* ---------- Detalle de ticket ---------- */

.ticket-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.ticket-detail {
    display: grid;
    /* 2026-10-23: `minmax(0, …)` en vez de `2fr 1fr` a secas. Una `fr` tiene como mínimo el contenido
       más ancho (min-content), así que una línea larga sin espacios en un comentario, el asunto, una
       URL o una tabla ensanchaba la columna principal y empujaba el panel lateral fuera de la
       pantalla (sobre todo con el menú sin colapsar, que resta 240-290px). Con `minmax(0, …)` las
       columnas se reparten SIEMPRE el ancho disponible y el contenido se ajusta a su columna. */
    /* Panel lateral de ANCHO FIJO (2026-10-23, petición del propietario: «la barra de la derecha siempre
       del mismo tamaño; lo que se adapta a la pantalla es el contenido central»). */
    grid-template-columns: minmax(0, 1fr) var(--ticket-side-width, 360px);
    gap: 1.5rem;
    align-items: start;
}

.ticket-detail > * {
    min-width: 0;
}

/* El texto largo se parte donde haga falta en vez de ensanchar la tarjeta; imágenes/tablas/código
   dentro de la ficha nunca pueden ser más anchos que su columna. */
.ticket-detail .card,
.ticket-detail .timeline-item,
.ticket-detail .ticket-description {
    overflow-wrap: anywhere;
}

.ticket-detail img,
.ticket-detail video {
    max-width: 100%;
    height: auto;
}

.ticket-detail pre,
.ticket-detail .table-wrap {
    max-width: 100%;
    overflow-x: auto;
}

/* Con el menú lateral desplegado el área útil es menor: el panel lateral pasa debajo antes. */
@media (max-width: 1100px) {
    .ticket-detail {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Menú colapsado: sobra ancho, así que se mantienen las dos columnas hasta pantallas más estrechas. */
@media (min-width: 961px) and (max-width: 1100px) {
    .app-shell:has(.sidebar-collapse-input:checked) .ticket-detail {
        grid-template-columns: minmax(0, 1fr) var(--ticket-side-width, 360px);
    }
}

.ticket-description {
    white-space: pre-wrap;
    word-break: break-word;
}

/* ---------- Timeline de comentarios ---------- */

.timeline {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin: 1.25rem 0;
}

.timeline-item {
    padding: 1rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border);
    background: var(--color-surface);
}

.timeline-item.is-internal {
    background: var(--color-warning-bg);
    border-color: #f0e0b8;
}

.timeline-item__head {
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
    font-size: 0.8rem;
    color: var(--color-text-muted);
    margin-bottom: 0.5rem;
}

.timeline-item__body {
    white-space: pre-wrap;
    word-break: break-word;
    overflow-wrap: anywhere;
}

.side-panel {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

/* Encuesta de satisfacción del cliente (portal, incidencias "Resuelta" — regla 43: cada opción
   funciona como un <button type="submit"> normal dentro de un <form>, sin JavaScript). */
.satisfaction-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 0.75rem;
}

.satisfaction-picker__option {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.3rem;
    flex: 1 1 110px;
    padding: 0.75rem 0.5rem;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-family: inherit;
    color: var(--color-text);
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

.satisfaction-picker__option:hover {
    border-color: var(--color-primary);
    background: var(--color-surface-muted);
}

.satisfaction-picker__option.is-selected {
    border-color: var(--color-primary);
    background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
    box-shadow: 0 0 0 1px var(--color-primary);
}

.satisfaction-picker__emoji {
    font-size: 1.6rem;
    line-height: 1;
}

.satisfaction-picker__label {
    font-size: 0.78rem;
    font-weight: 600;
    text-align: center;
}

.satisfaction-rating-display {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

.satisfaction-rating-display__emoji {
    font-size: 1.6rem;
    line-height: 1;
}

/* Variantes compactas para el listado de incidencias (petición del propietario, 2026-09-07: "que en
   el listado... aparezca el grado de satisfacción" con un desplegable para valorar sin salir de la
   lista) — mismas clases base, reutilizadas dentro de una celda de tabla en vez del panel lateral del
   detalle de ticket, así que no necesitan crecer tanto ni envolver en varias líneas. */
.satisfaction-picker--compact {
    margin-top: 0.5rem;
    gap: 0.4rem;
    flex-wrap: nowrap;
}

.satisfaction-picker--compact .satisfaction-picker__option {
    flex: 0 0 auto;
    padding: 0.35rem 0.45rem;
}

.satisfaction-picker--compact .satisfaction-picker__emoji {
    font-size: 1.25rem;
}

.satisfaction-picker--compact .satisfaction-picker__label {
    font-size: 0.68rem;
    white-space: nowrap;
}

.satisfaction-rating-display--compact {
    margin-top: 0;
    white-space: nowrap;
}

.satisfaction-rating-display--compact .satisfaction-rating-display__emoji {
    font-size: 1.1rem;
}

.form-actions {
    display: flex;
    gap: 0.5rem;
}

.form-actions .btn {
    width: auto;
}

/* ---------- Lista de casillas acotada (ej. tenants asignados a un técnico) ---------- */

.checkbox-list {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    max-height: 9rem;
    min-width: 12rem;
    overflow-y: auto;
    padding: 0.5rem;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    background: var(--color-bg);
}

.checkbox-list label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.82rem;
    white-space: nowrap;
}

/* Pantalla de reportes (regla 101, backlog "reportes para supervisores"): KPIs + gráficos SVG
   propios, sin librerías externas (regla 45). */
.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 1rem;
    margin: 1rem 0 1.5rem;
}

.kpi-tile {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 1.1rem 1.25rem;
}

.kpi-tile span {
    display: block;
    font-size: 0.78rem;
    color: var(--color-text-muted);
    margin-bottom: 0.35rem;
}

.kpi-tile strong {
    display: block;
    font-size: 1.6rem;
    color: var(--color-secondary);
}

.chart-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: 1.5rem;
    margin-bottom: 1.5rem;
}

.chart-card h2 {
    margin: 0 0 0.25rem;
    font-size: 1rem;
    color: var(--color-secondary);
}

.chart-card p.chart-hint {
    margin: 0 0 0.75rem;
    font-size: 0.82rem;
    color: var(--color-text-muted);
}

.chart-legend {
    display: flex;
    gap: 1.25rem;
    margin-bottom: 0.5rem;
    font-size: 0.82rem;
    color: var(--color-text-muted);
}

.chart-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.chart-legend-swatch {
    width: 0.7rem;
    height: 0.7rem;
    border-radius: 3px;
    display: inline-block;
}

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

.chart {
    display: block;
    width: 100%;
    height: 220px;
}

.chart-axis {
    stroke: var(--color-border);
    stroke-width: 1;
}

.chart-axis-label {
    font-size: 9px;
    fill: var(--color-text-muted);
}

.chart-empty {
    color: var(--color-text-muted);
    font-size: 0.9rem;
}

/* ---------- Paneles colapsables nativos (<details>/<summary>, sin JavaScript, regla 47/48) ----------
   Usado en "Gestión de técnicos" (2026-08-19) para que las notas de ayuda y los formularios de
   edición/cambio de contraseña de cada técnico no ocupen espacio en pantalla hasta que alguien
   pincha en ellos — antes iban siempre visibles y montados en una tabla estrecha, lo que en pantallas
   pequeñas provocaba que los campos se solaparan entre sí. El propio elemento <summary> se estiliza
   como un botón normal; se oculta el marcador triangular por defecto del navegador porque ya lo
   sustituye visualmente el propio botón (funciona igual sin CSS, solo se ve el triángulo nativo). */

.help-block {
    margin-bottom: 1.1rem;
}

.help-block > summary {
    cursor: pointer;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--color-primary);
    list-style: none;
}

.help-block > summary::-webkit-details-marker {
    display: none;
}

.help-block p {
    margin-top: 0.6rem;
    font-size: 0.85rem;
    color: var(--color-text-muted);
    max-width: 60ch;
}

.disclosure > summary {
    list-style: none;
    cursor: pointer;
}

.disclosure > summary::-webkit-details-marker {
    display: none;
}

.disclosure-panel {
    padding-top: 0.85rem;
    margin-top: 0.85rem;
    border-top: 1px solid var(--color-border);
}

/* ---------- Tarjetas de técnico (sustituye a la tabla editable, "Gestión de técnicos") ---------- */

.tech-list {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    margin-top: 1rem;
}

.tech-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: 1.1rem 1.25rem;
}

.tech-card__summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem 1rem;
}

.tech-card__identity strong {
    display: block;
    font-size: 1rem;
    color: var(--color-text);
}

.tech-card__identity span {
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

.tech-card__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-left: auto;
}

.tech-card__chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.6rem;
    font-size: 0.8rem;
    color: var(--color-text-muted);
}

.chip {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    font-size: 0.78rem;
    color: var(--color-text);
}

.tech-card__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.85rem;
}

.tech-card__actions .disclosure {
    flex: 1 1 auto;
    min-width: 12rem;
}

/* Cabeceras de tabla ordenables (Tareas, 2026-10-05). Enlace normal que hereda el color de la
   cabecera (funciona igual en claro y oscuro, sin tokens nuevos); la flecha es solo decorativa
   (`aria-hidden`), el estado real lo anuncia `aria-sort` en el <th>. */
.th-sort {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}

.th-sort:hover,
.th-sort:focus-visible {
    text-decoration: underline;
}

.th-sort__arrow {
    font-size: 0.75em;
    opacity: 0.55;
}

.th-sort.is-active .th-sort__arrow {
    opacity: 1;
}

/*
 * Pestañas de vista (2026-10-13): navegación entre secciones de una misma pantalla mediante enlaces
 * normales (`?vista=...`), sin JavaScript. Primer uso: «Servicios contratados» (Servicios / + Nuevo
 * servicio / Tipos de servicio) — el propietario pidió acabar con «ir saltando de lado a lado» en una
 * página larga. Solo usa tokens de color existentes, así que sigue modo claro/oscuro y el color primario
 * por tenant sin ninguna regla adicional.
 */
.view-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin: 0.75rem 0 1rem;
    border-bottom: 1px solid var(--color-border);
}

.view-tab {
    display: inline-block;
    padding: 0.55rem 1rem;
    margin-bottom: -1px;
    border: 1px solid transparent;
    border-radius: 8px 8px 0 0;
    color: var(--color-text-muted);
    font-weight: 600;
    text-decoration: none;
}

.view-tab:hover {
    color: var(--color-text);
    background: var(--color-surface-muted);
}

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

.view-tab[aria-current="page"] {
    color: var(--color-primary);
    background: var(--color-surface);
    border-color: var(--color-border);
    border-bottom-color: var(--color-surface);
}

.view-tab--primary:not([aria-current="page"]) {
    color: var(--color-primary);
}

/* Bloque que solo se muestra al pulsar un enlace `#id` que apunta a él (2026-10-26): editar asunto y
   descripción en la ficha de incidencia, sin JavaScript. «Cancelar» (href="#") lo vuelve a ocultar. */
.edit-details {
    display: none;
    margin-top: 0.75rem;
}

.edit-details:target {
    display: block;
}


/* ---- Sesión de soporte («login as», 2026-11-03) ---- */
.impersonation-banner {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1rem;
    flex-wrap: wrap;
    padding: 0.6rem 1.5rem;
    background: #b45309;
    color: #fff;
    font-size: 0.9rem;
}

.impersonation-banner .btn {
    background: #fff;
    color: #7c2d12;
    border-color: #fff;
}

/* La cabecera sticky baja para no quedar tapada por el aviso. */
.app-content.has-impersonation .app-header {
    top: var(--impersonation-banner-height, 3rem);
}

.inline-form {
    display: inline-block;
    margin: 0 0 0 0.4rem;
}

@media (max-width: 720px) {
    .app-content.has-impersonation {
        --impersonation-banner-height: 6.5rem;
    }
}


/* ---- Novedades en incidencias del portal (2026-11-06) ---- */
.sidebar__badge {
    margin-left: auto;
    min-width: 1.3rem;
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    background: #dc2626;
    color: #fff;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1.2rem;
    text-align: center;
}

.unread-dot {
    display: inline-block;
    width: 0.6rem;
    height: 0.6rem;
    margin-right: 0.4rem;
    border-radius: 50%;
    background: #dc2626;
    vertical-align: middle;
}

/* Menú colapsado: el contador pasa a ser un punto sobre el icono. */
.sidebar-collapse-input:checked ~ .sidebar .sidebar__badge {
    position: absolute;
    top: 0.25rem;
    right: 0.35rem;
    min-width: 0.6rem;
    width: 0.6rem;
    height: 0.6rem;
    padding: 0;
    font-size: 0;
    line-height: 0;
}


/* El cliente ha respondido (dashboard del técnico, 2026-11-10). */
.badge-client-reply {
    /* En su propia línea bajo el asunto: la celda del asunto recorta con «…» (max-width) y en línea
       se habría cortado también el aviso. */
    display: block;
    width: fit-content;
    max-width: 100%;
    margin-top: 0.25rem;
    background: #fee2e2;
    color: #991b1b;
    border-color: #fecaca;
    font-size: 0.72rem;
    text-transform: none;
    letter-spacing: 0;
    white-space: nowrap;
}

.badge-client-reply .unread-dot {
    width: 0.5rem;
    height: 0.5rem;
    margin-right: 0.3rem;
}

/* ---------- Pantalla «Novedades» (2026-11-11) ---------- */
.update-list {
    list-style: none;
    margin: 0 0 1.5rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.update-item {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-left: 4px solid #dc2626;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: 1rem 1.25rem;
}

.update-item__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
}

.update-item__meta {
    margin-top: 0.25rem;
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

.update-item__body {
    margin: 0.6rem 0 0.75rem;
    padding: 0.6rem 0.8rem;
    background: var(--color-bg, #f1f5f9);
    border-radius: var(--radius-sm, 6px);
    white-space: normal;
    overflow-wrap: anywhere;
}

.page-header__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
}

.update-item__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
}

/* Aviso «El cliente ha respondido» con «×» para descartarlo (2026-11-13). */
.client-reply {
    /* flex (no inline-flex): en su propia línea bajo el asunto, como antes el badge suelto. */
    display: flex;
    width: fit-content;
    max-width: 100%;
    align-items: center;
    gap: 0.25rem;
    margin-top: 0.25rem;
}

.client-reply .badge-client-reply {
    margin-top: 0;
}

.client-reply__dismiss {
    margin: 0;
}

.client-reply__dismiss button {
    appearance: none;
    border: 0;
    background: transparent;
    color: #991b1b;
    cursor: pointer;
    font-size: 1.1rem;
    line-height: 1;
    padding: 0.25rem 0.4rem;
    border-radius: 999px;
}

.client-reply__dismiss button:hover,
.client-reply__dismiss button:focus-visible {
    background: #fee2e2;
}
