/* ==========================================================================
   Rodago — Design System
   Fuente única de estilos compartidos por las 3 experiencias
   (admin / rider / company). Sustituye los <style> inline que vivían
   en base.html, rider_base.html y company_base.html.
   ========================================================================== */

:root {
    /* Color: marca */
    --primary: #4A90E2;
    --primary-dark: #3a7bc8;
    --primary-light: #eaf2fc;

    /* Color: marca Rodago (pantalla de acceso). La tinta es el fondo del panel
       de marca y el verde el acento; no son los tokens de la app (que sigue
       con --primary azul), sino la identidad que se muestra al entrar. */
    --rodago-tinta: #16233D;
    --rodago-tinta-2: #1d2b47;
    --rodago-verde: #15C77E;
    --rodago-verde-dark: #11a869;
    /* El azul SUAVE de las barras de uso: el tono del azul tinta de la marca
       (220°), aclarado. Un solo color, también por encima del límite. Validado
       a ≥3:1 sobre la superficie clara y la oscura del panel. */
    --cfg-bar: #5B82D6;
    --rodago-tinta-text: #9fb0cc;   /* gris azulado del lema sobre tinta */

    /* Color: acento portal empresa (mismo sistema, tono distinto) */
    --cp: #059669;
    --cp-dark: #047857;
    --cp-light: #d1fae5;
    --cp-text: #065f46;

    --secondary: #6c757d;
    --success: #27AE60;
    --danger: #E74C3C;
    --warning: #F39C12;
    --info: #3498DB;
    --dark: #2C3E50;
    --light: #F8F9FA;

    /* Escala de grises usada en textos/bordes secundarios */
    --gray-900: #111827;
    --gray-700: #374151;
    --gray-500: #6b7280;
    --gray-400: #9ca3af;
    --gray-200: #e5e7eb;
    --gray-100: #f3f4f6;
    --gray-50: #f9fafb;

    /* Tipografía */
    --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

    /* ── DENSIDAD DE LA UI (escritorio: dispatcher y portal) ───────────────
       `--ui-scale` es el ÚNICO punto para subir o bajar la densidad de toda la
       interfaz de escritorio. No es `zoom` ni `transform: scale()` a propósito:
       los dos escalan el LIENZO —y en el mapa eso desalinea el puntero contra
       las paradas, rompe los `position: fixed`, los scrollbars y el
       hit-testing—. Acá lo que se reduce es el TAMAÑO BASE, así que tipografía
       y espaciados encogen como layout de verdad y el mapa se queda a 1:1
       (dimensiona por contenedor y viewport, no por fuente).

       La palanca es el font-size RAÍZ, porque `rem` se resuelve siempre contra
       `html`: un font-size en un wrapper no movería un solo `rem`. Se aplica
       por SELECTOR DE ROL (`html.role-admin`, `html.role-company`) para que el
       rider quede fuera, y se anula en el teléfono con el media query del final
       de este bloque.

       `--ui-unscale` es su inversa: lo que se quiera dejar a tamaño natural
       —el chrome (sidebar/topbar) y la torre de control, que ya se ajustaron a
       mano— multiplica sus `rem` por esta variable y no sufre doble reducción.
       Ver la sección "DENSIDAD: LO QUE NO ENCOGE" al final del archivo. */
    --ui-scale: 0.85;
    --ui-unscale: calc(1 / var(--ui-scale));

    /* Espaciado. En `rem` y no en px: es lo que hace que la reducción se sienta
       como DENSIDAD y no solo como letra chica. Los valores son los de siempre
       (1rem = 16px con el raíz sin escalar), así que en el rider y en el
       teléfono —donde `--ui-scale` vale 1— no cambia ni un píxel. */
    --space-1: 0.25rem;   /*  4px */
    --space-2: 0.5rem;    /*  8px */
    --space-3: 0.75rem;   /* 12px */
    --space-4: 1rem;      /* 16px */
    --space-5: 1.25rem;   /* 20px */
    --space-6: 1.5rem;    /* 24px */

    /* Radios */
    --radius-sm: 6px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-xl: 16px;

    /* Sidebar */
    /* 240px eran 16px de más que ningún ítem del menú usaba, y que a la torre
       de control —tres columnas de tablas— sí le hacían falta: ahí cada píxel
       del sidebar sale de una dirección recortada. */
    --sidebar-w: 216px;
    --topbar-h: 60px;

    /* Ancho del panel del mapa. Vive ACÁ y no en `.map-screen-body` porque hay
       algo que se ancla al borde derecho del CANVAS sin ser descendiente de ese
       contenedor: el modal del traspaso / de la co-recogida cuando se APARTA
       para elegir en el mapa, que es `position: fixed`. Escrito dos veces, el
       día que el panel cambie de ancho el modal se le montaría encima. */
    --map-panel-w: 380px;

    /* Superficie / elevación (contenido en claro) */
    --surface: #ffffff;          /* superficie base de cards/tablas */
    --surface-sunken: #f5f6fa;   /* fondo de app / zonas hundidas */
    --surface-raised: #ffffff;   /* superficie elevada (dropdowns, modales) */
    --border-subtle: var(--gray-200);
    --elevation-1: 0 1px 3px rgba(16,24,40,0.06);
    --elevation-2: 0 2px 8px rgba(16,24,40,0.08);
    --elevation-3: 0 8px 24px rgba(16,24,40,0.12);

    /* Chrome oscuro del dispatcher (sidebar + topbar del admin — "command
       center"). Sólo se aplica bajo body.role-admin; empresa mantiene su
       chrome claro con acento verde. NO es un tema oscuro global. */
    --chrome-bg: #1f2937;
    --chrome-bg-2: #111827;
    --chrome-border: rgba(255,255,255,0.08);
    --chrome-text: #cbd5e1;
    --chrome-text-strong: #f8fafc;
    --chrome-muted: #94a3b8;
    --chrome-hover-bg: rgba(255,255,255,0.06);
    --chrome-active-bg: rgba(74,144,226,0.20);

    /* Cifras tabulares (tablas, KPIs, timers, precios) */
    --num: tabular-nums;

    /* Color por estado del pedido — ÚNICA fuente de color de estado.
       Estas variables son la fuente canónica del hue por estado; deben
       mantenerse sincronizadas con STATUS_HEX en
       app/services/dashboard_service.py (mismos hex), que las usa para las
       gráficas (Chart.js no puede leer estas variables desde Python).
       Paleta: pendiente=ámbar, asignado=azul, en_sitio/recogido/en_camino=
       cian, entregado=verde, cancelado/demorado=rojo. */
    --status-pendiente: #F39C12;   /* ámbar */
    --status-asignado:  #4A90E2;   /* azul */
    --status-progress:  #06B6D4;   /* cian: en_sitio / recogido / en_camino */
    --status-entregado: #27AE60;   /* verde */
    --status-cancelado: #E74C3C;   /* rojo: cancelado / demorado */
    /* POR ACEPTAR: el pedido del portal que espera la decisión del despacho.
       Índigo: no es ninguno de los colores del trabajo (ámbar/azul/cian), porque
       todavía no lo es. El rechazado comparte el rojo del cancelado. */
    --status-por-aceptar: #6366F1;
    /* Los KILÓMETROS de la torre de control. No son un estado del pedido: son
       la cuarta magnitud de la tarjeta de flota y necesitan un color propio
       que no choque con pedidos (azul), paradas (cian) ni ingresos (verde). */
    --metric-km: #8B5CF6;

    /* ── Código de color por TIPO de PARADA ────────────────────────────────
       Recogida verde · entrega ámbar · traspaso morado · fallida roja. Es el
       MISMO código que hornean los pines del mapa (design/markers/) y el que
       usan el panel del mapa, la hoja del rider, el detalle del pedido, el
       detalle de la ruta y todas las listas de pedidos: el dispatcher y el
       rider hablan de "la parada verde" por teléfono y tiene que significar lo
       mismo en las seis pantallas.

       Están escritas ACÁ, y no repetidas en cada bloque, porque hasta ahora
       cada superficie elegía su propio verde (o ninguno) — de ahí que la
       recogida saliera gris en la cola de pedidos y con un icono de caja en el
       detalle de la ruta. Un color nuevo se cambia en UN sitio.

       El par `-ink` es la variante para TEXTO e ICONO sobre fondo claro: el
       ámbar puro (#F59E0B) no llega al contraste mínimo como texto, así que el
       relleno de un pin/pastilla usa el base y la tipografía usa el `-ink`. */
    --stop-pickup: #22C55E;        /* recogida */
    --stop-pickup-ink: #16A34A;
    --stop-drop: #F59E0B;          /* entrega */
    --stop-drop-ink: #B45309;
    --stop-handoff: #7C3AED;       /* traspaso (punto de encuentro) */
    --stop-handoff-ink: #6D28D9;
    --stop-fail: #DC2626;          /* parada fallida */
    --stop-fail-ink: #B91C1C;
}

/* La palanca de densidad, aplicada. Sólo a las dos experiencias de ESCRITORIO:
   `html.role-rider` no aparece acá, así que la PWA del rider conserva su tamaño
   natural pase lo que pase. La clase de rol la emite `base.html` en el `<html>`
   (además del `<body>`, que la sigue necesitando para el chrome oscuro): un
   wrapper no sirve porque `rem` mira a la raíz. */
html.role-admin,
html.role-company {
    font-size: calc(16px * var(--ui-scale));
}

/* En el teléfono NO se densifica nada: la escala vuelve a 1 —con ella
   `--ui-unscale` también, así que todo el reinflado del final del archivo se
   convierte en `calc(x * 1)` y queda inerte—. Son dos razones distintas y las
   dos mandan:
   ① iOS hace ZOOM al enfocar un input de menos de 16px y desencuadra la
      pantalla (mismo invariante que el alta de pedidos y el login);
   ② los objetivos táctiles ya están calibrados en px (`.form-touch`, 44px) y
      encogerlos alrededor no los mejora, los descoloca.
   El corte es el `767.98px` de siempre. */
@media (max-width: 767.98px) {
    :root { --ui-scale: 1; }
}

* {
    font-family: var(--font-sans);
}

body {
    background-color: #f5f6fa;
    min-height: 100vh;
}

/* ==========================================================================
   Scrollbar moderna (global): fina y translúcida, no siempre "presente";
   el thumb es tenue por defecto y se ensancha/oscurece al pasar el puntero.
   Sirve igual sobre el claro (empresa/rider) y el chrome oscuro (admin).
   ========================================================================== */
* {
    scrollbar-width: thin;                                 /* Firefox */
    scrollbar-color: rgba(130,140,160,0.35) transparent;
}
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background-color: rgba(130,140,160,0.28);
    border-radius: 10px;
    border: 3px solid transparent;        /* margen transparente = thumb fino */
    background-clip: padding-box;
    transition: background-color 0.15s;
}
::-webkit-scrollbar-thumb:hover {
    background-color: rgba(130,140,160,0.6);
    border-width: 2px;                     /* se ensancha bajo el puntero */
}
::-webkit-scrollbar-corner { background: transparent; }
/* Sobre el chrome oscuro del admin, thumb en tono claro translúcido */
body.role-admin { scrollbar-color: rgba(255,255,255,0.22) transparent; }
body.role-admin ::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,0.18); }
body.role-admin ::-webkit-scrollbar-thumb:hover { background-color: rgba(255,255,255,0.4); }

/* ==========================================================================
   Layout: sidebar + topbar (admin / company)
   ========================================================================== */
.app-shell {
    display: flex;
    min-height: 100vh;
}

.app-sidebar {
    width: var(--sidebar-w);
    flex-shrink: 0;
    background: #fff;
    border-right: 1px solid var(--gray-200);
    display: flex;
    flex-direction: column;
    position: fixed;
    top: var(--topbar-h);
    left: 0;
    bottom: 0;
    z-index: 1040;
    transition: transform 0.2s ease;
}

/* Brand ahora vive en la topbar (franja superior unificada de ancho completo),
   ya no en la sidebar. Alineado con el ancho de la columna del sidebar para
   que el logo quede sobre ella (como el ejemplo command-center). */
.app-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    width: var(--sidebar-w);
    height: 100%;
    margin-left: calc(-1 * var(--space-5));   /* flush a la izquierda, alineado al sidebar */
    padding: 0 var(--space-5);
    font-weight: 700;
    font-size: 1.15rem;
    color: var(--primary);
    text-decoration: none;
    box-sizing: border-box;
    white-space: nowrap;
    overflow: hidden;
}
@media (max-width: 575px) {
    .app-brand { width: auto; }
}
/* El logo es una imagen: la altura manda y el ancho sale de su proporción. */
.app-brand-logo {
    height: 26px;
    width: auto;
    max-width: 100%;
    display: block;
}
.app-brand:hover { color: var(--primary-dark); }
.app-brand.cp-brand { color: var(--cp); }
.app-brand.cp-brand:hover { color: var(--cp-dark); }
.app-brand .bi { font-size: 1.3rem; }

.app-sidebar-company {
    padding: var(--space-3) var(--space-5);
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--gray-500);
    border-bottom: 1px solid var(--gray-100);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-sidebar-nav {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-2) var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 1px;
}

/* La navegación se LEE DE REOJO: el nombre de la sección se reconoce por su
   sitio y su icono, no por el tamaño de la letra. A 0,9rem competía con el
   contenido —y en la torre de control, que es tres columnas de tablas, lo que
   compite con el contenido se lo está quitando. */
.app-sidebar-link {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 7px 11px;
    border-radius: var(--radius-md);
    text-decoration: none;
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--gray-700);
    transition: background 0.15s, color 0.15s;
    border: none;
    background: none;
    width: 100%;
    text-align: left;
    cursor: pointer;
}
.app-sidebar-link i { font-size: 0.95rem; width: 18px; text-align: center; flex-shrink: 0; }
.app-sidebar-link:hover { background: var(--gray-100); color: var(--gray-900); }
.app-sidebar-link.active { background: var(--primary-light); color: var(--primary-dark); }
.app-sidebar-link.cp-scope.active { background: var(--cp-light); color: var(--cp-text); }
.app-sidebar-link.danger,
.app-sidebar-link.danger i { color: var(--danger); }
.app-sidebar-link.danger:hover { background: #fdecea; }

.app-sidebar-group { margin-top: 2px; }
.app-sidebar-group-head {
    list-style: none;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.68rem;
    font-weight: 600;
    color: var(--gray-500);
}
.app-sidebar-group-head::-webkit-details-marker { display: none; }
.app-sidebar-group-head:hover { color: var(--gray-900); }
.app-sidebar-caret {
    margin-left: auto;
    font-size: 0.68rem !important;
    width: auto !important;
    transition: transform 0.15s;
}
.app-sidebar-group[open] > .app-sidebar-group-head .app-sidebar-caret { transform: rotate(180deg); }
.app-sidebar-group-body {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding-left: var(--space-2);
}

.app-sidebar-footer {
    padding: var(--space-2);
    border-top: 1px solid var(--gray-100);
}

.app-topbar {
    height: var(--topbar-h);
    background: #fff;
    border-bottom: 1px solid var(--gray-200);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 0 var(--space-5);
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1045;
}

.app-sidebar-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    font-size: 1.4rem;
    color: var(--gray-700);
    cursor: pointer;
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    flex-shrink: 0;
    transition: background 0.15s, color 0.15s;
}
.app-sidebar-toggle:hover { background: var(--gray-100); color: var(--gray-900); }

.app-main {
    flex: 1;
    min-width: 0;
    margin-left: var(--sidebar-w);
    margin-top: var(--topbar-h);
    display: flex;
    flex-direction: column;
    transition: margin-left 0.2s ease;
}

/* Área de contenido de los portales de escritorio (admin / empresa):
   FLUIDA A ANCHO COMPLETO por defecto. Flexa para llenar el espacio tras el
   sidebar y provee el ÚNICO padding interno consistente (~24px), para que el
   contenido no toque el borde pero tampoco quede acotado como card centrada.
   El rider NO usa este contenedor (mantiene su .page-wrap mobile-first). */
.app-content {
    flex: 1;
    /* El padding del borde se publica como variable porque hay contenido que
       necesita SANGRAR hasta el borde de la pantalla —las filas de acciones y
       de pills que scrollean en el teléfono— y cancelarlo con un margen negativo
       escrito a ojo es la manera de que dejen de coincidir cuando este valor
       cambie. */
    --content-pad: var(--space-6);
    padding: var(--content-pad);
}
/* En un teléfono de 360px, 24px por lado son 48px (el 13% del ancho) que se
   suman a los 20px de .cp-card-body a cada lado: el contenido útil de un
   formulario quedaba en ~272px. El padding del borde es el multiplicador de
   todos los desbordes de dentro. El escritorio no se toca.
   El corte era `575.98px` (donde Bootstrap deja de tener columnas `-sm`) y pasó
   a `767.98px`, el mismo de la tanda del dispatcher en el teléfono: las tablas
   densas y las barras de filtros ya no entran en una tableta en vertical, y con
   dos cortes distintos el sangrado de las filas que scrollean —que cancela
   justo este padding— quedaba corto por 12px en esa franja. */
@media (max-width: 767.98px) {
    .app-content { --content-pad: var(--space-3); }
}

/* Neutraliza los contenedores de ancho máximo de Bootstrap dentro del shell:
   pasan a ocupar el 100% y ceden el padding a .app-content (así el borde de
   contenido queda alineado a ~24px sea .container o .container-fluid, y las
   tablas/grids ocupan todo el ancho). Fuera del shell (login/index/errores)
   Bootstrap sigue centrando normal. */
.app-shell .app-content > .container,
.app-shell .app-content > .container-fluid,
.app-shell .app-content > .container-sm,
.app-shell .app-content > .container-md,
.app-shell .app-content > .container-lg,
.app-shell .app-content > .container-xl,
.app-shell .app-content > .container-xxl,
.app-shell .app-content .container,
.app-shell .app-content .container-sm,
.app-shell .app-content .container-md,
.app-shell .app-content .container-lg,
.app-shell .app-content .container-xl,
.app-shell .app-content .container-xxl,
.app-shell .app-content .container-fluid {
    max-width: none;
    width: 100%;
    margin-right: 0;
    margin-left: 0;
    padding-right: 0;
    padding-left: 0;
}

/* El portal empresa envuelve todo en .page-wrap (max-width centrado). Dentro
   del shell de escritorio también debe ser fluido a ancho completo; el rider
   conserva su .page-wrap acotado vía body.role-rider más abajo. */
.app-content .page-wrap {
    max-width: none;
    margin: 0;
    padding: 0;
}

/* Ancho de lectura cómodo opcional para bloques de texto largo y formularios
   dentro del área fluida (se aplica por pantalla cuando convenga). */
.readable-col { max-width: 760px; }
.readable-col-wide { max-width: 1100px; }

.app-sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.35);
    z-index: 1035;
}

@media (max-width: 991px) {
    .app-sidebar {
        transform: translateX(-100%);
        box-shadow: 2px 0 12px rgba(0,0,0,0.12);
    }
    .app-sidebar.open { transform: translateX(0); }
    .app-sidebar.open ~ .app-sidebar-backdrop,
    .app-sidebar-backdrop.open { display: block; }
    .app-main { margin-left: 0; }
}

/* Colapso horizontal del sidebar en desktop (admin/company), estilo panel
   deslizable — el toggle de la topbar alterna esta clase en .app-shell. */
@media (min-width: 992px) {
    .app-shell.sidebar-collapsed .app-sidebar {
        transform: translateX(-100%);
    }
    .app-shell.sidebar-collapsed .app-main {
        margin-left: 0;
    }
}

/* ==========================================================================
   Chrome oscuro del dispatcher (SOLO admin — body.role-admin)
   Sidebar + topbar en gris azulado oscuro tipo "command center". El
   contenido sigue en claro. La empresa (role-company) NO se ve afectada:
   mantiene su sidebar claro y acento verde. No es un tema oscuro global ni
   hay toggle claro/oscuro.
   ========================================================================== */
body.role-admin .app-sidebar {
    background: var(--chrome-bg);
    border-right-color: var(--chrome-border);
}
body.role-admin .app-sidebar-brand {
    color: var(--chrome-text-strong);
    border-bottom-color: var(--chrome-border);
}
body.role-admin .app-sidebar-brand:hover { color: #fff; }
body.role-admin .app-sidebar-company {
    color: var(--chrome-muted);
    border-bottom-color: var(--chrome-border);
}
body.role-admin .app-sidebar-link { color: var(--chrome-text); }
body.role-admin .app-sidebar-link i { color: var(--chrome-muted); }
body.role-admin .app-sidebar-link:hover { background: var(--chrome-hover-bg); color: #fff; }
body.role-admin .app-sidebar-link:hover i { color: #fff; }
body.role-admin .app-sidebar-link.active { background: var(--chrome-active-bg); color: #fff; }
body.role-admin .app-sidebar-link.active i { color: #fff; }
body.role-admin .app-sidebar-link.danger,
body.role-admin .app-sidebar-link.danger i { color: #f87171; }
body.role-admin .app-sidebar-link.danger:hover { background: rgba(248,113,113,0.14); color: #fca5a5; }
body.role-admin .app-sidebar-nav { scrollbar-color: rgba(255,255,255,0.2) transparent; }
body.role-admin .app-sidebar-group-head,
body.role-admin .app-sidebar-group-head i { color: var(--chrome-muted); }
body.role-admin .app-sidebar-group-head:hover,
body.role-admin .app-sidebar-group-head:hover i { color: #fff; }

body.role-admin .app-sidebar-footer { border-top-color: var(--chrome-border); }

/* El nombre de quien está dentro: un enlace a Configuración → Cuenta, con su
   foto si la tiene. Hereda el color del chrome; el tamaño de la foto lo dan
   sus atributos `width`/`height`. */
.app-topbar-user {
    display: inline-flex; align-items: center; gap: 0.5rem;
    color: inherit; text-decoration: none; min-width: 0;
}
.app-topbar-user:hover { color: inherit; text-decoration: underline; }
.app-topbar-avatar { border-radius: 50%; object-fit: cover; }

body.role-admin .app-topbar {
    background: var(--chrome-bg-2);
    border-bottom-color: var(--chrome-border);
    color: var(--chrome-text);
}
body.role-admin .app-topbar .text-secondary { color: var(--chrome-muted) !important; }
body.role-admin .app-sidebar-toggle { color: var(--chrome-text); }
body.role-admin .app-sidebar-toggle:hover { background: var(--chrome-hover-bg); color: #fff; }
/* Buscador global sobre topbar oscura */
body.role-admin .navbar-search-wrap input[type="search"] {
    background: rgba(255,255,255,0.08);
    border-color: var(--chrome-border);
    color: var(--chrome-text-strong);
}
body.role-admin .navbar-search-wrap input[type="search"]::placeholder { color: var(--chrome-muted); }
body.role-admin .navbar-search-wrap input[type="search"]:focus {
    background: rgba(255,255,255,0.12);
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(74,144,226,0.20);
}
/* Marca en la topbar oscura */
body.role-admin .app-brand { color: var(--chrome-text-strong); }
body.role-admin .app-brand:hover { color: #fff; }
body.role-admin .app-brand .bi { color: var(--primary); }

/* ==========================================================================
   Tema oscuro del ÁREA CENTRAL del dispatcher (SOLO admin — body.role-admin)
   El contenido central acompaña al chrome (mismo #1f2937), como pidió el
   diseño "command center". Se resuelve acá, en la base, para TODAS las
   páginas admin sin tocar plantilla por plantilla.

   Estrategia: (1) remapear la escala de grises y los tokens de superficie a
   equivalentes oscuros — así todas las clases propias que ya usan var(--gray-*)
   /var(--surface) (cp-table, kpi-card, ops-*, queue-*, disp-*, timeline,
   steppers, etc.) se invierten solas; (2) overrides puntuales para los
   componentes de Bootstrap, que usan sus PROPIAS variables --bs-* y no se
   enteran del remapeo. La empresa (role-company) y el rider NO se ven
   afectados: este bloque está enteramente bajo body.role-admin.
   ========================================================================== */
body.role-admin {
    /* Grises usados como TEXTO → tonos claros */
    --gray-900: #f1f5f9;
    --gray-700: #cbd5e1;
    --gray-500: #94a3b8;
    --gray-400: #7f8ea3;
    /* Grises usados como BORDES / FONDOS suaves → blanco translúcido */
    --gray-200: rgba(255,255,255,0.12);
    --gray-100: rgba(255,255,255,0.07);
    --gray-50:  rgba(255,255,255,0.04);
    /* Superficies / elevación en oscuro */
    --surface: #253243;          /* cards/tablas: un paso sobre el fondo */
    --surface-sunken: #1f2937;   /* fondo de app (igual que el chrome) */
    --surface-raised: #2a3849;   /* dropdowns/menús */
    --border-subtle: rgba(255,255,255,0.10);
    --primary-light: rgba(74,144,226,0.18);  /* fondos "activos" azules */
    /* Los `-ink` del código de parada están calibrados para fondo CLARO (el
       ámbar puro no llega al contraste mínimo como texto sobre blanco). Sobre
       el chrome oscuro pasa lo contrario: #B45309 sobre #253243 es ilegible.
       Se remapean junto con los grises, por el mismo motivo y en el mismo
       sitio — el color base (el RELLENO de un pin o una pastilla) no se toca,
       porque un pin verde tiene que ser el mismo verde en las dos pantallas. */
    --stop-pickup-ink: #4ADE80;
    --stop-drop-ink: #FBBF24;
    --stop-handoff-ink: #C4B5FD;
    --stop-fail-ink: #FCA5A5;
    background-color: #1f2937;
}
body.role-admin .app-main { background: #1f2937; }
body.role-admin .app-content { color: #e5eaf0; }

/* Utilidades de texto tenue de Bootstrap (usan --bs-*, no nuestros grises) */
body.role-admin .app-content .text-muted,
body.role-admin .app-content .text-secondary { color: #94a3b8 !important; }

/* Fondos claros utilitarios → neutros dentro del área oscura */
body.role-admin .app-content .bg-white,
body.role-admin .app-content .bg-light { background-color: rgba(255,255,255,0.05) !important; }
body.role-admin .app-content .badge.bg-light { background-color: rgba(255,255,255,0.14) !important; }

/* Cards genéricas: degradado sutil + borde translúcido (como el ejemplo) */
body.role-admin .card,
body.role-admin .cp-card {
    background: linear-gradient(160deg, #2a3849 0%, #212e3d 100%);
    border: 1px solid rgba(255,255,255,0.08);
    box-shadow: 0 1px 3px rgba(0,0,0,0.35);
    color: #e5eaf0;
}
body.role-admin .card-header,
body.role-admin .cp-card-header {
    background: transparent;
    border-bottom-color: rgba(255,255,255,0.08);
    color: #e8eef5;
}
/* Pie de card: Bootstrap le pone su propio fondo y un borde OSCURO desde sus
   --bs-*, que sobre el chrome oscuro es una línea invisible. Mismo trato que la
   cabecera — el remapeo de --gray-* no llega a los componentes de Bootstrap. */
body.role-admin .card-footer {
    background: transparent;
    border-top-color: rgba(255,255,255,0.08);
    color: #e8eef5;
}

/* KPI-cards como en el ejemplo (degradado + label en mayúsculas) */
body.role-admin .kpi-card {
    background: linear-gradient(160deg, #2c3c50 0%, #212e3d 100%);
    border: 1px solid rgba(255,255,255,0.09);
    box-shadow: 0 1px 3px rgba(0,0,0,0.35);
}
body.role-admin .kpi-label {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.72rem;
    font-weight: 600;
    color: #93a1b3;
}
body.role-admin .kpi-value { color: #f8fafc; }
body.role-admin .kpi-eye { color: #93a1b3; }
body.role-admin .kpi-eye:hover { color: #f8fafc; background: rgba(255,255,255,0.08); }
body.role-admin .kpi-card-secret.is-hidden .kpi-secret::after { color: #93a1b3; }

/* La fila de trabajo de la torre de control. `.dash-panel` ya se invierte sola
   (usa --surface / --gray-*); lo que hace falta acá es el mismo degradado que
   las cards, el violeta de los km un punto más claro para que no se apague
   sobre el chrome, y el botón de rango elegido, que sobre oscuro necesita
   contraste propio: `--surface` es casi el mismo tono que el riel. */
body.role-admin {
    --metric-km: #A78BFA;
}
body.role-admin .dash-panel {
    background: linear-gradient(160deg, #2a3849 0%, #212e3d 100%);
    border: 1px solid rgba(255,255,255,0.08);
    box-shadow: 0 1px 3px rgba(0,0,0,0.35);
}
body.role-admin .dash-kpi {
    background: linear-gradient(160deg, #2c3c50 0%, #212e3d 100%);
    border: 1px solid rgba(255,255,255,0.09);
    box-shadow: 0 1px 3px rgba(0,0,0,0.35);
}
body.role-admin .dash-kpi-value { color: #f8fafc; }
body.role-admin .dash-range button.is-on {
    background: rgba(255,255,255,0.16);
    color: #f8fafc;
    box-shadow: none;
}
body.role-admin .dl-avatar-ph { background: rgba(255,255,255,0.10); }
body.role-admin .dash-eye:hover { color: #f8fafc; }

/* Números de alerta más brillantes sobre oscuro */
body.role-admin .ops-cell.alert-amber .ops-num,
body.role-admin .disp-timer.warn { color: #fbbf24; }

/* Tablas de Bootstrap (usan --bs-*).
   OJO: la regla base `.table { background: white }` (más abajo) pinta el
   ELEMENTO tabla de blanco; como acá las celdas quedan transparentes para
   mostrar la card oscura, hay que neutralizar ese fondo blanco o el <tbody>
   se ve blanco. */
body.role-admin .app-content .table {
    background: transparent;
    --bs-table-bg: transparent;
    --bs-table-color: #dbe2ea;
    --bs-table-border-color: rgba(255,255,255,0.08);
    --bs-table-striped-color: #dbe2ea;
    --bs-table-striped-bg: rgba(255,255,255,0.03);
    --bs-table-hover-color: #f1f5f9;
    --bs-table-hover-bg: rgba(255,255,255,0.05);
    color: #dbe2ea;
}
body.role-admin .app-content .table > thead th,
body.role-admin .app-content thead.bg-white,
body.role-admin .app-content thead.sticky-top.bg-white,
body.role-admin .app-content thead.sticky-top.bg-white th {
    background-color: #212e3d !important;
    color: #94a3b8;
    border-color: rgba(255,255,255,0.12);
}
/* Variantes contextuales de Bootstrap (usan --bs-table-bg claro por celda):
   thead.table-light, th/td .table-primary/.table-active/.table-secondary →
   fondo oscuro + texto claro, para que no queden recuadros blancos. */
body.role-admin .app-content .table-light,
body.role-admin .app-content .table-light > th,
body.role-admin .app-content .table-light > td,
body.role-admin .app-content .table-active,
body.role-admin .app-content .table-secondary {
    --bs-table-bg: #212e3d;
    --bs-table-color: #cbd5e1;
    background-color: #212e3d !important;
    color: #cbd5e1 !important;
    border-color: rgba(255,255,255,0.12);
}
body.role-admin .app-content .table-primary {
    --bs-table-bg: rgba(74,144,226,0.20);
    --bs-table-color: #dbe9fb;
    background-color: rgba(74,144,226,0.20) !important;
    color: #dbe9fb !important;
    border-color: rgba(74,144,226,0.35);
}

/* Formularios de Bootstrap */
body.role-admin .app-content .form-control,
body.role-admin .app-content .form-select {
    background-color: rgba(255,255,255,0.05);
    border-color: rgba(255,255,255,0.14);
    color: #f1f5f9;
}
body.role-admin .app-content .form-control::placeholder { color: #7f8ea3; }
body.role-admin .app-content .form-control:focus,
body.role-admin .app-content .form-select:focus {
    background-color: rgba(255,255,255,0.08);
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(74,144,226,0.20);
    color: #fff;
}
/* El desplegable nativo del select lo pinta el SO tomando el fondo del propio
   <select>, que acá es un blanco casi transparente: o sea, lista CLARA. Por eso
   sus filas van con texto oscuro escrito explícito — sin esto heredarían el
   #f1f5f9 del control y serían blanco sobre blanco.
   El <optgroup> necesita el suyo: es el que agrupa las rutas por rider en los
   destinos del mapa, y sin regla propia su rótulo hereda el color del select y
   desaparece — la lista quedaba con las rutas y sin saber de quién son. El fondo
   se declara también, para no depender de cómo componga cada navegador. */
body.role-admin .app-content .form-select option,
body.role-admin .app-content .form-select optgroup {
    color: #1f2937; background-color: #fff;
}
body.role-admin .app-content .form-label,
body.role-admin .app-content .col-form-label { color: #cbd5e1; }
body.role-admin .app-content .form-text { color: #94a3b8; }
body.role-admin .app-content .form-check-input {
    background-color: rgba(255,255,255,0.08);
    border-color: rgba(255,255,255,0.25);
}
body.role-admin .app-content .form-check-input:checked {
    background-color: var(--primary);
    border-color: var(--primary);
}
body.role-admin .app-content .input-group-text {
    background-color: rgba(255,255,255,0.06);
    border-color: rgba(255,255,255,0.14);
    color: #cbd5e1;
}

/* Listas, separadores y bordes utilitarios */
body.role-admin .app-content .list-group-item {
    background-color: transparent;
    border-color: rgba(255,255,255,0.08);
    color: #dbe2ea;
}
body.role-admin .app-content hr { border-top-color: rgba(255,255,255,0.14); opacity: 1; }
body.role-admin .app-content .border,
body.role-admin .app-content .border-top,
body.role-admin .app-content .border-bottom,
body.role-admin .app-content .border-start,
body.role-admin .app-content .border-end { border-color: rgba(255,255,255,0.12) !important; }

/* Filas CLICABLES de una lista (picker de productos, etc.): Bootstrap las pinta
   con sus --bs-list-group-action-*, que no se enteran del remapeo. */
body.role-admin .app-content .list-group-item-action:hover,
body.role-admin .app-content .list-group-item-action:focus {
    background-color: rgba(255,255,255,0.07);
    color: #f8fafc;
}
body.role-admin .app-content .list-group-item-action:disabled,
body.role-admin .app-content .list-group-item-action.disabled {
    background-color: transparent;
    color: #7f8ea3;
}

/* Modales de Bootstrap DENTRO del área oscura.
   .modal-content usa sus propias --bs-modal-* y no se entera del remapeo de
   arriba: quedaba BLANCO mientras el texto y los .list-group-item de adentro
   ya venían claros/transparentes — texto claro sobre fondo blanco = ilegible
   (es lo que pasaba con el modal "Ver catálogo").
   El modal de confirmación de base.html vive FUERA de .app-content y sigue en
   claro a propósito: es global (también lo usan empresa y rider). */
body.role-admin .app-content .modal-content {
    background: linear-gradient(160deg, #2a3849 0%, #212e3d 100%);
    border: 1px solid rgba(255,255,255,0.10);
    border-radius: var(--radius-lg);
    box-shadow: 0 20px 60px rgba(0,0,0,0.55);
    color: #e5eaf0;
}
body.role-admin .app-content .modal-header,
body.role-admin .app-content .modal-footer { border-color: rgba(255,255,255,0.08); }
body.role-admin .app-content .modal-title { color: #f1f5f9; }
body.role-admin .app-content .modal-title .bi { color: var(--primary); }
/* El ✕ de Bootstrap es un SVG negro incrustado: se invierte para que se vea */
body.role-admin .app-content .btn-close {
    filter: invert(1) grayscale(100%) brightness(200%);
    opacity: 0.7;
}
body.role-admin .app-content .btn-close:hover { opacity: 1; }

/* Dropdowns de Bootstrap sobre oscuro */
body.role-admin .dropdown-menu {
    background-color: var(--surface-raised);
    border-color: rgba(255,255,255,0.10);
}
body.role-admin .dropdown-item { color: #dbe2ea; }
body.role-admin .dropdown-item:hover,
body.role-admin .dropdown-item:focus { background-color: rgba(255,255,255,0.06); color: #fff; }
body.role-admin .dropdown-divider { border-top-color: rgba(255,255,255,0.10); }

/* Footer */
footer {
    background: none;
    padding: var(--space-6) 0;
}

/* ==========================================================================
   Componentes genéricos
   ========================================================================== */

/* Cards */
.card {
    border: none;
    border-radius: var(--radius-lg);
    box-shadow: 0 2px 8px rgba(0,0,0,0.06);
    margin-bottom: 1.5rem;
}
.card-header {
    background: white;
    border-bottom: 1px solid #e9ecef;
    font-weight: 600;
    padding: 1.25rem;
}

/* Card del portal empresa */
.cp-card {
    background: #fff;
    border-radius: var(--radius-lg);
    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
    margin-bottom: var(--space-5);
    overflow: hidden;
}
.cp-card-header {
    padding: 0.875rem 1.125rem;
    border-bottom: 1px solid var(--gray-100);
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--gray-700);
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.cp-card-body { padding: var(--space-5); }

/* Buttons */
.btn {
    border-radius: var(--radius-md);
    font-weight: 500;
    padding: 0.5rem 1.25rem;
    transition: all 0.2s;
}
.btn-primary { background: var(--primary); border: none; }
.btn-primary:hover {
    background: var(--primary-dark);
    transform: translateY(-1px);
    box-shadow: 0 4px 8px rgba(74, 144, 226, 0.3);
}
.btn-success { background: var(--success); border: none; }
.btn-danger { background: var(--danger); border: none; }
.btn-warning { background: var(--warning); border: none; color: white; }

/* Botón con acento del portal empresa */
.btn-cp {
    background: var(--cp);
    color: #fff;
    border: none;
    border-radius: var(--radius-md);
    font-weight: 500;
}
.btn-cp:hover { background: var(--cp-dark); color: #fff; }

/* Badges */
.badge {
    font-weight: 500;
    padding: 0.4rem 0.8rem;
    border-radius: var(--radius-sm);
}

/* Tables */
.table {
    background: white;
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.table thead th {
    background: #f8f9fa;
    border-bottom: 2px solid #dee2e6;
    font-weight: 600;
    text-transform: uppercase;
    font-size: 0.85rem;
    letter-spacing: 0.5px;
    color: var(--dark);
}

/* Cifras tabulares en tablas y métricas: columnas de números alineadas,
   sin "saltos" al actualizar timers/contadores. */
.table,
.cp-table,
.kpi-value,
.stat-cell-num,
.stat-card h3,
.session-timer,
.session-duration,
.price-tag,
.tnum { font-variant-numeric: var(--num); }

/* Tabla compacta del portal empresa */
.cp-table { width: 100%; border-collapse: collapse; }
.cp-table th {
    text-align: left;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-400);
    padding: 0 0.75rem 0.625rem;
    border-bottom: 1px solid var(--gray-100);
}
.cp-table td {
    padding: 0.75rem;
    font-size: 0.875rem;
    color: var(--gray-700);
    border-bottom: 1px solid var(--gray-50);
    vertical-align: middle;
}
.cp-table tr:last-child td { border-bottom: none; }
.cp-table tr.clickable { cursor: pointer; }
.cp-table tr.clickable:hover td { background: var(--gray-50); }
.cp-table td.truncate {
    max-width: 12.5rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cp-table td.muted { color: var(--gray-400); font-size: 0.82rem; }

/* Stats */
.stat-card {
    background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
    color: white;
    border-radius: var(--radius-lg);
    padding: 1.5rem;
    margin-bottom: 1.5rem;
}
.stat-card.success { background: linear-gradient(135deg, var(--success) 0%, #229954 100%); }
.stat-card.warning { background: linear-gradient(135deg, var(--warning) 0%, #ca8b0a 100%); }
.stat-card.danger { background: linear-gradient(135deg, var(--danger) 0%, #c0392b 100%); }
.stat-card h3 { font-size: 2.5rem; font-weight: 700; margin: 0; }
.stat-card p { margin: 0; opacity: 0.9; font-size: 0.95rem; }

.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-6);
}
/* Variante compacta: las 4 celdas SIEMPRE en una sola fila (dashboard rider,
   móvil). Cifras y paddings reducidos para que entren sin desbordar. */
.stat-grid--4 {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-2);
}
.stat-grid--4 .stat-cell { padding: 12px 5px; }
.stat-grid--4 .stat-cell-icon { font-size: 0.9rem; margin-bottom: 5px; }
.stat-grid--4 .stat-cell-num { font-size: 1.3rem; }
.stat-grid--4 .stat-cell-label {
    font-size: 0.58rem;
    letter-spacing: 0.01em;
    line-height: 1.15;
}
/* Variante compacta de 3 columnas SIEMPRE en una fila (jornadas del rider:
   Mi Jornada y detalle del día). Los valores son tipo "1h 30m", así que el
   número es algo más chico para que entren sin envolver. */
.stat-grid--3 {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-2);
}
.stat-grid--3 .stat-cell { padding: 12px 6px; }
.stat-grid--3 .stat-cell-num { font-size: 1.1rem; }
.stat-grid--3 .stat-cell-label {
    font-size: 0.6rem;
    letter-spacing: 0.01em;
    line-height: 1.15;
}

/* Grid de 2 columnas genérico para formularios largos (Tanda 6.6) — evita
   scroll interminable en pantallas de creación/edición. Colapsa a 1 columna
   en el mismo breakpoint donde .app-sidebar ya colapsa a mobile (991px). */
.mess-form-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
}
.mess-form-grid > .mess-form-col {
    min-width: 0;
}
@media (max-width: 991.98px) {
    .mess-form-grid {
        grid-template-columns: 1fr;
    }
}
.stat-cell {
    background: #fff;
    border-radius: var(--radius-lg);
    padding: 16px 12px;
    text-align: center;
    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
/* Ícono tenue de encabezado de la celda (escaneo rápido, sin ruido de color) */
.stat-cell-icon {
    display: block;
    font-size: 1.05rem;
    line-height: 1;
    margin-bottom: 8px;
    color: var(--gray-400);
}
.stat-cell-icon.green { color: var(--cp); opacity: 0.85; }
.stat-cell-num {
    display: block;
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--gray-900);
    line-height: 1;
    margin-bottom: 4px;
}
.stat-cell-num.green { color: var(--cp); }
.stat-cell-num.orange { color: #d97706; }
.stat-cell-label {
    display: block;
    font-size: 0.68rem;
    color: var(--gray-400);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Alerts */
.alert { border: none; border-radius: var(--radius-md); font-weight: 500; }

/* Forms */
.form-control, .form-select {
    border-radius: var(--radius-md);
    border: 1px solid #dee2e6;
    padding: 0.625rem 0.875rem;
}
.form-control:focus, .form-select:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 0.2rem rgba(74, 144, 226, 0.15);
}
.form-label { font-weight: 500; color: var(--gray-700); margin-bottom: 0.5rem; }

/* Search bar (genérica, usada en topbar) */
.search-container { position: relative; }
.search-container input { padding-left: 2.5rem; }
.search-container i {
    position: absolute;
    left: 0.875rem;
    top: 50%;
    transform: translateY(-50%);
    color: var(--gray-500);
}

/* Status badges — ÚNICA fuente visual de color de estado. El hue de cada
   estado sale de las variables --status-* de :root (paleta canónica, espejo
   de STATUS_HEX en dashboard_service.py). El texto/clase de estado viene de
   Order.status/status_display (modelo). Los fondos son tintes claros del
   mismo hue; el borde izquierdo usa la variable canónica para acoplarlos
   visualmente al color sólido de las gráficas. */
.status-badge {
    padding: 0.35rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: 0.875rem;
    font-weight: 500;
    font-variant-numeric: var(--num);
    border-left: 3px solid transparent;
}
.status-pendiente { background: #fef3c7; color: #92400e; border-left-color: var(--status-pendiente); }
.status-asignado  { background: #dbeafe; color: #1e40af; border-left-color: var(--status-asignado); }
.status-en_sitio,
.status-recogido,
.status-en_camino { background: #cffafe; color: #155e75; border-left-color: var(--status-progress); }
.status-entregado { background: #d1fae5; color: #065f46; border-left-color: var(--status-entregado); }
.status-cancelado,
.status-demorado  { background: #fee2e2; color: #991b1b; border-left-color: var(--status-cancelado); }
.status-por_aceptar { background: #e0e7ff; color: #3730a3; border-left-color: var(--status-por-aceptar); }
/* Rechazado: el rojo del cancelado en el borde, sobre gris — se lee «cerrado sin
   hacerse», pero no se confunde con un trabajo que se canceló. */
.status-rechazado { background: #f1f5f9; color: #475569; border-left-color: var(--status-cancelado); }

/* Los MOTIVOS de un estado, al pasar por encima (`_order_status_cell.html`): el
   del rechazo, el de la cancelación y la lista de paradas fallidas. No ocupan
   sitio en la fila: una cajita pequeña que aparece con el cursor, el foco o el
   toque. Solo CSS. Superficie propia (flota sobre la tabla) y color de tokens. */
.status-tip { position: relative; display: inline-block; cursor: help; outline: none; }
.status-tip-box {
    display: none; position: absolute; z-index: 30; top: calc(100% + 0.25rem); left: 0;
    width: max-content; max-width: 16rem; padding: 0.4375rem 0.5625rem;
    background: var(--surface-raised, var(--surface)); color: var(--gray-900);
    border: 1px solid var(--gray-200); border-radius: var(--radius-sm);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
    font-size: 0.74rem; line-height: 1.35; font-weight: 400; text-align: left; white-space: normal;
}
.status-tip:hover .status-tip-box,
.status-tip:focus .status-tip-box,
.status-tip:focus-within .status-tip-box { display: block; }
.status-tip-title { display: block; font-weight: 700; color: var(--gray-500);
                    text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.64rem; }
.status-tip-line { display: block; overflow-wrap: anywhere; }
.status-tip-item { display: block; margin-top: 0.3125rem; padding-top: 0.3125rem;
                   border-top: 1px solid var(--gray-200); }
.status-tip-title + .status-tip-item { border-top: 0; padding-top: 0; }
.status-tip-addr { display: block; font-weight: 600; }

/* Las acciones de un pedido en la cabecera de su ficha del portal: Hablar ·
   Editar · Cancelar, juntas. */
.cp-order-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.cp-order-actions form { margin: 0; }

/* Session control (rider) */
.session-control {
    background: white;
    border-radius: var(--radius-lg);
    padding: 1.5rem;
    box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.session-timer {
    font-size: 3rem;
    font-weight: 700;
    color: var(--primary);
    text-align: center;
    margin: 1rem 0;
}
.session-status { display: inline-block; padding: 0.5rem 1rem; border-radius: 20px; font-weight: 600; }
.session-active { background: #d4edda; color: #155724; }
.session-paused { background: #fff3cd; color: #856404; }

/* Empty state (usado en admin/rider: .empty-state, rider: .empty-card, empresa: .cp-empty) */
.empty-state, .empty-card, .cp-empty {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--gray-400);
}
.empty-state i, .empty-card i, .cp-empty i {
    font-size: 3rem;
    margin-bottom: 0.75rem;
    display: block;
    opacity: 0.5;
}
.empty-state p, .empty-card p, .cp-empty p { font-size: 0.9rem; margin: 0 0 1rem; }
.empty-card { background: #fff; border-radius: var(--radius-xl); box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.cp-empty { padding: 3rem 1rem; }

/* Highlight card (gradiente de marca) */
.highlight-card {
    background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
    color: #fff;
    border-radius: var(--radius-xl);
    padding: 18px;
    margin-bottom: var(--space-4);
    box-shadow: 0 4px 16px rgba(74,144,226,0.35);
}

/* Page header genérico (reemplaza duplicados en rider_base/company_base) */
.page-wrap { max-width: 900px; margin: 0 auto; padding: var(--space-6) var(--space-4) 40px; }
.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
    flex-wrap: wrap;
}
.page-title { font-size: 1.25rem; font-weight: 700; color: var(--gray-900); margin: 0; }
.page-subtitle { font-size: 0.82rem; color: var(--gray-500); margin: 2px 0 0; }
.section-label {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--gray-400);
    margin-bottom: 10px;
}
/* Rótulo de sección con aire arriba, para separar grupos apilados (rider) */
.section-label--spaced { margin-top: 22px; }

/* Detail row (label + valor con ícono) */
.detail-row { display: flex; gap: 8px; margin-bottom: 10px; font-size: 0.875rem; }
.detail-row i { color: var(--gray-400); flex-shrink: 0; margin-top: 2px; }
.detail-row i.cp-scope { color: var(--cp); }
.detail-label { font-weight: 600; color: var(--gray-700); min-width: 110px; }

/* Price */
.price-tag { font-weight: 600; color: var(--gray-900); }
.price-pending { color: var(--gray-400); font-style: italic; font-size: 0.82rem; }

/* ===== Session card (jornadas: rider/sessions.html y rider/shift_day_detail.html) ===== */
.session-card {
    background: #fff;
    border-radius: 12px;
    padding: 14px 16px;
    margin-bottom: 8px;
    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.session-card-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 12px;
}
.session-date {
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--dark);
    margin-bottom: 2px;
}
.session-time {
    font-size: 0.8rem;
    color: #6b7280;
}
.session-card-right {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    flex-shrink: 0;
}
.session-duration {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--dark);
}
.session-break {
    font-size: 0.75rem;
    color: #9ca3af;
    margin-top: 7px;
    display: flex;
    align-items: center;
    gap: 4px;
    border-top: 1px solid #f3f4f6;
    padding-top: 7px;
}
/* Bloque de varias líneas (extras/notas) dentro de una session-card */
.session-lines {
    font-size: 0.75rem;
    color: #9ca3af;
    margin-top: 7px;
    border-top: 1px solid #f3f4f6;
    padding-top: 7px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* Encabezado de tabla clickeable para ordenar (empresa) */
.cp-sort-link {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    color: inherit;
    text-decoration: none;
}
.cp-sort-link:hover { color: var(--cp); }
.cp-sort-link.active { color: var(--cp); font-weight: 700; }

/* Badge "más usado" (catálogo de empresa) */
.cp-badge-hot {
    display: inline-flex;
    align-items: center;
    gap: 0.1875rem;
    font-size: 0.68rem;
    font-weight: 600;
    background: #fef3c7;
    color: #92400e;
    border-radius: 4px;
    padding: 1px 0.375rem;
    margin-left: 0.375rem;
    vertical-align: middle;
}

/* Mini stats en el subtítulo del dashboard de empresa */
.cp-mini-stat {
    display: inline-flex;
    align-items: center;
    font-size: 0.8rem;
    color: var(--gray-500);
    margin-right: 0.75rem;
}
.cp-mini-stat.green { color: var(--cp); font-weight: 600; }
.cp-mini-stat.orange { color: #d97706; font-weight: 600; }

/* Chip / filtro clickeable */
.chip-filter {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    border-radius: 20px;
    padding: 6px 14px;
    font-size: 0.82rem;
    font-weight: 500;
    cursor: pointer;
    text-decoration: none;
    border: 1.5px solid var(--gray-200);
    background: #fff;
    color: var(--gray-700);
    white-space: nowrap;
    flex-shrink: 0;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.1s;
}
.chip-filter:hover { background: var(--gray-100); color: var(--gray-700); }
.chip-filter.active { background: var(--primary); border-color: var(--primary); color: #fff; }
.chip-filter.active:hover { background: var(--primary); color: #fff; }
.chip-filter.cp-scope.active { background: var(--cp); border-color: var(--cp); color: #fff; }
.chip-filter.cp-scope.active:hover { background: var(--cp); color: #fff; }

/* Image thumbnail grid (portal empresa) */
.cp-img-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    gap: var(--space-3);
}
.cp-img-wrap { text-align: center; }
.cp-img-thumb {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: var(--radius-md);
    border: 1px solid var(--gray-200);
    display: block;
}
.cp-img-caption { font-size: 0.7rem; color: var(--gray-500); margin: 0.25rem 0 0; }

/* Pagination (portal empresa) */
.cp-pagination { display: flex; justify-content: center; gap: 0.375rem; margin-top: var(--space-5); }
.cp-pagination a, .cp-pagination span {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.125rem;
    height: 2.125rem;
    border-radius: var(--radius-md);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: none;
    border: 1px solid var(--gray-200);
    color: var(--gray-700);
    background: #fff;
}
.cp-pagination a:hover { background: var(--gray-100); }
.cp-pagination .current { background: var(--cp); color: #fff; border-color: var(--cp); }
.cp-pagination .disabled { color: #d1d5db; pointer-events: none; }

/* Settings list (filas tipo iOS, usado en rider) */
.settings-list {
    background: #fff;
    border-radius: var(--radius-lg);
    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
    overflow: hidden;
    margin-bottom: var(--space-4);
}
.settings-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border: none;
    border-bottom: 1px solid var(--gray-100);
    background: none;
    text-decoration: none;
    color: var(--dark);
    font-size: 0.9rem;
    font-weight: 500;
    cursor: pointer;
    text-align: left;
    width: 100%;
    transition: background 0.12s;
    -webkit-tap-highlight-color: transparent;
}
.settings-list > *:last-child .settings-row,
.settings-list > a:last-child { border-bottom: none; }
.settings-row:hover { background: #f8f9fa; color: var(--dark); }
.settings-icon { font-size: 1rem; color: var(--primary); width: 20px; flex-shrink: 0; text-align: center; }
.settings-label { flex: 1; }
.settings-chevron { font-size: 0.75rem; color: #c7c7cc; }
.settings-row-danger,
.settings-row-danger .settings-icon { color: var(--danger); }
/* Una fila que solo INFORMA (los avisos con la app cerrada que este navegador
   no puede activar): misma fila, sin cursor de botón y sin la reacción al
   toque, para que no prometa un gesto que no existe. */
.push-note { cursor: default; align-items: flex-start; }
.push-note:hover { background: none; }
.push-note small { font-weight: 400; line-height: 1.3; margin-top: 2px; }
.page-title .order-card-map { vertical-align: middle; }

/* Chip genérico */
.chip { display: inline-flex; align-items: center; gap: 6px; border-radius: 20px; padding: 6px 16px; font-size: 0.82rem; font-weight: 500; }

/* Back crumb (link pequeño encima del page-title, rider/company) */
.back-crumb {
    font-size: 0.75rem;
    color: var(--gray-400);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin-bottom: 4px;
}
.back-crumb:hover { color: var(--primary); }
.back-crumb.cp-scope:hover { color: var(--cp); }

/* Responsive */
@media (max-width: 768px) {
    .stat-card h3 { font-size: 2rem; }
    .session-timer { font-size: 2rem; }
}

/* ==========================================================================
   Toast notifications
   ========================================================================== */
#mess-toast-container {
    position: fixed;
    top: 1rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1065;
    min-width: 300px;
    max-width: 480px;
    width: 90%;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}
.mess-toast {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 13px 15px;
    border-radius: 10px;
    font-size: 0.9rem;
    font-weight: 500;
    box-shadow: 0 4px 20px rgba(0,0,0,0.15);
    pointer-events: auto;
    animation: messToastIn 0.28s ease both;
}
.mess-toast.mess-toast-hiding { animation: messToastOut 0.28s ease both; }
@keyframes messToastIn {
    from { opacity: 0; transform: translateY(-14px); }
    to   { opacity: 1; transform: translateY(0); }
}
@keyframes messToastOut {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(-14px); }
}
.mess-toast-success { background: #d1fae5; color: #065f46; border-left: 4px solid #10b981; }
.mess-toast-danger  { background: #fee2e2; color: #991b1b; border-left: 4px solid #ef4444; }
.mess-toast-warning { background: #fef3c7; color: #92400e; border-left: 4px solid #f59e0b; }
.mess-toast-info    { background: #dbeafe; color: #1e40af; border-left: 4px solid #3b82f6; }
.mess-toast-icon    { flex-shrink: 0; margin-top: 1px; }
.mess-toast-content { flex: 1; line-height: 1.4; }
.mess-toast-close {
    background: none; border: none; cursor: pointer;
    padding: 0 0 0 4px; line-height: 1;
    opacity: 0.55; font-size: 1.25rem; font-weight: 700;
    color: inherit; flex-shrink: 0;
}
.mess-toast-close:hover { opacity: 1; }

/* Confirm modal */
.mess-confirm-content { border-radius: 14px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.2); }

/* ==========================================================================
   Global search (topbar)
   ========================================================================== */
.navbar-search-wrap { position: relative; flex: 1; max-width: 340px; }
.navbar-search-wrap input[type="search"] {
    background: #f5f6fa;
    border: 1px solid var(--gray-200);
    border-radius: 20px;
    padding: 0.35rem 1rem 0.35rem 2.25rem;
    font-size: 0.875rem;
    width: 100%;
    transition: border-color 0.2s, box-shadow 0.2s;
    outline: none;
}
.navbar-search-wrap input[type="search"]:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(74,144,226,0.12);
    background: #fff;
}
.navbar-search-wrap .search-icon {
    position: absolute;
    left: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    color: #aaa;
    font-size: 0.85rem;
    pointer-events: none;
}
#navbar-search-dropdown {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    background: #fff;
    border: 1px solid var(--gray-200);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.10);
    z-index: 9999;
    overflow: hidden;
}
.search-suggestion-item {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.55rem 1rem;
    text-decoration: none;
    color: var(--dark);
    font-size: 0.85rem;
    border-bottom: 1px solid #f0f0f0;
    transition: background 0.15s;
}
.search-suggestion-item:last-child { border-bottom: none; }
.search-suggestion-item:hover { background: #f5f6fa; }
.search-suggestion-item .sug-icon { margin-top: 2px; color: var(--primary); flex-shrink: 0; }
.search-suggestion-item .sug-text { flex: 1; min-width: 0; }
.search-suggestion-item .sug-label { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-suggestion-item .sug-sub { font-size: 0.75rem; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-suggestion-item .sug-type { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; color: #aaa; margin-bottom: 1px; }
.search-no-results { padding: 0.7rem 1rem; font-size: 0.83rem; color: #888; text-align: center; }

@media (max-width: 991px) {
    .navbar-search-wrap { max-width: 220px; }
}
@media (max-width: 640px) {
    .navbar-search-wrap { display: none; }
}

/* ==========================================================================
   Chat FAB + panel flotante (admin / empresa)
   ========================================================================== */
#chat-fab, #company-chat-fab {
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 54px;
    height: 54px;
    border-radius: 50%;
    background: var(--primary);
    color: #fff;
    border: none;
    font-size: 1.35rem;
    cursor: pointer;
    z-index: 1060;
    box-shadow: 0 4px 18px rgba(74,144,226,0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s, transform 0.1s;
}
#chat-fab:hover, #company-chat-fab:hover { background: var(--primary-dark); }
#chat-fab:active, #company-chat-fab:active { transform: scale(0.93); }
.chat-fab-badge {
    position: absolute;
    top: -3px;
    right: -3px;
    background: #ef4444;
    color: #fff;
    border-radius: 20px;
    font-size: 0.62rem;
    font-weight: 700;
    min-width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 4px;
    border: 2px solid #fff;
    line-height: 1;
}
.chat-panel {
    position: fixed;
    bottom: 84px;
    right: 20px;
    width: 360px;
    height: 500px;
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 8px 40px rgba(0,0,0,0.18);
    z-index: 1060;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    animation: chatPanelIn 0.2s ease both;
}
@keyframes chatPanelIn {
    from { opacity: 0; transform: translateY(12px) scale(0.97); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
@media (max-width: 520px) {
    .chat-panel {
        width: calc(100vw - 16px);
        height: calc(100dvh - 90px);
        bottom: 74px;
        right: 8px;
    }
}
.chat-panel-header {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 14px;
    border-bottom: 1px solid #e9ecef;
    flex-shrink: 0;
}
.chat-panel-title { flex: 1; font-weight: 600; font-size: 0.92rem; color: var(--dark); }
.chat-panel-btn {
    background: none;
    border: none;
    color: #9ca3af;
    cursor: pointer;
    font-size: 1rem;
    padding: 2px 4px;
    border-radius: 6px;
    line-height: 1;
    transition: color 0.12s, background 0.12s;
}
.chat-panel-btn:hover { color: var(--dark); background: #f3f4f6; }
#chat-list-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.chat-search-bar { padding: 8px 12px; border-bottom: 1px solid #f3f4f6; flex-shrink: 0; }
.chat-search-bar input {
    width: 100%;
    border: 1px solid #e5e7eb;
    border-radius: 20px;
    padding: 6px 12px;
    font-size: 0.82rem;
    outline: none;
    background: #f9fafb;
}
.chat-search-bar input:focus { border-color: var(--primary); background: #fff; }
.chat-tabs { display: flex; gap: 4px; padding: 8px 12px 0; flex-shrink: 0; }
.chat-tab-btn {
    flex: 1;
    border: none;
    background: #f3f4f6;
    color: #6b7280;
    font-size: 0.78rem;
    font-weight: 600;
    padding: 7px 8px;
    border-radius: 8px 8px 0 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
}
.chat-tab-btn.active { background: #fff; color: var(--primary); box-shadow: 0 -1px 0 var(--primary) inset; }
.chat-tab-btn .chat-tab-dot {
    background: #ef4444;
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
    border-radius: 10px;
    padding: 0 5px;
    min-width: 15px;
    line-height: 15px;
    text-align: center;
}
#chat-list-items { flex: 1; overflow-y: auto; }
.chat-rider-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 14px;
    cursor: pointer;
    border-bottom: 1px solid #f3f4f6;
    transition: background 0.1s;
    -webkit-tap-highlight-color: transparent;
}
.chat-rider-item:hover { background: #f8f9fa; }
.chat-rider-item.has-unread { background: #eff6ff; }
.chat-rider-item.has-unread:hover { background: #e3efff; }
.chat-rider-item.has-unread .chat-rider-name { font-weight: 700; }
.chat-rider-item.has-unread .chat-rider-last { color: var(--dark); font-weight: 600; }
.chat-rider-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--primary);
    color: #fff;
    font-size: 0.85rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.chat-rider-info { flex: 1; min-width: 0; }
.chat-rider-name { font-weight: 600; font-size: 0.85rem; color: var(--dark); }
.chat-rider-last { font-size: 0.75rem; color: #9ca3af; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-rider-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.chat-rider-time { font-size: 0.68rem; color: #9ca3af; }
.chat-unread-dot {
    background: #ef4444;
    color: #fff;
    border-radius: 20px;
    font-size: 0.62rem;
    font-weight: 700;
    min-width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 4px;
}
.chat-list-empty { padding: 40px 16px; text-align: center; color: #9ca3af; font-size: 0.85rem; }
#chat-conv-view, #company-chat-conv-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.chat-conv-messages {
    flex: 1;
    overflow-y: auto;
    padding: 12px;
    background: #f5f6fa;
    display: flex;
    flex-direction: column;
    gap: 5px;
}
.chat-bubble-wrap { display: flex; flex-direction: column; max-width: 80%; }
.chat-bubble-wrap.mine { align-self: flex-end; align-items: flex-end; }
.chat-bubble-wrap.theirs { align-self: flex-start; align-items: flex-start; }
.chat-bubble { padding: 8px 12px; border-radius: 14px; font-size: 0.83rem; line-height: 1.45; word-break: break-word; }
.chat-bubble-wrap.mine .chat-bubble { background: var(--primary); color: #fff; border-bottom-right-radius: 3px; }
.chat-bubble-wrap.theirs .chat-bubble { background: #fff; color: var(--dark); border-bottom-left-radius: 3px; box-shadow: 0 1px 3px rgba(0,0,0,0.07); }
.chat-bubble-meta { font-size: 0.62rem; color: #9ca3af; margin-top: 2px; padding: 0 2px; }
.chat-date-sep { text-align: center; font-size: 0.65rem; color: #9ca3af; margin: 6px 0 3px; font-weight: 500; }
.chat-conv-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: #9ca3af; font-size: 0.82rem; text-align: center; padding: 20px; }
/* El PEDIDO CITADO en el chat con un local («hablar de este pedido»). La CITA
   va sobre la caja de escribir, como la respuesta de un chat: una línea con el
   número, el estado y el trayecto, y su ✕. La TARJETA va dentro de la burbuja
   del mensaje enviado. El panel del chat es claro en las tres caras, como el
   resto de sus piezas. */
.chat-quote {
    display: flex; align-items: center; gap: 8px; padding: 6px 10px;
    background: #f3f4f6; border-top: 1px solid #e9ecef; border-left: 3px solid var(--primary);
    flex-shrink: 0;
}
.chat-quote[hidden] { display: none; }
.chat-quote-body { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; }
.chat-quote-ref { font-size: 0.78rem; font-weight: 700; color: #111827; }
.chat-quote-status { font-size: 0.66rem; font-weight: 600; padding: 1px 6px; border-radius: 4px; border-left-width: 0; }
.chat-quote-route { flex-basis: 100%; font-size: 0.72rem; color: #6b7280;
                    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-quote-close { border: 0; background: transparent; color: #6b7280; width: 28px; height: 28px;
                    border-radius: 50%; flex-shrink: 0; cursor: pointer; }
.chat-quote-close:hover { background: #e5e7eb; color: #111827; }
.chat-order-card {
    display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; align-items: baseline;
    margin: -2px -4px 6px; padding: 6px 8px; border-radius: 8px;
    background: rgba(255, 255, 255, 0.92); color: #111827; text-decoration: none;
    border-left: 3px solid var(--primary);
}
.chat-bubble-wrap.theirs .chat-order-card { background: #f3f4f6; }
.chat-order-card:hover { color: #111827; filter: brightness(0.97); }
.chat-order-ref { font-size: 0.78rem; font-weight: 700; }
.chat-order-status { font-size: 0.64rem; font-weight: 600; padding: 1px 6px; border-radius: 4px; border-left-width: 0; }
.chat-order-route { grid-column: 1 / -1; font-size: 0.72rem; color: #4b5563; }
.chat-order-link { grid-column: 1 / -1; font-size: 0.72rem; font-weight: 600; color: var(--primary-dark); }

/* RESPONDER a un mensaje concreto. Tres piezas: el botón de cada mensaje (al
   lado de la burbuja; aparece al pasar por encima y está siempre a la vista en
   táctil), «respondiendo a…» sobre la caja de escribir, y el original dentro de
   la burbuja de la respuesta (se pulsa para ir a él). */
.chat-bubble-wrap { position: relative; }
.chat-reply-btn {
    position: absolute; top: 2px; border: 0; background: #e5e7eb; color: #4b5563;
    width: 24px; height: 24px; border-radius: 50%; padding: 0; font-size: 0.72rem;
    cursor: pointer; opacity: 0; transition: opacity 0.12s;
}
.chat-bubble-wrap.mine .chat-reply-btn { left: -30px; }
.chat-bubble-wrap.theirs .chat-reply-btn { right: -30px; }
.chat-bubble-wrap:hover .chat-reply-btn, .chat-reply-btn:focus { opacity: 1; }
.chat-reply-btn:hover { background: var(--primary); color: #fff; }
@media (hover: none) { .chat-reply-btn { opacity: 0.75; } }
.chat-bubble-wrap.is-flash .chat-bubble { box-shadow: 0 0 0 2px var(--warning); }
.chat-reply {
    display: flex; align-items: center; gap: 8px; padding: 6px 10px;
    background: #f3f4f6; border-top: 1px solid #e9ecef; border-left: 3px solid var(--gray-400);
    flex-shrink: 0;
}
.chat-reply[hidden] { display: none; }
.chat-reply-body { flex: 1; min-width: 0; }
.chat-reply .chat-reply-who { display: block; font-size: 0.7rem; font-weight: 700; color: #111827; }
.chat-reply .chat-reply-text { display: block; font-size: 0.72rem; color: #6b7280;
                               white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-reply-ref {
    display: block; width: calc(100% + 8px); margin: -2px -4px 6px; padding: 5px 8px;
    border: 0; border-left: 3px solid var(--gray-400); border-radius: 6px;
    background: rgba(255, 255, 255, 0.85); color: #111827; text-align: left; cursor: pointer;
}
.chat-bubble-wrap.theirs .chat-reply-ref { background: #f3f4f6; }
.chat-reply-ref .chat-reply-who { display: block; font-size: 0.68rem; font-weight: 700; }
.chat-reply-ref .chat-reply-order { display: inline-block; font-size: 0.66rem; font-weight: 600;
                                    color: var(--primary-dark); margin-right: 4px; }
.chat-reply-ref .chat-reply-text { font-size: 0.72rem; color: #4b5563; overflow-wrap: anywhere; }

/* El botón «Hablar de este pedido». En la fila de una lista es solo el icono. */
.chat-order-btn[hidden] { display: none; }
.chat-order-btn.is-compact {
    border: 0; background: transparent; color: var(--gray-500);
    width: 1.75rem; height: 1.75rem; border-radius: 50%; padding: 0; margin-left: 0.25rem;
    font-size: 0.95rem; cursor: pointer; vertical-align: middle;
}
.chat-order-btn.is-compact:hover { background: var(--gray-100); color: var(--primary); }

.chat-conv-input-bar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: #fff; border-top: 1px solid #e9ecef; flex-shrink: 0; }
.chat-conv-input-bar textarea {
    flex: 1;
    border: 1px solid #e5e7eb;
    border-radius: 18px;
    padding: 7px 12px;
    font-size: 0.83rem;
    resize: none;
    outline: none;
    max-height: 80px;
    line-height: 1.4;
    font-family: inherit;
    background: #f9fafb;
}
.chat-conv-input-bar textarea:focus { border-color: var(--primary); background: #fff; }
.chat-conv-send-btn {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--primary);
    color: #fff;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.9rem;
    cursor: pointer;
    flex-shrink: 0;
    transition: background 0.15s;
}
.chat-conv-send-btn:hover { background: var(--primary-dark); }
.chat-conv-send-btn:disabled { background: #c7d2de; cursor: not-allowed; }

/* ==========================================================================
   Rider (mobile-first, mantiene bottom nav — ver rider_base.html)
   ========================================================================== */
/* Alto del bottom-nav. Es una VARIABLE porque hay dos consumidores que tienen
   que moverse juntos: el hueco que le reserva el contenido (padding-bottom) y el
   borde inferior del shell del mapa (.rider-map-shell) — si divergen, el mapa se
   dibuja por debajo del nav o deja una franja muerta. Bajó de 64 a 54px: en las
   vistas de mapa cada píxel del chrome es mapa que el rider no ve, y con el
   icono más chico la fila sigue teniendo área de toque de sobra. */
:root { --rider-nav-h: 54px; }

body.role-rider main { padding-top: 16px; padding-bottom: calc(var(--rider-nav-h) + 16px); }
body.role-rider .page-wrap { max-width: 480px; padding: 0 16px 16px; }
body.role-rider .page-header { align-items: center; padding: 4px 0 16px; margin-bottom: 0; }

/* La campana y el contador de la jornada, juntos a la derecha de la cabecera
   del Inicio. La campana ES el acceso a todas las notificaciones (ver, marcar,
   limpiar); arriba del hero queda solo la última. */
.dash-head-acts { display: flex; align-items: center; gap: 8px; }

.rider-bnav {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    height: var(--rider-nav-h);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-sizing: content-box;   /* el área segura se SUMA, no come alto útil */
    background: #fff;
    border-top: 1px solid #e9ecef;
    display: flex;
    z-index: 1040;
    box-shadow: 0 -2px 10px rgba(0,0,0,0.08);
}
.rider-bnav a {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    color: #9ca3af;
    font-size: 0.6rem;
    font-weight: 500;
    gap: 2px;
    transition: color 0.15s;
    -webkit-tap-highlight-color: transparent;
}
.rider-bnav a i { font-size: 1.15rem; line-height: 1; }
.rider-bnav a.active,
.rider-bnav a:hover { color: var(--primary); }

.rider-chat-badge-bubble {
    position: absolute;
    top: -5px;
    right: -8px;
    background: #ef4444;
    color: #fff;
    border-radius: 20px;
    font-size: 0.58rem;
    font-weight: 700;
    min-width: 16px;
    height: 16px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 3px;
    border: 2px solid #fff;
    line-height: 1;
}

/* ==========================================================================
   Utilidades (reemplazan estilos inline sueltos en templates)
   ========================================================================== */
.text-cp { color: var(--cp) !important; }
.text-gray-400 { color: var(--gray-400) !important; }
.text-gray-500 { color: var(--gray-500) !important; }
.text-gray-700 { color: var(--gray-700) !important; }
.fs-xs { font-size: 0.72rem; }
.fs-sm { font-size: 0.82rem; }
.fs-md { font-size: 0.875rem; }
.fw-medium { font-weight: 500; }

.cp-stack { display: flex; flex-direction: column; gap: 1.25rem; }
.cp-hr { border-color: var(--gray-100); margin: 1rem 0; }
.cp-notice {
    background: var(--cp-light);
    border: 1px solid #a7f3d0;
    border-radius: var(--radius-md);
    padding: 0.875rem 1rem;
    margin-bottom: var(--space-5);
    font-size: 0.85rem;
    color: var(--cp-text);
}
.cp-timeline-dot {
    /* Excepción del barrido (§Cierre): el punto es un ASSET de tamaño fijo —a
       8px ya es un punto y a 6,8px sería suciedad—, así que ni él ni el margen
       que lo centra contra la primera línea siguen a `--ui-scale`. */
    flex-shrink: 0;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--cp);
    margin-top: 5px;
}
.truncate-160 { max-width: 160px; }
.truncate-200 { max-width: 200px; }
.fs-lg { font-size: 1.1rem; }
.w-180 { width: 180px; }
.mw-160 { max-width: 160px; }
.mw-220 { max-width: 220px; }

/* Timeline de historial (usado en detalle de pedido) */
.cp-timeline-list { padding: 0.5rem 0; }
.cp-timeline-row { display: flex; gap: 0.75rem; padding: 0.75rem 1.125rem; border-bottom: 1px solid var(--gray-50); }
.cp-timeline-row:last-child { border-bottom: none; }
.cp-timeline-body { flex: 1; }
.cp-timeline-title { font-size: 0.875rem; font-weight: 600; color: var(--gray-900); margin: 0; }
.cp-timeline-note { font-size: 0.78rem; color: var(--gray-500); margin: 2px 0 0; }
.cp-timeline-usernote {
    font-size: 0.78rem;
    background: #fffbe6;
    border-left: 3px solid #f5c518;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    margin: 0.25rem 0 0;
    color: #5a4a00;
}
.cp-timeline-time { font-size: 0.72rem; color: var(--gray-400); flex-shrink: 0; white-space: nowrap; }

/* ==========================================================================
   Dashboard del admin (movido desde admin/dashboard.html — antes inline)
   ========================================================================== */
.kpi-card {
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: 12px;
    padding: 16px 18px;
    height: 100%;
}
.kpi-label {
    color: var(--gray-500);
    font-size: 0.8rem;
    font-weight: 500;
    margin-bottom: 6px;
}
.kpi-value {
    font-size: 1.7rem;
    font-weight: 700;
    color: var(--gray-900);
    margin-bottom: 4px;
}
.kpi-trend {
    font-size: 0.8rem;
    font-weight: 600;
    margin: 0;
}
.kpi-trend-positive { color: var(--success); }
.kpi-trend-negative { color: var(--danger); }
.kpi-trend-neutral { color: var(--gray-400); }
.kpi-trend-caption {
    color: var(--gray-400);
    font-weight: 400;
    margin-left: 4px;
}
.chart-box {
    position: relative;
    width: 100%;
}
/* La leyenda de la dona es una LISTA, no una fila de etiquetas sueltas: con la
   cifra y el porcentaje al lado se lee sin pasar el ratón por el gráfico, que
   es lo único que la dona sola no dice. */
.status-legend {
    list-style: none;
    margin: 0;
    padding: 0 12px 12px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.status-legend li {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.73rem;
    color: var(--gray-700);
    padding: 2px 0;
}
.status-legend .sl-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-legend .sl-num { font-weight: 700; font-variant-numeric: var(--num); color: var(--gray-900); }
.status-legend .sl-pct { width: 38px; text-align: right; color: var(--gray-400); font-variant-numeric: var(--num); }
.status-legend .dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}
.kpi-sub {
    font-size: 0.72rem;
    color: var(--gray-400);
    font-weight: 500;
    margin: 2px 0 0;
    font-variant-numeric: var(--num);
}

/* KPI de ingresos: ojo para ocultar/mostrar los importes. */
.kpi-card-secret .kpi-label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
}
.kpi-eye {
    border: none;
    background: transparent;
    color: var(--gray-400);
    padding: 2px 4px;
    line-height: 1;
    cursor: pointer;
    border-radius: 6px;
    font-size: 0.95rem;
    transition: color .15s, background .15s;
}
.kpi-eye:hover { color: var(--gray-700); background: var(--gray-100); }
.kpi-secret { position: relative; }
.kpi-card-secret.is-hidden .kpi-secret { visibility: hidden; }
.kpi-card-secret.is-hidden .kpi-secret::after {
    content: "•••";
    visibility: visible;
    position: absolute;
    left: 0;
    top: 0;
    letter-spacing: 0.06em;
    color: var(--gray-400);
}

/* Fila de KPI-cards adaptable (empresa y otros): rellena el ancho sin
   depender de la grilla de 12 columnas de Bootstrap. */
.kpi-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
/* Variante del portal empresa: acento verde en el ícono del KPI. */
.kpi-card.cp-scope .kpi-label i { color: var(--cp); }
/* KPI de alerta (ej. pedidos sin precio pendientes). */
.kpi-card.alert .kpi-value { color: #d97706; }

/* Alto del gráfico del dashboard de empresa (antes inline en dashboard.html) */
body.role-company .chart-box { height: 260px; }

/* ==========================================================================
   Torre de control del dispatcher (admin/dashboard.html)
   Disciplina de color: superficies y tipografía NEUTRAS; el color solo
   aparece para estado (badges .status-*), alerta (ámbar/rojo en los números
   de la barra operativa) y tendencia (flechas kpi-trend).
   ========================================================================== */

/* Encabezado de sección de dashboard */
.dash-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
    flex-wrap: wrap;
}
.dash-title { font-size: 1.35rem; font-weight: 700; color: var(--gray-900); margin: 0; }
.dash-title-sub { font-size: 0.82rem; color: var(--gray-500); margin: 2px 0 0; }
.dash-section-label {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--gray-400);
    margin: 0 0 10px;
}

/* Barra de estado operativo (siempre visible) */
.ops-bar {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 1px;
    background: var(--gray-200);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    overflow: hidden;
    margin-bottom: var(--space-6);
}
.ops-cell {
    background: var(--surface);
    padding: 16px 20px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.ops-label {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-500);
    display: flex;
    align-items: center;
    gap: 6px;
}
.ops-num {
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.05;
    color: var(--gray-900);          /* neutro por defecto */
    font-variant-numeric: var(--num);
}
.ops-sub { font-size: 0.74rem; color: var(--gray-400); font-variant-numeric: var(--num); }
/* Color SOLO cuando es alerta */
.ops-cell.alert-amber .ops-num { color: #b45309; }
.ops-cell.alert-red .ops-num { color: var(--status-cancelado); }
.ops-cell.alert-amber { box-shadow: inset 3px 0 0 var(--status-pendiente); }
/* Pedidos por aceptar: el índigo del estado (no es una alarma, es una decisión). */
.ops-cell.alert-acc { box-shadow: inset 3px 0 0 var(--status-por-aceptar); }
.ops-cell.alert-acc .ops-num { color: var(--status-por-aceptar); }
.ops-cell.alert-red { box-shadow: inset 3px 0 0 var(--status-cancelado); }

/* La FILA DE TRABAJO de la torre de control: cuatro tarjetas del mismo alto
   —estados, flota, empresas y la cola— que tienen que entrar en la pantalla
   sin desplazarse. Por eso el alto lo pone el CONTENEDOR y no el contenido: lo
   que crece sin techo (los riders, las empresas, la cola) scrollea DENTRO de su
   tarjeta. Los anchos no son iguales a propósito: la dona y la cola son
   columnas de lectura estrecha; flota y empresas llevan una tabla de tres
   cifras y necesitan aire.

   El alto sale de la pantalla menos lo que hay encima (cabecera + barra de
   estado): fijarlo en píxeles dejaba la cuarta tarjeta cortada en un portátil
   de 13". */
/* El tablero se queda con lo que SOBRA, y por eso no hay número mágico: la
   página mide exactamente el hueco del chrome (pantalla menos la barra
   superior menos el padding del borde, las dos leídas de su variable) y el
   tablero es el único que crece. Restar "unos 250px" por el encabezado y la
   barra de estado era la manera de que un cambio en cualquiera de los dos
   —una línea más en el título, una celda más alta— devolviera el scroll.
   Los avisos no cuentan: son toasts flotantes, no ocupan flujo. */
.dash-page {
    display: flex;
    flex-direction: column;
    /* `height` y no `min-height`: con un mínimo, cualquier cosa que crezca por
       dentro empuja la página y devuelve el scroll. Con el alto FIJADO al hueco
       del chrome, el tablero se reparte lo que hay y nada puede desbordar —
       lo que no entra en una tarjeta se desplaza DENTRO de ella, que es lo que
       esta pantalla promete. */
    height: calc(100vh - var(--topbar-h) - var(--content-pad) * 2);
    /* Solo para cuando las columnas se apilan (≤1400px): ahí cada una necesita
       un alto propio, porque "lo que sobra" ya no tiene sentido. */
    --dash-board-h: clamp(420px, calc(100vh - 300px), 900px);
}

/* Todo lo que hay ENCIMA del tablero se comprime, porque cada píxel que ocupa
   se lo quita a las tarjetas. Va scopeado a `.dash-page`: la cabecera y la
   barra de estado las comparten otras pantallas, que sí pueden desplazarse y
   donde una cifra más grande se lee mejor. */
.dash-page .dash-header { margin-bottom: var(--space-3); }
.dash-page .dash-title { font-size: 1.12rem; }
.dash-page .dash-title-sub { font-size: 0.76rem; }
.dash-page .ops-bar { margin-bottom: var(--space-3); }
.dash-page .ops-cell { padding: 9px 14px 10px; }
.dash-page .ops-label { font-size: 0.64rem; }
.dash-page .ops-num { font-size: 1.45rem; }
.dash-page .ops-sub { font-size: 0.68rem; }

/* La cola vive en la columna estrecha, y ahí el `<select>` de asignar rider más
   su botón se comían el ancho entero: quedaba una dirección recortada a tres
   palabras y un pedido que no se entendía. La fila ENVUELVE —la dirección se
   queda con la línea entera y el formulario baja a la suya, a lo ancho—, que es
   el mismo criterio que ya se usa en el teléfono por la misma razón. La
   pastilla de estado se achica: en una fila de 300px es una etiqueta, no un
   titular. */
/* La cola dentro de la TORRE DE CONTROL: acá el espaciado se queda en px y es
   una excepción del barrido (§Cierre). El tablero está REINFLADO con
   `--ui-unscale` —ya se ajustó a mano, tarjeta por tarjeta—, así que un rem
   escrito acá encogería dos veces. */
.dash-page .queue-row { flex-wrap: wrap; row-gap: 5px; }
.dash-page .queue-row-main { flex: 1 1 auto; min-width: 0; }
.dash-page .queue-assign { flex: 1 1 100%; gap: 5px; }
.dash-page .queue-assign select { flex: 1; min-width: 0; max-width: none; }
.dash-page .queue-row-id { font-size: 0.78rem; }
.dash-page .queue-leg { font-size: 0.73rem; }
.dash-page .queue-row-sub { font-size: 0.7rem; }
.dash-page .queue-row .status-badge {
    font-size: 0.66rem;
    padding: 2px 0.4375rem;
    border-left-width: 2px;
    white-space: nowrap;
    flex-shrink: 0;
}

/* Tres columnas del MISMO alto, alineadas por abajo. El ancho de la izquierda
   sale de la barra de estado: llega hasta la mitad de su segunda celda (1,5 de
   4 = 37,5%), que es lo que hace que la columna se lea como una continuación de
   esas cifras y no como un bloque suelto. La derecha es una columna de lectura
   estrecha (una cola de pedidos), y el centro se queda con el resto, que es
   donde hay tablas. */
.dash-board {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.7fr) minmax(0, 0.8fr);
    gap: var(--space-3);
    align-items: stretch;
    flex: 1;
    /* Sin suelo a propósito: un `min-height` acá es exactamente lo que haría
       que en una pantalla baja la página volviera a desplazarse. */
    min-height: 0;
}
/* Una columna es una PILA con alto propio: lo de arriba mide lo que necesita
   (el gráfico, los KPIs) y lo de abajo se queda con lo que sobra. Ese `1fr`
   sobre una fila de grilla es lo que alinea las tres columnas por abajo sin
   que ninguna tarjeta tenga que saber cuánto mide. */
.dash-col {
    display: grid;
    gap: var(--space-3);
    min-height: 0;
    height: 100%;
}
/* La izquierda son DOS tarjetas hermanas —la carga que viene y los estados—,
   y se leen juntas: con la fila de arriba en `auto`, "Estados" se quedaba con
   todo lo que sobraba y la comparación entre las dos se hacía cuesta arriba.
   Dos mitades iguales, y lo que no entre scrollea dentro de su tarjeta. */
.dash-col-left { grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }
.dash-col-center { grid-template-rows: auto minmax(0, 1fr); }
.dash-col-right { grid-template-rows: minmax(0, 1fr); }

/* Flota y empresas comparten la mitad de abajo del centro. */
.dash-pair {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-3);
    min-height: 0;
}

@media (max-width: 1400px) {
    /* Ya no entran tres columnas: la cola se va debajo, a lo ancho, y las dos
       que quedan conservan su proporción. */
    .dash-page { height: auto; }
    .dash-board { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); flex: none; }
    .dash-col { height: var(--dash-board-h); }
    .dash-col-right { grid-column: 1 / -1; height: 320px; }
}
@media (max-width: 1000px) {
    .dash-board { grid-template-columns: 1fr; flex: none; }
    .dash-col-right { grid-column: auto; }
    .dash-pair { grid-template-columns: 1fr; }
}

.dash-panel {
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}
.dash-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: 8px 12px;
    border-bottom: 1px solid var(--gray-100);
}
.dash-panel-head h2 {
    margin: 0;
    font-size: 0.74rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-700);
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
.dash-panel-head h2 i { color: var(--gray-400); }
/* La UNIDAD de lo que cuenta la tarjeta, pegada al título. "Carga de trabajo"
   a secas no dice si son pedidos o paradas, y la diferencia es justamente lo
   que hace útil el gráfico. */
.dash-panel-unit {
    font-weight: 600;
    font-size: 0.85em;
    letter-spacing: 0.02em;
    text-transform: none;
    color: var(--gray-400);
}
/* El cuerpo es lo ÚNICO que scrollea: `min-height: 0` es lo que deja que un
   hijo de flex se encoja por debajo de su contenido (sin él, la tarjeta crece
   y el scroll se va a la página). */
.dash-panel-body { flex: 1; min-height: 0; }
.dash-panel-scroll { overflow-y: auto; overscroll-behavior: contain; }

/* Los cuatro rangos. Chicos pero con área de toque real: son la única manera
   de cambiar lo que dicen las cifras, así que no pueden ser un adorno. */
.dash-range {
    display: inline-flex;
    background: var(--gray-100);
    border-radius: 999px;
    padding: 2px;
    gap: 2px;
    flex-shrink: 0;
}
.dash-range button {
    border: none;
    background: transparent;
    color: var(--gray-500);
    font-size: 0.66rem;
    font-weight: 600;
    padding: 3px 8px;
    min-height: 23px;
    border-radius: 999px;
    cursor: pointer;
    white-space: nowrap;
}
.dash-range button:hover { color: var(--gray-900); }
.dash-range button.is-on {
    background: var(--surface);
    color: var(--gray-900);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}

/* Las cifras GRANDES de la tarjeta: una por concepto, cada una con su color.
   El color no decora — es lo que deja emparejar la cifra de arriba con la
   columna de abajo sin leer la etiqueta. */
/* OJO CON EL NOMBRE: `.dash-tile` ya existe y es del Inicio del RIDER (blanco,
   borde claro, radio 18px), vive más abajo en esta hoja y por lo tanto GANA por
   orden — de ahí los rectángulos blancos que aparecían encima de estas cifras.
   La familia del dispatcher lleva prefijo propio, `tc-` (torre de control),
   igual que los dos calendarios usan `.cal-*` y `.cc-*`.

   Lo único que separa una cifra de la siguiente es una LÍNEA. El truco de la
   grilla con `gap: 1px` sobre un fondo dibujaba una caja alrededor de cada una;
   la franja de color de arriba sobraba, porque el color ya lo lleva el número.
   `minmax(0, 1fr)` y no `1fr`: un track `1fr` tiene mínimo `auto`, así que una
   cifra de cinco dígitos se salía del ancho de su columna en vez de
   repartirlo. */
.tc-stats {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    border-bottom: 1px solid var(--gray-100);
}
.tc-stat {
    padding: 8px 10px 9px;
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
.tc-stat + .tc-stat { border-left: 1px solid var(--gray-200); }
.tc-stat-label {
    font-size: 0.63rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-500);
    display: flex;
    align-items: center;
    gap: 5px;
}
/* Una cifra grande crece con el dato, y la caja no: con cinco dígitos "12.345"
   a 1,55rem se comía el padding y quedaba pegada al borde. El `clamp` la ata al
   ancho de la ventana (que es lo que manda el de la columna) y `nowrap` impide
   que se parta en dos líneas y desalinee la fila de cifras. Misma regla para
   las tres cifras grandes de la pantalla. */
.tc-stat-num,
.wl-num b,
.dash-kpi-value {
    font-size: clamp(0.98rem, 0.5rem + 0.72vw, 1.35rem);
    font-weight: 700;
    line-height: 1.1;
    font-variant-numeric: var(--num);
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.tc-stat-num { color: var(--tc-accent, var(--gray-900)); }

/* UN solo juego de acentos para toda la torre de control: lo usan la cifra
   grande de arriba y la de cada fila de la lista. Dos juegos paralelos (uno
   `.tile-*` y otro `.st-*`) eran dos sitios donde el azul de "pedidos" podía
   dejar de ser el mismo azul. */
.tc-orders  { --tc-accent: var(--status-asignado); }
.tc-stops   { --tc-accent: var(--status-progress); }
.tc-km      { --tc-accent: var(--metric-km); }
.tc-revenue { --tc-accent: var(--status-entregado); }

/* La variación contra el período previo. Verde/rojo dicen el SENTIDO; el 0%
   y el "sin base previa" no se pintan de ningún color, porque no son ni una
   mejora ni un empeoramiento. */
.dash-delta { font-size: 0.7rem; font-weight: 600; font-variant-numeric: var(--num); min-height: 1em; }
.dash-delta .is-up { color: var(--success); }
.dash-delta .is-down { color: var(--status-cancelado); }
.dash-delta .is-flat { color: var(--gray-400); }
.dash-delta i { font-size: 0.62rem; margin-right: 2px; }

/* Una fila de la lista (rider o empresa). Separadas de verdad: son entidades
   distintas, no renglones de una tabla.

   **Se ven CUATRO enteras, nunca tres y media.** El alto de la lista lo decide
   la columna (es "lo que sobra"), así que un alto de fila FIJO deja siempre un
   recorte abajo — que se lee como un error de maquetación y no como "hay más".
   La solución es al revés: la fila mide una FRACCIÓN de la lista
   (`100% / --tc-rows`), así que entran cuatro exactas mida lo que mida la
   columna, y la quinta empieza justo en el borde. El `min-height` es el suelo
   de legibilidad: en una pantalla muy baja se prefiere que entren más de cuatro
   a que ninguna se lea. */
.tc-list { --tc-rows: 4; }
.tc-list .dash-row {
    height: calc(100% / var(--tc-rows));
    min-height: 44px;
    box-sizing: border-box;
}
.dash-row {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 6px 12px;
    border-bottom: 1px solid var(--gray-100);
    overflow: hidden;
}
.dash-row:last-child { border-bottom: none; }
.dash-row:hover { background: var(--gray-100); }
.dl-avatar {
    width: 30px; height: 30px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    background: var(--gray-100);
}
.dl-avatar-ph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.9rem;
    color: var(--gray-500);
}
.dl-main { flex: 1; min-width: 0; }
.dl-name {
    display: block;
    font-size: 0.81rem;
    font-weight: 600;
    color: var(--gray-900);
    text-decoration: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.dl-name:hover { color: var(--primary); text-decoration: underline; }
/* En UNA línea y sin envolver: una fila que crece a dos renglones deja de medir
   lo mismo que sus hermanas y rompe el reparto en cuartos. Si el ancho aprieta,
   la última cifra se recorta con puntos suspensivos — que es información ("hay
   más de lo que entra") y no un salto de línea. */
.dl-stats {
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    margin-top: 1px;
    min-width: 0;
    overflow: hidden;
}
.dl-stat {
    font-size: 0.68rem;
    color: var(--gray-500);
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.dl-stat b {
    font-size: 0.78rem;
    font-weight: 700;
    font-variant-numeric: var(--num);
    color: var(--tc-accent, var(--gray-700));
    margin-right: 2px;
}
/* El icono ES la etiqueta de la cifra: "pedidos" y "paradas" escritos en cada
   renglón se comían el ancho de una fila que ya es estrecha, y repetidos cuatro
   veces no aportan nada. El texto sigue ahí para el lector de pantalla y en el
   `title`. */
.dl-stat i {
    margin-right: 3px;
    color: var(--gray-400);
    font-size: 0.92em;
}

/* "x/y" = lo HECHO sobre lo que FALTA. El color del acento se queda en la
   primera cifra —es la que responde—, y lo que falta va en gris: son dos datos
   distintos y no pueden pesar lo mismo. Va en `em` para seguir a su cifra sin
   necesitar pareja reinflada. */
.tc-xy-sep {
    color: var(--gray-400);
    font-weight: 400;
    margin: 0 1px;
    font-size: 0.86em;
}
.tc-xy-pend { color: var(--gray-500); font-weight: 600; font-size: 0.88em; }

/* Los pedidos SUELTOS no tienen perfil al que ir: su nombre no es un enlace. */
.dl-name-plain { color: var(--gray-700); }
.dl-name-plain i { color: var(--gray-400); margin-right: 2px; }

/* El ojo de los importes. Esconde la PANTALLA, no el cálculo: el número sigue
   ahí y por eso se reserva su ancho con un guión doble, para que la tarjeta no
   salte al mostrarlo. */
.dash-eye {
    border: none;
    background: transparent;
    color: var(--gray-400);
    padding: 0 2px;
    line-height: 1;
    cursor: pointer;
}
.dash-eye:hover { color: var(--gray-700); }
.is-money-hidden .dash-money { visibility: hidden; position: relative; }
.is-money-hidden .dash-money::after {
    content: '••••';
    visibility: visible;
    position: absolute;
    left: 0; top: 0;
    color: var(--gray-400);
    letter-spacing: 0.1em;
}

/* Estados: la dona a la IZQUIERDA y la lista a la derecha. Apiladas, la lista
   de siete estados empujaba la dona fuera de la tarjeta; al lado, las dos
   entran y se leen juntas — el color del sector y su cifra a la misma altura. */
.dash-states {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    min-height: 0;
}
.dash-donut { flex: 0 0 40%; max-width: 40%; min-width: 0; height: 100%; max-height: 220px; }
.dash-states .status-legend { flex: 1; min-width: 0; max-height: 100%; padding: 0; }

/* Carga de trabajo: las dos cifras a la izquierda y el gráfico a la derecha.
   Las cifras son la RESPUESTA (cuánto falta, cuánto se hizo) y el gráfico es el
   reparto por día: leerlas en el gráfico obligaría a sumar barras. */
.dash-workload {
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: stretch;
    gap: var(--space-2);
    padding: 6px 10px 8px;
}
.wl-nums {
    flex: 0 1 auto;
    min-width: 68px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 8px;
}
.wl-num { display: flex; flex-direction: column; line-height: 1.1; }
.wl-num b { color: var(--tc-accent, var(--gray-900)); }
.wl-num span {
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-500);
}
/* Los mismos dos colores que las barras: el número y su barra tienen que ser
   la misma cosa sin leyenda de por medio. */
.wl-todo { --tc-accent: var(--status-pendiente); }
.wl-done { --tc-accent: var(--status-entregado); }

.wl-chart { flex: 1; min-width: 0; min-height: 0; }
/* Sin suelo propio: su alto es la MITAD de la columna (ver `.dash-col-left`), y
   un `min-height` acá la desbordaría en una pantalla baja en vez de dejar que
   el gráfico se achique. En móvil, apilada, sí lleva alto fijo. */

/* Los tres KPIs de negocio del centro. Son de LECTURA (7d contra los 7
   previos, sin rango elegible): dicen cómo se está trabajando, no cuánto. */
.dash-kpis {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-3);
    min-width: 0;
}
.dash-kpi {
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    padding: 8px 10px 9px;
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
.dash-kpi-label {
    font-size: 0.63rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-500);
    display: flex;
    align-items: center;
    gap: 5px;
    min-width: 0;
}
.dash-kpi-label i { color: var(--gray-400); flex-shrink: 0; }
.dash-kpi-value { color: var(--gray-900); }
.dash-kpi-foot { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.dash-kpi-sub {
    font-size: 0.65rem;
    color: var(--gray-400);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Cola de despacho */
.queue-group-title {
    position: sticky;
    top: 0;
    background: var(--surface);
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-400);
    padding: 0.75rem 1rem 0.375rem;
    display: flex;
    align-items: center;
    gap: 0.375rem;
    z-index: 1;
}
.queue-group-title .count {
    background: var(--gray-100);
    color: var(--gray-700);
    border-radius: 20px;
    padding: 0 0.5rem;
    font-size: 0.7rem;
    font-variant-numeric: var(--num);
}
.queue-row {
    display: flex;
    align-items: center;
    gap: 0.5625rem;
    padding: 0.4375rem 0.75rem;
    border-top: 1px solid var(--gray-100);
}
.queue-row-link { color: inherit; text-decoration: none; cursor: pointer; transition: background 0.12s ease; }
.queue-row-link:hover { background: var(--gray-100); color: inherit; }
.queue-row-main { flex: 1; min-width: 0; }
/* Primera línea de la fila: el #id y su etiqueta (la antigüedad en "Sin
   asignar", el estado en "En curso"). La etiqueta vivía en una columna propia a
   la derecha de la fila, donde le robaba el ancho a la dirección — que en la
   columna estrecha del tablero es justo lo que hay que poder leer. */
.queue-row-head {
    display: flex; align-items: center; justify-content: space-between; gap: 0.375rem;
}
.queue-row-head > :last-child { flex-shrink: 0; }
.queue-row-id { font-weight: 600; font-size: 0.85rem; color: var(--gray-900); }
.queue-row-id a { color: inherit; text-decoration: none; }
.queue-row-id a:hover { color: var(--primary); }
.queue-row-sub {
    font-size: 0.75rem;
    color: var(--gray-500);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Trayecto de una fila de la cola del dashboard. Antes esta fila solo decía la
   dirección de ENTREGA recortada a 32 caracteres, sin ninguna marca de qué era:
   al lado del mapa no se entendía qué se estaba mirando. Con los dos pines de
   color se lee de dónde sale y a dónde va sin leer una palabra. */
.queue-legs { display: flex; flex-direction: column; gap: 1px; margin-top: 2px; }
.queue-leg {
    display: flex; align-items: center; gap: 0.3125rem;
    font-size: 0.76rem; color: var(--gray-700); min-width: 0;
}
.queue-leg .leg-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Cuántas entregas más lleva el pedido. Acá NO se despliega (la fila entera es
   un enlace): la cola es un resumen y el desglose está a un clic. */
.queue-leg-more {
    flex-shrink: 0; font-size: 0.68rem; font-weight: 700;
    color: var(--stop-drop-ink);
    background: color-mix(in srgb, var(--stop-drop) 16%, transparent);
    border-radius: 999px; padding: 0 0.375rem;
}
.queue-row-age { font-size: 0.72rem; color: var(--gray-400); font-variant-numeric: var(--num); white-space: nowrap; }
.queue-assign {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    flex-shrink: 0;
}
.queue-assign select {
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-sm);
    padding: 0.3125rem 0.5rem;
    font-size: 0.78rem;
    max-width: 8.125rem;
    background: var(--surface);
    color: var(--gray-700);
}
.queue-assign button {
    border: none;
    background: var(--primary);
    color: #fff;
    border-radius: var(--radius-sm);
    padding: 0.3125rem 0.625rem;
    font-size: 0.78rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.queue-assign button:hover { background: var(--primary-dark); }
.queue-empty { padding: 1.5rem 1rem; text-align: center; color: var(--gray-400); font-size: 0.82rem; }

/* ==========================================================================
   Cola de despacho (admin/orders.html)
   Tabla densa, cifras tabulares, agrupada por prioridad operativa.
   Color solo para estado/alerta.
   ========================================================================== */
.disp-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}
.disp-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 0.625rem;
    align-items: flex-end;
    margin-bottom: var(--space-4);
}
.disp-filters .form-label { margin-bottom: 2px; font-size: 0.72rem; color: var(--gray-500); }
.disp-filters .form-control, .disp-filters .form-select { font-size: 0.82rem; }

.disp-card {
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.disp-table-wrap { overflow-x: auto; }
.disp-table { width: 100%; border-collapse: collapse; font-variant-numeric: var(--num); }
.disp-table thead th {
    text-align: left;
    font-size: 0.66rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-400);
    padding: 0.5625rem 0.75rem;
    border-bottom: 1px solid var(--gray-200);
    white-space: nowrap;
}
.disp-table thead th a { color: inherit; text-decoration: none; }
.disp-table thead th a:hover { color: var(--gray-700); }
.disp-table td {
    padding: 0.5rem 0.75rem;
    font-size: 0.82rem;
    color: var(--gray-700);
    border-bottom: 1px solid var(--gray-100);
    vertical-align: middle;
}
.disp-row { cursor: pointer; }
.disp-row:hover td { background: var(--gray-50); }
.disp-id { font-weight: 700; color: var(--gray-900); }
.disp-route { line-height: 1.35; min-width: 13.75rem; }
.disp-route .leg {
    display: flex;
    align-items: center;
    gap: 0.3125rem;
    max-width: 20rem;
}
/* El TEXTO es lo que se recorta, no la fila: con el `overflow` en la fila el
   pin entraba en el cálculo del elipsis y en una dirección larga se recortaba
   el pin —justo lo único que dice de qué parada se trata— en vez de la calle. */
.disp-route .leg-txt {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.disp-route .leg.to { color: var(--gray-900); }
/* Cuántas recogidas MÁS lleva el pedido (multi-recogida). En un listado no se
   despliega: lo que crece sin techo no entra en una fila y el detalle vive en
   la ficha. Lleva la tinta de la RECOGIDA, no la de la entrega. */
.disp-route .leg-more, .queue-leg .leg-more {
    flex-shrink: 0; font-size: 0.72rem; font-weight: 700;
    color: var(--stop-pickup-ink);
    background: color-mix(in srgb, var(--stop-pickup) 16%, transparent);
    border-radius: 999px; padding: 0 0.375rem;
}
/* Entregas 2..N de un pedido multi-parada. Antes eran un "+2" mudo: el número
   de paradas que no se pueden ver es justo el dato que obliga a abrir la ficha.
   Se despliega con <details>, sin JS — y el click NO puede navegar la fila, así
   que los tres handlers de `.disp-row[data-href]` ignoran `summary`. */
.disp-legs-more { margin-top: 1px; }
.disp-legs-more > summary {
    display: inline-flex; align-items: center; gap: 0.25rem;
    list-style: none; cursor: pointer; width: fit-content;
    font-size: 0.72rem; font-weight: 700;
    color: var(--stop-drop-ink);
    background: color-mix(in srgb, var(--stop-drop) 14%, transparent);
    border-radius: 999px; padding: 1px 0.5rem;
}
.disp-legs-more > summary::-webkit-details-marker { display: none; }
.disp-legs-more > summary::after {
    content: '\F282'; font-family: 'bootstrap-icons'; font-size: 0.7em;
}
.disp-legs-more[open] > summary::after { content: '\F286'; }
.disp-legs-more > summary:hover { background: color-mix(in srgb, var(--stop-drop) 26%, transparent); }
.disp-legs-more .leg { padding-left: 0.625rem; }

/* El desplegable de las RECOGIDAS 2..N lleva el verde de su pin, no el ambar de
   las entregas: el codigo de color de una parada no cambia porque el marcado se
   comparta. Es el MISMO componente con su token. */
.disp-legs-more.is-recogida > summary {
    color: var(--stop-pickup-ink);
    background: color-mix(in srgb, var(--stop-pickup) 14%, transparent);
}
.disp-legs-more.is-recogida > summary:hover {
    background: color-mix(in srgb, var(--stop-pickup) 26%, transparent);
}

/* QUIEN HACE el pedido, cuando es mas de uno. Mismo componente y mismo gesto
   que las direcciones, pero NEUTRO: un rider no es una parada y no le
   corresponde ninguno de los tres colores del codigo. */
.disp-riders { line-height: 1.35; }
.disp-rider { display: block; }
.disp-rider-link { color: inherit; text-decoration: none; }
.disp-rider-link:hover { text-decoration: underline; }
.disp-legs-more.is-riders > summary {
    color: var(--gray-600);
    background: var(--gray-100);
}
.disp-legs-more.is-riders > summary:hover { background: var(--gray-200); }
.disp-legs-more.is-riders .disp-rider { padding-left: 0.625rem; }
.disp-sub { font-size: 0.72rem; color: var(--gray-400); }
.disp-origin { color: var(--gray-400); font-size: 0.8rem; }
/* La misma procedencia, pero con la PALABRA: el perfil de la empresa la dibuja
   así donde la lista se conforma con el icono. Medía por `style=` en línea, y
   con colores literales que el chrome oscuro del admin no remapea. */
.disp-origin-chip {
    font-size: 0.72rem; font-weight: 600; white-space: nowrap;
    border-radius: var(--radius-sm); padding: 2px 0.375rem;
    background: color-mix(in srgb, var(--success) 18%, transparent);
    color: color-mix(in srgb, var(--success) 85%, var(--gray-900));
}

/* Encabezado de grupo (fila de tabla que ocupa todo el ancho) */
.disp-group-row td {
    background: var(--surface-sunken);
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-500);
    padding: 0.5625rem 0.75rem;
    border-bottom: 1px solid var(--gray-200);
    border-top: 1px solid var(--gray-200);
}
.disp-group-row .count {
    background: var(--gray-200);
    color: var(--gray-700);
    border-radius: 20px;
    padding: 0 0.5rem;
    margin-left: 0.375rem;
    font-variant-numeric: var(--num);
}
.disp-group-row.g-sin_asignar td { box-shadow: inset 3px 0 0 var(--status-pendiente); }
.disp-group-row.g-demoradas td { box-shadow: inset 3px 0 0 var(--status-cancelado); }
/* Separador por DÍA DE SERVICIO: la cola leída por día (sort=service_day). No
   es un grupo operativo, así que se distingue con su propio filo. */
.disp-group-row.g-dia td {
    box-shadow: inset 3px 0 0 var(--primary);
    text-transform: none;
    font-size: 0.78rem;
    color: var(--gray-700);
}
.disp-group-link {
    float: right;
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
    color: var(--primary);
    text-decoration: none;
}
.disp-group-link:hover { text-decoration: underline; }

/* Asignación inline dentro de la fila */
.disp-assign { display: flex; align-items: center; gap: 0.3125rem; }
.disp-assign select {
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-sm);
    padding: 0.25rem 0.375rem;
    font-size: 0.76rem;
    max-width: 8.125rem;
    background: var(--surface);
    color: var(--gray-700);
}
.disp-assign button {
    border: none;
    background: var(--primary);
    color: #fff;
    border-radius: var(--radius-sm);
    padding: 0.25rem 0.5625rem;
    font-size: 0.76rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}
.disp-assign button:hover { background: var(--primary-dark); }

/* ── ACEPTAR / RECHAZAR un pedido del portal (`_acceptance_actions.html`) ──
   Los dos gestos de un pedido POR ACEPTAR: en la fila de la lista (`is-compact`,
   donde un pendiente ofrece «Asignar») y en la franja de la ficha. Color solo de
   tokens; el índigo es el del estado. */
.disp-group-row.g-por_aceptar td { box-shadow: inset 3px 0 0 var(--status-por-aceptar); }
.acc-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-2); }
/* En la FILA de la lista van en su propia columna, «Acción pendiente», al final:
   SOLO los dos iconos, uno al lado del otro (el rótulo va en `title`). */
.acc-actions.is-compact { flex-wrap: nowrap; align-items: center; gap: 0.375rem; }
.acc-actions.is-compact .acc-btn { padding: 0.3125rem 0.625rem; font-size: 0.9rem; }
.acc-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 0; }
.acc-btn {
    display: inline-flex; align-items: center; gap: 0.3125rem;
    border: 1px solid transparent; border-radius: var(--radius-sm);
    padding: 0.25rem 0.5625rem; font-size: 0.76rem; font-weight: 600;
    cursor: pointer; white-space: nowrap; list-style: none;
}
.acc-btn::-webkit-details-marker { display: none; }
.acc-accept { background: var(--success); color: #fff; }
.acc-accept:hover { filter: brightness(0.93); }
.acc-reject { background: transparent; color: var(--stop-fail-ink); border-color: currentColor; }
.acc-reject:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }
.acc-template { display: flex; align-items: flex-start; gap: var(--space-2); flex-basis: 100%;
                font-size: 0.82rem; color: var(--gray-700); cursor: pointer; }
.acc-reject-box[open] { flex-basis: 100%; }
.acc-reject-box[open] > summary { margin-bottom: var(--space-2); }
.acc-reject-box .acc-form { align-items: stretch; flex-direction: column; max-width: 28rem; }

/* La franja de la ficha: lo único que se puede hacer con el ESTADO del pedido. */
.acc-banner {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--space-3); margin-bottom: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--status-por-aceptar); border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--status-por-aceptar) 12%, transparent);
}
.acc-banner-title { margin: 0; font-weight: 600; color: var(--gray-900); }
.acc-banner-sub { margin: 0; font-size: 0.85rem; color: var(--gray-500); }

/* El CONTADOR de pedidos por aceptar: un número, no un punto. En la barra
   lateral va junto a «Pedidos» y «Empresas» y REPETIDO en la cabecera del grupo
   (`is-group`), que solo se ve con el grupo plegado. */
.acc-count {
    display: inline-block; min-width: 1.25rem; margin-left: auto; padding: 0 0.35rem;
    border-radius: 999px; background: var(--status-por-aceptar); color: #fff;
    font-size: 0.7rem; font-weight: 700; line-height: 1.25rem; text-align: center;
    font-variant-numeric: tabular-nums;
}
/* `[hidden]` pierde contra el `display` de una clase: hace falta la pareja (y
   con más peso que la regla del teléfono que enciende `.is-toggle`). */
.acc-count[hidden], .acc-count.is-toggle[hidden], .acc-count.is-group[hidden] { display: none; }
.nav-pills .acc-count { margin-left: 0.25rem; }
.app-sidebar-group[open] > summary .acc-count.is-group { display: none; }
.app-sidebar-group-head .acc-count.is-group { margin-right: 0.375rem; }
/* El del botón del cajón solo existe en el teléfono (abajo). */
.acc-count.is-toggle { display: none; }

/* EN EL MAPA: el botón de la barra. Los pines de un pedido por aceptar son los
   de siempre con un rayado diagonal (los hornea `map-common.js`, estado
   'pending'), dentro de las mismas capas que el resto. */
.acc-map-btn { background: var(--status-por-aceptar); color: #fff; border: 0; font-weight: 600; }
.acc-map-btn:hover { color: #fff; filter: brightness(1.08); }
.acc-map-btn[hidden] { display: none; }
/* En la TARJETA de la parada (la de siempre): la nota de que el pedido está por
   aceptar y sus dos gestos, con el motivo del rechazo desplegado ahí mismo. La
   tarjeta es oscura en los dos temas, como el resto de sus notas. */
.map-status-por_aceptar { background: #e0e7ff; color: #3730a3; }
.msc-note.is-acceptance { background: rgba(99, 102, 241, 0.2); color: #c7d2fe; }
.msc-acc { margin: var(--space-2) 0; }
.msc-acc .acc-btn { flex: 1; justify-content: center; }
.msc-acc .acc-btn:not(.acc-accept):not(.acc-reject) {
    background: transparent; color: inherit; border-color: currentColor; opacity: 0.8;
}
.msc-acc .acc-reject { color: #fecaca; }
.msc-acc-reason { margin-top: var(--space-2); }
.msc-acc-reason .form-label { font-size: 0.78rem; margin-bottom: 0.25rem; }
.msc-acc-reason .acc-actions { margin-top: var(--space-2); }

/* Una tarjeta de KPI que es un ATAJO a la lista filtrada. */
a.kpi-card.is-link { display: block; text-decoration: none; color: inherit; }
a.kpi-card.is-link:hover { filter: brightness(0.97); }

/* «N por aceptar» de un local: una etiqueta que es un enlace a la lista ya
   filtrada. */
.acc-chip {
    display: inline-block; margin-left: 0.25rem; padding: 0.125rem 0.5rem;
    border-radius: 999px; background: color-mix(in srgb, var(--status-por-aceptar) 16%, transparent);
    color: var(--gray-900); border: 1px solid var(--status-por-aceptar);
    font-size: 0.76rem; font-weight: 600; text-decoration: none; white-space: nowrap;
}
.acc-chip:hover { background: var(--status-por-aceptar); color: #fff; }

/* Precio editable dentro de la fila. Mismo patrón que la asignación inline:
   un control de verdad en la celda, no un modo de edición que haya que
   descubrir. Se lee como TEXTO (sin caja, sin flechas: el spinner de un
   `number` no sirve para un importe y se come el ancho del campo) y solo se
   dibuja como campo cuando la celda tiene el ratón encima o el foco. El botón
   sigue el mismo criterio: una tabla de 20 filas no puede ser una tabla de 20
   botones. */
.disp-price { display: flex; align-items: center; gap: 0.1875rem; }
.disp-price input {
    /* Suelo pensado para que "Pendiente" entre ENTERO: un placeholder recortado
       no dice nada. */
    width: 5.75rem;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    padding: 0.1875rem 0.3125rem;
    font-size: 0.82rem;
    font-weight: 600;
    font-variant-numeric: var(--num);
    background: transparent;
    color: var(--gray-900);
    text-align: right;
}
.disp-price input::placeholder {
    font-weight: 400;
    font-style: italic;
    color: var(--gray-400);
    opacity: 1;  /* Firefox lo baja a .54 y el gris ya es suave de por sí. */
}
/* Las flechitas del `number` fuera en los dos motores. */
.disp-price input::-webkit-outer-spin-button,
.disp-price input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.disp-price input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.disp-price:hover input,
.disp-price input:focus {
    border-color: var(--gray-200);
    background: var(--surface);
    outline: none;
}
.disp-price input:focus { border-color: var(--primary); }
/* La moneda, siempre a la vista: el número solo no dice en qué se cobra. */
.disp-price-cur { font-size: 0.82rem; font-weight: 600; color: var(--gray-500); }
.disp-price button {
    border: none;
    background: var(--primary);
    color: #fff;
    border-radius: var(--radius-sm);
    padding: 0.25rem 0.5rem;
    font-size: 0.76rem;
    line-height: 1.2;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s ease;
}
.disp-price:hover button,
.disp-price:focus-within button { opacity: 1; pointer-events: auto; }
.disp-price button:hover { background: var(--primary-dark); }

/* Timer 'en_sitio' en la fila */
.disp-timer { font-size: 0.72rem; font-weight: 600; display: inline-block; margin-top: 2px; }
.disp-timer.warn { color: #b45309; }
.disp-timer.over { color: var(--status-cancelado); }

/* ==========================================================================
   Formulario de pedido (crear / editar): grilla producto×parada,
   editor de paradas y mini-card de precio. La lógica cliente (IDs y names)
   NO cambia: esto es solo presentación.
   ========================================================================== */

/* Grilla producto × parada */
.item-grid { width: 100%; border-collapse: collapse; font-variant-numeric: var(--num); }
.item-grid thead th {
    text-align: left;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-500);
    padding: 8px 10px;
    border-bottom: 1px solid var(--gray-200);
    background: var(--surface-sunken);
    white-space: nowrap;
    vertical-align: bottom;
}
.item-grid tbody td {
    padding: 8px 10px;
    border-bottom: 1px solid var(--gray-100);
    vertical-align: top;
    font-size: 0.85rem;
}
.item-grid tbody tr:last-child td { border-bottom: none; }
.item-grid .cell-qty-input { max-width: 88px; text-align: right; }
.item-grid .row-total-cell { text-align: right; font-weight: 700; color: var(--gray-900); }
.item-grid .remove-stop-col-btn { font-weight: 700; }

/* Buscador + "Ver catálogo": una fila en escritorio, apilados a ancho completo
   en el teléfono. Reemplaza al `style="min-width: 220px"` inline que había en
   las tres superficies del formulario — a 360px ese mínimo forzaba el wrap del
   flex y dejaba el botón solo en su línea, alineado a la izquierda, leyéndose
   como si perteneciera a otra cosa. */
.product-picker-bar {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: stretch;
}
@media (min-width: 576px) {
    .product-picker-bar { flex-direction: row; align-items: flex-start; }
    .product-picker-bar #product-search { flex: 1 1 220px; min-width: 0; }
    .product-picker-bar > .btn { flex-shrink: 0; }
}

/* Selector de productos (buscador + "Ver catálogo"), compartido por el alta, la
   edición y los dos formularios de plantilla — vía admin/_order_grid_js.html y
   su gemelo del portal, company/_order_grid_js.html.
   Los resultados FLOTAN sobre la grilla, así que necesitan superficie propia:
   en el tema oscuro del admin los .list-group-item quedan transparentes y sin
   esto se leería la tabla de abajo a través de la lista. */
#product-search-results {
    background: var(--surface-raised);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-md);
    overflow: hidden;
}
#catalog-modal-list .list-group-item,
#product-search-results .list-group-item { font-size: 0.85rem; }
#catalog-modal-list { max-height: 52vh; overflow-y: auto; }

/* Desplegable de direcciones (address-picker.js): la LIBRETA (tanda E) y, desde
   el paso 3 del autocompletado, las direcciones que encuentra el geocoder. Las
   dos secciones comparten estas clases — se distinguen por su cabecera de
   grupo, no por un estilo propio: son la misma pregunta con dos orígenes.
   Misma regla que los resultados de producto de arriba: la lista FLOTA sobre el
   formulario, así que lleva superficie propia — con el remapeo oscuro del admin
   los .list-group-item quedan transparentes y se leerían los campos de abajo a
   través de las direcciones. */
.address-picker-list {
    background: var(--surface-raised);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-md);
    overflow: hidden auto;
    max-height: 280px;
    z-index: 6;
}
.address-picker-group {
    padding: 6px 12px 2px;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-500);
}
.address-picker-empty {
    padding: 10px 12px;
    font-size: 0.82rem;
    color: var(--gray-500);
}
/* Crédito de los proveedores del geocoder (CartoCiudad va con CC BY 4.0): es
   una nota al pie de la lista, no una fila elegible. */
.address-picker-credit {
    padding: 8px 12px;
    border-top: 1px solid var(--gray-200);
    font-size: 0.68rem;
    line-height: 1.35;
    color: var(--gray-500);
}
.address-picker-credit a { color: inherit; text-decoration: underline; }
/* Los proveedores del buscador FALLARON (no contestaron vacío): el aviso y
   la salida al mapa. No es una fila elegible, igual que el crédito. */
.address-picker-unavailable {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 0.625rem 0.75rem;
    font-size: 0.82rem;
    color: var(--gray-600);
}
.address-picker-item { display: block; text-align: left; font-size: 0.85rem; }
.address-picker-item.is-active { background: var(--gray-100); }
.address-picker-name { display: block; font-weight: 600; }
.address-picker-addr { display: block; }
/* Segunda línea de un candidato del geocoder (barrio/distrito + municipio).
   UNA línea siempre, cortada con puntos suspensivos: una fila del teléfono no
   puede crecer a tres renglones porque un barrio se llame largo. */
.address-picker-sub {
    display: block;
    font-size: 0.75rem;
    color: var(--gray-600);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.address-picker-meta {
    display: block;
    font-size: 0.75rem;
    color: var(--gray-500);
}
/* Dirección elegida: el aro dice que esos campos los llenó una lista —la
   libreta o el geocoder— y que hay algo viajando en el formulario (el id de la
   entrada, o el punto y su firma). Tocar cualquier campo lo suelta. */
.address-picker.is-picked .address-picker-input {
    border-color: var(--primary);
}

/* "Ajustar en el mapa" (paso 4 del autocompletado). El estado se dice con
   PALABRAS al lado del botón ("Punto ajustado en el mapa") y no con un color:
   es un dato del pedido, no una decoración, y tiene que leerse igual en el
   chrome oscuro del admin y en el claro del portal. */
.address-adjust.is-manual .address-adjust-state {
    color: var(--primary);
    font-weight: 600;
}
/* El mapa del modal. Alto fijo porque el modal no tiene alto propio: un mapa
   con height:100% dentro de un contenedor sin altura mide 0 y MapLibre no
   dibuja nada. En un teléfono baja para que el botón "Usar este punto" siga
   entrando en la pantalla sin scrollear el modal. */
.address-map {
    height: 420px;
    width: 100%;
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--gray-100);
}
@media (max-width: 767.98px) {
    .address-map { height: 300px; }
}
/* Lo que el mapa dice al abrirse: dónde ha quedado el pin, si es una propuesta
   o si no se pudo ubicar (`data-state`, de `initialPin`). Reserva su línea para
   que el mapa no salte cuando llega el texto. Tinta de los tokens de parada,
   que ya tienen su par para el chrome oscuro del admin. */
.address-map-status { min-height: 1.5em; color: var(--gray-700); font-weight: 500; }
.address-map-status[data-state="proposed"] { color: var(--stop-drop-ink); }
.address-map-status[data-state="missing"],
.address-map-status[data-state="unavailable"] { color: var(--stop-fail-ink); }

/* Editor de paradas adicionales (bloques generados por JS: .extra-stop-block).
   design-system.css se carga DESPUÉS de Bootstrap, así que estas reglas ganan
   sobre las utilidades .border/.rounded/.p-2 del markup sin !important. */
.extra-stop-block {
    background: var(--surface);
    border: 1px solid var(--gray-200);
    /* El borde ámbar de la izquierda es el mismo que lleva esta parada en el
       panel del mapa y en la ficha del pedido: una entrega se reconoce por el
       color en las tres pantallas, también mientras se la está escribiendo. */
    border-left: 3px solid var(--stop-drop);
    border-radius: var(--radius-md);
    padding: 12px;
    margin-bottom: 10px;
}
/* El número lo escribe el JS de la grilla (`textContent` sobre este mismo
   elemento, también en el portal de empresa): el pin va FUERA, como hermano,
   o cada renumeración se lo llevaría por delante. */
.extra-stop-block .stop-block-label { font-weight: 800; }
/* El bloque es una `.cp-card` por dentro, así que su padding de 20px se SUMABA
   a los 12px de acá: con tres entregas, la mitad de la pantalla era margen. El
   marco lo pone el bloque; el cuerpo no necesita el suyo. */
.extra-stop-block > .cp-card-body { padding: 0; }

/* Mini-card de precio (preview / último cálculo) */
.price-preview .cp-card-body,
.price-preview.cp-card-body { padding: 0.875rem 1rem; }
.price-preview-total {
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--gray-900);
    font-variant-numeric: var(--num);
}
.price-line-list { list-style: none; margin: 0 0 8px; padding: 0; }
.price-line-list li {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    font-size: 0.82rem;
    color: var(--gray-600, var(--gray-700));
    padding: 2px 0;
    font-variant-numeric: var(--num);
}

/* --------------------------------------------------------------------------
   El formulario de pedido EN EL TELÉFONO
   --------------------------------------------------------------------------
   El dispatcher da de alta pedidos EN VIVO desde el móvil: esta pantalla no es
   una versión degradada de la de escritorio, es la superficie principal. Todo
   lo de abajo vive en `max-width: 575.98px` (el breakpoint donde Bootstrap deja
   de tener columnas `-sm`, o sea donde el formulario ya está en una sola
   columna) y NADA de esto llega al escritorio.
   -------------------------------------------------------------------------- */

/* Acá vivía `.mess-form-sticky`, que pegaba la tarjeta de precio a partir de
   992px. Se RETIRÓ: las dos columnas derechas que la usaban (alta de pedido y
   formulario de plantilla) dejaron de ser UNA tarjeta corta, y una pegajosa
   arriba de una PILA hace que las de abajo le pasen por DETRÁS al scrollear.
   La regla no se deja huérfana a propósito: una clase sin dueño es una
   invitación a volver a ponérsela a la primera tarjeta de una columna. */

/* La grilla producto × parada NO cabe en un teléfono en cuanto hay más de una
   entrega, y comprimirla no es una opción: la columna del producto se partía
   letra a letra. El scroll horizontal es la respuesta correcta, pero tiene que
   estar CONTENIDO (nunca desbordar la página) y ser evidente. */
.item-grid-scroll { -webkit-overflow-scrolling: touch; }
.item-grid-hint { display: flex; align-items: center; gap: 6px; }
/* El `display: flex` de arriba GANA sobre el `display: none` que el navegador
   le da a `[hidden]` (una clase pesa más que un atributo), así que el aviso se
   veía igual cuando el JS lo apagaba. La regla no es defensiva: sin ella el
   `hidden` de `renderGrid()` no hace nada. */
.item-grid-hint[hidden] { display: none; }
@media (max-width: 575.98px) {
    /* Columna del producto: un suelo para que el nombre se lea, y un techo
       para que no se coma sola el ancho del scroll. */
    .item-grid thead th:first-child,
    .item-grid tbody td:first-child { min-width: 9.5rem; max-width: 12rem; }
    /* Las columnas de cantidad son el objetivo de toque real de la grilla. */
    .item-grid thead th + th,
    .item-grid tbody td + td { min-width: 5.5rem; }
    .item-grid .cell-qty-input { max-width: none; }
}

/* Barra de acciones del formulario. En escritorio es la fila de siempre
   (submit + cancelar, alineados a la izquierda). En el teléfono se pega al
   borde inferior con los dos botones a ancho completo: el alta mide varias
   pantallas de alto y buscar "Crear pedido" al final es el gesto que más se
   repite del día. `env(safe-area-inset-bottom)` la levanta de la barra de
   gestos de iOS — funciona porque `base.html` emite `viewport-fit=cover`. */
.form-actionbar { display: flex; gap: var(--space-4); }
.form-actionbar .btn { flex-shrink: 0; }
@media (max-width: 575.98px) {
    .form-actionbar {
        position: sticky;
        bottom: 0;
        z-index: 10;
        gap: var(--space-2);
        margin: var(--space-4) calc(var(--space-3) * -1) 0;
        padding: var(--space-3);
        padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
        background: var(--surface);
        border-top: 1px solid var(--gray-200);
    }
    /* `.form-touch .btn:not(.btn-link)` de más abajo pesa MÁS que un
       `.form-actionbar .btn` pelado y le impondría sus 40px: los botones
       principales van con la misma cuenta de selectores para ganar por orden. */
    .form-touch .form-actionbar .btn:not(.btn-link),
    .form-actionbar .btn {
        flex: 1 1 0;
        min-height: 48px;
        padding-left: var(--space-2);
        padding-right: var(--space-2);
    }
}

@media (max-width: 575.98px) {
    /* Aire interno de las tarjetas: 20px por lado sobre 360px es el 11% del
       ancho, y el formulario son ~8 tarjetas anidando paradas dentro. */
    .cp-card-body { padding: var(--space-3); }
    .extra-stop-block { padding: var(--space-2); }

    /* iOS hace ZOOM automático al enfocar un input de menos de 16px, y el admin
       usa `form-control-sm` en casi todo el formulario de pedido (las paradas
       extra van enteras en `compact`). El zoom deja la página desencuadrada y
       obliga a un pellizco por campo. 16px es el mínimo que lo evita; la altura
       de 44px es el objetivo de toque.
       Se marca con `.form-touch` en el <form>, no se aplica a todo el admin:
       el resto de las pantallas del portal tiene su propia tanda y subirles la
       altura de los controles les cambia el layout de los filtros. Cuando les
       toque, heredan esto poniéndose la clase. */
    .form-touch .form-control,
    .form-touch .form-select,
    .form-touch .form-control-sm,
    .form-touch .form-select-sm {
        font-size: 16px;
        min-height: 44px;
    }
    .form-touch textarea.form-control { min-height: 66px; }
    /* El input de la planta pierde su tope: con Planta a ancho completo, el
       select y el número se reparten la fila y 5rem lo dejaban de sobra. */
    .form-touch .address-planta-num { max-width: 6rem; }

    /* Botones de acción del formulario (quitar parada, quitar producto,
       agregar producto/parada, ver catálogo). `btn-sm` deja ~31px de alto.
       `.btn-link` queda FUERA: son enlaces en línea ("+ nueva unidad", la × de
       la cabecera de una columna) y estirarlos a 40px rompe la fila. */
    .form-touch .btn:not(.btn-link) { min-height: 40px; }
    /* Las listas flotantes (libreta de direcciones, resultados de producto,
       catálogo) son lo que más se toca del alta y sus filas eran de ~30px. */
    .form-touch .list-group-item-action,
    #catalog-modal .list-group-item-action { min-height: 44px; }
}

/* De `md` para abajo el tope de 5rem del número de planta sigue valiendo (la
   fila es de dos columnas, no de cinco). No se declara dentro del media query
   de arriba porque el tope es el comportamiento por DEFECTO, no la excepción. */
.address-planta-num { max-width: 5rem; }

/* ==========================================================================
   Detalle de pedido (admin/order_detail.html): stepper de estado + desglose
   ========================================================================== */
.status-stepper {
    display: flex;
    align-items: flex-start;
    margin: 4px 0 var(--space-5);
}
.stepper-node {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    flex: 1;
    min-width: 56px;
    position: relative;
}
.stepper-line {
    position: absolute;
    top: 13px;
    right: 50%;
    width: 100%;
    height: 2px;
    background: var(--gray-200);
    z-index: 0;
}
.stepper-node:first-child .stepper-line { display: none; }
/* Muchos pasos (la ficha del portal: uno por recogida y por entrega): la barra
   RUEDA en horizontal en vez de apretar las etiquetas hasta que no se lean. */
.status-stepper.is-long { overflow-x: auto; padding-bottom: var(--space-2); }
.status-stepper.is-long .stepper-node { flex: 1 0 4.5rem; }
.stepper-dot {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 2px solid var(--gray-300);
    background: var(--surface);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8rem;
    color: var(--gray-400);
    z-index: 1;
}
.stepper-label {
    font-size: 0.68rem;
    color: var(--gray-400);
    text-align: center;
    line-height: 1.2;
}
.stepper-node.done .stepper-dot { border-color: var(--status-entregado); color: var(--status-entregado); }
.stepper-node.done .stepper-line { background: var(--status-entregado); }
.stepper-node.current .stepper-dot { border-color: var(--primary); background: var(--primary); color: #fff; }
.stepper-node.current .stepper-line { background: var(--status-entregado); }
.stepper-node.current .stepper-label { color: var(--gray-900); font-weight: 700; }
.stepper-node.cancelled .stepper-dot { border-color: var(--status-cancelado); background: var(--status-cancelado); color: #fff; }
.stepper-node.cancelled .stepper-label { color: var(--status-cancelado); font-weight: 700; }
/* Variante verde del portal empresa (identidad --cp) */
.status-stepper.cp-scope .stepper-node.done .stepper-dot { border-color: var(--cp); color: var(--cp); }
.status-stepper.cp-scope .stepper-node.done .stepper-line,
.status-stepper.cp-scope .stepper-node.current .stepper-line { background: var(--cp); }
.status-stepper.cp-scope .stepper-node.current .stepper-dot { border-color: var(--cp); background: var(--cp); }

/* Día de servicio del pedido (detalle) — va bajo el título, donde antes se
   repetía el estado. Un pedido "al vuelo" (sin scheduled_date) se ve en gris:
   se hace el día en que se creó, pero nadie lo comprometió para ese día. */
.order-day-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px;
    border-radius: 20px;
    background: var(--primary-light);
    color: var(--primary-dark);
    border: 1px solid rgba(74, 144, 226, 0.25);
    font-size: 0.82rem;
    font-weight: 600;
    font-variant-numeric: var(--num);
}
.order-day-chip .order-day-rel {
    font-weight: 500;
    opacity: 0.75;
    text-transform: lowercase;
}
.order-day-chip .order-day-rel::before { content: '· '; }
.order-day-chip.is-loose {
    background: var(--gray-100);
    color: var(--gray-700);
    border-color: var(--gray-200);
}
.order-day-chip.is-late {
    background: #fdecea;
    color: #a93226;
    border-color: rgba(231, 76, 60, 0.3);
}

/* Franja horaria comprometida de una parada (Stop.window_from/window_to) */
.win-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 1px 8px;
    border-radius: 20px;
    background: var(--gray-100);
    color: var(--gray-700);
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
    font-variant-numeric: var(--num);
}
.win-chip i { color: var(--gray-400); }

/* Parada (detalle) */
.stop-block { padding: 0.75rem 0; border-bottom: 1px solid var(--gray-100); }
/* En la ficha cada dirección es un bloque SEPARADO: con el selector de estado
   al pie, pegadas se leía mal a cuál pertenece cada uno. */
.order-detail-page .stop-block + .stop-block { margin-top: 1.25rem; }
.order-detail-page .stop-block { padding-top: 0.875rem; padding-bottom: 0.875rem; }
/* Una parte de una recogida repartida (o una recogida de varias): su rider y
   lo que carga, y debajo su estado. */
.stop-leg + .stop-leg { margin-top: 0.5rem; }
.stop-leg-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }
.stop-block:last-child { border-bottom: none; }
.stop-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.stop-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.375rem;
    height: 1.375rem;
    border-radius: 50%;
    background: var(--gray-100);
    color: var(--gray-700);
    font-size: 0.72rem;
    font-weight: 700;
    flex-shrink: 0;
    font-variant-numeric: var(--num);
}
.stop-head .stop-addr { font-weight: 600; color: var(--gray-900); font-size: 0.88rem; }

/* Desglose de precio */
.price-breakdown { width: 100%; font-variant-numeric: var(--num); }
.price-breakdown td { padding: 5px 0; font-size: 0.85rem; color: var(--gray-700); }
.price-breakdown td:last-child { text-align: right; white-space: nowrap; }
.price-breakdown tr.total td {
    border-top: 1px solid var(--gray-200);
    font-weight: 700;
    padding-top: 9px;
    color: var(--gray-900);
    font-size: 0.95rem;
}

/* ==========================================================================
   Dashboard del rider (movido desde rider/dashboard.html — antes inline)
   ========================================================================== */
.dash-greeting {
    font-size: 1.15rem;
    font-weight: 600;
    color: var(--dark);
    margin: 0;
}

/* ===== Session pill (jornada activa) ===== */
.session-pill-wrap { position: relative; }
.session-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: #eaf3fe;
    border: none;
    border-radius: 20px;
    padding: 6px 14px 6px 10px;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--primary);
    cursor: pointer;
    white-space: nowrap;
}
.session-pill .dot {
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--success);
    flex-shrink: 0;
}
.session-pill.paused .dot { background: var(--warning); }
.session-pill .pill-chevron {
    font-size: 0.7rem;
    transition: transform 0.2s;
}
.session-pill.open .pill-chevron { transform: rotate(180deg); }

.session-pill-dropdown {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    background: #fff;
    border: 1px solid #e5e7eb;
    border-radius: 12px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.12);
    z-index: 200;
    overflow: hidden;
    min-width: 200px;
}
.session-pill-dropdown form { margin: 0; }
.session-pill-dropdown button {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 13px 16px;
    background: none;
    border: none;
    border-bottom: 1px solid #f3f4f6;
    font-size: 0.88rem;
    font-weight: 500;
    color: var(--dark);
    cursor: pointer;
    text-align: left;
    transition: background 0.12s;
}
.session-pill-dropdown form:last-child button { border-bottom: none; }
.session-pill-dropdown button:hover { background: #f8f9fa; }
.session-pill-dropdown button.danger { color: var(--danger); }

/* ===== PANTALLA 1: sin jornada ===== */
.no-session-screen {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 56px 16px 32px;
}
.no-session-icon {
    font-size: 4.5rem;
    color: var(--primary);
    opacity: 0.25;
    margin-bottom: 20px;
}
/* El título es lo ÚNICO que hay entre el icono y el botón: se retiró el
   subtítulo ("no vas a recibir pedidos nuevos hasta que arranques", que además
   no era cierto), así que hereda su separación de abajo. */
.no-session-title {
    font-size: 1.2rem;
    font-weight: 700;
    color: var(--dark);
    margin-bottom: 24px;
}
.start-session-btn {
    font-size: 1rem;
    padding: 14px 40px;
    border-radius: 14px;
}

/* ===== Pedido activo — específico ===== */
#activeOrderCard { cursor: pointer; }
.highlight-card:hover { transform: none; box-shadow: 0 4px 16px rgba(74,144,226,0.35); }
.aoc-label {
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    opacity: 0.85;
    text-transform: uppercase;
}
.aoc-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 0.75rem;
}
.aoc-id {
    font-size: 1.2rem;
    font-weight: 700;
}
.aoc-pickup {
    display: flex;
    align-items: flex-start;
    gap: 0.375rem;
    font-size: 0.9rem;
    margin-bottom: 0.625rem;
    opacity: 0.95;
}
.aoc-pickup i { flex-shrink: 0; margin-top: 2px; }
.aoc-status-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.875rem;
}
.aoc-status-badge {
    background: rgba(255,255,255,0.2);
    border-radius: 8px;
    padding: 0.1875rem 0.625rem;
    font-size: 0.8rem;
    font-weight: 600;
}
.aoc-timer {
    font-size: 0.8rem;
    font-weight: 600;
    opacity: 0.9;
}
.aoc-timer.overdue {
    color: #ff8a8a;
    opacity: 1;
}
.aoc-details {
    border-top: 1px solid rgba(255,255,255,0.2);
    margin-bottom: 0.875rem;
}
.aoc-details summary {
    padding: 0.625rem 0 0;
    font-size: 0.82rem;
    font-weight: 600;
    opacity: 0.9;
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 0.375rem;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}
.aoc-details summary::-webkit-details-marker { display: none; }
.aoc-details summary .det-chevron { font-size: 0.7rem; transition: transform 0.2s; }
.aoc-details[open] summary .det-chevron { transform: rotate(180deg); }
.aoc-detail-body {
    padding: 0.625rem 0 0.25rem;
    font-size: 0.85rem;
}
.aoc-detail-body p { margin-bottom: 0.375rem; opacity: 0.95; }
.aoc-detail-body ul {
    margin: 0.25rem 0 0.375rem 0;
    padding-left: 1rem;
    opacity: 0.9;
}
.aoc-detail-body li { margin-bottom: 2px; }
.aoc-multi-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    background: rgba(255,255,255,0.2);
    border-radius: 6px;
    padding: 0.1875rem 0.5625rem;
    font-size: 0.75rem;
    font-weight: 600;
    margin-top: 0.375rem;
    width: fit-content;
}
.aoc-route-badge {
    display: inline-block;
    background: rgba(255,255,255,0.2);
    border-radius: 6px;
    padding: 2px 0.5rem;
    font-size: 0.78rem;
    font-weight: 500;
    margin-top: 0.25rem;
}
.aoc-action-btn {
    width: 100%;
    background: #fff;
    color: var(--primary);
    border: none;
    border-radius: 10px;
    font-weight: 700;
    font-size: 0.95rem;
    padding: 0.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}
.aoc-action-btn:hover {
    background: #f0f4ff;
    color: var(--primary);
    transform: none;
    box-shadow: none;
}
/* Aviso en el lugar del botón de acción: la parada existe y hay que ir hasta
   ahí, pero no se opera desde este lado (punto de traspaso del rider origen).
   Ocupa el mismo hueco que el botón para que la tarjeta no "salte". */
.aoc-hint {
    background: #f5f3ff;
    color: #5b21b6;
    border-radius: 10px;
    padding: 0.6875rem 0.75rem;
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.35;
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}
/* Variante ÁMBAR: falta algo que el rider tiene que resolver antes (el pedido
   todavía no está recogido), distinto del morado informativo del traspaso. */
.aoc-hint--warn { background: #fef3c7; color: #92400e; }
/* Dentro del aviso ámbar el círculo verde saltaría: se lee como parte de la
   etiqueta, con el mismo ámbar en negativo. */
.aoc-hint--warn .seq-chip { background: #92400e; color: #fef3c7; }
/* Sobre la tarjeta de color del Inicio, donde el morado no contrastaría. Va
   ÚLTIMO a propósito: gana sobre las variantes de color de arriba. */
.aoc-hint--on-color { background: rgba(255, 255, 255, 0.18); color: #fff; }

/* ===== Gráfico semanal (rider) ===== */
.weekly-chart-card {
    background: #fff;
    border-radius: 12px;
    padding: 14px 16px 8px;
    margin: 18px 0;
    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
.weekly-chart-title {
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--dark);
    margin: 0 0 10px;
}
.weekly-chart-box { height: 200px; position: relative; }

/* ===== Cumplimiento de turno (tarjeta destacada del dashboard rider) ===== */
.shift-progress {
    display: block;
    background: #fff;
    border: 1px solid var(--gray-200);
    border-radius: 12px;
    padding: 12px 14px;
    margin-bottom: 14px;
    text-decoration: none;
    color: inherit;
    -webkit-tap-highlight-color: transparent;
}
.shift-progress:active { background: #f9fafb; }
.shift-progress-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
}
.shift-progress-label {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--gray-500);
    display: flex;
    align-items: center;
    gap: 6px;
}
.shift-progress-pct { font-size: 0.95rem; font-weight: 700; color: var(--gray-900); font-variant-numeric: var(--num); }
.shift-progress-pct.ok { color: var(--cp); }
.shift-progress-bar { height: 8px; border-radius: 6px; background: var(--gray-100); overflow: hidden; }
.shift-progress-fill { height: 100%; border-radius: 6px; background: var(--primary); transition: width 0.3s; }
.shift-progress-fill.ok { background: var(--cp); }
.shift-progress-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 0.75rem;
    color: var(--gray-500);
    margin-top: 7px;
    font-variant-numeric: var(--num);
}
.shift-progress-meta .delta.pos { color: var(--cp); font-weight: 600; }
.shift-progress-meta .delta.neg { color: #b45309; font-weight: 600; }
.shift-progress-times {
    font-size: 0.75rem;
    color: var(--gray-500);
    margin-top: 7px;
    border-top: 1px solid var(--gray-100);
    padding-top: 7px;
    font-variant-numeric: var(--num);
}

/* Chip informativo (turno planificado en pantalla "sin jornada") */
.chip.chip-info { background: #f0f9ff; border: 1px solid #bae6fd; color: #0369a1; }

/* ==========================================================================
   Rider: drag-and-drop de "Próximos" + cards de parada compartidas.
   (Las clases del viejo route_detail.html/routes.html del rider se retiraron
   al migrar "Mis Rutas" al modelo por-parada; lo que queda acá lo usan la
   pestaña "Próximos" de Mis Pedidos, order_detail y las vistas de admin.)
   ========================================================================== */

/* Estados de drag-and-drop (SortableJS de "Próximos") */
.sortable-ghost {
    opacity: 0.4;
    background: #eef2ff !important;
    border: 2px dashed #c7d2fe;
    border-radius: 12px;
}
.sortable-ghost * { visibility: hidden; }
.sortable-drag {
    opacity: 0.98 !important;
    box-shadow: 0 12px 32px rgba(0,0,0,0.22) !important;
    background: #fff !important;
}
/* Elemento agarrado (aún en su lugar): lo elevamos sutilmente para dar
   feedback de "listo para arrastrar". */
.sortable-chosen {
    box-shadow: 0 0 0 2px var(--primary), 0 6px 18px rgba(74,144,226,0.22) !important;
}

/* ===== Card de parada (compartida por order_detail/orders del rider y admin) =====
   Sobrevive a la poda del route_detail del rider: .stop-card y .stop-addr las
   siguen usando rider/order_detail, rider/orders y las vistas de admin. */
.stop-card {
    display: flex;
    align-items: stretch;
    gap: 0.75rem;
    background: #fff;
    border-radius: 12px;
    padding: 0.875rem 1rem;
    margin-bottom: 0.625rem;
    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.1s, box-shadow 0.12s;
}
.stop-card.is-current { box-shadow: 0 0 0 2px var(--primary), 0 1px 4px rgba(0,0,0,0.06); }

.stop-addr {
    font-size: 0.83rem;
    color: #374151;
    margin-bottom: 0.3125rem;
    display: flex;
    align-items: flex-start;
    gap: 0.375rem;
}
.stop-addr i { flex-shrink: 0; margin-top: 2px; }
.order-card-stops-list li.delivered { color: #6b7280; text-decoration: line-through; }

/* ══════════════════════════════════════════════════════════════════════════
   MAPA (MapLibre + Protomaps) — dispatcher + rider
   ═════════════════════════════════════════════════════════════════════════ */

/* ── Pantalla completa del dispatcher ─────────────────────────────────────── */
/* El alto sale de LO QUE SOBRA, no de un número mágico. Era
   `calc(100vh - 190px)` repetido en el canvas y en el panel, y ese 190
   descontaba a ojo la barra superior, el padding del borde y la barra de
   herramientas — o sea tres cosas que cambian solas: basta que el título gane
   una línea, que los filtros envuelvan o que la densidad de la UI encoja el
   padding para que sobre (o falte) aire debajo del mapa.
   Misma disciplina que `.dash-page`: la pantalla mide exactamente el hueco del
   chrome —las dos medidas leídas de SU variable, no escritas acá—, el cuerpo se
   queda con lo que sobra y el canvas y el panel se estiran solos. Y `height`,
   no `min-height`: con un mínimo, lo que crezca por dentro empuja la página y
   devuelve el scroll. */
.map-screen {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: calc(100vh - var(--topbar-h) - var(--content-pad) * 2);
}
.map-screen-bar {
    display: flex; align-items: center; justify-content: flex-start;
    flex-wrap: wrap; gap: var(--space-2);
}

/* ── El DÍA en la barra del mapa (navegación + alcance + contador) ──────────
   El mapa del dispatcher se mira POR DÍA: el día es el contexto, no un filtro
   más, y por eso vive arriba del todo y no entre los chips del panel. Mismo
   gesto que el calendario (‹ día ›) para que se opere sin volver a aprenderlo. */
.map-daynav { display: flex; align-items: center; gap: 0.25rem; }
.map-daynav-label {
    border: 1px solid var(--gray-300); background: var(--surface);
    border-radius: var(--radius-sm); cursor: pointer;
    font-size: 0.82rem; font-weight: 700; color: var(--gray-700);
    padding: 0.1875rem 0.625rem; min-width: 8.25rem; text-align: center;
    text-transform: capitalize; white-space: nowrap;
}
.map-daynav-label:hover { border-color: var(--primary); color: var(--primary); }
/* El <input type="date"> es el selector real; el botón de arriba lo abre. Se
   deja en el DOM (y no display:none) porque showPicker() necesita un campo
   visible en varios navegadores: se esconde por tamaño, no por visibilidad. */
.map-daynav-input {
    width: 0; height: 0; padding: 0; margin: 0; border: 0;
    opacity: 0; pointer-events: none;
}
.map-daycount {
    display: flex; align-items: center; gap: 0.375rem;
    font-size: 0.78rem; color: var(--gray-600);
    padding: 0.1875rem 0.625rem; border-radius: 999px; background: var(--gray-100);
}
.map-daycount-main { font-weight: 700; color: var(--gray-800); }
.map-daycount-sep { color: var(--gray-400); }
.map-daycount-warn.is-on { color: #b45309; font-weight: 700; }
.map-daycount-late {
    display: inline-flex; align-items: center; gap: 0.25rem;
    color: #92400e; background: #fde68a; font-weight: 700;
    border-radius: 999px; padding: 0 0.5rem; cursor: help;
}
/* Lo PREVISTO en el contador: cuánto falta por generar. Tono propio (y no el
   ámbar de "sin rutear") porque no es trabajo atrasado ni una parada: es algo
   que todavía no existe.

   Es un BOTÓN, no un rótulo: abre la ventana "Previsto" sobre el mapa. Dos cosas
   tienen que verse SIN tocarlo:

   - que se puede tocar → va con el tono de ACCIÓN (`--primary`), no con el gris
     de los rótulos vecinos, y con un chevron que dice para dónde abre. Estaba
     escrito con colores a mano (`#334155` sobre `--gray-200`), y como
     `body.role-admin` remapea los grises a blancos translúcidos, el resultado era
     texto oscuro sobre fondo oscuro: ILEGIBLE en la única pantalla donde vive.
     Los dos tokens de acá se remapean juntos y siguen contrastando en las dos.
   - si está encendido (`is-on`) → un interruptor que no lo dice obliga a mirar el
     mapa para saber qué se acaba de hacer. */
.map-daycount-plan {
    display: inline-flex; align-items: center; gap: 0.3125rem;
    color: var(--primary); background: var(--primary-light); font-weight: 700;
    font-size: inherit; line-height: 1.5;
    border: 1px solid var(--primary); border-radius: 999px; padding: 0 0.5625rem;
    cursor: pointer;
}
.map-daycount-plan:hover { background: var(--primary); color: #fff; }
.map-daycount-plan.is-on { background: var(--primary); border-color: var(--primary); color: #fff; }
.map-daycount-plan-caret { font-size: 0.62rem; opacity: 0.85; }

/* ── Cargar trabajo en el día: en la BARRA DEL MAPA son DOS botones ─────────
   Las dos maneras de crear un pedido —desde una plantilla y desde cero— son dos
   gestos distintos y ninguno es "más principal" que el otro: en el mapa cada uno
   lleva su botón, en vez de esconder uno detrás del ▾.

   El botón PARTIDO de abajo no se va con eso: lo emite `_new_order_split.html`,
   que pone «Nuevo pedido ▾» en el RESTO de las pantallas de las dos caras. Ahí
   la barra de acciones no tiene sitio para dos botones y el ▾ sigue siendo la
   respuesta; acá la barra es del DÍA y el sitio sobra. */
.map-addbtns { display: inline-flex; gap: 0.25rem; }
.map-addbtns[hidden] { display: none; }

/* ── Botón partido "Nuevo pedido ▾" (fuera del mapa) ────────────────────────
   Las dos maneras de crear un pedido en UN control: la mitad principal es el
   enlace de siempre (sigue siendo un `<a href>`) y el ▾ abre el menú. */
.map-addsplit { position: relative; display: inline-flex; }
.map-addsplit[hidden] { display: none; }
.map-addsplit-main { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.map-addsplit-toggle {
    border-top-left-radius: 0; border-bottom-left-radius: 0;
    border-left-color: rgba(255, 255, 255, 0.35);
    padding-left: 0.375rem; padding-right: 0.375rem;
}
/* El menú FLOTA: superficie propia y por encima de los controles de MapLibre
   (que van en z-index 2-3). */
.map-addmenu {
    position: absolute; top: calc(100% + 4px); right: 0; z-index: 20;
    min-width: 14.5rem;
    display: flex; flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-md);
    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
    padding: 0.25rem;
}
.map-addmenu[hidden] { display: none; }
.map-addmenu-item {
    display: flex; align-items: center; gap: 0.5rem;
    width: 100%; text-align: left;
    background: transparent; border: 0; border-radius: var(--radius-sm);
    color: var(--gray-700); font-size: 0.84rem;
    padding: 0.4375rem 0.625rem; text-decoration: none;
}
.map-addmenu-item[hidden] { display: none; }
.map-addmenu-item:hover { background: var(--gray-100); color: var(--primary); }
.map-addmenu-count {
    margin-left: auto;
    font-size: 0.7rem; font-weight: 700;
    color: #fff; background: var(--primary);
    border-radius: 999px; padding: 0 0.4375rem;
}
.map-addmenu-count[hidden] { display: none; }

@media (max-width: 700px) {
    .map-daynav-label { min-width: 0; }
    .map-daycount { width: 100%; justify-content: center; }
}
.map-screen-body {
    display: flex;
    gap: var(--space-3);
    align-items: stretch;
    /* `flex: 1` + `min-height: 0`: se lleva el alto que deja la barra de
       herramientas y puede encogerse por debajo de su contenido, que es lo que
       deja que el scroll viva DENTRO del panel y no en la página. */
    flex: 1;
    min-height: 0;
    /* El ancho del panel (`--map-panel-w`, declarado en `:root`) es lo que deja
       anclar al borde derecho del CANVAS lo que se dibuja encima: la ventana
       flotante "Previsto" se ancla a este contenedor (que mide el mapa MÁS el
       panel) y tiene que quedar a la izquierda de los botones +/− de zoom, que
       MapLibre pone en el borde derecho del CANVAS. O sea, a
       `panel + gap + zoom` del borde derecho de acá. Escrito a mano, el día que
       el panel cambie de ancho la ventana taparía el zoom. */
    --map-zoom-clear: 52px;   /* los +/− de MapLibre (29px) más su margen */
    --map-planwin-right: calc(var(--map-panel-w) + var(--space-3) + var(--map-zoom-clear));
}
.map-screen-canvas {
    flex: 1;
    min-width: 0;
    /* Lo estira el `align-items: stretch` del cuerpo. Sin suelo a propósito
       (el mismo criterio que el tablero de la torre de control): un
       `min-height` acá es justo lo que haría desbordar la pantalla en un
       portátil bajo, que es de lo que este cambio la saca. */
    height: auto;
    min-height: 0;
    border-radius: var(--radius-lg);
    border: 1px solid var(--gray-200);
    overflow: hidden;
}
.map-legend-panel {
    /* El ancho del panel se declara ACÁ y en un solo sitio. Estaba escrito dos
       veces —240px acá y `.map-legend-panel { width: 380px }` unas líneas más
       abajo, DESPUÉS del media query de 900px—, y esa segunda copia, con la
       misma especificidad y más tarde en el archivo, GANABA también dentro del
       media query: en un teléfono de 360px el panel medía 380px y desbordaba la
       página entera hacia la derecha. El 380 era el ancho real de escritorio, así
       que se conserva. Desde la mudanza de "Previsto" a una ventana flotante el
       valor vive en `--map-panel-w` (declarada en `.map-screen-body`), que es lo
       que esa ventana necesita para no taparle los botones de zoom al mapa: el
       ancho sigue estando en UN solo sitio. */
    width: var(--map-panel-w);
    flex-shrink: 0;
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    padding: var(--space-3);
    overflow-y: auto;
    /* Igual que el canvas: lo estira el cuerpo. El `min-height: 0` es lo que
       hace que la lista de riders scrollee ACÁ DENTRO en vez de estirar la
       columna. Apilado (≤900px) ya volvía a `auto` por su cuenta. */
    height: auto;
    min-height: 0;
}
.map-legend-title {
    font-size: 0.8rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.03em; color: var(--gray-500);
    margin-bottom: var(--space-2);
}
.map-legend-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; font-size: 0.88rem; }
.map-legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.map-legend-empty { color: var(--gray-400); font-size: 0.85rem; }
.map-legend-warn {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--gray-200);
    color: #b45309;
    font-size: 0.8rem;
}
/* Aviso de "tenés paradas sin ruta" en el panel del rider: va pegado a sus
   rutas (no al pie del panel), así que sin la línea de arriba ni el margen. */
.map-legend-loose {
    margin: 0.25rem 0 0; padding: 0.375rem 0.5rem; border-top: 0;
    background: color-mix(in srgb, #b45309 8%, transparent);
    border-radius: var(--radius-sm); line-height: 1.3;
}
@media (max-width: 900px) {
    /* Apilado, el panel deja de estar a la derecha del mapa: la ventana
       "Previsto" ya solo tiene que esquivar los botones de zoom. */
    .map-screen-body { flex-direction: column; --map-planwin-right: var(--map-zoom-clear); }
    /* Apilados, el mapa y el panel ya no comparten una línea: "lo que sobra"
       deja de tener sentido y cada uno vuelve a medir lo suyo. */
    .map-screen { height: auto; }
    .map-screen-canvas { height: 60vh; }
    .map-legend-panel { width: auto; height: auto; }
    .map-rider-block { --rider-color: var(--primary); }
}

/* ── Panel del dispatcher: bloques por rider ───────────────────────────────── */

/* ── Bloque de filtros del panel (encima de la lista de riders) ─────────────
   Chips, no una matriz de casillas: lo que está activo se ve encendido y se
   toca de una. Va pegado arriba (sticky) porque la lista de abajo puede ser
   larga y perder los filtros al hacer scroll es perder el contexto. */
/* Modo UNA RUTA del dispatcher (`?route=<id>`, "Ver en el mapa" del detalle de
   la ruta): ocupa el lugar del bloque de filtros, porque en ese modo NINGUNO se
   aplica. Finalizada va en verde y con el aviso de que es solo información: el
   panel de al lado tampoco dibuja sus acciones. */
.map-routeview {
    position: sticky; top: calc(var(--space-3) * -1); z-index: 3;
    display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem;
    background: var(--surface);
    padding: var(--space-2) 0;
    margin-bottom: var(--space-2);
    border-bottom: 1px solid var(--gray-200);
}
.map-routeview-title {
    display: flex; align-items: center; gap: 0.375rem;
    font-weight: 700; color: var(--gray-800);
}
.map-routeview-sub { font-size: 0.78rem; color: var(--gray-600); }
.map-routeview-tag {
    font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: #166534; background: #dcfce7;
    border-radius: 999px; padding: 1px 0.5rem;
}
.map-routeview.is-finished .map-routeview-title { color: #166534; }
/* Una ruta que solo se mira: el bloque se atenúa apenas para que se lea de un
   vistazo que ahí no hay nada que tocar (las acciones ni siquiera existen). */
.map-route.is-readonly > .map-route-head { opacity: 0.92; }
/* `[hidden]` no gana contra el display de la clase: sin esto, esconder la
   navegación por día en el modo UNA RUTA no la escondía. */
.map-daynav[hidden], .map-daycount[hidden] { display: none; }

.map-filters {
    position: sticky; top: calc(var(--space-3) * -1); z-index: 3;
    display: flex; flex-direction: column; gap: 0.375rem;
    background: var(--surface);
    padding: var(--space-2) 0;
    margin-bottom: var(--space-2);
    border-bottom: 1px solid var(--gray-200);
}
.map-filters-label {
    font-size: 0.64rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--gray-400); flex-shrink: 0;
}
.map-preset-row, .map-chip-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.25rem; }
/* Vistas rápidas de DOS EN DOS. Con flex-wrap las cuatro píldoras rompían por
   donde caía su ancho ("Recogidas pendientes" arrastraba a la de al lado) y el
   bloque se leía desordenado; la grilla fija las dos columnas, así que cada par
   comparte línea y ancho pase lo que pase con el texto. La etiqueta "Vistas
   rápidas" es su propia línea: dentro de la fila competía con las píldoras. */
.map-preset-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.25rem; }
.map-preset {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.7rem; font-weight: 600; line-height: 1.5;
    color: var(--gray-600); background: var(--gray-100);
    border: 1px solid transparent; border-radius: 999px;
    padding: 2px 0.5625rem; cursor: pointer;
}
/* En la grilla, la píldora ocupa su celda entera y el texto envuelve si hace
   falta: no se corta ni desborda la columna. */
.map-preset-grid .map-preset {
    justify-content: center; text-align: center; min-width: 0;
    padding: 0.1875rem 0.5rem; overflow-wrap: anywhere;
}
.map-preset:hover { color: var(--primary); border-color: var(--primary); }
/* Un preset ENCENDIDO no es una vista cerrada: solo dice "el estado coincide
   con esta combinación". En cuanto se toca un chip a mano, se apaga solo. */
.map-preset.is-on {
    color: #fff; background: var(--primary); border-color: var(--primary);
}
.map-filter-rider { display: flex; align-items: center; gap: 0.375rem; margin: 0; }
.map-filter-rider .form-select {
    font-size: 0.76rem; padding: 2px 22px 2px 8px;
    flex: 1; min-width: 0;   /* ocupa la fila: el nombre del rider se lee entero */
}
.map-chip {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.72rem; font-weight: 600; line-height: 1.5;
    color: var(--gray-600); background: var(--surface);
    border: 1px solid var(--gray-300); border-radius: 999px;
    padding: 2px 0.5625rem; cursor: pointer;
}
.map-chip:hover { border-color: var(--primary); color: var(--primary); }
.map-chip.is-on {
    color: var(--primary); border-color: var(--primary);
    background: color-mix(in srgb, var(--primary) 12%, var(--surface));
}
.map-chip-sep { width: 1px; align-self: stretch; background: var(--gray-200); margin: 0 2px; }

.map-panel-toolbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-2); margin-bottom: var(--space-2); flex-wrap: wrap;
}
.map-rider-block {
    border: 1px solid var(--gray-200);
    border-left: 4px solid var(--rider-color);
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--rider-color) 8%, var(--surface));
    margin-bottom: var(--space-2);
    overflow: hidden;
}
.map-rider-block.is-selected {
    box-shadow: 0 0 0 2px var(--rider-color);
    background: color-mix(in srgb, var(--rider-color) 16%, var(--surface));
}
.map-rider-head {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.5rem 0.625rem; cursor: pointer; margin: 0; user-select: none;
}
/* El nombre del rider LLEVA A SU PERFIL, y por eso es un `<a>` — pero no se
   pinta de azul: en una lista donde cada bloque ya está teñido con el color del
   rider, un enlace azul se leería como un estado más. Hereda el color y solo se
   subraya al pasar por encima, que es lo que dice "esto se puede tocar" sin
   competir con el color que identifica al rider. */
.map-rider-name { font-weight: 600; font-size: 0.9rem; flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: inherit; text-decoration: none; }
a.map-rider-name:hover, a.map-rider-name:focus-visible {
    color: inherit; text-decoration: underline;
}
.map-rider-count {
    font-size: 0.72rem; font-weight: 700;
    background: var(--rider-color); color: #fff;
    padding: 1px 0.4375rem; border-radius: 999px; flex-shrink: 0;
}
.map-rider-orders { padding: 0 0.5rem 0.5rem; display: flex; flex-direction: column; gap: 0.375rem; }
.map-order {
    background: var(--surface); border: 1px solid var(--gray-200);
    border-radius: var(--radius-sm); padding: 0.375rem 0.5rem; font-size: 0.8rem;
    cursor: pointer;
}
.map-order.is-flash { animation: mapOrderFlash 1.2s ease; }
@keyframes mapOrderFlash {
    0%, 40% { box-shadow: 0 0 0 2px var(--rider-color); }
    100% { box-shadow: none; }
}
.map-order-multi > summary { list-style: none; cursor: pointer; }
.map-order-multi > summary::-webkit-details-marker { display: none; }
.map-order-top { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.map-order-seq, .map-del-seq {
    font-size: 0.7rem; font-weight: 700; color: var(--gray-600);
    background: var(--gray-100); border-radius: 4px; padding: 0 0.3125rem; flex-shrink: 0;
}
.map-order-addr {
    color: var(--gray-500); font-size: 0.75rem; margin-top: 2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.map-order-addr .bi { color: var(--gray-400); }
.map-order-multi-hint { font-size: 0.7rem; color: var(--gray-400); display: block; margin-top: 0.1875rem; }
.map-order-deliveries { margin-top: 0.375rem; display: flex; flex-direction: column; gap: 0.25rem; }
.map-del { display: flex; align-items: center; gap: 0.375rem; font-size: 0.76rem; }
.map-del-addr { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-status {
    font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
    padding: 1px 0.375rem; border-radius: 4px; flex-shrink: 0;
    background: var(--gray-100); color: var(--gray-600);
}
.map-status-pendiente { background: #e5e7eb; color: #374151; }
.map-status-asignado { background: #dbeafe; color: #1e40af; }
.map-status-en_sitio { background: #fef3c7; color: #92400e; }
.map-status-recogido { background: #cffafe; color: #155e75; }
.map-status-en_camino { background: #ffedd5; color: #9a3412; }
.map-status-entregado { background: #d1fae5; color: #065f46; }
.map-status-fallida { background: #fee2e2; color: #991b1b; }
.map-status-traspasada { background: #ede9fe; color: #5b21b6; }
.map-order-fail {
    font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
    color: #991b1b; background: #fee2e2; padding: 1px 0.375rem; border-radius: 4px;
}

/* ── Panel del dispatcher: acordeón rider→rutas + reorden (Tanda 2) ────────── */
.map-panel-msg {
    font-size: 0.76rem; padding: 0.3125rem 0.5rem; border-radius: var(--radius-sm);
    margin-bottom: var(--space-2);
    background: #d1fae5; color: #065f46;
}
.map-panel-msg.is-error { background: #fee2e2; color: #991b1b; }
.map-rider-routes { padding: 0 0.5rem 0.5rem; display: flex; flex-direction: column; gap: 0.375rem; }
.map-route {
    background: var(--surface); border: 1px solid var(--gray-200);
    border-radius: var(--radius-sm); overflow: hidden;
}
.map-route-head {
    display: flex; align-items: center; gap: 0.375rem;
    padding: 0.3125rem 0.5rem; cursor: pointer; list-style: none; user-select: none;
    font-size: 0.78rem;
}
.map-route-head::-webkit-details-marker { display: none; }
.map-route-name { font-weight: 700; flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Cabecera de ruta a DOS líneas — SOLO el panel del dispatcher (`is-two-line`).
   Identidad arriba (etiqueta + NOMBRE + avance), contexto y acciones abajo
   (fecha + botones). Antes iba todo en una sola línea y el nombre —lo único por
   lo que se reconoce una ruta— competía con seis elementos y se cortaba.
   La clase es obligatoria: `.map-route-head` a secas la comparte la hoja del
   RIDER, y volver esa regla de dos líneas le desarma su cabecera. */
.map-route-head.is-two-line { flex-direction: column; align-items: stretch; gap: 0.1875rem; padding: 0.375rem 0.5rem; }
.map-route-l1 { display: flex; align-items: center; gap: 0.375rem; min-width: 0; }
.map-route-l2 { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.map-route-actions { display: flex; align-items: center; gap: 2px; margin-left: auto; }
/* El nombre se ve ENTERO: envuelve en vez de cortarse con puntos suspensivos.
   Dos rutas del mismo cliente se distinguen justo por el final del nombre. */
.map-route-head.is-two-line .map-route-name {
    overflow: visible; white-space: normal;
    overflow-wrap: anywhere; line-height: 1.25;
}
.map-route-count {
    font-size: 0.68rem; font-weight: 700; color: var(--gray-600);
    background: var(--gray-100); border-radius: 999px; padding: 1px 0.375rem; flex-shrink: 0;
}
/* Cola de RUTAS del rider (cuál va primero). El asa vive en la cabecera del
   <details> y sigue siendo un asa —a diferencia de las PARADAS, donde arrastra
   la tarjeta entera—: acá el gesto tendría que salir del <summary>, que ya
   despliega la ruta. `touch-action: none` + área de toque agrandada, porque en
   el panel del rider esto se arrastra con el pulgar dentro de un contenedor con
   scroll propio. */
.map-route-queue { display: flex; flex-direction: column; gap: 0.375rem; }
.map-route-queue > details.map-route.is-dragging { opacity: 0.5; }
.map-route-drag {
    color: var(--gray-400); cursor: grab; flex-shrink: 0;
    font-size: 0.8rem; line-height: 1;
    touch-action: none;
    padding: 10px 6px; margin: -10px -2px -10px -6px;
    -webkit-tap-highlight-color: transparent;
}
.map-route-drag:active { cursor: grabbing; }
/* Punto que marca una ruta con trabajo ya empezado: no puede quedar por debajo
   de una intacta (el backend lo rechaza con 422). */
.map-route-started { color: var(--warning, #F59E0B); flex-shrink: 0; font-size: 0.9rem; }
.map-route-restore {
    border: none; background: transparent; color: var(--gray-500);
    cursor: pointer; padding: 2px 0.25rem; font-size: 0.8rem; line-height: 1; flex-shrink: 0;
}
.map-route-restore:hover { color: var(--primary); }
/* Optimizar el orden. Mismo peso visual que restaurar: son las dos caras de la
   misma decisión (el orden lo propone el algoritmo o el orden de carga), y
   ninguna de las dos manda sobre el drag, que es lo que decide de verdad. */
.map-route-optimize {
    border: none; background: transparent; color: var(--gray-500);
    cursor: pointer; padding: 2px 0.25rem; font-size: 0.8rem; line-height: 1; flex-shrink: 0;
}
.map-route-optimize:hover { color: var(--primary); }
/* Círculo de activación del TRAZADO de una ruta (solo el mapa general del
   rider): encendido = la línea desde mi posición va hasta la parada que toca de
   ESTA ruta. Es excluyente entre rutas y se apaga con el ⌖, nunca con un segundo
   toque. Área táctil grande (padding negativo) sobre un punto chico.

   VA SCOPEADO a `.rider-sheet` a propósito: el dispatcher tiene su propio
   `.map-route-trace` (más abajo en este archivo) que es un BOTÓN CON ICONO —
   `background: none; border: none` y el dibujo lo pone un `<i>`. Los dos
   nombres eran iguales y sin ámbito, así que la regla del dispatcher, por venir
   después, le borraba el borde y el fondo a la del rider… cuyo botón está
   VACÍO (el círculo ES el borde). Resultado: un botón invisible de 0 px que
   seguía siendo clickeable. Con el ámbito, cada uno gana en su panel sin
   depender del orden del archivo. */
.rider-sheet .map-route-trace {
    position: relative; flex-shrink: 0;
    border: 2px solid var(--gray-400); background: transparent;
    width: 16px; height: 16px; border-radius: 999px; padding: 0; cursor: pointer;
    box-sizing: border-box; -webkit-tap-highlight-color: transparent;
    outline-offset: 2px; transition: background-color .15s, border-color .15s;
}
.rider-sheet .map-route-trace::before {   /* zona táctil, sin agrandar el punto */
    content: ''; position: absolute; top: -13px; left: -13px;
    width: 40px; height: 40px;
}
.rider-sheet .map-route-trace.is-on {
    border-color: #1D4ED8;                                 /* el azul del trazado */
    background: #1D4ED8;
    box-shadow: 0 0 0 3px color-mix(in srgb, #1D4ED8 22%, transparent);
}
.map-route-stops { list-style: none; margin: 0; padding: 2px 0.25rem 0.25rem; display: flex; flex-direction: column; gap: 0.1875rem; }
.map-stop-row {
    display: flex; align-items: flex-start; gap: 0.3125rem;
    padding: 0.25rem 0.3125rem; border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--rider-color) 6%, var(--surface));
    border: 1px solid var(--gray-200); font-size: 0.76rem; cursor: pointer;
}
/* Arrastrable = fila pendiente de una lista reordenable. El `draggable` nativo
   ya no se emite en el marcado (lo pone y lo saca SortableJS al vuelo), así que
   el cursor cuelga de la lista, no del atributo. `user-select`/`touch-callout`
   son para el DEDO: en iOS mantener pulsado sobre texto abre la selección y la
   lupa, que es justo lo que se comía el gesto de reordenar. */
ol[data-reorderable="1"] > .map-stop-row:not(.is-frozen) {
    cursor: grab;
    -webkit-user-select: none; user-select: none;
    -webkit-touch-callout: none;
}
.map-stop-row.is-dragging { opacity: 0.5; cursor: grabbing; }
.map-stop-row.is-frozen { opacity: 0.6; background: var(--gray-100); }
/* Parada FALLIDA: se resalta, no se atenúa. Está cerrada, pero es trabajo que
   quedó sin hacer y reclama una decisión del dispatcher — por eso tampoco la
   esconde el chip "ocultar hechas". Va después de .is-frozen para ganarle la
   opacidad, que la dejaba más apagada que una entregada. */
.map-stop-row.is-fallida.is-frozen, .map-stop-row.is-fallida {
    opacity: 1;
    background: color-mix(in srgb, #dc2626 8%, var(--surface));
    border-color: color-mix(in srgb, #dc2626 35%, var(--gray-200));
}
.map-stop-row.is-fallida .map-stop-seq { color: #b91c1c; font-weight: 700; }
/* El asa ⠿ de una parada ya NO existe: arrastra la TARJETA ENTERA, en las tres
   superficies (panel del mapa, detalle de la ruta y lista del rider). El glifo
   medía unos 10px pegados al borde izquierdo —el peor blanco de la lista para el
   pulgar— y no ofrecía ningún gesto que la tarjeta no ofreciera ya. Quien
   separa el arrastre del scroll es el TIEMPO: en el rider, el `delay` de
   SortableJS; en el escritorio, el drag nativo del <li>. */
.map-stop-seq {
    font-size: 0.68rem; font-weight: 700; color: #fff;
    background: var(--rider-color); border-radius: 4px; padding: 0 0.3125rem; flex-shrink: 0;
    min-width: 1.125rem; text-align: center; margin-top: 1px;
}
/* Botón "quitar pedido de la ruta" en la fila de recogida */
.map-stop-del {
    flex-shrink: 0; background: none; border: none; padding: 0;
    width: 1.15rem; text-align: center;
    color: var(--gray-400); cursor: pointer; font-size: 0.9rem; line-height: 1;
}
.map-stop-del:hover { color: #dc2626; }
/* ELIMINAR, que no es lo mismo que QUITAR (el ✕ de arriba, reversible: el pedido
   cae al pozo). Se distinguen por FORMA y por COLOR —aro fino neutro contra
   papelera roja— porque dos iconos parecidos a un clic de distancia es un borrado
   por accidente. En escritorio aparece con el hover/foco de la fila: una lista
   larga no puede llevar un icono destructivo por renglón gritando. */
.map-stop-trash {
    flex-shrink: 0; background: none; border: none; padding: 0;
    width: 1.15rem; text-align: center;
    color: #f87171; cursor: pointer; font-size: 0.85rem; line-height: 1;
    opacity: 0; transition: opacity 0.12s ease;
}
.map-stop-row:hover .map-stop-trash,
.map-stop-row:focus-within .map-stop-trash,
.map-stop-trash:focus-visible { opacity: 1; }
.map-stop-trash:hover { color: #dc2626; }
/* Fila de parada en 3 líneas: (1) nº de pedido + estado + quitar, (2) dirección
   completa, (3) CP + ícono de ir al detalle del pedido. */
.map-stop-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.map-stop-l1 { display: flex; align-items: center; gap: 0.375rem; }
.map-stop-order { flex-shrink: 0; }
/* Extremo derecho de la primera línea: estado + acciones, SIEMPRE en la misma
   columna. Lo que se mira en vertical es el estado de todo el recorrido, así que
   no puede correrse porque una parada tenga ventana horaria y la de al lado no.
   Las ranuras vacías reservan el sitio del ✕ (que solo llevan las recogidas) y
   el de la papelera: sin ellas la entrega mandaba su estado contra el borde. */
.map-stop-l1-end {
    margin-left: auto; flex: 0 0 auto;
    display: flex; align-items: center; gap: 0.25rem;
}
.map-stop-slot { width: 1.15rem; flex: 0 0 auto; }
.map-stop-l3 { display: flex; align-items: center; gap: 0.3125rem; }
.map-stop-cp { color: var(--gray-500); font-size: 0.72rem; letter-spacing: 0.02em; }
/* Franja horaria comprometida de la parada (winChip, en las dos vistas). En el
   panel del DISPATCHER va pegada al número de pedido —el sitio que dejó libre el
   estado, que se mudó al extremo derecho con las acciones—; en la lista del
   RIDER, que no tiene ni estado ni acciones en la fila, se sigue empujando al
   final con `auto` (ver más abajo, en el bloque .rider-sheet) para que todas las
   ventanas del recorrido caigan en la misma columna. */
.map-stop-win {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: 0.1875rem;
    padding: 1px 0.4375rem; border-radius: 20px;
    background: #eef2ff; color: #3730a3; border: 1px solid #c7d2fe;
    font-size: 0.68rem; font-weight: 800; white-space: nowrap;
}
.map-stop-win i { color: #6366f1; font-size: 0.92em; }
/* Las acciones de la tercera línea son UN grupo pegado al borde derecho, y por
   eso viven en su propio contenedor: con `margin-left: auto` repartido botón a
   botón, el segundo ➕ (el de agregar otra RECOGIDA) abría un segundo hueco y
   dejaba el lápiz y el primer ➕ colgando en el medio de la fila. El enlace
   suelto —la lista de paradas del detalle de la RUTA, que emite el mismo
   marcado sin acciones— conserva su `auto`. */
.map-stop-l3-end {
    margin-left: auto; flex: 0 0 auto;
    display: flex; align-items: center; gap: 0.3125rem;
}
.map-stop-l3 > .map-stop-open { margin-left: auto; }
/* Direcciones completas: envuelven en varias líneas en vez de cortarse. */
.map-stop-addr { min-width: 0; overflow-wrap: anywhere; line-height: 1.3; }
.map-stop-nocoord { color: #b45309; }
.mess-pop-rt {
    font-size: 0.68rem; font-weight: 700; color: var(--gray-500);
    background: var(--gray-100); border-radius: 4px; padding: 0 4px;
}

/* Swatch del patrón de guiones de cada ruta (correlación panel↔mapa, Tanda 4b) */
.map-route-dash {
    width: 22px; height: 3px; flex-shrink: 0; border-radius: 2px;
    background-image: repeating-linear-gradient(90deg, var(--rider-color) 0 4px, transparent 4px 6px);
}
.map-dash-0 { background-image: repeating-linear-gradient(90deg, var(--rider-color) 0 2px, transparent 2px 4px); }
.map-dash-1 { background-image: repeating-linear-gradient(90deg, var(--rider-color) 0 4px, transparent 4px 6px); }
.map-dash-2 { background-image: repeating-linear-gradient(90deg, var(--rider-color) 0 1px, transparent 1px 3px); }
.map-dash-3 { background-image: repeating-linear-gradient(90deg, var(--rider-color) 0 6px, transparent 6px 8px); }
/* Etiqueta de ruta (letra A/B/…) cuando el rider tiene más de una: correlaciona
   el prefijo del número del marcador ("A5") con su ruta en el panel. */
.map-route-tag {
    flex-shrink: 0; font-size: 0.62rem; font-weight: 700; line-height: 1.4;
    color: #fff; background: var(--rider-color); border-radius: 4px;
    padding: 0 0.3125rem; min-width: 1rem; text-align: center;
}
/* Botón "Añadir pedido" en la cabecera de cada ruta */
.map-route-addorder {
    flex-shrink: 0; background: none; border: none; padding: 0 2px;
    color: var(--gray-400); cursor: pointer; font-size: 0.85rem; line-height: 1;
}
.map-route-addorder:hover { color: var(--primary, #4A90E2); }
/* Botón "Eliminar ruta" en la cabecera de cada ruta (mismo estilo que el "+") */
.map-route-delete {
    flex-shrink: 0; background: none; border: none; padding: 0 2px;
    color: var(--gray-400); cursor: pointer; font-size: 0.85rem; line-height: 1;
}
.map-route-delete:hover { color: #dc2626; }
/* "Finalizar ruta". En el DISPATCHER es un icono más de la fila de acciones
   (está en toda ruta, siempre). En el RIDER es un botón con texto —"Terminé"—
   y solo aparece cuando ya no le queda nada por hacer: ahí no es una acción
   más entre otras, es el gesto que cierra el recorrido, y con guantes y en
   movimiento un icono suelto de 0,85rem no se acierta. */
.map-route-finish {
    flex-shrink: 0; background: none; border: none; padding: 0 2px;
    color: var(--gray-400); cursor: pointer; font-size: 0.85rem; line-height: 1;
}
.map-route-finish:hover { color: var(--success, #16a34a); }
.rider-sheet .map-route-finish {
    display: inline-flex; align-items: center; gap: 0.25rem;
    padding: 0.25rem 0.625rem; border-radius: 999px;
    background: var(--success, #16a34a); color: #fff;
    font-size: 0.72rem; font-weight: 700;
}
.rider-sheet .map-route-finish:hover { color: #fff; filter: brightness(1.06); }
/* Fecha de servicio de la ruta (pedidos programados): chip en la cabecera del
   bloque. Solo aparece si la ruta tiene fecha — sin fecha es "hoy", que es el
   caso normal y no necesita decirse. En ámbar cuando es FUTURA: esa ruta
   todavía no está en el mapa del rider, y esa es la información que el
   dispatcher necesita ver de un vistazo para no buscar un bug que no existe. */
.map-route-date {
    flex-shrink: 0; font-size: 0.62rem; font-weight: 700; line-height: 1.4;
    border-radius: 4px; padding: 0 0.3125rem; white-space: nowrap;
    color: var(--gray-600); background: var(--gray-200);
}
.map-route-date.is-future { color: #92400e; background: #fde68a; }
/* Botón "Nombre de la ruta" (mismo estilo que el "+" y la papelera). El nombre
   propio no lleva chip aparte: ya se ve en `.map-route-name`, que es el título
   del bloque. */
.map-route-namebtn {
    flex-shrink: 0; background: none; border: none; padding: 0 2px;
    color: var(--gray-400); cursor: pointer; font-size: 0.85rem; line-height: 1;
}
.map-route-namebtn:hover { color: var(--primary, #4A90E2); }
/* Botón "Fecha de la ruta" (mismo estilo que el "+" y la papelera) */
.map-route-datebtn {
    flex-shrink: 0; background: none; border: none; padding: 0 2px;
    color: var(--gray-400); cursor: pointer; font-size: 0.85rem; line-height: 1;
}
.map-route-datebtn:hover { color: var(--primary, #4A90E2); }
/* Botón de RUTA ACTIVA del DISPATCHER: "mirar SOLO el trazado de esta ruta".
   Mismo estilo que los demás botones de la cabecera; encendido se tiñe del color
   del rider (el del bloque) porque lo que enciende es una línea de ESE color en
   el mapa. Acá el dibujo lo pone un `<i>`, así que va sin borde ni fondo.
   OJO: el rider usa la MISMA clase para un botón VACÍO cuyo círculo es el borde
   — su regla está arriba, scopeada a `.rider-sheet`. Quitarle ese ámbito (o
   dárselo a esta) es lo que dejaba el círculo del rider invisible. */
.map-route-trace {
    flex-shrink: 0; background: none; border: none; padding: 0 2px;
    color: var(--gray-400); cursor: pointer; font-size: 0.85rem; line-height: 1;
}
.map-route-trace:hover { color: var(--primary, #4A90E2); }
.map-route-trace.is-on { color: var(--rider-color, var(--primary, #4A90E2)); }
/* La ruta que se está mirando queda marcada en el panel: con varias rutas a la
   vista, el botón encendido solo no alcanza para saber cuál es la del trazo. */
.map-route.is-tracing {
    border-color: var(--rider-color, var(--primary, #4A90E2));
    box-shadow: inset 2px 0 0 var(--rider-color, var(--primary, #4A90E2));
}
/* Bloque "Sin ruta": paradas de pedidos activos aún no agrupadas en una ruta */
.map-route-loose { border-style: dashed; }
.map-route-loose .map-stop-row { cursor: default; }
.map-legend-sub { font-size: 0.72rem; padding: 0.25rem 0.5rem; }
/* Bloque "Previsto para este día": lo que las plantillas recurrentes prevén y
   todavía NO existe como pedido (Fase 1 de docs/archivo/PLAN_MAPA.md). Borde punteado,
   igual que el contorno de sus pines en el mapa: es la misma idea dicha en las
   dos superficies — todavía no hay nada acá. La lista tiene tope de ALTO con
   scroll propio (nunca recorte de filas): crece con las plantillas activas. */
.map-plan-block { border-style: dashed; }
.map-plan-list {
    list-style: none; margin: 0; padding: 0 0.5rem 0.5rem;
    display: flex; flex-direction: column; gap: 0.25rem;
    max-height: 220px; overflow-y: auto;
}
.map-plan-row {
    display: flex; flex-direction: column; gap: 1px;
    font-size: 0.78rem; padding: 0.25rem 0.375rem;
    border: 1px dashed var(--gray-300); border-radius: var(--radius-sm);
    opacity: 0.75;
}
.map-plan-row.is-previsto { opacity: 1; }
.map-plan-name { font-weight: 600; overflow-wrap: anywhere; }
.map-plan-sub { font-size: 0.7rem; color: var(--gray-500); overflow-wrap: anywhere; }
/* Acciones de la ocurrencia (Fase 3): van en su propio renglón y alineadas al
   final — la fila es una lista de nombres, no una barra de botones, y meterlas
   al lado del nombre lo recortaría justo en la plantilla de nombre largo. */
.map-plan-acts { display: flex; justify-content: flex-end; }
.map-plan-act { font-size: 0.7rem; padding: 0; text-decoration: none; }
.map-plan-gen { flex-shrink: 0; font-size: 0.68rem; padding: 1px 0.375rem; }
.map-route-create { flex-shrink: 0; font-size: 0.68rem; padding: 1px 0.375rem; }
/* Modal "Crear ruta": lista de pedidos sin ruta con checkboxes */
.map-create-orders { display: flex; flex-direction: column; gap: 0.25rem; max-height: 240px; overflow-y: auto; margin-bottom: var(--space-2); }
.map-create-order {
    display: flex; align-items: flex-start; gap: 0.375rem; font-size: 0.78rem;
    padding: 0.25rem 0.375rem; border: 1px solid var(--gray-200); border-radius: var(--radius-sm); cursor: pointer;
}
.map-create-order span { overflow-wrap: anywhere; }
/* El buscador de plantillas (modal "Nuevo pedido") esconde filas con [hidden]:
   `display: flex` le gana al atributo si nadie lo dice, y la fila filtrada
   seguiría a la vista. */
.map-create-order[hidden] { display: none; }
/* Título de cada grupo de la lista (los del rider / los sin asignar) */
.map-create-group {
    margin: 0.375rem 0 2px; font-size: 0.7rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .04em; color: var(--gray-500);
}
.map-create-group:first-child { margin-top: 0; }
/* "Nueva ruta" del bloque de un rider: crear OTRA ruta sin depender de que le
   queden pedidos sueltos (el modal ya ofrece también los sin asignar). */
.map-rider-newroute { align-self: flex-start; font-size: 0.68rem; padding: 1px 0.375rem; margin: 2px 0 0.25rem; }

/* ── Los dos extremos del panel: el pozo ("Sin rutear", arriba) y las rutas sin
   rider ("Por asignar", al final). Eran un solo bloque "Sin asignar", y son dos
   preguntas distintas: nadie lo tiene todavía vs ya está compuesto y le falta
   quién. Los dos van con el borde punteado de "esto no es de nadie". ─────────── */
.map-pool-block, .map-unassigned-block { border-left-style: dashed; }
.map-route-assign { flex-shrink: 0; font-size: 0.68rem; padding: 1px 0.375rem; }

/* ── Barra flotante del modo "crear ruta" (selección de recogidas) ─────────── */
.map-screen-body { position: relative; }
.map-select-bar {
    position: absolute; left: 50%; bottom: var(--space-3); transform: translateX(-50%);
    z-index: 20; display: flex; align-items: center; gap: var(--space-3);
    flex-wrap: wrap; max-width: min(680px, calc(100% - 360px));
    padding: 0.625rem 0.875rem; border-radius: var(--radius-lg);
    background: #1f2937; color: #f9fafb;
    border: 1px solid rgba(255, 255, 255, 0.12);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}
.map-select-bar-info { font-size: 0.82rem; }
.map-select-bar-info b { color: #fff; }
.map-select-bar-rider { display: flex; align-items: center; gap: 0.375rem; font-size: 0.76rem; margin: 0; }
.map-select-bar-rider select { min-width: 9.375rem; }
.map-select-bar-actions { display: flex; gap: 0.375rem; margin-left: auto; }
.map-select-bar-msg { flex-basis: 100%; font-size: 0.74rem; color: #fca5a5; }
.map-select-bar-msg.is-error { color: #fca5a5; }

/* Nombre y día de la ruta ANTES de crearla. Plegado por defecto: abierto sería
   pedirle dos datos opcionales a quien solo quiere agrupar cuatro recogidas. */
.map-select-bar-opts { flex-basis: 100%; font-size: 0.76rem; }
.map-select-bar-opts > summary {
    cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 0.375rem;
    padding: 2px 0.5rem; border-radius: var(--radius-sm);
    color: #e5e7eb; background: rgba(255, 255, 255, 0.08);
}
.map-select-bar-opts > summary::-webkit-details-marker { display: none; }
.map-select-bar-opts > summary:hover { background: rgba(255, 255, 255, 0.16); }
.map-select-bar-opts-body { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: 0.375rem; }
.map-select-bar-opts-body label { display: flex; flex-direction: column; gap: 2px; }
.map-select-bar-opts-body span { color: #cbd5e1; }
.map-select-bar-opts-body .form-control { min-width: 10rem; }

/* Resumen por PEDIDO ("recogida → N entregas"). Crece con la selección, así que
   el tope es de ALTO y con scroll propio: recortar filas escondería justo lo que
   hay que confirmar antes de crear la ruta. */
.map-select-bar-list {
    flex-basis: 100%; max-height: 132px; overflow-y: auto;
    display: flex; flex-direction: column; gap: 2px;
    font-size: 0.74rem; color: #e5e7eb;
    padding: 0.375rem 0.5rem; border-radius: var(--radius-sm);
    background: rgba(0, 0, 0, 0.25);
}
.map-select-bar-row { display: flex; align-items: baseline; gap: 0.375rem; }
.map-select-bar-row b { color: #fff; flex: none; }
.map-select-bar-from { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.map-select-bar-to { flex: none; color: #cbd5e1; }
/* El «+N» de un pedido que se carga en varias tiendas: va pegado a la dirección
   de la PRIMERA, con la tinta de la RECOGIDA —el código de color es una sola
   fuente— y es el mismo gesto que en los listados. */
.map-select-bar-n { flex: none; color: var(--stop-pickup); font-size: 0.68rem; }
/* Un conjunto puede venir MEZCLADO: lo que se clickeó en el mapa y lo que entró
   ya seleccionado (recién generado desde su plantilla). La etiqueta dice cuál es
   cuál — sin ella, tres pedidos marcados que nadie tocó se leen como elegidos. */
.map-select-bar-new {
    flex: none; font-size: 0.62rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: #052e16; background: #86efac;
    border-radius: 999px; padding: 0 0.375rem; cursor: help;
}
@media (max-width: 900px) {
    .map-select-bar { max-width: calc(100% - 24px); position: fixed; }
}

/* ── Tarjeta de detalle de la parada (esquina inferior IZQUIERDA) ───────────
   Sustituye al popup que se abría sobre el marcador. Sigue el chrome oscuro del
   dispatcher (los mismos grises que la barra de "crear ruta", escritos a mano y
   no heredados: esta tarjeta va POR ENCIMA del canvas del mapa, que es claro u
   oscuro según el basemap, y no puede depender de él).

   El indicativo de qué clase de parada es NO es solo un texto: la tarjeta lleva
   una franja del color del TIPO —el mismo par verde/ámbar/morado del pin y de la
   fila del panel, más el rojo de la fallida—, de modo que "qué estoy mirando" se
   contesta antes de leer una palabra. Es la misma correlación que hace que el
   mapa se pueda leer de un vistazo; romperla acá obligaría a leer siempre. */
.map-stopcard {
    position: absolute; left: var(--space-3); bottom: var(--space-3); z-index: 25;
    width: min(340px, calc(100% - 2 * var(--space-3)));
    max-height: min(62%, 520px); overflow-y: auto;
    display: flex; flex-direction: column;
    border-radius: var(--radius-lg);
    background: #1f2937; color: #e5e7eb;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-left: 4px solid var(--msc-accent, #64748b);
    box-shadow: 0 10px 32px rgba(0, 0, 0, 0.45);
    font-size: 0.86rem;
}
.map-stopcard.is-recogida { --msc-accent: #22C55E; }
.map-stopcard.is-entrega  { --msc-accent: #F59E0B; }
.map-stopcard.is-traspaso { --msc-accent: #7C3AED; }
.map-stopcard.is-fallida  { --msc-accent: #DC2626; }
.map-stopcard.is-grupo    { --msc-accent: #64748B; }

.msc-head {
    display: flex; align-items: center; gap: 0.4375rem; flex-wrap: wrap;
    padding: 0.5625rem 0.625rem; border-bottom: 1px solid rgba(255, 255, 255, 0.10);
    background: color-mix(in srgb, var(--msc-accent) 16%, transparent);
}
.msc-kind {
    font-size: 0.7rem; font-weight: 800; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--msc-accent);
}
/* Número de recorrido de ESTA parada: el mismo que lleva su pin en el mapa y su
   fila en el panel — es como el dispatcher y el rider nombran una parada. */
.msc-seq {
    min-width: 1.625rem; height: 22px; padding: 0 0.375rem;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 7px; font-size: 0.78rem; font-weight: 800; line-height: 1;
    background: var(--msc-accent); color: #0b1220;
}
.map-stopcard.is-traspaso .msc-seq, .map-stopcard.is-fallida .msc-seq { color: #fff; }
.msc-order { font-size: 0.78rem; font-weight: 700; color: #cbd5e1; }
.msc-open, .msc-close, .msc-back {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: 7px; flex-shrink: 0;
    border: 0; background: rgba(255, 255, 255, 0.07); color: #cbd5e1;
    cursor: pointer; text-decoration: none;
}
.msc-open:hover, .msc-close:hover, .msc-back:hover { background: rgba(255, 255, 255, 0.16); color: #fff; }
.msc-open { margin-left: auto; }
/* El icono de chat de la tarjeta: «hablar de este pedido». Va antes del de abrir
   la ficha, así que es él quien empuja el grupo a la derecha. */
.msc-chat {
    margin-left: auto; border: 0; background: transparent; color: inherit; opacity: 0.85;
    width: 28px; height: 28px; border-radius: 50%; cursor: pointer; flex-shrink: 0;
}
.msc-chat:hover { background: rgba(255, 255, 255, 0.16); opacity: 1; }
.msc-chat + .msc-open { margin-left: 0; }
.msc-back { order: -1; }

.msc-body { padding: 0.625rem; display: flex; flex-direction: column; gap: 0.5rem; }
.msc-addr { font-size: 0.95rem; font-weight: 600; color: #f8fafc; line-height: 1.35; }
.msc-cp { font-weight: 600; color: #94a3b8; }
/* El detalle de unidad y la empresa son el segundo renglón de la dirección: se
   dibujan solo cuando existen (ver addrBlock/empresaRow en map-dispatcher.js),
   así que no reservan espacio ni dejan un hueco cuando faltan. */
.msc-unit,
.msc-empresa { display: flex; align-items: center; gap: 0.375rem; font-size: 0.8rem; }
.msc-unit { color: #cbd5e1; }
.msc-empresa { color: #e2e8f0; font-weight: 600; }
.msc-unit i, .msc-empresa i { color: #94a3b8; font-size: 0.85em; }
/* Franja horaria comprometida (windowRow en map-dispatcher.js): va pegada a la
   dirección porque dónde y cuándo son la misma pregunta al armar un recorrido.
   Superficie propia sobre el fondo oscuro de la tarjeta para que se encuentre
   sin leer, sin llegar al peso de la dirección. */
.msc-win {
    align-self: flex-start;
    display: inline-flex; align-items: center; gap: 0.3125rem;
    padding: 2px 0.5625rem; border-radius: 20px;
    background: rgba(99, 102, 241, 0.18); color: #c7d2fe;
    border: 1px solid rgba(129, 140, 248, 0.45);
    font-size: 0.78rem; font-weight: 700; white-space: nowrap;
}
.msc-win i { color: #a5b4fc; font-size: 0.9em; }
.msc-meta { display: flex; flex-wrap: wrap; gap: 0.625rem; font-size: 0.78rem; color: #cbd5e1; }
.msc-route, .msc-rider { display: inline-flex; align-items: center; gap: 0.3125rem; }
.msc-route.is-none, .msc-rider.is-none { color: #94a3b8; font-style: italic; }
.msc-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.msc-hint { font-size: 0.78rem; color: #94a3b8; }

/* Avisos de la tarjeta (traspaso / fallida / sin ubicar): una línea, con el tono
   de lo que informan. */
.msc-note {
    display: flex; gap: 0.4375rem; align-items: flex-start;
    font-size: 0.78rem; line-height: 1.35; border-radius: var(--radius-sm);
    padding: 0.375rem 0.5rem;
}
.msc-note.is-traspaso { background: rgba(124, 58, 237, 0.18); color: #ddd6fe; }
.msc-note.is-fallida { background: rgba(220, 38, 38, 0.18); color: #fecaca; }
.msc-note.is-warn { background: rgba(245, 158, 11, 0.16); color: #fde68a; }
/* Recorrido ya cerrado: la tarjeta es información y no lleva acciones. Va en
   verde (el mismo tono con que el historial dice "finalizada") y no en ámbar:
   no es un aviso de que algo esté mal, es el estado del recorrido. */
.msc-note.is-finished { background: rgba(22, 163, 74, 0.16); color: #bbf7d0; }

/* Bloque "dónde está la otra mitad" del pedido. */
.msc-block { border-top: 1px solid rgba(255, 255, 255, 0.10); padding-top: 0.5rem; }
.msc-block-t {
    font-size: 0.68rem; font-weight: 800; letter-spacing: 0.05em;
    text-transform: uppercase; color: #94a3b8;
}
.msc-empty { color: #64748b; }
.msc-more > summary {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    cursor: pointer; list-style: none;
}
.msc-more > summary::-webkit-details-marker { display: none; }
/* El chevron es un <i> real y no un `content` con el codepoint del icono: el
   codepoint depende de la versión de bootstrap-icons del CDN y un cambio menor
   lo convertiría en un cuadrito. */
.msc-caret { margin-left: auto; font-size: 0.8rem; color: #94a3b8; transition: transform 0.15s; }
.msc-more[open] > summary .msc-caret { transform: rotate(180deg); }
.msc-sibs { list-style: none; margin: 0.375rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.msc-sib {
    display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
    padding: 0.25rem 0.375rem; border-radius: var(--radius-sm);
}
.msc-sib:hover { background: rgba(255, 255, 255, 0.08); }
.msc-sib-addr { color: #e2e8f0; line-height: 1.3; }

/* Punto con 3 o más paradas: la lista para elegir cuál mirar (el pin "xN"). */
.msc-picks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3125rem; }
.msc-pick {
    display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
    padding: 0.375rem 0.4375rem; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.05);
    border-left: 3px solid var(--msc-pick-accent, #64748b);
}
.msc-pick:hover { background: rgba(255, 255, 255, 0.12); }
.msc-pick.is-recogida { --msc-pick-accent: #22C55E; }
.msc-pick.is-entrega  { --msc-pick-accent: #F59E0B; }
.msc-pick.is-traspaso { --msc-pick-accent: #7C3AED; }
.msc-pick.is-fallida  { --msc-pick-accent: #DC2626; }
.msc-pick .msc-kind { color: var(--msc-pick-accent); }
.msc-pick-body { min-width: 0; flex: 1; }
.msc-pick-l1 { display: flex; align-items: center; gap: 0.4375rem; flex-wrap: wrap; }
.msc-pick-rider { font-size: 0.72rem; color: #94a3b8; }
.msc-pick-addr { color: #e2e8f0; line-height: 1.3; overflow-wrap: anywhere; }
.msc-go { color: #64748b; flex-shrink: 0; }

.msc-action {
    display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem;
    border: 1px solid rgba(255, 255, 255, 0.18); background: transparent;
    color: #e5e7eb; border-radius: var(--radius-sm); padding: 0.375rem 0.625rem;
    font-size: 0.8rem; cursor: pointer;
}
.msc-action:hover { background: var(--primary); border-color: var(--primary); color: #fff; }
/* "Quitar la ubicación" acompaña a "corregir" pero es la acción secundaria: se
   apaga para que las dos no compitan por el mismo clic. */
.msc-action.is-quiet { color: #94a3b8; border-color: rgba(255, 255, 255, 0.1); }
.msc-action.is-quiet:hover { background: rgba(248, 113, 113, 0.16); border-color: rgba(248, 113, 113, 0.5); color: #fecaca; }
/* "Deshacer el traspaso" BORRA el punto de encuentro de dos rutas: se lee en
   rojo desde el principio, no solo al pasar por encima. */
.msc-action.is-danger { color: #fca5a5; border-color: rgba(248, 113, 113, 0.45); }
.msc-action.is-danger:hover { background: rgba(220, 38, 38, 0.85); border-color: rgba(220, 38, 38, 0.9); color: #fff; }

/* ELIMINAR (tanda 9). NO es un quinto botón sólido: la tarjeta ya ofrece hasta
   cuatro acciones a ancho completo, y un rojo lleno al final las gana todas. Es
   una LÍNEA discreta, separada por un filete, que se llena solo al pasar por
   encima: se ve sin discusión y no compite con lo que se usa a diario. */
.msc-danger {
    display: inline-flex; align-items: center; gap: 0.375rem;
    background: none; border: none; padding: 0.375rem 0 0;
    margin-top: 0.25rem; border-top: 1px solid rgba(255, 255, 255, 0.08);
    width: 100%; color: #f87171; font-size: 0.78rem; cursor: pointer;
}
.msc-danger:hover { color: #fecaca; }
/* El aviso previo vive DENTRO de la tarjeta: lo que se está por borrar es lo que
   la tarjeta muestra. Mismo marcado de lista que el de generar (plan-confirm-*),
   con el marco en rojo porque acá la salida por defecto es no seguir. */
.msc-confirm {
    margin-top: 0.5rem; padding: 0.5rem; border-radius: var(--radius-sm);
    border: 1px solid rgba(248, 113, 113, 0.45);
    background: rgba(220, 38, 38, 0.12);
}
.msc-confirm ul { list-style: none; margin: 0.375rem 0 0.5rem; padding: 0; display: grid; gap: 0.25rem; }
.msc-confirm .plan-confirm-item { color: #fecaca; }
.msc-confirm .plan-confirm-head { color: #fca5a5; margin: 0; font-size: 0.78rem; }

/* El lápiz y los dos ➕ viven ABAJO, en su propia fila alineada a la derecha
   (`.msc-tools`), y no en la cabecera: **arriba queda solo lo que se hace con la
   TARJETA** —abrir la ficha y cerrarla—, y editar o agregar una parada es algo
   que se le hace a la PARADA. Con los cinco iconos arriba, dos familias
   distintas compartían fila y la ✕ de cerrar quedaba a un pixel de un alta.
   El cuerpo es el mismo de "ver el pedido" y la ✕: siguen siendo los mismos
   botones, lo que cambió es en qué fila caen. */
.msc-tools {
    display: flex; align-items: center; justify-content: flex-end;
    gap: 0.375rem; flex-wrap: wrap;
}
.msc-edit, .msc-add {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: 7px; flex-shrink: 0;
    border: 0; background: rgba(255, 255, 255, 0.07); color: #cbd5e1;
    cursor: pointer;
}
.msc-edit:hover, .msc-add:hover { background: rgba(255, 255, 255, 0.16); color: #fff; }

/* Qué lleva esta puerta. Es el componente compartido `.stop-items` (el mismo del
   detalle del pedido y de la ficha del portal), con la superficie oscura de la
   tarjeta: la cantidad toma el acento del TIPO de parada, que es el mismo color
   que ya lleva el pin. */
.msc-items { margin-top: 0.125rem; }
.msc-items li { border-top-color: rgba(255, 255, 255, 0.10); color: #e2e8f0; }
.msc-items .pi-qty { color: var(--msc-accent); }

/* El motivo de la cancelación, dentro del mismo aviso: es OPCIONAL y por eso no
   se pide en una pantalla propia — se escribe donde se confirma o no se escribe. */
.msc-confirm-motivo {
    margin-bottom: 0.5rem; background: rgba(255, 255, 255, 0.06);
    border-color: rgba(255, 255, 255, 0.18); color: #f8fafc;
}
.msc-confirm-motivo::placeholder { color: #94a3b8; }

/* Los chips de número de las hermanas usan la misma escala de color que el pin.
   `--pickup`/`--delivery` ya existían para la hoja del rider; acá se nombran por
   TIPO para que traspaso y fallida tengan el suyo y no caigan en un gris mudo. */
.seq-chip--recogida { background: var(--stop-pickup); color: #fff; }
.seq-chip--entrega { background: var(--stop-drop); color: #fff; }
.seq-chip--traspaso { background: var(--stop-handoff); color: #fff; }
.seq-chip--fallida { background: var(--stop-fail); color: #fff; }
/* Una entrega que se DA en un punto de encuentro y ya salió del recorrido de
   quien la lleva: no tiene número que mostrar, así que el chip lleva la flecha
   del relevo en el morado del traspaso, apagado. Poner un número inventado —o
   el guion de "no sé"— sería peor: acá el dato es la dirección. */
.seq-chip--muted {
    background: transparent; color: var(--stop-handoff);
    box-shadow: inset 0 0 0 1px var(--stop-handoff);
}

@media (max-width: 900px) {
    /* Con el panel debajo del mapa, la tarjeta pasa a fija: si no, queda anclada
       al fondo del contenedor entero y se sale de la pantalla. */
    .map-stopcard { position: fixed; width: min(340px, calc(100% - 24px)); }
}

/* ── Ventana flotante "Previsto para este día" ─────────────────────────────
   Antes era el primer bloque del panel derecho. Con muchas plantillas se comía
   la altura justo por encima de lo que SÍ existe (el pozo, las rutas), y lo
   previsto tiene otra frecuencia de uso: se mira al empezar el día, se genera y
   se olvida — o sea, es algo que se ABRE, no algo que está siempre.

   Va ARRIBA a la derecha del canvas pero desplazada a la izquierda de los
   botones +/− de zoom (`--map-planwin-right`), que no se pueden tapar: son el
   control con el que uno mira lo que la ventana está describiendo. A diferencia
   de la tarjeta de la parada NO usa el chrome oscuro escrito a mano: es el mismo
   bloque del panel, sacado del panel, y el remapeo de `body.role-admin` ya lo
   deja en el gris que le toca. Borde punteado como el contorno de sus pines. */
.map-planwin {
    position: absolute;
    top: var(--space-3); right: var(--map-planwin-right); z-index: 24;
    width: min(330px, calc(100% - 2 * var(--space-3)));
    max-height: min(70%, 520px);
    display: flex; flex-direction: column;
    background: var(--surface);
    border: 1px dashed var(--gray-300);
    border-left: 4px solid #94A3B8;
    border-radius: var(--radius-lg);
    box-shadow: 0 10px 32px rgba(0, 0, 0, 0.35);
    overflow: hidden;
}
.map-planwin[hidden] { display: none; }
.map-planwin-head {
    display: flex; align-items: center; gap: 0.375rem; flex-shrink: 0;
    padding: 0.4375rem 0.5625rem; border-bottom: 1px solid var(--gray-200);
    background: var(--gray-100);
}
.map-planwin-title {
    display: inline-flex; align-items: center; gap: 0.3125rem;
    font-size: 0.8rem; font-weight: 700; color: var(--gray-800);
}
.map-planwin-count {
    margin-left: auto; font-size: 0.72rem; font-weight: 700;
    background: var(--gray-200); color: var(--gray-700);
    border-radius: 999px; padding: 0 0.4375rem;
}
.map-planwin-close {
    border: 0; background: transparent; padding: 0 2px; cursor: pointer;
    font-size: 1.2rem; line-height: 1; color: var(--gray-500);
}
.map-planwin-close:hover { color: var(--gray-800); }
.map-planwin-body { padding: 0.5rem; overflow-y: auto; }
/* Adentro de la ventana la lista ya no necesita su propio tope de alto: el que
   scrollea es el cuerpo. Con los dos, el scroll quedaba anidado. */
.map-planwin-body .map-plan-list { padding: 0; max-height: none; }
.map-planwin-body .map-plan-gen { width: 100%; margin-top: 0.5rem; }

/* ── Buscar una dirección EN el mapa y su PIN AZUL (map-search.js) ─────────
   Arriba al CENTRO del canvas, no de `.map-screen-body` (que mide el mapa MÁS
   el panel): el centro sale restando el panel y su hueco, igual que la ventana
   «Previsto» ancla su borde derecho. Flota encima del mapa, así que lleva
   superficie propia (`--surface-raised`), y va por encima de «Previsto» (24):
   su lista es lo que se está usando en ese momento. El azul es el de la marca
   (`--primary`) y NO es ninguno de los cuatro colores de parada: el pin no es
   una parada todavía. */
.map-search {
    position: absolute; z-index: 25;
    top: var(--space-3);
    left: calc((100% - var(--map-panel-w) - var(--space-3)) / 2);
    transform: translateX(-50%);
    width: min(26rem, calc(100% - var(--map-panel-w) - var(--space-3) * 2
                           - var(--map-zoom-clear) * 2));
}
.map-search-bar { display: flex; align-items: center; gap: 0.375rem; }
.map-search-field {
    position: relative; flex: 1; min-width: 0;
    display: flex; align-items: center;
    background: var(--surface-raised);
    border: 1px solid var(--gray-300);
    border-radius: 999px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
    transition: border-color 0.15s, box-shadow 0.15s;
}
.map-search-field:focus-within {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 30%, transparent),
                0 6px 20px rgba(0, 0, 0, 0.35);
}
.map-search-icon {
    position: absolute; left: 0.875rem;
    font-size: 0.9rem; color: var(--gray-500); pointer-events: none;
}
.map-search-input {
    flex: 1; min-width: 0; height: 2.375rem;
    border: 0; outline: 0; background: transparent;
    padding: 0 2.25rem 0 2.375rem;
    font-size: 0.9rem; color: var(--gray-900);
}
.map-search-input::placeholder { color: var(--gray-500); }
.map-search-clear {
    position: absolute; right: 0.375rem;
    width: 1.75rem; height: 1.75rem; border: 0; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; color: var(--gray-500); font-size: 0.75rem;
}
.map-search-clear:hover { background: var(--gray-200); color: var(--gray-800); }
.map-search-clear[hidden] { display: none; }
.map-search-hold {
    flex-shrink: 0; width: 2.375rem; height: 2.375rem;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--gray-300); border-radius: 50%;
    background: var(--surface-raised); color: var(--gray-700);
    font-size: 1rem; cursor: pointer;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.map-search-hold:hover { color: var(--primary); border-color: var(--primary); }
.map-search-hold.is-active {
    background: var(--primary); border-color: var(--primary); color: var(--bs-white, #fff);
}
.map-search-list {
    margin-top: 0.375rem; padding: 0.25rem;
    max-height: min(22rem, 55vh); overflow-y: auto;
    background: var(--surface-raised);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
}
.map-search-list[hidden] { display: none; }
.map-search-item {
    display: flex; align-items: flex-start; gap: 0.625rem;
    width: 100%; padding: 0.5rem 0.625rem;
    border: 0; border-radius: var(--radius-md);
    background: transparent; color: var(--gray-800); text-align: left; cursor: pointer;
}
.map-search-item:hover, .map-search-item.is-active { background: var(--gray-100); }
.map-search-item-icon { margin-top: 0.125rem; color: var(--primary); }
.map-search-item-text { display: flex; flex-direction: column; min-width: 0; }
.map-search-item-addr { font-size: 0.85rem; line-height: 1.3; }
.map-search-item-meta { font-size: 0.72rem; color: var(--gray-500); }
/* Segunda línea (barrio/distrito + municipio), la MISMA que la del combobox
   (`.address-picker-sub`): una línea con puntos suspensivos. */
.map-search-item-sub {
    font-size: 0.75rem;
    color: var(--gray-600);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.map-search-empty {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.625rem 0.75rem; font-size: 0.82rem; color: var(--gray-500);
}
.map-search-spin {
    width: 0.875rem; height: 0.875rem; border-radius: 50%;
    border: 2px solid var(--gray-300); border-top-color: var(--primary);
    animation: map-search-spin 0.7s linear infinite;
}
@keyframes map-search-spin { to { transform: rotate(360deg); } }
.map-search-credit {
    margin-top: 0.25rem; padding: 0.375rem 0.625rem 0.25rem;
    border-top: 1px solid var(--gray-200);
    font-size: 0.65rem; color: var(--gray-500);
}
.map-search-credit a { color: inherit; }

/* El CRÉDITO dentro de cada mapa (rutas + datos de OpenStreetMap; lo dibuja
   `addCredit` de map-common.js). Abajo a la IZQUIERDA: la derecha es de los
   FABs del rider y de la barra del dispatcher. En el rider sube con la hoja
   (`--sheet-visible`, la misma variable y la misma curva que `.rider-map-fabs`),
   así que el bottom sheet nunca lo tapa; fuera del rider la variable no existe y
   se queda en el borde. Deja libre la columna de los FABs. */
.map-credit {
    position: absolute; z-index: 4;
    left: 0.25rem; bottom: 0.25rem;
    max-width: calc(100% - 4.5rem);
    padding: 0.0625rem 0.375rem; border-radius: 0.25rem;
    background: color-mix(in srgb, var(--surface) 82%, transparent);
    color: var(--gray-700);
    font-size: 0.625rem; line-height: 1.35;
    transform: translate3d(0, calc(-1 * var(--sheet-visible, 0px)), 0);
}
.map-credit a { color: inherit; }
.rider-map-shell.is-sheet-animating .map-credit {
    transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.map-search-hint {
    width: fit-content; max-width: 100%; margin: 0.375rem auto 0;
    padding: 0.25rem 0.75rem; border-radius: 999px;
    background: var(--primary); color: var(--bs-white, #fff);
    font-size: 0.78rem; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
}
.map-search-hint[hidden] { display: none; }
.map-search-hint kbd { font-size: 0.7rem; padding: 0 0.25rem; }
.map-search.is-holding .map-search-field { opacity: 0.85; }

/* El PIN: un pin de marca con sus dos acciones ENCIMA. Se abren al pasar por
   encima o al tocarlo (en táctil no hay hover), y el puente invisible de abajo
   evita que el hover se pierda en el hueco entre el pin y las acciones. El pin
   mide en px como los demás pines: el mapa se queda a 1:1. */
.map-search-pin { position: relative; display: flex; flex-direction: column; align-items: center; color: var(--primary); }
.map-search-pin-head {
    width: 32px; height: 42px; padding: 0; border: 0; background: transparent;
    color: inherit; cursor: pointer;
    filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.45));
    animation: map-search-drop 0.3s ease-out;
}
@keyframes map-search-drop { from { transform: translateY(-12px); opacity: 0; } }
.map-search-pin-svg { display: block; width: 100%; height: 100%; }
.map-search-pin-pop {
    position: absolute; bottom: calc(100% + 0.375rem); left: 50%;
    transform: translateX(-50%);
    display: none; flex-direction: column; align-items: center; gap: 0.375rem;
}
.map-search-pin-pop::after {
    content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 0.5rem;
}
.map-search-pin:hover .map-search-pin-pop,
.map-search-pin.is-open .map-search-pin-pop { display: flex; }
.map-search-pin-label {
    width: max-content; max-width: 16rem; padding: 0.375rem 0.625rem;
    text-align: center;
    background: var(--surface-raised);
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-md);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.map-search-pin-addr { display: block; font-size: 0.8rem; font-weight: 600; line-height: 1.25; color: var(--gray-900); }
.map-search-pin-meta { display: block; margin-top: 0.125rem; font-size: 0.68rem; color: var(--gray-500); }
.map-search-pin-tools { display: flex; gap: 0.5rem; }
.map-search-pin-btn {
    width: 36px; height: 36px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 1rem; cursor: pointer;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    transition: transform 0.12s, background 0.15s, color 0.15s;
}
.map-search-pin-btn:hover { transform: translateY(-1px); }
.map-search-pin-btn.is-add { border: 0; background: var(--primary); color: var(--bs-white, #fff); }
.map-search-pin-btn.is-remove {
    border: 1px solid var(--gray-300); background: var(--surface-raised); color: var(--gray-700);
}
.map-search-pin-btn.is-remove:hover { color: var(--danger); border-color: var(--danger); }
/* Un modal está eligiendo en el mapa: el pin se ofrece para TOMARSE (aro que
   late) y el ya tomado lleva ✓, el mismo gesto que una parada elegida. */
.map-search-pin.is-takeable .map-search-pin-head { cursor: crosshair; }
.map-search-pin.is-takeable::before {
    content: ''; position: absolute; left: 50%; bottom: -0.375rem;
    width: 1.5rem; height: 0.625rem; margin-left: -0.75rem; border-radius: 50%;
    border: 2px solid var(--primary); pointer-events: none;
    animation: map-search-take 1.2s ease-out infinite;
}
@keyframes map-search-take { from { transform: scale(0.6); opacity: 1; } to { transform: scale(1.8); opacity: 0; } }
.map-search-pin.is-taken .map-search-pin-head::after {
    content: '\2713'; position: absolute; top: -0.375rem; right: -0.5rem;
    width: 1.125rem; height: 1.125rem; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 0.7rem; font-weight: 700; line-height: 1;
    background: var(--success); color: var(--bs-white, #fff);
}
.map-search-pin-head { position: relative; }

/* La tarjeta RÁPIDA de una parada al pasar el puntero: la misma pieza que la
   del pin (`.map-search-pin-label`), anclada ENCIMA del puntero. No recibe el
   puntero: si lo recibiera, taparía el pin y dispararía su propio mouseleave. */
.map-hover-card {
    position: absolute; z-index: 3; pointer-events: none;
    transform: translate(-50%, calc(-100% - 0.875rem));
}
.map-hover-card[hidden] { display: none; }

@media (max-width: 900px) {
    /* Apilado, el panel ya no está al lado: el centro es el del canvas entero. */
    .map-search {
        left: 50%;
        width: min(26rem, calc(100% - var(--map-zoom-clear) * 2));
    }
}

/* ── Modal "Generar lo previsto": direcciones y horas por plantilla ─────────
   La casilla de "qué se genera" y el desplegable de "con qué hora" son dos
   preguntas distintas, y por eso la casilla vive FUERA del `<summary>`: dentro,
   marcarla abriría el desplegable de paso. Nace cerrado — la hora casi siempre
   ya viene contestada por la plantilla. */
.plangen-tpl {
    border: 1px solid var(--gray-200); border-radius: var(--radius-sm);
    padding: 0.25rem 0.375rem;
}
.plangen-more > summary {
    font-size: 0.72rem; color: var(--gray-600); cursor: pointer;
    padding: 2px 0 2px 22px;
}
.plangen-more[open] > summary { margin-bottom: 0.25rem; }
.plangen-stops {
    display: flex; flex-direction: column; gap: 0.4375rem;
    padding: 2px 0 4px 22px;
}
/* El color del tipo de parada, el MISMO de los pines y del panel: qué es cada
   renglón se contesta antes de leer la palabra. */
.plangen-stop {
    display: flex; flex-direction: column; gap: 0.25rem;
    border-left: 3px solid var(--gray-300); padding-left: 0.4375rem;
}
/* La fila de siempre —casilla, tipo, dirección y ventana— pasa a ser la CABECERA
   de la parada: debajo de ella, en las entregas, cuelgan los productos. */
.plangen-stop-head {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem;
}
.plangen-stop.is-recogida { border-left-color: var(--stop-pickup); }
.plangen-stop.is-entrega { border-left-color: var(--stop-drop); }
/* Elegir QUÉ entregas entran (tanda 10). La casilla va al principio de la fila,
   antes del tipo: es la pregunta que se contesta primero. Blanco táctil de 44px
   sin engordar la fila (el padding negativo lo devuelve). La recogida no lleva
   —es una por pedido—, así que las filas quedan alineadas por el texto. */
.plangen-pick {
    flex: 0 0 auto; display: inline-flex; align-items: center;
    padding: 12px 6px 12px 0; margin: -12px 0;
    cursor: pointer;
}
.plangen-pick input { width: 16px; height: 16px; cursor: pointer; }
.plangen-stop:has(input[data-del-pick]:not(:checked)) { opacity: 0.5; }
/* «N de M entregas» en el `<summary>`: lo único que se ve con el desplegable
   cerrado, así que sin él una plantilla recortada se lee igual que una entera. */
.plangen-count { color: var(--stop-drop-ink); font-weight: 600; }
.plangen-count.is-empty { color: var(--danger); }
.plangen-stop-what { flex: 1 1 8.75rem; min-width: 0; display: flex;
    flex-direction: column; align-items: flex-start; gap: 2px; }
/* El tipo va en una PASTILLA del color que ya lo identifica en el resto de la
   app (recogida verde, entrega ámbar), no en un gris uniforme: con la lista
   apretada, un borde izquierdo de 3px no alcanza para distinguir de un vistazo
   qué renglón es cuál, y el color es justo el dato que se busca ahí.

   El TEXTO va con el `-ink`, que `body.role-admin` remapea; el fondo es una
   tinta del color BASE, que no se remapea (un verde de recogida es el mismo
   verde en las dos pantallas) y a esta opacidad funciona en claro y en oscuro. */
.plangen-stop-kind {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.64rem; font-weight: 800; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--gray-500);
    border-radius: 999px; padding: 1px 0.4375rem; line-height: 1.5;
    background: var(--gray-100);
}
.plangen-stop.is-recogida .plangen-stop-kind {
    color: var(--stop-pickup-ink); background: rgba(34, 197, 94, 0.16);
}
.plangen-stop.is-entrega .plangen-stop-kind {
    color: var(--stop-drop-ink); background: rgba(245, 158, 11, 0.16);
}
.plangen-stop-addr { font-size: 0.76rem; overflow-wrap: anywhere; }
.plangen-stop-addr.is-empty { color: var(--gray-500); font-style: italic; }
.plangen-win { display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; }
.plangen-win input[type="time"] { width: 98px; }
.plangen-win-sep { color: var(--gray-500); font-size: 0.75rem; }
.plangen-tpl[hidden] { display: none; }
/* ── Los PRODUCTOS de una entrega, en el mismo desplegable ───────────────────
   Qué se lleva a esa puerta y cuánto, ajustable para ESTE pedido: la plantilla
   no se toca. Apagar la casilla (o poner 0) deja el producto fuera — quitar un
   producto y ponerle cantidad son la misma pregunta, no dos listas. */
.plangen-items { list-style: none; margin: 0; padding: 0 0 0 1.375rem;
    display: flex; flex-direction: column; gap: 0.1875rem; }
.plangen-item {
    display: flex; align-items: center; gap: 0.375rem;
    font-size: 0.72rem;
}
.plangen-item-pick {
    flex: 1 1 6.25rem; min-width: 0;
    display: inline-flex; align-items: center; gap: 0.3125rem; cursor: pointer;
}
.plangen-item-pick input { width: 14px; height: 14px; cursor: pointer; }
.plangen-item-name { overflow-wrap: anywhere; }
.plangen-item-qty { width: 4.375rem; flex: 0 0 auto; }
.plangen-item-unit { flex: 0 0 auto; color: var(--gray-500); font-size: 0.68rem; }
/* Lo que se agrega SOLO para este pedido: se distingue con su marca y se quita
   del todo (no hay nada que apagar — no venía de ningún lado). */
.plangen-item-tag {
    font-size: 0.6rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--primary);
    background: var(--primary-light); border-radius: 999px; padding: 0 0.375rem;
}
.plangen-item-del {
    flex: 0 0 auto; border: 0; background: transparent; cursor: pointer;
    color: var(--gray-500); font-size: 1rem; line-height: 1; padding: 0 0.25rem;
}
.plangen-item-del:hover { color: var(--danger); }
/* La puerta para SUMAR un producto. Se dibuja siempre, aunque el molde no lleve
   ninguno: lo que hay debajo no es un listado vacío, es por dónde se agrega. */
.plangen-additem { padding-left: 1.375rem; }
.plangen-additem-open, .plangen-newprod-open {
    border: 0; background: transparent; padding: 0.125rem 0;
    color: var(--primary); font-size: 0.7rem; cursor: pointer;
}
.plangen-additem-box {
    display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.25rem;
}
/* La lista FLOTA sobre el modal oscuro: superficie propia (el chrome del admin
   remapea los grises, y sin fondo se leería el texto sobre el modal). */
.plangen-additem-list {
    list-style: none; margin: 0; padding: 0.125rem; max-height: 9.375rem;
    overflow-y: auto; border: 1px solid var(--gray-200);
    border-radius: var(--radius-sm); background: var(--surface);
}
.plangen-additem-list:empty { display: none; }
.plangen-additem-list button {
    display: flex; width: 100%; align-items: center; justify-content: space-between;
    gap: 0.375rem; border: 0; background: transparent; text-align: left;
    padding: 0.25rem 0.375rem; font-size: 0.72rem; color: var(--gray-700);
    border-radius: var(--radius-sm); cursor: pointer;
}
.plangen-additem-list button:hover { background: var(--gray-100); color: var(--primary); }
.plangen-additem-unit { color: var(--gray-500); font-size: 0.66rem; }
.plangen-additem-empty {
    padding: 0.25rem 0.375rem; font-size: 0.7rem; color: var(--gray-500);
}
/* Dar de alta un producto que el catálogo todavía no tiene, sin salir de acá. */
.plangen-newprod { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; }
.plangen-newprod input { flex: 1 1 7.5rem; min-width: 0; }
.plangen-newprod-msg { flex: 1 1 100%; margin: 0; font-size: 0.68rem; color: var(--gray-600); }
.plangen-newprod-msg.is-error { color: var(--danger); }
/* Una entrega que no entra se apaga: sus campos se conservan (volver a marcarla
   no puede borrar lo escrito). */
.plangen-stop.is-off { opacity: 0.5; }
/* Lo que esa plantilla YA tiene generado para el día: un chip discreto pegado al
   nombre. Es información, no una alarma — la alarma, si hace falta, la pone el
   bloque de confirmación de abajo. */
.plangen-done {
    display: inline-block; margin-left: 0.375rem; font-size: 0.66rem; font-weight: 700;
    color: var(--primary); background: var(--primary-light);
    border-radius: 999px; padding: 0 0.4375rem; white-space: nowrap;
}

/* ── Aviso PREVIO a generar (bloque compartido por los dos modales) ──────────
   Lo que el servidor advierte antes de escribir: una hora de hoy que ya pasó, o
   una plantilla que ya tiene pedido ese día. No es un error —no se escribió
   nada— así que no va en rojo: va en ámbar, que es el color de "mirá esto".
   Nada de colores literales: `body.role-admin` remapea los `--gray-*`. */
.plan-confirm {
    border: 1px solid var(--stop-drop); border-radius: var(--radius-md);
    background: rgba(245, 158, 11, 0.10);
    padding: 8px 10px; margin: var(--space-2) 0;
}
.plan-confirm-head {
    font-size: 0.78rem; font-weight: 700; margin: 0 0 4px;
    color: var(--stop-drop-ink);
}
.plan-confirm-tpl { font-size: 0.78rem; margin-bottom: 4px; }
.plan-confirm-tpl ul { margin: 2px 0 0; padding-left: 18px; }
.plan-confirm-item { font-size: 0.76rem; color: var(--gray-700); }
.plan-confirm-item.is-ventana_vencida { color: var(--stop-drop-ink); }
.plan-confirm-actions {
    display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end;
    margin-top: 6px;
}

/* ── Destino de lo que se genera (bloque compartido por los dos modales) ─────
   "Generar lo previsto" y "Nuevo pedido desde una plantilla" terminan en la
   misma pregunta —¿y esto dónde va?— y ahora la hacen con el mismo marcado.
   Radios y no casillas: un pedido va a UN sitio. El cuerpo de cada opción se
   dibuja solo cuando esa opción está elegida. */
.map-dest {
    border: 1px solid var(--gray-200); border-radius: var(--radius-md);
    padding: 0.5rem 0.625rem; margin: var(--space-2) 0;
}
.map-dest > legend {
    float: none; width: auto; padding: 0; margin-bottom: 0.25rem;
    font-size: 0.78rem; font-weight: 600;
}
.map-dest-opt {
    display: flex; align-items: flex-start; gap: 0.4375rem;
    font-size: 0.82rem; padding: 0.1875rem 0; cursor: pointer;
}
.map-dest-opt input { margin-top: 3px; flex-shrink: 0; }
.map-dest-note { display: block; font-size: 0.74rem; color: var(--gray-500); }
/* El cuerpo cuelga de su opción (sangrado) para que se lea como parte de ella y
   no como una pregunta nueva. */
.map-dest-body {
    margin: 2px 0 6px 24px; padding-left: 0.625rem;
    border-left: 2px solid var(--gray-200);
}
.map-dest-body[hidden] { display: none; }

/* ── Traspaso propuesto por el rider (Pieza 1) ─────────────────────────────
   Mismo violeta que ya identifica al traspaso en el panel del rider
   (.addr-block-handoff / .handoff-source), para que "esto es un relevo" se
   lea igual en las dos puntas. */
.handoff-inbox-card {
    background: #faf5ff; border: 1px solid #d8b4fe; border-left: 4px solid #7c3aed;
    border-radius: 12px; padding: 12px 14px; margin-bottom: 12px;
}
.handoff-inbox-title {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    margin: 0 0 8px; font-size: 0.92rem; font-weight: 700; color: #5b21b6;
}
.handoff-inbox-ttl {
    margin-left: auto; font-size: 0.7rem; font-weight: 700;
    background: #ede9fe; color: #6d28d9; border-radius: 20px; padding: 2px 8px;
}
.handoff-inbox-line {
    margin: 0 0 4px; font-size: 0.83rem; color: #4b5563;
}
.handoff-inbox-actions {
    display: flex; gap: 8px; margin-top: 10px;
}
.handoff-inbox-actions form { flex: 1; }
.handoff-btn-accept, .handoff-btn-reject {
    width: 100%; border: none; border-radius: 8px; padding: 9px 10px;
    font-size: 0.85rem; font-weight: 700; cursor: pointer;
}
.handoff-btn-accept { background: #7c3aed; color: #fff; }
.handoff-btn-reject { background: #fff; color: #b91c1c; border: 1px solid #fecaca; }
.handoff-outbox-note {
    background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px;
    padding: 9px 12px; margin-bottom: 10px; font-size: 0.82rem; color: #7f1d1d;
}
.handoff-outbox-note.ok {
    background: #f0fdf4; border-color: #bbf7d0; color: #14532d;
}

/* ── Traspaso (handoff) de entrega — Tanda 3 ──────────────────────────────── */
.mess-pop-handoff {
    display: inline-flex; align-items: center; gap: 4px;
    margin: 6px 10px 8px; padding: 3px 8px;
    font-size: 0.72rem; font-weight: 600;
    border: 1px solid var(--primary); color: var(--primary);
    background: transparent; border-radius: 6px; cursor: pointer;
}
.mess-pop-handoff:hover { background: var(--primary); color: #fff; }
/* Marca de traspaso en la fila de parada del panel */
.map-stop-row.is-traspasada .map-stop-seq { color: var(--stop-handoff); }
.map-stop-handoff-tag {
    font-size: 0.6rem; font-weight: 700; text-transform: uppercase;
    color: var(--primary); background: color-mix(in srgb, var(--primary) 14%, transparent);
    border-radius: 3px; padding: 0 0.25rem; flex-shrink: 0;
}
/* Traspaso PROPUESTO por un rider, todavía sin respuesta del destino: ámbar
   para que se distinga de un traspaso ya efectivo (que es un hecho, no algo
   que el dispatcher pueda tener que resolver). */
.map-stop-handoff-tag.is-proposed {
    color: #92400e; background: rgba(250, 204, 21, 0.22);
}


/* ── CO-RECOGIDA: la recogida repartida entre riders (fase 3) ──────────────── */
/* No estrena un COLOR de parada: una copia de recogida es una recogida y va
   verde (§Código de color: la forma no lleva información que el color ya lleve).
   Lo que la distingue es esta etiqueta de texto, hermana de la del traspaso, y
   en la recogida REAL el chip con cuántos riders se la reparten. */
.map-stop-copickup-tag {
    font-size: 0.6rem; font-weight: 700; text-transform: uppercase;
    color: var(--stop-pickup-ink, var(--stop-pickup));
    background: color-mix(in srgb, var(--stop-pickup) 16%, transparent);
    border-radius: 3px; padding: 0 0.25rem; flex-shrink: 0;
}

/* El tablero del reparto. La zona de riders scrollea DENTRO del modal y
   «SIN ASIGNAR» queda pegado arriba: con seis riders la lista crece hacia abajo
   y el dato que decide si se puede guardar no puede quedar fuera de pantalla. */
.cop-riders {
    display: flex; flex-direction: column; gap: var(--space-2);
    max-height: 40vh; overflow-y: auto; margin-bottom: var(--space-2);
}
.cop-rider {
    border: 1px solid var(--gray-300); border-left: 3px solid var(--cop-color, var(--primary));
    border-radius: var(--radius-sm); padding: var(--space-2);
    cursor: pointer;
}
/* El rider ACTIVO: a él va lo próximo que se toque, en la lista o en el mapa. */
.cop-rider.is-active {
    background: color-mix(in srgb, var(--cop-color, var(--primary)) 10%, transparent);
    border-color: var(--cop-color, var(--primary));
}
.cop-rider-head { display: flex; align-items: center; gap: var(--space-2); }
.cop-dot {
    width: 0.6rem; height: 0.6rem; border-radius: 50%;
    background: var(--cop-color, var(--primary)); flex-shrink: 0;
}
.cop-name { font-weight: 600; }
.cop-count {
    font-weight: 700; color: var(--cop-color, var(--primary));
}
.cop-route { width: auto; max-width: 11rem; margin-left: auto; }
.cop-drop {
    border: 0; background: none; color: var(--gray-500); padding: 0 0.2rem; cursor: pointer;
}
.cop-drop:hover { color: var(--danger); }
.cop-stops { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem; }
/* Es un <button>, y un botón NO hereda el color del texto: sin decirlo se
   queda en el negro del navegador, ilegible sobre el chrome oscuro del admin
   (la dirección es justo lo que hay que leer para repartir). */
.cop-stop {
    border: 1px solid var(--gray-300); background: var(--surface);
    color: var(--body-color, inherit);
    border-radius: 999px; padding: 0.1rem 0.5rem; font-size: 0.72rem;
    max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    cursor: pointer;
}
.cop-stop:hover { border-color: var(--primary); }
/* Lo que ya se cargó: queda con su rider y el chip no se puede soltar. */
.cop-stop.is-locked { opacity: 0.75; cursor: not-allowed; }
.cop-stop.is-locked:hover { border-color: var(--gray-300); }
/* Una tienda que el reparto le dio entera a este rider (no es un botón). */
.cop-stop.is-pickup {
    cursor: default;
    border-color: var(--stop-pickup);
    color: var(--stop-pickup-ink, var(--stop-pickup));
}
.cop-stop.is-pickup:hover { border-color: var(--stop-pickup); }
/* A quién va cada recogida, con dos o más. */
.cop-picks {
    border: 1px solid var(--gray-300); border-radius: var(--radius-sm);
    padding: var(--space-2); margin-bottom: var(--space-2);
}
.cop-pick {
    display: flex; align-items: center; gap: var(--space-2);
    border-left: 3px solid var(--cop-color, var(--stop-pickup));
    padding: 0.15rem 0 0.15rem var(--space-2);
}
.cop-pick + .cop-pick { margin-top: 0.25rem; }
.cop-pick-addr {
    flex: 1; min-width: 0; font-size: 0.78rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cop-pick-deps { color: var(--gray-500); }
.cop-pick-sel { width: auto; max-width: 11rem; }
/* Lo que falta por asignar. En 0 se pone en verde y «Guardar reparto» se
   enciende; con cualquier número queda en rojo y el botón, apagado — y el
   backend lo valida igual: la UI es comodidad, no autorización. */
.cop-pool {
    border: 1px dashed var(--gray-400); border-radius: var(--radius-sm);
    padding: var(--space-2); margin-bottom: var(--space-2);
}
.cop-pool.is-pending { border-color: var(--danger); }
.cop-pool.is-ready { border-color: var(--success, #22C55E); }
.cop-pool-head {
    font-size: 0.62rem; font-weight: 700; text-transform: uppercase;
    color: var(--gray-600); margin-bottom: 0.25rem;
}
.cop-pool.is-pending .cop-pool-head { color: var(--danger); }
.cop-ok { color: var(--success, #22C55E); }

/* El estado de una parada a mano (ficha del pedido y tarjeta del mapa). */
.stop-status-form {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
    margin: 0.5rem 0 0; padding-top: 0.5rem;
    border-top: 1px dashed var(--gray-200);
}
.stop-status-form .stop-status-label {
    font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.03em; color: var(--gray-500);
}
.stop-status-form .stop-status-select { width: auto; flex: 0 1 14rem; min-width: 0; }
.stop-status-form .stop-status-go[disabled] { opacity: 0.45; }
.msc-status { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; margin-top: 0.5rem; }
.msc-status .form-select { width: auto; flex: 1 1 8rem; min-width: 0; }
.msc-status .form-control { flex: 1 1 100%; }
.msc-status .form-control[hidden] { display: none; }

/* ── Código de color por TIPO de parada (mismo que el halo del mapa) ───────── */
/* Recogida = verde (#22C55E), entrega = ámbar (#FACC15). El borde izquierdo de
   la tarjeta de cada parada repite ese código, igual que el bloque del rider
   marca su color a la izquierda. */
.map-stop-row.is-recogida { border-left: 3px solid var(--stop-pickup); }
.map-stop-row.is-entrega { border-left: 3px solid var(--stop-drop); }
/* Punto de TRASPASO: morado, el mismo que marca el traspaso en el detalle del
   pedido. Va después de las dos de arriba a propósito — una parada de traspaso
   es tipo 'recogida' en el modelo, y acá gana el morado. La entrega asociada
   NO se tiñe: sigue siendo una entrega normal (ámbar). */
.map-stop-row.is-handoff { border-left-color: var(--stop-handoff); }
/* Parada marcada desde el mapa: aro con el color del rider + pulso al llegar */
.map-stop-row.is-active {
    box-shadow: 0 0 0 2px var(--rider-color);
    background: color-mix(in srgb, var(--rider-color) 14%, var(--surface));
}
/* Hermanas del mismo pedido: mismo marcador, más tenue (la clickeada manda). */
.map-stop-row.is-related {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--rider-color) 45%, transparent);
    background: color-mix(in srgb, var(--rider-color) 8%, var(--surface));
}
.map-stop-row.is-flash { animation: mapStopGlow 1.4s ease; }
@keyframes mapStopGlow {
    0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--rider-color) 70%, transparent); }
    35% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--rider-color) 35%, transparent); }
    100% { box-shadow: 0 0 0 2px var(--rider-color); }
}
/* Ícono "ver el detalle del pedido" (mismo estilo que el "+" de la ruta) */
.map-stop-open {
    flex-shrink: 0; padding: 0 2px; line-height: 1;
    color: var(--gray-400); font-size: 0.85rem; text-decoration: none;
}
.map-stop-open:hover { color: var(--primary, #4A90E2); }
/* El lápiz de la fila vive en la TERCERA línea, pegado al enlace del pedido: son
   la misma familia y ahí no compite con el ✕ ni con la cancelación, que son los
   gestos destructivos de la primera. Es el que empuja al grupo contra el borde,
   así que con él el enlace ya no lo hace. */
.map-stop-edit {
    background: none; border: 0; padding: 0; line-height: 1; cursor: pointer;
    color: var(--gray-500); font-size: 0.85rem;
}
.map-stop-edit:hover { color: var(--primary, #4A90E2); }
/* El ➕ de la recogida, al lado del lápiz y con su mismo cuerpo: las dos caras
   —la tarjeta y esta fila— ofrecen lo mismo sobre la misma parada. */
.map-stop-add {
    background: none; border: 0; padding: 0; line-height: 1; cursor: pointer;
    color: var(--gray-500); font-size: 0.85rem;
}
.map-stop-add:hover { color: var(--primary, #4A90E2); }

/* ══════════════════════════════════════════════════════════════════════════
   EL CÓDIGO DE COLOR DE LAS PARADAS, FUERA DEL MAPA
   Los tres componentes que hacen que "recogida verde / entrega ámbar /
   traspaso morado" se lea IGUAL en el detalle del pedido, en el detalle de la
   ruta y en cualquier lista de pedidos. Las plantillas los usan a través de
   los macros de `admin/_stop_visuals.html`: el marcado se escribe una vez.
   ══════════════════════════════════════════════════════════════════════════ */

/* (1) EL PIN. Es el mismo símbolo en todas partes —un pin relleno— y lo único
   que cambia es el color: la forma no puede llevar información que el color ya
   lleva, o el detalle de la ruta acaba con una cajita para la recogida y un pin
   para la entrega, que es lo que había. */
.stop-pin { flex-shrink: 0; line-height: 1; }
.stop-pin.is-recogida { color: var(--stop-pickup-ink); }
.stop-pin.is-entrega  { color: var(--stop-drop-ink); }
.stop-pin.is-traspaso { color: var(--stop-handoff-ink); }
.stop-pin.is-fallida  { color: var(--stop-fail-ink); }

/* (2) EL RÓTULO de un bloque de paradas ("Recogida", "Entregas (1/3)"). Es un
   TÍTULO, no la etiqueta menuda de `.section-label`: en el detalle del pedido
   es lo que parte la ficha en dos mitades, y a 0.7rem en gris no partía nada.
   Lleva el pin de su color y una línea inferior del mismo tono, que es lo que
   hace que las dos columnas se lean como dos bloques y no como texto seguido. */
.stop-title {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    margin: 0 0 0.625rem; padding-bottom: 0.375rem;
    font-size: 1rem; font-weight: 800; letter-spacing: -0.01em;
    border-bottom: 2px solid var(--st-accent, var(--gray-200));
}
.stop-title.is-recogida { --st-accent: var(--stop-pickup); color: var(--stop-pickup-ink); }
.stop-title.is-entrega  { --st-accent: var(--stop-drop);   color: var(--stop-drop-ink); }
.stop-title.is-traspaso { --st-accent: var(--stop-handoff); color: var(--stop-handoff-ink); }
.stop-title .stop-pin { font-size: 1.1em; }
/* Contador "1/3": va en el título pero no compite con él. */
.stop-title-count {
    font-size: 0.78rem; font-weight: 700;
    color: var(--st-accent); background: color-mix(in srgb, var(--st-accent) 16%, transparent);
    border-radius: 999px; padding: 1px 0.5625rem;
}

/* (3) LA PASTILLA DE PESO. El peso es lo que decide si el paquete entra en la
   mochila: leerlo era buscar un `disp-sub` gris de 0.72rem entre el CP y el
   teléfono. Se lee como un dato, con su icono y su número en negrita.
   Sin peso cargado NO se dibuja nada (la plantilla ni la emite): reservar la
   fila haría del caso normal un hueco. */
.peso-chip {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 2px 9px; border-radius: 8px;
    font-size: 0.8rem; font-weight: 700; font-variant-numeric: var(--num);
    color: var(--gray-900);
    background: var(--gray-100); border: 1px solid var(--gray-200);
}
.peso-chip i { color: var(--gray-500); font-size: 0.95em; }

/* Bloque de UNA parada en el detalle del pedido: el mismo borde izquierdo de
   color que la fila del panel del mapa, para que las dos superficies se lean
   con el mismo gesto. */
.stop-block.is-entrega { border-left: 3px solid var(--stop-drop); padding-left: 0.625rem;
                         --si-accent: var(--stop-drop-ink); }
.stop-block.is-recogida { border-left: 3px solid var(--stop-pickup); padding-left: 0.625rem;
                          --si-accent: var(--stop-pickup-ink); }
.stop-block.is-traspaso { border-left: 3px solid var(--stop-handoff); padding-left: 0.625rem;
                          --si-accent: var(--stop-handoff-ink); }
.stop-block.is-entrega .stop-num { background: var(--stop-drop); color: #fff; }
/* La dirección de una parada es el dato que se busca: se lee antes que el CP,
   el contacto o el estado que la acompañan. */
.stop-head .stop-addr { font-size: 0.95rem; }
/* Metadatos de una parada en una sola fila (CP · peso · contacto), en vez de
   cuatro párrafos apilados que empujan la siguiente entrega fuera de la vista. */
.stop-meta {
    display: flex; align-items: center; gap: 0.375rem 0.875rem; flex-wrap: wrap;
    margin: 0.375rem 0 0; font-size: 0.8rem; color: var(--gray-500);
}
.stop-meta i { color: var(--gray-400); }
.stop-meta-cp { font-variant-numeric: var(--num); letter-spacing: 0.02em; }

/* QUÉ PRODUCTOS lleva una parada. Es LA MISMA lista en las dos mitades de la
   ficha —en la recogida el total del pedido, en cada entrega su parte— y por
   eso es un solo componente: leerlas con marcados distintos (una con viñetas
   de Bootstrap y un guión, la otra con la cantidad delante) obligaba a
   traducir mentalmente entre las dos columnas de la misma pantalla.

   Lo único que cambia es el COLOR de la cantidad, que lo hereda del bloque:
   verde donde se carga, ámbar donde se descarga. `--si-accent` se declara en
   `.stop-block.is-*`, así que una parada de traspaso sale morada sola. */
.stop-items { margin: 0.5rem 0 0; padding: 0; list-style: none; }
.stop-items li {
    display: flex; align-items: baseline; gap: 0.5rem;
    padding: 0.1875rem 0; font-size: 0.83rem; border-top: 1px dashed var(--gray-100);
}
.stop-items li:first-child { border-top: none; }
.stop-items .pi-qty {
    font-weight: 800; font-variant-numeric: var(--num);
    color: var(--si-accent, var(--gray-900)); white-space: nowrap;
}
.stop-items .pi-name { min-width: 0; overflow-wrap: anywhere; }

/* ── EL MISMO CÓDIGO, EN EL FORMULARIO ─────────────────────────────────────
   Crear un pedido, editarlo y crear una plantilla son la misma pantalla, y sin
   ninguna marca eran "puros formularios": nada decía si el campo "Nombre" que
   se está rellenando es el del contacto de la RECOGIDA o el de la tercera
   entrega, y con dos o tres destinos los bloques se leían todos iguales.

   Se marcan las TARJETAS y las cabeceras, no los campos: el color tiene que
   contestar "dónde estoy escribiendo", y teñir cada input convertiría el alta
   en un semáforo. Es el MISMO verde/ámbar del mapa y de la ficha del pedido —
   quien crea el pedido y quien lo mira después ven el mismo código. */
.cp-card.is-recogida { --cc: var(--stop-pickup); --cc-ink: var(--stop-pickup-ink); }
.cp-card.is-entrega  { --cc: var(--stop-drop);   --cc-ink: var(--stop-drop-ink); }
.cp-card.is-recogida,
.cp-card.is-entrega { border-left: 3px solid var(--cc); }
.cp-card.is-recogida > .cp-card-header,
.cp-card.is-entrega > .cp-card-header {
    color: var(--cc-ink);
    font-weight: 700;
    background: color-mix(in srgb, var(--cc) 9%, transparent);
    border-bottom-color: color-mix(in srgb, var(--cc) 32%, transparent);
}
.cp-card.is-recogida > .cp-card-header i,
.cp-card.is-entrega > .cp-card-header i { color: var(--cc-ink); }
/* El color llega también a DONDE SE ESCRIBE, no solo al marco: un tinte muy
   bajo sobre el cuerpo de la tarjeta. Es lo que hace que con tres entregas
   apiladas se vea de un vistazo dónde termina una y empieza la siguiente, sin
   teñir un solo input (eso convertiría el formulario en un semáforo). Va con
   `color-mix` sobre transparente, así que sirve igual en el chrome oscuro. */
.cp-card.is-recogida > .cp-card-body,
.cp-card.is-entrega > .cp-card-body {
    background: color-mix(in srgb, var(--cc) 4%, transparent);
}
/* Una parada extra no tiene cabecera: el color entra por su cuerpo y por el
   borde de la tarjeta, y se separa un poco más de la entrega que la contiene. */
.extra-stop-block.is-entrega > .cp-card-body {
    background: color-mix(in srgb, var(--cc) 7%, transparent);
}

/* Pastilla que NOMBRA una parada dentro del formulario ("Entrega 1
   (principal)", "Entrega 2"). Es lo que hace que tres bloques idénticos dejen
   de ser tres bloques idénticos. El texto lo escribe el JS de la grilla al
   renumerar; acá solo vive el aspecto. */
.stop-tag {
    display: inline-flex; align-items: center; gap: 0.375rem;
    padding: 2px 0.625rem; border-radius: 999px;
    font-size: 0.76rem; font-weight: 800; line-height: 1.5;
    color: var(--stop-drop-ink);
    background: color-mix(in srgb, var(--stop-drop) 15%, transparent);
}
.stop-tag.is-recogida {
    color: var(--stop-pickup-ink);
    background: color-mix(in srgb, var(--stop-pickup) 15%, transparent);
}
.stop-tag .stop-pin { font-size: 0.95em; }
.stop-tag[hidden] { display: none; }

/* «Esta entrega viene de» (multi-recogida). Va ARRIBA de cada entrega, bajo su
   nombre, en una franja con el VERDE de la recogida: es una pregunta sobre las
   recogidas, dicha dentro de la entrega. Al final del bloque se leía como parte
   de la tarjeta siguiente. */
.stop-sources {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.75rem;
    padding: 0.375rem 0.625rem;
    margin-bottom: 0.625rem;
    border-left: 3px solid var(--stop-pickup);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--stop-pickup) 9%, transparent);
}
.stop-sources[hidden] { display: none; }
.stop-sources-label {
    margin: 0; font-size: 0.78rem; font-weight: 700; color: var(--stop-pickup-ink);
}
.stop-sources .form-check-inline { margin: 0; }

/* Cada entrega es una TARJETA separada, no un bloque pegado al anterior: la
   primaria contiene a las 2..N, así que sin aire ni una línea entre medias todo
   se leía como una sola tarjeta larga. Con `:has` y no `:not(:empty)`: la
   edición sirve el contenedor con los espacios del bucle de Jinja, y ahí
   «vacío» nunca es verdad — la línea saldría sin ninguna entrega extra. */
#extra-stops:has(> .extra-stop-block) {
    margin-top: var(--space-4) !important;
    padding-top: var(--space-3);
    border-top: 1px dashed color-mix(in srgb, var(--stop-drop) 45%, transparent);
}
#extra-stops > .extra-stop-block { margin-bottom: var(--space-3) !important; }
#extra-stops > .extra-stop-block:last-child { margin-bottom: 0 !important; }

/* Cabecera de una columna de ENTREGA en la grilla producto × parada. Las
   columnas son "Producto | Entrega 1 | Entrega 2 | … | Total": sin la marca,
   saber en qué entrega se está escribiendo una cantidad era contar columnas.
   La cantidad se pinta en ámbar por lo mismo que en la ficha del pedido. */
.item-grid thead th.ig-stop {
    color: var(--stop-drop-ink);
    background: color-mix(in srgb, var(--stop-drop) 12%, transparent);
    border-bottom: 2px solid color-mix(in srgb, var(--stop-drop) 45%, transparent);
}

/* ── Lista de rutas agrupada por rider (admin/routes) ──────────────────────── */
.route-groups { display: flex; flex-direction: column; gap: var(--space-4); }
.route-group {
    background: var(--surface); border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg); overflow: hidden;
    border-left: 4px solid var(--rider-color);
}
.route-group-unassigned { border-left-style: dashed; }
.route-group-head {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 16px; border-bottom: 1px solid var(--gray-200);
    background: color-mix(in srgb, var(--rider-color) 6%, var(--surface));
}
.route-group-dot {
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--rider-color); flex-shrink: 0;
}
.route-group-name { font-weight: 700; font-size: 0.95rem; }
.route-group-count {
    margin-left: auto; font-size: 0.76rem; font-weight: 600;
    color: var(--gray-600); background: var(--gray-100);
    border-radius: 999px; padding: 2px 10px;
}
.route-group-list { display: flex; flex-direction: column; }
.route-item {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 16px; text-decoration: none; color: inherit;
    border-top: 1px solid var(--gray-100); transition: background 0.12s ease;
}
.route-group-list > .route-item:first-child { border-top: none; }
.route-item:hover { background: color-mix(in srgb, var(--rider-color) 8%, var(--surface)); }
.route-item-num {
    width: 34px; height: 34px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: 0.9rem; color: #fff;
    background: var(--rider-color); border-radius: 8px;
}
.route-item-body { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.route-item-name { font-weight: 600; }
.route-item-meta { font-size: 0.76rem; color: var(--gray-600); display: flex; gap: 6px; flex-wrap: wrap; }
.route-item-chevron { color: var(--gray-400); flex-shrink: 0; }

/* ── Detalle de ruta (admin/routes/<id>) ──────────────────────────────────── */
.route-detail-stops .map-stop-seq { background: var(--rider-color); }
.route-rider-dot {
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--rider-color); display: inline-block; flex-shrink: 0;
}
/* Fila de pedido clickeable: quitar el color de link del número */
.route-order-row { cursor: pointer; }
.route-order-id { font-weight: 700; color: var(--body-color, inherit); }

.mess-handoff-backdrop {
    position: fixed; inset: 0; z-index: 1080;
    background: rgba(0, 0, 0, 0.45);
    display: flex; align-items: center; justify-content: center;
    padding: var(--space-3);
}
.mess-handoff-backdrop[hidden] { display: none; }
.mess-handoff-modal {
    background: var(--surface); color: var(--body-color, inherit);
    border-radius: var(--radius-lg); width: 100%; max-width: 380px;
    max-height: 90vh; overflow-y: auto;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.mess-handoff-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--space-3); border-bottom: 1px solid var(--gray-200);
}
.mess-handoff-close {
    border: none; background: transparent; font-size: 1.4rem; line-height: 1;
    color: var(--gray-500); cursor: pointer;
}
.mess-handoff-body { padding: var(--space-3); }
/* «Editar parada» y «Agregar una entrega» son la MISMA pantalla preguntando lo
   mismo —dónde, cuándo, a quién, cuánto pesa y qué lleva—, así que miden lo
   mismo: el ancho de los demás modales del mapa. La edición tuvo un `is-wide`
   de 640px mientras era la única con lista de productos; lo que la deja entrar
   en el ancho normal es que la fila de producto ENVUELVA (`.sedit-row`), con el
   nombre en su renglón y la cantidad debajo, en vez de pelear por el ancho. */

/* ── El LOTE del traspaso (fase 2): N entregas, UNA cita ───────────────────
   La lista de lo elegido vive DENTRO del modal de siempre; lo único que cambia
   mientras se eligen más entregas es que el modal se aparta a un costado para
   no tapar el mapa — se APARTA, no se cierra: lo escrito (rider, punto, motivo)
   tiene que seguir ahí cuando se vuelve. */
.mess-handoff-lote { border-left: 3px solid var(--stop-handoff, #7C3AED); padding-left: 0.55rem; }
.mhl-list { list-style: none; margin: 0 0 0.4rem; padding: 0; display: flex;
            flex-direction: column; gap: 0.25rem; }
.mhl-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; }
.mhl-dir { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
           white-space: nowrap; }
.mhl-order { flex: none; color: var(--gray-500); }
.mhl-drop { flex: none; width: 1.6rem; height: 1.6rem; line-height: 1;
            border: 1px solid var(--gray-300); border-radius: var(--radius-sm);
            background: transparent; color: var(--gray-500); cursor: pointer; }
/* Editando un traspaso, una entrega que el destino ya empezó no se puede sacar:
   la fila lo DICE con el texto del servidor en vez de quedarse sin ✕ y sin
   explicación. La línea deja de ser de una sola altura, así que el corte por
   elipsis se levanta acá y no en `.mhl-dir`, que la comparte con el alta. */
.mhl-row:has(.mhl-note) .mhl-dir { white-space: normal; overflow: visible; }
.mhl-note { display: block; font-size: 0.72rem; color: var(--gray-500);
            white-space: normal; }
/* Editando, el rider destino se LEE (no se elige): cambiar de destino es otro
   traspaso. Es la misma letra chica de los avisos del modal. */
.mess-handoff-dest-fixed { font-size: 0.78rem; }
[data-handoff-add].is-on {
    background: var(--stop-handoff, #7C3AED); border-color: var(--stop-handoff, #7C3AED);
    color: #fff;
}
/* Apartado, el modal deja de ser un modal y pasa a ser una tarjeta DENTRO del
   mapa: esquina inferior DERECHA, es decir la de `.map-stopcard` (que vive
   abajo a la izquierda) del otro lado — así el gesto de elegir paradas tiene
   siempre el mismo sitio donde mirar y ninguno de los dos tapa al otro.
   Sigue siendo `position: fixed` —vive fuera de `.map-screen-body`—, así que la
   esquina se calcula con las MISMAS medidas del chrome: el padding del borde,
   el ancho del panel y el hueco entre panel y canvas. Ninguna se escribe a
   mano; `--map-panel-w` está en `:root` justamente por esto. */
.mess-handoff-backdrop.is-picking {
    background: transparent; pointer-events: none;
    justify-content: flex-end; align-items: flex-end;
    padding: 0 calc(var(--content-pad) + var(--map-panel-w) + var(--space-3) * 2)
             calc(var(--content-pad) + var(--space-3)) 0;
}
.mess-handoff-backdrop.is-picking .mess-handoff-modal {
    pointer-events: auto; max-width: 340px; opacity: 0.97;
    /* Anclado abajo, el 90vh de un modal centrado se le subiría por encima de
       la barra del mapa: el techo es el de la tarjeta de la parada. */
    max-height: min(62vh, 520px);
}
@media (max-width: 900px) {
    /* Apilado, el panel deja de estar a la derecha del canvas (el mismo caso
       que `--map-planwin-right`): la tarjeta se pega al borde derecho. */
    .mess-handoff-backdrop.is-picking {
        padding-right: calc(var(--content-pad) + var(--space-3));
    }
}
/* ── Las DOS pantallas de una PARADA en el mapa ────────────────────────────
   «Editar parada» y «Agregar una entrega» son la MISMA pantalla preguntando lo
   mismo, así que miden lo mismo — un poco más que un modal de dos campos, muy
   lejos de los 640px que tuvo la edición: lo que llevan es un formulario corto
   con una lista, no una tabla.

   Y adentro se reparte la fila de la dirección a su medida: la rejilla del
   macro (`address-slim-*`) está calculada para el formulario a ancho de página,
   donde el CP en 2/12 sobra; en un modal esos 2/12 son ~50px y el campo enseña
   tres dígitos de los cinco. Acá el CP mide lo que tiene que escribirse
   —ancho FIJO— y la ciudad se queda con lo que sobra, que es el reparto al
   revés: lo que tiene largo conocido manda sobre lo que se puede recortar.
   Vale en las dos vistas, así que el bloque del teléfono no lo repite.

   Son TRES pantallas desde que la recogida también pregunta productos
   («Agregar una recogida»): el CP se quedaba con ~50px y enseñaba tres dígitos
   de los cinco, que es exactamente el fallo que este bloque vino a arreglar. */
#pickup-add-modal .mess-handoff-modal,
#stop-add-modal .mess-handoff-modal,
#stop-edit-modal .mess-handoff-modal { max-width: 440px; }
#pickup-add-modal .address-slim-row,
#stop-add-modal .address-slim-row,
#stop-edit-modal .address-slim-row { flex-wrap: wrap; }
#pickup-add-modal .address-slim-local,
#stop-add-modal .address-slim-local,
#stop-edit-modal .address-slim-local { flex: 1 1 100%; max-width: 100%; }
#pickup-add-modal .address-slim-ciudad,
#stop-add-modal .address-slim-ciudad,
#stop-edit-modal .address-slim-ciudad { flex: 1 1 8rem; max-width: none; width: auto; }
#pickup-add-modal .address-slim-cp,
#stop-add-modal .address-slim-cp,
#stop-edit-modal .address-slim-cp { flex: 0 0 6.5rem; max-width: 6.5rem; width: 6.5rem; }

/* Las ENTREGAS que se arman dentro de «Agregar una recogida»: la lista de las
   ya armadas, los dos botones y el sub-formulario. Y la fila de un POZO
   (cuánto de lo cargado se lleva una entrega), gemela de `.sedit-row`. */
.padd-drops { margin: 0.75rem 0 0.5rem; }
.padd-addr-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 0.5rem; margin-bottom: 0.125rem;
}
#dup-modal [data-dup-order-box][hidden], #dup-modal [data-dup-origin][hidden],
#dup-modal .mess-handoff-body[hidden] { display: none; }
/* El pedido existente: la ruta y el pedido, cada uno con su lista y, al
   lado, elegirlo tocando una parada. */
#dup-modal .dup-existing { display: flex; flex-direction: column; gap: 0.375rem; }
#dup-modal .dup-empty { margin: 0; font-size: 0.78rem; color: var(--gray-500); }
#dup-modal .dup-empty[hidden] { display: none; }
#dup-modal .dup-order { display: flex; align-items: center; gap: 0.5rem; }
#dup-modal .dup-order .form-select { flex: 1; min-width: 0; }
#dup-modal .dup-order-pick { flex-shrink: 0; white-space: nowrap; }
.padd-drop-list { list-style: none; margin: 0 0 0.5rem; padding: 0; }
.padd-drop-list:empty { display: none; }
.padd-drop-list li {
    display: flex; align-items: flex-start; gap: 0.5rem;
    padding: 0.375rem 0; border-top: 1px dashed var(--gray-200); font-size: 0.85rem;
}
.padd-drop-list li:first-child { border-top: 0; }
.padd-drop-list .padd-drop-pin { color: var(--stop-drop); flex: 0 0 auto; }
.padd-drop-list .padd-drop-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.padd-drop-list .padd-drop-meta { display: block; color: var(--gray-500); font-size: 0.78rem; }
/* Qué lleva cada entrega, escrito EN la lista: − · cantidad · + y «de N». */
/* Tres COLUMNAS fijas (nombre · control · cuánto queda), iguales en todas las
   filas de todas las entregas: así los controles caen uno debajo del otro. */
.padd-drop-qty {
    display: block; margin-top: 0.375rem; padding-top: 0.25rem;
    border-top: 1px solid var(--gray-100);
}
.padd-qty-row {
    display: grid; grid-template-columns: minmax(0, 1fr) auto 7.5rem;
    align-items: center; column-gap: 0.625rem; padding: 0.125rem 0;
}
.padd-qty-name {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 0.8rem;
}
.padd-qty-ctl {
    display: inline-flex; align-items: stretch; height: 1.625rem;
    border: 1px solid var(--gray-200); border-radius: var(--radius-sm);
    background: var(--surface); overflow: hidden;
}
.padd-qty-btn {
    width: 1.5rem; padding: 0; border: 0; background: transparent;
    color: var(--gray-500); font-size: 0.9rem; font-weight: 600; line-height: 1;
}
.padd-qty-btn:hover:not(:disabled) { background: var(--gray-100); color: var(--gray-900); }
.padd-qty-btn:disabled { opacity: 0.35; cursor: default; }
.padd-qty-input {
    width: 2.5rem; padding: 0 0.125rem; border: 0; border-inline: 1px solid var(--gray-200);
    background: transparent; color: inherit; font-size: 0.8rem; text-align: center;
    -moz-appearance: textfield; appearance: textfield;
}
.padd-qty-input::-webkit-outer-spin-button,
.padd-qty-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.padd-qty-input:focus { outline: 0; background: color-mix(in srgb, var(--stop-drop) 10%, transparent); }
.padd-qty-of { font-size: 0.75rem; color: var(--gray-500); text-align: right; white-space: nowrap; }
.padd-qty-of b { color: var(--gray-900); font-weight: 600; }
.padd-drop-actions, .padd-drop-form-actions {
    display: flex; flex-wrap: wrap; gap: 0.5rem;
}
.padd-drop-actions[hidden] { display: none; }
.padd-drop-form-actions { justify-content: flex-end; margin-top: 0.5rem; }
/* El sub-formulario va ENCAJADO en el modal: ancho del cuerpo, sin márgenes
   negativos y con el color de la entrega en el borde. */
.padd-drop-form {
    padding: 0.75rem; margin: 0.5rem 0 0; max-width: 100%; min-width: 0;
    border: 1px solid var(--gray-200); border-left: 3px solid var(--stop-drop);
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--stop-drop) 4%, transparent);
}
.padd-drop-form .row { margin-left: 0; margin-right: 0; }
.padd-drop-form .row > * { padding-left: 0.25rem; padding-right: 0.25rem; }
.padd-drop-form[hidden], #pickup-add-modal .mess-handoff-body[hidden] { display: none; }
/* El modal de la recogida CRECE con las entregas que se arman adentro, así que
   todo él scrollea DENTRO de la pantalla visible: tope en `svh` (con `vh` antes
   como fallback) y nada se escapa por el costado. */
#pickup-add-modal:not(.is-picking) .mess-handoff-modal {
    max-height: 90vh; max-height: 90svh;
    overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
}
#pickup-add-modal .mess-handoff-body { min-width: 0; }
.pool-row {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    padding: 0.25rem 0; border-top: 1px dashed var(--gray-200);
}
.pool-row:first-of-type { border-top: 0; }

/* Una fila de producto: nombre · cantidad · unidad · (a qué entrega va) · quitar.
   La cantidad y la ✕ tienen ancho fijo para que las filas se lean en columna;
   el nombre es lo único que se estira.

   ENVUELVE a propósito: el bloque vive en los dos modales del mapa, que miden
   lo que miden los demás, y una dirección de entrega como destino no entra en
   la misma línea que el nombre. El nombre se lleva el renglón entero
   (`flex-basis: 100%`) y debajo quedan cantidad, unidad, destino y la ✕. */
.sedit-row {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    padding: 0.25rem 0; border-top: 1px dashed var(--gray-200);
}
.sedit-row:first-child { border-top: 0; }
.sedit-name { flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; font-size: 0.85rem; }
.sedit-qty { width: 5.5rem; flex-shrink: 0; }
.sedit-unit { font-size: 0.8rem; color: var(--gray-500); flex-shrink: 0; }
.sedit-dest { flex: 1 1 8rem; max-width: 11rem; min-width: 0; }
.sedit-del {
    background: none; border: 0; padding: 0.125rem 0.25rem; cursor: pointer;
    color: var(--status-cancelado, #dc2626); flex-shrink: 0; line-height: 1;
}
/* El buscador del catálogo y su lista. La lista FLOTA sobre el modal, así que
   necesita superficie propia (regla del chrome oscuro: una lista que flota sin
   fondo se lee sobre lo que haya debajo). */
.sedit-add { position: relative; margin-top: 0.5rem; }
/* El buscador y «Ver catálogo» comparten renglón: son la misma pregunta hecha
   de dos maneras —sé cómo se llama / mostrame lo que hay—. */
.sedit-add-row { display: flex; align-items: center; gap: 0.375rem; }
.sedit-add-row input { flex: 1 1 auto; min-width: 0; }
.sedit-results {
    position: absolute; left: 0; right: 0; top: 2.25rem; z-index: 5;
    max-height: 12rem; overflow-y: auto; background: var(--surface, #fff);
}
/* El alta rápida de un producto cuelga debajo del buscador, no flota: es un
   formulario corto y taparía la lista que se acaba de mirar. */
.sedit-add .plangen-newprod-open { display: block; margin-top: 0.25rem; }
.sedit-add .plangen-newprod { margin-top: 0.25rem; }
.sedit-empty { margin: 0.25rem 0; }
.mess-handoff-body .form-label { font-size: 0.78rem; font-weight: 600; margin-bottom: 2px; }
.mess-handoff-order { font-size: 0.82rem; }
.mess-handoff-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
.mess-handoff-msg {
    font-size: 0.78rem; padding: 5px 8px; border-radius: var(--radius-sm);
    margin-bottom: var(--space-2); background: #fee2e2; color: #991b1b;
}
.mess-handoff-msg.is-error { background: #fee2e2; color: #991b1b; }
/* Un pin FUERA de todas las zonas de operación convertido en parada: el aviso
   (con el municipio del geocodificador) y la casilla que exige guardar. */
.map-outside-warn {
    margin: 0.5rem 0;
    padding: 0.5rem 0.625rem;
    border: 1px solid var(--warning);
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    font-size: 0.8rem;
}
.map-outside-warn[hidden] { display: none; }
.map-outside-warn-text { margin: 0 0 0.375rem; font-weight: 600; }
.map-outside-warn-check { display: flex; gap: 0.5rem; align-items: flex-start; cursor: pointer; }
.map-outside-warn-check input { margin-top: 0.125rem; }


/* ── Rider: mapa a pantalla completa ──────────────────────────────────────── */
.rider-map-screen { position: relative; }
.rider-map-canvas {
    width: 100%;
    height: calc(100dvh - 72px); /* deja lugar para el bottom-nav del rider */
    min-height: 320px;
}
.rider-map-warn {
    position: absolute;
    z-index: 2;
    top: 10px; left: 10px; right: 10px;
    background: rgba(180, 83, 9, 0.92);
    color: #fff;
    font-size: 0.78rem;
    padding: 6px 10px;
    border-radius: 8px;
    text-align: center;
}

/* ── Rider: mapa a pantalla completa + bottom sheet ────────────────────────
   Chrome de las vistas de mapa del rider (enfocada y general): el mapa ocupa
   toda la pantalla por debajo del bottom-nav y el panel vive en una hoja
   arrastrable encima (bottom-sheet.js). En >=900px la hoja deja de ser hoja y
   pasa a panel lateral fijo: en desktop hay alto de sobra y arrastrar con el
   mouse un panel que no estorba es fricción sin beneficio. */
.rider-map-shell {
    position: fixed;
    inset: 0;
    bottom: calc(var(--rider-nav-h) + env(safe-area-inset-bottom, 0px));  /* alto de .rider-bnav */
    /* `clip`, NO `hidden`: `hidden` recorta pero deja el elemento como
       contenedor de SCROLL programático, y el translateY de la hoja colapsada
       genera área desplazable real (medido: clientHeight 632 / scrollHeight
       1050). Cualquier scrollIntoView() o focus() dentro de la hoja —o del
       popup del mapa— corría el shell entero 140px: el mapa y la hoja subían y
       la hoja parecía trabada (el gesto funcionaba, pero se dibujaba fuera de
       sitio). `clip` no crea contenedor de scroll, así que eso no puede pasar.
       Si alguien lo "normaliza" a hidden, vuelve el bug. */
    overflow: clip;
    background: var(--bg, #f8f9fa);
}
.rider-map-canvas { position: absolute; inset: 0; }

/* Controles del mapa. "Mi ubicación" NO vive en la banda superior: va anclado al
   borde SUPERIOR DE LA HOJA, a la derecha, y viaja con ella (colapsada, media,
   máxima). Así nunca queda tapado ni compite con la banda de arriba, y en la
   altura máxima —donde antes desaparecía— sigue a mano.

   El anclaje es `--sheet-visible` (alto visible de la hoja, que bottom-sheet.js
   publica en el shell en cada frame del gesto) aplicado como translate: se anima
   solo `transform`. Durante el snap la variable salta al valor final, así que la
   transición se replica desde `.is-sheet-animating` con la MISMA curva de la
   hoja — si no, el botón llegaría antes que ella. */
.rider-map-fabs {
    position: absolute;
    right: var(--space-2);
    bottom: var(--space-2);
    z-index: 5;                 /* sobre la hoja: el borde de la hoja lo roza */
    display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
    transform: translate3d(0, calc(-1 * var(--sheet-visible, 0px)), 0);
}
.rider-map-shell.is-sheet-animating .rider-map-fabs {
    transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.rider-map-back {
    position: absolute;
    top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
    left: var(--space-2);
    z-index: 3;
}
.rider-map-fab {
    width: 44px; height: 44px;              /* área de tap mínima */
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid #e5e7eb; border-radius: 12px;
    background: rgba(255,255,255,0.94);
    color: #374151; font-size: 1.05rem; line-height: 1;
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;
}
.rider-map-fab:disabled { opacity: 0.45; cursor: default; }
.rider-map-fab.is-back { font-size: 1.15rem; }

/* ── Mi ubicación (geolocalización Fase A) ─────────────────────────────────
   Tres estados visibles en el propio botón, porque entre pedir el permiso y
   tener un fix pueden pasar varios segundos y sin señal el rider no sabe si
   tocó bien: buscando (parpadea), activa (relleno primario) y error. */
.rider-map-geo.is-locating { color: var(--primary); animation: geoPulse 1.2s ease-in-out infinite; }
.rider-map-geo.is-active {
    background: var(--primary); border-color: var(--primary); color: #fff;
}
.rider-map-geo.is-error { color: #b45309; border-color: #fcd34d; }
/* Ubicación encendida pero SIN trazado (el rider lo apagó con el mismo botón):
   estado intermedio — el punto azul sigue en el mapa, así que el botón no puede
   verse apagado del todo, pero tampoco relleno como cuando hay ruta. Contorno
   primario: "te estoy ubicando, sin la línea". */
.rider-map-geo.is-active.is-dot-only {
    background: rgba(255,255,255,0.94); color: var(--primary); border-color: var(--primary);
}

/* Ojo: filtro "solo lo que falta". Activo = relleno primario, igual que "mi
   ubicación": los dos son interruptores de la misma columna y encendido se lee
   igual en ambos. El icono (ojo / ojo tachado) lo cambia map-rider.js. */
.rider-map-eye.is-active {
    background: var(--primary); border-color: var(--primary); color: #fff;
}
@keyframes geoPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

/* Mensaje bajo los controles: no tapa el mapa (pointer-events: none) y solo el
   error de permiso queda fijo — el resto se va solo (lo maneja map-rider.js). */
.rider-geo-msg {
    position: absolute;
    z-index: 3;
    /* 52px de guarda a cada lado (= 44 del control + gap): a la izquierda por el
       botón de volver, a la derecha por simetría — así el aviso queda centrado
       sobre el mapa y puede envolver en varias líneas sin pisar nada. */
    top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
    left: calc(var(--space-2) + 52px); right: calc(var(--space-2) + 52px);
    margin: 0 auto; max-width: 24rem;
    pointer-events: none;
    background: rgba(17, 24, 39, 0.88); color: #fff;
    font-size: 0.78rem; line-height: 1.3;
    padding: 7px 10px; border-radius: 10px; text-align: center;
    box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
.rider-geo-msg.is-warn { background: rgba(180, 83, 9, 0.92); }
.rider-geo-msg.is-error { background: rgba(185, 28, 28, 0.94); }

/* ── Elegir el punto de encuentro de un traspaso sobre el mapa ──────────────
   Mientras se elige, el mapa ES la pantalla: la hoja se APAGA (no se cierra —
   conserva el rider elegido y lo escrito, igual que el modal del despacho, que
   tampoco se cierra) y esta barra ocupa su sitio abajo, que es donde está el
   pulgar. La hoja se apaga con `opacity`/`pointer-events` y NO con una
   transformación: su posición la escribe bottom-sheet.js en el `style=` del
   propio nodo, así que una regla de la hoja no puede moverla sin `!important`.

   Los controles que flotan sobre el mapa se van con ella: en este modo el mapa
   solo acepta un gesto, y un FAB en el camino es un toque perdido. */
.rider-map-shell.is-picking .rider-sheet,
.rider-map-shell.is-picking .rider-map-fabs,
.rider-map-shell.is-picking .rider-route-pill,
.rider-map-shell.is-picking .rider-handoff-pill {
    opacity: 0; pointer-events: none;
}
.rider-pick-hint {
    position: absolute; z-index: 6;
    left: var(--space-2); right: var(--space-2);
    bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
    display: flex; align-items: center; gap: var(--space-2);
    border-radius: 14px; padding: 10px 12px;
    background: rgba(76, 29, 149, 0.94); color: #fff;
    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.28);
    backdrop-filter: blur(6px);
}
.rider-pick-hint[hidden] { display: none; }
.rph-text { flex: 1 1 auto; font-size: 0.82rem; font-weight: 600; line-height: 1.3; }
.rph-cancel {
    flex: 0 0 auto; min-height: 40px; padding: 0 14px;
    border: 1px solid rgba(255, 255, 255, 0.55); border-radius: 10px;
    background: transparent; color: #fff; font-size: 0.8rem; font-weight: 700;
    cursor: pointer;
}

/* ── Píldora "traspaso entrante" ───────────────────────────────────────────────
   Aviso de que otro rider le propuso una entrega, sobre el mapa y en el mismo
   morado que todo el traspaso. Va en la banda superior CENTRAL (entre las dos
   columnas de controles, igual que el mensaje de ubicación) y lleva al Inicio,
   donde están los botones de aceptar/rechazar. Translúcida con blur: es un
   aviso, no una barra — el mapa se sigue leyendo detrás. */
.rider-handoff-pill {
    position: absolute;
    z-index: 5;                 /* por encima del mapa, por debajo de la hoja */
    top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
    left: 50%; transform: translateX(-50%);
    max-width: calc(100% - 116px);   /* 52px de guarda por columna de controles */
    display: flex; align-items: center; gap: 9px;
    padding: 7px 13px 7px 9px; border-radius: 999px;
    text-decoration: none;
    background: rgba(243, 238, 254, 0.86);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    border: 1px solid rgba(124, 58, 237, 0.28);
    box-shadow: 0 6px 20px rgba(124, 58, 237, 0.16);
}
.rider-handoff-pill .rhp-icon {
    flex: none; width: 24px; height: 24px; border-radius: 50%;
    background: rgba(124, 58, 237, 0.92); color: #fff;
    display: flex; align-items: center; justify-content: center; font-size: 0.78rem;
}
.rider-handoff-pill .rhp-text {
    font-size: 0.8rem; font-weight: 800; color: #6d28d9;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rider-handoff-pill .rhp-ttl {
    flex: none; font-size: 0.74rem; font-weight: 800; color: #6d28d9;
    background: rgba(124, 58, 237, 0.14); border-radius: 999px; padding: 2px 8px;
}
/* Con píldora presente, el mensaje de ubicación baja para no solaparse (los dos
   viven en la misma banda; la píldora es persistente y el mensaje, pasajero). */
.rider-handoff-pill ~ .rider-geo-msg {
    top: calc(var(--space-2) + 46px + env(safe-area-inset-top, 0px));
}

/* ── Píldora "cuánto falta" (trazado hasta la parada, Fase B) ─────────────────
   Distancia y tiempo estimados en formato CORTO ("4,2 km · 18 min"), sobre el
   mapa y arriba a la izquierda. No vive en la hoja a propósito: es el dato que
   se mira de reojo con el móvil en el manillar, y ahí abajo quedaba debajo del
   resumen —desapareciendo al colapsar la hoja— compitiendo con la lista de
   paradas, que es lo que se opera. Separada de las esquinas (no pegada al borde)
   para no pelear con el redondeo de la pantalla ni con la barra de estado.
   Tocarla encuadra mi posición + la ruta activa entera. */
.rider-route-pill {
    position: absolute;
    z-index: 5;
    top: calc(var(--space-2) + 6px + env(safe-area-inset-top, 0px));
    left: calc(var(--space-2) + 6px);
    display: flex; align-items: center; gap: 7px;
    padding: 7px 12px; border-radius: 999px;
    border: 1px solid rgba(29, 78, 216, 0.22);
    background: rgba(255, 255, 255, 0.9);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    box-shadow: 0 4px 14px rgba(17, 24, 39, 0.14);
    color: #1d4ed8;                     /* el azul del trazado que resume */
    font-size: 0.82rem; font-weight: 800; line-height: 1;
    white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
    cursor: pointer;
}
.rider-route-pill .bi { font-size: 0.9rem; opacity: 0.8; }
/* Vista enfocada: el botón de volver ocupa la esquina, la píldora se corre. */
.rider-route-pill.is-with-back { left: calc(var(--space-2) + 52px); }
/* Con píldora de traspaso (centrada en la misma banda) baja una fila, igual que
   el mensaje de ubicación. */
.rider-handoff-pill ~ .rider-route-pill {
    top: calc(var(--space-2) + 46px + env(safe-area-inset-top, 0px));
}

/* ── La hoja ──────────────────────────────────────────────────────────────── */
.rider-sheet {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    z-index: 4;
    display: flex; flex-direction: column;
    background: #fff;
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -6px 24px rgba(0,0,0,0.16);
    will-change: transform;                 /* solo transform, nunca height/top */
    transform: translate3d(0, 0, 0);
    touch-action: pan-y;
}
.rider-sheet.is-animating { transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1); }

/* Asa: barrita fina. Su alto bajó de 44 a 30px porque ya NO es la única forma de
   mover la hoja — desde 2026-08-05 se arrastra desde la cabecera y el cuerpo
   (bottom-sheet.js), así que el asa es la señal de que la hoja se mueve y el
   atajo del tap, no el único blanco. Esos 14px son mapa: entran enteros en la
   altura colapsada, que se mide como asa + cabecera + pie.
   touch-action:none SOLO acá — así el tap-toggle no compite con nada. */
.rider-sheet-handle {
    flex: 0 0 auto;
    height: 30px; width: 100%;
    display: flex; align-items: center; justify-content: center;
    border: 0; background: transparent; padding: 0;
    touch-action: none;
    cursor: grab;
    -webkit-tap-highlight-color: transparent;
}
.rider-sheet-handle:active { cursor: grabbing; }
.rider-sheet-grip {
    display: block; width: 42px; height: 5px; border-radius: 3px; background: #d1d5db;
}
.rider-sheet-handle:active .rider-sheet-grip { background: #9ca3af; }

/* Línea de status compacto: lo ÚNICO que se lee con la hoja colapsada, así que
   nunca se corta ni se envuelve en dos líneas. */
.rider-sheet-peek {
    flex: 0 0 auto;
    display: flex; align-items: center; gap: 8px;
    padding: 0 var(--space-3) 8px;
    /* La cabecera también arrastra la hoja (bottom-sheet.js) y no scrollea nada:
       con `pan-y` heredado, el navegador podía interpretar el gesto como un
       scroll de la página y cancelar el arrastre a mitad de camino. */
    touch-action: none;
    font-size: 0.86rem; color: #111827; font-weight: 600;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rider-sheet-peek .peek-sub {
    font-weight: 400; color: #6b7280; overflow: hidden; text-overflow: ellipsis;
}
.rider-sheet-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;           /* el scroll no se escapa al mapa */
    -webkit-overflow-scrolling: touch;
    padding: 0 var(--space-3) var(--space-3);
}

/* Contenido propio de la hoja en la vista ENFOCADA */
.rider-sheet-next {
    display: flex; align-items: center; gap: 8px;
    background: #eff6ff; color: #1e3a8a; border: 1px solid #bfdbfe;
    border-radius: 10px; padding: 8px 10px; font-size: 0.84rem; margin-bottom: 10px;
}
.rider-sheet-next span { flex: 1; min-width: 0; }
/* Trazado hasta la parada en curso (geolocalización Fase B): lo que hay que LEER
   y decidir — la parada no está ubicada, ORS no contestó. Los km y el tiempo se
   fueron a la píldora sobre el mapa (.rider-route-pill): son un dato de un
   vistazo, no algo que se lee dentro de la hoja. Por eso esta caja solo aparece
   en 'is-warn': el rider tiene que enterarse de que no hay ruta, no quedarse
   esperándola. */
.rider-route-me {
    display: flex; align-items: center; gap: 8px;
    background: #fffbeb; color: #92400e; border: 1px solid #fde68a;
    border-radius: 10px; padding: 8px 10px; font-size: 0.84rem; margin-bottom: 10px;
}
.rider-route-me .rrm-text { flex: 1; min-width: 0; }

.rider-sheet-actions { margin-bottom: 12px; }
.rider-sheet-actions .aoc-action-btn { width: 100%; }

/* Parada recién completada (?done=): pulso de ~2s. Es feedback, no navegación:
   el foco NO avanza solo a la parada siguiente. */
.map-stop-row.is-just-done {
    animation: mapStopDone 2s ease-out 1;
}
@keyframes mapStopDone {
    0%   { box-shadow: 0 0 0 0 rgba(34,197,94,0.55); background: #ecfdf5; }
    70%  { box-shadow: 0 0 0 10px rgba(34,197,94,0); background: #ecfdf5; }
    100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
}

@media (min-width: 900px) {
    /* Panel lateral fijo: sin gestos, sin transform (bottom-sheet.js lo detecta
       con la misma media query y se desactiva). */
    .rider-sheet {
        top: 0; left: auto; right: 0; bottom: 0;
        width: 380px;
        height: auto !important;
        transform: none !important;
        border-radius: 0;
        box-shadow: -4px 0 18px rgba(0,0,0,0.10);
    }
    .rider-sheet-handle { display: none; }
    .rider-sheet-peek { padding-top: var(--space-3); }
    /* Panel fijo: la hoja no se traslada, así que el pie tampoco. */
    .rider-sheet-foot { transform: none !important; }
    .rider-map-fabs { right: calc(380px + var(--space-2)); }
    .rider-geo-msg { right: calc(380px + var(--space-2) + 52px); }
    /* El panel lateral ocupa 380px a la derecha: la píldora se centra sobre el
       MAPA, no sobre el shell entero (si no, queda medio tapada por el panel). */
    .rider-handoff-pill { left: calc(50% - 190px); }
}

/* ── Popup de parada (compartido) ─────────────────────────────────────────── */
.maplibregl-popup-content { border-radius: 10px; padding: 0; overflow: hidden; }
.mess-map-popup { font-size: 0.82rem; color: #1f2937; }
.mess-pop-head {
    display: flex; align-items: center; gap: 6px;
    padding: 8px 10px;
    border-left: 4px solid var(--primary);
    background: #f9fafb;
    font-size: 0.9rem;
}
.mess-pop-status {
    margin-left: auto;
    font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
    color: var(--gray-500);
}
.mess-pop-row { padding: 4px 10px 0; }
.mess-pop-sub { color: var(--gray-500); font-size: 0.75rem; padding-bottom: 8px; }
/* Selector de pedido cuando varios coinciden en el mismo punto (modo "crear ruta") */
.mess-pop-pick {
    display: flex; align-items: flex-start; gap: 7px; cursor: pointer;
    padding: 6px 10px; font-size: 0.8rem; border-top: 1px solid var(--gray-200);
}
.mess-pop-pick:hover { background: var(--gray-100); }
.mess-pop-pick input { margin-top: 2px; flex-shrink: 0; }
.mess-pop-pick .mess-pop-sub { padding: 0; }

/* Tarjeta del popup teñida por TIPO de parada (mismo código de color que el
   halo del mapa y que el borde de la fila del panel): recogida verde, entrega
   ámbar, traspaso morado. Tinte translúcido sobre fondo claro + borde fino del
   color, para que el texto siga legible sobre el mapa.
   Las reglas cuelgan de la clase común `.mess-pop-stop` y cada variante solo
   aporta sus tres variables — así sumar un tipo nuevo es una línea, no repetir
   el bloque entero (ver popupClassFor en map-common.js). */
.mess-pop-stop .maplibregl-popup-content {
    border: 1px solid var(--pop-accent);
    background: var(--pop-bg);
    /* Translúcido de verdad: el mapa se ve detrás. El blur + la sombra de texto
       de abajo son los que mantienen legible el contenido. */
    backdrop-filter: blur(3px) saturate(1.1);
    -webkit-backdrop-filter: blur(3px) saturate(1.1);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
}
/* Tinte claro y semitransparente + tinta CASI NEGRA teñida del color: mantiene
   la transparencia pero deja el contraste texto/fondo bien por encima de 7:1. */
.mess-pop-recogida { --pop-accent: #15803D; --pop-bg: rgba(226, 252, 235, 0.74); --pop-ink: #04240F; }
.mess-pop-entrega { --pop-accent: #A16207; --pop-bg: rgba(254, 249, 205, 0.74); --pop-ink: #2E1704; }
.mess-pop-traspaso { --pop-accent: #6D28D9; --pop-bg: rgba(240, 233, 255, 0.74); --pop-ink: #1E0A3C; }
.mess-pop-stop .mess-map-popup {
    color: var(--pop-ink);
    font-weight: 500;
    /* Halo claro mínimo: despega el texto si debajo cae una zona oscura del
       mapa, sin llegar a verse borroso. */
    text-shadow: 0 0 3px rgba(255, 255, 255, 0.9);
}
.mess-pop-stop .mess-pop-head {
    background: color-mix(in srgb, var(--pop-accent) 22%, transparent);
    border-left-color: var(--pop-accent);
    color: var(--pop-ink); font-weight: 700;
}
/* Subtexto: opaco (mezclado con blanco, NO con transparente) para que no se
   desvanezca sobre el mapa. */
.mess-pop-stop .mess-pop-sub { color: color-mix(in srgb, var(--pop-ink) 80%, #ffffff); }
/* Botón "Traspasar a otro rider": relleno sólido oscuro, texto blanco — sobre
   el tinte de color el contorno en color primario no se leía. */
.mess-pop-stop .mess-pop-handoff {
    background: var(--pop-ink);
    border-color: var(--pop-ink);
    color: #fff;
    text-shadow: none;
    font-weight: 600;
}
.mess-pop-stop .mess-pop-handoff:hover {
    background: var(--pop-accent);
    border-color: var(--pop-accent);
    color: #fff;
}
.mess-pop-stop .mess-pop-rt {
    color: var(--pop-ink);
    background: color-mix(in srgb, var(--pop-accent) 25%, transparent);
}
.mess-pop-stop .maplibregl-popup-tip { border-top-color: var(--pop-bg); border-bottom-color: var(--pop-bg); }
.mess-pop-stop .maplibregl-popup-close-button { color: var(--pop-ink); }

/* ══════════════════════════════════════════════════════════════════════════
   RIDER · hoja del mapa: contextos LISTA y PARADA (rediseño 2026-07-25)

   La hoja muestra UNA de dos cosas y el nivel al que está abierta decide
   cuánto de esa cosa se ve:

     data-context="lista"   resumen (colapsada) → lista de paradas.
     data-context="parada"  cabecera + acciones (colapsada) → detalle (medio)
                            → notas del pedido y del traspaso (completo).

   `data-level` lo escribe bottom-sheet.js al asentarse la hoja (0/1/2). Es el
   único mecanismo de "qué entra en cada altura": nada se calcula en JS por
   cada bloque, así que agregar una sección nueva es ponerle la clase de su
   nivel y nada más.
   ══════════════════════════════════════════════════════════════════════════ */
.rider-sheet[data-level="0"] .sheet-lvl-mid,
.rider-sheet[data-level="0"] .sheet-lvl-full,
.rider-sheet[data-level="1"] .sheet-lvl-full { display: none; }

/* `data-level` se escribe EN VIVO (ver bottom-sheet.js): el bloque aparece
   mientras la hoja sube, no al terminar de subir. Entra con un fundido corto —
   sin él, el bloque se materializa de golpe a mitad del recorrido y se lee como
   un salto; con él, la hoja y su contenido son el mismo movimiento. Es más corto
   que la transición de la hoja (0.26s) a propósito: tiene que estar entero antes
   de que la hoja se detenga. */
.rider-sheet .sheet-lvl-mid,
.rider-sheet .sheet-lvl-full { animation: sheetLvlIn 0.18s ease-out both; }
@keyframes sheetLvlIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

/* Acción en curso (una acción de parada viaja por fetch: ver map-rider.js).
   Hilo indeterminado en el borde superior de la hoja + el pie apagado: el rider
   tocó "Entregado" y tiene que ver que se está haciendo algo. Antes esto era una
   recarga de página entera —el mapa se ponía en blanco y volvía—, así que el
   "está pasando algo" lo daba el parpadeo; sin recarga hay que decirlo. */
.rider-sheet::after {
    content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
    border-radius: 18px 18px 0 0;
    background: linear-gradient(90deg, transparent, var(--primary), transparent);
    opacity: 0; pointer-events: none;
    transition: opacity 0.15s;
}
.rider-sheet.is-busy::after { opacity: 1; animation: sheetBusy 1.1s linear infinite; }
@keyframes sheetBusy {
    from { transform: translateX(-100%); }
    to   { transform: translateX(100%); }
}
.rider-sheet.is-busy .rider-sheet-foot { opacity: 0.55; pointer-events: none; }
.rider-sheet-foot { transition: opacity 0.15s; }

/* Cambio de parada (tocar otra, o el avance automático al cerrar una): la
   tarjeta nueva entra con el mismo fundido. Sin esto el reemplazo es un corte
   seco y se lee como el parpadeo de recarga que esta tanda vino a sacar. */
.sheet-stop-head.is-swap-in,
.sheet-stop-body.is-swap-in { animation: sheetLvlIn 0.2s ease-out both; }
/* El PIE solo se funde, nunca se desplaza: vive contra-trasladado
   (`--sheet-shift`, ver .rider-sheet-foot) y una animación de `transform` le
   pisaría esa traslación — los botones se irían fuera de pantalla durante la
   animación, que es exactamente el bug que ese contra-traslado arregla. */
.sheet-stop-foot.is-swap-in { animation: sheetFadeIn 0.2s ease-out both; }
@keyframes sheetFadeIn { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
    .rider-sheet .sheet-lvl-mid,
    .rider-sheet .sheet-lvl-full,
    .sheet-stop-head.is-swap-in,
    .sheet-stop-body.is-swap-in,
    .sheet-stop-foot.is-swap-in { animation: none; }
    /* La transición de la HOJA se queda: `settle()` (y con él los insets del
       mapa) cuelga de su `transitionend`. */
    .rider-sheet.is-busy::after { animation: none; }
}

/* Pie: las acciones de la parada abierta. Vive FUERA del cuerpo scrolleable a
   propósito — con la hoja colapsada, los botones tienen que verse igual (el
   snap "auto" de bottom-sheet.js mide asa + cabecera + este pie).

   Y va CONTRA-TRASLADADO (2026-07-26). La hoja tiene siempre el alto del snap
   máximo y se colapsa empujándola hacia abajo con translateY: su borde inferior
   queda FUERA de la pantalla, así que el pie —último hijo del flex— se iba con
   él y los botones no se veían en ninguna altura menor que la máxima (era el bug
   reportado: solo la cabecera y una franja blanca). Deshaciendo esa traslación
   (`--sheet-shift`, que bottom-sheet.js escribe en cada frame del gesto), el pie
   se queda pegado al borde VISIBLE de la hoja a cualquier altura, y el detalle
   scrollea EN MEDIO, entre la cabecera y los botones.

   La transición replica exactamente la de la hoja: si no, al animar el snap el
   pie se adelantaría o quedaría atrás y se vería flotar. */
.rider-sheet-foot {
    flex: 0 0 auto; padding: 0;
    position: relative; z-index: 2;
    background: #fff;
    transform: translate3d(0, calc(-1 * var(--sheet-shift, 0px)), 0);
}
.rider-sheet.is-animating .rider-sheet-foot {
    transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* El cuerpo reserva el alto del pie: si no, con la hoja a media altura el final
   del detalle quedaría siempre debajo de los botones, sin forma de leerlo. */
.rider-sheet[data-context="parada"] .rider-sheet-body {
    padding-bottom: calc(var(--space-3) + var(--sheet-foot-h, 0px));
}
/* El padding lo pone la tarjeta, no el contenedor: en el contexto LISTA (y en
   una parada ya resuelta) no hay pie visible, y un contenedor con padding
   dejaría una franja blanca bajo la hoja colapsada. */
.sheet-stop-foot { padding: 0 var(--space-3) var(--space-3); }
.rider-sheet-foot .aoc-action-btn,
.rider-sheet-foot .status-action-btn { width: 100%; }

/* ── Contexto LISTA: el resumen de la ruta ─────────────────────────────── */
.sheet-lista-peek { display: flex; flex-direction: column; gap: 2px; }
/* El peek era de una sola línea (status compacto): el contenido de los dos
   contextos nuevos ocupa varias, así que anulan el nowrap heredado. */
.sheet-lista-peek, .sheet-stop-head, .sheet-grupo-head { width: 100%; white-space: normal; }
/* Nombre del recorrido y cuántas paradas quedan comparten LÍNEA: son la misma
   frase ("tu ruta, 12 paradas") y apiladas costaban una línea entera de la
   altura colapsada, que es la que decide cuánto mapa se ve. */
.slp-top { display: flex; align-items: baseline; gap: 8px; }
.slp-title {
    flex: 1; min-width: 0;
    font-size: 1.02rem; font-weight: 800; color: #0f172a;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.slp-count { flex: none; font-size: 0.8rem; font-weight: 600; color: #64748b; }
.slp-now {
    display: flex; align-items: center; gap: 7px;
    font-size: 0.82rem; font-weight: 700; color: #64748b;
}
.slp-now::before {
    content: ""; width: 10px; height: 10px; border-radius: 50%;
    background: currentColor; flex-shrink: 0;
}
.slp-now.is-recogida { color: #16A34A; }
.slp-now.is-entrega { color: #B45309; }
.slp-now.is-traspaso { color: #6D28D9; }

/* ── Contexto PARADA: cabecera ─────────────────────────────────────────── */
.sheet-stop-head { display: flex; align-items: flex-start; gap: 10px; width: 100%; }
.sheet-stop-head .ssh-main { flex: 1; min-width: 0; }
.ssh-addr {
    font-size: 1.16rem; font-weight: 800; line-height: 1.18; color: #1e293b;
    overflow-wrap: anywhere; white-space: normal;
}
/* Una parada ya resuelta no compite por la atención: su dirección es un dato,
   no una instrucción. */
.sheet-stop-head.is-done .ssh-addr { font-size: 1.05rem; color: #475569; }
.ssh-meta {
    display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
    margin-top: 5px; white-space: normal;
}
.ssh-type { font-size: 0.82rem; font-weight: 800; letter-spacing: 0.04em; }
.sheet-stop-head.is-recogida .ssh-type { color: #16A34A; }
.sheet-stop-head.is-entrega .ssh-type { color: #B45309; }
.sheet-stop-head.is-traspaso .ssh-type { color: #6D28D9; }
.ssh-order { font-size: 0.82rem; font-weight: 600; color: #64748b; }
/* El CP y la ciudad cierran la línea de la calle, apagados y sin cortar: son
   contexto, no el dato que se busca. */
/* Segundo nivel de la dirección: local, planta, puerta. Solo se dibuja cuando
   existe (ver la cabecera en rider/_stop_sheet.html), así que no reserva
   espacio ni deja un hueco cuando la parada no lo tiene. */
.ssh-unit {
    display: flex; align-items: center; gap: 6px; margin-top: 3px;
    font-size: 0.92rem; font-weight: 700; color: #334155;
    overflow-wrap: anywhere;
}
.ssh-unit i { color: #94a3b8; font-size: 0.9em; flex: 0 0 auto; }
/* La franja comprometida: es lo único de la cabecera que marca un límite, así
   que se distingue del resto de la meta con superficie propia — pero sin
   competir con la dirección, que sigue mandando. */
.ssh-win {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 9px; border-radius: 20px;
    background: #eef2ff; color: #3730a3; border: 1px solid #c7d2fe;
    font-size: 0.8rem; font-weight: 800; white-space: nowrap;
}
.ssh-win i { color: #6366f1; font-size: 0.9em; }
.ssh-close {
    flex: 0 0 auto; width: 40px; height: 40px; border: 0; border-radius: 12px;
    background: #f1f3f7; color: #475569; font-size: 1rem; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    -webkit-tap-highlight-color: transparent;
}

/* ── Contexto GRUPO: 3+ paradas en el mismo punto ──────────────────────────
   El marcador agrupado no puede decir cuál es cuál (y si mezcla tipos va en
   gris), así que esta lista es donde se distingue: cada fila lleva el color y
   el número de SU parada, y al tocarla se entra al contexto PARADA normal. */
.sheet-grupo-head { display: flex; align-items: flex-start; gap: 10px; width: 100%; }
.sheet-grupo-head .sgh-main { flex: 1; min-width: 0; }
.sgh-title { font-size: 1.15rem; font-weight: 800; color: #0f172a; }
.sgh-sub { font-size: 0.85rem; color: #64748b; margin-top: 3px; }
.sheet-group-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sheet-group-row {
    display: flex; align-items: center; gap: 10px; cursor: pointer;
    background: #fff; border: 1px solid #e6e9ef; border-left-width: 4px;
    border-radius: 12px; padding: 10px 11px; min-height: 56px;
    -webkit-tap-highlight-color: transparent;
}
.sheet-group-row.is-recogida { border-left-color: #22C55E; }
.sheet-group-row.is-entrega { border-left-color: #F59E0B; }
.sheet-group-row.is-traspaso { border-left-color: #7C3AED; }
.sheet-group-row.is-done { opacity: 0.62; }
.sheet-group-row .sgr-body { flex: 1; min-width: 0; }
.sgr-l1 { display: flex; align-items: center; gap: 8px; }
.sheet-group-row.is-recogida .map-stop-kind { color: #16A34A; }
.sheet-group-row.is-entrega .map-stop-kind { color: #B45309; }
.sheet-group-row.is-traspaso .map-stop-kind { color: #6D28D9; }
.sgr-go { flex: 0 0 auto; color: #94a3b8; font-size: 0.9rem; }

/* ── Contexto LOTE: armar un multi-traspaso (fase 2) ───────────────────────
   La barra del lote es el PEEK de la hoja con otro contenido, no un componente
   nuevo: cuántas llevás, seguir y salir. Va en MORADO porque ése es el color
   del traspaso en toda la app (`--stop-handoff`), así que el modo se reconoce
   sin leer una palabra. Los dos botones son de DEDO (44px): son lo único que
   se toca acá. */
/* Se lee de arriba abajo —qué llevás, después qué hacer con eso— y NO en una
   fila: dos botones de dedo a la derecha se comen el ancho de un teléfono y
   dejan el título y el aviso partidos o directamente fuera, que es justo lo
   único que hay que leer acá. */
.sheet-lote-head {
    display: flex; flex-direction: column; align-items: stretch; gap: 10px;
    width: 100%;
    border-left: 4px solid var(--stop-handoff, #7C3AED);
    background: #f7f3ff; border-radius: 12px; padding: 9px 11px;
}
.sheet-lote-head .slt-main { min-width: 0; }
.slt-title {
    display: flex; align-items: center; gap: 7px;
    font-size: 1.02rem; font-weight: 800; color: #5b21b6;
}
.slt-sub { font-size: 0.82rem; color: #6b5b95; margin-top: 2px; }
.slt-actions { display: flex; align-items: center; gap: 8px; }
/* Los dos reparten el ancho: debajo del texto, media barra cada uno se toca
   sin apuntar. */
.slt-actions > .slt-cancel, .slt-actions > .slt-go { flex: 1 1 0; }
.slt-cancel, .slt-go {
    min-height: 44px; padding: 0 14px; border-radius: 11px;
    font-size: 0.88rem; font-weight: 700; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.slt-cancel { background: #fff; border: 1px solid #ddd6fe; color: #6d28d9; }
.slt-go { background: var(--stop-handoff, #7C3AED); border: 1px solid transparent; color: #fff; }
.slt-go:disabled { opacity: 0.5; cursor: default; }
/* Soltar una elegida sin volver al mapa. Área de toque completa: es una ✕ en
   una fila que ya se lee entera. */
.sheet-lote-drop {
    flex: 0 0 auto; width: 40px; height: 40px; border-radius: 10px;
    display: inline-flex; align-items: center; justify-content: center;
    background: #fff; border: 1px solid #e6e9ef; color: #6d28d9; cursor: pointer;
}
/* El FAB del multi-traspaso, encendido mientras se elige. */
.rider-map-lote.is-on {
    background: var(--stop-handoff, #7C3AED);
    border-color: var(--stop-handoff, #7C3AED); color: #fff;
}

/* ── Contexto PARADA: detalle (nivel medio) ───────────────────────────── */
.sheet-stop-detail {
    background: #f6f7fa; border-radius: 14px; padding: 12px 13px;
    display: flex; flex-direction: column; gap: 11px; margin-bottom: 10px;
}
.ssd-label {
    font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em;
    text-transform: uppercase; color: #94a3b8;
}
.ssd-value { font-size: 0.95rem; font-weight: 600; color: #1e293b; margin-top: 2px; }
.ssd-muted { font-weight: 500; color: #94a3b8; }
.ssd-contact {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; flex-wrap: wrap;
}
.ssd-tel {
    display: inline-flex; align-items: center; gap: 7px;
    background: #eaf2fc; color: #2563EB; font-weight: 700; font-size: 0.86rem;
    padding: 9px 13px; border-radius: 10px; text-decoration: none; min-height: 40px;
}
.ssd-items { margin: 3px 0 0; padding-left: 18px; font-size: 0.9rem; color: #334155; }
.ssd-items li { margin-bottom: 3px; }
.ssd-belongs { font-size: 0.8rem; color: #64748b; line-height: 1.9; }
/* Un pedido que se carga en VARIAS tiendas: una línea, no un párrafo. Morado
   —el mismo de los avisos que no son una falta (`.aoc-hint`)— porque informa;
   el ámbar está reservado para lo que falta resolver. El "x/y" es lo único que
   se lee de reojo, así que va en negrita y con su propio fondo. */
.ssd-multi {
    display: flex; align-items: center; gap: 0.375rem;
    background: #f5f3ff; color: #5b21b6;
    border-radius: 10px; padding: 0.4375rem 0.625rem;
    font-size: 0.8rem; line-height: 1.3;
}
.ssd-multi-xy {
    margin-left: auto; flex: none;
    background: #5b21b6; color: #f5f3ff;
    border-radius: 999px; padding: 0.0625rem 0.5rem; font-size: 0.78rem;
}
/* Número de una parada HERMANA del pedido dentro de una frase ("Recogida en la
   parada ③"). Círculo del color de su TIPO —verde recogida, ámbar entrega—, el
   mismo par que usan los marcadores y las filas del panel, para que el número
   se lea como "ese pin de allá" y no como un dato suelto. Los rellena
   map-rider.js desde el feed (ver setSeqLabels). */
.seq-chip {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 21px; height: 21px; padding: 0 6px; border-radius: 999px;
    font-size: 0.74rem; font-weight: 800; line-height: 1;
    vertical-align: -3px;
}
.seq-chip--pickup { background: var(--stop-pickup); color: #fff; }
.seq-chip--delivery { background: var(--stop-drop); color: #fff; }
/* Varias entregas: los círculos se separan y bajan de línea si no caben. */
.seq-chip-list { display: inline-flex; flex-wrap: wrap; gap: 5px; vertical-align: -3px; }
/* Parada ya resuelta: el acuse, con su hora */
.sheet-done-mark {
    display: flex; align-items: center; gap: 11px;
    background: #d1fae5; border-radius: 14px; padding: 11px 13px; margin-bottom: 10px;
}
.sheet-done-mark.is-fail { background: #fee2e2; }
.sheet-done-mark.is-handoff { background: #f3eefe; }
.sdm-icon {
    flex: 0 0 auto; width: 36px; height: 36px; border-radius: 50%;
    background: #fff; color: #16A34A; font-size: 1.1rem;
    display: flex; align-items: center; justify-content: center;
}
.sheet-done-mark.is-fail .sdm-icon { color: #dc2626; }
.sheet-done-mark.is-handoff .sdm-icon { color: #6D28D9; }
.sdm-title { font-size: 0.95rem; font-weight: 800; color: #065f46; }
.sheet-done-mark.is-fail .sdm-title { color: #991b1b; }
.sheet-done-mark.is-handoff .sdm-title { color: #5b21b6; }
.sdm-sub { font-size: 0.82rem; font-weight: 600; color: #64748b; }

/* Modo CONSULTA: por qué esta parada no tiene botones (recorrido de otro día o
   ya finalizado). Neutro a propósito — no es un error ni algo que resolver. */
.sheet-readonly-note {
    display: flex; align-items: center; gap: 9px;
    background: #f1f5f9; border-radius: 14px; padding: 10px 13px; margin-bottom: 10px;
    font-size: 0.85rem; font-weight: 700; color: #475569;
}
.sheet-readonly-note .bi { font-size: 1rem; color: #94a3b8; }
/* Chip "solo consulta" en la cabecera de una ruta del panel del rider. */
.map-route-ro {
    flex: 0 0 auto; font-size: 0.62rem; font-weight: 800; letter-spacing: 0.06em;
    text-transform: uppercase; color: #64748b; background: #e2e8f0;
    border-radius: 999px; padding: 2px 0.4375rem;
}

/* ── Contexto PARADA: notas y traspaso (nivel completo) ───────────────── */
.sheet-stop-notes {
    border: 1px dashed #dfe3ea; border-radius: 14px; padding: 12px 13px;
    display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px;
}
.ssn-title, .sho-title {
    font-size: 0.7rem; font-weight: 800; letter-spacing: 0.09em;
    text-transform: uppercase; color: #94a3b8;
}
.ssn-text { font-size: 0.9rem; color: #475569; line-height: 1.45; margin-top: 3px; }
.ssn-special {
    display: flex; align-items: flex-start; gap: 8px;
    background: #FEF3C7; border: 1px solid #FCD34D; color: #92400E;
    border-radius: 10px; padding: 9px 11px; font-size: 0.85rem; font-weight: 600;
}
.sheet-stop-handoff {
    background: #F3EEFE; border: 1px solid #ddd6fe; border-radius: 14px;
    padding: 12px 13px; display: flex; flex-direction: column; gap: 7px;
    margin-bottom: 10px;
}
.sheet-stop-handoff .sho-title { color: #7C3AED; }
.sho-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.sho-row span { font-size: 0.82rem; font-weight: 700; color: #7c6bb0; flex: 0 0 auto; }
.sho-row b { font-size: 0.82rem; font-weight: 700; color: #3b2a63; text-align: right; }
/* QUÉ entregas cambian de manos en el encuentro. No es una fila más de la ficha
   (`sho-row`, etiqueta a la izquierda y dato a la derecha): son direcciones, se
   leen desde la bici y pueden ser varias, así que van a lo ancho y apiladas. */
.sho-lines { display: flex; flex-direction: column; gap: 5px; }
.sho-lines-t { font-size: 0.82rem; font-weight: 700; color: #7c6bb0; }
.sho-lines ul { list-style: none; margin: 0; padding: 0; display: flex;
    flex-direction: column; gap: 5px; }
.sho-lines li {
    background: #fff; border: 1px solid #e3dbfb; border-radius: 10px;
    padding: 7px 9px; display: flex; flex-direction: column; gap: 1px;
}
.shol-addr { font-size: 0.86rem; font-weight: 700; color: #3b2a63; }
.shol-unit, .shol-emp { font-size: 0.76rem; font-weight: 600; color: #7c6bb0; }
/* Qué toca hacer en el encuentro y si el otro rider ya llegó. Vive en el CUERPO
   de la hoja porque en el pie ocupaba una fila entera de `.sheet-actions` y
   empujaba "Ir" a un tercer renglón (ver `_stop_actions.html`). */
.sho-state {
    margin: 2px 0 0; padding: 8px 10px; border-radius: 10px;
    background: #fff; border: 1px solid #e3dbfb;
    font-size: 0.8rem; font-weight: 600; color: #3b2a63;
}
.sho-state.is-ready { border-color: #86efac; background: #f0fdf4; color: #166534; }

/* ── En una RECOGIDA: lo que de esto se lo lleva OTRO rider ─────────────────
   Va debajo de "Se entrega en" y con el morado del traspaso: son dos preguntas
   distintas —lo que entrega él y lo que cede— y mezclarlas escondía la segunda,
   dejando la recogida como si cargara solo lo suyo. */
.ssd-handoff {
    margin-top: 6px; padding: 8px 10px; border-radius: 12px;
    background: #F3EEFE; border: 1px solid #ddd6fe;
    display: flex; flex-direction: column; gap: 5px;
}
.ssd-handoff-head {
    display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
    font-size: 0.8rem; font-weight: 700; color: #6d28d9;
}
.ssd-handoff-more > summary {
    list-style: none; cursor: pointer; font-size: 0.78rem; font-weight: 700;
    color: #7c6bb0; display: flex; align-items: center; gap: 5px;
}
.ssd-handoff-more > summary::-webkit-details-marker { display: none; }
.ssd-handoff-more[open] > summary i { transform: rotate(180deg); }
.ssd-handoff-more ul {
    list-style: none; margin: 5px 0 0; padding: 0;
    display: flex; flex-direction: column; gap: 4px;
}
.ssd-handoff-more li {
    background: #fff; border: 1px solid #e3dbfb; border-radius: 10px;
    padding: 6px 9px; display: flex; flex-direction: column; gap: 1px;
}
.ssd-handoff-note { margin: 0; font-size: 0.76rem; font-weight: 600; color: #7c6bb0; }

/* ── Contexto PARADA: imágenes y nota (nivel completo) ────────────────────
   Una sola fila que envuelve: las dos HERRAMIENTAS (nota y cámara) y, a
   continuación, las miniaturas — primero las heredadas (con candado: se abren,
   no se tocan) y después las de esta parada (con ✕ mientras siga abierta).
   Herramienta y miniatura miden lo mismo y comparten radio: la fila se lee como
   una tira de fichas y no como "botones + galería". La nota, cuando existe, va
   siempre DEBAJO. */
.sheet-stop-media {
    border: 1px dashed #dfe3ea; border-radius: 14px; padding: 12px 13px;
    display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px;
}
.ssm-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ssm-cam-form, .ssm-del-form { display: contents; }
.ssm-tool {
    flex: 0 0 auto; width: 54px; height: 54px; border-radius: 14px;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid #dbe6f6; background: #f4f8fe; color: var(--primary, #4A90E2);
    font-size: 1.25rem; cursor: pointer; position: relative;
    -webkit-tap-highlight-color: transparent;
}
.ssm-tool:hover { background: #e8f1fd; }
/* Punto azul: ya hay una nota escrita (el botón pasa a "editar" sin cambiar de
   sitio ni de tamaño). */
.ssm-tool.has-content::after {
    content: ''; position: absolute; top: 7px; right: 7px;
    width: 8px; height: 8px; border-radius: 50%; background: var(--primary, #4A90E2);
}
.ssm-thumb {
    flex: 0 0 auto; position: relative; width: 54px; height: 54px;
}
.ssm-thumb-btn {
    width: 100%; height: 100%; padding: 0; border-radius: 14px; overflow: hidden;
    border: 1px solid #e5e7eb; background: #f1f5f9; cursor: pointer; display: block;
    -webkit-tap-highlight-color: transparent;
}
.ssm-thumb-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Candado de la imagen heredada (del pedido o de la recogida): dice por qué no
   trae ✕, en vez de dejar la ausencia sin explicación. */
.ssm-lock {
    position: absolute; right: -3px; bottom: -3px;
    width: 20px; height: 20px; border-radius: 50%;
    background: #64748b; color: #fff; font-size: 0.6rem;
    display: flex; align-items: center; justify-content: center;
    border: 2px solid #fff;
}
.ssm-del {
    position: absolute; right: -6px; top: -6px;
    width: 22px; height: 22px; border-radius: 50%; padding: 0;
    background: #fee2e2; color: #b91c1c; border: 2px solid #fff;
    font-size: 0.6rem; display: flex; align-items: center; justify-content: center;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.ssm-empty { font-size: 0.85rem; color: #94a3b8; }
.ssm-note {
    background: #f8fafc; border-radius: 10px; padding: 9px 11px;
    display: flex; flex-direction: column; gap: 4px;
}
.ssm-note.is-locked { background: #f1f5f9; }
.ssm-note-text { font-size: 0.88rem; color: #334155; line-height: 1.45; white-space: pre-wrap; }
.ssm-note-meta { font-size: 0.72rem; color: #94a3b8; font-weight: 600; }
.stop-action-panel--note { border-color: #bfdbfe; }
.sap-submit--note { background: #eff6ff; color: #1d4ed8; }

/* Foto OBLIGATORIA de la entrega: ocupa el lugar del botón principal entre
   "Llegué" y "Entregado" (es un <label> porque contiene el input de la cámara,
   pero se ve y se toca como el botón que reemplaza). */
.aoc-action-btn--cam { margin: 0; cursor: pointer; }

/* Las DOS maneras de subir la foto, dentro del panel: cada una es un <label>
   con su <input> escondido, y se leen como dos filas de una lista, no como dos
   botones que compiten. De DEDO (48px) porque es lo que se toca. */
.sap-photo-opt {
    display: flex; align-items: center; gap: 10px;
    width: 100%; min-height: 48px; margin: 0 0 8px;
    border: 1px solid #dbe6f6; border-radius: 12px; padding: 8px 12px;
    background: #f4f8fe; color: #1d4ed8; cursor: pointer;
    font-size: 0.88rem; font-weight: 700;
    -webkit-tap-highlight-color: transparent;
}
.sap-photo-opt:last-child { margin-bottom: 0; }
.sap-photo-opt:hover { background: #e8f1fd; }
.sap-photo-opt > i { font-size: 1.25rem; }
.sap-photo-opt .spo-text { display: flex; flex-direction: column; line-height: 1.2; }
.sap-photo-opt small { font-weight: 600; color: #64748b; font-size: 0.74rem; }

/* Visor de una imagen a pantalla completa (`js/photo-viewer.js`), el MISMO en
   las tres caras. Por encima de la hoja del rider y de los controles del mapa
   (y de un modal de Bootstrap, que va en 1055): mientras se mira una foto no
   hay nada más.

   Namespace `.pv-*` y no `.rider-*` porque lo comparten el dispatcher y el
   portal: dos pantallas no comparten un nombre de clase que nombra a una de
   ellas (la lección de `.dash-tile`).

   Los colores son literales a propósito, y es la excepción anotada a "nada de
   colores literales en una regla del admin": el visor es un `position: fixed`
   sobre el `<body>`, o sea fuera de `body.role-admin .app-content …`, y va
   oscuro en las TRES caras — mirar una foto es mirar una foto. */
.pv-viewer {
    position: fixed; inset: 0; z-index: 1090;
    background: rgba(15, 23, 42, 0.92);
    display: flex; align-items: center; justify-content: center; padding: 16px;
}
.pv-viewer[hidden] { display: none; }
.pv-viewer img { max-width: 100%; max-height: 100%; border-radius: 10px; }
.pv-viewer .pv-close {
    position: absolute; top: 14px; right: 14px;
    width: 40px; height: 40px; border-radius: 50%; border: 0;
    background: rgba(255, 255, 255, 0.16); color: #fff; font-size: 1rem;
    display: flex; align-items: center; justify-content: center; cursor: pointer;
}

/* ── Filas de la lista de paradas (panel del rider) ───────────────────── */
/* El número lleva el color del TIPO, igual que el marcador del mapa: la
   correlación lista↔mapa se lee sin pensar. */
.map-stop-badge {
    flex: 0 0 auto; min-width: 1.625rem; height: 26px; padding: 0 0.3125rem;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 8px; font-size: 0.85rem; font-weight: 800; color: #fff;
}
.map-stop-badge.is-recogida { background: #22C55E; }
.map-stop-badge.is-entrega { background: #B45309; }
.map-stop-badge.is-traspaso { background: #7C3AED; border-radius: 5px; }
.map-stop-kind {
    font-size: 0.7rem; font-weight: 800; letter-spacing: 0.04em;
    text-transform: uppercase; color: #64748b;
}
.map-stop-row.is-recogida .map-stop-kind { color: #16A34A; }
.map-stop-row.is-entrega .map-stop-kind { color: #B45309; }
.map-stop-row.is-handoff .map-stop-kind { color: #6D28D9; }
.map-stop-row .map-stop-order { font-size: 0.72rem; color: #94a3b8; font-weight: 600; }
.map-stop-row.is-now { border-color: var(--primary, #4A90E2); }
/* .map-status-now se retiró junto con la etiqueta "Ahora" de la lista del rider
   (era su único uso): la parada en curso se marca con la fila entera. */
/* En la hoja del rider la dirección se ve ENTERA: envuelve en las líneas que
   haga falta (calle, número, distrito, CP) en vez de recortarse con puntos
   suspensivos. Antes se recortaba "porque la completa está a un toque", pero
   eso escondía justo el número — el dato que se necesita al llegar. */
.rider-sheet .map-stop-row { align-items: flex-start; }
.rider-sheet .map-stop-addr {
    white-space: normal; overflow-wrap: anywhere;
    font-size: 0.85rem; font-weight: 600; color: #1e293b;
}

@media (min-width: 900px) {
    /* Panel lateral fijo: no hay niveles que valgan, se ve todo. */
    .rider-sheet[data-level] .sheet-lvl-mid,
    .rider-sheet[data-level] .sheet-lvl-full { display: block; }
}

/* ══════════════════════════════════════════════════════════════════════════
   RIDER · Inicio aligerado (rediseño 2026-07-25)

   El Inicio dejó de tener tarjeta de pedido activo y mini-mapa: quedan el
   estado de la jornada, las cifras del día, el cumplimiento, la semana y UNA
   salida al mapa. Los valores salen del diseño (docs/archivo/design/rider_design.dc.html).
   ══════════════════════════════════════════════════════════════════════════ */
.dash-greeting { font-size: 1.6rem; font-weight: 800; color: #0f172a; letter-spacing: -0.01em; }

/* ── KPIs del Inicio (rediseño 2026-07-28) ─────────────────────────────────
   Sustituyen a la grilla 2×2 de cifras iguales (.dash-kpi), que además contaba
   PEDIDOS. Tres alturas de lectura, no una pila de tarjetas: hero de jornada →
   avance del día en paradas → bento de KPIs. El tamaño ES la jerarquía; cuatro
   números idénticos no dicen cuál mirar primero.

   Superficie común de todas las piezas: blanco, borde #eceef2, radio 18px
   (mismo lenguaje que .weekly-chart-card y .dash-map-cta). */
.dash-hero, .dash-progress, .dash-tile, .dash-break, .dash-daysum {
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
}

/* ── Pausa en curso ────────────────────────────────────────────────────────
   Lo único ámbar de la pantalla, y arriba de todo: mientras dura, es el estado
   del rider. El reloj corre en segundos (ver el JS del dashboard). */
.dash-break {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    margin-top: 16px; padding: 14px 16px;
    background: linear-gradient(180deg, #fffbeb 0%, #fef7e0 100%);
    border-color: #fcd34d;
}
.dash-break-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.dash-break-icon {
    flex: none; width: 40px; height: 40px; border-radius: 50%;
    display: grid; place-items: center;
    background: var(--warning, #F39C12); color: #fff; font-size: 1.25rem;
}
.dash-break-label { margin: 0; font-size: 0.98rem; font-weight: 800; color: #92400e; }
.dash-break-since { margin: 2px 0 0; font-size: 0.78rem; font-weight: 600; color: #b45309; }
.dash-break-right { display: flex; align-items: center; gap: 10px; flex: none; }
.dash-break-clock {
    font-size: 1.5rem; font-weight: 800; color: #92400e; letter-spacing: -0.02em;
    font-variant-numeric: var(--num);
}
.dash-break-btn {
    display: inline-flex; align-items: center; gap: 5px;
    border: none; border-radius: 999px; padding: 9px 14px;
    background: #92400e; color: #fff; font-size: 0.85rem; font-weight: 700;
}
.dash-break-btn:active { background: #78350f; }

/* ── Hero: la jornada ──────────────────────────────────────────────────────
   Anillo de cumplimiento con el reloj neto adentro. El reloj vive DENTRO del
   anillo porque son la misma pregunta ("¿cómo voy con mi turno?"): separados
   eran una tarjeta de cumplimiento y un KPI "Horas trabajadas" que decían lo
   mismo dos veces. */
.dash-hero {
    display: flex; align-items: center; gap: 18px;
    margin-top: 16px; padding: 18px 18px 18px 14px;
    text-decoration: none; color: inherit;
}
.dash-hero:active { background: #fafbfc; }
.dash-hero.is-paused { opacity: 0.72; }

.dash-hero-ring { position: relative; flex: none; width: 118px; height: 118px; }
.dash-hero-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.dash-hero-ring circle { fill: none; stroke-width: 11; stroke-linecap: round; }
.dhr-track { stroke: #eef1f5; }
.dhr-fill { stroke: var(--primary, #4A90E2); transition: stroke-dasharray 0.4s ease; }
.dhr-fill.is-done { stroke: var(--success, #27AE60); }
.dash-hero-ring-mid {
    position: absolute; inset: 0; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 1px;
}
.dhr-time {
    font-size: 1.42rem; font-weight: 800; color: #0f172a; line-height: 1;
    letter-spacing: -0.02em; font-variant-numeric: var(--num);
}
.dhr-cap { font-size: 0.68rem; font-weight: 700; color: #94a3b8; text-transform: lowercase; }

.dash-hero-side { min-width: 0; flex: 1; }
.dash-hero-title { margin: 0 0 8px; font-size: 1.02rem; font-weight: 800; color: #0f172a; }
.dash-hero-row {
    display: flex; align-items: baseline; gap: 8px; margin: 0 0 5px;
    font-size: 0.86rem;
}
.dhs-key { color: #64748b; font-weight: 600; flex: 1; }
.dhs-val { color: #0f172a; font-weight: 700; font-variant-numeric: var(--num); }
.dhs-pct {
    font-weight: 800; color: var(--primary, #4A90E2);
    font-variant-numeric: var(--num);
}
.dhs-pct.ok { color: var(--success, #27AE60); }
.dhs-delta { font-weight: 700; font-variant-numeric: var(--num); }
.dhs-delta.pos { color: var(--success, #27AE60); }
.dhs-delta.neg { color: #b45309; }
.dash-hero-shift {
    margin: 8px 0 0; font-size: 0.78rem; font-weight: 600; color: #94a3b8;
    font-variant-numeric: var(--num);
}

/* ── Avance del día, en PARADAS ────────────────────────────────────────────
   Barra segmentada: "14 de 21" no dice si esas 14 salieron bien. Una fallida
   es trabajo cerrado pero no es una entrega, y se ve. */
.dash-progress { margin-top: 12px; padding: 16px; }
.dash-progress-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.dash-progress-count {
    margin: 0; font-size: 0.95rem; font-weight: 600; color: #64748b;
    font-variant-numeric: var(--num);
}
.dash-progress-count strong {
    font-size: 1.7rem; font-weight: 800; color: #0f172a; letter-spacing: -0.02em;
}
.dash-progress-pct {
    font-size: 1.05rem; font-weight: 800; color: var(--primary, #4A90E2);
    font-variant-numeric: var(--num);
}
.dash-progress-bar {
    display: flex; height: 10px; margin-top: 10px;
    border-radius: 999px; background: #eef1f5; overflow: hidden;
}
.dpb-seg { display: block; height: 100%; transition: width 0.4s ease; }
.dpb-seg.is-ok { background: var(--success, #27AE60); }
.dpb-seg.is-fail { background: var(--danger, #E74C3C); }
.dash-progress-legend {
    display: flex; flex-wrap: wrap; gap: 12px; margin-top: 9px;
    font-size: 0.78rem; font-weight: 600;
}
.dpl { display: inline-flex; align-items: center; gap: 5px; color: #64748b; }
.dpl::before {
    content: ''; width: 8px; height: 8px; border-radius: 50%; background: #cbd5e1;
}
.dpl.is-ok::before { background: var(--success, #27AE60); }
.dpl.is-fail::before { background: var(--danger, #E74C3C); }

.dash-route-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.dash-route-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 5px 11px; border-radius: 999px;
    background: #f1f5f9; color: #334155;
    font-size: 0.78rem; font-weight: 600;
}
.dash-route-chip b { font-weight: 800; }
.dash-route-chip .num { font-variant-numeric: var(--num); color: #64748b; }
.dash-route-chip.is-done { background: #d1fae5; color: #065f46; }
.dash-route-chip.is-done .num { color: #047857; }
.dash-progress-empty {
    margin: 12px 0 0; font-size: 0.8rem; font-weight: 600; color: #94a3b8;
}

/* ── Bento de KPIs ─────────────────────────────────────────────────────────
   2 columnas; las piezas .is-wide ocupan la fila entera. El ritmo va ancho
   porque es el número sobre el que el rider puede ACTUAR, y del que sale la
   hora estimada de fin. */
.dash-bento {
    display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;
}
.dash-tile { padding: 14px 15px; display: flex; flex-direction: column; }
.dash-tile.is-wide { grid-column: 1 / -1; flex-direction: row; align-items: center; gap: 14px; }
.dash-tile.is-wide .dt-main { flex: 1; min-width: 0; }
.dash-tile.is-paused { border-color: #fcd34d; background: #fffdf5; }
.dash-tile.is-quiet { background: #f8fafc; }

.dt-label {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 0.76rem; font-weight: 700; color: #64748b;
    text-transform: uppercase; letter-spacing: 0.03em;
}
.dt-label i { font-size: 0.9rem; color: #94a3b8; }
.dt-value {
    margin: 7px 0 0; font-size: 1.85rem; font-weight: 800; color: #0f172a;
    line-height: 1.05; letter-spacing: -0.02em; font-variant-numeric: var(--num);
}
.dt-value.is-empty { color: #cbd5e1; }
.dt-value.is-warn { color: #b45309; }
.dt-unit { font-size: 0.82rem; font-weight: 700; color: #94a3b8; margin-left: 4px; }
.dt-note { margin: 3px 0 0; font-size: 0.74rem; font-weight: 600; color: #94a3b8; }

/* Costado de las piezas anchas: la consecuencia del número grande (la hora a
   la que terminaría, el enlace al mapa). Separado por una línea, no por otra
   tarjeta: es la misma idea leída hacia adelante. */
.dt-aside {
    flex: none; max-width: 44%; padding-left: 14px; border-left: 1px solid #eef1f5;
    display: flex; flex-direction: column; gap: 2px; text-align: right;
}
.dt-aside-label { font-size: 0.72rem; font-weight: 700; color: #94a3b8; }
.dt-aside-value {
    font-size: 1.35rem; font-weight: 800; color: var(--primary, #4A90E2);
    line-height: 1.1; font-variant-numeric: var(--num);
}
.dt-aside-note { font-size: 0.72rem; font-weight: 600; color: #94a3b8; }
.dt-aside-done {
    font-size: 0.82rem; font-weight: 700; color: var(--success, #27AE60);
    display: inline-flex; align-items: center; gap: 5px; justify-content: flex-end;
}
.dt-aside-link {
    font-size: 0.8rem; font-weight: 700; color: var(--primary, #4A90E2);
    text-decoration: none; white-space: nowrap;
}

/* Resumen del día en la pantalla SIN jornada activa: quien acaba de terminar
   no debería perder de vista lo que hizo. */
.dash-daysum {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
    margin-top: 12px; padding: 14px 10px; text-align: center;
}
.dash-daysum > div { display: flex; flex-direction: column; gap: 2px; }
.dds-num {
    font-size: 1.45rem; font-weight: 800; color: #0f172a;
    font-variant-numeric: var(--num);
}
.dds-num small { font-size: 0.72rem; font-weight: 700; color: #94a3b8; margin-left: 2px; }
.dds-cap { font-size: 0.72rem; font-weight: 600; color: #94a3b8; }

/* Pantallas angostas: el hero pasa a columna antes de que el anillo y el texto
   se peleen el ancho. */
@media (max-width: 380px) {
    .dash-hero { flex-direction: column; align-items: stretch; text-align: center; }
    .dash-hero-ring { align-self: center; }
    .dash-tile.is-wide { flex-direction: column; align-items: stretch; }
    .dt-aside {
        max-width: none; padding: 10px 0 0; text-align: left;
        border-left: none; border-top: 1px solid #eef1f5;
    }
    .dt-aside-done { justify-content: flex-start; }
}

/* Única salida operativa: el mapa. */
.dash-map-cta {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    width: 100%; min-height: 60px; margin: 22px 0 8px;
    border-radius: 18px; background: var(--primary, #4A90E2); color: #fff;
    font-size: 1.05rem; font-weight: 800; text-decoration: none;
    box-shadow: 0 8px 20px rgba(74, 144, 226, 0.35);
}
.dash-map-cta:hover, .dash-map-cta:focus { color: #fff; }
.dash-map-cta i { font-size: 1.3rem; }

/* La tarjeta del gráfico se alinea con las de arriba (el rótulo "Esta semana"
   ya lo pone la sección, así que el título interno se retiró). */
.weekly-chart-card {
    border: 1px solid #eceef2; border-radius: 18px; padding: 16px 14px 10px;
    box-shadow: none; margin: 0;
}

/* ══════════════════════════════════════════════════════════════════════════
   RIDER · hoja del mapa, fidelidad con el diseño (docs/archivo/design/rider_design.dc.html
   + docs/archivo/design/screenshots/). Todo lo de acá vive bajo `.rider-sheet`: el panel
   del DISPATCHER comparte clases (.map-stop-row, .map-status…) y no se toca.
   ══════════════════════════════════════════════════════════════════════════ */

/* Resumen de la ruta (contexto LISTA, hoja colapsada). */
.slp-title { font-size: 1.28rem; }
.slp-count { font-size: 0.95rem; margin-top: 4px; }
.slp-now { font-size: 0.95rem; font-weight: 800; margin-top: 5px; }
.slp-now::before { width: 11px; height: 11px; }

/* Cabecera de la parada (contexto PARADA): la dirección manda. */
.ssh-addr { font-size: 1.45rem; }
.sheet-stop-head.is-done .ssh-addr { font-size: 1.1rem; }
.ssh-type { font-size: 0.86rem; }
.ssh-order { font-size: 0.86rem; }
.ssh-unit { font-size: 0.98rem; }

/* Filas de la lista de paradas: tarjeta con borde de color por tipo, número a
   la izquierda y la DIRECCIÓN ENTERA como protagonista (screenshots
   sheet_ruta2/3). A la derecha ya no va la etiqueta de estado, solo la marca de
   cerrada (.map-stop-mark): con una parada por línea, el texto del estado le
   comía el ancho a la dirección. El panel del dispatcher usa estas mismas
   clases mucho más chicas y CON estado — de ahí el scope. */
.rider-sheet .map-route-stops { gap: 0.4375rem; padding: 0.25rem 2px 0.5rem; }
.rider-sheet .map-stop-row {
    align-items: flex-start; gap: 0.5625rem;
    padding: 0.5625rem 0.6875rem; border-radius: 14px;
    background: #f7f8fb; border: 1px solid transparent; border-left-width: 4px;
    font-size: 0.8rem;
}
.rider-sheet .map-stop-row.is-recogida { border-left-color: #22C55E; }
.rider-sheet .map-stop-row.is-entrega { border-left-color: #FACC15; }
.rider-sheet .map-stop-row.is-handoff { border-left-color: #7C3AED; }
/* La parada EN CURSO ya no lleva la etiqueta "Ahora" (la lista no repite
   estados): el aviso es la fila entera — fondo azul y contorno, dejando el
   borde IZQUIERDO con el color del tipo, que sigue significando otra cosa. */
.rider-sheet .map-stop-row.is-now {
    background: #eaf2fc;
    border-top-color: #4A90E2; border-right-color: #4A90E2; border-bottom-color: #4A90E2;
}
.rider-sheet .map-stop-row.is-now .map-stop-addr { color: #0f172a; font-weight: 700; }
.rider-sheet .map-stop-badge {
    min-width: 1.6875rem; height: 27px; border-radius: 9px; font-size: 0.85rem;
}
/* Mismos colores que el marcador: el número de la fila y el del mapa son el
   mismo número, así que no pueden ser de dos colores distintos. */
.rider-sheet .map-stop-badge.is-recogida { background: #22C55E; color: #fff; }
.rider-sheet .map-stop-badge.is-entrega { background: #FACC15; color: #3A2A04; }
.rider-sheet .map-stop-badge.is-traspaso {
    background: #7C3AED; color: #fff; border-radius: 6px;
    clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
}
.rider-sheet .map-stop-kind { font-size: 0.64rem; letter-spacing: 0.05em; }
.rider-sheet .map-stop-row .map-stop-order { font-size: 0.68rem; }
.rider-sheet .map-stop-body { gap: 1px; }
.rider-sheet .map-stop-addr { font-size: 0.84rem; line-height: 1.25; }
/* El CP cierra la dirección en la misma línea, más apagado: es contexto, no el
   dato que se busca. */
.rider-sheet .map-stop-cp { color: #64748b; font-weight: 600; white-space: nowrap; }
/* La franja horaria en la fila: es el único dato de la lista que no se puede
   deducir del mapa (el orden lo dice el número, el sitio la dirección), así que
   lleva superficie propia para que se encuentre de un vistazo — chica, porque
   la que manda sigue siendo la dirección. Se empuja a la derecha de su línea:
   así todas las ventanas del recorrido quedan en la misma columna y se leen en
   vertical, que es como se mira si el orden aguanta. */
.rider-sheet .map-stop-win { margin-left: auto; font-size: 0.72rem; padding: 1px 0.4375rem; }

/* Marca de parada CERRADA, en lugar de la etiqueta de estado (ver stopMark en
   map-rider.js). Un círculo del tamaño del pulgar-nada: no compite con la
   dirección y se ve de un vistazo cuáles ya están hechas. */
.map-stop-mark {
    flex: 0 0 auto; width: 21px; height: 21px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 0.7rem; line-height: 1; margin-top: 0.1875rem;
}
/* La fila del grupo alinea al centro: ahí la marca no se despega de arriba. */
.sheet-group-row .map-stop-mark { margin-top: 0; }
.map-stop-mark.is-ok { background: #d1fae5; color: #047857; }
.map-stop-mark.is-fail { background: #fee2e2; color: #b91c1c; }
.map-stop-mark.is-handoff { background: #ede9fe; color: #6d28d9; }

/* ── Acciones de la parada, EN LÍNEA ───────────────────────────────────────
   La fila es lo único que se toca con la hoja abajo: acción principal ancha +
   "Fallida" (y "Traspaso") al lado. Los botones secundarios NO se abren en el
   sitio: son disparadores de tamaño FIJO y su formulario se despliega en el
   cuerpo de la hoja (.stop-action-panel). Antes eran <details> acá adentro y al
   abrirlos el botón saltaba de fila y el pie crecía hasta tapar la dirección. */
.sheet-actions { display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; }
/* El <form> es FLEX para que su botón se ESTIRE al alto de la fila. Sin esto el
   botón se queda en su `min-height` mientras el <form> sí crece (`align-items:
   stretch` del contenedor), así que en cuanto una etiqueta parte en dos
   renglones —«Foto entrega» al lado de «Galería»— el de una sola línea queda
   más bajo y la fila se ve descuadrada. El alto lo pone la fila, no cada
   botón. */
.sheet-actions > form { flex: 1.6 1 84px; min-width: 0; margin: 0; display: flex; }
/* La acción principal puede ser un <button> suelto (el que ABRE el panel de la
   foto) y no un <form>: mide lo mismo que el formulario al que reemplaza — sin
   esta línea su `width: 100%` se llevaría la fila entera. El ancho base va en
   rem (5.25rem = los 84px del `> form`; la hoja del rider no escala) porque un
   `flex` en px sobre una familia del panel es justo lo que vigila el barrido de
   DENSIDAD. */
.sheet-actions > .aoc-action-btn { flex: 1.6 1 5.25rem; min-width: 0; width: auto; }
/* Las DOS puertas de la foto de la entrega («Foto entrega» y «Galería») son
   caminos EQUIVALENTES: miden lo MISMO —una base compartida, el mismo crecer—
   y ninguna se lee como la principal. La base es más chica que la de un botón
   solo porque acá hay dos en la misma fila (más «Ir», «Fallida» y el chico de
   deshacer, que NO crece: sigue en sus 2,75rem). */
.sheet-actions > .aoc-photo-form { flex: 1.1 1 4.25rem; min-width: 0; margin: 0; }
.sheet-actions > .aoc-hint { flex: 1 0 100%; margin: 0; }
/* «Deshacer Llegué»: chico y al lado, nunca compitiendo con la acción
   principal. Sigue siendo de DEDO (44px). */
.sheet-actions > .aoc-undo-form { flex: 0 0 2.75rem; margin: 0; }
.aoc-undo-form { display: inline-flex; margin: 0; }
.aoc-undo-btn {
    width: 2.75rem; min-height: 2.75rem; height: 100%;
    border: 2px solid #cbd5e1; border-radius: 14px;
    background: #fff; color: #475569; font-size: 1.1rem;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.aoc-undo-btn:hover { border-color: #94a3b8; color: #0f172a; }
.sheet-actions .aoc-action-btn,
.sheet-actions .status-action-btn {
    width: 100%; min-height: 3.875rem; border-radius: 16px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px; font-size: 0.72rem; font-weight: 800; line-height: 1.1; text-align: center;
}
.sheet-actions .aoc-action-btn i { font-size: 1.2rem; }
/* Hasta CUATRO en la misma fila (acción principal + Ir + Fallida + Traspaso):
   las bases son chicas a propósito y `min-width: 0` deja que se respeten — sin
   eso el ancho MÍNIMO DEL CONTENIDO ("Traspaso" es una palabra que no parte)
   hace de suelo, la suma no entra y el último botón cae a una segunda fila. Esa
   fila de más engorda el pie, y el pie le come alto al mapa, que es lo único
   que el rider mira mientras pedalea. Por eso se achican los cuatro antes que
   apilar: la fila importa más que el tamaño de cada botón.
   El tamaño NO cambia con el panel abierto — el botón se queda donde el rider
   lo tocó. */
.sheet-actions > .sheet-action-trigger {
    flex: 1 1 54px; min-width: 0; margin: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px; min-height: 62px; border-radius: 16px;
    border: 2px solid #fecaca; background: #fff; color: #dc2626;
    font-size: 0.72rem; font-weight: 800; line-height: 1.1; text-align: center;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.sheet-actions > .sheet-action-trigger i { font-size: 1.2rem; }
/* La etiqueta puede achicarse un punto más antes que romper la fila: el icono ya
   dice cuál es cada botón y el texto es la confirmación, no la señal. */
.sheet-actions > .sheet-action-trigger > span { font-size: 0.68rem; }
/* Panel abierto: el botón queda "hundido" para que se vea de dónde salió lo que
   está mostrando el cuerpo de la hoja. */
.sheet-actions > .sheet-action-trigger.is-open { background: #fef2f2; }
.sheet-actions > .sheet-action-trigger--handoff.is-open { background: #faf5ff; }
/* El botón principal en la hoja va RELLENO (el `.aoc-action-btn` de siempre es
   blanco sobre azul, porque nació dentro de la tarjeta de color del Inicio, que
   ya no existe). Acá el fondo es blanco, así que el relleno es el azul. */
.sheet-actions .aoc-action-btn {
    background: var(--primary, #4A90E2); color: #fff;
    box-shadow: 0 6px 16px rgba(74, 144, 226, 0.3);
}
.sheet-actions .aoc-action-btn:hover,
.sheet-actions .aoc-action-btn:focus { background: #3a7bc8; color: #fff; }

/* "Traspaso" no es un problema como "Fallida": es ceder la entrega a otro
   rider. Mismo botón, código de color del traspaso (morado, el de los
   marcadores y los badges de la cadena de custodia). */
.sheet-actions > .sheet-action-trigger--handoff {
    border-color: #ddd6fe; color: #7C3AED;
}

/* "Ir" (navegación externa) no cierra ni cambia nada: solo abre Google Maps.
   Por eso va en NEUTRO —ni el rojo de "Fallida" ni el morado del traspaso, que
   son códigos de desenlace— y sin relleno, para no competir con la acción
   principal, que sigue siendo la que manda en la fila. Es un <a>, no un
   <button>: hereda el layout de `.sheet-action-trigger` pero necesita que le
   quiten el subrayado del enlace. */
.sheet-actions > .sheet-action-trigger--go {
    border-color: #cbd5e1; color: #334155; text-decoration: none;
}
.sheet-actions > .sheet-action-trigger--go:hover,
.sheet-actions > .sheet-action-trigger--go:focus {
    border-color: #94a3b8; color: #0f172a; background: #f8fafc;
}

/* ── Paneles de las acciones secundarias (2026-07-27) ──────────────────────
   El formulario de "Fallida" / "Traspaso" vive en el CUERPO scrolleable, no en
   el pie: acá puede medir lo que necesite sin mover un solo botón ni comerse la
   cabecera. Mientras uno está abierto la tarjeta no muestra nada más, así que
   la hoja subida a tope es el panel y punto. */
.sheet-stop-body.has-open-panel > :not(.sheet-stop-panels) { display: none; }
.stop-action-panel {
    border: 1px solid #e5e7eb; border-radius: 14px; background: #fff;
    padding: var(--space-3); margin-top: var(--space-2);
}
.stop-action-panel--fail { border-color: #fecaca; }
.stop-action-panel--handoff { border-color: #ddd6fe; }
.stop-action-panel--photo { border-color: #bfdbfe; }
.sap-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.sap-title { flex: 1 1 auto; font-size: 0.98rem; font-weight: 800; color: #0f172a; }
.sap-title .pkg-ico { margin-right: 4px; }
.stop-action-panel--fail .sap-title { color: #b91c1c; }
.stop-action-panel--handoff .sap-title { color: #6d28d9; }
.stop-action-panel--photo .sap-title { color: #1d4ed8; }
.sap-close {
    flex: 0 0 auto; width: 34px; height: 34px; border-radius: 10px;
    border: 1px solid #e5e7eb; background: #fff; color: #6b7280; cursor: pointer;
}
.sap-lead { font-size: 0.82rem; color: #475569; margin: 0 0 8px; }
.sap-empty {
    display: flex; align-items: center; gap: 8px; margin: 0;
    border-radius: 10px; padding: 10px 12px;
    background: #f8fafc; color: #475569; font-size: 0.86rem; font-weight: 600;
}
.sap-submit {
    width: 100%; min-height: 44px; border: 0; border-radius: 10px;
    font-size: 0.88rem; font-weight: 700; cursor: pointer;
}
.sap-submit--fail { background: #fef2f2; color: #b91c1c; }
.sap-submit--handoff { background: #faf5ff; color: #6d28d9; }

/* Elegir el punto de encuentro EN EL MAPA (panel de traspaso del rider). No es
   el botón que ENVÍA —ése es `.sap-submit`, ancho completo y morado lleno—:
   éste abre un gesto sobre el mapa y por eso va con borde y no pintado. Sigue
   disponible después de elegir (mover el punto es el mismo gesto que ponerlo),
   así que lo único que cambia al fijarlo es su etiqueta. 44px: se toca. */
.sap-pick {
    width: 100%; min-height: 44px; border: 1px solid #ddd6fe; border-radius: 10px;
    background: #fff; color: #6d28d9; font-size: 0.84rem; font-weight: 700;
    cursor: pointer;
}
.sap-picked {
    margin: 0; font-size: 0.76rem; color: #6d28d9; line-height: 1.35;
    word-break: break-word;
}

/* ── Editar un traspaso vivo (panel del rider) ─────────────────────────────
   Las entregas del encuentro, una por línea con su casilla: destildar es
   sacarla. Área de TOQUE completa (44px) porque se opera con una mano y en
   movimiento — es la regla ④, y acá no hay excepción que la invierta. */
.sap-lote { list-style: none; margin: 0 0 0.5rem; padding: 0; }
.sap-lote-lbl {
    display: flex; align-items: center; gap: 0.5rem; min-height: 44px;
    font-size: 0.84rem; line-height: 1.3; cursor: pointer;
}
.sap-lote-lbl input { width: 20px; height: 20px; flex: none; }
/* La que ya no se puede sacar: se LEE, no se toca (y su id viaja igual en un
   oculto — el contrato es el conjunto final, no un delta). */
.sap-lote-lbl.is-fixed { cursor: default; color: var(--gray-500); }
/* «Añadir entregas desde el mapa» necesita el modo de selección, que es JS: sin
   él no se dibuja. La clase la pone map-rider.js al arrancar, y va en la HOJA y
   no en cada botón porque las tarjetas se reemplazan enteras al recargarse. */
[data-hoedit-add] { display: none; }
.sheet-js [data-hoedit-add] { display: block; }

/* ── Familia de iconos "paquete" (2026-07-26) ──────────────────────────────
   Una MISMA caja (bi-box-seam) con un badge distinto según lo que hace el
   botón: check al cerrar la parada, aspa en "Fallida", flechas en "Traspaso".
   Se compone de dos glifos en vez de dibujar un SVG: la silueta sale toda de
   `currentColor`, así el mismo marcado se ve blanco sobre el botón azul, rojo
   sobre el de borde rojo y morado sobre el de traspaso — sin una variante de
   icono por color.

   El badge lleva un disco del color del FONDO del botón (--pkg-bg) para
   separarse de la caja: sin ese corte, los dos glifos se pisan y no se lee
   ninguno. Por eso cada botón declara su --pkg-bg; el default es blanco. */
.pkg-ico {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 1em; height: 1em; line-height: 1;
    font-size: 1.35rem;                       /* el tamaño de icono de la fila */
}
.pkg-ico .pkg-box { font-size: 1em; line-height: 1; }
.pkg-ico .pkg-badge {
    position: absolute; right: -0.30em; bottom: -0.20em;
    display: inline-flex; align-items: center; justify-content: center;
    width: 1em; height: 1em; font-size: 0.62em; line-height: 1;
    border-radius: 50%;
    background: var(--pkg-bg, #fff);
    box-shadow: 0 0 0 0.14em var(--pkg-bg, #fff);
}
.sheet-actions .aoc-action-btn { --pkg-bg: var(--primary, #4A90E2); }
.sheet-actions .aoc-action-btn:hover,
.sheet-actions .aoc-action-btn:focus { --pkg-bg: #3a7bc8; }


/* ── Calendario (jornadas y despacho) ──────────────────────────────────────
   Las clases `.cal-*` las comparten `admin/shifts_calendar.html` (jornadas) y
   `admin/calendar.html` (pedidos programados, tanda 4). Estaban inline en la
   primera; con dos calendarios vivos, mantener dos copias del mismo bloque es
   la manera de que se separen. */
.cal-table { table-layout: fixed; }
.cal-cell {
    min-height: 4.375rem;
    vertical-align: top;
    padding: 0.375rem 0.5rem;
    width: 14.28%;
}
.cal-cell.empty { background-color: var(--gray-100); }
.cal-cell.today { outline: 2px solid var(--primary); outline-offset: -2px; }
.cal-cell.has-shifts { background-color: color-mix(in srgb, var(--primary) 12%, transparent); }
.cal-day-num { font-size: 0.85rem; font-weight: 600; line-height: 1; margin-bottom: 0.25rem; }
.cal-shift-badge { font-size: 0.72rem; }
.cal-add { color: var(--gray-500); font-size: 0.7rem; }

/* Celda del calendario de JORNADAS de un rider (`admin/rider_sessions_calendar.html`).
   Estaban en un `<style>` del cuerpo de esa plantilla, o sea DESPUÉS de esta
   hoja: con la misma especificidad ganaban siempre, y por eso el ajuste móvil
   del calendario no la alcanzaba (§19 del bloque del teléfono). Acá viven las
   clases PROPIAS de esa vista; `.cal-cell` y compañía son las de arriba.
   El color sale de los tokens: un literal escrito acá sería texto oscuro sobre
   el fondo oscuro del admin. */
.cal-hours { font-size: 0.78rem; font-weight: 600; }
.cal-sessions { font-size: 0.72rem; }
.cal-shift {
    font-size: 0.7rem;
    margin-top: 0.1875rem;
    padding-top: 0.1875rem;
    border-top: 1px dashed rgba(128, 128, 128, 0.4);
    color: inherit;
    opacity: 0.85;
}
/* La diferencia contra lo planificado. Es REGISTRO, no un juicio: se muestra
   con su signo y sin adjetivos — qué significa lo decide quien la mira. */
.cal-delta { font-size: 0.7rem; font-weight: 600; margin-top: 2px; }
/* Un día con jornada planificada al que nadie fichó, y que ya pasó. */
.cal-absent {
    font-size: 0.72rem; font-weight: 600;
    color: color-mix(in srgb, var(--danger) 85%, #000);
}
.cal-cell.is-absent { background-color: color-mix(in srgb, var(--danger) 14%, transparent); }
/* La muestra de color de la leyenda del calendario de jornadas. Medía por
   `style=` en línea —lo único que el barrido no alcanza— y es un ASSET de
   tamaño fijo, así que se queda en px; del `style=` solo sobrevive el color,
   que es el dato. */
.cal-legend-swatch {
    display: inline-block; width: 16px; height: 16px;
    border: 1px solid rgba(255,255,255,0.25);
    vertical-align: middle;
}

/* Celda del calendario de DESPACHO: chips de pedidos/rutas + semáforo de
   RESULTADO (tanda C). Tres colores y tres significados distintos:
     rojo  = hay trabajo que NADIE ruteó (lo único accionable),
     ámbar = día pasado que quedó a medias ("esto lo arrastro"),
     verde = día cerrado.
   Rojo y ámbar eran el mismo rojo antes de la tanda C, y son problemas
   distintos: uno se resuelve planificando, el otro ya pasó. */
.cal-cell.is-alert {
    background-color: color-mix(in srgb, var(--danger) 10%, transparent);
    box-shadow: inset 3px 0 0 var(--danger);
}
.cal-cell.is-late {
    background-color: color-mix(in srgb, var(--warning) 14%, transparent);
    box-shadow: inset 3px 0 0 var(--warning);
}
.cal-cell.is-done { box-shadow: inset 3px 0 0 var(--success); }
.cal-chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.cal-chip {
    display: inline-flex; align-items: center; gap: 0.1875rem;
    font-size: 0.7rem; font-weight: 600; line-height: 1;
    padding: 0.1875rem 0.4375rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-700);
    text-decoration: none; white-space: nowrap;
}
.cal-chip.is-orders { background: color-mix(in srgb, var(--primary) 18%, transparent); }
/* El verde es del RESULTADO ("se entregó"), no de las rutas: dos verdes con
   significados distintos en la misma celda no se leen de un golpe, que es todo
   el punto de la vista mes. La ruta es contexto → neutra. */
.cal-chip.is-routes { background: var(--gray-200); color: var(--gray-700); }
.cal-chip.is-done {
    background: color-mix(in srgb, var(--success) 20%, transparent);
    color: color-mix(in srgb, var(--success) 75%, #000);
}
.cal-chip.is-pending {
    background: color-mix(in srgb, var(--warning) 26%, transparent);
    color: color-mix(in srgb, var(--warning) 70%, #000);
}
.cal-chip.is-alert { background: var(--danger); color: #fff; }
/* Muestras de color de la leyenda: replican el borde de la celda, no el chip. */
.cal-legend-swatch {
    display: inline-block; width: 0.85em; height: 0.85em;
    border-radius: 3px; vertical-align: -0.1em;
    background: var(--gray-200);
}
.cal-legend-swatch.is-alert { background: var(--danger); }
.cal-legend-swatch.is-late { background: var(--warning); }
.cal-legend-swatch.is-done { background: var(--success); }
/* Previsto por una plantilla recurrente: el pedido TODAVÍA no existe, así que
   va punteado — no es trabajo, es una intención (tanda 6). */
.cal-chip.is-planned {
    background: transparent; color: var(--gray-600);
    border: 1px dashed var(--gray-400);
}
.cal-day-link { display: block; text-decoration: none; color: inherit; }
.cal-day-link:hover .cal-day-num { text-decoration: underline; }

/* Cabecera del DÍA (admin/calendar_day.html) */
.cal-day-head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.cal-day-titlerow { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.cal-day-title { font-size: 1.25rem; font-weight: 700; margin: 0; text-transform: capitalize; }
/* Estado del día: un día pasado a medias ("arrastra") no puede verse igual que
   uno terminado ("cerrado"). Va fuera del `<h1>` porque el título capitaliza. */
.cal-day-state {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.72rem; font-weight: 700; line-height: 1;
    padding: 0.25rem 0.625rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-600);
}
.cal-day-state.is-done {
    background: color-mix(in srgb, var(--success) 18%, transparent);
    color: var(--success);
}
/* "Arrastra" es ÁMBAR, no rojo: el rojo del calendario significa "nadie lo
   ruteó" (`is-alert`), que es lo accionable. Un día que quedó a medias es otra
   cosa y con el mismo color no se distinguían (tanda C). El mismo `is-late`
   pinta la celda del mes: una clase, un significado. */
.cal-day-state.is-late {
    background: var(--warning); color: #fff;
}
.cal-day-state.is-now {
    background: color-mix(in srgb, var(--primary) 18%, transparent);
    color: var(--primary);
}
.cal-day-sub { font-size: 0.82rem; color: var(--gray-500); }
.cal-section { margin-bottom: var(--space-4); }
.cal-section-head {
    display: flex; align-items: center; gap: 0.5rem;
    font-size: 0.9rem; font-weight: 700; margin-bottom: var(--space-2);
}
.cal-section-count {
    font-size: 0.74rem; font-weight: 600; color: var(--gray-500);
    background: var(--gray-100); border-radius: 999px; padding: 2px 0.625rem;
}

/* Editar / eliminar un pedido SIN salir del día (`.cal-row-actions`).
   El día es donde se gestiona la jornada entera, así que las dos acciones que
   antes obligaban a entrar a la ficha viven en la propia fila.
   Son ICONOS, no botones con texto: la columna es la última de siete y con
   etiquetas empujaría al trayecto —lo único que se lee de verdad— fuera de
   cuadro. La intención la lleva el `title`/`aria-label`, y el color solo
   aparece al pasar por encima: en reposo una tabla con un rojo por fila se
   lee como una lista de errores. */
.cal-row-actions {
    display: inline-flex; gap: 0.25rem; justify-content: flex-end;
}
.cal-row-actions form { display: contents; }
.cal-row-act {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.875rem; height: 1.875rem; padding: 0;
    border: 1px solid var(--gray-300); border-radius: var(--radius-sm);
    background: transparent; color: var(--gray-500);
    font-size: 0.86rem; line-height: 1; cursor: pointer;
    text-decoration: none;
    transition: background var(--transition-fast), color var(--transition-fast),
                border-color var(--transition-fast);
}
.cal-row-act:hover, .cal-row-act:focus-visible {
    background: color-mix(in srgb, var(--primary) 12%, transparent);
    border-color: var(--primary); color: var(--primary);
}
.cal-row-act.is-danger:hover, .cal-row-act.is-danger:focus-visible {
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border-color: var(--danger); color: var(--danger);
}
/* El pedido entregado no se elimina (es un registro de facturación): en vez de
   un botón que va a rebotar, el hueco queda vacío y se dice por qué. */
.cal-row-act.is-off {
    opacity: 0.35; cursor: not-allowed; pointer-events: none;
}

/* ── SEMANA riders × días (admin/calendar_week.html, tanda 7) ───────────────
   Grilla de reparto: una fila por rider, una columna por día, y abajo el pozo
   de lo que ese día no tiene recorrido. Con 7 columnas + la de riders, la
   tabla no entra en pantallas chicas: scrollea ELLA (`.cal-week-wrap`), nunca
   el body. */
.cal-week-wrap { overflow-x: auto; }
/* El `min-width` es el UMBRAL de scroll de la tabla, no densidad: dice a
   partir de qué ancho de PANTALLA hay que arrastrar, así que se mide contra el
   viewport y se queda en px (excepción del barrido; el teléfono lo baja a
   760px en §5). */
.cal-week { table-layout: fixed; min-width: 1080px; }
.cal-week th, .cal-week td { padding: 0.375rem; vertical-align: top; }
.cal-week-rider-col { width: 9.375rem; }
.cal-week-rider-name { font-weight: 700; font-size: 0.85rem; }
.cal-week-dayname { font-size: 0.72rem; color: var(--gray-500); text-transform: uppercase; }
.cal-week th.today, .cal-week-cell.today { background: color-mix(in srgb, var(--primary) 7%, transparent); }
.cal-week-cell { width: 13.2%; }
.cal-week-cell.is-alert { box-shadow: inset 3px 0 0 var(--danger); }

/* Una ruta dentro de la celda: la barra de color es la del rider, la misma
   que el mapa y la lista de rutas. */
.cal-week-route {
    border-left: 3px solid var(--rider-color, var(--gray-400));
    background: var(--gray-100);
    border-radius: 4px; padding: 0.25rem 0.375rem; margin-bottom: 0.3125rem;
}
.cal-week-route-name {
    display: block; font-size: 0.78rem; font-weight: 700;
    text-decoration: none; color: inherit;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal-week-route-name:hover { text-decoration: underline; }
.cal-week-route-meta { font-size: 0.68rem; color: var(--gray-600); }

/* El "soltar acá": es un submit de verdad (form= por columna), no un gesto de
   drag-and-drop. Punteado porque es un HUECO donde va lo marcado. */
.cal-week-drop {
    display: inline-flex; align-items: center; gap: 0.1875rem;
    font-size: 0.68rem; font-weight: 600; line-height: 1;
    padding: 0.1875rem 0.4375rem; margin-top: 0.1875rem;
    border: 1px dashed var(--gray-400); border-radius: 999px;
    background: transparent; color: var(--gray-600); cursor: pointer;
}
.cal-week-drop:hover { border-color: var(--primary); color: var(--primary); }
.cal-week-drop.is-new { border-style: dotted; }

/* El pozo: lo que ese día nadie recorre todavía. */
.cal-week-pool > .cal-week-cell, .cal-week-pool > th { background: var(--gray-50, var(--gray-100)); }
.cal-week-all { display: block; font-size: 0.68rem; color: var(--gray-600); margin-bottom: 0.1875rem; }
.cal-week-order {
    display: flex; align-items: center; gap: 0.25rem;
    font-size: 0.7rem; line-height: 1.2; margin-bottom: 2px; cursor: pointer;
}
.cal-week-order-id { font-weight: 700; }
.cal-week-order-addr { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-week-order-win { color: var(--gray-500); white-space: nowrap; }

/* ══════════════════════════════════════════════════════════════════════════
   RIDER · "Mi semana" (pedidos programados, tanda 8) + la línea del Inicio
   que lleva hasta ahí. Vista de LECTURA: ningún elemento de acá es un botón
   que toque una parada, y el lenguaje visual es deliberadamente más apagado
   que el del Inicio (blanco, sin sombras de color) — lo que se opera es el
   mapa, esto solo informa.
   ══════════════════════════════════════════════════════════════════════════ */

/* Línea "lo que te viene" en el Inicio. */
.dash-next {
    display: flex; align-items: center; gap: 12px;
    margin-top: 18px; padding: 13px 14px;
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
    text-decoration: none; color: inherit;
}
.dash-next.is-quiet { background: #f8fafc; }
.dash-next-icon {
    flex: none; width: 38px; height: 38px; border-radius: 12px;
    display: grid; place-items: center;
    background: #eef4fd; color: var(--primary, #4A90E2); font-size: 1.1rem;
}
.dash-next-body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.dash-next-title { font-size: 0.94rem; font-weight: 700; color: #0f172a; line-height: 1.25; }
.dash-next-title b { font-weight: 800; }
.dash-next-sub { font-size: 0.74rem; font-weight: 600; color: #94a3b8; }
.dash-next-chev { flex: none; color: #cbd5e1; font-size: 1.05rem; }

/* Cabecera y navegación de semana. */
.rw-back {
    display: inline-flex; align-items: center; gap: 6px;
    font-size: 0.82rem; font-weight: 700; color: #64748b; text-decoration: none;
}
.rw-nav {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    margin-top: 6px; padding: 10px 12px;
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
}
.rw-nav-btn {
    flex: none; width: 38px; height: 38px; border-radius: 12px;
    display: grid; place-items: center;
    background: #f1f5f9; color: #475569; text-decoration: none; font-size: 1.05rem;
}
.rw-nav-title { text-align: center; line-height: 1.2; }
.rw-nav-title strong { display: block; font-size: 1rem; font-weight: 800; color: #0f172a; }
.rw-nav-title span { font-size: 0.74rem; font-weight: 600; color: #94a3b8; }

/* Totales de la semana (mismo lenguaje que .dash-daysum). */
.rw-summary {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
    margin-top: 12px; padding: 14px 10px; text-align: center;
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
}
.rw-summary > div { display: flex; flex-direction: column; gap: 2px; }
.rw-sum-num {
    font-size: 1.45rem; font-weight: 800; color: #0f172a;
    font-variant-numeric: var(--num);
}
.rw-sum-num small { font-size: 0.72rem; font-weight: 700; color: #94a3b8; margin: 0 1px; }
.rw-sum-cap { font-size: 0.72rem; font-weight: 600; color: #94a3b8; }

/* Un día por tarjeta: en un teléfono el eje útil es el día, no la grilla
   riders × días del dispatcher. */
.rw-days { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 10px; }
.rw-day {
    padding: 12px 14px;
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
}
.rw-day.is-today { border-color: var(--primary, #4A90E2); box-shadow: 0 0 0 1px var(--primary, #4A90E2) inset; }
.rw-day.is-past { opacity: 0.62; }
.rw-day.is-empty { background: #f8fafc; }

.rw-day-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 10px; flex-wrap: wrap;
}
.rw-day-when { display: flex; align-items: baseline; gap: 7px; }
.rw-day-name { font-size: 0.95rem; font-weight: 800; color: #0f172a; }
.rw-day-num { font-size: 0.95rem; font-weight: 700; color: #94a3b8; font-variant-numeric: var(--num); }
.rw-today-tag {
    font-size: 0.66rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em;
    color: var(--primary, #4A90E2); background: #eef4fd;
    padding: 2px 7px; border-radius: 999px;
}
.rw-day-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.rw-chip {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 0.72rem; font-weight: 700; color: #475569;
    background: #f1f5f9; border-radius: 999px; padding: 3px 9px;
}
.rw-chip.is-shift { background: #eef4fd; color: #1d4ed8; }
.rw-chip i { font-size: 0.8rem; }

/* Las rutas del día. Se muestra el RECORRIDO (cuántas paradas, en qué franja),
   no la lista de pedidos: el rider ejecuta paradas, no gestiona pedidos. */
.rw-routes { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 7px; }
.rw-route {
    padding: 9px 11px; border-radius: 13px;
    background: #f8fafc; border: 1px solid #eef1f5;
}
.rw-route.is-done { background: #f6fdf8; border-color: #d6f0de; }
/* La fila entera es el enlace al mapa (modo consulta): sin subrayado ni color de
   enlace — se lee como la tarjeta que ya era, solo que se puede tocar. */
.rw-route-link { display: block; text-decoration: none; color: inherit; }
.rw-route-main { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.rw-route-name { font-size: 0.9rem; font-weight: 800; color: #0f172a; }
.rw-route-stops { font-size: 0.82rem; font-weight: 700; color: #475569; font-variant-numeric: var(--num); }
.rw-route-meta {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 3px;
    font-size: 0.74rem; font-weight: 600; color: #94a3b8;
}
.rw-route-window { display: inline-flex; align-items: center; gap: 5px; }
.rw-route-state.is-open { color: #b45309; }
.rw-route-state.is-done { color: #15803d; display: inline-flex; align-items: center; gap: 5px; }
.rw-route-note { margin: 4px 0 0; font-size: 0.7rem; font-weight: 600; color: #b45309; }

.rw-day-empty { margin: 8px 0 0; font-size: 0.8rem; font-weight: 600; color: #94a3b8; }

.rw-foot {
    margin: 16px 0 0; font-size: 0.76rem; font-weight: 600; color: #94a3b8;
    line-height: 1.4;
}
.rw-cta {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    margin-top: 14px; min-height: 46px; border-radius: 14px;
    background: #f1f5f9; color: #334155; font-size: 0.9rem; font-weight: 800;
    text-decoration: none;
}

/* ── Historial de rutas del rider (.rh-*) ──────────────────────────────────
   Hermano de "Mi semana": misma tarjeta blanca sobre el fondo del rider, misma
   escala tipográfica. Vive acá y no en un <style> de la plantilla, igual que
   los `.rw-*` y los `.cal-*`. */
.rh-filter {
    display: flex; align-items: flex-end; gap: 8px;
    margin-top: 12px; padding: 12px;
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
}
.rh-filter label { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.rh-filter label span { font-size: 0.72rem; font-weight: 700; color: #94a3b8; }
.rh-filter input {
    width: 100%; min-height: 40px; padding: 0 10px;
    border: 1px solid #e2e8f0; border-radius: 12px;
    font-size: 0.85rem; font-weight: 700; color: #0f172a; background: #f8fafc;
}
.rh-filter button {
    flex: none; width: 44px; height: 40px; border: 0; border-radius: 12px;
    background: var(--primary, #4A90E2); color: #fff; font-size: 1rem;
}

.rh-empty { margin-top: 28px; text-align: center; color: #94a3b8; }
.rh-empty i { font-size: 2rem; }
.rh-empty p { margin: 10px 0 0; font-size: 0.85rem; font-weight: 600; }

.rh-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 10px; }
.rh-item {
    padding: 12px 14px;
    background: #fff; border: 1px solid #eceef2; border-radius: 18px;
}
.rh-item-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.rh-item-name { margin: 0; font-size: 0.95rem; font-weight: 800; color: #0f172a; }
.rh-item-day { margin: 2px 0 0; font-size: 0.74rem; font-weight: 600; color: #94a3b8; }
.rh-item-total {
    flex: none; font-size: 1.3rem; font-weight: 800; color: #0f172a;
    font-variant-numeric: var(--num);
}
.rh-letter {
    display: inline-block; margin-left: 4px; padding: 1px 6px; border-radius: 7px;
    background: #f1f5f9; color: #475569; font-size: 0.72rem; font-weight: 800;
}

.rh-marks { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.rh-mark {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 3px 8px; border-radius: 9px;
    font-size: 0.74rem; font-weight: 700; background: #f1f5f9; color: #475569;
}
.rh-mark.is-done { background: #eaf7ee; color: #15803d; }
.rh-mark.is-fail { background: #fdecec; color: #b91c1c; }
.rh-mark.is-hand { background: #f3eefd; color: #6d28d9; }
.rh-mark.is-open { background: #fdf4e6; color: #b45309; }

/* "Ver en el mapa": abre el recorrido archivado en el mapa de siempre, en modo
   consulta. Es la acción PRINCIPAL de la fila; ocultar es la secundaria. */
.rh-map {
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    width: 100%; min-height: 40px; margin-top: 10px; text-decoration: none;
    border: 1px solid #dbeafe; border-radius: 12px; background: #eff6ff;
    color: #1d4ed8; font-size: 0.82rem; font-weight: 800;
}
.rh-hide { margin-top: 10px; }
.rh-hide button, .rh-range button {
    width: 100%; min-height: 40px; border: 1px solid #e2e8f0; border-radius: 12px;
    background: #f8fafc; color: #64748b; font-size: 0.8rem; font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
}
.rh-range { margin-top: 14px; }
.rh-range button { border-style: dashed; }

/* ==========================================================================
   Acceso (login) — pantalla partida
   Izquierda: panel de marca (tinta). Derecha: el formulario de Flask de
   siempre, solo re-estilizado. En móvil pasa a una columna con la marca
   ARRIBA (el panel va primero en el DOM, así que el orden sale solo).
   ========================================================================== */

.login-split {
    display: grid;
    grid-template-columns: 1fr;
    min-height: 100vh;
}
@media (min-width: 900px) {
    .login-split { grid-template-columns: 1fr 1fr; }
}

/* El pie global de base.html lo comparten todas las vistas anónimas; bajo la
   pantalla partida sobra (deja una franja muerta tras un panel a 100vh) y el
   copyright ya no aporta nada al lado del panel de marca. */
body:has(.login-split) > footer { display: none; }

/* ── Panel de marca ─────────────────────────────────────────────────────── */
/* El fondo NO es un plano liso: dos halos muy tenues (verde arriba-izquierda,
   tinta clara abajo-derecha) le dan profundidad sin competir con el texto.
   Van en `background-image` sobre el color, así que el fallback es el plano
   de siempre. */
.login-brand {
    position: relative;
    background-color: var(--rodago-tinta);
    background-image:
        radial-gradient(60% 55% at 12% 8%,  rgba(21,199,126,0.16), transparent 70%),
        radial-gradient(55% 50% at 92% 96%, rgba(85,120,190,0.16), transparent 70%);
    color: #fff;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--space-6);
    padding: 48px 32px;
    overflow: hidden;
}
@media (min-width: 900px) {
    .login-brand { padding: 56px clamp(32px, 5vw, 72px); }
}

/* El logotipo es lo primero que se lee: manda en la jerarquía del panel. */
.login-brand-logo {
    height: clamp(52px, 8vw, 96px);
    width: auto;
    max-width: 100%;
    display: block;
}

.login-lema {
    margin: 0;
    color: var(--rodago-tinta-text);
    font-size: 1rem;
    line-height: 1.5;
}

.login-features {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
}

.login-feature {
    background: var(--rodago-tinta-2);
    border: 1px solid rgba(255,255,255,0.07);
    border-radius: 14px;
    padding: var(--space-6) var(--space-5);
    text-align: center;
}
.login-feature-icon {
    width: 32px; height: 32px;
    color: var(--rodago-verde);
    margin-bottom: var(--space-3);
}
.login-feature-title {
    margin: 0 0 var(--space-2);
    color: var(--rodago-verde);
    font-size: 1rem;
    font-weight: 700;
}
.login-feature-text {
    margin: 0;
    color: #fff;
    font-size: 0.9rem;
    line-height: 1.45;
}

/* ── La separación del medio ────────────────────────────────────────────── */
/* Un corte a hueso entre tinta y blanco se lee como dos capturas pegadas. La
   junta lleva tres cosas: una línea fina que se DESVANECE por los extremos y
   se enciende en verde en el centro, la sombra del panel de marca cayendo
   sobre el claro, y un lavado del lado claro que recoge esa sombra. Solo en
   dos columnas: en móvil los paneles se apilan y no hay junta vertical. */
@media (min-width: 900px) {
    .login-brand {
        z-index: 1;                                  /* la sombra por encima del panel claro */
        box-shadow: 18px 0 40px -18px rgba(22,35,61,0.45);
    }
    .login-brand::after {
        content: "";
        position: absolute;
        top: 0; right: 0; bottom: 0;
        width: 2px;
        background: linear-gradient(
            to bottom,
            transparent 0%,
            rgba(21,199,126,0.15) 22%,
            rgba(21,199,126,0.75) 50%,
            rgba(21,199,126,0.15) 78%,
            transparent 100%);
    }
    .login-access {
        background-image: linear-gradient(
            to right, rgba(22,35,61,0.055), rgba(22,35,61,0) 40%);
    }
}

/* ── Panel de acceso ────────────────────────────────────────────────────── */
.login-access {
    background: var(--surface);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 48px 24px;
}

.login-form-wrap { width: 100%; max-width: 380px; }

/* Marca en pequeño sobre el formulario: el logotipo grande ya está enfrente,
   acá solo hace falta que el formulario no se lea como una caja anónima. */
.login-lockup {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-6);
}
.login-lockup-mark { height: 34px; width: 34px; display: block; }
.login-lockup-word {
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--rodago-tinta);
}

.login-title {
    margin: 0 0 var(--space-1);
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--gray-900);
}
.login-subtitle {
    margin: 0 0 var(--space-6);
    color: var(--gray-500);
    font-size: 0.95rem;
}

.login-field { margin-bottom: var(--space-4); }
.login-label {
    display: block;
    margin-bottom: var(--space-2);
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--gray-700);
}
/* El campo arranca con un relleno apenas gris y se ABRE a blanco al enfocarlo:
   es lo que hace que el formulario no se lea como una hoja lisa. */
.login-input {
    width: 100%;
    padding: 13px 14px;
    font-size: 0.95rem;
    color: var(--gray-900);
    background: var(--gray-50);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    transition: border-color .15s, box-shadow .15s, background-color .15s;
}
.login-input::placeholder { color: var(--gray-400); }
.login-input:hover:not(:focus) { border-color: var(--gray-400); }
.login-input:focus {
    outline: none;
    background: var(--surface);
    border-color: var(--rodago-verde);
    box-shadow: 0 0 0 3px rgba(21,199,126,0.18);
}
.login-input.is-invalid { border-color: var(--danger); }

/* Los errores de validación del form siguen siendo los mismos, solo cambia
   cómo se ven. */
.login-error {
    margin-top: var(--space-2);
    color: var(--danger);
    font-size: 0.8rem;
}

.login-submit {
    width: 100%;
    padding: 13px 16px;
    margin-top: var(--space-2);
    border: none;
    border-radius: var(--radius-md);
    background: var(--rodago-verde);
    color: #fff;
    font-size: 0.95rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 6px 16px -6px rgba(21,199,126,0.65);
    transition: background .15s, box-shadow .15s, transform .15s;
}
.login-submit:hover {
    background: var(--rodago-verde-dark);
    box-shadow: 0 10px 22px -8px rgba(21,199,126,0.75);
    transform: translateY(-1px);
}
.login-submit:active { transform: translateY(0); }
.login-submit:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(21,199,126,0.35);
}

/* La línea de aceptación, bajo el botón de entrar: se lee, no compite. */
.login-legal {
    margin: var(--space-3) 0 0;
    font-size: 0.78rem; line-height: 1.5;
    color: var(--gray-500);
}
.login-legal a { color: inherit; text-decoration: underline; }

.login-remember {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-4);
}
.login-remember input {
    width: 16px; height: 16px;
    accent-color: var(--rodago-verde);
    margin: 0;
    cursor: pointer;
}
.login-remember label {
    margin: 0;
    font-size: 0.85rem;
    color: var(--gray-500);
    cursor: pointer;
}

/* «¿Has olvidado tu contraseña?» y los enlaces de vuelta de las pantallas de
   recuperación: texto, no botón. */
.login-link-row {
    margin-top: var(--space-4);
    font-size: 0.85rem;
}
.login-link {
    color: var(--rodago-verde-dark);
    font-weight: 600;
    text-decoration: none;
}
.login-link:hover { text-decoration: underline; }
/* El aviso fijo de «si ese email existe…»: un toast desaparece solo. */
.login-notice {
    margin: 0 0 var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    background: rgba(21,199,126,0.10);
    color: var(--gray-700);
    font-size: 0.9rem;
    line-height: 1.45;
}

/* ── El acceso EN EL TELÉFONO ───────────────────────────────────────────────
   En el escritorio la pantalla partida enseña la marca al lado del formulario:
   los dos se ven de una vez. Apilada tal cual, en un móvil eso son DOS
   pantallas —el panel de marca solo ya medía 100vh— y había que desplazar para
   llegar a los campos. Entrar es lo único que se hace acá, así que el
   formulario tiene que estar A LA VISTA sin un gesto previo.

   La marca no se tira: se COMPRIME a una banda superior (logotipo + lema + los
   cuatro rasgos reducidos a icono y palabra) y el formulario se queda con todo
   el alto que sobra, centrado. El corte es el de la propia pantalla partida
   (900px), no los 767.98px del panel del dispatcher: acá lo que decide es
   cuándo el grid deja de ser de dos columnas.

   `svh` (alto de viewport CON la barra del navegador a la vista) y no `vh`: en
   iOS y Chrome móvil `100vh` es el viewport EXPANDIDO, así que la última fila
   —el "recordarme"— queda tapada justo hasta que uno desplaza, que es
   exactamente lo que esta sección viene a evitar. `vh` queda antes como
   fallback para el navegador que no entienda la unidad.
   ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 899.98px) {
    .login-split {
        min-height: 100vh;
        min-height: 100svh;
        grid-template-rows: auto 1fr;   /* la marca ocupa lo que pide; el resto es del formulario */
    }

    .login-brand {
        gap: var(--space-3);
        padding: max(20px, env(safe-area-inset-top)) 20px 20px;
        text-align: center;
    }
    .login-brand-logo {
        height: clamp(36px, 11vw, 52px);
        margin-inline: auto;
    }
    .login-lema { font-size: 0.86rem; line-height: 1.4; }

    /* Los cuatro rasgos pasan de tarjetas 2×2 a una fila de cuatro: se quedan
       el icono y la palabra, que es lo que se lee de un vistazo. La frase de
       cada uno es lo que costaba media pantalla. */
    .login-features { grid-template-columns: repeat(4, 1fr); gap: var(--space-2); }
    .login-feature {
        background: none;
        border: 0;
        padding: 0;
    }
    .login-feature-icon { width: 22px; height: 22px; margin-bottom: 4px; }
    .login-feature-title { margin: 0; font-size: 0.68rem; line-height: 1.2; }
    .login-feature-text { display: none; }

    /* El lockup pequeño existe para que el formulario no se lea como una caja
       anónima cuando la marca está ENFRENTE; apilado queda el mismo logotipo
       dos veces a 200px de distancia. */
    .login-lockup { display: none; }

    .login-access {
        padding: var(--space-5) 20px calc(var(--space-5) + env(safe-area-inset-bottom));
    }
    .login-title { font-size: 1.4rem; }
    .login-subtitle { margin-bottom: var(--space-5); font-size: 0.88rem; }

    /* Mismo criterio que `.form-touch`: 16px para que iOS no haga zoom al
       enfocar y 44px de objetivo táctil. */
    .login-input, .login-submit { font-size: 16px; min-height: 44px; }
}

/* Teléfono en horizontal (o pantalla muy corta): ni comprimida entra la banda
   de marca entera. Lo que se va es lo prescindible —el lema y los rasgos—; el
   logotipo se queda, porque sin él la pantalla no dice de qué app es. */
@media (max-width: 899.98px) and (max-height: 620px) {
    .login-lema, .login-features { display: none; }
    .login-brand { padding-bottom: 14px; }
}

/* ==========================================================================
   PERFILES (rider y empresa)
   ==========================================================================
   Los dos perfiles se leen igual: identidad y acciones arriba, las cifras en
   una `.ops-bar` (la misma del dashboard) y debajo dos columnas — la ficha a la
   izquierda, lo que se consulta a la derecha.

   La regla de fondo es qué NO vive en un perfil: cualquier lista que pueda
   crecer sin techo. Los pedidos tienen su propia vista filtrable
   (`admin_company_orders` / `admin_rider_orders`); lo que se queda va plegado en
   un `<details>` con la CUENTA en el resumen, para que se sepa cuánto hay sin
   tener que abrirlo.
   ========================================================================== */
.prof-head { display: flex; align-items: center; gap: 12px; min-width: 0; }
/* La calificación se escribe en un campo de dos cifras: medía por `style=` en
   línea y ahora sigue a la densidad como el resto de los controles. */
.prof-rating-input { width: 5rem; }
.prof-avatar {
    width: 56px; height: 56px; flex: 0 0 56px;
    border-radius: 50%; object-fit: cover;
}
.prof-avatar-ph {
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--surface-sunken);
    border: 1px solid var(--gray-200);
    color: var(--gray-400);
    font-size: 1.5rem;
}

/* ==========================================================================
   LA CONFIGURACIÓN (`/settings`) — namespace `.cfg-*`
   ==========================================================================
   Las mismas piezas en todas las secciones, para los tres roles (el panel
   oscuro, el portal y el rider) y para las DOS formas de abrirla: dentro del
   MODAL (`.cfg-modal`, un <dialog> que `settings.js` llena) y como página
   entera (`.settings-page`). Por eso TODO el color sale de los tokens
   (`--surface`, `--gray-*`) y no hay una regla por rol.

   Piezas: `.cfg` (dos columnas: `.cfg-side` con el título y las secciones,
   `.cfg-main` con la sección) · `.cfg-card` (un grupo: título y, como mucho, un
   subtítulo) · `.cfg-row` (etiqueta · valor, con su barra debajo si la lleva) ·
   `.cfg-bar` (UN color, también por encima del límite: no se alarma a quien
   puede pasarse y pagar el extra) · `.cfg-month` (un mes que se despliega en su
   desglose) · `.cfg-inline` (un dato que se edita en el sitio) · `.cfg-plan-*`
   y `.cfg-cmp` (los planes y la lista que los compara).

   TAMAÑO: la configuración NO sigue a la densidad del panel. El raíz encogido
   (`--ui-scale`) la dejaba pequeña, así que dentro de `.cfg` los `--space-*` y
   cada `font-size` se multiplican por `--cfg-k`, la inversa de la escala (en el
   teléfono vale 1 sola; en el rider, que no escala, se fija a 1). */
.cfg,
.cfg-modal {
    --cfg-k: var(--ui-unscale);
    --space-1: calc(0.25rem * var(--cfg-k));
    --space-2: calc(0.5rem  * var(--cfg-k));
    --space-3: calc(0.75rem * var(--cfg-k));
    --space-4: calc(1rem    * var(--cfg-k));
    --space-5: calc(1.25rem * var(--cfg-k));
    --space-6: calc(1.5rem  * var(--cfg-k));
}
html.role-rider .cfg { --cfg-k: 1; }

.cfg {
    display: grid; grid-template-columns: calc(14.5rem * var(--cfg-k)) minmax(0, 1fr);
    background: var(--surface); color: var(--gray-900);
    font-size: calc(0.95rem * var(--cfg-k)); line-height: 1.5;
    min-height: 0;
}
.cfg.is-single { grid-template-columns: minmax(0, 1fr); }
.cfg.is-single .cfg-side { display: none; }

.cfg-side {
    background: var(--surface-sunken); border-right: 1px solid var(--gray-200);
    padding: var(--space-5) var(--space-3); min-width: 0;
}
.cfg-title {
    font-size: calc(1.15rem * var(--cfg-k)); font-weight: 600; color: var(--gray-900);
    margin: 0 0 var(--space-4); padding: 0 var(--space-3);
}
.cfg-nav { display: flex; flex-direction: column; gap: var(--space-1); }
.cfg-nav-link {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
    color: var(--gray-500); font-size: calc(0.95rem * var(--cfg-k));
    text-decoration: none; white-space: nowrap;
}
.cfg-nav-link:hover { background: color-mix(in srgb, var(--gray-500) 12%, transparent); color: var(--gray-900); }
.cfg-nav-link.is-active {
    background: color-mix(in srgb, var(--cfg-bar) 18%, transparent);
    color: var(--gray-900); font-weight: 600;
}
/* La versión del despliegue: una línea discreta al pie de las secciones. */
.cfg-version {
    margin: var(--space-5) 0 0; padding: 0 var(--space-3);
    color: var(--gray-500); font-size: calc(0.78rem * var(--cfg-k));
    font-variant-numeric: tabular-nums; white-space: nowrap;
}

.cfg-main { padding: var(--space-6) calc(var(--space-6) * 1.5); min-width: 0; }
.cfg-section-title {
    font-size: calc(1.4rem * var(--cfg-k)); font-weight: 600; color: var(--gray-900);
    margin: 0 0 var(--space-5);
}

/* Como PÁGINA: el mismo panel, con su borde, centrado en el contenido. */
.app-content .page-wrap.settings-page { max-width: none; }
.settings-page .cfg {
    max-width: calc(66rem * var(--cfg-k)); margin: 0 auto;
    border: 1px solid var(--gray-200); border-radius: var(--radius-lg); overflow: hidden;
    min-height: calc(32rem * var(--cfg-k));
}

/* Como MODAL: un <dialog> grande. El panel lo llena; la sección rueda dentro. */
.cfg-modal {
    padding: 0; border: 0; border-radius: var(--radius-lg); overflow: hidden;
    background: var(--surface); color: var(--gray-900);
    width: min(calc(68rem * var(--cfg-k)), calc(100vw - 2rem));
    height: min(calc(46rem * var(--cfg-k)), calc(100vh - 3rem));
    max-width: none; max-height: none;
    box-shadow: 0 24px 64px color-mix(in srgb, var(--rodago-tinta) 45%, transparent);
}
.cfg-modal::backdrop { background: color-mix(in srgb, var(--rodago-tinta) 62%, transparent); }
.cfg-modal[open] { animation: cfg-modal-in 0.16s ease-out; }
@keyframes cfg-modal-in {
    from { opacity: 0; translate: 0 0.5rem; }
    to   { opacity: 1; translate: 0 0; }
}
.cfg-modal-body,
.cfg-modal .cfg { height: 100%; }
.cfg-modal .cfg-side,
.cfg-modal .cfg-main { overflow-y: auto; min-height: 0; }
.cfg-modal .cfg-main { transition: opacity 0.12s ease; }
.cfg-modal.is-loading .cfg-main { opacity: 0.5; pointer-events: none; }
.cfg-modal-close {
    position: absolute; top: var(--space-4); right: var(--space-4); z-index: 2;
    width: calc(2.25rem * var(--cfg-k)); height: calc(2.25rem * var(--cfg-k));
    display: grid; place-items: center;
    border: 0; border-radius: 50%; background: transparent;
    color: var(--gray-500); font-size: calc(1rem * var(--cfg-k)); cursor: pointer;
}
.cfg-modal-close:hover { background: var(--surface-sunken); color: var(--gray-900); }

/* Los avisos, dentro del modal: un tono para todos. */
.cfg-flash-item {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-3); margin: 0 0 var(--space-4);
    border-radius: var(--radius-md); background: var(--surface-sunken); color: var(--gray-900);
}
.cfg-flash-item.is-error { color: var(--stop-fail-ink); }

/* Un grupo: sin caja. Los separa una línea. */
.cfg-card { padding-bottom: var(--space-5); margin-bottom: var(--space-5); border-bottom: 1px solid var(--gray-200); }
.cfg-card:last-child { padding-bottom: 0; margin-bottom: 0; border-bottom: 0; }
.cfg-card-head { margin-bottom: var(--space-3); }
.cfg-card-title { font-size: calc(1.05rem * var(--cfg-k)); font-weight: 600; margin: 0; color: var(--gray-900); }
.cfg-card-title.is-plan { font-size: calc(1.3rem * var(--cfg-k)); }
.cfg-card-sub { font-size: calc(0.88rem * var(--cfg-k)); color: var(--gray-500); margin: var(--space-1) 0 0; }

.cfg-row {
    display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    gap: var(--space-2) var(--space-4); align-items: baseline;
    padding: var(--space-3) 0; border-top: 1px solid var(--gray-200);
}
.cfg-row:first-child { border-top: 0; padding-top: 0; }
.cfg-row:last-child { padding-bottom: 0; }
.cfg-row-label { color: var(--gray-500); min-width: 0; }
.cfg-row-value {
    color: var(--gray-900); font-weight: 500; text-align: right; min-width: 0;
    font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.cfg-row-link { color: var(--gray-900); text-decoration: underline; text-underline-offset: 0.2em; }
.cfg-row-link:hover { color: var(--cfg-bar); }

/* La barra: un <progress> nativo (el ancho lo pone el navegador, sin medidas
   en línea). La pista es el MISMO azul, más claro. */
.cfg-bar {
    grid-column: 1 / -1; display: block; width: 100%; height: calc(0.5rem * var(--cfg-k));
    border: 0; border-radius: 999px; overflow: hidden; appearance: none;
    background: color-mix(in srgb, var(--cfg-bar) 18%, transparent);
    accent-color: var(--cfg-bar);
}
.cfg-bar::-webkit-progress-bar { background: color-mix(in srgb, var(--cfg-bar) 18%, transparent); }
.cfg-bar::-webkit-progress-value { background: var(--cfg-bar); border-radius: 999px; }
.cfg-bar::-moz-progress-bar { background: var(--cfg-bar); border-radius: 999px; }

.cfg-list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-2); }
.cfg-list li { display: flex; align-items: baseline; gap: var(--space-2); color: var(--gray-900); }
.cfg-list .bi-check2 { color: var(--rodago-verde); }
.cfg-list a { color: inherit; }

.cfg-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-top: var(--space-4); }
.cfg-actions.is-split { justify-content: space-between; }
.cfg-link { font-size: calc(0.88rem * var(--cfg-k)); color: var(--gray-500); text-decoration: underline; cursor: pointer; }
.cfg-link:hover { color: var(--gray-900); }
.cfg-foot { margin: var(--space-4) 0 0; font-size: calc(0.88rem * var(--cfg-k)); color: var(--gray-500); }
/* Portal de locales → fotos (specs/016): dos modos y la lista de casillas, con
   alto máximo y su propio scroll para que cien locales no estiren la sección. */
.cfg-photos-status { margin: 0 0 var(--space-3); color: var(--gray-900); font-weight: 500; }
.cfg-photos-modes { border: 0; padding: 0; margin: 0 0 var(--space-3); display: grid; gap: var(--space-2); }
.cfg-photos-mode {
    display: flex; align-items: flex-start; gap: var(--space-3); cursor: pointer;
    padding: var(--space-3); border: 1px solid var(--gray-200); border-radius: var(--radius-md);
}
.cfg-photos-mode:has(input:checked) { border-color: var(--cfg-bar); }
.cfg-photos-mode input { margin-top: 0.2em; flex: none; }
.cfg-photos-mode strong { display: block; color: var(--gray-900); font-weight: 600; }
.cfg-photos-mode small,
.cfg-photos-item small { color: var(--gray-500); font-size: calc(0.82rem * var(--cfg-k)); }
.cfg-photos-list[hidden] { display: none; }
.cfg-photos-tools { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.cfg-photos-tools input { flex: 1 1 12rem; min-width: 0; }
.cfg-photos-counter { margin: var(--space-2) 0; font-size: calc(0.85rem * var(--cfg-k)); color: var(--gray-500); }
.cfg-photos-items {
    list-style: none; margin: 0; padding: var(--space-1) 0; max-height: 18rem; overflow-y: auto;
    border: 1px solid var(--gray-200); border-radius: var(--radius-md);
}
.cfg-photos-items li[hidden] { display: none; }
.cfg-photos-item {
    display: flex; align-items: center; gap: var(--space-2); cursor: pointer; margin: 0;
    padding: var(--space-2) var(--space-3); color: var(--gray-900); min-width: 0;
}
.cfg-photos-item span { min-width: 0; overflow-wrap: anywhere; }
.cfg-photos-item small { margin-left: auto; }
.cfg-photos-item.is-off span { color: var(--gray-500); }
.cfg-photos-empty { margin: var(--space-2) 0 0; color: var(--gray-500); font-size: calc(0.88rem * var(--cfg-k)); }
.cfg-photos-warning {
    margin: var(--space-3) 0 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
    border-left: 3px solid var(--warning); background: var(--surface-sunken); color: var(--gray-900);
}
.cfg-photos-warning[hidden] { display: none; }
.cfg-disclosure > summary { list-style: none; display: inline-flex; }
.cfg-disclosure > summary::-webkit-details-marker { display: none; }
.cfg-disclosure[open] > summary { margin-bottom: var(--space-2); }

/* CUENTA: la identidad, y el dato que se edita en el sitio. */
.cfg-identity { display: flex; align-items: center; gap: var(--space-4); margin-bottom: var(--space-5); min-width: 0; }
.cfg-identity .prof-avatar {
    width: calc(4rem * var(--cfg-k)); height: calc(4rem * var(--cfg-k));
    flex: 0 0 calc(4rem * var(--cfg-k));
}
.cfg-identity-text { min-width: 0; }
.cfg-identity-name { display: block; color: var(--gray-900); font-size: calc(1.15rem * var(--cfg-k)); font-weight: 600; }
.cfg-role { display: block; font-size: calc(0.88rem * var(--cfg-k)); color: var(--gray-500); }

/* El dato ES un campo, dibujado como texto: sin caja ni fondo. El lápiz aparece
   al pasar por encima; al escribir, una línea fina debajo y nada más. */
.cfg-inline { display: inline-flex; align-items: center; gap: var(--space-1); margin: 0; max-width: 100%; min-width: 0; }
.cfg-inline-input {
    font: inherit; color: inherit; background: transparent;
    border: 0; border-bottom: 1px solid transparent; border-radius: 0;
    padding: 0; margin: 0; min-width: 0; max-width: 100%;
    field-sizing: content; outline: 0; text-overflow: ellipsis;
}
.cfg-inline-input:focus { border-bottom-color: var(--cfg-bar); }
.cfg-inline-pencil {
    flex: 0 0 auto; border: 0; background: transparent; cursor: pointer;
    padding: 0 var(--space-1); color: var(--gray-400); font-size: 0.85em; line-height: 1;
    opacity: 0; transition: opacity 0.12s ease;
}
.cfg-inline:hover .cfg-inline-pencil,
.cfg-inline:focus-within .cfg-inline-pencil { opacity: 1; }
.cfg-row-value.is-inline .cfg-inline { flex-direction: row-reverse; }
.cfg-row-value.is-inline .cfg-inline-input { text-align: right; }
/* En táctil no existe «pasar por encima»: el lápiz queda a la vista. */
@media (hover: none) {
    .cfg-inline-pencil { opacity: 1; }
}

/* USO: un mes que se despliega en su desglose (<details>, sin JS). */
.cfg-month { border-top: 1px solid var(--gray-200); }
.cfg-months .cfg-month:first-child { border-top: 0; }
.cfg-month.is-current { margin-top: var(--space-4); }
.cfg-month-head {
    display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
    gap: var(--space-3); align-items: center;
    padding: var(--space-3) 0; cursor: pointer; list-style: none;
}
.cfg-month-head::-webkit-details-marker { display: none; }
.cfg-month-name { display: block; color: var(--gray-900); font-weight: 500; }
.cfg-month-name::first-letter { text-transform: uppercase; }
.cfg-month-name small { font-weight: 400; color: var(--gray-500); }
.cfg-month-amount { color: var(--gray-900); font-weight: 600; font-variant-numeric: tabular-nums; }
.cfg-month-amount.is-none { font-weight: 400; color: var(--gray-500); }
.cfg-month-chevron { color: var(--gray-400); font-size: 0.85em; transition: rotate 0.15s ease; }
.cfg-month[open] .cfg-month-chevron { rotate: 180deg; }
.cfg-month[open] > .cfg-breakdown:last-child { padding-bottom: var(--space-3); }
.cfg-month.is-flat .cfg-month-head { cursor: default; }
/* Una factura: el día, en qué va el pago, el total. */
.cfg-month-head.is-invoice { grid-template-columns: minmax(0, 1fr) auto calc(7rem * var(--cfg-k)) calc(1rem * var(--cfg-k)); }
.cfg-month-head.is-invoice .cfg-month-amount { text-align: right; }
.cfg-pill {
    font-size: calc(0.78rem * var(--cfg-k)); font-weight: 600; white-space: nowrap;
    padding: 0 var(--space-2); border-radius: 999px; color: var(--gray-900);
    background: var(--surface-sunken);
}
.cfg-pill.is-pagada { background: color-mix(in srgb, var(--rodago-verde) 22%, transparent); }
.cfg-pill.is-pendiente { background: color-mix(in srgb, var(--cfg-bar) 22%, transparent); }
.cfg-pill.is-cancelada { color: var(--gray-500); text-decoration: line-through; }
.cfg-breakdown-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) calc(4rem * var(--cfg-k)) calc(7rem * var(--cfg-k));
    gap: var(--space-3); padding: var(--space-1) 0 var(--space-1) var(--space-4);
    color: var(--gray-500); font-size: calc(0.9rem * var(--cfg-k));
}
.cfg-breakdown-units,
.cfg-breakdown-amount { text-align: right; font-variant-numeric: tabular-nums; }
.cfg-breakdown-amount { color: var(--gray-900); }

/* PLAN: los tres planes a la vista y la lista que los compara.

   Las tarjetas son las <label> de tres radios (`#cfg-plan-<clave>`), hermanos
   de todo lo demás, así que el estado entero es `:checked ~`: sin JS. Una fila
   lleva `in-<plan>` por cada plan que la trae, `is-mine` si la trae el plan de
   hoy y `up-<plan>` si en ese plan el límite sube. **Un plan nuevo en el
   catálogo necesita su juego de selectores acá** (hay test). */
/* Cómo se pagan los precios de las tarjetas, y lo que ahorra el anual. */
.cfg-plan-note {
    display: flex; align-items: center; gap: var(--space-2);
    margin: 0 0 var(--space-3); padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md); color: var(--gray-900);
    background: color-mix(in srgb, var(--rodago-verde) 14%, transparent);
}
.cfg-plan-note .bi { color: var(--rodago-verde-dark); }
.cfg-plan-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); }
.cfg-plan-card {
    display: flex; flex-direction: column; gap: var(--space-1); margin: 0;
    padding: var(--space-4); border: 1px solid var(--gray-200); border-radius: var(--radius-lg);
    cursor: pointer; transition: border-color 0.15s ease, background-color 0.15s ease, translate 0.15s ease;
}
.cfg-plan-card:hover { border-color: var(--cfg-bar); translate: 0 -2px; }
.cfg-plan-card-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.cfg-plan-card-name { font-size: calc(1.1rem * var(--cfg-k)); font-weight: 600; color: var(--gray-900); }
.cfg-plan-card-price { font-weight: 600; color: var(--gray-900); font-variant-numeric: tabular-nums; }
.cfg-plan-card-price small { font-weight: 400; color: var(--gray-500); }
.cfg-plan-card-pitch { font-size: calc(0.88rem * var(--cfg-k)); color: var(--gray-500); }
.cfg-plan-badge {
    font-size: calc(0.75rem * var(--cfg-k)); font-weight: 600; white-space: nowrap;
    padding: 0 var(--space-2); border-radius: 999px;
    background: var(--surface-sunken); color: var(--gray-500);
}
.cfg-plan-badge.is-gain { background: color-mix(in srgb, var(--rodago-verde) 20%, transparent); color: var(--gray-900); }

#cfg-plan-base:checked ~ .cfg-plan-cards [for="cfg-plan-base"],
#cfg-plan-pro:checked ~ .cfg-plan-cards [for="cfg-plan-pro"],
#cfg-plan-business:checked ~ .cfg-plan-cards [for="cfg-plan-business"] {
    border-color: var(--cfg-bar); box-shadow: 0 0 0 1px var(--cfg-bar);
    background: color-mix(in srgb, var(--cfg-bar) 9%, transparent);
}
#cfg-plan-base:focus-visible ~ .cfg-plan-cards [for="cfg-plan-base"],
#cfg-plan-pro:focus-visible ~ .cfg-plan-cards [for="cfg-plan-pro"],
#cfg-plan-business:focus-visible ~ .cfg-plan-cards [for="cfg-plan-business"] {
    outline: 2px solid var(--cfg-bar); outline-offset: 2px;
}

.cfg-cmp { list-style: none; padding: 0; margin: var(--space-4) 0 0; }
.cfg-cmp-row {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--space-3); align-items: center;
    padding: var(--space-2) var(--space-2); border-radius: var(--radius-md);
    color: var(--gray-400); transition: color 0.2s ease;
}
.cfg-cmp-mark {
    width: calc(1.5rem * var(--cfg-k)); height: calc(1.5rem * var(--cfg-k));
    display: grid; place-items: center; border-radius: 50%;
    background: var(--surface-sunken); color: var(--gray-400);
    font-size: calc(0.8rem * var(--cfg-k));
    transition: background-color 0.2s ease, color 0.2s ease;
}
.cfg-cmp-yes { display: none; }
.cfg-cmp-val { font-weight: 600; font-variant-numeric: tabular-nums; padding: 0 var(--space-2); border-radius: 999px; }
.cfg-cmp-val span,
.cfg-plan-submit span { display: none; }

/* Lo que TRAE el plan elegido: se enciende. */
#cfg-plan-base:checked ~ .cfg-cmp .in-base,
#cfg-plan-pro:checked ~ .cfg-cmp .in-pro,
#cfg-plan-business:checked ~ .cfg-cmp .in-business { color: var(--gray-900); }
#cfg-plan-base:checked ~ .cfg-cmp .in-base .cfg-cmp-mark,
#cfg-plan-pro:checked ~ .cfg-cmp .in-pro .cfg-cmp-mark,
#cfg-plan-business:checked ~ .cfg-cmp .in-business .cfg-cmp-mark {
    background: color-mix(in srgb, var(--cfg-bar) 18%, transparent); color: var(--cfg-bar);
}
#cfg-plan-base:checked ~ .cfg-cmp .in-base .cfg-cmp-yes,
#cfg-plan-pro:checked ~ .cfg-cmp .in-pro .cfg-cmp-yes,
#cfg-plan-business:checked ~ .cfg-cmp .in-business .cfg-cmp-yes { display: inline-block; }
#cfg-plan-base:checked ~ .cfg-cmp .in-base .cfg-cmp-no,
#cfg-plan-pro:checked ~ .cfg-cmp .in-pro .cfg-cmp-no,
#cfg-plan-business:checked ~ .cfg-cmp .in-business .cfg-cmp-no { display: none; }
#cfg-plan-base:checked ~ * .for-base,
#cfg-plan-pro:checked ~ * .for-pro,
#cfg-plan-business:checked ~ * .for-business { display: inline; }

/* Lo que se GANARÍA: lo trae el plan elegido y no el de hoy. La marca pasa al
   verde de la marca y entra con un salto, una fila detrás de otra. */
#cfg-plan-base:checked ~ .cfg-cmp .in-base:not(.is-mine),
#cfg-plan-pro:checked ~ .cfg-cmp .in-pro:not(.is-mine),
#cfg-plan-business:checked ~ .cfg-cmp .in-business:not(.is-mine) {
    font-weight: 600;
    animation: cfg-gain 0.9s ease-out both; animation-delay: calc(var(--cfg-i, 0) * 55ms);
}
#cfg-plan-base:checked ~ .cfg-cmp .in-base:not(.is-mine) .cfg-cmp-mark,
#cfg-plan-pro:checked ~ .cfg-cmp .in-pro:not(.is-mine) .cfg-cmp-mark,
#cfg-plan-business:checked ~ .cfg-cmp .in-business:not(.is-mine) .cfg-cmp-mark {
    background: var(--rodago-verde); color: var(--surface);
    animation: cfg-pop 0.45s cubic-bezier(0.3, 1.6, 0.5, 1) both;
    animation-delay: calc(var(--cfg-i, 0) * 55ms);
}
/* Un límite que SUBE en el plan elegido. */
#cfg-plan-base:checked ~ .cfg-cmp .up-base .cfg-cmp-val,
#cfg-plan-pro:checked ~ .cfg-cmp .up-pro .cfg-cmp-val,
#cfg-plan-business:checked ~ .cfg-cmp .up-business .cfg-cmp-val {
    background: color-mix(in srgb, var(--rodago-verde) 22%, transparent);
    animation: cfg-pop 0.45s cubic-bezier(0.3, 1.6, 0.5, 1) both;
    animation-delay: calc(var(--cfg-i, 0) * 55ms);
}
.cfg-cmp-row:nth-child(2)  { --cfg-i: 1; }
.cfg-cmp-row:nth-child(3)  { --cfg-i: 2; }
.cfg-cmp-row:nth-child(4)  { --cfg-i: 3; }
.cfg-cmp-row:nth-child(5)  { --cfg-i: 4; }
.cfg-cmp-row:nth-child(6)  { --cfg-i: 5; }
.cfg-cmp-row:nth-child(7)  { --cfg-i: 6; }
.cfg-cmp-row:nth-child(8)  { --cfg-i: 7; }
.cfg-cmp-row:nth-child(9)  { --cfg-i: 8; }
.cfg-cmp-row:nth-child(10) { --cfg-i: 9; }
@keyframes cfg-pop {
    0%   { scale: 0.4; opacity: 0; }
    100% { scale: 1; opacity: 1; }
}
@keyframes cfg-gain {
    0%   { background-color: color-mix(in srgb, var(--rodago-verde) 22%, transparent); }
    100% { background-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .cfg-modal[open],
    .cfg-cmp-row,
    .cfg-cmp-row .cfg-cmp-mark,
    .cfg-cmp-row .cfg-cmp-val { animation: none !important; }
    .cfg-plan-card:hover { translate: none; }
}

/* El botón que pide el cambio: solo con OTRO plan elegido. */
.cfg-plan-cta {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space-3); margin-top: var(--space-4);
}
.cfg-plan-cta .cfg-disclosure { flex: 1 1 auto; min-width: 0; }
.cfg-plan-submit { flex: 0 0 auto; }
.cfg-plan-radio.is-current:checked ~ .cfg-plan-cta { display: none; }

/* Los controles de Bootstrap de dentro, al tamaño del texto de la sección. En
   el teléfono no: ahí mandan los 16px de `.form-touch`. */
@media (min-width: 768px) {
    .cfg .btn,
    .cfg .form-control,
    .cfg .form-select,
    .cfg .form-label { font-size: calc(0.9rem * var(--cfg-k)); }
}

/* En el TELÉFONO: el modal ocupa la pantalla entera; el título y las secciones
   pasan arriba, en una fila que rueda (una fila de navegación rueda; no
   envuelve), y todo lo que se toca mide un dedo. */
@media (max-width: 767.98px) {
    .cfg { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
    .cfg-side {
        border-right: 0; border-bottom: 1px solid var(--gray-200);
        padding: var(--space-3) var(--space-4) var(--space-2);
    }
    .cfg-title { padding: 0; margin-bottom: var(--space-2); }
    .cfg-nav {
        flex-direction: row; overflow-x: auto; gap: var(--space-2);
        margin: 0 calc(var(--space-4) * -1); padding: 0 var(--space-4);
        scrollbar-width: none;
    }
    .cfg-nav::-webkit-scrollbar { display: none; }
    .cfg-nav-link { min-height: 44px; flex: 0 0 auto; }
    .cfg-version { margin-top: var(--space-1); padding: 0; }
    .cfg-main { padding: var(--space-4); }
    .cfg-section-title { font-size: 1.2rem; margin-bottom: var(--space-4); }
    .settings-page .cfg { min-height: 0; }

    .cfg-modal {
        width: 100vw; height: 100vh; height: 100svh;
        margin: 0; border-radius: 0;
    }
    .cfg-modal .cfg-side {
        overflow: visible;
        padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
    }
    .cfg-modal .cfg-main { padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px)); }
    .cfg-modal-close {
        top: calc(var(--space-1) + env(safe-area-inset-top, 0px)); right: var(--space-2);
        width: 44px; height: 44px;
    }

    .cfg-inline-input { font-size: 16px; }
    .cfg-inline-pencil { min-width: 44px; min-height: 44px; }
    .cfg-month-head { min-height: 44px; }
    .cfg-month-head.is-invoice { grid-template-columns: minmax(0, 1fr) auto auto 1rem; }
    .cfg-breakdown-row { grid-template-columns: minmax(0, 1fr) 3rem 5.5rem; padding-left: var(--space-2); }
    .cfg-actions .btn { min-height: 44px; display: inline-flex; align-items: center; }

    .cfg-plan-cards { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
    .cfg-plan-card { padding: var(--space-3) var(--space-4); }
    .cfg-plan-card:hover { translate: none; }
    .cfg-plan-cta { flex-direction: column; align-items: stretch; }
    .cfg-plan-submit { min-height: 48px; }
}

/* LA FOTO SE CAMBIA DESDE EL PROPIO CÍRCULO (Configuración → Cuenta). El círculo
   es un <label> del campo de archivo: el lápiz aparece al pasar por encima y al
   enfocarlo con el teclado. Todo el color sale de los tokens (un velo de la
   superficie y la tinta de siempre), así que vale en el panel oscuro, en el
   portal y en el rider sin una regla por rol. */
.avatar-edit-form { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0; }
.avatar-edit {
    position: relative; display: inline-flex; flex: 0 0 auto;
    border-radius: 50%; cursor: pointer; margin: 0;
}
.avatar-edit-overlay {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%;
    background: color-mix(in srgb, var(--surface-raised) 72%, transparent);
    color: var(--gray-900);
    font-size: 1rem;
    opacity: 0; transition: opacity 0.12s ease;
}
.avatar-edit:hover .avatar-edit-overlay,
.avatar-edit:focus-within .avatar-edit-overlay { opacity: 1; }
.avatar-edit:focus-within { outline: 2px solid var(--primary); outline-offset: 2px; }
.avatar-edit-form.is-uploading .avatar-edit { opacity: 0.5; pointer-events: none; }
.avatar-edit-remove { padding: 0; }
/* En táctil no existe «pasar por encima»: el lápiz queda a la vista, como una
   insignia en la esquina, para que se sepa que el círculo se puede tocar. */
@media (hover: none) {
    .avatar-edit-overlay {
        inset: auto 0 0 auto; width: 1.4rem; height: 1.4rem;
        background: var(--surface-raised); border: 1px solid var(--gray-200);
        font-size: 0.7rem; opacity: 1;
    }
}

/* Las cifras enlazan a la lista ya filtrada: son un atajo, no un adorno. Se
   dibujan como celda, nunca como enlace de texto. */
a.ops-cell { text-decoration: none; color: inherit; }
a.ops-cell:hover { background: var(--gray-50); }

.prof-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
    gap: var(--space-5);
    align-items: start;
}
@media (max-width: 1100px) {
    .prof-grid { grid-template-columns: 1fr; }
}

/* La ficha de la TIENDA ONLINE de un local (specs/013). */
.shop-secret-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.shop-secret-row > .form-control { flex: 1 1 14rem; min-width: 0; }
.shop-url {
    flex: 1 1 14rem; min-width: 0; padding: 0.375rem 0.6rem;
    background: var(--surface-sunken); border: 1px solid var(--gray-200); border-radius: 6px;
    color: var(--gray-700); word-break: break-all; font-size: 0.8rem;
}
.shop-chips { display: flex; flex-wrap: wrap; gap: 0.375rem; align-items: center; }
.shop-preview {
    display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;
    padding: 0.5rem 0.75rem; border-radius: 6px; font-size: 0.82rem;
    background: var(--primary-light); color: var(--gray-700);
}
/* El registro se acota en ALTO, no en filas: con 30 o con 300 avisos se lee
   entero con su barra, y la cabecera se queda arriba. */
.shop-log { max-height: 26rem; overflow-y: auto; }
.shop-log thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); }
.shop-json { max-height: 28rem; overflow: auto; }
/* Aquí el resumen ES la tarjeta: el chevron se tiene que ver. */
.shop-json-box > summary::after { font-size: 1em; color: var(--gray-600); }

/* Bloque plegable con la cuenta en el resumen. */
.prof-details > summary {
    display: flex; align-items: center; gap: 8px;
    cursor: pointer; list-style: none;
    font-size: 0.82rem; font-weight: 600; color: var(--gray-700);
}
.prof-details > summary::-webkit-details-marker { display: none; }
.prof-details > summary::after {
    content: '\F282'; font-family: 'bootstrap-icons';
    font-size: 0.7em; color: var(--gray-400); margin-left: auto;
}
.prof-details[open] > summary::after { content: '\F286'; }
.prof-count {
    background: var(--gray-200); color: var(--gray-700);
    border-radius: 999px; padding: 0 8px;
    font-size: 0.72rem; font-weight: 700;
    font-variant-numeric: var(--num);
}
/* Dentro de una `.card` con el cuerpo sin padding: el resumen es la fila que
   hace de cabecera de la sección. */
.prof-details-card > summary {
    padding: 12px 16px;
    border-bottom: 1px solid transparent;
}
.prof-details-card[open] > summary { border-bottom-color: var(--gray-200); }
.prof-details-card > summary:hover { background: var(--gray-50); }

/* Una lista larga se acota en ALTO, no se recorta en filas: lo que hay se
   sigue pudiendo ver entero, sin empujar el resto de la página fuera. */
.prof-scroll { max-height: 420px; overflow-y: auto; }
.prof-scroll thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }

/* ── El DIRECTORIO: las listas que llevan a un perfil ──────────────────────
   `/admin/riders` y `/admin/companies` son la puerta de los dos perfiles, y el
   catálogo de una empresa se lee igual. La identidad de una fila —foto y
   nombre— tenía su tamaño escrito EN LÍNEA en la plantilla (`style="font-size:
   2.5rem"`), que es justo lo que el barrido del teléfono no puede alcanzar. */
.dir-ident { display: flex; align-items: center; gap: 8px; min-width: 0; }
.dir-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex: 0 0 40px; }
.dir-avatar-ph { font-size: 2.1rem; color: var(--primary); line-height: 1; }
/* Los botones de la fila de filtros van juntos en su propia ranura: filtrar,
   limpiar y exportar son tres verbos del mismo gesto. */
.dir-filter-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }

@media (max-width: 767.98px) {
    .prof-head { gap: 10px; }
    .prof-avatar { width: 44px; height: 44px; flex-basis: 44px; }
    .prof-scroll { max-height: 60vh; }
}

/* ==========================================================================
   REGISTRO HORARIO (`/admin/shifts/review`) — la fase 2 de las jornadas
   ==========================================================================
   Todo el color sale de los tokens: `body.role-admin` remapea los `--gray-*`,
   así que un literal escrito acá sería texto oscuro sobre fondo oscuro.

   El estado de un día se DERIVA (no hay columna) y por eso se dibuja siempre
   igual en las tres superficies: la lista, el detalle del día y el calendario.
   El ámbar es "hay algo que decidir", el rojo es la ausencia y el verde es
   "ya lo miró una persona" — el mismo orden de prioridad que el calendario. */

/* El anexo de horas extra es una columna estrecha: un rider, un día y dos
   horas. Medía por `style=` en línea y ahora encoge con la pantalla. */
.review-narrow { max-width: 43.75rem; }
.wdr-state {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.72rem; font-weight: 600; line-height: 1.2;
    padding: 0.1875rem 0.5rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-700);
    white-space: nowrap;
}
.wdr-state.is-ausente { background: var(--danger); color: #fff; }
.wdr-state.is-pendiente {
    background: color-mix(in srgb, var(--warning) 26%, transparent);
    color: color-mix(in srgb, var(--warning) 70%, #000);
}
.wdr-state.is-resuelto,
.wdr-state.is-trabajado {
    background: color-mix(in srgb, var(--success) 20%, transparent);
    color: color-mix(in srgb, var(--success) 75%, #000);
}
.wdr-state.is-curso {
    background: color-mix(in srgb, var(--primary) 20%, transparent);
    color: var(--primary);
}
.wdr-state.is-previsto,
.wdr-state.is-sinplan { background: var(--gray-200); color: var(--gray-600); }

/* La fila que espera una decisión se marca con el borde, no con el fondo: el
   fondo compite con los chips que lleva dentro. */
.wdr-row-pendiente td:first-child { box-shadow: inset 3px 0 0 var(--warning); }

.wdr-day { text-decoration: none; color: inherit; font-weight: 600; }
.wdr-day:hover { text-decoration: underline; }
.wdr-day .disp-sub { text-transform: capitalize; margin-left: 0.25rem; }
.wdr-rider { color: inherit; text-decoration: none; }
.wdr-rider:hover { text-decoration: underline; }

/* Que la app dedujera la hora de fin es un dato del REGISTRO, no un error: se
   señala para que se sepa qué se puede corregir, sin gritar. */
.wdr-flag {
    display: inline-flex; align-items: center; gap: 0.1875rem;
    font-size: 0.65rem; color: var(--gray-600); white-space: nowrap;
}

/* La cifra de horas MANDA y el horario que la explica va debajo, más pequeño
   pero legible: un "5h 30min" a secas no dice si empezaba a las 7 o a las 15. */
.wdr-hours { font-size: 0.95rem; font-weight: 600; line-height: 1.2; }
.wdr-times {
    display: flex; flex-direction: column;
    font-size: 0.72rem; color: var(--gray-600);
    font-variant-numeric: var(--num);
}

/* El fichaje en crudo: entrada → salida de cada sesión del día. */
.wdr-punch {
    display: flex; align-items: center; gap: 0.25rem;
    font-size: 0.78rem; font-variant-numeric: var(--num);
    white-space: nowrap;
}
.wdr-punch + .wdr-punch { margin-top: 2px; }
.wdr-punch > i { font-size: 0.7rem; color: var(--gray-500); }
.wdr-punch-auto { color: var(--warning) !important; }
.wdr-punch-open { color: var(--primary); font-weight: 600; }

/* El reparto del día en una barra: lo que entró en la jornada y lo que no.
   Es la lectura rápida; los minutos exactos van debajo. */
.wdr-bar {
    display: flex; height: 0.375rem; border-radius: 999px; overflow: hidden;
    background: var(--gray-200); margin-bottom: 0.25rem; min-width: 5.625rem;
}
.wdr-bar-in { background: var(--success); }
.wdr-bar-out { background: var(--warning); }
/* Lo que FALTA de la jornada pactada. Va en rojo y se dibuja siempre que haya
   horas sin cumplir, aunque parte del trabajo cayera dentro del horario: es lo
   primero que hay que ver, y lo que decide qué se puede resolver. */
.wdr-bar-miss { background: var(--danger); }
.wdr-split {
    display: flex; flex-wrap: wrap; gap: 2px 0.5rem;
    font-size: 0.72rem; color: var(--gray-600);
}
.wdr-split .is-out { color: color-mix(in srgb, var(--warning) 70%, #000); font-weight: 600; }
.wdr-split .is-miss { color: var(--danger); font-weight: 600; }
.cal-chip.is-miss {
    background: color-mix(in srgb, var(--danger) 18%, transparent);
    color: color-mix(in srgb, var(--danger) 80%, #000);
}

.wdr-reason {
    font-size: 0.72rem; color: var(--gray-600);
    margin-top: 2px; white-space: pre-wrap;
}

/* Resolver sin salir de la lista: el formulario se despliega dentro de la fila
   (sin JS, como los tramos del detalle de una ruta). */
.wdr-resolve { margin-top: 0.375rem; }
.wdr-resolve > summary {
    cursor: pointer; font-size: 0.75rem; font-weight: 600;
    color: var(--primary); list-style: none;
}
.wdr-resolve > summary::-webkit-details-marker { display: none; }
.wdr-resolve > summary::before { content: '\F4FE'; font-family: 'bootstrap-icons'; margin-right: 0.25rem; }
.wdr-resolve[open] > summary::before { content: '\F2E6'; }
.wdr-resolve > form { margin-top: 0.375rem; min-width: 13.125rem; }
.wdr-choices { display: flex; flex-wrap: wrap; gap: 0.375rem; }

/* La PAUSA es informativa: ya está descontada de lo trabajado, así que se
   dibuja apagada — no compite con las cifras que sí deciden algo. */
.wdr-pause {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.78rem; color: var(--gray-600); font-variant-numeric: var(--num);
}

/* La DIFERENCIA con su signo: verde lo que sobra, rojo lo que falta. Es
   REGISTRO, no un juicio — por eso lleva color pero no icono ni adjetivo. */
.wdr-delta { font-weight: 700; font-size: 0.9rem; font-variant-numeric: var(--num); }
.wdr-delta.is-plus { color: var(--success); }
.wdr-delta.is-minus { color: var(--danger); }

/* Poner y corregir sin salir de la fila.

   Los botones nacen APAGADOS y aparecen al pasar por encima de la celda o al
   llegar con el tabulador: en una tabla de cien días, cien lápices y cien
   papeleras siempre visibles tapan justamente los números que se vienen a
   leer. `opacity` y no `display`, para que el hueco no salte al aparecer. */
.wdr-add, .wdr-act {
    border: 1px solid var(--gray-300); background: var(--surface);
    color: var(--gray-600); border-radius: var(--radius-sm);
    line-height: 1; cursor: pointer; padding: 0.1875rem 0.375rem; font-size: 0.72rem;
    opacity: 0; transition: opacity .12s ease, color .12s ease, border-color .12s ease;
}
.wdr-add { margin-top: 0.25rem; }
.wdr-act { padding: 2px 0.3125rem; }
.wdr-act.is-danger:hover { color: var(--danger); border-color: var(--danger); }
.wdr-add:hover, .wdr-act:hover { color: var(--primary); border-color: var(--primary); }
.wdr-editable:hover .wdr-add,
.wdr-editable:hover .wdr-act,
.wdr-editable:focus-within .wdr-add,
.wdr-editable:focus-within .wdr-act,
.wdr-add:focus-visible, .wdr-act:focus-visible { opacity: 1; }
.wdr-acts { display: inline-flex; gap: 0.1875rem; margin-left: 0.25rem; }
.wdr-acts form { display: inline; }
.wdr-time-row { display: inline-flex; align-items: center; gap: 2px; }
.wdr-note { font-size: 0.7rem; color: var(--gray-500); cursor: help; }

/* Quién y qué día, arriba del modal: es lo que le da sentido a las dos horas. */
.jm-who {
    display: flex; flex-wrap: wrap; gap: 4px 16px;
    padding: 8px 10px; margin-bottom: 4px;
    border-radius: var(--radius-sm); background: var(--gray-100);
    font-size: 0.85rem; color: var(--gray-700);
}

@media (max-width: 767.98px) {
    /* En tarjeta el reparto ocupa el ancho entero: la barra es lo que se lee. */
    .wdr-bar { min-width: 0; }
    .wdr-resolve > form { min-width: 0; }
    .wdr-choices .btn { flex: 1; min-height: 38px; }
    /* En el teléfono no hay "pasar por encima": si se esconden, no existen.
       Y el blanco táctil se gana ENSANCHANDO el botón, nunca con padding. */
    .wdr-add, .wdr-act { opacity: 1; min-width: 32px; min-height: 32px; }
}

/* ==========================================================================
   PORTAL DE EMPRESA · PLANTILLAS (`/company/templates`)
   ==========================================================================
   La lista de moldes del cliente y el modal desde el que pide uno. Dos reglas:

   - **El pin y el aviso previo NO se redefinen acá.** El código de color de las
     paradas (`stop_pin`) y el bloque `.plan-confirm` son los mismos que usa el
     dispatcher, y siguen viniendo de sus secciones: el cliente y el despacho
     tienen que estar mirando el mismo verde y leyendo el mismo aviso.
   - **El modal es propio, y por eso es ANCHO.** `.mess-handoff-modal` está
     dimensionado para preguntar UNA cosa (380px); acá adentro entra un pedido
     entero —paradas, horarios y cantidades—, y a 380px eso es una columna de
     campos apretados donde no se entiende qué pertenece a qué.
   ========================================================================== */

.ct-card.is-off { opacity: 0.72; }
.ct-card-grid {
    display: flex; flex-wrap: wrap; gap: var(--space-4);
    align-items: flex-start; justify-content: space-between;
}
.ct-main { flex: 1 1 320px; min-width: 0; }
.ct-side { flex: 0 1 300px; min-width: 240px; display: flex; flex-direction: column; gap: var(--space-2); }

/* La fila de una parada en la LISTA es una línea de texto; la del MODAL es un
   bloque editable. Son dos cosas distintas y por eso son dos clases: con un
   nombre compartido, la regla del modal (que va después) reescribía la de la
   lista. */
.ct-line { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
.ct-line > span { overflow-wrap: anywhere; }
.ct-line-win {
    font-size: 0.76rem; color: var(--gray-600);
    border: 1px solid var(--gray-200); border-radius: var(--radius-sm);
    padding: 0 5px; white-space: nowrap;
}
.ct-done { color: var(--success, #16a34a); font-weight: 600; }
.ct-done a { color: inherit; }
.ct-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: flex-end; }
.ct-actions form { margin: 0; }

/* ── El modal ─────────────────────────────────────────────────────────────── */
.ct-modal-backdrop {
    position: fixed; inset: 0; z-index: 1080;
    background: rgba(0, 0, 0, 0.45);
    display: flex; align-items: center; justify-content: center;
    padding: var(--space-3);
}
.ct-modal-backdrop[hidden] { display: none; }
.ct-modal {
    background: var(--surface); color: var(--body-color, inherit);
    border-radius: var(--radius-lg); width: 100%; max-width: 720px;
    max-height: 92vh; display: flex; flex-direction: column;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.ct-modal-head {
    display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-2);
    padding: var(--space-3); border-bottom: 1px solid var(--gray-200);
}
.ct-modal-sub { font-size: 0.82rem; color: var(--gray-600); font-weight: 600; }
.ct-modal-close {
    border: none; background: transparent; font-size: 1.4rem; line-height: 1;
    color: var(--gray-500); cursor: pointer;
}
/* El CUERPO es lo único que scrollea: el pie con «Crear pedido» tiene que
   quedar a la vista aunque la plantilla tenga diez entregas. */
.ct-modal-body { padding: var(--space-3); overflow-y: auto; flex: 1 1 auto; }
.ct-modal-foot {
    display: flex; justify-content: flex-end; gap: var(--space-2);
    padding: var(--space-3); border-top: 1px solid var(--gray-200);
}
.ct-modal-msg {
    font-size: 0.8rem; padding: 6px 10px; border-radius: var(--radius-sm);
    margin-top: var(--space-2); background: var(--gray-100); color: var(--gray-700);
}
.ct-modal-msg.is-error { background: #fee2e2; color: #991b1b; }

.ct-day { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.ct-day input[type="date"] { width: 170px; }
.ct-day-note { font-size: 0.78rem; color: var(--stop-drop-ink); font-weight: 600; }
.ct-hint { font-size: 0.8rem; color: var(--gray-600); margin: var(--space-2) 0; }

.ct-stops { display: flex; flex-direction: column; gap: 6px; }
.ct-stop {
    border: 1px solid var(--gray-200); border-left-width: 3px;
    border-radius: var(--radius-sm); padding: 8px 10px; margin: 0;
}
.ct-stop.is-recogida { border-left-color: var(--stop-pickup); }
.ct-stop.is-entrega { border-left-color: var(--stop-drop); }
.ct-stop.is-off { opacity: 0.5; }
.ct-stop-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ct-pick { display: flex; align-items: center; margin: 0; }
.ct-pick input { width: 17px; height: 17px; cursor: pointer; }
.ct-stop-what { flex: 1 1 180px; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ct-stop-kind {
    font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.02em; border-radius: 999px; padding: 1px 7px;
    align-self: flex-start;
}
/* El fondo sale del MISMO token que el pin, rebajado — como `.plangen-stop-kind`:
   una pastilla con su color escrito a mano se despega del pin en cuanto el token
   cambia. */
.ct-stop.is-recogida .ct-stop-kind {
    color: var(--stop-pickup-ink); background: rgba(34, 197, 94, 0.16);
}
.ct-stop.is-entrega .ct-stop-kind {
    color: var(--stop-drop-ink); background: rgba(245, 158, 11, 0.16);
}
.ct-stop-addr { font-size: 0.8rem; overflow-wrap: anywhere; }
.ct-stop-addr.is-empty { color: var(--gray-500); font-style: italic; }
.ct-win { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.ct-win input[type="time"] { width: 104px; }
.ct-win-sep { color: var(--gray-500); font-size: 0.75rem; }

.ct-items { list-style: none; margin: 8px 0 0; padding: 0 0 0 26px;
            display: flex; flex-direction: column; gap: 4px; }
.ct-item { display: flex; align-items: center; gap: 8px; }
.ct-item-pick { display: flex; align-items: center; gap: 6px; margin: 0; flex: 1 1 auto; min-width: 0; }
.ct-item-pick input { width: 15px; height: 15px; cursor: pointer; }
.ct-item-name { font-size: 0.8rem; overflow-wrap: anywhere; }
.ct-item-qty { width: 88px; flex-shrink: 0; }
.ct-item-unit { font-size: 0.74rem; color: var(--gray-500); width: 64px; flex-shrink: 0; }
/* Lo AGREGADO para este pedido se lee distinto de lo que dice el molde: mismo
   renglón, misma grilla, pero con su marca y con la ✕ que lo saca del todo. */
.ct-item.is-extra .ct-item-pick { padding-left: 21px; }
.ct-item-tag {
    font-size: 0.66rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.03em; color: var(--stop-drop-ink);
    background: rgba(245, 158, 11, 0.16);
    border-radius: 999px; padding: 1px 7px; flex-shrink: 0;
}
.ct-item-del {
    border: 0; background: none; color: var(--gray-500); font-size: 1.1rem;
    line-height: 1; padding: 0 4px; cursor: pointer; flex-shrink: 0;
}
.ct-item-del:hover { color: var(--danger, #dc2626); }

/* Añadir un producto: la puerta está SIEMPRE, aunque la plantilla no tenga
   ninguno — es lo que evita tener que editar el molde (o sea la serie entera)
   para sumar una caja un día. */
.ct-additem { padding-left: 26px; margin-top: 6px; }
.ct-additem-open {
    border: 0; background: none; padding: 0; cursor: pointer;
    font-size: 0.78rem; font-weight: 600; color: var(--cp);
}
.ct-additem-open:hover { text-decoration: underline; }
.ct-additem-box { margin-top: 6px; max-width: 420px; }
.ct-additem-list {
    list-style: none; margin: 4px 0 0; padding: 0;
    max-height: 168px; overflow-y: auto;
    border: 1px solid var(--gray-200); border-radius: var(--radius-sm);
    background: var(--surface);
}
.ct-additem-list li + li { border-top: 1px solid var(--gray-100); }
.ct-additem-list button {
    width: 100%; display: flex; justify-content: space-between; gap: 8px;
    align-items: baseline; border: 0; background: none; cursor: pointer;
    padding: 6px 8px; font-size: 0.8rem; text-align: left;
}
.ct-additem-list button:hover { background: var(--gray-100); }
.ct-additem-unit { font-size: 0.72rem; color: var(--gray-500); flex-shrink: 0; }
.ct-additem-empty { padding: 6px 8px; font-size: 0.78rem; color: var(--gray-500); }

/* «Guardar también como plantilla», en el alta de pedido del portal: es una
   opción del envío, no un aviso, así que se lee como un bloque aparte y no
   como un párrafo suelto al pie. */
.cp-saveas {
    border: 1px dashed var(--gray-300);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    background: var(--surface-sunken);
}

@media (max-width: 575.98px) {
    /* En el teléfono la hora baja a su propio renglón: al lado de la dirección
       los dos `input[type=time]` la comprimen hasta partirla en cinco líneas. */
    .ct-stop-head { align-items: flex-start; }
    .ct-win { width: 100%; }
    .ct-win input[type="time"] { flex: 1 1 0; width: auto; }
    .ct-items { padding-left: 0; }
    .ct-additem { padding-left: 0; }
    .ct-modal { max-height: 96vh; }
    .ct-side { flex-basis: 100%; }
}

/* ==========================================================================
   PORTAL DE EMPRESA · CALENDARIO (`/company/calendar`)
   ==========================================================================
   El mes y el día del CLIENTE. Namespace propio (`.cc-*`) y no las `.cal-*` del
   dispatcher, y no es purismo: allá el rojo significa "nadie lo ruteó" y el
   ámbar "quedó a medias", que son preguntas del despacho sobre recorridos. Acá
   el color dice lo único que el cliente puede leer y hacer con un día:

     acento = tenés algo POR PEDIR (una repetición tuya lo prevé) — lo único
              accionable, y por eso manda sobre los otros dos;
     ámbar  = hay pedidos EN MARCHA;
     verde  = día CERRADO.

   Compartir las clases habría obligado a que un cambio del semáforo del
   despacho no tocara el del cliente, que es exactamente lo que un nombre
   compartido no puede garantizar. Lo que SÍ se comparte es lo que significa lo
   mismo en las dos pantallas: `stop_pin`, `.status-badge` y `.cp-card`.
   ========================================================================== */

.cc-navhead {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-3);
}
.cc-month { font-size: 1.15rem; font-weight: 700; margin: 0; }
.cc-month-sub { font-size: 0.82rem; color: var(--gray-500); margin: 2px 0 0; }
.cc-hl { color: var(--cp-text); font-weight: 700; }

/* ── El mes ─────────────────────────────────────────────────────────────── */
.cc-table { table-layout: fixed; }
.cc-table thead th {
    font-size: 0.75rem; font-weight: 600; color: var(--gray-500);
    text-transform: uppercase; letter-spacing: 0.04em;
    background: var(--gray-50);
}
.cc-cell {
    width: 14.28%; height: 5.25rem; vertical-align: top; padding: 0;
}
.cc-cell.is-empty { background: var(--gray-50); }
.cc-cell.is-today { outline: 2px solid var(--cp); outline-offset: -2px; }
/* El estado se marca en el BORDE izquierdo y no con el fondo: un fondo lleno
   por celda convierte el mes en un tablero de colores donde ya no se lee el
   número del día. */
.cc-cell.is-por_pedir { box-shadow: inset 3px 0 0 var(--cp); }
.cc-cell.is-en_curso  { box-shadow: inset 3px 0 0 var(--warning); }
.cc-cell.is-cerrado   { box-shadow: inset 3px 0 0 var(--success); }

.cc-daylink {
    display: flex; flex-direction: column; gap: 0.25rem;
    height: 100%; padding: 0.375rem 0.5rem;
    text-decoration: none; color: inherit;
}
.cc-daylink:hover { background: var(--gray-50); color: inherit; }
.cc-daynum { font-size: 0.85rem; font-weight: 600; line-height: 1; }
.cc-cell.is-today .cc-daynum { color: var(--cp-text); }
.cc-chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.cc-chip {
    display: inline-flex; align-items: center; gap: 0.1875rem;
    font-size: 0.7rem; font-weight: 600; line-height: 1;
    padding: 0.1875rem 0.375rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-700);
}
.cc-chip.is-topedir { background: var(--cp-light); color: var(--cp-text); }
.cc-chip.is-done {
    background: color-mix(in srgb, var(--success) 16%, transparent);
    color: #15803d;
}
.cc-chip.is-live {
    background: color-mix(in srgb, var(--warning) 20%, transparent);
    color: #92400e;
}
.cc-chip.is-off { background: var(--gray-100); color: var(--gray-500); }

.cc-legend { font-size: 0.82rem; color: var(--gray-500); }
/* La muestra de color es un asset de tamaño fijo: se queda en px (barrido). */
.cc-swatch {
    display: inline-block; width: 12px; height: 12px;
    border-radius: 3px; vertical-align: -0.1em;
}
.cc-swatch.is-por_pedir { background: var(--cp); }
.cc-swatch.is-en_curso { background: var(--warning); }
.cc-swatch.is-cerrado { background: var(--success); }

/* ── El día ─────────────────────────────────────────────────────────────── */
.cc-daytitle { text-transform: capitalize; }
.cc-state {
    display: inline-flex; align-items: center; gap: 0.3125rem;
    font-size: 0.78rem; font-weight: 600;
    padding: 0.25rem 0.625rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-700);
}
.cc-state.is-por_pedir { background: var(--cp-light); color: var(--cp-text); }
.cc-state.is-en_curso {
    background: color-mix(in srgb, var(--warning) 20%, transparent);
    color: #92400e;
}
.cc-state.is-cerrado {
    background: color-mix(in srgb, var(--success) 16%, transparent);
    color: #15803d;
}
.cc-state.is-hoy { background: var(--cp); color: #fff; }

/* Las cifras del día: la unidad más grande manda y la etiqueta explica. Van en
   una fila que ENVUELVE, nunca en columnas fijas — en el teléfono cinco
   columnas comprimen el número hasta partirlo. */
.cc-sum {
    display: flex; flex-wrap: wrap; gap: var(--space-3);
    margin-bottom: var(--space-4);
}
.cc-sum-cell {
    flex: 1 1 7.5rem; min-width: 6.875rem;
    background: var(--surface); border: 1px solid var(--gray-200);
    border-radius: var(--radius-sm); padding: var(--space-3);
    display: flex; flex-direction: column; gap: 2px;
}
.cc-sum-num { font-size: 1.35rem; font-weight: 700; line-height: 1.1; }
.cc-sum-lbl { font-size: 0.75rem; color: var(--gray-500); }
.cc-sum-cell.is-done .cc-sum-num { color: #15803d; }
.cc-sum-cell.is-live .cc-sum-num { color: #92400e; }
.cc-sum-cell.is-off .cc-sum-num { color: var(--gray-400); }

.cc-block.is-topedir { border-left: 3px solid var(--cp); }
.cc-hint { font-size: 0.82rem; color: var(--gray-500); margin: 0 0 var(--space-3); }
.cc-empty { font-size: 0.88rem; color: var(--gray-500); margin: 0; }
.cc-count {
    font-size: 0.75rem; font-weight: 700; color: var(--gray-500);
    background: var(--gray-100); border-radius: 999px; padding: 1px 0.5rem;
    margin-left: 0.375rem;
}

/* La OCURRENCIA de una repetición en un día (`company/_occurrence_card.html`).
   Un bloque y no una fila: lleva las direcciones con su código de color, la
   regla, los pedidos que salieron de ella y sus acciones — o sea lo mismo que
   el dispatcher ve de las suyas. Las líneas de parada son las `.ct-line` de la
   lista de plantillas: la misma cosa dibujada igual en las dos pantallas. */
.cc-occ {
    border: 1px solid var(--gray-200); border-left: 3px solid var(--gray-300);
    border-radius: var(--radius-sm);
    padding: var(--space-3); background: var(--surface);
}
.cc-occ + .cc-occ { margin-top: var(--space-3); }
.cc-occ.is-previsto { border-left-color: var(--cp); }
.cc-occ.is-generado { border-left-color: var(--success); }
.cc-occ.is-saltado, .cc-occ.is-cancelado { border-left-color: var(--gray-300); }
.cc-occ.is-saltado, .cc-occ.is-cancelado { background: var(--gray-50); }

.cc-occ-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2);
}
.cc-occ-open { display: contents; }
/* El nombre ES el botón que abre el ajuste de este día: se lee como un enlace,
   no como un control más de la barra de acciones. */
.cc-occ-name {
    border: 0; background: none; padding: 0; cursor: pointer;
    font-size: 1rem; font-weight: 700; color: var(--cp-text);
    text-align: left; overflow-wrap: anywhere;
}
.cc-occ-name:hover { text-decoration: underline; }
.cc-occ-name.is-static { cursor: default; color: var(--gray-700); }
.cc-occ-state {
    display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0;
    font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.03em; padding: 0.1875rem 0.5625rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-500);
}
.cc-occ-state.is-previsto { background: var(--cp-light); color: var(--cp-text); }
.cc-occ-state.is-generado {
    background: color-mix(in srgb, var(--success) 16%, transparent);
    color: #15803d;
}

.cc-occ-orders { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.cc-occ-orders li {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    padding: var(--space-2) 0; font-size: 0.85rem;
}
.cc-occ-orders li + li { border-top: 1px solid var(--gray-100); }
.cc-occ-actions {
    display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
    margin-top: var(--space-3);
}
.cc-occ-actions:empty { margin-top: 0; }
.cc-occ-note { font-size: 0.78rem; color: var(--gray-500); }

/* La lista de pedidos del día ES la lista de pedidos (`.disp-table`), con el
   acote de este día: sin columna de día de servicio y con la ventana horaria
   pegada a cada dirección. Acá solo van las tres diferencias, no un segundo
   juego de estilos de tabla — la tarjeta del teléfono, el trayecto y la
   navegación de fila los pone `.disp-*`.

   La tabla vive dentro de un `.cp-card`, que ya trae su padding: la tabla lo
   suelta para llegar a los bordes como en «Mis pedidos». */
.cc-orders-body { padding-left: 0; padding-right: 0; padding-bottom: 0; }

/* El estado se REFERENCIA con un borde y se LEE en la pastilla. Antes pintaba
   la fila entera con el color del estado: una franja de color por pedido
   compite con el código de color de las paradas, que es el que dice de qué
   puerta se habla. */
.cc-row > td:first-child { border-left: 3px solid var(--gray-200); }
.cc-row.is-pendiente > td:first-child { border-left-color: var(--status-pendiente); }
.cc-row.is-asignado > td:first-child { border-left-color: var(--status-asignado); }
.cc-row.is-en_sitio > td:first-child,
.cc-row.is-recogido > td:first-child,
.cc-row.is-en_camino > td:first-child { border-left-color: var(--status-progress); }
.cc-row.is-entregado > td:first-child { border-left-color: var(--status-entregado); }
.cc-row.is-cancelado > td:first-child { border-left-color: var(--status-cancelado); }
.cc-row.is-cancelado .cc-route { opacity: 0.7; }

/* La dirección se queda con el ancho y empuja la ventana al extremo: así las
   horas quedan en columna y se comparan entre puertas. */
.cc-route .leg { max-width: none; }
.cc-route .cc-win { margin-left: auto; }

/* Lo que sube a la bici, en UNA línea: el reparto puerta por puerta está en la
   ficha. En una fila de tabla una lista por parada es la mitad del alto de la
   fila para el dato que menos se mira. */
.cc-load {
    display: flex; flex-wrap: wrap; gap: 2px 0.625rem;
    margin: 2px 0 0.25rem; padding-left: 1.1875rem;
    font-size: 0.74rem; color: var(--gray-500);
}
.cc-load-qty { font-weight: 700; color: var(--gray-700); }

/* La hora comprometida de esa puerta, al lado de su dirección. */
.cc-win {
    flex: 0 0 auto; font-size: 0.74rem; font-weight: 600; color: var(--gray-600);
    background: var(--gray-100); border-radius: var(--radius-sm); padding: 1px 0.375rem;
    white-space: nowrap;
}
.cc-win .bi { margin-right: 0.1875rem; }

@media (max-width: 575.98px) {
    /* El mes tiene siete columnas fijas: lo que cede es el ALTO de la celda y
       el tamaño del chip, nunca el ancho — una tabla de siete días que
       scrollea deja de leerse como un mes. */
    .cc-cell { height: 68px; }
    .cc-chip { font-size: 0.66rem; padding: 2px 0.3125rem; }
    .cc-daylink { padding: 0.25rem 0.3125rem; }
}

/* En el teléfono la fila es una TARJETA (`.disp-table.is-cards`): el borde del
   estado se va del primer `td` —que ahí es una línea más— al bloque entero. */
@media (max-width: 767.98px) {
    .cc-row > td:first-child { border-left: 0; }
    .disp-table.is-cards tbody tr.cc-row { border-left: 3px solid var(--gray-200); }
    .disp-table.is-cards tbody tr.cc-row.is-pendiente { border-left-color: var(--status-pendiente); }
    .disp-table.is-cards tbody tr.cc-row.is-asignado { border-left-color: var(--status-asignado); }
    .disp-table.is-cards tbody tr.cc-row.is-en_sitio,
    .disp-table.is-cards tbody tr.cc-row.is-recogido,
    .disp-table.is-cards tbody tr.cc-row.is-en_camino { border-left-color: var(--status-progress); }
    .disp-table.is-cards tbody tr.cc-row.is-entregado { border-left-color: var(--status-entregado); }
    .disp-table.is-cards tbody tr.cc-row.is-cancelado { border-left-color: var(--status-cancelado); }
}

/* ==========================================================================
   PORTAL DE EMPRESA · LIBRETA DE DIRECCIONES (`/company/addresses`)
   ==========================================================================
   Namespace propio (`.lb-*`), como `.ct-*` en Plantillas y `.cc-*` en el
   Calendario: son tres pantallas del portal con listas parecidas y una clase
   compartida es la manera de que tocar una mueva las otras dos.

   La lista NO es una tabla. La del dispatcher lo es porque ahí se comparan
   filas (uso, contacto, precisión) de muchos clientes; acá se reconoce UN
   lugar, y una tarjeta pone la dirección entera en una línea legible en vez de
   repartirla en cuatro columnas que en el teléfono no entran.
   ========================================================================== */

.lb-list { display: flex; flex-direction: column; gap: 0.625rem; }

.lb-card {
    display: flex; gap: var(--space-4); align-items: flex-start;
    flex-wrap: wrap;
    padding: 0.75rem 0.875rem;
    border: 1px solid var(--gray-200); border-radius: var(--radius-md);
    background: #fff;
}
/* El borde de la izquierda es lo único que distingue a la principal en el
   barrido vertical: el resto de la tarjeta es idéntico a propósito, porque es
   la misma clase de cosa. */
.lb-card.is-primary { border-left: 3px solid var(--cp); }
.lb-card.is-off { opacity: 0.6; }
.lb-card-main { flex: 1 1 16.25rem; min-width: 0; }
.lb-card-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.lb-label { font-weight: 600; color: var(--gray-900); }

.lb-tag {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.02em;
    padding: 2px 0.5rem; border-radius: 999px;
    background: var(--gray-100); color: var(--gray-600);
}
.lb-tag.is-primary { background: var(--cp); color: #fff; }
.lb-tag.is-fija { background: #e0f2fe; color: #075985; }
.lb-tag.is-usada { background: var(--gray-100); color: var(--gray-500); }
.lb-tag.is-off { background: var(--gray-800); color: #fff; }

.lb-addr {
    margin: 0.3125rem 0 0; font-size: 0.9rem; color: var(--gray-700);
    overflow-wrap: anywhere;
}

/* Las marcas de estado van en una fila que ENVUELVE y no en columnas: son
   cero, una o tres según la dirección, y una columna vacía se lee como un dato
   que falta. */
.lb-flags { display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; margin-top: 0.375rem; }
.lb-flag {
    display: inline-flex; align-items: center; gap: 0.25rem;
    font-size: 0.76rem; color: var(--gray-500);
}
.lb-flag.is-warn { color: #b45309; font-weight: 600; }
.lb-flag.is-ok { color: var(--cp); }

.lb-actions {
    display: flex; flex-wrap: wrap; gap: 0.375rem; align-items: center;
    margin-left: auto;
}
.lb-actions form { margin: 0; }
/* Los botones son SOLO el icono (el nombre de la acción vive en `title` y en
   `aria-label`), así que el blanco táctil hay que ponerlo a mano: sin ancho
   mínimo un `bi-trash` deja un botón de 26px. Cuadrado y centrado para que las
   cinco acciones se lean como una fila de iconos y no como cinco cajas de
   anchos distintos. */
.lb-act {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 2.125rem; min-height: 2.125rem; padding: 0.25rem;
    line-height: 1;
}
.lb-act i { font-size: 1rem; }

/* El formulario y el bloque de las aprendidas son `<details>`: sin JS se
   abren y se cierran igual, que es la razón de usarlos. El triángulo nativo se
   reemplaza por un chevron para que el `<summary>` se lea como la cabecera de
   tarjeta que es. */
.lb-form-summary {
    cursor: pointer; list-style: none;
    display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
}
.lb-form-summary::-webkit-details-marker { display: none; }
.lb-form-summary::after {
    content: '\F282'; font-family: 'bootstrap-icons';
    font-size: 0.8rem; color: var(--gray-400); transition: transform 0.15s ease;
}
details[open] > .lb-form-summary::after { transform: rotate(180deg); }

.lb-count {
    display: inline-block; margin-left: 0.25rem; padding: 1px 0.5rem;
    border-radius: 999px; background: var(--gray-100); color: var(--gray-600);
    font-size: 0.75rem; font-weight: 700;
}

/* Los tres pasos del formulario. Numerarlos es lo que lo hace legible de una
   pasada: son tres preguntas distintas (dónde, cómo se llama, a quién se
   pregunta) y sin la numeración se leen como un muro de doce campos. */
.lb-step {
    display: flex; align-items: center; gap: 0.5rem;
    margin: 0 0 0.25rem; font-weight: 600; color: var(--gray-900);
}
.lb-step-n {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.375rem; height: 1.375rem; border-radius: 50%;
    background: var(--cp); color: #fff; font-size: 0.76rem; font-weight: 700;
}
.lb-help { font-size: 0.82rem; color: var(--gray-500); margin: 0 0 0.625rem; }

.lb-search { max-width: 15rem; }
/* Los dos filtros del catálogo del portal: medían por `style=` en línea y
   ahora encogen con la pantalla como cualquier otro control. */
.prod-filter-q { min-width: 10rem; }
.prod-filter-unit { min-width: 8.75rem; }
/* Tope de ALTO, nunca recorte de filas: mismo criterio que `.prof-scroll` del
   perfil. Lo que crece solo se acota con scroll, no escondiendo entradas. */
.lb-scroll { max-height: 26.25rem; overflow-y: auto; }

@media (max-width: 575.98px) {
    /* Las acciones dejan de ir a la derecha y pasan a su propio renglón: a
       360px compartir la fila con la dirección deja los dos en un canal de dos
       palabras. */
    .lb-actions { margin-left: 0; width: 100%; }
    .lb-search { max-width: 100%; }
}

/* ==========================================================================
   LANDING PÚBLICA (`/`)
   ==========================================================================
   La única página de la app que ve alguien SIN cuenta, y la primera impresión
   del producto. Va en OSCURO —la tinta de marca— por una razón concreta: el
   panel del dispatcher es oscuro, así que las capturas se apoyan sobre la
   página en vez de flotar como recortes sobre blanco. Las del rider, que son
   claras, resaltan justo por lo contrario.

   Móvil primero: todas las rejillas arrancan en UNA columna. Sin JS: ni la
   barra fija, ni el anclado de las secciones, ni un solo estado de esta página
   dependen de un script.
   ========================================================================== */

/* El pie global de `base.html` (el copyright de las vistas anónimas) sobra
   acá: la landing trae el suyo. Mismo recurso que usa la pantalla de acceso. */
body:has(.lp) > footer { display: none; }
body:has(.lp) { background-color: #0d1626; }

.lp {
    /* Paleta local. Nace de los tokens `--rodago-*` de la marca; lo que suma
       son los dos escalones de fondo que una página larga necesita para que
       las secciones se distingan sin líneas. */
    --lp-bg: #0d1626;
    --lp-bg-2: #111d33;
    --lp-surface: #16233D;
    --lp-surface-2: #1d2b47;
    --lp-line: rgba(255,255,255,0.09);
    --lp-line-strong: rgba(255,255,255,0.16);
    --lp-text: #e9eef8;
    --lp-muted: #9fb0cc;
    --lp-accent: var(--rodago-verde);
    --lp-pad: clamp(20px, 5vw, 40px);

    background: var(--lp-bg);
    color: var(--lp-text);
    font-size: 1rem;
    line-height: 1.65;
    overflow-x: clip;              /* el resplandor del hero desborda a propósito */
}
.lp-wrap {
    width: 100%;
    max-width: 1240px;
    margin: 0 auto;
    padding-inline: var(--lp-pad);
}
.lp-wrap-narrow { max-width: 900px; }

.lp a { color: inherit; }
.lp ::selection { background: rgba(21,199,126,0.3); }

/* ── Barra fija ─────────────────────────────────────────────────────────────
   `sticky` y no `fixed`: ocupa su sitio en el flujo, así que ninguna sección
   nace tapada y no hace falta compensar con un padding en el cuerpo. El fondo
   es translúcido con desenfoque para que se note que hay contenido pasando por
   debajo; sin soporte de `backdrop-filter` queda el color plano, que sirve
   igual. */
.lp-nav {
    position: sticky;
    top: 0;
    z-index: 50;
    background: rgba(13,22,38,0.86);
    border-bottom: 1px solid var(--lp-line);
}
@supports (backdrop-filter: blur(1px)) {
    .lp-nav { background: rgba(13,22,38,0.72); backdrop-filter: blur(14px) saturate(140%); }
}
.lp-nav .lp-wrap {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    min-height: 68px;
}
/* El logotipo manda: es lo que tiene que quedar grabado de la visita. */
.lp-nav-logo { height: 30px; width: auto; display: block; }
@media (min-width: 760px) { .lp-nav-logo { height: 36px; } }

.lp-nav-links {
    display: none;
    margin-left: auto;
    gap: clamp(14px, 2.2vw, 30px);
}
@media (min-width: 900px) { .lp-nav-links { display: flex; } }
.lp-nav-links a {
    color: var(--lp-muted);
    font-size: 0.94rem;
    font-weight: 600;
    text-decoration: none;
    padding-block: 6px;
}
.lp-nav-links a:hover { color: #fff; }

/* Sin los enlaces (móvil), el botón es quien empuja a la derecha. */
.lp-nav-cta { margin-left: auto; }
@media (min-width: 900px) { .lp-nav-cta { margin-left: clamp(14px, 2.2vw, 30px); } }

/* El ancla de cada sección tiene que quedar POR DEBAJO de la barra fija, o el
   título al que se salta queda tapado. */
.lp [id] { scroll-margin-top: 88px; }
.lp-scroll-smooth { scroll-behavior: smooth; }

/* ── Botones ────────────────────────────────────────────────────────────── */
.lp-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 0 var(--space-5);
    border: 1px solid transparent;
    border-radius: var(--radius-lg);
    background: var(--lp-accent);
    color: #05271a;
    font-size: 0.95rem;
    font-weight: 700;
    text-decoration: none;
    white-space: nowrap;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.lp-btn:hover, .lp-btn:focus-visible { background: var(--rodago-verde-dark); color: #05271a; }
.lp-btn-lg { min-height: 54px; padding: 0 var(--space-6); font-size: 1.05rem; }
.lp-btn-ghost {
    background: transparent;
    border-color: var(--lp-line-strong);
    color: #fff;
}
.lp-btn-ghost:hover, .lp-btn-ghost:focus-visible {
    background: rgba(255,255,255,0.07);
    border-color: rgba(255,255,255,0.32);
    color: #fff;
}
.lp :focus-visible { outline: 2px solid var(--lp-accent); outline-offset: 3px; }

/* ── Tipografía de sección ──────────────────────────────────────────────── */
.lp-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-4);
    padding: 5px 14px;
    border: 1px solid rgba(21,199,126,0.32);
    border-radius: 999px;
    background: rgba(21,199,126,0.10);
    color: var(--lp-accent);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.lp-kicker {
    margin: 0 0 var(--space-3);
    color: var(--lp-accent);
    font-size: 0.8rem;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.lp-h2 {
    margin: 0 0 var(--space-4);
    max-width: 24ch;
    font-size: clamp(1.9rem, 4.6vw, 3rem);
    font-weight: 700;
    line-height: 1.12;
    letter-spacing: -0.025em;
}
.lp-h2-center { max-width: 22ch; margin-inline: auto; text-align: center; }
.lp-lead {
    margin: 0 0 var(--space-6);
    max-width: 64ch;
    color: var(--lp-muted);
    font-size: clamp(1rem, 1.6vw, 1.15rem);
}
.lp-lead-center { margin-inline: auto; text-align: center; }

.lp-section { padding-block: clamp(56px, 9vw, 112px); }
.lp-section-2 { background: var(--lp-bg-2); }
/* La franja del rider: fondo claro para que las capturas del móvil —que son
   claras— dejen de flotar y el corte de sección se lea de un vistazo. */
.lp-section-light { background: #f4f7fb; color: var(--gray-900); }
.lp-section-light .lp-lead,
.lp-section-light .lp-muted { color: var(--gray-500); }
.lp-section-light .lp-kicker { color: var(--rodago-verde-dark); }

/* ── Hero ───────────────────────────────────────────────────────────────── */
.lp-hero {
    position: relative;
    padding-block: clamp(44px, 8vw, 92px) clamp(0px, 2vw, 24px);
    background-image:
        radial-gradient(70% 60% at 18% -10%, rgba(21,199,126,0.16), transparent 68%),
        radial-gradient(65% 55% at 88% 4%,  rgba(74,144,226,0.18), transparent 70%);
}
.lp-hero-copy { max-width: 900px; }
.lp-h1 {
    margin: 0 0 var(--space-5);
    max-width: 17ch;
    font-size: clamp(2.5rem, 7.4vw, 4.75rem);
    font-weight: 700;
    line-height: 1.04;
    letter-spacing: -0.035em;
}
.lp-h1 em {
    font-style: normal;
    background: linear-gradient(96deg, var(--rodago-verde) 12%, #6fe3b0 88%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.lp-hero-sub {
    margin: 0 0 var(--space-6);
    max-width: 58ch;
    color: var(--lp-muted);
    font-size: clamp(1.05rem, 2.1vw, 1.35rem);
    line-height: 1.55;
}
.lp-hero-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
}
.lp-hero-note { margin: 0; color: var(--lp-muted); font-size: 0.9rem; }

/* Cifras de apoyo del hero: dicen de qué está hecho el producto sin pedir que
   nadie baje. Cuatro en fila desde tableta; en el teléfono, dos y dos. */
.lp-stats {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-4) var(--space-5);
    margin-top: clamp(32px, 5vw, 52px);
    padding-top: clamp(24px, 4vw, 36px);
    border-top: 1px solid var(--lp-line);
    max-width: 900px;
}
@media (min-width: 760px) { .lp-stats { grid-template-columns: repeat(4, 1fr); } }
/* Es una `<dl>` de verdad (término + explicación), y el `dd` del navegador
   nace con 40px de sangría heredados del estilo de lista. */
.lp-stats > div { margin: 0; }
.lp-stats dt, .lp-stats dd { margin: 0; }
.lp-stat-n {
    display: block;
    font-size: clamp(1.5rem, 3vw, 2rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    font-variant-numeric: var(--num);
}
.lp-stat-t { color: var(--lp-muted); font-size: 0.88rem; line-height: 1.35; }

/* ── El escaparate: el panel grande + el teléfono encima ────────────────── */
.lp-showcase {
    position: relative;
    margin-top: clamp(36px, 6vw, 64px);
}
/* Resplandor detrás del marco. Es decorativo puro y no recibe puntero. */
.lp-showcase::before {
    content: "";
    position: absolute;
    inset: -14% -6% -22%;
    background: radial-gradient(50% 50% at 50% 45%, rgba(74,144,226,0.28), transparent 72%);
    filter: blur(28px);
    z-index: 0;
    pointer-events: none;
}
.lp-showcase > * { position: relative; z-index: 1; }

/* El teléfono se superpone al panel: es la imagen de las DOS mitades del
   producto en una sola figura. Solo desde 900px — apilado sobre la captura
   taparía justo el mapa. */
.lp-showcase-phone { display: none; }
@media (min-width: 900px) {
    .lp-showcase { padding-right: 140px; }
    .lp-showcase-phone {
        display: block;
        position: absolute;
        right: 0;
        bottom: -6%;
        width: 232px;
        z-index: 2;
    }
}
@media (min-width: 1140px) {
    .lp-showcase { padding-right: 190px; }
    .lp-showcase-phone { width: 280px; }
}

/* ── Marcos ─────────────────────────────────────────────────────────────── */
/* Ventana de escritorio: la barra de puntos dice "esto es una pantalla" sin
   escribirlo. El borde claro despega la captura oscura del fondo oscuro. */
.lp-frame {
    margin: 0;
    border: 1px solid var(--lp-line-strong);
    border-radius: 14px;
    background: var(--lp-surface);
    box-shadow: 0 30px 80px -30px rgba(0,0,0,0.85);
    overflow: hidden;
}
.lp-frame-bar {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 10px 14px;
    background: var(--lp-surface-2);
    border-bottom: 1px solid var(--lp-line);
}
.lp-frame-dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.22); }
.lp-frame img, .lp-shot img {
    display: block;
    width: 100%;
    height: auto;
}
.lp-section-light .lp-frame {
    border-color: var(--gray-200);
    background: var(--surface);
    box-shadow: var(--elevation-3);
}
.lp-section-light .lp-frame-bar { background: var(--gray-100); border-bottom-color: var(--gray-200); }
.lp-section-light .lp-frame-dot { background: var(--gray-400); }

/* Teléfono: sin muescas ni marcos de plástico. Un radio grande, un borde y una
   sombra bastan para leerlo como pantalla de móvil. */
.lp-phone {
    margin: 0;
    border: 1px solid var(--lp-line-strong);
    border-radius: 26px;
    background: #fff;
    box-shadow: 0 24px 60px -24px rgba(0,0,0,0.8);
    overflow: hidden;
}
.lp-phone img { display: block; width: 100%; height: auto; }
.lp-section-light .lp-phone {
    border-color: rgba(22,35,61,0.12);
    box-shadow: 0 26px 60px -28px rgba(22,35,61,0.6);
}

/* Hueco de una captura que todavía no existe: reserva su alto (`aspect-ratio`)
   para que el layout no se mueva cuando entre la imagen real. */
.lp-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    aspect-ratio: 16 / 10;
    padding: var(--space-5);
    border: 1px dashed var(--lp-line-strong);
    border-radius: 14px;
    background: rgba(255,255,255,0.03);
    color: var(--lp-muted);
    text-align: center;
}
.lp-placeholder-name { font-weight: 700; color: #fff; }
.lp-placeholder-hint { font-size: 0.8rem; }
.lp-phone .lp-placeholder { aspect-ratio: 9 / 19.5; border: 0; }
.lp-section-light .lp-placeholder {
    border-color: var(--gray-400);
    background: var(--gray-50);
    color: var(--gray-500);
}
.lp-section-light .lp-placeholder-name { color: var(--gray-700); }

/* ── Rejilla de valor ───────────────────────────────────────────────────── */
.lp-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-4);
}
@media (min-width: 700px) { .lp-grid-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 700px) { .lp-grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .lp-grid-3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 700px) { .lp-grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .lp-grid-4 { grid-template-columns: repeat(4, 1fr); } }

.lp-card {
    padding: var(--space-5);
    border: 1px solid var(--lp-line);
    border-radius: var(--radius-lg);
    background: var(--lp-surface);
}
.lp-card-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px; height: 42px;
    margin-bottom: var(--space-4);
    border-radius: var(--radius-md);
    background: rgba(21,199,126,0.13);
    color: var(--lp-accent);
    font-size: 1.3rem;
}
.lp-card h3 { margin: 0 0 var(--space-2); font-size: 1.08rem; font-weight: 700; }
.lp-card p { margin: 0; color: var(--lp-muted); font-size: 0.95rem; }

/* Pasos: el número es el ancla visual, así que va grande y en el verde. */
.lp-step-n {
    display: block;
    margin-bottom: var(--space-2);
    font-size: clamp(2.4rem, 5vw, 3.4rem);
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.04em;
    color: var(--lp-accent);
    font-variant-numeric: var(--num);
}

/* ── Filas de producto (texto + captura, alternadas) ────────────────────── */
.lp-feature {
    display: grid;
    grid-template-columns: 1fr;
    gap: clamp(24px, 4vw, 48px);
    align-items: center;
}
.lp-feature + .lp-feature,
.lp-feature-apart { margin-top: clamp(48px, 8vw, 96px); }
@media (min-width: 940px) {
    .lp-feature { grid-template-columns: 0.85fr 1.15fr; }
    /* Alternar el lado evita que cuatro bloques seguidos se lean como una
       lista; el contenido va SIEMPRE primero en el DOM (orden de lectura). */
    .lp-feature-flip .lp-feature-media { order: -1; }
}
.lp-feature h3 {
    margin: 0 0 var(--space-3);
    font-size: clamp(1.35rem, 2.6vw, 1.75rem);
    font-weight: 700;
    letter-spacing: -0.02em;
}
.lp-feature p { margin: 0 0 var(--space-4); color: var(--lp-muted); }
.lp-section-light .lp-feature p { color: var(--gray-700); }

.lp-list { margin: 0; padding: 0; list-style: none; }
.lp-list li {
    position: relative;
    padding-left: 26px;
    margin-bottom: var(--space-2);
    color: var(--lp-text);
    font-size: 0.95rem;
}
/* Un teléfono ocupando media fila es un póster: la columna de una fila de
   producto llega a 600px y la pantalla de un móvil no se lee más grande por
   estar más grande. */
.lp-feature-media .lp-phone { max-width: 300px; margin-inline: auto; }

.lp-list li::before {
    content: "";
    position: absolute;
    left: 4px; top: 0.62em;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--lp-accent);
}
.lp-section-light .lp-list li { color: var(--gray-900); }

/* ── Tira de teléfonos del rider ────────────────────────────────────────── */
.lp-phones {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: clamp(18px, 3vw, 32px);
    max-width: 1000px;
    margin-inline: auto;
}
.lp-phone-item { display: flex; flex-direction: column; }
.lp-phone-item .lp-phone { margin-inline: auto; max-width: 300px; width: 100%; }
.lp-phone-label {
    margin: var(--space-3) 0 0;
    text-align: center;
    font-size: 0.9rem;
    font-weight: 700;
}
.lp-phone-sub {
    margin: 2px 0 0;
    text-align: center;
    color: var(--gray-500);
    font-size: 0.85rem;
}

/* ── Cierre ─────────────────────────────────────────────────────────────── */
.lp-closing {
    text-align: center;
    background-image:
        radial-gradient(60% 70% at 50% 0%, rgba(21,199,126,0.16), transparent 70%);
}
.lp-closing .lp-h2 { margin-inline: auto; text-align: center; }
.lp-closing-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-4);
    margin-top: var(--space-6);
}

/* ── Pie ────────────────────────────────────────────────────────────────── */
.lp-foot {
    padding-block: var(--space-6);
    border-top: 1px solid var(--lp-line);
    background: var(--lp-bg);
}
.lp-foot .lp-wrap {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
}
.lp-foot-logo { height: 26px; width: auto; display: block; }
.lp-foot p { margin: 0; color: var(--lp-muted); font-size: 0.88rem; }
.lp-foot a { color: #fff; font-weight: 600; text-decoration: none; }
.lp-foot a:hover { text-decoration: underline; }
/* Los créditos de los datos van debajo y en voz baja: es una obligación de
   licencia, no un mensaje de la página. */
.lp-foot .lp-credits {
    display: block;
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--lp-line);
}
.lp-credits p { font-size: 0.78rem; line-height: 1.6; }
.lp-legal { font-size: 0.82rem; }
.lp-legal a { color: inherit; text-decoration: underline; }

/* Un TEXTO LEGAL: una columna estrecha que se lee. Las tablas scrollean dentro
   de su caja en el teléfono en vez de ensanchar la página. */
.legal-doc { max-width: 46rem; margin: 0 auto; line-height: 1.65; }
.legal-doc h1 { font-size: 1.6rem; margin-bottom: var(--space-4); }
.legal-doc h2 { font-size: 1.15rem; margin-top: var(--space-5); }
.legal-doc h3 { font-size: 1rem; margin-top: var(--space-4); }
.legal-doc table { display: block; overflow-x: auto; width: 100%; border-collapse: collapse; margin: var(--space-4) 0; }
.legal-doc th, .legal-doc td { border: 1px solid var(--gray-200); padding: var(--space-2) var(--space-3); vertical-align: top; text-align: left; }
.legal-doc code { font-size: 0.9em; }
.legal-meta { font-size: 0.82rem; color: var(--gray-500); margin-bottom: var(--space-4); }
.lp-credits a { color: inherit; font-weight: 500; text-decoration: underline; }

/* Quien pide menos movimiento no quiere el desplazamiento animado del ancla. */
@media (prefers-reduced-motion: reduce) {
    .lp-scroll-smooth { scroll-behavior: auto; }
    .lp * { transition: none !important; }
}

/* ==========================================================================
   CUADRANTE SEMANAL DE JORNADAS (admin/shifts_week.html)
   ==========================================================================
   Riders (filas) × días (columnas), y a la derecha el horario que se le pone a
   lo elegido. Tres cosas que decide esta sección y no la plantilla:

   1. La grilla OCUPA EL ANCHO (`width: 100%` con `table-layout: fixed`) y a la
      vez conserva el suelo que la hace scrollear en un teléfono (`min-width`).
      Sin el 100% la tabla se quedaba en el ancho de su contenido y dejaba media
      pantalla vacía; sin el suelo, las 8 columnas se reparten los 336px de un
      móvil (42px cada una) y el `overflow-x` del contenedor no se activa nunca
      porque la tabla siempre "entra".
   2. El hueco de la derecha ES el formulario. El bloque de "asignar en lote"
      vivía DEBAJO, así que había que bajar y subir por cada tanda, y repetía la
      lista entera de riders en checkboxes — una segunda lista sin techo justo al
      lado del cuadrante donde ya estaban todos.
   3. Se ELIGE en la grilla: cada casilla lleva su checkbox y el panel solo dice
      qué horario ponerle. La selección se marca en la CELDA (`.is-picked`), no
      solo en la casilla de 15px, porque lo que se está eligiendo es la casilla
      del cuadrante entera.

   Los dos ejes llevan su total: el de la fila contesta "cuánto le toca a cada
   uno" y el de la columna "quién cubre el martes", que es la pregunta que se
   hace al repartir.
   ========================================================================== */

/* ── La semana: navegación y cifras, en una fila que envuelve ── */
.sw-weeknav {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    background: var(--surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    padding: 0.5rem 0.75rem;
    margin-bottom: var(--space-4);
}
.sw-weeknav-label { display: flex; flex-direction: column; line-height: 1.25; }
.sw-weeknav-label strong { font-size: 1rem; font-variant-numeric: var(--num); }
.sw-weeknav-today { font-size: 0.76rem; }
.sw-badge-now {
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--primary);
}
/* Las cifras se van al otro extremo: son contexto, no navegación. */
.sw-weeknav-stats {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    margin-left: auto;
    font-size: 0.8rem;
    color: var(--gray-500);
    font-variant-numeric: var(--num);
}
.sw-weeknav-stats i { margin-right: 0.1875rem; }

/* ── Las dos columnas ── */
.sw-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: var(--space-4);
    align-items: start;
}
.sw-main { min-width: 0; }
/* La grilla llega hasta el borde de la tarjeta: sin recortar, sus esquinas
   cuadradas asoman por debajo del radio de la card. */
.sw-main > .card { overflow: hidden; }
/* El panel acompaña al scroll: con veinte riders, el formulario que se queda
   arriba obliga a volver a subir para poner la hora de lo que se acaba de
   marcar abajo. */
.sw-panel { position: sticky; top: var(--space-3); }
@media (max-width: 1199.98px) {
    .sw-layout { grid-template-columns: 1fr; }
    .sw-panel { position: static; }
}

/* ── Cabecera de la tarjeta de la grilla: título + filtro de riders ── */
.sw-grid-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.sw-search { position: relative; min-width: 11.875rem; }
.sw-search > i {
    position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
    color: var(--gray-400); font-size: 0.85rem; pointer-events: none;
}
/* El hueco de la izquierda reserva la lupa, que se posiciona en px sobre el
   campo: se queda en px para no despegarse de ella (excepción del barrido). */
.sw-search input { padding-left: 28px; }

/* ── La grilla ──
   El contenedor scrollea en los DOS ejes y la cabecera + la columna del rider
   se quedan fijas: al llegar al jueves o al rider veinte, la pregunta que
   contesta la vista es justo de quién y de qué día es la casilla. */
.sw-scroll { overflow: auto; max-height: 72vh; }
.sw-grid {
    width: 100%;
    /* UMBRAL de scroll de la grilla, no densidad: se mide contra el viewport y
       se queda en px (el teléfono lo baja a 620px en §19). */
    min-width: 760px;
    table-layout: fixed;
    border-collapse: collapse;
}
.sw-grid th, .sw-grid td {
    border: 1px solid var(--gray-200);
    vertical-align: top;
    padding: 0.375rem;
}
.sw-grid thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--surface-sunken);
    text-align: center;
    padding: 0.3125rem 0.375rem;
}
.sw-grid th.sw-rider-col {
    position: sticky;
    left: 0;
    z-index: 3;
    width: 11.875rem;
    background: var(--surface);
    text-align: left;
}
.sw-grid thead th.sw-rider-col { z-index: 4; background: var(--surface-sunken); }
.sw-day-col.is-today { background: var(--primary-light) !important; }

/* Cabecera de un día: el nombre es el botón que marca la columna entera. */
.sw-dayhead {
    display: block;
    width: 100%;
    background: none;
    border: 1px solid transparent;
    border-radius: 6px;
    padding: 2px 0.25rem;
    color: inherit;
    cursor: pointer;
}
.sw-dayhead:hover, .sw-dayhead:focus-visible {
    border-color: var(--primary);
    background: var(--primary-light);
}
.sw-dayname {
    display: block;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-500);
}
.sw-daynum { display: block; font-size: 1rem; font-weight: 700; line-height: 1.1; }
.sw-daymeta {
    display: block;
    font-size: 0.67rem;
    color: var(--gray-500);
    font-variant-numeric: var(--num);
    white-space: nowrap;
}
.sw-daymeta-empty { color: var(--gray-400); font-style: italic; }

/* Identidad de la fila: el toggle, el nombre (enlace a sus jornadas) y las
   horas que ya le tocan. */
.sw-rider { display: flex; align-items: flex-start; gap: 0.375rem; min-width: 0; }
.sw-rider-id { min-width: 0; }
.sw-rider-name {
    display: block;
    font-weight: 600;
    font-size: 0.85rem;
    color: var(--gray-900);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sw-rider-name:hover { text-decoration: underline; }
.sw-rider-hours {
    font-size: 0.7rem;
    color: var(--gray-500);
    font-variant-numeric: var(--num);
}
.sw-rider-hours.is-empty { color: var(--gray-400); font-style: italic; }

/* Botón de "marcar el grupo": el de la fila es solo el icono (la columna del
   rider es estrecha y el nombre es lo que tiene que leerse). */
.sw-pick-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    background: none;
    border: 1px solid var(--gray-200);
    border-radius: 6px;
    color: var(--gray-500);
    font-size: 0.72rem;
    padding: 0.1875rem 0.4375rem;
    cursor: pointer;
}
.sw-pick-btn.is-icon { padding: 2px 0.25rem; font-size: 0.8rem; }
.sw-pick-btn:hover, .sw-pick-btn:focus-visible {
    border-color: var(--primary);
    color: var(--primary);
}

/* ── La casilla del cuadrante ── */
/* En una celda de tabla el suelo se pone con `height`, no con `min-height`: el
   segundo no se aplica y las filas sin jornada quedaban de 22px. */
.sw-cell { height: 3.875rem; position: relative; }
.sw-cell.is-today { background: color-mix(in srgb, var(--primary) 7%, transparent); }
.sw-cell.is-picked {
    background: var(--primary-light);
    box-shadow: inset 0 0 0 2px var(--primary);
}
.sw-cell:hover { background: var(--gray-50); }
.sw-cell.is-picked:hover { background: var(--primary-light); }

.sw-chip {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    background: color-mix(in srgb, var(--primary) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--primary) 38%, transparent);
    border-radius: 6px;
    padding: 2px 0.3125rem;
    margin-bottom: 0.1875rem;
    font-size: 0.73rem;
    line-height: 1.25;
    font-variant-numeric: var(--num);
}
.sw-chip.is-picked {
    background: var(--primary-light);
    border-color: var(--primary);
    box-shadow: inset 0 0 0 1px var(--primary);
}
/* La casilla de la jornada: tenue como la de la celda, y a la vista en cuanto
   está marcada — es lo que va a recibir el horario del panel. */
.sw-chip-pick { display: inline-flex; margin: 0; cursor: pointer; }
.sw-chip-pick input {
    /* Checkbox NATIVO: lo dibuja el sistema y su caja se declara en px
       (excepción del barrido, como la del mapa). */
    width: 12px; height: 12px; margin: 0;
    cursor: pointer;
    accent-color: var(--primary);
    opacity: 0.4;
}
.sw-cell:hover .sw-chip-pick input,
.sw-chip-pick input:checked,
.sw-chip-pick input:focus-visible { opacity: 1; }
.sw-chip-time { white-space: nowrap; }
.sw-chip-note { color: var(--gray-500); font-size: 0.7rem; }
/* Editar y eliminar aparecen al acercarse: en una grilla de 140 casillas, dos
   iconos por chip son ruido permanente. Con `:focus-within` siguen alcanzables
   por teclado. */
.sw-chip-acts {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: auto;
    opacity: 0;
    transition: opacity 0.12s ease;
}
.sw-chip:hover .sw-chip-acts,
.sw-chip:focus-within .sw-chip-acts { opacity: 1; }
.sw-chip-acts form { display: inline-flex; }
.sw-chip-acts a, .sw-chip-acts button {
    background: none; border: 0; padding: 0;
    line-height: 1; font-size: 0.72rem; cursor: pointer;
    color: var(--gray-500);
}
.sw-chip-acts a:hover { color: var(--primary); }
.sw-chip-acts button:hover { color: var(--danger); }

/* El pie de la casilla: elegirla. Tenue hasta que se pasa por encima. */
.sw-cell-foot {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin-top: 2px;
}
.sw-pick { display: inline-flex; margin: 0; cursor: pointer; }
.sw-pick input {
    /* Checkbox NATIVO, igual que el del chip: se queda en px. */
    width: 15px; height: 15px; margin: 0;
    cursor: pointer;
    accent-color: var(--primary);
    opacity: 0.4;
}
.sw-cell:hover .sw-pick input,
.sw-pick input:checked,
.sw-pick input:focus-visible { opacity: 1; }
.sw-empty-filter { color: var(--gray-500); font-size: 0.85rem; padding: 0.75rem 1rem; margin: 0; }

/* ── El panel de la derecha ── */
/* El interruptor de modo. Va ARRIBA del todo y ocupa el ancho: es lo primero
   que hay que leer, porque decide qué marca cada clic del cuadrante. Dos
   botones y no un checkbox: "poner" y "cambiar" son dos trabajos, no un
   estado encendido/apagado. */
.sw-mode {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.25rem;
    background: var(--gray-100);
    border: 1px solid var(--gray-200);
    border-radius: 10px;
    padding: 0.25rem;
    margin-bottom: var(--space-3);
}
.sw-mode-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    min-height: 2.375rem;
    padding: 0.25rem 0.5rem;
    background: none;
    border: 0;
    border-radius: 7px;
    color: var(--gray-600);
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.15;
    text-align: center;
    cursor: pointer;
}
.sw-mode-btn:hover { color: var(--gray-900); }
.sw-mode-btn.is-on {
    background: var(--surface);
    color: var(--primary);
    box-shadow: var(--shadow-sm);
}

/* Qué se puede marcar en cada modo. Lo que no se puede elegir no se DESHABILITA
   a medias: su casilla directamente no está, y lo que queda se lee más apagado
   — sigue siendo información (qué hay puesto / qué día es), pero no invita a
   tocarlo. */
[data-sw-mode="crear"] .sw-chip-pick { display: none; }
[data-sw-mode="crear"] .sw-chip { opacity: 0.75; }
[data-sw-mode="editar"] .sw-cell-foot { display: none; }
[data-sw-mode="editar"] .sw-cell:not(.has-shift) { opacity: 0.45; cursor: default; }
[data-sw-mode="editar"] .sw-cell:not(.has-shift):hover { background: none; }

.sw-picked {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    background: var(--gray-50);
    border: 1px solid var(--gray-200);
    border-radius: 8px;
    padding: 0.5rem 0.625rem;
}
.sw-picked-count { display: flex; align-items: baseline; gap: 0.375rem; min-width: 0; }
.sw-picked-num {
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1;
    color: var(--gray-900);
    font-variant-numeric: var(--num);
}
.sw-picked-label { font-size: 0.75rem; color: var(--gray-500); }
.sw-picked-hint { font-size: 0.74rem; color: var(--gray-500); margin: 0.375rem 0 0; }
/* "Horario" + la rueda que edita los turnos de atajo. La rueda es tenue: es
   configuración, se toca una vez y después estorba. */
.sw-horario-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin: var(--space-3) 0 0.25rem;
}
.sw-cog {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.625rem; height: 1.625rem;
    background: none;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--gray-400);
    font-size: 0.9rem;
    cursor: pointer;
}
.sw-cog:hover, .sw-cog:focus-visible {
    color: var(--primary);
    border-color: var(--gray-200);
    background: var(--gray-50);
}
.sw-presets { display: flex; gap: 0.3125rem; margin-bottom: 0.375rem; }
.sw-presets .btn { flex: 1; font-size: 0.72rem; padding: 0.1875rem 0.25rem; }
.sw-times { display: flex; align-items: center; gap: 0.375rem; }
.sw-times input { flex: 1; min-width: 0; }
.sw-times-sep { color: var(--gray-400); font-size: 0.8rem; }
.sw-total {
    font-size: 0.76rem;
    color: var(--gray-700);
    font-variant-numeric: var(--num);
    margin: 0.375rem 0 0;
}
/* Aplicar el horario y eliminar: el segundo solo está cuando hay jornadas
   elegidas, y va debajo — es la salida destructiva, no la de al lado. */
.sw-bulk-acts { display: grid; gap: 0.375rem; }
.sw-note { font-size: 0.72rem; color: var(--gray-500); margin: 0.5rem 0 0; }

/* ── El modal de los turnos de atajo (`_shift_presets_modal.html`) ──
   Una fila por turno: nombre + las dos horas. Sin estilos de modal propios —el
   chrome oscuro lo pone la sección de Bootstrap del admin—; acá solo la grilla
   de la fila, que es lo que Bootstrap no sabe. */
.sp-rows { display: flex; flex-direction: column; gap: var(--space-2); }
.sp-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto;
    align-items: center;
    gap: 6px;
}
.sp-sep { color: var(--gray-400); font-size: 0.8rem; }
@media (max-width: 575.98px) {
    /* El nombre se lleva su renglón: tres campos y una flecha no entran en
       360px, y el que se comprime es justo el que se lee. */
    .sp-row { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
    .sp-label { grid-column: 1 / -1; }
}

/* ==========================================================================
   EL ALTA/EDICIÓN DE PEDIDO: LA ENTREGA COMO UNIDAD
   ==========================================================================
   El formulario de pedido dejó de ser una pila de bloques con título ("Dirección",
   "Contacto", "Ventana horaria", "Productos" al final en una matriz aparte) y pasó
   a leerse por PARADA: cada entrega es una tarjeta con su dirección, su contacto,
   su franja y —al final— lo que se deja en esa puerta. Lo que se busca es que la
   pregunta y su respuesta estén juntas y que el alta entre en una pantalla.
   -------------------------------------------------------------------------- */

/* El buscador ES el campo de la dirección (modo SLIM del macro): se lee más que
   ningún otro campo del formulario, así que pesa un poco más. */
.address-line-input { font-weight: 500; }

/* La columna de las DECISIONES (precio · ruta · plantilla · los botones) baja
   con el scroll: un pedido con seis entregas mide varias pantallas y "Guardar"
   quedaba al final de la columna larga, o sea a un viaje de vuelta arriba.

   Es la columna ENTERA la que se pega, no una tarjeta suelta: `.mess-form-sticky`
   se retiró justo porque una tarjeta pegajosa arriba de una PILA deja que las de
   abajo le pasen por DETRÁS. Acá se mueve el bloque completo, así que no hay
   nada que pueda pasar por detrás de nada.

   Con `max-height` + `overflow-y`: si las decisiones no entran en la pantalla
   (tarifa automática desplegada, con su desglose), el bloque scrollea POR
   DENTRO en vez de dejar los botones fuera de alcance. Necesita el
   `align-items: start` que la grilla ya tiene. Solo de `lg` para arriba: por
   debajo la grilla se apila y la barra de acciones ya es sticky abajo. */
@media (min-width: 992px) {
    .mess-form-grid > .mess-form-col.is-sticky {
        position: sticky;
        top: var(--space-4);
        max-height: calc(100vh - var(--space-6));
        overflow-y: auto;
        /* El scroll interno recorta a ras del borde: un pelo de aire para que
           no se coma la sombra de las tarjetas. */
        padding-right: 4px;
    }
}

/* El peso APROXIMADO de una entrega: un rango y, solo con "Otro", el número. */
.stop-peso-controls {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    flex-wrap: wrap;
}
/* Un número de kilos no necesita el ancho de un campo de texto, y estirado
   hasta el borde se lee como si esperara una frase. */
.stop-peso-controls .peso-value { flex: 0 1 8.75rem; }

/* Los productos de UNA entrega. Se separan del resto del bloque con una línea:
   es la única sección de la tarjeta que no es "dónde y a quién". */
.stop-products {
    border-top: 1px solid var(--gray-200);
    padding-top: var(--space-3);
}
.product-picker-bar .stop-product-search { min-width: 0; }
@media (min-width: 576px) {
    .product-picker-bar .stop-product-search { flex: 1 1 11.25rem; }
}
/* La lista FLOTA sobre lo que hay debajo, así que necesita superficie propia:
   en el tema oscuro del admin un .list-group-item queda transparente y se leería
   el formulario a través de ella. */
.stop-product-results {
    top: 100%;
    z-index: 6;
    max-height: 15rem;
    overflow-y: auto;
    background: var(--surface-raised);
    border-radius: var(--radius-md);
}
.stop-products-list { margin-top: var(--space-2); }
.stop-products-empty {
    margin: var(--space-2) 0 0;
    font-size: 0.85rem;
    color: var(--gray-500);
}
.stop-item-line {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    padding: 0.375rem 0;
    border-bottom: 1px solid var(--gray-100);
}
.stop-item-line:last-child { border-bottom: 0; }
.stop-item-name {
    flex: 1 1 10rem;
    min-width: 0;
    font-size: 0.9rem;
}
.stop-item-new { display: flex; gap: var(--space-2); }
.stop-item-new .stop-item-newname { flex: 1 1 7.5rem; min-width: 0; }
.stop-item-new .stop-item-unit { flex: 0 0 6.875rem; }
.stop-item-line .cell-qty-input { flex: 0 0 5.5rem; width: 5.5rem; }
.stop-item-remove {
    flex: 0 0 auto;
    padding: 2px 0.375rem;
    color: var(--gray-500);
    border: none;
    background: transparent;
}
.stop-item-remove:hover { color: var(--danger); }
.stop-item-badge { flex-basis: 100%; }

/* "Agregar otro punto de entrega": es la acción que abre trabajo nuevo y estaba
   escondida en un botón de contorno gris del tamaño de un enlace. Ancho completo
   y con el ÁMBAR de las entregas — el mismo código de color de la tarjeta a la
   que va a sumar. */
.btn-add-stop {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px;
    font-weight: 600;
    color: var(--stop-drop-ink);
    background: transparent;
    border: 2px dashed var(--stop-drop);
    border-radius: var(--radius-md);
}
.btn-add-stop:hover {
    color: var(--stop-drop-ink);
    border-style: solid;
}

/* Los dos extras del PEDIDO (foto y nota), en la cabecera de la tarjeta de
   entrega. Solo el icono: el nombre va en `title`/`aria-label`, como las
   acciones de la libreta. El blanco táctil se pone a mano. */
.order-extras {
    display: flex;
    gap: 6px;
    margin-left: auto;
}
.order-extra-btn {
    position: relative;
    display: inline-flex;
    width: 34px;
    height: 34px;
    align-items: center;
    justify-content: center;
    padding: 0;
    color: var(--gray-600);
    background: transparent;
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-md);
}
.order-extra-btn:hover {
    color: var(--gray-900);
    border-color: var(--gray-400);
}
/* El puntito dice que HAY algo cargado sin abrir el modal. */
.order-extra-dot {
    position: absolute;
    top: -3px;
    right: -3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--stop-drop);
}

/* ==========================================================================
   LA CAMPANA (`.ntf-*`) — avisos in-app
   ==========================================================================
   Un namespace propio para las TRES superficies que dibujan un aviso, porque
   las tres emiten el MISMO marcado: el panel que cuelga de la campana (lo
   pinta `static/js/notifications.js`), la página `/notifications` —la salida
   sin JS— y el bloque del Inicio del rider, que no tiene campana a propósito.
   Dos marcados para el mismo aviso acaban divergiendo; acá hay uno.

   El PANEL flota sobre el contenido, así que lleva **superficie propia**: sin
   un fondo declarado se leería la pantalla de abajo a través suyo. No hace
   falta una regla por rol — `--surface-raised` y los grises ya están
   remapeados bajo `body.role-admin`, así que el panel nace oscuro sobre el
   chrome del dispatcher y claro en el portal, que es exactamente lo que se
   quiere. El rider conserva los tokens claros.

   El tamaño va en `rem` (densidad); lo reinflado del chrome vive en su
   sección, más abajo — la campana ES chrome y no puede encoger dos veces. */
.ntf-wrap { position: relative; display: inline-flex; }

/* La campana es un enlace de VERDAD a /notifications con el click
   interceptado: sin JS sigue llevando a la bandeja. */
.ntf-bell {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.1rem; height: 2.1rem;
    border-radius: var(--radius-md);
    color: inherit; text-decoration: none;
    font-size: 1.05rem;
}
.ntf-bell:hover { background: var(--gray-100); color: inherit; }

/* El badge no dice "hay avisos": dice CUÁNTOS sin leer. El tope de «99+» es de
   presentación y lo aplican por igual la plantilla y el JS. */
.ntf-badge {
    position: absolute; top: 0.05rem; right: 0.05rem;
    min-width: 1rem; padding: 0 0.2rem;
    border-radius: 999px;
    background: var(--danger); color: #fff;
    font-size: 0.6rem; font-weight: 700; line-height: 1rem;
    text-align: center;
}

.ntf-panel {
    position: absolute; top: calc(100% + 0.5rem); right: 0;
    width: 22rem; max-width: calc(100vw - 2rem);
    display: flex; flex-direction: column;
    background: var(--surface-raised);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    box-shadow: var(--elevation-3);
    /* Encima de la topbar (1045): el panel cuelga de ella. */
    z-index: 1050;
    overflow: hidden;
}
/* `[hidden]` es una regla del NAVEGADOR y pierde contra el `display` de una
   clase: sin esto el panel nace desplegado y el toggle no cambia nada — que
   es el mismo tropiezo que ya tienen anotado `.map-addmenu` y `.map-daynav`. */
.ntf-panel[hidden] { display: none; }

.ntf-panel-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3);
    border-bottom: 1px solid var(--gray-200);
}
.ntf-panel-title { font-size: 0.8rem; font-weight: 600; color: var(--gray-900); }
/* «Marcar todas» es un gesto explícito, no el efecto de abrir el panel. */
.ntf-mark-all {
    background: none; border: none; padding: 0;
    color: var(--primary); font-size: 0.7rem; font-weight: 600;
    cursor: pointer;
}
.ntf-mark-all:hover { text-decoration: underline; }
.ntf-panel-acts { display: inline-flex; align-items: center; gap: var(--space-2); }
/* Limpiar VACÍA la bandeja: va en gris, no en el color de acción. Lo que
   borra sin vuelta atrás no se ofrece con el mismo peso que lo que no. */
.ntf-clear-all {
    background: none; border: none; padding: 0;
    color: var(--gray-500); font-size: 0.7rem; font-weight: 600;
    cursor: pointer;
}
.ntf-clear-all:hover { color: var(--danger); text-decoration: underline; }
.ntf-panel-body { overflow-y: auto; max-height: 60vh; }
.ntf-panel-foot {
    display: block; padding: var(--space-2);
    border-top: 1px solid var(--gray-200);
    color: var(--gray-500); font-size: 0.7rem; font-weight: 600;
    text-align: center; text-decoration: none;
}
.ntf-panel-foot:hover { color: var(--primary); }

/* ── La FILA de un aviso, igual en las tres superficies ────────────────────
   En el panel es un `<a>` (el JS marca y navega); sin JS es un `<button>`
   dentro de un `<form method="POST">`, porque marcar leído ESCRIBE. Las dos
   se ven igual: el marcado de dentro es el mismo. */
.ntf-list { list-style: none; margin: 0; padding: 0; }
/* `position: relative` porque las acciones FLOTAN sobre la fila: van dentro
   del `<li>` pero fuera del enlace (un `<a>` dentro de otro `<a>` y un
   `<form>` dentro de otro `<form>` no existen), así que se posicionan encima
   en vez de robarle ancho al texto. */
.ntf-item { position: relative; border-bottom: 1px solid var(--gray-100); }
.ntf-item:last-child { border-bottom: none; }
.ntf-item form { margin: 0; }
/* El formulario de limpiar no dibuja nada: su botón lo alcanza por `form=`. */
.ntf-del-form { display: none; }
.ntf-item-btn {
    display: flex; align-items: flex-start; gap: var(--space-2);
    width: 100%; padding: var(--space-3);
    background: none; border: none; text-align: left;
    color: inherit; text-decoration: none; cursor: pointer;
}
.ntf-item-btn:hover { background: var(--gray-50); color: inherit; }
/* El punto es lo ÚNICO que distingue una no leída: el peso de la letra
   acompaña, pero un estado no se cuelga solo de la tipografía. */
.ntf-dot {
    flex: 0 0 auto; width: 0.4rem; height: 0.4rem; margin-top: 0.4rem;
    border-radius: 50%; background: transparent;
}
.ntf-item.is-unread .ntf-dot { background: var(--primary); }
.ntf-item-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.ntf-item-title { font-size: 0.78rem; font-weight: 600; color: var(--gray-900); }
.ntf-item.is-unread .ntf-item-title { font-weight: 700; }
.ntf-item-sub { font-size: 0.72rem; color: var(--gray-500); }
.ntf-item-age { font-size: 0.66rem; color: var(--gray-400); }
/* El texto deja sitio a la derecha SIEMPRE, tape o no un icono: si el hueco
   apareciera con el hover, la línea se recortaría al pasar por encima. */
.ntf-item-btn { padding-right: 3.4rem; }

/* ── Las acciones de una fila (mapa · limpiar) ─────────────────────────────
   Se ven al pasar por encima o al tabular hasta ellas: son un SEGUNDO gesto,
   y dibujadas siempre competirían con el destino de la fila. En una pantalla
   táctil no hay hover, así que ahí se quedan a la vista (más abajo): esconder
   una acción detrás de un gesto que no existe es no tenerla. */
.ntf-item-actions {
    position: absolute; top: var(--space-2); right: var(--space-2);
    display: flex; align-items: center; gap: 0.15rem;
    opacity: 0; transition: opacity 0.12s ease;
}
.ntf-item:hover .ntf-item-actions,
.ntf-item:focus-within .ntf-item-actions { opacity: 1; }
.ntf-item-act {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.5rem; height: 1.5rem;
    padding: 0; border: none; border-radius: var(--radius-sm);
    background: none; color: var(--gray-500);
    font-size: 0.8rem; line-height: 1; text-decoration: none; cursor: pointer;
}
.ntf-item-act:hover { background: var(--gray-100); color: var(--gray-900); }
@media (hover: none) {
    .ntf-item-actions { opacity: 1; }
}
.ntf-empty {
    margin: 0; padding: var(--space-4);
    color: var(--gray-500); font-size: 0.75rem; text-align: center;
}

/* ── /notifications: la bandeja entera ────────────────────────────────────
   La misma lista dentro de una tarjeta de contenido (acá no flota nada). */
.ntf-page {
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.ntf-more { padding: var(--space-3); text-align: center; }

/* ── El bloque del Inicio del RIDER ────────────────────────────────────────
   Sin campana y sin JS: la nav es de cuatro ítems y no se toca. La tarjeta
   sigue el tratamiento de las suyas (blanca, radio grande). */
.ntf-rider {
    background: #fff;
    border-radius: 18px;
    box-shadow: var(--elevation-1);
    overflow: hidden;
}
/* ── La campana EN EL RIDER ────────────────────────────────────────────────
   Va en la cabecera de su Inicio, al lado del contador de la jornada (su nav
   sigue siendo de cuatro ítems y no se toca). Acá manda el DEDO, no la
   densidad: `html.role-rider` queda fuera de `--ui-scale` y sus objetivos de
   toque son de 44px, como el resto de su pantalla. */
body.role-rider .ntf-bell { width: 44px; height: 44px; font-size: 1.25rem; }
body.role-rider .ntf-bell:hover { background: transparent; }
/* El panel se mide contra la PANTALLA, no contra la campana: en un teléfono
   22rem no entran a la derecha de un icono que ya está pegado al borde. */
body.role-rider .ntf-panel {
    width: min(22rem, calc(100vw - 2 * var(--space-4)));
}

.ntf-count {
    display: inline-block; min-width: 1.1rem; padding: 0 0.3rem;
    border-radius: 999px;
    background: var(--primary); color: #fff;
    font-size: 0.68rem; font-weight: 700; line-height: 1.1rem;
    text-align: center;
}

/* Formulario de empresa (spec 015): con «La sede es la misma dirección»
   marcada, el bloque de facturación no se ve. Decide el servidor por la
   casilla; esto es solo vista. */
form:has(#billing_same_as_sede:checked) .billing-fields { display: none; }

/* ==========================================================================
   DENSIDAD: LO QUE NO ENCOGE
   ==========================================================================
   `--ui-scale` (arriba, en `:root`) baja el font-size RAÍZ, así que alcanza a
   TODO lo que está en `rem` — que es casi toda la tipografía de la app y todo
   Bootstrap. Dos superficies no tienen que seguirlo, porque ya se ajustaron a
   mano en su momento y una segunda reducción las dejaría el doble de chicas:

     ① el CHROME (sidebar + topbar). El sidebar ya bajó de 240px a 216px y su
        navegación se leyó a 0,8rem justamente para quitarle sitio al contenido.
        Además `--sidebar-w` y `--topbar-h` están en px y NO encogen: una letra
        más chica dentro de una barra del mismo alto es aire, no densidad.
     ② la TORRE DE CONTROL (`.dash-page`), que ya está comprimida a propósito
        para entrar en una pantalla sin desplazarse.

   El mecanismo es `--ui-unscale` (la inversa exacta de la escala), y son las
   ÚNICAS dos cosas que hace esta sección:
     · redefinir los tokens `--space-*` a su tamaño natural dentro del scope,
       que reinfla de una vez los usos de `var(--space-*)` que heredan;
     · multiplicar por `--ui-unscale` los `font-size` en `rem` de esos scopes.

   Va ANTES de "EL DISPATCHER EN EL TELÉFONO" a propósito: tiene que ganar por
   orden a los bloques que reinfla (que están más arriba) pero NO puede pisar
   los overrides del teléfono. Aun así sería inocuo — en el móvil `--ui-scale`
   vale 1, con lo que `--ui-unscale` también y cada `calc(x * 1)` de acá se
   queda en `x`—; el orden es cinturón, no mecanismo.

   Los px literales de padding/margin/gap del resto de la app NO se tocan en
   esta tanda: no siguen al raíz y hay que revisarlos pantalla por pantalla.
   ========================================================================== */

/* ── ① El chrome: sidebar y topbar ─────────────────────────────────────────
   El scope cubre el sidebar entero y la topbar entera. `.app-main` NO entra:
   ahí abajo cuelga `.app-content`, que es justo lo que sí tiene que encoger. */
.app-sidebar,
.app-topbar,
.app-brand {
    --space-1: calc(0.25rem * var(--ui-unscale));
    --space-2: calc(0.5rem  * var(--ui-unscale));
    --space-3: calc(0.75rem * var(--ui-unscale));
    --space-4: calc(1rem    * var(--ui-unscale));
    --space-5: calc(1.25rem * var(--ui-unscale));
    --space-6: calc(1.5rem  * var(--ui-unscale));
}

.app-brand { font-size: calc(1.15rem * var(--ui-unscale)); }
.app-brand .bi { font-size: calc(1.3rem * var(--ui-unscale)); }
.app-sidebar-company { font-size: calc(0.78rem * var(--ui-unscale)); }
.app-sidebar-link { font-size: calc(0.8rem * var(--ui-unscale)); }
.app-sidebar-link i { font-size: calc(0.95rem * var(--ui-unscale)); }
.app-sidebar-group-head { font-size: calc(0.68rem * var(--ui-unscale)); }
/* El `!important` es el del bloque original (gana al `font-size` del icono de
   `.app-sidebar-link i`): sin repetirlo acá esta regla no llegaría a aplicarse. */
.app-sidebar-caret { font-size: calc(0.68rem * var(--ui-unscale)) !important; }
.app-sidebar-toggle { font-size: calc(1.4rem * var(--ui-unscale)); }

/* El buscador de la topbar mide TODO en rem (alto de campo, hueco del icono y
   posición del icono): si sólo se reinflara la letra, el icono se despegaría
   del texto. */
.app-topbar .navbar-search-wrap input[type="search"] {
    padding: calc(0.35rem * var(--ui-unscale)) calc(1rem * var(--ui-unscale))
             calc(0.35rem * var(--ui-unscale)) calc(2.25rem * var(--ui-unscale));
    font-size: calc(0.875rem * var(--ui-unscale));
}
.app-topbar .navbar-search-wrap .search-icon {
    left: calc(0.75rem * var(--ui-unscale));
    font-size: calc(0.85rem * var(--ui-unscale));
}

/* La CAMPANA es chrome: ya está calibrada contra una topbar de alto FIJO, así
   que sigue al sidebar y al buscador y no a la escala del contenido. El panel
   entra en el mismo scope porque cuelga de ella —visualmente pertenece a la
   barra—, y sus tokens de espacio ya vienen reinflados por el bloque de
   arriba: acá solo van los `font-size` y las medidas escritas en rem. */
.app-topbar .ntf-bell {
    width: calc(2.1rem * var(--ui-unscale));
    height: calc(2.1rem * var(--ui-unscale));
    font-size: calc(1.05rem * var(--ui-unscale));
}
.app-topbar .ntf-badge { font-size: calc(0.6rem * var(--ui-unscale)); }
.app-topbar .ntf-panel { width: calc(22rem * var(--ui-unscale)); }
.app-topbar .ntf-panel-title { font-size: calc(0.8rem * var(--ui-unscale)); }
.app-topbar .ntf-mark-all { font-size: calc(0.7rem * var(--ui-unscale)); }
.app-topbar .ntf-clear-all { font-size: calc(0.7rem * var(--ui-unscale)); }
.app-topbar .ntf-item-act { font-size: calc(0.8rem * var(--ui-unscale)); }
.app-topbar .ntf-panel-foot { font-size: calc(0.7rem * var(--ui-unscale)); }
.app-topbar .ntf-item-title { font-size: calc(0.78rem * var(--ui-unscale)); }
.app-topbar .ntf-item-sub { font-size: calc(0.72rem * var(--ui-unscale)); }
.app-topbar .ntf-item-age { font-size: calc(0.66rem * var(--ui-unscale)); }
.app-topbar .ntf-empty { font-size: calc(0.75rem * var(--ui-unscale)); }

/* ── ② La torre de control (`/admin/dashboard`) ────────────────────────────
   El scope es `.dash-page`, que es la pantalla entera. Casi toda su tipografía
   ya estaba declarada en su propia sección (incluidos los controles que trae
   Bootstrap: el `<select>` de asignar rider y los botones de rango tienen
   font-size propio), así que reinflarla es enumerar esa lista y nada más.
   Sus paddings ya estaban en px y por eso no aparecen acá. */
.dash-page {
    --space-1: calc(0.25rem * var(--ui-unscale));
    --space-2: calc(0.5rem  * var(--ui-unscale));
    --space-3: calc(0.75rem * var(--ui-unscale));
    --space-4: calc(1rem    * var(--ui-unscale));
    --space-5: calc(1.25rem * var(--ui-unscale));
    --space-6: calc(1.5rem  * var(--ui-unscale));
}

/* Cabecera y barra de estado (los valores son los ya comprimidos de
   `.dash-page`, no los de la cabecera genérica). */
.dash-page .dash-title { font-size: calc(1.12rem * var(--ui-unscale)); }
.dash-page .dash-title-sub { font-size: calc(0.76rem * var(--ui-unscale)); }
.dash-page .dash-section-label { font-size: calc(0.7rem * var(--ui-unscale)); }
.dash-page .ops-label { font-size: calc(0.64rem * var(--ui-unscale)); }
.dash-page .ops-num { font-size: calc(1.45rem * var(--ui-unscale)); }
.dash-page .ops-sub { font-size: calc(0.68rem * var(--ui-unscale)); }

/* Tarjetas del tablero. */
.dash-page .dash-panel-head h2 { font-size: calc(0.74rem * var(--ui-unscale)); }
.dash-page .dash-range button { font-size: calc(0.66rem * var(--ui-unscale)); }
.dash-page .tc-stat-label { font-size: calc(0.63rem * var(--ui-unscale)); }
/* El `vw` del clamp NO se reinfla: es viewport, no tipografía — escalarlo
   rompería justo lo que el clamp resuelve (que una cifra de cinco dígitos no
   toque el borde de su columna). */
.dash-page .tc-stat-num,
.dash-page .wl-num b,
.dash-page .dash-kpi-value {
    font-size: clamp(calc(0.98rem * var(--ui-unscale)),
                     calc(0.5rem * var(--ui-unscale) + 0.72vw),
                     calc(1.35rem * var(--ui-unscale)));
}
.dash-page .dash-delta { font-size: calc(0.7rem * var(--ui-unscale)); }
.dash-page .dash-delta i { font-size: calc(0.62rem * var(--ui-unscale)); }
.dash-page .wl-num span { font-size: calc(0.62rem * var(--ui-unscale)); }
.dash-page .dash-kpi-label { font-size: calc(0.63rem * var(--ui-unscale)); }
.dash-page .dash-kpi-sub { font-size: calc(0.65rem * var(--ui-unscale)); }

/* Filas de rider y de empresa. */
.dash-page .dl-avatar-ph { font-size: calc(0.9rem * var(--ui-unscale)); }
.dash-page .dl-name { font-size: calc(0.81rem * var(--ui-unscale)); }
.dash-page .dl-stat { font-size: calc(0.68rem * var(--ui-unscale)); }
.dash-page .dl-stat b { font-size: calc(0.78rem * var(--ui-unscale)); }

/* La cola. Los tres primeros son los valores ya comprimidos de `.dash-page`;
   el resto los hereda de la cola genérica y también hay que sostenerlos. */
.dash-page .queue-row-id { font-size: calc(0.78rem * var(--ui-unscale)); }
.dash-page .queue-leg { font-size: calc(0.73rem * var(--ui-unscale)); }
.dash-page .queue-row-sub { font-size: calc(0.7rem * var(--ui-unscale)); }
.dash-page .queue-row .status-badge { font-size: calc(0.66rem * var(--ui-unscale)); }
.dash-page .queue-group-title { font-size: calc(0.68rem * var(--ui-unscale)); }
.dash-page .queue-group-title .count { font-size: calc(0.7rem * var(--ui-unscale)); }
.dash-page .queue-leg-more { font-size: calc(0.68rem * var(--ui-unscale)); }
.dash-page .queue-row-age { font-size: calc(0.72rem * var(--ui-unscale)); }
.dash-page .queue-assign select { font-size: calc(0.78rem * var(--ui-unscale)); }
.dash-page .queue-assign button { font-size: calc(0.78rem * var(--ui-unscale)); }
.dash-page .queue-empty { font-size: calc(0.82rem * var(--ui-unscale)); }

/* ==========================================================================
   ADMIN · PLANTILLAS (`/admin/templates`)
   ==========================================================================
   La lista de moldes del dispatcher. Es la hermana de la del PORTAL
   (`.ct-*`, más arriba): misma lectura —qué molde hay, qué genera y desde
   cuándo se repite— y por eso el reparto es el mismo, pero con clases propias
   a propósito: dos pantallas no comparten un nombre de clase, y ésta lleva
   cosas que la del cliente no tiene (recurrencia, activar/desactivar, borrar).

   Lo que vive acá estaba en un `style=` en línea de la plantilla (dos
   `min-width` a ojo), que es justo lo que el barrido del teléfono no puede
   alcanzar: un mínimo de 260px escrito en el marcado le gana a cualquier
   regla de §21. Ahora es una regla más de la hoja. */
.tpl-card-grid {
    display: flex; flex-wrap: wrap; gap: var(--space-3);
    align-items: flex-start; justify-content: space-between;
}
/* `min-width: 0` y no un mínimo en px: lo que no entra ENVUELVE, y una
   dirección larga no puede empujar la tarjeta fuera de la pantalla. */
.tpl-main { flex: 1 1 320px; min-width: 0; }
.tpl-side {
    flex: 0 1 300px; min-width: 0;
    display: flex; flex-direction: column; gap: var(--space-2);
}
/* Generar el pedido: la fecha y el verbo. Envuelve, porque en el teléfono el
   `<input type="date">` solo ya se lleva el renglón. */
.tpl-use { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: flex-end; }
.tpl-use > div { flex: 1 1 9rem; min-width: 0; }
/* Recurrencia · Editar · Activar · Eliminar: las acciones de la FILA. */
.tpl-row-actions {
    display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: flex-end;
}

/* ==========================================================================
   ADMIN · TARIFA (`/admin/pricing/...`)
   ==========================================================================
   Las cinco pantallas de la tabla de precios (ciudades, zonas, códigos
   postales, festivos y la configuración general). Lo único que necesitan de
   estructura es el ancho del precio que se edita EN LA FILA de una zona: eso
   estaba en un `style="width: 6rem"` en línea, que es justo lo que el barrido
   del teléfono no puede alcanzar —un ancho escrito en el MARCADO le gana a
   cualquier regla de §22—. `max-width: 100%` es lo que lo deja encoger cuando
   la columna es más estrecha que las 6rem. */
.pz-price { width: 6rem; max-width: 100%; }

/* ==========================================================================
   ADMIN + PORTAL - INFORME DE FACTURACION (`.bill-*`)
   ==========================================================================
   Cuanto cobrarle a cada local por un periodo. Es una TABLA de locales
   (`.disp-table`, que ya sabe leerse) mas tres cosas propias: la cabecera del
   periodo con el total, la marca de PROVISIONAL y el detalle por pedido, que
   vive en un `<details>` ocupando su propia fila.

   Todo en rem: aca la densidad es la de siempre (`--ui-scale`) y esta familia
   entra en el barrido del panel, igual que `.disp-*` o `.cal-*`.

   Sin colores literales: el chrome oscuro del admin se resuelve remapeando los
   tokens, asi que una regla que escriba un `#fff` se ve rota en una de las dos
   caras. Y esta hoja la comparten el dispatcher y el PORTAL, que la dibuja en
   claro. */

.bill-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.bill-head-period {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--gray-700);
}

/* El periodo que contiene HOY arrastra trabajo abierto: su importe todavia se
   mueve. Se dice, no se esconde. */
.bill-provisional {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: var(--space-2);
    padding: 0.1rem 0.5rem;
    border: 1px solid var(--warning);
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--warning);
}

.bill-head-total { text-align: right; }

.bill-total-label {
    display: block;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--gray-500);
}

/* La cifra que se viene a mirar. `tabular-nums` para que dos importes de la
   misma pantalla se comparen columna contra columna. */
.bill-total-value {
    display: block;
    font-size: 1.6rem;
    font-weight: 700;
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
    color: var(--gray-900);
}

.bill-total-sub { font-size: 0.78rem; color: var(--gray-500); }

.bill-local-link { color: inherit; font-weight: 600; text-decoration: none; }
.bill-local-link:hover { text-decoration: underline; }

/* Los sueltos no son un local: no llevan enlace y se leen apagados. */
.bill-loose { color: var(--gray-500); font-weight: 600; }

/* El detalle es lo SECUNDARIO: su fila no lleva el borde superior de la tabla,
   asi que se lee pegada al local del que cuelga. */
.bill-detail-row > td {
    padding-top: 0;
    padding-bottom: 0;
    border-top: 0;
}

.bill-detail > summary {
    cursor: pointer;
    padding: 0.3rem 0;
    font-size: 0.78rem;
    color: var(--gray-500);
}

.bill-detail[open] > summary { margin-bottom: 0.25rem; }

/* El CSV de UN local cuelga del detalle abierto: es la accion de ese
   bloque, no de la pantalla. */
.bill-detail-actions { margin-bottom: var(--space-2); }

.bill-orders { margin-bottom: var(--space-2); font-size: 0.82rem; }

.bill-id { font-variant-numeric: tabular-nums; color: var(--gray-500); }

/* El trayecto de un pedido: la recogida y TODAS sus entregas, con el codigo de
   color de siempre (`.disp-route`/`.leg`, los mismos de la cola y del
   calendario). Que no estire la columna: en un local con veinte pedidos, una
   direccion larga decide el ancho de la tabla entera. */
.bill-route .leg { max-width: 34rem; }
.bill-route .leg-txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Lo que sube a la bici, en UNA linea, igual que `.cc-load` en el dia del
   calendario del cliente: el reparto detallado vive en la ficha del pedido:
   aca se reconoce lo que se llevo. La cantidad es lo que se mira, asi que es lo
   unico que se destaca. */
.bill-load {
    display: flex; flex-wrap: wrap; gap: 2px 0.625rem;
    margin: 2px 0 0.25rem; padding-left: 1.1875rem;
    font-size: 0.74rem; color: var(--gray-500);
}
.bill-load-qty { font-weight: 700; color: var(--gray-700); }

/* La pastilla de peso, PEGADA a su direccion y no en una columna propia: es el
   total del pedido en la recogida y el de esa puerta en una entrega, asi que
   una columna no podria decir las dos cosas. Pegada y no empujada al borde
   (`margin-left: auto`, lo que hace la ventana horaria en `.cc-route`): aca la
   linea puede medir 34rem y el peso quedaria a media pantalla de su puerta. */
.bill-route .peso-chip { flex: 0 0 auto; margin-left: 0.375rem; }


/* ==========================================================================
   EL DOCUMENTO DE COBRO (`.inv-*`) — facturas
   ==========================================================================
   Namespace propio, separado de `.bill-*`: aquel es el INFORME (refleja el
   estado actual) y esto es la FACTURA (congela). Que se parezcan en pantalla
   no los hace lo mismo, y compartir clases haria que tocar una moviera la
   otra.

   Nada de colores literales: en admin el chrome oscuro remapea `--gray-*` y un
   literal se queda claro sobre fondo oscuro. */

/* El estado, que es lo unico que se busca de un vistazo en la lista. El color
   NO es decorativo: dice que se puede hacer con esa factura. */
.inv-badge {
    font-weight: 600;
    font-size: 0.72rem;
    padding: 0.25em 0.6em;
    border: 1px solid transparent;
}
/* El borrador es el unico que todavia no es un documento: va apagado. */
.inv-borrador {
    background: var(--gray-100);
    color: var(--gray-600);
    border-color: var(--gray-300);
}
/* Cada estado con SU color, el mismo que el boton que lo pone: azul lo
   emitido, ambar lo enviado, verde lo cobrado, rojo lo anulado. Antes salian
   los cuatro grises porque colgaban de unos tokens `--info-bg`/`--warning-bg`
   que NO EXISTEN y caian al fallback: una pastilla de color que es gris no
   dice nada, y era el dato que se busca de un vistazo en la lista.

   Se compone con `color-mix` sobre los tokens de verdad —el patron de los
   pines de parada—, asi que funciona igual bajo el chrome oscuro del admin sin
   un solo literal. */
.inv-emitida {
    background: color-mix(in srgb, var(--info) 16%, transparent);
    color: var(--info);
    border-color: color-mix(in srgb, var(--info) 45%, transparent);
}
.inv-enviada {
    background: color-mix(in srgb, var(--warning) 18%, transparent);
    color: var(--warning);
    border-color: color-mix(in srgb, var(--warning) 45%, transparent);
}
.inv-cobrada {
    background: color-mix(in srgb, var(--success) 16%, transparent);
    color: var(--success);
    border-color: color-mix(in srgb, var(--success) 45%, transparent);
}
/* Anulada: roja Y tachada. El color solo no le dice "esto ya no vale" a quien
   no distingue el rojo, asi que la raya se queda. */
.inv-anulada {
    background: color-mix(in srgb, var(--danger) 14%, transparent);
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 45%, transparent);
    text-decoration: line-through;
}

/* Los totales: alineados a la derecha en una columna estrecha pegada al borde,
   que es donde se los busca en cualquier factura de papel. */
.inv-totals {
    margin-left: auto;
    max-width: 22rem;
    display: grid;
    gap: 0.25rem;
    font-size: 0.86rem;
}
.inv-totals > div {
    display: flex;
    justify-content: space-between;
    gap: var(--space-4);
}
.inv-totals span { color: var(--gray-600); }
/* El TOTAL se separa con una LINEA, nunca con una caja (misma regla que las
   cifras de la torre de control). */
.inv-total-final {
    border-top: 1px solid var(--gray-300);
    padding-top: 0.25rem;
    margin-top: 0.25rem;
    font-size: 1rem;
}

/* ── Las acciones de ESTADO de una factura ─────────────────────────────────
   Un solo componente para las dos superficies. Lo que cambia entre ellas es el
   TAMANO (`.is-lg` en la ficha, donde son la linea de acciones de la pantalla)
   y nada mas: el marcado es el mismo macro. */
.inv-actions { display: flex; flex-wrap: wrap; gap: 0.375rem; }
.inv-actions .inv-act-form { display: contents; }
.inv-act { display: inline-flex; align-items: center; gap: 0.375rem; }

/* La LINEA de la ficha: centrada y separada de la cabecera. Es donde se decide
   lo que le pasa al documento, asi que no comparte fila con "Volver". */
.inv-actionbar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-md);
}
.inv-actionbar .inv-actions { justify-content: center; }
/* Un poco mas grande que un boton de barra: se tiene que entender de un
   vistazo cual es cual, que es justo lo que un icono suelto no consigue. */
.inv-actions.is-lg .inv-act {
    font-size: 0.92rem;
    padding: 0.5rem 0.9rem;
    gap: 0.5rem;
}
.inv-actions.is-lg .inv-act i { font-size: 1.1em; }

/* En la LISTA compiten con seis columnas: ahi el icono manda y la palabra se
   retira. El nombre sigue estando en el `title` y en el `aria-label`, asi que
   no se pierde para quien no ve el icono. */
.disp-table .inv-actions .inv-act-txt {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.disp-table .inv-actions .inv-act { padding: 0.25rem 0.45rem; }

/* CUANDO se marco cada estado. Lo que no paso por aca no se dibuja. */
.inv-timeline {
    display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem;
    justify-content: center;
}
.inv-stamp { font-size: 0.72rem; color: var(--gray-500); }
.inv-stamp strong { color: var(--gray-600); font-weight: 600; }

.inv-logo-preview {
    max-height: 5rem;
    max-width: 14rem;
    object-fit: contain;
}

/* ── El selector de pedidos de una factura (`.inv-picker`) ─────────────────
   El TRAYECTO va entero (las N recogidas y las M entregas, las 2..N de cada
   lado desplegables): aca se decide que se cobra. Como en el informe, que una
   direccion larga no decida el ancho de la tabla entera. */
.inv-picker .disp-route .leg { max-width: 30rem; }
.inv-picker .disp-route .leg-txt {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* El `#id` es el enlace a la ficha del pedido, y se lee como el numero que es:
   ni azul ni subrayado en reposo. Mismo criterio que `.bill-local-link` en el
   informe, que es el enlace gemelo en la tabla de al lado. */
.inv-order-link { color: inherit; font-weight: 700; text-decoration: none; }
.inv-order-link:hover { text-decoration: underline; }

/* El PRECIO tiene dos caras del mismo dato y solo una a la vez.
   SIN JS manda el campo: es lo unico que puede guardar sin un script. Con JS
   (`.is-js`, que pone `invoice-orders.js`) manda la cifra, y el campo aparece
   al tocar el lapiz. Asi "una vez fijado solo sale el precio" es el estado de
   reposo, no algo que haya que conseguir. */
.inv-price {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.25rem;
    flex-wrap: wrap;
}
.inv-price-view { display: none; align-items: center; gap: 0.25rem; }
.inv-price-edit { display: inline-flex; align-items: center; gap: 0.1875rem; }
.inv-picker.is-js .inv-price-view { display: inline-flex; }
.inv-picker.is-js .inv-price-edit { display: none; }
.inv-picker.is-js .inv-price.is-editing .inv-price-view { display: none; }
.inv-picker.is-js .inv-price.is-editing .inv-price-edit { display: inline-flex; }

.inv-price-value { font-variant-numeric: tabular-nums; }

/* El lapiz solo se ve al pasar por encima: la columna se LEE mucho mas de lo
   que se escribe, y un icono por fila la ensucia. Con el foco tambien, o no
   habria forma de llegar con el teclado; en tactil no hay "pasar por encima",
   asi que ahi se queda a la vista. */
.inv-price-pencil {
    opacity: 0;
    border: 0;
    background: transparent;
    color: var(--gray-500);
    padding: 0.125rem 0.25rem;
    line-height: 1;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: opacity 0.12s ease, color 0.12s ease;
}
.inv-price:hover .inv-price-pencil,
.inv-price-pencil:focus-visible { opacity: 1; }
.inv-price-pencil:hover { color: var(--primary); }
@media (hover: none) {
    .inv-price-pencil { opacity: 1; }
}

.inv-price-edit input {
    width: 5.5rem;
    text-align: right;
    font-size: 0.82rem;
    padding: 0.125rem 0.375rem;
    border: 1px solid var(--gray-300);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: inherit;
}
.inv-price-edit input:focus { border-color: var(--primary); outline: none; }
.inv-price-edit input::-webkit-outer-spin-button,
.inv-price-edit input::-webkit-inner-spin-button {
    -webkit-appearance: none; margin: 0;
}
.inv-price-edit input[type="number"] {
    -moz-appearance: textfield; appearance: textfield;
}
.inv-price-cur { font-size: 0.82rem; font-weight: 600; color: var(--gray-500); }
.inv-price-edit button {
    border: 0;
    background: var(--primary);
    color: #fff;
    padding: 0.1875rem 0.375rem;
    line-height: 1;
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.inv-price-edit button:hover { background: var(--primary-dark); }
.inv-price.is-saving { opacity: 0.6; pointer-events: none; }

/* Lo que fallo se DICE donde se intento, no en un flash de otra pantalla. */
.inv-price-msg {
    flex: 1 0 100%;
    text-align: right;
    font-size: 0.72rem;
    color: var(--danger);
}
.inv-price:not(.is-error) .inv-price-msg { display: none; }

/* ── La franja de facturas pendientes del portal (`.inv-pendbar`) ──────────
   Arriba de la pantalla y CENTRADA. Es un aviso del PORTAL, asi que no puede
   dar por hecho el chrome oscuro del admin: sale de los tokens de siempre y
   compone el color con `color-mix`, como las pastillas de estado.

   No es `position: fixed`: va en el flujo, arriba del contenido. Fija se
   comeria la primera linea de cada pantalla y en el telefono taparia la barra
   de acciones, que es justo donde se trabaja. */
.inv-pendbar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
    margin: 0 var(--content-pad) var(--space-3);
    padding: 0.5rem 2.25rem 0.5rem 0.875rem;
    position: relative;
    border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
    background: color-mix(in srgb, var(--warning) 14%, var(--surface));
    color: var(--gray-700);
    border-radius: var(--radius-md);
    font-size: 0.82rem;
    text-align: center;
}
.inv-pendbar[hidden] { display: none; }

/* ── La franja del pago del plan (`.plan-paybar`, specs/011) ──────────────
   La del ADMINISTRADOR: misma forma que la de facturas, sin la X (no se
   cierra) y con los tokens, que bajo `body.role-admin` ya son los del chrome
   oscuro. En el flujo, no fija, por lo mismo que la de facturas. */
.plan-paybar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
    margin: 0 var(--content-pad) var(--space-3);
    padding: 0.5rem 0.875rem;
    border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
    background: color-mix(in srgb, var(--warning) 14%, var(--surface));
    color: var(--gray-700);
    border-radius: var(--radius-md);
    font-size: 0.82rem;
    text-align: center;
}
.plan-paybar-txt { font-weight: 600; }
.plan-paybar-link { color: var(--primary); font-weight: 600; }
/* La del portal sin pedidos (`_portal_paused_bar.html`): neutra, sin el tono
   de aviso del pago. */
.plan-paybar.is-neutral {
    border-color: var(--gray-300);
    background: var(--surface);
}
/* El marco del administrador SUSPENDIDO (specs/011, B-3): sin panel, el logo
   y cerrar sesión (`partials/_suspended_bar.html`). */
.plan-suspended-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--content-pad);
    border-bottom: 1px solid var(--gray-200);
    background: var(--surface);
}
.inv-pendbar-txt { font-weight: 600; }
.inv-pendbar-amount { font-variant-numeric: tabular-nums; }
.inv-pendbar-link { color: var(--primary); font-weight: 600; }
/* La X se mide por el DEDO en el telefono (44px, regla 4) y con el raton alcanza
   con el area del icono. Va absoluta a la derecha para que no participe del
   centrado del texto. */
.inv-pendbar-close {
    position: absolute;
    top: 50%;
    right: 0.25rem;
    transform: translateY(-50%);
    border: 0;
    background: transparent;
    color: var(--gray-500);
    font-size: 1.25rem;
    line-height: 1;
    padding: 0.125rem 0.5rem;
    cursor: pointer;
}
.inv-pendbar-close:hover { color: var(--gray-700); }
@media (max-width: 767.98px) {
    .inv-pendbar { font-size: 0.82rem; }
    .inv-pendbar-close {
        min-width: 44px;
        min-height: 44px;
    }
}

/* ==========================================================================
   EL DISPATCHER EN EL TELÉFONO
   ==========================================================================
   El dispatcher trabaja de pie: la cola de pedidos, el calendario (mes / día /
   semana / rango), las jornadas y el mapa se miran desde el móvil tanto como
   desde el escritorio. Esta sección es el único sitio donde vive ese ajuste, y
   va AL FINAL del archivo a propósito: son overrides de bloques que ya existen
   más arriba y tienen que ganar por ORDEN, no por especificidad inflada.

   El corte es `767.98px` (y no los `575.98px` del formulario de pedido): lo que
   se rompe acá no son columnas de Bootstrap sino barras de acciones, filas de
   filtros y tablas densas, y eso ya no entra en una tableta en vertical.
   Ninguna regla de acá llega al escritorio.
   ========================================================================== */

/* ── 1. Barra de acciones de una pantalla ──────────────────────────────────
   `.disp-toolbar` es título + acciones. En el teléfono se apilan, y las
   acciones ENVUELVEN en tantos renglones como haga falta, repartiéndose el
   ancho del bloque.
   Antes era una fila que scrollaba en horizontal y sangraba hasta el borde. Se
   cambió porque una barra de ACCIONES no es una barra de filtros: los pills de
   estado se recorren de vez en cuando y el que importa suele estar al
   principio, pero acá el botón que más se usa —"Nuevo Pedido"— es el ÚLTIMO, y
   quedaba fuera de cuadro hasta deslizar la fila entera. En una pantalla que se
   mira de pie, un botón que hay que buscar es un botón que no está.
   `flex: 1 1 auto` reparte lo que sobra de cada renglón, así que la fila
   termina a ras del bloque en vez de dejar un hueco a la derecha, y
   `white-space: normal` deja que una etiqueta larga parta en dos líneas antes
   que desbordar. */
.disp-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
@media (max-width: 767.98px) {
    .disp-toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }
    .disp-toolbar .dash-title { font-size: 1.15rem; }
    .disp-actions { flex-wrap: wrap; }
    /* Un `<form>` (eliminar, generar…) es un botón más de la fila: sin esto se
       queda en su ancho de contenido y rompe el reparto del renglón. */
    .disp-actions > .btn,
    .disp-actions > form { flex: 1 1 auto; min-width: 0; }
    .disp-actions > form { display: flex; }
    .disp-actions > form > .btn { flex: 1; }
    .disp-actions > .btn,
    .disp-actions > form > .btn {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 0.3125rem;
        white-space: normal;
        text-align: center;
        padding-left: 0.625rem;
        padding-right: 0.625rem;
    }
}

/* ── 2. Filas de pills (estado, día de servicio, rangos rápidos) ───────────
   Ocho estados + siete días de servicio son quince pills: envueltos ocupaban
   cinco renglones y empujaban la tabla fuera de la primera pantalla. Misma
   respuesta que las acciones: una fila que scrollea y sangra hasta el borde.
   Va acotado a `.app-content` del admin — el portal de empresa y el rider
   tienen sus propias superficies y ninguna usa `.nav-pills` así. */
@media (max-width: 767.98px) {
    body.role-admin .app-content .nav-pills {
        flex-wrap: nowrap;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        margin-left: calc(var(--content-pad) * -1);
        margin-right: calc(var(--content-pad) * -1);
        padding: 0 var(--content-pad);
    }
    body.role-admin .app-content .nav-pills::-webkit-scrollbar { display: none; }
    body.role-admin .app-content .nav-pills > .nav-item { flex: 0 0 auto; }
    body.role-admin .app-content .nav-pills .nav-link {
        white-space: nowrap;
        padding: 7px 12px;
    }
}

/* ── 3. Los FILTROS se pliegan ─────────────────────────────────────────────
   Diez campos en columna son ~600px de formulario ANTES de la primera fila de
   la tabla: en el teléfono, la pantalla que sirve para mirar pedidos no mostraba
   ninguno. Se pliegan en un `<details>` cuyo resumen dice CUÁNTOS filtros hay
   puestos: un filtro activo que no se ve es peor que uno que estorba, porque
   explica una lista vacía sin decirlo.
   En el escritorio el `<details>` no se nota: el resumen no se dibuja y el
   formulario se ve entero como siempre. Nace con `open`, así que sin JS el
   comportamiento del teléfono es también el de siempre. */
.disp-filterbox { margin-bottom: var(--space-4); }
.disp-filterbox > summary { display: none; }
.disp-filterbox > .disp-filters { margin-bottom: 0; }
@media (max-width: 767.98px) {
    .disp-filterbox > summary {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        list-style: none;
        cursor: pointer;
        padding: 0.625rem 0.75rem;
        border: 1px solid var(--gray-200);
        border-radius: var(--radius-md);
        background: var(--surface);
        font-size: 0.88rem;
        font-weight: 600;
        color: var(--gray-700);
        min-height: 44px;
    }
    .disp-filterbox > summary::-webkit-details-marker { display: none; }
    .disp-filterbox > summary::after {
        content: "\F282";                       /* bi-chevron-down */
        font-family: "bootstrap-icons";
        margin-left: auto;
        font-size: 0.8rem;
        color: var(--gray-400);
        transition: transform 0.15s;
    }
    .disp-filterbox[open] > summary::after { transform: rotate(180deg); }
    .disp-filterbox[open] > summary {
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
    }
    .disp-filter-count {
        background: var(--primary);
        color: #fff;
        border-radius: 999px;
        font-size: 0.7rem;
        font-weight: 700;
        padding: 1px 0.5rem;
    }
    /* El formulario abierto: dos columnas, que es lo que hace que "Desde /
       Hasta" y "Precio min / max" se lean como el par que son. */
    .disp-filterbox[open] > .disp-filters {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-2);
        padding: var(--space-3);
        border: 1px solid var(--gray-200);
        border-top: 0;
        border-radius: 0 0 var(--radius-md) var(--radius-md);
    }
    /* El buscador y la fila de botones ocupan el ancho entero. */
    .disp-filterbox > .disp-filters > .disp-filter-wide,
    .disp-filterbox > .disp-filters > .d-flex { grid-column: 1 / -1; }
    .disp-filterbox > .disp-filters > .d-flex > .btn { flex: 1 1 0; min-height: 44px; }
    /* 16px es el mínimo que evita el zoom automático de iOS al enfocar, y 44px
       el objetivo de toque. El `style="width:92px"` en línea de los precios se
       neutraliza acá: en una celda de grilla, un ancho fijo deja media columna
       vacía. */
    .disp-filters .form-control,
    .disp-filters .form-select {
        font-size: 16px;
        min-height: 44px;
        width: 100%;
        max-width: none;
    }
}

/* ── 4. Tablas densas: TARJETAS en el teléfono ─────────────────────────────
   Una tabla de ocho columnas en 360px no se arregla comprimiéndola: o se parte
   el texto letra a letra o desborda la página. Con `.is-cards` cada fila pasa a
   ser una TARJETA y cada celda una línea con su etiqueta, que sale del atributo
   `data-label` de la propia celda — la cabecera desaparece, así que sin la
   etiqueta un "Sin asignar" suelto no dice de qué columna es.
   La `<tr>` conserva su `data-href`: el click de fila sigue funcionando igual.
   Las tablas SIN `.is-cards` (una matriz de números como el resumen por rango)
   se quedan como tabla y scrollean: ahí las columnas SON la información. */
@media (max-width: 767.98px) {
    .disp-table-wrap {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior-x: contain;
    }
    /* `max-content` en vez de un ancho a ojo: cada columna se queda con lo que
       necesita y la tabla scrollea lo que haga falta, ni un píxel más. */
    .disp-table:not(.is-cards) { min-width: max-content; }

    .disp-table.is-cards,
    .disp-table.is-cards tbody,
    .disp-table.is-cards tfoot,
    .disp-table.is-cards tr,
    .disp-table.is-cards td { display: block; }
    .disp-table.is-cards thead { display: none; }
    .disp-table.is-cards tbody tr {
        padding: 0.625rem 0.75rem;
        border-bottom: 1px solid var(--gray-200);
    }
    .disp-table.is-cards td {
        display: flex;
        align-items: baseline;
        gap: 0.5rem;
        padding: 0.1875rem 0;
        border: 0;
    }
    .disp-table.is-cards td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 5.25rem;
        font-size: 0.64rem;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: var(--gray-400);
    }
    /* El identificador es el TÍTULO de la tarjeta, no un campo más: va solo,
       arriba y sin etiqueta (para eso lleva el #). */
    .disp-table.is-cards td:first-child {
        display: block;
        font-size: 0.95rem;
        margin-bottom: 2px;
    }
    /* La cabecera de grupo (prioridad operativa o día) sigue siendo una franja
       de ancho completo: es lo que ordena la lista. */
    .disp-table.is-cards tr.disp-group-row { padding: 0; }
    .disp-table.is-cards tr.disp-group-row td {
        display: block;
        padding: 0.5625rem 0.75rem;
        font-size: 0.72rem;
    }
    .disp-table.is-cards tr.disp-group-row td::before { content: none; }
    .disp-group-link { float: none; display: block; margin-top: 0.1875rem; }
    /* Las direcciones dejan de estar acotadas a 320px y de recortarse con
       puntos suspensivos: en una tarjeta hay ancho para la línea entera, y la
       dirección es lo único que se lee de verdad en movimiento. */
    .disp-table.is-cards .disp-route { min-width: 0; }
    .disp-table.is-cards .disp-route .leg {
        max-width: none;
        white-space: normal;
        align-items: flex-start;
        overflow: visible;
    }
    .disp-table.is-cards .disp-route .leg i { padding-top: 0.1875rem; }
    /* Las acciones de la fila son lo ÚLTIMO de la tarjeta y con el dedo hay
       que poder darles: 44px, separadas del resto y alineadas a la derecha.
       La etiqueta "Acciones" no se dibuja — dos iconos no la necesitan. */
    .disp-table.is-cards td[data-label="Acciones"]::before { content: none; }
    .disp-table.is-cards td[data-label="Acciones"] {
        justify-content: flex-end;
        margin-top: 0.375rem; padding-top: 0.5rem;
        border-top: 1px solid var(--gray-200);
    }
    .cal-row-actions { gap: 0.5rem; }
    .cal-row-act { width: 44px; height: 44px; font-size: 1rem; }

    /* Aceptar / rechazar desde la fila: dos botones de DEDO, a lo ancho. */
    .acc-actions.is-compact .acc-btn { min-width: 44px; padding: 0; font-size: 1.05rem; }
    /* «Hablar de este pedido» de una fila: un objetivo de dedo. */
    .chat-order-btn.is-compact { width: 44px; height: 44px; font-size: 1.15rem; }
    /* En modo tarjeta, la fila que no tiene nada que decidir no dibuja la
       línea «Acción pendiente» vacía. */
    .disp-table.is-cards td.is-empty { display: none; }
    .acc-btn { min-height: 44px; padding: 0 0.875rem; font-size: 0.9rem; justify-content: center; }
    .disp-table.is-cards .acc-btn { width: 100%; }
    .acc-banner .acc-actions { width: 100%; }
    .acc-chip { padding: 0.375rem 0.75rem; font-size: 0.85rem; }
    /* El cajón está cerrado: el número se asoma sobre el botón que lo abre. */
    .app-sidebar-toggle { position: relative; }
    .acc-count.is-toggle {
        display: inline-block; position: absolute; top: -0.25rem; right: -0.375rem; margin: 0;
    }

    /* Asignar un rider desde la fila: el select y el botón a lo ancho. */
    .disp-table.is-cards .disp-assign { flex: 1; min-width: 0; }
    .disp-table.is-cards .disp-assign select { flex: 1; min-width: 0; min-height: 38px; }
    .disp-table.is-cards .disp-assign button { min-height: 38px; }

    /* Poner un precio desde la fila: control de dedo y el botón SIEMPRE
       visible (en el teléfono no hay hover que lo descubra). */
    .disp-table.is-cards .disp-price { flex: 1; min-width: 0; }
    /* Sin ratón no hay nada que descubra un campo sin caja: acá se dibuja
       siempre como lo que es. */
    .disp-table.is-cards .disp-price input {
        flex: 1; min-width: 0; min-height: 38px; font-size: 16px;
        border-color: var(--gray-200); background: var(--surface);
    }
    .disp-table.is-cards .disp-price button {
        min-height: 38px; min-width: 44px;
        opacity: 1; pointer-events: auto;
    }
}

/* ── 5. El CALENDARIO de despacho (mes · día · semana · rango) ─────────────
   Las cuatro caras siguen el MODELO DE LA TORRE DE CONTROL, que es la
   referencia del panel en el teléfono (ver `docs/archivo/PLAN_movil_dispatcher.md`):

     ① un SCOPE de pantalla (`.cal-page`, que la plantilla solo MARCA),
     ② la tipografía a ~0,6× de la de escritorio,
     ③ el aire en px acompañando a la letra,
     ④ lo que no entra se REORDENA o se apila, nunca se esconde,
     ⑤ y dos excepciones que no se tocan: 16px donde se ESCRIBE (por debajo
        iOS hace zoom al enfocar) y 44px donde se TOCA.

   El porqué del punto ② en esta pantalla: en escritorio el panel va a
   `--ui-scale: 0.85`, y en el corte móvil la escala vuelve a 1 — o sea que sin
   este bloque el calendario se dibuja MÁS GRANDE en el teléfono que en el
   monitor, que es justo al revés de lo que hace falta.

   Todo va scopeado a `.cal-page` por dos motivos: el `.cal-*` lo comparte el
   calendario de JORNADAS y el `.disp-*` lo comparten la cola y los listados.
   (Ese calendario de jornadas ya lleva la marca desde el paso 8: hereda de acá
   la celda y el número del día, y sus cinco clases propias las encoge §19.)

   Del mes, siete columnas no se negocian: es un mes. Lo que se achica es todo
   lo demás, y el COLOR de la celda —lo que se lee de un vistazo— no se toca. */
@media (max-width: 767.98px) {
    /* ── El MES ─────────────────────────────────────────────────────────── */
    .cal-cell { padding: 0.1875rem 2px; min-height: 46px; position: relative; }
    .cal-page .cal-day-num { font-size: 0.52rem; margin-bottom: 2px; }
    .cal-chips { gap: 2px; }
    .cal-page .cal-chip { font-size: 0.42rem; padding: 2px 0.25rem; gap: 2px; }
    /* El `+` va arriba a la derecha, sobre el aire que deja el número del día.
       El objetivo de toque se agranda con PADDING, no con tamaño de fuente:
       es de los que no encogen (⑤). */
    .cal-cell > .mt-1 { position: absolute; top: 0; right: 0; margin: 0 !important; }
    .cal-cell > .mt-1 .cal-add { display: block; padding: 0.375rem 0.4375rem; font-size: 0.62rem; }
    .cal-page .cal-table thead th { font-size: 0.46rem; padding: 0.25rem 2px !important; }

    /* ── El DÍA ─────────────────────────────────────────────────────────── */
    .cal-page .cal-day-title { font-size: 0.86rem; }
    .cal-page .cal-day-state { font-size: 0.48rem; padding: 0.1875rem 0.4375rem; }
    .cal-page .cal-day-sub { font-size: 0.52rem; }
    .cal-page .cal-section-head { font-size: 0.6rem; }
    .cal-page .cal-section-count { font-size: 0.48rem; padding: 1px 0.4375rem; }

    /* Las filas del día ya son TARJETAS (§4). Lo que cambia acá es la escala:
       la etiqueta de cada línea encoge y con ella la columna que reserva, así
       que el dato —la dirección, el rider, la empresa— se lleva el ancho. */
    .cal-page .disp-table td { font-size: 0.56rem; padding: 0.375rem 0.5rem; }
    .cal-page .disp-table thead th { font-size: 0.46rem; padding: 0.375rem 0.5rem; }
    .cal-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .cal-page .disp-table.is-cards td:first-child { font-size: 0.64rem; }
    .cal-page .disp-sub { font-size: 0.5rem; }
    .cal-page .status-badge { font-size: 0.52rem; padding: 0.2rem 0.45rem; }
    .cal-page .badge { font-size: 0.5rem; }
    /* El icono encoge; el BOTÓN no (⑤): sigue midiendo 44px. */
    .cal-page .cal-row-act { font-size: 0.8rem; }

    /* ── La SEMANA ──────────────────────────────────────────────────────── */
    /* La tabla scrollea por diseño (7 días + la columna del rider no entran en
       ninguna pantalla chica) y su columna fija se queda. Lo que cambia es
       cuánto hay que arrastrar: con la letra a 0,6× el mínimo baja de 1080 a
       760px, o sea casi un tercio menos de recorrido para llegar al domingo. */
    .cal-page .cal-week { min-width: 760px; }
    .cal-page .cal-week th,
    .cal-page .cal-week td { padding: 0.25rem; }
    .cal-page .cal-week-rider-col { width: 96px; }
    .cal-page .cal-week-rider-name { font-size: 0.56rem; }
    .cal-page .cal-week-dayname { font-size: 0.46rem; }
    .cal-page .cal-week-route { padding: 0.25rem 0.3125rem; }
    .cal-page .cal-week-route-name { font-size: 0.52rem; }
    .cal-page .cal-week-route-meta { font-size: 0.44rem; }
    .cal-page .cal-week-all { font-size: 0.44rem; }
    .cal-page .cal-week-order { font-size: 0.46rem; }
    /* "a esta ruta" / "nueva ruta" son BOTONES de verdad (repartir es un POST,
       no un drag): encogen como todo, pero conservan un alto de dedo — los
       44px de la regla ⑤ no caben en una grilla de siete columnas, así que
       acá el objetivo de toque se acota a lo que la celda permite y queda
       ANOTADO como la excepción que es. */
    .cal-page .cal-week-drop {
        font-size: 0.46rem;
        padding: 0.3125rem 0.4375rem;
        min-height: 28px;
    }
}

/* ── 6. Calendario de la SEMANA: la columna del rider se queda ──────────────
   La tabla de reparto scrollea en horizontal por diseño (7 días + la columna de
   riders no entran en ninguna pantalla chica). Sin fijar la primera columna, al
   llegar al jueves ya no se sabe de quién es la fila que se está mirando, que es
   la única pregunta que contesta esta vista. Vale en todos los anchos: donde la
   tabla entera entra, `sticky` no hace nada. */
.cal-week th.cal-week-rider-col {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--surface);
}
.cal-week thead th.cal-week-rider-col { z-index: 3; }

/* ── 7. Cabecera de sección del calendario ────────────────────────────────
   `.cal-section-head` lleva título + contador + un botón empujado con `ms-auto`.
   Sin dejarla envolver, el botón se salía de la pantalla. */
.cal-section-head { flex-wrap: wrap; }
@media (max-width: 767.98px) {
    .cal-section-head > .btn,
    .cal-section-head > form { margin-left: 0 !important; }
    .cal-section-head > form { width: 100%; }
    .cal-section-head > form > .btn { width: 100%; }
}

/* ── 8. El MAPA del dispatcher ─────────────────────────────────────────────
   El cuerpo ya se apila bajo 900px (mapa arriba, panel abajo). Lo que faltaba
   era el REPARTO: la barra de arriba se comía cuatro renglones —título,
   navegación por día, alcance, calendario, añadir, contador y ciudad— y al mapa
   le quedaban 58vh medidos contra el viewport GRANDE, o sea que ni siquiera
   entraba entero en la pantalla visible. Con muchos pines encima, elegir
   recogidas para armar una ruta era hacer zoom a ciegas.

   La regla de esta pantalla: **el mapa es el contenido, todo lo demás es
   chrome**. Así que el chrome encoge y se ordena en renglones que se leen, y el
   mapa se queda con lo que sobra de la pantalla VISIBLE.

     ① el `<h1>` sale del flujo (queda para el lector de pantalla): una pantalla
        que es un mapa no necesita que la nombren, y ese renglón es del mapa;
     ② tres renglones fijos y previsibles — ‹ día › · alcance | ciudad ·
        contador + calendario + añadir— en vez de cuatro impredecibles;
     ③ los controles bajan a 26px de alto y 0.68rem, por debajo de la densidad
        del escritorio (`--ui-scale` vuelve a 1 en el teléfono, así que este
        chrome se veía MÁS grande acá que en un portátil);
     ④ el canvas mide `100svh` menos el chrome —`svh`, el viewport CHICO: con
        `vh` el mapa se dibuja más alto que la pantalla y la última franja queda
        debajo de la barra del navegador— menos una FRANJA de agarre, y sangra a
        los dos bordes (§11), que son 32px más de ancho útil.

   Lo de la franja no es estética: el canvas se queda con el gesto del dedo, así
   que un mapa que llega justo al borde de abajo deja la página sin ningún sitio
   desde donde desplazarla. Se declara aparte (`--map-grab-h`) para poder
   calibrarla sin tocar el resto de la cuenta. */
@media (max-width: 767.98px) {
    /* El padding de borde de arriba se va ENTERO: el mapa es la pantalla, y
       esos píxeles se los lleva el canvas. La barra queda al ras de la superior,
       que es el borde de una barra contra otra — no hace falta aire entre dos
       chromes. */
    .map-screen {
        gap: 0.25rem;
        margin-top: calc(var(--content-pad) * -1);
    }
    .map-screen-bar { gap: 0.125rem; }
    /* Fuera del flujo, no borrado: la página conserva su encabezado. */
    .map-screen-bar > h1 {
        position: absolute;
        width: 1px; height: 1px;
        margin: -1px; padding: 0; border: 0;
        overflow: hidden; clip-path: inset(50%); white-space: nowrap;
    }
    /* Los dos renglones. El `order` los fija: sin él, cada ancho de pantalla
       arma una barra distinta. (El selector de ciudad que ocupaba el renglón
       del medio ya no existe: el mapa es toda España y se navega a mano —
       docs/PLAN_MAPA_MULTICLIENTE.md §1.5 A.) */
    .map-daynav { order: 1; flex: 1 1 100%; }
    .map-daynav-label { flex: 1; }
    /* El contador se queda EN LA BARRA, en su propio renglón: su contenido es
       variable ("8 paradas · 2 sin rutear" más los chips de atrasadas y de
       previstos), así que junto a cualquier otro control envuelve igual.
       Flotándolo sobre el mapa se probó y se revirtió: tapaba el terreno justo
       en la esquina en la que se empieza a mirar. */
    .map-daycount {
        order: 3;
        /* El SEGUNDO renglón lo abre el contador, y su base tiene que decirlo:
           el 60% NO cabe detrás del día (100%), así que envuelve SIEMPRE. Con la base en el contenido (`auto`) un día con
           chips —"2 sin rutear", "3 atrasadas", "N previstos"— medía más que el
           renglón entero y empujaba el calendario y el ▾ a una CUARTA línea
           pegados al borde IZQUIERDO, y desde ahí el menú, que abre hacia la
           izquierda, se salía de la pantalla. Con la base en 0 el problema se
           dio vuelta: cabía en el renglón de la ciudad con ancho CERO y el
           texto desbordaba fuera de la pantalla.
           Lo que no entra envuelve DENTRO de la píldora (`flex-wrap`), que es
           lo que deja fijos los dos botones de la derecha. */
        flex: 1 1 60%; min-width: 0; width: auto;
        justify-content: flex-start;
        flex-wrap: wrap; row-gap: 0.1875rem;
    }
    /* Los dos botones de cargar trabajo comparten el renglón del contador y se
       van al extremo DERECHO. */
    .map-addbtns { order: 4; margin-left: auto; }
    /* El botón partido de las DEMÁS pantallas: su menú abre anclado con
       `right: 0`, así que en un teléfono se acota al ancho de la pantalla o se
       sale por el costado. */
    .map-addmenu {
        min-width: 0;
        width: max-content;
        max-width: calc(100vw - var(--content-pad) * 2);
    }

    /* Los dos botones se quedan en el ICONO: en una barra que ya envuelve, la
       palabra le roba el renglón al contador. El `title` sigue
       diciendo qué hace cada uno. */
    .map-daycal-text { display: none; }

    /* Densidad del chrome. El alto de toque queda en 26px: son controles de
       CHROME (navegar el día), no campos de formulario —
       ahí sigue mandando la regla de los 44px, que no se toca. */
    .map-screen-bar .btn-sm {
        --bs-btn-padding-y: 0.0625rem;
        --bs-btn-padding-x: 0.3125rem;
        --bs-btn-font-size: 0.66rem;
        min-height: 24px;
    }
    .map-daynav-label { font-size: 0.68rem; padding: 0.0625rem 0.3125rem; min-height: 24px; }
    .map-daycount { font-size: 0.64rem; padding: 0.0625rem 0.375rem; }

    /* ④ El mapa se queda con la pantalla MENOS una franja, y el descuento va
       en DOS variables porque son dos cosas distintas:

       - `--map-bar-h` es lo que hay ENCIMA del canvas dentro de la página (los
         renglones de la barra y su separación). **La MIDE el navegador**: el
         valor de acá es solo el arranque, y `map-dispatcher.js` lo reescribe
         con la distancia real entre el borde de arriba de `.map-screen` y el
         del canvas (`syncMapBarHeight`, con un ResizeObserver sobre la barra).
         Estimarlo era la razón por la que el mapa no llegaba abajo: la barra
         mide dos renglones o tres según el ancho, la fuente del sistema y lo
         que diga el contador del día, y ese sobrante se lo comía el chrome.
       - `--map-grab-h` es la franja del panel que queda SIEMPRE a la vista, y
         no es decoración: **el canvas se come el gesto del dedo**, así que sin
         una franja fuera del mapa no hay de dónde agarrar la página para
         desplazarla. Llegar justo al borde de abajo deja la pantalla atrapada.

       El canvas mide más que antes aunque la franja crezca: los píxeles salen
       del chrome —medio padding por arriba y renglones de 26px—, no del mapa. */
    .map-screen {
        --map-bar-h: 5rem;
        --map-grab-h: 1.75rem;
    }
    .map-screen-canvas {
        height: 70vh;   /* fallback: navegadores sin unidades dinámicas */
        height: calc(100svh - var(--topbar-h) - var(--map-bar-h) - var(--map-grab-h));
        /* El suelo va en px, no en rem: el canvas dimensiona por CONTENEDOR y
           por viewport, nunca por fuente (misma regla que su declaración de
           escritorio). */
        min-height: 320px;
    }

    /* Los filtros del panel van `sticky` porque la lista de riders es larga y
       el panel scrollea POR DENTRO. Apilado, el que scrollea es el documento y
       su altura pasa a `auto`: el sticky se resuelve entonces contra el
       viewport, y la barra de filtros se despegaba para quedar flotando encima
       del mapa y de las paradas. En una sola columna no hay contexto que
       perder al bajar — la lista viene justo debajo. */
    .map-filters, .map-routeview { position: static; }

    /* El panel de abajo, con la misma densidad que el chrome de arriba. */
    .map-legend-panel { padding: var(--space-2); }
    .map-legend-title { font-size: 0.72rem; margin-bottom: 0.25rem; }
    .map-chip { font-size: 0.68rem; padding: 2px 0.5rem; }
    .map-rider-head { padding: 0.375rem 0.5rem; }
    .map-rider-name { font-size: 0.82rem; }
    .map-stop-row { font-size: 0.72rem; }

    /* La tarjeta de la parada y la barra de "crear ruta" están posicionadas
       contra `.map-screen-body`, que apilado mide el mapa MÁS el panel: la
       tarjeta aparecía al final de todo, a varias pantallas del pin que se
       acababa de tocar. Fijas al viewport quedan donde se las mira.
       Su tope de alto baja a 45svh: una tarjeta que ocupa el 62% de la pantalla
       tapa justo las paradas vecinas, que es lo que se está mirando cuando se
       la abre. */
    /* Y se apoya ENCIMA del botón del chat (`#chat-fab`: fijo, 54px a 20px del
       borde y con z-index por encima): a todo el ancho, la esquina de abajo a
       la derecha de la tarjeta —donde viven sus botones— quedaba tapada. Se
       sube lo que mide el botón más su margen; el tope de alto no cambia. */
    .map-stopcard {
        position: fixed;
        left: var(--space-2);
        right: var(--space-2);
        bottom: calc(20px + 54px + var(--space-2) + env(safe-area-inset-bottom, 0px));
        width: auto;
        max-height: 45svh;
    }
    .map-select-bar {
        left: var(--space-2);
        right: var(--space-2);
        bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
        transform: none;
        max-width: none;
        gap: var(--space-2);
        padding: 0.5rem 0.625rem;
    }
    .map-select-bar-list { max-height: 22svh; }
    .map-select-bar-actions { margin-left: 0; width: 100%; }
    .map-select-bar-actions .btn { flex: 1; min-height: 40px; }
    /* La ventana "Previsto" flota SOBRE el mapa, y el mapa acá mide media
       pantalla: sin este tope se estiraba hasta taparlo entero. */
    .map-planwin { max-height: 46svh; }
    /* El buscador del mapa: ④ manda — se ESCRIBE (16px, o iOS hace zoom al
       enfocarlo) y se TOCA (44px). Va pegado a la izquierda y deja libres los
       +/− de zoom a la derecha, en vez de centrarse en un canvas de 360px. */
    .map-search {
        top: var(--space-2); left: var(--space-2); right: var(--map-zoom-clear);
        width: auto; transform: none;
    }
    .map-search-input { height: 44px; font-size: 16px; }
    .map-search-hold { width: 44px; height: 44px; }
    .map-search-list { max-height: 40svh; }
    .map-search-pin-btn { width: 44px; height: 44px; }

    /* Los modales del mapa (traspaso, crear ruta, optimizar, fecha de la ruta)
       no son de Bootstrap: son `.mess-handoff-modal` a mano. El ancho ya se
       adapta (100% con tope de 380px), pero sus controles van todos en `-sm`, o
       sea por debajo de los 16px que evitan el zoom automático de iOS — y son
       formularios que el dispatcher completa DE PIE. */
    .mess-handoff-modal .form-control,
    .mess-handoff-modal .form-select { font-size: 16px; min-height: 44px; }
    /* La EXCEPCIÓN anotada, y es la misma que ya vale en la lista de plantillas
       (§21): el editor de una plantilla —`Direcciones, horas y productos`— no es
       el formulario del modal, es una LISTA de ajustes. Cada entrega trae dos
       horas, una cantidad por producto y un buscador; a 16px/44px un molde de
       tres entregas no entraba en la pantalla y el desplegable se leía como una
       columna de cajas todas iguales. Va a la escala de `.templates-page`
       —0.62rem/30px—, que es donde el dueño lo vio bien.

       El resto del modal NO se toca: el día, el rider, el nombre de la ruta y la
       ruta destino se completan DE PIE y se quedan en 16px/44px. El zoom de iOS
       acá está aceptado a sabiendas y el revert es borrar este bloque. */
    .mess-handoff-modal .plangen-stops .form-control,
    .mess-handoff-modal .plangen-stops .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 4px 7px;
    }
    /* Con la letra chica los `time` no necesitan sus 98px, y esos píxeles son
       justo los que hacían envolver la fila de una parada. */
    .mess-handoff-modal .plangen-win input[type="time"] { width: 78px; }
    .mess-handoff-modal .plangen-item-qty { width: 3.75rem; }

    /* La SEGUNDA excepción anotada, y por el mismo motivo que la del editor de
       plantillas: ni «Traspasar entrega» ni «Repartir la recogida» son un
       formulario corto. El traspaso apila la lista del lote, dos desplegables,
       el buscador de direcciones con su botón de mapa, el CP y el motivo; el
       reparto, una fila por rider con su ruta y el pozo de entregas sin
       asignar. A 16px/44px ninguno de los dos entra en la pantalla de un
       teléfono, y lo que se pierde por abajo son justo los botones que
       confirman — el dispatcher completaba el modal a ciegas, desplazándolo.

       Bajan a la MISMA escala de la excepción de al lado (0.62rem/30px), que es
       la de `.templates-page`, y el modal se ciñe al alto de la pantalla en vez
       de estirarse. El resto de los modales del mapa —crear ruta, optimizar,
       fecha, nombre, asignar rider— se quedan de dedo: son dos campos y un
       botón. El zoom de iOS acá está aceptado a sabiendas y el revert es borrar
       este bloque. */
    #handoff-modal .form-control,
    #handoff-modal .form-select,
    #copickup-modal .form-control,
    #copickup-modal .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 4px 7px;
    }
    #handoff-modal .form-label,
    #copickup-modal .form-label { font-size: 0.66rem; }
    #handoff-modal .mess-handoff-body,
    #copickup-modal .mess-handoff-body { padding: var(--space-2); }
    /* Con los campos chicos el modal ya no llega al borde de abajo, pero las
       dos listas que crecen solas (el lote y el reparto) sí pueden: el tope se
       mide contra la pantalla VISIBLE (`svh`), no contra `vh`, que en el móvil
       es el viewport expandido. */
    #handoff-modal .mess-handoff-modal,
    #copickup-modal .mess-handoff-modal { max-height: 88svh; overflow-y: auto; }
    /* Los botones NO encogen: son lo que se TOCA, y la barra de acciones ya
       envuelve a ancho completo unas líneas más arriba. */
    #handoff-modal .mess-handoff-actions .btn,
    #copickup-modal .mess-handoff-actions .btn { font-size: 0.8rem; }

    /* La TERCERA excepción anotada, y del mismo tamaño que las dos de arriba:
       las TRES pantallas de una parada —«Agregar una entrega», «Agregar una
       recogida» y «Editar parada»—, que preguntan lo mismo y por eso van
       juntas. Apilan el buscador
       de direcciones con su lista de candidatos, el detalle de unidad, la
       ciudad y el CP, la franja con el peso, el contacto y la lista de
       productos con su buscador y su alta rápida. A 16px/44px eso no entra en
       una pantalla y lo que se va por debajo del borde son justo los botones
       que confirman — el mismo fallo que tenían el traspaso y el reparto.

       El resto de los modales del mapa —crear ruta, optimizar, fecha, nombre,
       asignar rider— se quedan de dedo: son dos campos y un botón. El zoom de
       iOS acá está aceptado a sabiendas y el revert es borrar este bloque. */
    #pickup-add-modal .form-control,
    #pickup-add-modal .form-select,
    #stop-add-modal .form-control,
    #stop-add-modal .form-select,
    #stop-edit-modal .form-control,
    #stop-edit-modal .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 4px 7px;
    }
    #pickup-add-modal .form-label,
    #stop-add-modal .form-label,
    #stop-edit-modal .form-label { font-size: 0.66rem; }
    #pickup-add-modal .mess-handoff-body,
    #stop-add-modal .mess-handoff-body,
    #stop-edit-modal .mess-handoff-body { padding: var(--space-2); }
    #pickup-add-modal .mess-handoff-modal,
    #stop-add-modal .mess-handoff-modal,
    #stop-edit-modal .mess-handoff-modal { max-height: 88svh; overflow-y: auto; }
    /* Los botones NO encogen: son lo que se TOCA. */
    #pickup-add-modal .mess-handoff-actions .btn,
    #stop-add-modal .mess-handoff-actions .btn,
    #stop-edit-modal .mess-handoff-actions .btn { font-size: 0.8rem; }
    .mess-handoff-actions { flex-wrap: wrap; }
    .mess-handoff-actions .btn { flex: 1 1 auto; min-height: 44px; }
}

/* ── 9. Formularios de filtro sueltos (jornadas, sesiones, rangos) ─────────
   Las pantallas de jornadas no usan `.disp-filters`: llevan un `<form>` con dos
   fechas y un botón, con anchos escritos en línea (`style="width:140px"`). En el
   teléfono eso son tres controles peleando por 336px. `.disp-inline-filters` los
   apila a ancho completo y neutraliza el ancho en línea. */
.disp-inline-filters { display: flex; gap: var(--space-2); align-items: flex-end; flex-wrap: wrap; }
@media (max-width: 767.98px) {
    .disp-inline-filters { width: 100%; }
    .disp-inline-filters > * { flex: 1 1 auto; }
    .disp-inline-filters .form-control,
    .disp-inline-filters .form-select {
        width: 100% !important;         /* gana al style="width:140px" en línea */
        font-size: 16px;
        min-height: 44px;
    }
    .disp-inline-filters .btn { min-height: 44px; }
}

/* ── 10. La cabecera `row / col / col-auto` de TODO el panel ───────────────
   Dieciocho pantallas del admin (riders, empresas, rutas, precios, plantillas,
   historial…) empiezan igual: un `.row` con el título en un `.col` y los
   botones en un `.col-auto`. Ninguna de las dos clases tiene breakpoint —no son
   `.col-md-*`—, así que en el teléfono se quedan en la MISMA línea: `.col`
   puede encogerse hasta cero, y el título terminaba partido letra a letra
   contra un bloque de botones que igual se salía por la derecha.
   Se arregla acá y no plantilla por plantilla porque el patrón es el mismo en
   las dieciocho: darle a cada `.col-auto` la línea entera. En una fila de
   filtros (riders, historial de rutas, pedidos de un rider) el efecto es el
   mismo que se querría de todos modos — un campo por línea, a ancho completo. */
@media (max-width: 767.98px) {
    body.role-admin .app-content .row > .col-auto { flex: 1 0 100%; }
    body.role-admin .app-content .row > .col-auto > .form-control,
    body.role-admin .app-content .row > .col-auto > .form-select {
        width: 100%;
        font-size: 16px;      /* por debajo, iOS hace zoom al enfocar */
        min-height: 44px;
    }
    /* Un `.col-auto` de botones: a lo ancho y con altura de dedo. `.btn-link`
       queda fuera (es un enlace en línea, no un botón). */
    body.role-admin .app-content .row > .col-auto > .btn:not(.btn-link) {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        gap: 5px;
    }
}

/* ── 11. Un solo ancho: los BLOQUES sangran hasta donde llega la barra ─────
   La fila de acciones (§1) y las de pills (§2) sangran hasta el borde de la
   pantalla, y todo lo de abajo —la tabla, el calendario, los filtros— quedaba
   metido `--content-pad` hacia adentro: la línea de "Nuevo Pedido" se leía como
   si perteneciera a otra pantalla. La respuesta NO es recortar esa fila (el
   ancho que gana es justo el que le falta a una tabla de ocho columnas o a un
   mes de siete): es que el resto llegue HASTA AHÍ.
   Sangra el BLOQUE, no su contenido: el marco se va al borde y el texto sigue
   separado por el padding interno que la tarjeta ya tenía (10-16px), así que
   nada queda pegado al cristal. Un marco cortado contra el borde no puede
   conservar ni el borde lateral ni el radio: sin quitarlos se lee como una
   tarjeta a la que le falta un trozo.
   El TÍTULO y el texto suelto se quedan donde están: nunca fueron el problema,
   y son lo único que no puede tocar el borde.
   Qué sangra: `.disp-card` y `.disp-filterbox` (son SIEMPRE bloques de ancho
   completo, así que no hace falta marcarlos) y lo que se marque a mano con
   `.disp-bleed` — que es lo que hay que ponerle a una `.card` o a un
   `.table-responsive` de ancho completo, porque `.card` también se usa dentro
   de una columna de KPIs y ahí sangrar sería romperla.
   El MAPA es el caso más claro de todos —el canvas y su panel son el contenido
   de esa pantalla, no un bloque dentro de ella—: sangrando, el mapa gana los
   32px de los dos bordes, que es ancho de terreno para separar pines. */
@media (max-width: 767.98px) {
    body.role-admin .app-content .disp-card,
    body.role-admin .app-content .disp-filterbox,
    body.role-admin .app-content .disp-bleed,
    body.role-admin .app-content .route-group,
    body.role-admin .app-content .cp-card,
    body.role-admin .app-content .ops-bar,
    body.role-admin .app-content .map-screen-canvas,
    body.role-admin .app-content .map-legend-panel {
        margin-left: calc(var(--content-pad) * -1);
        margin-right: calc(var(--content-pad) * -1);
        border-left-width: 0;
        border-right-width: 0;
        border-radius: 0;
    }
    /* La franja de color del rider ES la identidad de la ruta en la lista del
       día: se queda aunque el marco se vaya al borde. */
    body.role-admin .app-content .route-group { border-left-width: 4px; }
    /* `.disp-filterbox` no tiene marco propio: lo tienen su resumen y el
       formulario abierto, y son ellos los que hay que abrir. */
    body.role-admin .app-content .disp-filterbox > summary,
    body.role-admin .app-content .disp-filterbox[open] > .disp-filters {
        border-left-width: 0;
        border-right-width: 0;
        border-radius: 0;
    }
}

/* ── 12. La TORRE DE CONTROL (dashboard) ───────────────────────────────────
   Es la primera pantalla del dispatcher y la que peor entraba. En escritorio
   son TRES columnas del mismo alto; en el teléfono el ancho no da para
   columnas de trabajo, así que el tablero se rearma como una LECTURA DE ARRIBA
   ABAJO, en el orden en que se pregunta:

     ① la barra de estado — cuatro cifras en UNA línea (es un vistazo: en dos
        renglones deja de serlo, y en una columna hay que desplazarse),
     ② los tres KPIs de negocio — cómo se está trabajando, también en una línea,
     ③ carga de trabajo · estados — qué viene y en qué estado está,
     ④ flota · empresas — quién lo está haciendo,
     ⑤ la cola — lo que reclama una decisión ahora, a lo ancho porque cada fila
        es una dirección que hay que poder LEER.

   ③ y ④ van de a DOS por renglón: son pares que se comparan entre sí, y de a
   uno el dedo tenía que recorrer cinco pantallas para llegar a la cola.

   Cómo se consigue el orden sin tocar el marcado: las tres `.dash-col` (y la
   `.dash-pair` de adentro) pasan a `display: contents`, así sus tarjetas se
   vuelven hijas directas de `.dash-board` y se colocan con `order`. Las
   columnas siguen existiendo en el HTML —el escritorio no se entera— y acá no
   estorban.

   El criterio de la tanda no cambia: nada se esconde, se reordena. Lo único
   que se retira son los dos LIENZOS, y solo en el teléfono angosto
   (≤575.98px), donde miden menos de 170px: ahí un gráfico no es un gráfico, y
   las cifras que ya viven al lado dicen lo mismo sin fingir precisión. */
@media (max-width: 767.98px) {
    /* El encabezado se apila y su acción principal ocupa la línea entera. Lo
       comparte el detalle de pedido, que usa la misma cabecera. */
    .dash-header {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
        margin-bottom: var(--space-3);
    }
    .dash-header .dash-title { font-size: 1.1rem; }
    .dash-header > .btn {
        width: 100%;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
    }

    /* ① Las cuatro cifras, en UNA línea. En 360px son celdas de 90px: la cifra
       se queda como el elemento más grande de la pantalla y lo que encoge es
       todo lo demás. El pie es una aclaración, no un dato: se limita a dos
       renglones —lo que no entra se corta— porque el que manda es el rótulo de
       arriba, que dice lo mismo en dos palabras. */
    .ops-bar {
        grid-template-columns: repeat(4, minmax(0, 1fr));
        margin-bottom: var(--space-3);
    }
    .ops-cell { padding: 7px 8px 8px; gap: 1px; }
    .ops-num { font-size: 1.15rem; }
    /* La torre de control encoge un cuarto más que el resto: es la única
       pantalla que mete cinco bloques de información en media pantalla, y la
       barra tiene que leerse a la misma escala que el tablero de abajo. Va
       scopeada porque la `.ops-bar` la comparten los perfiles, donde no hay
       nada que apriete. */
    .dash-page .ops-num { font-size: 0.86rem; }
    .dash-page .ops-label { font-size: 0.41rem; }
    .dash-page .ops-sub { font-size: 0.42rem; }
    /* Las cuatro cifras arrancan a la MISMA altura. El rótulo mide una línea o
       dos según la celda ("Demorados" contra "Incidencias 7d"), y con el alto
       libre la cifra de al lado quedaba un renglón más arriba: cuatro números
       en cuatro alturas distintas no se leen como una barra. Se le reserva el
       alto de DOS líneas —no se recorta nada, solo se reserva—, que es lo que
       las alinea sin que ninguna celda tenga que saber qué escriben las otras. */
    .ops-label {
        font-size: 0.55rem;
        letter-spacing: 0.02em;
        gap: 4px;
        line-height: 1.2;
        min-height: 2.4em;
        align-items: flex-start;
    }
    .ops-sub {
        font-size: 0.56rem;
        line-height: 1.25;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }

    /* ── El tablero ──────────────────────────────────────────────────────
       Dos columnas iguales y el orden explícito. El alto de cada tarjeta es
       FIJO: apiladas, el "lo que queda de pantalla" del escritorio las dejaría
       a todas del alto de la primera. Lo que no entra se desplaza DENTRO de su
       tarjeta, que es lo que esta pantalla promete en cualquier ancho. */
    .dash-page {
        --dash-board-h: 265px;
        --dash-queue-h: 380px;
        min-height: 0;
    }
    .dash-board {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: var(--space-2);
        flex: none;
    }
    /* Las columnas dejan de dibujar caja: sus tarjetas suben a la grilla del
       tablero. Sin esto el `order` no las alcanzaría — `order` ordena HERMANOS
       dentro de un contenedor, y acá cada tarjeta vivía en el suyo. */
    .dash-col,
    .dash-pair { display: contents; }

    .dash-kpis {
        order: 1;
        grid-column: 1 / -1;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: var(--space-2);
    }
    .dash-panel-workload { order: 2; }
    .dash-panel-status { order: 3; }
    .dash-panel-fleet { order: 4; }
    .dash-panel-companies { order: 5; }
    .dash-panel-queue { order: 6; grid-column: 1 / -1; }

    .dash-col > .dash-panel,
    .dash-pair > .dash-panel { height: var(--dash-board-h); }
    /* La cola es la única de ancho completo, y la única que se lee fila por
       fila: le toca más alto. Va por el mismo selector de hijo que la de
       arriba, o esa ganaría por especificidad. */
    .dash-col > .dash-panel-queue { height: var(--dash-queue-h); }

    /* Media pantalla es media pantalla: acá NADA sangra al borde (§11). Ese
       tratamiento es para un bloque de ancho completo, y en una grilla de dos
       columnas el margen negativo mete la tarjeta debajo de su vecina. Por eso
       `.dash-panel` salió de la lista de §11 —solo existe en esta pantalla— y
       por eso la barra de estado, que la comparten los perfiles y ahí sí
       sangra, se cancela acá: si sangrara ella sola, el borde izquierdo de la
       pantalla tendría dos alineaciones distintas. */
    body.role-admin .app-content .dash-page .ops-bar {
        margin-left: 0;
        margin-right: 0;
        border-left-width: 1px;
        border-right-width: 1px;
        border-radius: var(--radius-lg);
    }

    /* ② Los tres KPIs de negocio en una línea, y cada tarjeta en TRES filas:
       rótulo · (cifra + variación) · pie.

       Dos cosas que en una columna de 110px estaban mal. La variación iba en la
       misma línea que el pie, así que "de la jornada" y "productividad" se
       quedaban sin ancho y se cortaban; y la flecha del % es una lectura de la
       CIFRA —dice hacia dónde se mueve—, no del pie: su sitio es al lado del
       número. La reordena el GRID, sin tocar el marcado: `.dash-kpi-foot` pasa a
       `display: contents` y sus dos hijos suben a la grilla de la tarjeta.

       El rótulo reserva dos líneas por lo mismo que la barra de estado: "SLA a
       tiempo" ocupa una y "Entregas por rider-hora" dos, y sin la reserva las
       tres cifras arrancan a alturas distintas. */
    .dash-kpi {
        padding: 7px 8px 8px;
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-items: baseline;
        gap: 0 5px;
    }
    .dash-kpi-label {
        grid-column: 1 / -1;
        letter-spacing: 0.02em;
        line-height: 1.2;
        min-height: 2.4em;
        align-items: flex-start;
    }
    .dash-kpi-foot { display: contents; }
    .dash-kpi-value { grid-column: 1; }
    .dash-kpi-foot > .dash-delta { grid-column: 2; justify-self: start; }
    .dash-kpi-sub { grid-column: 1 / -1; }
    .dash-page .dash-kpi-label { font-size: 0.37rem; }
    .dash-page .dash-kpi-sub { font-size: 0.38rem; }

    /* La cabecera de una tarjeta a media pantalla: el título y los cuatro
       rangos no comparten línea, así que el grupo baja entero y se reparte el
       ancho — cuatro botones de tres caracteres entran de sobra. */
    /* El título de la tarjeta y su UNIDAD ("Carga de trabajo · paradas") tienen
       que entrar enteros en media pantalla: el h2 va `nowrap`, así que lo que
       no entra se corta contra el borde y la unidad —que es justo lo que dice
       qué se está contando— desaparecía a medias. La respuesta es encoger la
       letra, que a este tamaño sigue leyéndose, y no recortar el rótulo. */
    /* TODA la tipografía del tablero va scopeada a `.dash-page`, y eso no es
       cosmético: la sección de DENSIDAD reinfla cada `font-size` de la torre de
       control con selectores `.dash-page X` (0,2,x), que le GANAN por
       especificidad a un `X` suelto por más abajo que esté en la hoja — las
       media queries no suman especificidad. Sin el scope, estas reglas se
       escribían y no se aplicaban ni una. */
    .dash-panel-head { flex-wrap: wrap; padding: 5px 8px; gap: 4px; }
    .dash-page .dash-panel-head h2 { font-size: 0.42rem; gap: 3px; }
    .dash-range { flex: 1 1 100%; }
    .dash-range button { flex: 1; min-height: 34px; }
    .dash-page .dash-range button { font-size: 0.42rem; }

    /* Las cifras de flota y empresas comparten media pantalla: el rótulo
       encoge, la cifra no (es la respuesta). */
    .tc-stat { padding: 4px 6px 5px; }
    .dash-page .tc-stat-label { font-size: 0.35rem; letter-spacing: 0.02em; }
    .dash-row { gap: 7px; padding: 5px 8px; }
    .dl-avatar { width: 24px; height: 24px; }
    .dash-page .dl-avatar-ph { font-size: 0.55rem; }
    .dash-page .dl-name { font-size: 0.53rem; }
    .dash-page .dl-stat { font-size: 0.44rem; }
    .dash-page .dl-stat b { font-size: 0.5rem; }
    .dl-stat i { margin-right: 2px; }

    /* Las dos tarjetas con LIENZO se leen de arriba abajo: las cifras (o la
       leyenda) en un bloque y el gráfico en el otro, nunca al lado.

       Al lado, el lienzo se queda con lo que SOBRA del ancho de media pantalla
       —y media pantalla de un teléfono angosto son unos 165px—: la dona bajaba
       a 60px de diámetro y el gráfico de barras a columnas de menos de un
       píxel, así que en cuanto la pantalla no era ancha el gráfico dejaba de
       verse. Apilado, el lienzo se lleva el ANCHO ENTERO de la tarjeta y lo
       único que cede es alto, que es lo que un gráfico puede ceder. */
    .dash-workload,
    .dash-states { flex-direction: column; align-items: stretch; gap: 5px; }

    /* Las dos cifras de la carga pasan a UNA línea: apiladas se comían la
       mitad del alto de la tarjeta, que ahora es del gráfico. Cada una lleva
       su rótulo al lado del número, no debajo. */
    .wl-nums {
        flex: 0 0 auto;
        flex-direction: row;
        align-items: baseline;
        justify-content: flex-start;
        gap: 12px;
        min-width: 0;
    }
    .wl-num { flex-direction: row; align-items: baseline; gap: 4px; min-width: 0; }
    .wl-num span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .wl-chart { flex: 1; min-height: 0; }

    /* La dona arriba con alto propio y la leyenda debajo con lo que sobra: la
       leyenda crece con los estados que haya (hasta siete) y ya scrollea sola
       (`.dash-panel-scroll`), la dona no. */
    .dash-donut {
        flex: 0 0 45%;
        width: 100%;
        max-width: 100%;
        max-height: none;
        min-height: 0;
    }
    .dash-states .status-legend { flex: 1; width: 100%; min-height: 0; }
    .dash-page .status-legend li { font-size: 0.47rem; }
    .dash-page .wl-num span { font-size: 0.38rem; }
    .dash-page .dash-delta { font-size: 0.42rem; }
    .dash-page .dash-delta i { font-size: 0.42rem; }

    /* La COLA se lee a la misma escala que el resto: es una tarjeta más del
       tablero, y con la tipografía de escritorio pasaba a ser el bloque más
       grande de la pantalla. El `<select>` de asignar rider queda FUERA a
       propósito (más abajo, en 16px): por debajo de eso iOS hace zoom al
       enfocarlo. */
    .dash-page .queue-group-title { font-size: 0.41rem; }
    .dash-page .queue-group-title .count { font-size: 0.42rem; }
    .dash-page .queue-row-id { font-size: 0.47rem; }
    .dash-page .queue-row-age { font-size: 0.43rem; }
    .dash-page .queue-row-sub { font-size: 0.42rem; }
    .dash-page .queue-leg { font-size: 0.44rem; }
    .dash-page .queue-leg-more { font-size: 0.41rem; }
    .dash-page .queue-row .status-badge { font-size: 0.4rem; }
    .dash-page .queue-empty { font-size: 0.5rem; }
    .dash-page .queue-assign button { font-size: 0.52rem; }

    /* La VARIACIÓN sube a la línea del NOMBRE. En una columna de media
       pantalla el % ocupaba su propia columna a la derecha de la fila entera,
       o sea que le quitaba ancho justo al renglón de las cifras: "12/3" de
       pedidos y paradas se cortaban con puntos suspensivos (`.dl-stats` va
       `nowrap` a propósito) y quedaba a la vista el dato que menos se mira.
       Arriba, en cambio, comparte línea con el nombre, que ya se recorta solo.

       Lo reordena el GRID sin tocar el marcado, el mismo recurso que ordena el
       tablero: `.dl-main` pasa a `display: contents` y sus dos hijos suben a la
       grilla de la fila. El hueco del avatar es un `margin`, no un `gap`, para
       que a ≤575.98px —donde el avatar se retira— la columna no deje aire. */
    .dash-panel-fleet .dash-row,
    .dash-panel-companies .dash-row {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        gap: 0;
    }
    .dash-panel-fleet .dl-avatar,
    .dash-panel-companies .dl-avatar {
        grid-column: 1;
        grid-row: 1 / span 2;
        margin-right: 7px;
    }
    .dash-panel-fleet .dl-main,
    .dash-panel-companies .dl-main { display: contents; }
    .dash-panel-fleet .dl-name,
    .dash-panel-companies .dl-name { grid-column: 2; grid-row: 1; }
    .dash-panel-fleet .dash-row > .dash-delta,
    .dash-panel-companies .dash-row > .dash-delta {
        grid-column: 3;
        grid-row: 1;
        justify-self: end;
        padding-left: 5px;
    }
    .dash-panel-fleet .dl-stats,
    .dash-panel-companies .dl-stats { grid-column: 2 / -1; grid-row: 2; }
    /* Las tres cifras grandes comparten `clamp`, y acá el techo baja: a media
       pantalla el ancho lo pone la tarjeta, no la ventana, así que el tramo de
       escritorio dejaba un número que se comía su propia caja. */
    .dash-page .tc-stat-num,
    .dash-page .wl-num b,
    .dash-page .dash-kpi-value { font-size: clamp(0.62rem, 0.38rem + 0.75vw, 0.77rem); }

    /* ⑤ Asignar un rider desde la cola: el select y el botón no entran en la
       misma línea que la dirección y la antigüedad. Pasan a su propio renglón,
       a lo ancho de la fila y con altura de dedo. Los 16px del select no son
       estética: por debajo de eso iOS hace zoom al enfocar y deja la pantalla
       desencuadrada. */
    .queue-row { flex-wrap: wrap; }
    .queue-assign { flex: 1 1 100%; }
    .queue-assign select {
        flex: 1;
        min-width: 0;
        max-width: none;
        min-height: 40px;
        font-size: 16px;
    }
    .queue-assign button { min-height: 40px; padding: 0.3125rem 0.875rem; }
}

/* En el teléfono angosto los dos LIENZOS se QUEDAN, y lo que los salva es que
   ya están apilados debajo de sus cifras (ver arriba): a lo ancho de la tarjeta
   entera una dona de 45% de alto se lee, y el gráfico de barras también.
   Retirarlos era la respuesta cuando compartían la línea con las cifras y se
   quedaban con 60px de ancho; apilados el problema no existe, y un tablero que
   esconde justo el gráfico en la pantalla donde más se mira no es un tablero.
   Lo único que se va es el avatar de la fila: en esa columna, cada píxel que
   ocupa se lo quita al NOMBRE, que es lo que identifica al rider. */
@media (max-width: 575.98px) {
    .dash-workload { padding: 6px 8px 8px; }
    .dash-states { padding: var(--space-2); }
    /* Un poco menos de dona: acá la leyenda es la que responde. */
    .dash-donut { flex-basis: 40%; }

    .dash-panel-fleet .dl-avatar,
    .dash-panel-companies .dl-avatar { display: none; }
    .dash-row { gap: 6px; padding: 5px 8px; }
}

/* ── 13. El DETALLE de un pedido ───────────────────────────────────────────
   La pantalla ya se apilaba sola (`.col-lg-8` / `.col-lg-4`), así que lo que
   quedaba era el stepper y las dos filas de acciones. */
@media (max-width: 767.98px) {
    /* Seis nodos × 56px son 336px: justo el ancho útil de un teléfono de 360,
       o sea que el stepper "entraba" al milímetro y cualquier etiqueta larga lo
       desbordaba. Con 44px sobra aire y las etiquetas parten en dos líneas
       —que es lo que ya hacían— sin empujar nada. */
    .stepper-node { min-width: 44px; }
    .stepper-label { font-size: 0.62rem; }
    /* El desglose de precio y la tabla de información son de dos columnas
       (concepto / importe): sin esto el importe se parte en dos líneas. */
    .price-breakdown td:last-child { padding-left: 8px; }
}
/* ── 14. El CUADRANTE SEMANAL de jornadas ──────────────────────────────────
   Las dos columnas ya se apilan solas a los 1199.98px (el panel deja de ser
   `sticky` ahí mismo: apilado, un panel pegado arriba taparía la grilla). Lo
   que queda es lo de siempre — controles de dedo y una tabla de ocho columnas
   que scrollea con sus dos bordes fijos, que ya vienen de la sección propia.
   La barra de la semana envuelve y sus cifras dejan de estar empujadas al otro
   extremo: en 360px `margin-left: auto` las manda a un renglón propio pegadas
   a la derecha, y se leen mejor alineadas con el resto. */
@media (max-width: 767.98px) {
    .sw-weeknav { gap: var(--space-2); padding: 0.5rem var(--space-3); }
    .sw-weeknav > .btn { min-width: 44px; min-height: 44px; }
    .sw-weeknav-label { flex: 1 1 auto; min-width: 0; text-align: center; }
    .sw-weeknav-stats {
        flex: 1 1 100%;
        margin-left: 0;
        justify-content: space-between;
        border-top: 1px solid var(--border-subtle);
        padding-top: 0.375rem;
        font-size: 0.74rem;
    }
    /* El alto de la grilla: en el móvil el `max-height` de escritorio deja dos
       barras de scroll compitiendo con la de la página. */
    .sw-scroll { max-height: 70vh; }
    .sw-search { flex: 1 1 100%; }
    .sw-search input { min-height: 44px; font-size: 16px; }
    /* Sin hover no hay "aparecer al acercarse": en un teléfono, lo que está en
       opacidad 0 simplemente no existe. */
    .sw-chip-acts { opacity: 1; }
    .sw-pick input { width: 20px; height: 20px; opacity: 1; }
    .sw-chip-pick input { width: 18px; height: 18px; opacity: 1; }
    .sw-cell { padding: 0.3125rem 0.25rem; }
    .sw-pick-btn.is-icon { min-width: 30px; min-height: 30px; justify-content: center; }
    .sw-presets .btn { min-height: 38px; }
    .sw-mode-btn { min-height: 44px; }
}

@media (max-width: 767.98px) {
    /* La papelera de la fila de parada (ELIMINAR) se muestra en escritorio con el
       hover de la fila. En un teléfono no hay hover, y lo que queda en opacidad 0
       simplemente no existe: acá va siempre visible, con blanco táctil.
       El blanco se gana ENSANCHANDO, no con padding: el ancho es lo que alinea
       la columna del extremo derecho, y la ranura vacía tiene que crecer igual
       o la entrega sin ✕ vuelve a descolgarse de la recogida. */
    .map-stop-trash { opacity: 1; }
    .map-stop-del, .map-stop-trash, .map-stop-slot { width: 1.75rem; }
    .map-stop-del, .map-stop-trash { min-height: 28px; }
}

/* Los formularios de la barra lateral (asignar rider, recalcular precio, subir
   una imagen) van todos en `-sm`, o sea por debajo de los 16px con los que iOS
   hace zoom al enfocar. No se cubren con una regla nueva: heredan `.form-touch`
   del alta poniéndose la clase, que es exactamente lo que esa sección dejó
   dicho. Por eso la marca está en el `<form>` y acá no hay nada. */

/* ── 15. La COLA y los LISTADOS de pedidos ─────────────────────────────────
   Paso 4 del modelo (`docs/archivo/PLAN_movil_dispatcher.md`): la pantalla donde el
   dispatcher pasa el día. Las filas ya eran TARJETAS (§4) y los filtros ya se
   plegaban (§3); lo que faltaba era la ESCALA —se dibujaba con la tipografía de
   escritorio, que en el corte móvil es más grande que en el monitor porque la
   `--ui-scale` vuelve a 1— y el ORDEN de lo que dice cada tarjeta.

   Todo va scopeado a `.orders-page`, que marcan las TRES vistas del panel
   (`/admin/orders`, la de una empresa y la de un rider). `.disp-*` es la familia
   más compartida de la hoja —el día del calendario, el detalle de la ruta y el
   PORTAL emiten el mismo marcado—, así que sin el scope este paso le cambiaría
   la letra a media aplicación. El portal queda fuera a propósito: es otro rol,
   otra densidad y otro paso. */
@media (max-width: 767.98px) {
    /* ── Lo que dice una tarjeta, y en qué orden ────────────────────────────
       La TARJETA se lee por el TRAYECTO, no por el número: "#128" no distingue
       un pedido de otro cuando se está buscando cuál es el de Balmes, y hasta
       ahora el `#id` era el título (§4 lo pone así para cualquier tabla) y el
       trayecto una línea etiquetada más abajo.

       El trayecto sube a la primera línea y el `#id` se le pone AL LADO, a la
       derecha — el mismo gesto que subió el % a la línea del nombre en la torre
       de control, y por el mismo motivo: lo secundario acompaña al título en vez
       de comerle una línea entera. Lo reordena el GRID sin tocar el marcado; las
       demás celdas siguen siendo renglones de ancho completo. */
    .orders-page .disp-table.is-cards tbody tr.disp-row {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        column-gap: 0.5rem;
    }
    .orders-page .disp-table.is-cards tr.disp-row > td { grid-column: 1 / -1; }
    .orders-page .disp-table.is-cards tr.disp-row > td[data-label="Trayecto"] {
        grid-column: 1;
        grid-row: 1;
        padding-top: 0;
    }
    .orders-page .disp-table.is-cards tr.disp-row > td:first-child {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
        margin-bottom: 0;
        padding-top: 0;
    }
    /* El `#id` deja de ser el título: baja al tamaño de un dato secundario y se
       queda en gris. Sigue siendo lo que se dicta por teléfono, así que no se
       esconde. */
    .orders-page .disp-table.is-cards tr.disp-row > td:first-child { font-size: 0.5rem; }
    .orders-page .disp-id { color: var(--gray-500); }
    /* Y el trayecto, que ahora ES el título, se lee como tal. */
    .orders-page .disp-table.is-cards td[data-label="Trayecto"]::before { content: none; }
    .orders-page .disp-route { font-size: 0.58rem; line-height: 1.3; }

    /* ── La escala (② del modelo) ───────────────────────────────────────── */
    .orders-page .disp-table td { font-size: 0.56rem; }
    .orders-page .disp-table thead th { font-size: 0.46rem; }
    .orders-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .orders-page .disp-table.is-cards tbody tr { padding: 0.5rem 0.625rem; }
    .orders-page .disp-sub { font-size: 0.5rem; }
    .orders-page .status-badge { font-size: 0.52rem; padding: 0.2rem 0.45rem; }
    .orders-page .disp-timer { font-size: 0.5rem; }
    .orders-page .price-tag,
    .orders-page .price-pending { font-size: 0.56rem; }
    /* La cabecera de grupo (prioridad operativa o día) es lo que ORDENA la
       lista: encoge, pero se sigue leyendo como una franja. */
    .orders-page .disp-table.is-cards tr.disp-group-row td { font-size: 0.52rem; }
    .orders-page .disp-group-link { font-size: 0.46rem; }

    /* Los pills y los filtros ya tienen su tratamiento (§2 y §3); acá bajan de
       tamaño con el resto. */
    .orders-page .nav-pills .nav-link { font-size: 0.52rem; padding: 6px 10px; }
    .orders-page .disp-filter-count { font-size: 0.5rem; }
    .orders-page .disp-filters .form-label { font-size: 0.5rem; margin-bottom: 2px; }
    .orders-page .empty-state p { font-size: 0.62rem; }
    .orders-page .pagination .page-link { font-size: 0.56rem; }

    /* ── El desplegable de FILTROS ──────────────────────────────────────────
       Diez campos a 16px y 44px de alto son casi una pantalla entera de
       formulario antes de la primera fila: abierto, el bloque que sirve para
       ACOTAR la lista tapaba la lista. Encoge como todo lo demás. */
    .orders-page .disp-filterbox > summary {
        font-size: 0.6rem;
        min-height: 34px;
        padding: 0.4375rem 0.625rem;
    }
    .orders-page .disp-filterbox[open] > .disp-filters {
        padding: var(--space-2);
        gap: 0.375rem;
    }
    .orders-page .disp-filters .form-control,
    .orders-page .disp-filters .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 0.25rem 0.4375rem;
    }
    .orders-page .disp-filterbox > .disp-filters > .d-flex > .btn {
        min-height: 32px;
        font-size: 0.56rem;
        padding: 0.25rem 0.5rem;
    }

    /* ── Asignar un rider y escribir un precio DESDE la fila ────────────────
       Los dos gestos que se hacen sin abrir la ficha. Estaban blindados en
       16px y 38px de alto —el umbral por debajo del cual iOS hace ZOOM al
       enfocar un campo—, y con la tarjeta a 0,56rem eso los convertía en lo
       más grande de la pantalla: un pedido se leía como un formulario con una
       dirección arriba.

       **El dueño lo probó y decidió la escala**: los dos controles se leen como
       el resto de la tarjeta. La contrapartida es la que era —al tocarlos, iOS
       acerca la pantalla y no la devuelve solo—, y está ACEPTADA a sabiendas;
       si molesta, se revierte subiendo estas dos reglas a 16px y no hay nada
       más que tocar. La regla ④ del modelo sigue en pie para todo lo demás:
       esto es una EXCEPCIÓN anotada, no un cambio de criterio. */
    .orders-page .disp-table.is-cards .disp-assign select,
    .orders-page .disp-table.is-cards .disp-assign button,
    .orders-page .disp-table.is-cards .disp-price input,
    .orders-page .disp-table.is-cards .disp-price button {
        font-size: 0.56rem;
        min-height: 26px;
        padding: 0.1875rem 0.375rem;
    }
    .orders-page .disp-table.is-cards .disp-price-cur { font-size: 0.56rem; }
}

/* ── 16. El DETALLE de un pedido ───────────────────────────────────────────
   Paso 5 del modelo (`docs/archivo/PLAN_movil_dispatcher.md`). La pantalla ya se
   apilaba sola (`.col-lg-8` / `.col-lg-4`) y §13 dejó calibrado lo suyo —el
   stepper de estado y la tabla de dos columnas del desglose—: **eso no se
   toca**. Lo que faltaba era la ESCALA de todo lo demás, que se dibujaba con
   la tipografía de escritorio (y en el corte móvil la `--ui-scale` vuelve a 1,
   así que la ficha salía MÁS grande en el teléfono que en el monitor: la
   dirección de la recogida a 0,95rem, la cabecera de cada tarjeta a 0,9rem y
   una barra lateral de formularios de 44px por control).

   Todo va scopeado a `.order-detail-page`, que marca la plantilla. `.cp-card`,
   `.stop-*`, `.status-badge`, `.cp-timeline-*` y `.price-*` son de las familias
   más compartidas de la hoja —el portal, las plantillas, el detalle de la ruta
   y los perfiles emiten el mismo marcado—, así que sin el scope este paso le
   cambiaría la letra a media aplicación antes de que le toque su paso. */
@media (max-width: 767.98px) {
    /* ── La cabecera: qué pedido es y cuándo se hace ───────────────────────
       El `<h1>` y el chip del día siguen siendo lo primero que se lee, pero a
       tamaño de teléfono: con el título a 1,1rem y el chip a 0,82rem la
       cabecera se comía media pantalla antes del stepper. */
    .order-detail-page .dash-header .dash-title { font-size: 0.86rem; }
    .order-detail-page .back-crumb { font-size: 0.5rem; }
    .order-detail-page .order-day-chip { font-size: 0.5rem; padding: 3px 9px; gap: 4px; }
    .order-detail-page .status-badge { font-size: 0.5rem; padding: 0.18rem 0.4rem; }
    /* Las dos alertas de estado (cancelado, tiempo en sitio) son AVISOS: se
       leen, no se operan. */
    .order-detail-page .alert { font-size: 0.54rem; }
    .order-detail-page .alert .fs-5 { font-size: 0.8rem; }

    /* ── Las tarjetas ──────────────────────────────────────────────────────
       Ocho `.cp-card` apiladas: cada píxel de padding se paga ocho veces. */
    .order-detail-page .cp-card { margin-bottom: var(--space-3); }
    .order-detail-page .cp-card-header { font-size: 0.56rem; padding: 0.5rem 0.625rem; gap: 0.3125rem; }
    .order-detail-page .cp-card-body { padding: 0.625rem; }
    .order-detail-page .cp-hr { margin: 0.625rem 0; }

    /* ── Recogida y entregas ───────────────────────────────────────────────
       El bloque de una parada se lee igual que su fila en el panel del mapa:
       la DIRECCIÓN manda y el resto la acompaña. El rótulo (`.stop-title`) es
       lo que parte la ficha en dos mitades, así que encoge pero se queda como
       título —con su línea de color—, no baja a etiqueta menuda. */
    .order-detail-page .stop-title { font-size: 0.62rem; margin-bottom: 0.4375rem; padding-bottom: 0.25rem; gap: 0.3125rem; }
    .order-detail-page .stop-title-count { font-size: 0.46rem; padding: 1px 0.375rem; }
    .order-detail-page .stop-block { padding-top: 0.625rem; padding-bottom: 0.625rem; }
    .order-detail-page .stop-head { gap: 0.3125rem; }
    .order-detail-page .stop-head .stop-addr { font-size: 0.58rem; }
    .order-detail-page .stop-num { width: 16px; height: 16px; font-size: 0.44rem; }
    .order-detail-page .stop-meta { font-size: 0.48rem; gap: 0.25rem 0.5625rem; margin-top: 0.25rem; }
    .order-detail-page .peso-chip { font-size: 0.48rem; padding: 1px 6px; gap: 3px; }
    .order-detail-page .win-chip { font-size: 0.46rem; padding: 1px 6px; }
    .order-detail-page .stop-items { margin-top: 0.3125rem; }
    .order-detail-page .stop-items li { font-size: 0.52rem; padding: 2px 0; gap: 0.375rem; }
    /* Lo que el rider dejó escrito (nota, motivo de una fallida), los traspasos
       y las dos líneas sueltas del pie (descripción, instrucciones). */
    .order-detail-page .fs-sm { font-size: 0.5rem; }
    .order-detail-page .disp-sub { font-size: 0.48rem; }
    .order-detail-page .cp-card-body > p:not(.stop-title) { font-size: 0.54rem; margin-bottom: 0.25rem; }

    /* ── El desglose de precio ─────────────────────────────────────────────
       §13 ya separó las dos columnas (concepto / importe) para que el importe
       no parta en dos líneas; acá va la escala. El TOTAL se queda un punto por
       encima: es la cifra que se busca. */
    .order-detail-page .price-breakdown td { font-size: 0.54rem; padding: 3px 0; }
    .order-detail-page .price-breakdown tr.total td { font-size: 0.62rem; padding-top: 6px; }
    .order-detail-page .price-tag { font-size: 0.62rem; }
    .order-detail-page .price-pending { font-size: 0.54rem; }

    /* ── El historial ──────────────────────────────────────────────────────
       Es la bitácora: se consulta, no se opera. Encoge entera y la hora se
       queda en su columna (el `white-space: nowrap` ya viene de arriba). */
    .order-detail-page .cp-timeline-row { padding: 0.4375rem 0.625rem; gap: 0.5rem; }
    .order-detail-page .cp-timeline-title { font-size: 0.54rem; }
    .order-detail-page .cp-timeline-note { font-size: 0.46rem; }
    .order-detail-page .cp-timeline-usernote { font-size: 0.46rem; padding: 0.1875rem 0.375rem; }
    .order-detail-page .cp-timeline-time { font-size: 0.44rem; }

    /* ── La barra lateral: Información e imágenes ──────────────────────────
       La tabla de Información es de dos columnas (rótulo / dato) y no scrollea:
       encoge como el resto. */
    .order-detail-page .cp-card-body .table td,
    .order-detail-page .cp-card-body .table th { font-size: 0.54rem; padding: 0.1875rem 0.375rem 0.1875rem 0; }
    .order-detail-page .cp-card-body .table th { font-size: 0.48rem; }
    .order-detail-page small.text-muted { font-size: 0.46rem; }

    /* ── ④ revisado: los CONTROLES de esta pantalla ────────────────────────
       Acá está la EXCEPCIÓN anotada del paso, y es la misma que decidió el
       paso 4 para la cola: **un control METIDO EN UNA FICHA que se LEE no es
       un formulario**. La barra lateral son cuatro formularios cortos
       (asignar rider, devolver a pendiente, recalcular precio, subir una foto)
       que heredaban `.form-touch` —16px de letra y 44px de alto por control—,
       y con la ficha a 0,5–0,58rem eso los convertía en lo más grande de la
       pantalla: el pedido se leía como un formulario con una dirección arriba,
       exactamente el síntoma que el paso 4 ya había corregido en la lista.

       La contrapartida es la que era —al enfocarlos, iOS acerca la pantalla y
       no la devuelve solo—, y está ACEPTADA a sabiendas. Revertirlo es subir
       estas reglas a `font-size: 16px` y `min-height: 44px`; no hay nada más
       que tocar, y `.form-touch` sigue en el `<form>` para que el día que se
       revierta baste con borrar este bloque. La regla ④ del modelo sigue en
       pie para los FORMULARIOS de verdad (el alta, la edición): esto es una
       excepción anotada, no un cambio de criterio. */
    .order-detail-page .form-touch .form-control,
    .order-detail-page .form-touch .form-select,
    .order-detail-page .form-touch .form-control-sm,
    .order-detail-page .form-touch .form-select-sm,
    .order-detail-page .cp-card-body .form-control,
    .order-detail-page .cp-card-body .form-select {
        font-size: 0.6rem;
        min-height: 30px;
        padding: 0.25rem 0.4375rem;
    }
    .order-detail-page .form-label,
    .order-detail-page .form-check-label { font-size: 0.5rem; }
    .order-detail-page .form-check-input { width: 0.9rem; height: 0.9rem; margin-top: 0.16rem; }
    /* Los botones de las tarjetas: los de la barra lateral van a ancho
       completo, así que 34px de alto se tocan sin apuntar. Los de una fila
       ("Marcar como fallida", "Deshacer traspaso") acompañan a su texto. */
    .order-detail-page .form-touch .btn:not(.btn-link),
    .order-detail-page .cp-card-body .btn {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 0.3125rem 0.5625rem;
    }
    /* La ✕ de una imagen es un icono sobre la foto: no se estira. */
    .order-detail-page .cp-card-body .btn.py-0 { min-height: 22px; padding: 0 0.25rem; }

    /* Lo que NO encoge: la barra de acciones de arriba (Editar / Eliminar) es
       lo que se TOCA en esta pantalla, y §1 la deja en 44px de alto con los dos
       botones repartiéndose el ancho. Solo baja la LETRA. */
    .order-detail-page .disp-actions > .btn,
    .order-detail-page .disp-actions > form > .btn { font-size: 0.62rem; }
}

/* ── 17. Las RUTAS (lista · detalle · historial) ───────────────────────────
   Paso 6 del modelo (`docs/archivo/PLAN_movil_dispatcher.md`). Tres pantallas que se
   miran de pie y en el mismo momento del día: qué está en la calle, qué lleva
   una ruta y qué pasó ayer.

   Dos marcas, no tres: `.routes-page` la comparten la lista de rutas activas y
   el HISTORIAL —es la misma lista leída por sus campos congelados— y
   `.route-detail-page` es la ficha. Van scopeadas por la razón de siempre:
   `.card`, `.badge`, `.disp-table` y `.disp-inline-filters` los emite medio
   panel, y varias de esas pantallas todavía no tienen su paso.

   **Lo que este bloque NO toca, y es deliberado: la lista de paradas del
   detalle.** Emite el MISMO marcado que `stopRow` de `map-dispatcher.js` (hay
   tests que lo fijan) y §8 ya la calibró para el teléfono —`.map-stop-row` a
   0,72rem, el quitar y la papelera a 28px—. Ponerle una escala propia acá
   separaría las dos superficies que el proyecto mantiene idénticas a
   propósito: la misma parada se leería distinto según por qué pantalla se
   entró. Si algún día hay que encogerla, se encoge en §8 y se mueven las dos a
   la vez.

   El asa de arrastre tampoco se toca: la fila ENTERA es el asa y SortableJS ya
   tiene sus parámetros (`delay: 150` + `delayOnTouchOnly`). */
@media (max-width: 767.98px) {
    /* ── La LISTA de rutas activas ─────────────────────────────────────────
       Un grupo por rider, con su franja de color, y dentro una fila por ruta.
       La franja y el número son la identidad de la ruta en el mapa y en el
       chat: encogen, pero no se van (③). */
    .routes-page .route-group-head { padding: 7px 10px; gap: 6px; }
    .routes-page .route-group-dot { width: 9px; height: 9px; }
    .routes-page .route-group-name { font-size: 0.62rem; }
    .routes-page .route-group-count { font-size: 0.44rem; padding: 1px 7px; }
    .routes-page .route-item-num {
        width: 24px; height: 24px; font-size: 0.56rem; border-radius: 6px;
    }
    .routes-page .route-item-name { font-size: 0.58rem; }
    .routes-page .route-item-meta { font-size: 0.44rem; gap: 4px; }
    .routes-page .route-item-chevron { font-size: 0.66rem; }
    .routes-page .route-item .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    /* ④: la fila ENTERA es el enlace, y es lo único que se toca en esta
       pantalla. Encoge la letra, no el objetivo. */
    .routes-page .route-item { padding: 7px 10px; gap: 8px; min-height: 44px; }
    .routes-page .empty-state p { font-size: 0.62rem; }

    /* ── El HISTORIAL: la escala de la tarjeta ─────────────────────────────
       Los mismos números que la cola (§15): las dos son listas del panel y
       leerlas distinto no tendría ningún motivo. */
    .routes-page .disp-table td { font-size: 0.56rem; }
    .routes-page .disp-table thead th { font-size: 0.46rem; }
    .routes-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .routes-page .disp-table.is-cards tbody tr { padding: 0.5rem 0.625rem; }
    /* El día de servicio es el TÍTULO de la tarjeta (§4 se lo da a la primera
       celda), y un título no lleva su etiqueta delante. */
    .routes-page .disp-table.is-cards td:first-child { font-size: 0.62rem; }
    .routes-page .disp-table.is-cards td:first-child::before { content: none; }
    .routes-page .disp-table.is-cards .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    .routes-page .disp-table.is-cards small { font-size: 0.46rem; }

    /* ── ④ revisado: los CONTROLES del historial ───────────────────────────
       Acá está la EXCEPCIÓN anotada del paso, y es la MISMA que decidieron el
       paso 4 (la cola) y el paso 5 (la ficha): un control metido en una LISTA
       que se lee no es un formulario. El rango, el rider y el ámbito del
       borrado son tres campos que ACOTAN el listado de abajo; a 16px y 44px
       eran lo más grande de la pantalla y empujaban la primera ruta fuera de
       cuadro, exactamente el síntoma que el paso 4 ya había corregido.

       La contrapartida es la que era —al enfocarlos, iOS acerca la pantalla y
       no la devuelve solo— y está ACEPTADA a sabiendas. Revertirlo es subir
       estas reglas a `font-size: 16px` y `min-height: 44px`, y no hay nada más
       que tocar: `.disp-inline-filters` sigue puesta en el formulario. La
       regla ④ sigue en pie para los FORMULARIOS de verdad (paso 9). */
    .routes-page .disp-inline-filters .form-control,
    .routes-page .disp-inline-filters .form-select,
    .routes-page .routes-range-form .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 0.25rem 0.4375rem;
    }
    .routes-page .disp-inline-filters .form-label { font-size: 0.5rem; margin-bottom: 2px; }
    .routes-page .disp-inline-filters .btn { font-size: 0.56rem; min-height: 32px; padding: 0.25rem 0.5rem; }
    /* El borrado por rango: el texto que dice SOBRE QUÉ opera y su botón. El
       `w-auto` en línea del select deja media columna vacía en una fila que ya
       envuelve. */
    .routes-page .routes-range-form { gap: 6px; }
    .routes-page .routes-range-form > .small { font-size: 0.5rem; }
    .routes-page .routes-range-form .form-select.w-auto { width: 100% !important; }
    .routes-page .routes-range-form .btn { font-size: 0.56rem; min-height: 32px; padding: 4px 8px; }
    /* Las acciones de una fila (ver, mapa, ocultar, reabrir) van al final de la
       tarjeta, que es donde §4 las pone. 34px es el mismo suelo que el paso 5
       le dio a los botones de una tarjeta: por debajo de los 44px de ④ y
       anotado, porque acá son CUATRO en un renglón. */
    .routes-page .disp-table.is-cards td[data-label="Acciones"] .btn {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 0.25rem 0.5rem;
    }
    /* Un menú SÍ se toca de a uno: sus opciones se quedan cómodas. */
    .routes-page .dropdown-item { font-size: 0.6rem; padding: 8px 12px; }

    /* ── El DETALLE de una ruta ────────────────────────────────────────────
       La cabecera, las tarjetas y la tabla de pedidos. La lista de paradas
       queda fuera a propósito (ver el encabezado de la sección). */
    .route-detail-page .dash-title,
    .route-detail-page h1.h3 { font-size: 0.86rem; }
    .route-detail-page .breadcrumb { font-size: 0.5rem; margin-bottom: 2px; }
    .route-detail-page h1 .badge.fs-6 { font-size: 0.5rem !important; padding: 0.18rem 0.4rem; }
    .route-detail-page .card { margin-bottom: var(--space-3); }
    .route-detail-page .card-header { padding: 8px 10px; }
    .route-detail-page .card-header h6 { font-size: 0.56rem; }
    .route-detail-page .card-header .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    .route-detail-page .card-body { padding: 10px; }
    .route-detail-page .card-body > p,
    .route-detail-page .card-body .small,
    .route-detail-page .card-body small { font-size: 0.5rem; }
    .route-detail-page .card-title { font-size: 0.58rem; }
    .route-detail-page .row.g-4 { --bs-gutter-y: var(--space-3); }

    /* El panel de "optimizar orden": dos radios y una explicación. El radio es
       un asset de tamaño fijo, así que acompaña a su etiqueta y no encoge más
       de lo que la etiqueta necesita. */
    .route-detail-page .form-check-label { font-size: 0.54rem; }
    .route-detail-page .form-check-input { width: 0.9rem; height: 0.9rem; margin-top: 0.16rem; }

    /* La tabla de pedidos de la ruta, con la escala del historial. */
    .route-detail-page .disp-table td { font-size: 0.56rem; }
    .route-detail-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .route-detail-page .disp-table.is-cards tbody tr { padding: 0.5rem 0.625rem; }
    .route-detail-page .disp-table.is-cards td:first-child { font-size: 0.62rem; }
    .route-detail-page .disp-table.is-cards td:first-child::before { content: none; }
    .route-detail-page .disp-table .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    .route-detail-page .disp-table.is-cards td[data-label="Acciones"] .btn {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 0.25rem 0.5rem;
    }

    /* ── ④ revisado: los CONTROLES del detalle ─────────────────────────────
       Misma excepción anotada, mismo motivo y mismos números que la barra
       lateral de la ficha de un pedido (§16): nombre de la ruta, día de la
       ruta, rider asignado y el select de "agregar pedido existente" son
       cuatro formularios cortos DENTRO de una ficha que se LEE. Con la ficha a
       0,5–0,58rem, 16px y 44px por control la convertían en un formulario con
       un recorrido arriba.

       Revertirlo es subir estas reglas a `font-size: 16px` y
       `min-height: 44px`. Los botones de una tarjeta van a 34px, el mismo
       suelo del paso 5. */
    .route-detail-page .card-body .form-control,
    .route-detail-page .card-body .form-select {
        font-size: 0.6rem;
        min-height: 30px;
        padding: 4px 7px;
    }
    .route-detail-page .card-body .btn:not(.btn-link) {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 5px 9px;
    }
    /* El quitar de la ruta es un icono dentro de una celda: no se estira a la
       altura de un botón de tarjeta. */
    .route-detail-page .disp-table .btn.py-0 { min-height: 26px; padding: 0 0.375rem; }
    .route-detail-page .card-header .btn { font-size: 0.54rem; min-height: 30px; padding: 4px 8px; }

    /* Lo que NO encoge: "Ver en el mapa" es lo que se TOCA en esta pantalla y
       §10 lo deja en 44px de alto con el ancho entero. Solo baja la LETRA. */
    .route-detail-page .row > .col-auto > .btn { font-size: 0.62rem; }
}

/* ── 18. Los PERFILES y el DIRECTORIO ─────────────────────────────────────
   Paso 7 del modelo (`docs/archivo/PLAN_movil_dispatcher.md`). Cinco pantallas y DOS
   marcas, porque son dos lecturas:

   - `.profile-page` — el perfil del RIDER y el de la EMPRESA. Los dos se leen
     IGUAL (identidad y acciones, las cifras en una `.ops-bar`, y debajo dos
     columnas), así que una sola pasada los cubre a los dos: escribirlos por
     separado sería garantizar que se separen.
   - `.directory-page` — las dos listas que llevan a un perfil (riders y
     empresas) y el catálogo de una empresa. También es una sola lectura: qué
     hay, en qué estado, y el atajo a su ficha.

   Van scopeadas sin discusión. `.ops-bar` la comparte la TORRE DE CONTROL —que
   la encogió scopeada a `.dash-page` justamente para no arrastrar a los
   perfiles—, y `.card`, `.badge`, `.disp-table`, `.disp-inline-filters`,
   `.status-badge` y `.prof-*` los emite medio panel. Hay test que recorre las
   reglas de este bloque y falla nombrando la que no lleve el scope.

   Los números son los de la tabla ② del plan y los mismos que la cola (§15) y
   las rutas (§17): dos listas del panel no se leen distinto. */
@media (max-width: 767.98px) {
    /* ══ EL PERFIL (rider y empresa) ══════════════════════════════════════

       La identidad: foto, nombre y las pastillas de estado. La foto encoge —no
       es un objetivo de toque, es un reconocimiento— para darle a un nombre
       largo la línea entera. */
    .profile-page .dash-title { font-size: 0.86rem; }
    .profile-page .dash-title-sub { font-size: 0.5rem; margin-top: 1px; }
    .profile-page .prof-head { gap: 8px; }
    .profile-page .prof-avatar { width: 38px; height: 38px; flex-basis: 38px; font-size: 1rem; }
    .profile-page .dash-title-sub .badge { font-size: 0.44rem; padding: 0.16rem 0.36rem; }

    /* Las cuatro cifras, con la escala de la torre de control: es la MISMA
       barra y decir lo mismo más grande acá no tendría motivo. El pie se
       reserva sus dos líneas (§12) y eso las alinea entre sí. */
    .profile-page .ops-num { font-size: 0.86rem; }
    .profile-page .ops-label { font-size: 0.41rem; }
    .profile-page .ops-sub { font-size: 0.42rem; }

    /* Las tarjetas: cabecera, cuerpo y el texto suelto que las acompaña. */
    .profile-page .prof-grid { gap: var(--space-3); }
    .profile-page .card-header { padding: 8px 10px; }
    .profile-page .card-header h6 { font-size: 0.56rem; }
    .profile-page .card-header .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    .profile-page .card-body { padding: 10px; }
    .profile-page .card-body > p,
    .profile-page .card-body .small,
    .profile-page .card-body small { font-size: 0.5rem; }
    .profile-page .form-text { font-size: 0.46rem; }
    .profile-page .badge { font-size: 0.46rem; }
    .profile-page .status-badge { font-size: 0.5rem; }

    /* Las dos fichas de datos (Información y Jornada) son tablas de DOS
       columnas cortas —rótulo y valor—: se quedan como tabla y solo encogen
       (§4: una matriz angosta no gana nada partida en tarjetas). */
    .profile-page .table-borderless th,
    .profile-page .table-borderless td { font-size: 0.54rem; padding: 3px 6px; }
    .profile-page .table-borderless th { white-space: nowrap; }

    /* Lo plegado: el resumen es la fila que hace de cabecera de sección, así
       que se TOCA — encoge la letra, no el objetivo (④). */
    .profile-page .prof-details > summary { font-size: 0.56rem; min-height: 34px; }
    .profile-page .prof-details-card > summary { padding: 8px 10px; }
    .profile-page .prof-count { font-size: 0.44rem; padding: 0 6px; }

    /* Las listas de la ficha —pedidos recientes, pozo por asignar y la
       LIBRETA— con los mismos números que la cola (§15) y el historial de
       rutas (§17). */
    .profile-page .disp-table td { font-size: 0.56rem; }
    .profile-page .disp-table thead th { font-size: 0.46rem; }
    .profile-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .profile-page .disp-table.is-cards tbody tr { padding: 0.5rem 0.625rem; }
    .profile-page .disp-table.is-cards td:first-child { font-size: 0.62rem; }
    .profile-page .disp-table.is-cards .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    .profile-page .disp-table.is-cards small { font-size: 0.46rem; }

    /* ── ④ revisado: los CONTROLES del perfil, y la EXCEPCIÓN anotada ──────
       La misma que decidieron el paso 4 (la cola), el 5 (la ficha) y el 6 (las
       rutas): un control METIDO en una ficha que se LEE no es un formulario. En
       el perfil son la calificación del rider, la corrección del email del
       portal y el buscador de la libreta —un campo cada uno, dentro de una
       tarjeta a 0,5–0,56rem—; a 16px y 44px eran lo más grande de la pantalla.

       La contrapartida es la de siempre —al enfocarlos, iOS acerca la pantalla
       y no la devuelve solo— y está ACEPTADA a sabiendas. Revertirlo es subir
       estas reglas a `font-size: 16px` y `min-height: 44px`.

       Y acá está el LÍMITE de la excepción: un FORMULARIO de verdad se queda en
       16px. Los tres del perfil de la empresa —cargar una dirección de la
       libreta, crear el acceso al portal y resetear su contraseña— se marcan
       con `.form-touch` en el `<form>` y se re-inflan abajo: una dirección
       acaba en el geocoder y en el mapa del rider, y una contraseña no se
       teclea a 0,6rem. */
    .profile-page .card-body .form-control,
    .profile-page .card-body .form-select,
    .profile-page .card-header .form-control {
        font-size: 0.6rem;
        min-height: 30px;
        padding: 4px 7px;
    }
    .profile-page .card-body .form-label { font-size: 0.5rem; margin-bottom: 2px; }
    .profile-page .card-body .btn:not(.btn-link) {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 5px 9px;
    }
    .profile-page .card-header .btn { font-size: 0.54rem; min-height: 30px; padding: 4px 8px; }
    /* Los cinco botones de una entrada de la libreta van en el pie de su
       tarjeta (§4) y son CINCO en un renglón: 34px, el mismo suelo anotado que
       las acciones de fila del historial de rutas. */
    .profile-page .disp-table.is-cards td[data-label="Acciones"] .btn {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 0.25rem 0.5rem;
    }
    /* El límite de la excepción, escrito: lo que lleva `.form-touch` vuelve a
       los 16px de ④. Va DESPUÉS y con el mismo peso (0,3,0) que la regla de
       arriba, así que gana por ORDEN. */
    .profile-page .form-touch .form-control,
    .profile-page .form-touch .form-select {
        font-size: 16px;
        min-height: 44px;
        padding: 8px 12px;
    }
    .profile-page .form-touch .btn:not(.btn-link) { min-height: 44px; font-size: 0.8rem; }

    /* ══ EL DIRECTORIO (riders · empresas · catálogo) ═════════════════════ */
    .directory-page .dash-title { font-size: 0.86rem; }
    .directory-page .nav-pills .nav-link { font-size: 0.52rem; padding: 6px 10px; }

    /* La fila de una lista es una TARJETA (§4) y se lee con los números de la
       cola: el nombre del rider o de la empresa es el TÍTULO y no lleva
       etiqueta delante (para eso está arriba y en negrita). */
    .directory-page .disp-table td { font-size: 0.56rem; }
    .directory-page .disp-table thead th { font-size: 0.46rem; }
    .directory-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .directory-page .disp-table.is-cards tbody tr { padding: 0.5rem 0.625rem; }
    .directory-page .disp-table.is-cards td:first-child { font-size: 0.62rem; }
    .directory-page .disp-table.is-cards td:first-child::before { content: none; }
    .directory-page .disp-table .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }
    .directory-page .disp-table small { font-size: 0.46rem; }
    /* La foto de la fila encoge con su línea: es lo que hace reconocible al
       rider de un vistazo, así que NO se retira (③) — al contrario que el
       avatar de la torre de control, que ahí competía con la cifra. */
    .directory-page .dir-avatar { width: 30px; height: 30px; flex-basis: 30px; }
    .directory-page .dir-avatar-ph { font-size: 1.5rem; }
    .directory-page .pagination .page-link { font-size: 0.56rem; }
    .directory-page .empty-state p { font-size: 0.62rem; }
    /* El catálogo de una empresa: cabecera y cuerpo de sus dos tarjetas. */
    .directory-page .card-header { padding: 8px 10px; }
    .directory-page .card-header h6 { font-size: 0.56rem; }
    .directory-page .card-body { padding: 10px; }
    .directory-page .card-body > p { font-size: 0.5rem; }

    /* ── ④ revisado: los CONTROLES del directorio ──────────────────────────
       Misma excepción anotada y mismos números que los filtros del historial de
       rutas (§17): zona, vehículo y orden ACOTAN la lista de abajo, y a 16px y
       44px empujaban al primer rider fuera de cuadro. Revertirlo es subir estas
       reglas a `font-size: 16px` y `min-height: 44px`.

       El alta de un producto NO entra acá: lleva `.form-touch` y es un
       FORMULARIO (④ en pie). */
    .directory-page .disp-inline-filters .form-control,
    .directory-page .disp-inline-filters .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 0.25rem 0.4375rem;
    }
    .directory-page .disp-inline-filters .form-label { font-size: 0.5rem; margin-bottom: 2px; }
    .directory-page .disp-inline-filters .btn { font-size: 0.56rem; min-height: 32px; padding: 0.25rem 0.5rem; }
    /* Los tres verbos del filtro comparten renglón y se reparten el ancho: en
       columna eran tres botones a lo ancho por encima de la lista. */
    .directory-page .dir-filter-actions { width: 100%; }
    .directory-page .dir-filter-actions > .btn { flex: 1 1 auto; }
    /* Ver y editar son lo que se TOCA en una lista, y son solo DOS en un
       renglón: se quedan en 44px (④ sin excepción). Solo baja la letra. */
    .directory-page .disp-table.is-cards td[data-label="Acciones"] .btn {
        font-size: 0.62rem;
        min-height: 44px;
        min-width: 44px;
    }
    /* El borrar de una fila del catálogo, que es una TABLA y no una tarjeta. */
    .directory-page .disp-table:not(.is-cards) .btn { font-size: 0.56rem; min-height: 30px; padding: 0.1875rem 0.4375rem; }
    /* El formulario de alta del catálogo se queda de dedo: `.form-touch` ya lo
       pone en 16px/44px y acá no se lo toca. Solo su etiqueta encoge. */
    .directory-page .form-touch .form-label { font-size: 0.62rem; }
}

/* ── 19. Las JORNADAS (cuadrante · registro · horas extra · calendario) ────
   Paso 8 del modelo (`docs/archivo/PLAN_movil_dispatcher.md`). Son las dos pantallas
   con MÁS COLUMNAS del panel —ocho el cuadrante, nueve el registro— y también
   las que más CONTROLES tienen por centímetro: casillas, chips, dos horas, un
   desplegable de riders, dos fechas y los botones de resolver. Así que acá el
   ④ se revisa entero, control por control, y no solo la tipografía.

   DOS marcas nuevas y una prestada, porque son tres lecturas:

   - `.shifts-page` — el CUADRANTE SEMANAL (`/admin/shifts/week`): PLANIFICAR.
   - `.review-page` — el REGISTRO HORARIO (`/admin/shifts/review`) y su anexo
     de horas extra (`/admin/session/<id>/extras`): REVISAR lo que pasó. Las
     dos leen lo mismo —un rider, un día y dos horas—, así que una sola pasada
     las cubre.
   - `.cal-page`   — el calendario de jornadas de un rider, que §5 dejó fuera a
     propósito ("el `.cal-*` lo comparte el calendario de JORNADAS, que no
     entra en esta tanda"). Ahora entra: la plantilla se marca y sus cinco
     clases propias —`.cal-hours`, `.cal-sessions`, `.cal-shift`, `.cal-delta`
     y `.cal-absent`— se mudaron del `<style>` de su cuerpo a la hoja. Ese
     `<style>` iba DESPUÉS de este archivo y ganaba siempre: es exactamente el
     fallo silencioso que ① describe.

   Todo va scopeado: `.cal-*` lo comparten los dos calendarios de despacho,
   `.ops-bar` la torre de control y los perfiles, y `.card`/`.badge`/
   `.disp-table`/`.disp-inline-filters` los emite medio panel. */
@media (max-width: 767.98px) {
    /* ══ EL CUADRANTE SEMANAL ═════════════════════════════════════════════
       La grilla scrollea por diseño (siete días + la columna del rider no
       entran en ninguna pantalla chica) y sus dos bordes fijos vienen de su
       sección propia. Lo que cambia acá es cuánto hay que ARRASTRAR: con la
       letra a 0,6× el suelo baja de 760 a 620px, o sea que el domingo queda a
       un dedo en vez de a dos. */
    .shifts-page .dash-title { font-size: 0.86rem; }
    .shifts-page .sw-weeknav-label strong { font-size: 0.68rem; }
    .shifts-page .sw-weeknav-today { font-size: 0.5rem; }
    .shifts-page .sw-badge-now { font-size: 0.44rem; }
    .shifts-page .sw-weeknav-stats { font-size: 0.5rem; gap: var(--space-3); }

    .shifts-page .sw-grid { min-width: 620px; }
    .shifts-page .sw-grid th,
    .shifts-page .sw-grid td { padding: 0.25rem; }
    .shifts-page .sw-grid th.sw-rider-col { width: 104px; }
    .shifts-page .sw-rider-name { font-size: 0.56rem; }
    .shifts-page .sw-rider-hours { font-size: 0.44rem; }
    .shifts-page .sw-dayname { font-size: 0.44rem; }
    .shifts-page .sw-daynum { font-size: 0.66rem; }
    .shifts-page .sw-daymeta { font-size: 0.42rem; }
    /* El chip es la jornada puesta: la hora se lee, el resto acompaña. */
    .shifts-page .sw-chip { font-size: 0.48rem; padding: 1px 0.25rem; gap: 0.1875rem; }
    .shifts-page .sw-chip-note { font-size: 0.44rem; }
    .shifts-page .sw-chip-acts a,
    .shifts-page .sw-chip-acts button { font-size: 0.56rem; }
    /* En una celda de tabla el suelo se pone con `height`, nunca con
       `min-height` (el segundo no se aplica y la fila se cierra a 22px). */
    .shifts-page .sw-cell { height: 52px; }
    .shifts-page .sw-empty-filter { font-size: 0.56rem; padding: 0.625rem 0.75rem; }

    /* El panel de la derecha, que apilado pasa a ser la mitad de abajo. */
    .shifts-page .card-header h6 { font-size: 0.56rem; }
    .shifts-page .card-header { padding: 8px 10px; }
    .shifts-page .card-body { padding: 10px; }
    .shifts-page .sw-picked-num { font-size: 1rem; }
    .shifts-page .sw-picked-label { font-size: 0.5rem; }
    .shifts-page .sw-picked-hint { font-size: 0.5rem; }
    .shifts-page .sw-total { font-size: 0.52rem; }
    .shifts-page .sw-note { font-size: 0.48rem; }
    .shifts-page .sw-times-sep { font-size: 0.6rem; }
    .shifts-page .form-label { font-size: 0.56rem; }

    /* ── ④ revisado: los CONTROLES del cuadrante ──────────────────────────
       Acá el ④ manda casi entero, y es lo que separa esta pantalla de una
       lista: el panel de la derecha ES un formulario —se le escribe la hora a
       treinta jornadas de una vez—, así que `#sw-bulk` lleva `.form-touch` y
       sus dos horas y sus notas se quedan en 16px / 44px. No hay regla acá que
       las toque: eso es deliberado.

       Lo que sí encoge es lo que se TOCA pero no se escribe, y solo en la
       LETRA — el alto se queda donde ⑤ lo dejó:
         · el interruptor de modo conserva sus 44px (§14): decide qué marca
           cada clic del cuadrante, es lo primero que se toca;
         · los turnos de atajo y el botón de marcar un grupo conservan los suyos
           (38px y 30px, ya anotados en §14 como lo que la grilla permite);
         · la rueda de configurar los turnos SUBE a 34px: en escritorio mide
           26px porque se descubre con el ratón. */
    .shifts-page .sw-mode-btn { font-size: 0.6rem; }
    .shifts-page .sw-presets .btn { font-size: 0.56rem; padding: 0.1875rem 0.25rem; }
    .shifts-page .sw-pick-btn { font-size: 0.56rem; }
    .shifts-page .sw-pick-btn.is-icon { font-size: 0.66rem; }
    .shifts-page .sw-cog { width: 34px; height: 34px; font-size: 0.8rem; }
    .shifts-page .sw-search input { font-size: 16px; }
    /* La barra de acciones de arriba es lo que se TOCA para cambiar de
       pantalla: 44px de §1 intactos, solo baja la letra. */
    .shifts-page .disp-toolbar .disp-actions > .btn { font-size: 0.62rem; }

    /* ══ EL REGISTRO HORARIO (y las horas extra) ══════════════════════════ */
    .review-page .dash-title { font-size: 0.86rem; }
    /* Las tres cifras del rider, con los números de la torre de control: es la
       MISMA `.ops-bar` y decir lo mismo más grande acá no tendría motivo. */
    .review-page .ops-num { font-size: 0.86rem; }
    .review-page .ops-label { font-size: 0.41rem; }
    .review-page .ops-sub { font-size: 0.42rem; }
    .review-page .cal-day-sub { font-size: 0.5rem; }
    .review-page .cal-section-head { font-size: 0.6rem; }
    .review-page .cal-section-count { font-size: 0.48rem; padding: 1px 0.4375rem; }
    .review-page .cal-chip { font-size: 0.44rem; padding: 2px 0.3125rem; gap: 2px; }
    .review-page .card-header h6 { font-size: 0.56rem; }
    .review-page .card-header { padding: 8px 10px; }
    .review-page .card-body { padding: 10px; }

    /* La fila de nueve columnas ya es una TARJETA (§4). Lo que cambia es la
       escala: la etiqueta de cada línea encoge y con ella la columna que
       reserva, así que el dato —las horas, el reparto— se lleva el ancho. */
    .review-page .disp-table td { font-size: 0.56rem; }
    .review-page .disp-table thead th { font-size: 0.46rem; }
    .review-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 3.4rem;
        font-size: 0.44rem;
    }
    .review-page .disp-table.is-cards tbody tr { padding: 0.5rem 0.625rem; }
    .review-page .disp-table.is-cards td:first-child { font-size: 0.62rem; }
    .review-page .disp-table.is-cards tfoot td { font-size: 0.56rem; padding: 0.5rem 0.625rem; }
    .review-page .disp-sub { font-size: 0.46rem; }
    .review-page .badge { font-size: 0.46rem; padding: 0.18rem 0.4rem; }

    /* Las piezas del día: estado, fichaje, planificado, pausa, diferencia y
       reparto. La cifra de horas MANDA y el resto la explica. */
    .review-page .wdr-state { font-size: 0.46rem; padding: 2px 0.375rem; }
    .review-page .wdr-day { font-size: 0.62rem; }
    .review-page .wdr-rider { font-size: 0.56rem; }
    .review-page .wdr-hours { font-size: 0.6rem; }
    .review-page .wdr-times { font-size: 0.46rem; }
    .review-page .wdr-punch { font-size: 0.5rem; }
    .review-page .wdr-punch > i { font-size: 0.44rem; }
    .review-page .wdr-note { font-size: 0.44rem; }
    .review-page .wdr-split { font-size: 0.46rem; }
    .review-page .wdr-reason { font-size: 0.46rem; }
    .review-page .wdr-pause { font-size: 0.5rem; }
    .review-page .wdr-delta { font-size: 0.58rem; }
    .review-page .wdr-resolve > summary { font-size: 0.56rem; min-height: 30px; }
    .review-page .jm-who { font-size: 0.6rem; padding: 6px 8px; }

    /* ── ④ revisado: los CONTROLES del registro, y la EXCEPCIÓN anotada ───
       La misma que decidieron el paso 4 (la cola), el 5 (la ficha), el 6 (las
       rutas) y el 7 (los perfiles): un control que ACOTA una lista que se LEE
       no es un formulario. Acá son los rangos rápidos, el par de fechas, el
       desplegable de riders y el pill de "Solo pendientes" — con la tarjeta a
       0,56rem, a 16px y 44px eran lo más grande de la pantalla y empujaban el
       primer día fuera de cuadro.

       La contrapartida es la de siempre —al enfocar un campo, iOS acerca la
       pantalla y no la devuelve solo— y está ACEPTADA a sabiendas. Revertirlo
       es subir estas reglas a `font-size: 16px` y `min-height: 44px`. */
    .review-page .disp-actions > .btn { font-size: 0.56rem; min-height: 30px; }
    .review-page .disp-inline-filters .form-control,
    .review-page .disp-inline-filters .form-select {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 0.25rem 0.4375rem;
    }
    .review-page .disp-inline-filters .form-label { font-size: 0.5rem; margin-bottom: 2px; }
    .review-page .disp-inline-filters .btn { font-size: 0.56rem; min-height: 32px; padding: 0.25rem 0.5rem; }
    /* Resolver un día: dos verbos que se reparten el renglón. §7 ya les puso
       38px de alto para el dedo; acá solo baja la letra. */
    .review-page .wdr-choices .btn { font-size: 0.56rem; }
    /* Poner y corregir una jornada o un fichaje: iconos solos, que en el
       teléfono no se descubren con el ratón y viven siempre a la vista (§7,
       32px). Solo encoge el icono. */
    .review-page .wdr-add,
    .review-page .wdr-act { font-size: 0.6rem; }

    /* ── El LÍMITE de la excepción, escrito ────────────────────────────────
       Tres cosas de esta pantalla son FORMULARIOS de verdad y se quedan en
       16px / 44px: el modal que pone y corrige las horas (`.form-touch`), el
       alta de un bloque de horas extra (ídem) y el MOTIVO de una resolución
       —que es prosa que alguien teclea y queda escrita en el registro—.
       Las tres reglas van DESPUÉS de las que encogen y con peso igual o mayor,
       así que ganan por ORDEN. */
    .review-page .form-touch .form-control,
    .review-page .form-touch .form-select {
        font-size: 16px;
        min-height: 44px;
        padding: 8px 12px;
    }
    .review-page .form-touch .btn:not(.btn-link) { min-height: 44px; font-size: 0.8rem; }
    .review-page .wdr-resolve textarea.form-control { font-size: 16px; }
    /* Y la barra de acciones de arriba —cambiar de rider, ir al cuadrante— es
       lo que se TOCA: los 44px de §1 se quedan, solo baja la letra. Va después
       de la regla que encoge `.disp-actions > .btn` y le gana por peso. */
    .review-page .disp-toolbar .disp-actions > .btn {
        font-size: 0.62rem;
        min-height: 44px;
    }

    /* ══ EL CALENDARIO DE JORNADAS de un rider ════════════════════════════
       Marcado con `.cal-page`, así que la celda, el número del día y la
       cabecera de la semana ya los encoge §5. Lo que queda son sus cinco
       clases propias, las que estaban en el `<style>` del cuerpo. El COLOR de
       la celda —la intensidad de horas y el rojo de la ausencia— no se toca:
       es lo que se lee de un vistazo. */
    .cal-page .cal-hours { font-size: 0.5rem; }
    .cal-page .cal-sessions { font-size: 0.42rem; }
    .cal-page .cal-delta { font-size: 0.42rem; }
    .cal-page .cal-absent { font-size: 0.42rem; }
    .cal-page .cal-shift { font-size: 0.42rem; margin-top: 2px; padding-top: 2px; }
}

/* ── 20. Los FORMULARIOS (alta y edición de pedido · empresa · rider · perfil)
   Paso 9 del modelo (`docs/archivo/PLAN_movil_dispatcher.md`). Nueve pantallas y UNA
   marca, `.form-page`, porque son una sola lectura: el dispatcher escribe algo
   y lo guarda. Entran el alta y la edición de pedido (que son la MISMA pila de
   tarjetas), el alta y la edición de empresa (las dos incluyen
   `_company_form.html`), el alta de rider, su edición, el registro de admin, el
   perfil y el cambio de contraseña.

   **El corte acá es `575.98px`, no los `767.98px` del resto de la sección**: lo
   que se rompe en un formulario no son barras de acciones ni tablas densas sino
   las columnas `-sm` de Bootstrap, y ese es el ancho donde el formulario ya
   está en una sola columna. Es el mismo corte que usa desde siempre el bloque
   "El formulario de pedido EN EL TELÉFONO", más arriba en la hoja: éste lo
   CONTINÚA, no lo reemplaza.

   ── La EXCEPCIÓN de este paso, que es la grande ──────────────────────────
   El plan escrito decía que acá ④ mandaba por encima de ② —"un formulario no
   encoge por debajo de 16px en ningún campo"— y el dueño lo revisó en el
   teléfono y decidió lo contrario: **los botones, los `<select>` y los
   `<input>` encogen y se ajustan como el resto de la pantalla**. El motivo es
   el mismo que llevó a la excepción de los pasos 4 a 7, solo que multiplicado:
   el alta de un pedido son ~8 tarjetas anidando paradas, y con cada control a
   16px de letra y 44px de alto la pantalla entera era una columna de cajas
   iguales —dos entregas ya no entraban sin varias pantallas de scroll— y las
   ETIQUETAS, que son lo que dice qué se está escribiendo, quedaban más chicas
   que las cajas. Encogido, una tarjeta de parada entra completa y se ve qué
   pide.

   La contrapartida es la de siempre y está ACEPTADA a sabiendas: **iOS hace
   zoom al enfocar un control de menos de 16px y no devuelve la pantalla solo**.
   Revertirlo es UN gesto: subir el bloque "los CONTROLES" de acá abajo a
   `font-size: 16px` / `min-height: 44px` y borrar el resto; `.form-touch` sigue
   puesto en los `<form>` del pedido para que la regla global de más arriba
   vuelva a alcanzarlos sola.

   ── El LÍMITE, que sigue existiendo ──────────────────────────────────────
   Encoge lo que se ESCRIBE, no lo que se TOCA para terminar ni lo que se ELIGE
   de una lista con el dedo. Se quedan de dedo, y va escrito abajo con su
   motivo: la barra de acciones del pie (`.form-actionbar`, 48px — es el gesto
   que cierra el formulario), el `submit` de las pantallas que no la tienen,
   "Agregar otro punto de entrega" (ancho completo, abre trabajo nuevo) y las
   filas de las listas FLOTANTES —libreta de direcciones, resultados de producto
   y catálogo—, que son objetivos de toque de 44px y no campos.

   Todo va scopeado: `.cp-card`, `.card`, `.form-label`, `.form-control` y
   `.btn` los emite medio panel, y el detalle del pedido (§16) y las jornadas
   (§19) ya tienen los suyos. */
@media (max-width: 575.98px) {
    /* ══ La CABECERA y las TARJETAS ══════════════════════════════════════ */
    .form-page .dash-title { font-size: 0.86rem; }
    .form-page .back-crumb { font-size: 0.56rem; }
    /* Ocho `.cp-card` apiladas: cada píxel de padding se paga ocho veces (el
       mismo criterio y los mismos números que la ficha del pedido, §16). */
    .form-page .cp-card { margin-bottom: var(--space-3); }
    .form-page .cp-card-header { font-size: 0.56rem; padding: 0.5rem 0.625rem; gap: 0.3125rem; }
    .form-page .cp-card-body { padding: 0.625rem; }
    .form-page .card-header { padding: 8px 10px; }
    .form-page .card-header h5,
    .form-page .card-header h6 { font-size: 0.62rem; }
    .form-page .card-body { padding: 12px; }
    .form-page .cp-hr { margin: 0.625rem 0; }
    .form-page .section-label { font-size: 0.46rem; }
    .form-page .alert { font-size: 0.56rem; padding: 8px 10px; }
    /* La pastilla que NOMBRA la parada es lo que separa tres bloques idénticos:
       encoge, pero se queda por encima de su etiqueta. */
    .form-page .stop-tag { font-size: 0.5rem; padding: 1px 0.5rem; gap: 0.25rem; }
    .form-page .extra-stop-block { padding: var(--space-2); }
    .form-page .stop-products { padding-top: var(--space-2); }
    .form-page .stop-item-name { font-size: 0.62rem; }
    .form-page .stop-products-empty { font-size: 0.54rem; }
    .form-page .price-line-list li { font-size: 0.58rem; }
    .form-page .price-preview-total { font-size: 0.8rem; }
    .form-page .map-dest > legend { font-size: 0.56rem; }
    .form-page .map-dest-opt { font-size: 0.6rem; }
    .form-page .map-dest-note { font-size: 0.5rem; }
    .form-page .address-picker-item { font-size: 0.62rem; }
    .form-page .address-picker-meta { font-size: 0.5rem; }
    .form-page .address-picker-sub { font-size: 0.5rem; }
    .form-page .address-adjust-state { font-size: 0.5rem; }
    .form-page .item-grid th,
    .form-page .item-grid td { font-size: 0.6rem; }

    /* ══ ④ revisado: los CONTROLES ═══════════════════════════════════════
       Acá está la excepción anotada arriba. La ETIQUETA baja menos que el
       campo a propósito: en un formulario lo que hay que leer antes de escribir
       es qué pide, y a 0,54rem sigue estando por encima del texto gris de
       ayuda. Los dos selectores de cada regla —con y sin `.form-touch`— existen
       porque el pedido lleva la clase y las cinco pantallas cortas no: sin el
       primero, la regla global de 16px/44px le ganaría por especificidad al
       formulario que más falta le hace. */
    .form-page .form-label { font-size: 0.54rem; margin-bottom: 3px; }
    .form-page .form-text { font-size: 0.48rem; }
    .form-page .form-check-label { font-size: 0.56rem; }
    .form-page .form-check-input { width: 0.95rem; height: 0.95rem; margin-top: 0.16rem; }
    .form-page .form-control,
    .form-page .form-select,
    .form-page .form-control-sm,
    .form-page .form-select-sm,
    .form-page .form-touch .form-control,
    .form-page .form-touch .form-select,
    .form-page .form-touch .form-control-sm,
    .form-page .form-touch .form-select-sm {
        font-size: 0.72rem;
        min-height: 34px;
        padding: 5px 8px;
    }
    .form-page textarea.form-control,
    .form-page .form-touch textarea.form-control { min-height: 54px; }
    /* El número de la planta se reparte la fila con su `<select>`: con el campo
       a 34px el tope de 6rem sobra. */
    .form-page .form-touch .address-planta-num { max-width: 5rem; }
    /* Los botones de dentro de una tarjeta: quitar una parada, agregar un
       producto, ver el catálogo, ajustar en el mapa, calcular el precio. */
    .form-page .btn:not(.btn-link),
    .form-page .form-touch .btn:not(.btn-link) {
        font-size: 0.62rem;
        min-height: 34px;
        padding: 5px 9px;
    }
    .form-page .btn-link { font-size: 0.58rem; }
    /* Los dos iconos del pedido (foto y nota) viven en la cabecera de la
       tarjeta: acompañan a su título, no lo empujan. */
    .form-page .order-extra-btn { width: 28px; height: 28px; font-size: 0.7rem; }

    /* ══ El LÍMITE: lo que se TOCA y no se escribe ════════════════════════
       Va DESPUÉS de las reglas que encogen y con peso igual o mayor, así que
       gana por ORDEN. */
    /* ① La barra del pie es el gesto que CIERRA el formulario, y el alta mide
       varias pantallas de alto: sus 48px son de su propio bloque, acá solo se
       fija la letra para que "Crear pedido" no quede en dos renglones.
       El `[type="submit"]` va acá además de en ② porque si no la regla de ②
       —que viene después y pesa lo mismo— le bajaría a 44px justo al botón
       principal de la barra. */
    .form-page .form-actionbar .btn,
    .form-page .form-actionbar .btn[type="submit"],
    .form-page .form-touch .form-actionbar .btn:not(.btn-link) {
        font-size: 0.72rem;
        min-height: 48px;
    }
    /* ② Las pantallas cortas (empresa, rider, perfil, contraseña) no tienen
       barra de pie: ahí el que cierra es el `submit`, y se queda de dedo. El
       selector lleva el `.btn` además del atributo para pesar lo mismo que la
       regla que encoge (`.btn:not(.btn-link)`) y ganarle por orden; WTForms
       emite unos como `<button>` y otros como `<input type="submit">`. */
    .form-page .btn[type="submit"] { min-height: 44px; font-size: 0.72rem; }
    /* ③ "Agregar otro punto de entrega" abre trabajo NUEVO y va a ancho
       completo: se toca sin apuntar. */
    .form-page .btn-add-stop { font-size: 0.66rem; min-height: 40px; padding: 9px; }
    /* ④ Las listas que FLOTAN —libreta, resultados de producto, catálogo— se
       eligen con el dedo: son objetivos de toque, no campos. */
    .form-page .list-group-item-action,
    .form-page #catalog-modal .list-group-item-action { min-height: 44px; }
}


/* ── 21. PLANTILLAS y recurrencia ──────────────────────────────────────────
   Paso 10 del plan móvil (`docs/archivo/PLAN_movil_dispatcher.md`). Son TRES pantallas
   y **DOS marcas, no tres**:

   - `.templates-page` — la LISTA de moldes (`/admin/templates`). Es una lista
     del panel como la cola o el historial de rutas, así que corta en los
     `767.98px` de la sección y se lee con los MISMOS números que aquéllas
     (§15, §17, §18, §19): dos listas del panel no se leen distinto.
   - `.form-page` — el molde (`/admin/templates/new`, `…/<id>/edit`) y su
     recurrencia (`…/<id>/recurrence`). **No estrenan escala**: el molde es la
     MISMA pila de tarjetas que el alta de un pedido —mismo buscador de
     direcciones, mismos productos por entrega, misma columna pegajosa—, y el
     paso 9 ya la calibró entera. Reutilizan §20 (corte `575.98px`) y acá
     abajo solo se agrega lo que el alta NO tiene: el subtítulo de la cabecera,
     el sufijo "días" de la recurrencia y la lista de próximas fechas. Si algún
     día hay que encoger un campo de la plantilla, se encoge en §20 y se mueven
     las dos a la vez, que es lo que las mantiene idénticas.

   ── ④ revisado control por control, que es lo que pidió esta pasada ───────
   En la LISTA, cada tarjeta lleva adentro un formulario de generar (fecha +
   verbo) y cuatro acciones más. Es la EXCEPCIÓN anotada de siempre (pasos 4 a
   7): lo que ACOTA o ACOMPAÑA a una tarjeta que se LEE encoge con ella —la
   fecha del servicio a 0.62rem/30px, las cuatro acciones de la fila a
   0.56rem/34px, los dos botones del aviso previo a 34px—, **con el zoom de iOS
   aceptado a sabiendas**: revertirlo es subir el bloque "los CONTROLES" de
   acá abajo a 16px/44px. Lo que NO encoge, y va escrito con su motivo: la
   barra de acciones («Nueva plantilla», los 44px de §1 — solo le baja la
   letra), «Crear la primera» del estado vacío (mismo gesto) y **«Crear
   pedido», que se queda en 38px** porque no acota nada: escribe un pedido de
   verdad en la calle.

   Todo va scopeado: `.cp-card`, `.badge`, `.alert`, `.btn` y `.plan-confirm`
   los emite medio panel —`.plan-confirm` lo comparten los dos modales del mapa
   y la lista del PORTAL, que no entra en este plan—. */
@media (max-width: 767.98px) {
    /* ══ La LISTA de moldes ══════════════════════════════════════════════ */
    .templates-page .dash-title { font-size: 0.86rem; }
    .templates-page .dash-title-sub { font-size: 0.5rem; margin-top: 1px; }
    /* La barra de acciones es lo que se TOCA acá: §1 la deja en 44px de alto
       con el ancho entero, y esta pasada solo le baja la letra. */
    .templates-page .disp-actions .btn { font-size: 0.62rem; }

    .templates-page .cp-card { margin-bottom: var(--space-3); }
    .templates-page .cp-card-body { padding: 0.625rem; }
    .templates-page .tpl-card-grid { gap: var(--space-2); }
    /* El NOMBRE del molde es el título de la tarjeta: los mismos 0.62rem que
       el título de una fila en la cola (§15) y en el historial (§17). */
    .templates-page .tpl-main h2 { font-size: 0.62rem; }
    .templates-page .badge { font-size: 0.44rem; padding: 0.16rem 0.36rem; }
    /* Empresa, rider y express: el dato secundario de la tarjeta. */
    .templates-page .tpl-main p { font-size: 0.5rem; margin-bottom: 4px; }
    /* Las direcciones y la recurrencia son lo que IDENTIFICA un molde de otro
       —dos plantillas de la misma empresa se distinguen por dónde recogen—,
       así que se quedan por encima del dato secundario. */
    .templates-page .tpl-main .small { font-size: 0.54rem; }
    .templates-page .alert { font-size: 0.54rem; padding: 8px 10px; }
    .templates-page .empty-state { font-size: 0.56rem; }
    /* El aviso PREVIO a generar: se lee entero antes de decidir. */
    .templates-page .plan-confirm { padding: 7px 9px; }
    .templates-page .plan-confirm-head { font-size: 0.54rem; }
    .templates-page .plan-confirm-tpl,
    .templates-page .plan-confirm-item { font-size: 0.5rem; }

    /* ══ ④ Los CONTROLES de la tarjeta ═══════════════════════════════════
       La excepción anotada arriba, con su revert al lado: subir estas dos
       reglas a `font-size: 16px` / `min-height: 44px`. */
    .templates-page .form-label { font-size: 0.5rem; }
    .templates-page .form-control,
    .templates-page .form-control-sm {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 4px 7px;
    }
    .templates-page .tpl-row-actions .btn,
    .templates-page .plan-confirm-actions .btn {
        font-size: 0.56rem;
        min-height: 34px;
        padding: 4px 8px;
    }
    /* El LÍMITE, en reglas puestas DESPUÉS de las que encogen. «Crear pedido»
       no acota una lista: escribe un pedido de verdad, así que se queda
       cómodo y con el ancho que le deje la fecha. */
    .templates-page .tpl-use .btn { font-size: 0.6rem; min-height: 38px; }
    .templates-page .empty-state .btn { font-size: 0.62rem; min-height: 44px; }
}

/* Lo que el molde tiene y el alta de un pedido no. Corte `575.98px`, el de
   §20: es su continuación, no otra pantalla. */
@media (max-width: 575.98px) {
    .form-page .dash-title-sub { font-size: 0.5rem; margin-top: 1px; }
    /* El sufijo "días / semanas / meses" va PEGADO a su campo: si no encoge
       con él, el `.input-group` queda de dos alturas distintas. */
    .form-page .input-group-text {
        font-size: 0.72rem;
        min-height: 34px;
        padding: 5px 8px;
    }
    /* Las próximas fechas que produce la regla son lo más importante de la
       pantalla de recurrencia: es la única forma de revisarla antes de
       confiarle la generación. Encoge, pero "ver el día" es un enlace que se
       TOCA y conserva su alto. */
    .form-page .tpl-preview li { font-size: 0.6rem; }
    .form-page .tpl-preview a {
        font-size: 0.56rem;
        min-height: 34px;
        display: inline-flex;
        align-items: center;
    }
}


/* ── 22. La TARIFA (ciudades · zonas · CPs · festivos · configuración) ──────
   Paso 11 del plan móvil (`docs/archivo/PLAN_movil_dispatcher.md`). CINCO pantallas y
   **UNA marca, `.pricing-page`**, con el corte `767.98px` de la sección: son
   la misma pantalla cinco veces —una tarjeta de alta a la izquierda y una
   tabla a la derecha, apiladas en el teléfono— y leerlas distinto no tendría
   ningún motivo. La configuración general no tiene tabla, pero es la misma
   pila de tarjetas y entra por la misma puerta.

   **Las tablas se quedan como TABLA y scrollean**, que es lo que dice el plan:
   acá las columnas SON la información (una zona es su nombre, su precio
   laborable, su precio festivo y sus CPs, y esos cuatro números se comparan
   ENTRE FILAS). Convertirlas en tarjetas —§4— separaría los precios que se
   miran juntos. Nada se esconde: lo que no entra se desplaza dentro de su
   `.table-responsive`, que ya estaba.

   ── ④ revisado control por control ────────────────────────────────────────
   Acá ④ se INVIERTE y va anotado, con el mismo criterio que §20: los campos,
   los `<select>` y los botones bajan a **0.72rem/34px** en vez de quedarse en
   16px/44px. El motivo es el de siempre en esta pantalla: la tarifa es una
   MATRIZ de números que se LEE mucho más de lo que se escribe —un precio de
   zona se toca una vez al mes—, y a 16px/44px la columna de la izquierda se
   comía media pantalla con tres campos, mientras la tabla que se venía a
   mirar quedaba debajo del pliegue. El zoom de iOS al enfocar un campo queda
   **aceptado a sabiendas**, y el revert es subir el bloque «los CONTROLES» a
   `font-size: 16px` / `min-height: 44px`.
   Lo que se ESCRIBE en la fila de la tabla (los dos precios de una zona)
   encoge un punto más —0.62rem/30px—: acompaña a la fila, como la fecha del
   servicio de §21.
   El LÍMITE, en reglas puestas DESPUÉS de las que encogen: el `submit` que
   GUARDA (alta de ciudad, zona, CP, festivo y la configuración entera) se
   queda en **38px**, porque es el gesto que escribe la tarifa con la que se
   cobra; y la barra de acciones de arriba conserva los 44px de §1 —«Volver»,
   «Festivos» y «Configuración general» son lo que se TOCA acá— y solo le baja
   la letra.

   Todo va scopeado: `.card`, `.badge`, `.table` y `.btn` los emite medio
   panel. */
@media (max-width: 767.98px) {
    .pricing-page .dash-title { font-size: 0.86rem; }
    .pricing-page .dash-title-sub { font-size: 0.5rem; margin-top: 1px; }
    /* La barra de acciones es lo que se TOCA: §1 la deja en 44px con el ancho
       entero y esta pasada solo le baja la letra. */
    .pricing-page .disp-actions .btn { font-size: 0.62rem; }

    .pricing-page .card { margin-bottom: var(--space-3); }
    .pricing-page .card-header { padding: 8px 10px; }
    .pricing-page .card-header h6 { font-size: 0.6rem; margin-bottom: 0; }
    .pricing-page .card-body { padding: 10px; }

    /* ══ La TABLA: sigue siendo una tabla y sigue scrolleando ═════════════ */
    .pricing-page .table { font-size: 0.62rem; margin-bottom: 0; }
    .pricing-page .table th { font-size: 0.54rem; }
    .pricing-page .table > :not(caption) > * > * { padding: 6px 7px; }
    .pricing-page .badge { font-size: 0.44rem; padding: 0.16rem 0.36rem; }
    .pricing-page .text-muted { font-size: 0.56rem; }
    .pricing-page .form-text { font-size: 0.5rem; }

    /* ══ ④ Los CONTROLES ═════════════════════════════════════════════════
       La excepción INVERTIDA anotada arriba, con su revert al lado: subir
       este bloque a `font-size: 16px` / `min-height: 44px`. La ETIQUETA baja
       MENOS que el campo (§20): es lo que hay que leer antes de escribir. */
    .pricing-page .form-label { font-size: 0.54rem; margin-bottom: 3px; }
    .pricing-page .form-control,
    .pricing-page .form-select {
        font-size: 0.72rem;
        min-height: 34px;
        padding: 5px 8px;
    }
    /* Lo que se escribe EN LA FILA acompaña a la fila, no a la tarjeta. */
    .pricing-page .table .form-control,
    .pricing-page .table .form-control-sm {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 3px 6px;
    }
    /* Guardar · CPs · eliminar: las acciones de la fila. */
    .pricing-page .table .btn {
        font-size: 0.56rem;
        min-height: 30px;
        padding: 3px 7px;
    }

    /* El LÍMITE, DESPUÉS de las reglas que encogen: el `submit` que escribe la
       tarifa con la que se cobra no acota nada y se queda cómodo. */
    .pricing-page .pricing-save {
        font-size: 0.66rem;
        min-height: 38px;
    }
}

/* ── 23. La CAMPANA en el teléfono ─────────────────────────────────────────
   El panel es lo único de la campana que no entra: colgado de la esquina
   derecha con 22rem de ancho se sale de la pantalla. Deja de colgar de la
   campana y pasa a medir contra el VIEWPORT —fijo, del padding de borde a
   padding de borde y bajo la topbar—, que es el mismo criterio que el resto
   de esta sección: el ancho lo pone la pantalla, no el bloque.

   El alto se mide en `svh` (con `vh` antes como fallback): `100vh` en móvil
   es el viewport EXPANDIDO y dejaría el pie del panel debajo de la barra del
   navegador. La campana en sí NO se toca: es un objetivo de TOQUE y sus
   2,1rem con la escala en 1 son los ~34px que ya tenía. */
@media (max-width: 767.98px) {
    .ntf-panel {
        position: fixed;
        top: calc(var(--topbar-h) + var(--space-2));
        /* El mismo hueco que el padding de borde del contenido, escrito con
           su token y no con `--content-pad`: esa variable la declara
           `.app-content` y el panel cuelga de la TOPBAR, que está fuera. En
           el teléfono `--content-pad` ES `--space-3`, así que el borde
           coincide. */
        left: var(--space-3);
        right: var(--space-3);
        width: auto;
        max-width: none;
    }
    .ntf-panel-body {
        max-height: 60vh;
        max-height: 60svh;
    }
}

/* ── 24. El INFORME DE FACTURACIÓN (dispatcher + portal) ───────────────────
   Paso 7 de `docs/archivo/PLAN_facturacion.md`, con el modelo de
   `docs/archivo/PLAN_movil_dispatcher.md`: ① un scope de pantalla, ② la tipografía a
   ~0,6×, ③ nada se esconde, ④ 16px donde se ESCRIBE y 44px donde se TOCA, con
   sus excepciones anotadas, ⑤ un solo ancho y nada de escalar el lienzo.

   **UNA marca, `.billing-page`, y la llevan las DOS caras** —`/admin/billing`
   y `/company/billing`—. Es la primera sección de este bloque que alcanza al
   PORTAL, y va a propósito: §15 dejó la cola del portal fuera porque allá son
   dos pantallas distintas con dos chromes distintos, pero acá es la MISMA
   pantalla leída con otro acote (el mismo `report`, la misma familia `.bill-*`,
   el mismo marcado de tabla), y el local que quiere mirar lo que se le cobra lo
   mira desde el teléfono tanto o más que el dispatcher. Dejarla fuera sería
   escribir dos veces la misma pasada para que una de las dos no llegue nunca.

   ── ③ La tabla de LOCALES pasa a TARJETAS (§4), al revés que la tarifa ─────
   §22 dejó las tablas de la tarifa como TABLA porque ahí las columnas SON la
   información. Acá la comparación también importa, pero hay algo que decide por
   encima: debajo de cada local cuelga su `<details>` con el detalle por pedido,
   dentro de un `<td colspan="5">`. Con la tabla scrolleando (`min-width:
   max-content`, §4), ese bloque anidado —que lleva direcciones enteras— le
   estira el ancho a la tabla ENTERA, y comparar dos locales pasaría a ser un
   arrastre horizontal de varias pantallas. En tarjetas, el detalle mide lo suyo
   y el local de al lado está a un dedo de distancia. Lo que se compara —el
   importe— queda igual de legible porque cada línea lleva su etiqueta.

   ── ④ revisado control por control ────────────────────────────────────────
   La EXCEPCIÓN anotada de siempre, la misma que decidieron la cola (§15), el
   historial de rutas (§17) y el registro horario (§19): las dos fechas del
   período y su "Ver" son un filtro que ACOTA lo de abajo, no un formulario, y a
   16px/44px eran lo más grande de una pantalla que se viene a LEER. Bajan a
   0.62rem/30px. El zoom de iOS al enfocarlos queda ACEPTADO a sabiendas y el
   revert es subir ese bloque a `font-size: 16px` / `min-height: 44px`.
   El LÍMITE, en reglas puestas DESPUÉS: la barra de acciones de arriba —CSV,
   PDF y "Resumen por día", que es lo que se TOCA acá— conserva los 44px de §1 y
   solo le baja la letra, y los dos botones de dentro del detalle se quedan en
   34px, el mismo suelo que §17 le dio a las acciones de una tarjeta.

   Todo va scopeado: `.disp-table`, `.disp-actions`, `.disp-inline-filters` y
   `.cp-card` los emite medio panel y también el portal entero. */
@media (max-width: 767.98px) {
    /* ── ② La escala ───────────────────────────────────────────────────────
       En escritorio esta pantalla va a `--ui-scale: 0.85` y en el corte móvil
       la escala vuelve a 1: sin esta pasada se dibujaría MÁS GRANDE en el
       teléfono que en el monitor. */
    .billing-page .dash-title { font-size: 0.86rem; }
    .billing-page .page-title { font-size: 0.95rem; }
    .billing-page .page-subtitle { font-size: 0.56rem; }
    .billing-page .disp-actions .btn { font-size: 0.62rem; }
    .billing-page .cal-day-sub { font-size: 0.56rem; }

    /* La cabecera del período se apila: el rango arriba y el total debajo,
       alineado a la izquierda como todo lo demás — un bloque de ancho completo
       con el texto pegado al borde derecho se lee como si fuera de otra cosa. */
    .billing-page .bill-head { gap: 0.5rem; }
    .billing-page .bill-head-period { font-size: 0.68rem; }
    .billing-page .bill-provisional { font-size: 0.54rem; }
    .billing-page .bill-head-total { text-align: left; }
    .billing-page .bill-total-label { font-size: 0.54rem; }
    /* La cifra que se viene a mirar NO encoge a 0,6×: es el contenido de la
       pantalla, no su chrome. Baja solo lo justo para no partirse en dos. */
    .billing-page .bill-total-value { font-size: 1.35rem; }
    .billing-page .bill-total-sub { font-size: 0.58rem; }

    /* ── ③ Las TARJETAS ────────────────────────────────────────────────────
       §4 ya las dibuja; acá va la escala y lo que esta pantalla tiene de
       propio. */
    .billing-page .disp-table.is-cards { font-size: 0.66rem; }
    .billing-page .disp-table.is-cards td[data-label]::before {
        flex: 0 0 4.75rem;
        font-size: 0.52rem;
    }
    .billing-page .disp-table.is-cards td:first-child { font-size: 0.78rem; }

    /* El detalle de un local NO es una tarjeta más: cuelga de la de arriba, así
       que la línea que separa un local del siguiente la dibuja ÉL y no el
       local. Sin esto quedaba un borde entre el nombre del local y su propio
       desplegable, que lo leía como dos bloques distintos. */
    .billing-page .bill-locals.is-cards > tbody > tr { border-bottom: 0; }
    .billing-page .bill-locals.is-cards > tbody > tr.bill-detail-row {
        padding: 0 0.75rem 0.5rem;
        border-bottom: 1px solid var(--gray-200);
    }
    .billing-page .bill-locals.is-cards tr.bill-detail-row > td {
        display: block;
        font-size: 0.66rem;
        margin-bottom: 0;
    }
    .billing-page .bill-detail > summary { font-size: 0.6rem; }

    /* La fila TOTAL es la única cifra que la pantalla repite, y es la que se
       viene a buscar: §4 le quita las etiquetas a una `.disp-group-row` (allá
       es una franja de agrupado, no datos) y acá hay que devolvérselas, o el
       pie queda en cuatro números sueltos sin decir de qué columna es cada uno. */
    .billing-page .disp-table.is-cards .disp-group-row td[data-label]::before {
        content: attr(data-label);
    }
    .billing-page .disp-table.is-cards tfoot .disp-group-row td {
        display: flex;
        font-size: 0.66rem;
        padding: 0.1875rem 0.75rem;
    }

    /* El trayecto deja de recortarse: en una tarjeta hay ancho para la línea
       entera, y la dirección es lo único que se lee de verdad en movimiento
       (§4 hace lo mismo con `.disp-route .leg`). */
    .billing-page .bill-route .leg { max-width: none; }
    .billing-page .bill-route .leg-txt {
        white-space: normal;
        overflow: visible;
    }

    /* La carga —peso y productos— encoge con el resto. Es lo que hace
       reconocible un pedido en la lista, así que ③ manda: no se esconde. */
    .billing-page .bill-load { font-size: 0.56rem; padding-left: 0.75rem; }
    .billing-page .bill-route .peso-chip { font-size: 0.54rem; }

    /* ── ④ Los CONTROLES ───────────────────────────────────────────────────
       La excepción INVERTIDA anotada arriba, con su revert al lado: subir este
       bloque a `font-size: 16px` / `min-height: 44px`. La ETIQUETA baja MENOS
       que el campo (§20): es lo que hay que leer antes de escribir. */
    .billing-page .disp-inline-filters .form-control {
        font-size: 0.62rem;
        min-height: 30px;
        padding: 0.25rem 0.4375rem;
    }
    .billing-page .disp-inline-filters .form-label { font-size: 0.5rem; margin-bottom: 2px; }
    .billing-page .disp-inline-filters .btn {
        font-size: 0.56rem;
        min-height: 32px;
        padding: 0.25rem 0.5rem;
    }
    /* Los atajos de período son pills de rango: acompañan al filtro, no a la
       barra de acciones. */
    .billing-page .disp-card > .disp-actions .btn,
    .billing-page .cp-card > .disp-actions .btn {
        font-size: 0.56rem;
        min-height: 30px;
        padding: 0.25rem 0.5rem;
        flex: 0 1 auto;
    }

    /* El LÍMITE, DESPUÉS de las reglas que encogen: lo que se TOCA se queda de
       dedo. Arriba, la barra de acciones con los 44px de §1 (§1 ya se los da:
       acá solo se declara que esta pasada no se los quita). Adentro del
       detalle, los dos botones que bajan el CSV y el PDF de ESE local: 34px,
       el suelo de una acción de tarjeta. */
    .billing-page .bill-detail-actions .btn {
        font-size: 0.56rem;
        min-height: 34px;
    }
}
