/* OnSite by Gestacore, hoja 4 de 4: diálogos y modales, y las piezas propias de cada vista
   (picker de mapa, píldoras, buscador, paginación, toasts, configuración, temporizador, evidencia,
   firma, combo, contrato, tramos, curación...). Se carga AL FINAL: pisa a las tres anteriores. */
/* ---------- Diálogos (modales anti-cierre, D32) ---------- */
/* Flex columna + overflow hidden (fix 2026-08-31): el cascarón NUNCA scrollea -- el único
   scroller de un modal es su cuerpo (.modal-form o .dialogo-cuerpo). Chromium le puso
   `overflow: auto` al <dialog> en su hoja de estilos base y la barra sticky de .acciones (con
   sus márgenes negativos) le inflaba el scrollHeight al dialog: salía una segunda barra que no
   scrolleaba nada útil. Con flex, en pantallas chaparras el cuerpo se ENCOGE al espacio que
   queda (min-height 0) en vez de desbordar el cascarón. */
.dialogo { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); color: var(--fg); padding: 0; width: min(100% - 2 * var(--s-4), 560px); max-height: min(calc(90dvh / var(--zoom)), 100% - 36px); box-shadow: var(--sh-panel); overflow: hidden; }
/* SOLO con [open]: un `display` en la regla base pisa el `display: none` con el que el navegador
   esconde los <dialog> cerrados y los deja TODOS pegados en pantalla (pasó en vivo, 2026-08-31). */
.dialogo[open] { display: flex; flex-direction: column; }
.dialogo-ancho { width: min(100% - 2 * var(--s-4), 920px); }
/* Dropdown de la campana (T5, Tanda 1 de campanita, Diseño §3.1/§5): mismo motivo que
   `.dialogo-ancho` de arriba para vivir en ESTA hoja y no en `componentes.css` -- las cuatro
   hojas cargan en cascada (`web/templating.py::HOJAS_CSS`) y `.dialogo` (regla base, más abajo en
   este archivo) pisaría un ancho/alto puesto en un archivo anterior por puro orden de carga,
   aunque las dos reglas tengan la misma especificidad. `<dialog>` con `showModal()` se centra por
   default del navegador; aquí se ancla a mano con `position:fixed` -- un solo punto fijo cerca de
   la campana no necesita el Anchor Positioning API. `min-width:0` en el propio dialog y en su
   lista para que un título largo no desborde en la banda de tablet (700-1024, la que se rompe). */
.dialogo-avisos { min-width: 0; }
.dialogo-avisos #avisos-panel { min-width: 0; }
@media (min-width: 1025px) {
  /* Los cuatro insets a mano (no solo top/right): la hoja base del navegador para <dialog>
     abierto con `showModal()` trae `inset: 0` -- dejar `bottom`/`left` como venían los estira
     entre el `top` de aquí y el borde de la pantalla (height:auto se infla al hueco completo). */
  .dialogo-avisos { position: fixed; inset-block-start: 64px; inset-inline-end: var(--s-5); inset-inline-start: auto; inset-block-end: auto; margin: 0; width: min(360px, calc(100vw / var(--zoom) - 2 * var(--s-5))); max-height: min(calc(70dvh / var(--zoom)), 520px); }
}
@media (max-width: 1024px) {
  .dialogo-avisos { position: fixed; inset-block-end: 0; inset-block-start: auto; inset-inline: 0; margin: 0; width: 100%; max-width: 100%; border-radius: var(--r-md) var(--r-md) 0 0; max-height: min(calc(80dvh / var(--zoom)), 640px); }
}
.dialogo-cab { flex-shrink: 0; }
#modal-cuerpo { display: flex; flex-direction: column; min-height: 0; }
/* Formularios en modal: en tablet y escritorio ocupan el 80% de la pantalla (pedido de Joe,
   2026-08-31 -- los 560px de H1 se veían de juguete en monitores grandes). Las confirmaciones
   y avisos (fragmentos sin .modal-form) se quedan compactos a propósito: ahí el chico es
   correcto. En móvil (< 700px) manda el ancho completo de siempre. */
@media (min-width: 700px) {
  .dialogo:has(.modal-form) { width: min(100% - 2 * var(--s-4), calc(80vw / var(--zoom))); }
  /* Densidad (Joe, 2026-08-31: "todo se ve enorme, como con zoom del 150%"): el 80% no es para
     estirar cada campo a sábana sino para que QUEPA más. Un corrido de .campo sueltos (hijos
     directos del form o de sus <section> de pasos del wizard) se acomoda en columnas de ~320px;
     lo estructural (avisos, errores, fieldsets, .filas, acciones) sigue a lo ancho. OJO:
     section:not([hidden]) -- un display en la regla base pisaría el `hidden` con el que el
     wizard esconde sus pasos (misma lección que .dialogo[open]). */
  .modal-form > section:not([hidden]) { display: grid; gap: var(--s-4); }
  /* align-items start: sin él, la celda más alta de un renglón (p. ej. Sucursal con sus
     responsables y checklist debajo) ESTIRA a sus vecinos y los <select> salen inflados y
     desproporcionados (cazado por Joe en video, 2026-08-31). */
  .modal-form, .modal-form > section:not([hidden]) { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); align-items: start; }
  .modal-form > *, .modal-form > section:not([hidden]) > * { grid-column: 1 / -1; }
  /* OJO: #sucursal-wrap va en SU regla, nunca dentro de un `:is()` junto a .campo -- `:is()`
     regala la especificidad de su argumento MÁS específico (el id) a todo lo que matchee, y esa
     especificidad de ID aplastaba la regla de textareas de abajo (mordió en vivo). */
  .modal-form > .campo, .modal-form > section:not([hidden]) > .campo { grid-column: auto; }
  .modal-form > section:not([hidden]) > #sucursal-wrap { grid-column: auto; }
  /* Los textos largos (notas) NO van en columna con los campos cortos (Joe, 2026-08-31: "las
     dos notas al final se ve feo"): un campo con textarea baja y toma todo lo ancho del modal. */
  .modal-form > .campo:has(> textarea), .modal-form > section:not([hidden]) > .campo:has(> textarea) { grid-column: 1 / -1; }
  /* H28b, Cambiar administrador: los campos del NUEVO administrador van en un <fieldset> con su
     leyenda (así se lee como grupo), y sus .campo dejan de ser hijos directos del form: sin esto se
     apilan en UNA columna a todo el ancho del modal (1100px de input a 1440), distinto de Asignar. El
     fieldset repite la cuadrícula del form (misma columna mínima y mismo gap) y lo estructural de
     adentro (el radio de acceso, los avisos) sigue a lo ancho. Opt-in con `.campos-en-columnas`: el
     resto de los fieldset.pila de un modal (radios, listas de casillas) no cambia. */
  .modal-form > fieldset.campos-en-columnas { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--s-4); align-items: start; }
  .modal-form > fieldset.campos-en-columnas > * { grid-column: 1 / -1; }
  .modal-form > fieldset.campos-en-columnas > .campo { grid-column: auto; }
}
/* Info de la sucursal elegida (responsables/checklist) en el form de orden: zona propia de
   ancho completo bajo el renglón de selects (Joe, 2026-08-31: adentro de la columna de Sucursal
   se apretaba en un tercio del modal). Vacía (sin sucursal elegida) se esconde -- el div
   envuelve siempre para que el swap fuera de banda tenga a quién apuntar. */
#sucursal-info { display: grid; gap: var(--s-1); }
#sucursal-info:not(:has(p)) { display: none; }
@media (min-width: 700px) {
  /* Y dentro de un renglón de campos, ninguno crece a sábana completa. */
  .fila-campos > .campo { max-width: 560px; }
}
.dialogo::backdrop { background: rgb(35 34 34 / .45); backdrop-filter: blur(2px); }
.dialogo-cab { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--border); }
.dialogo-cuerpo { padding: var(--s-5); max-height: min(calc(70dvh / var(--zoom)), 720px); overflow: auto; }
.menu-lista { list-style: none; margin: 0; padding: var(--s-2); display: grid; }
.menu-lista a { display: block; min-height: 44px; padding: var(--s-3) var(--s-4); border-radius: var(--r-md); color: var(--fg); text-decoration: none; }
.menu-lista a:hover { background: var(--surface-2); }
/* Un ítem del menú que es una acción (Salir) o que todavía no existe (Stock): mismo trato que
   .menu-lista a, pero con <button> real (semántica correcta para "hacer algo", no "ir a algo"). */
.menu-lista button { display: block; width: 100%; min-height: 44px; padding: var(--s-3) var(--s-4); border: 0; border-radius: var(--r-md); background: transparent; color: var(--fg); font: inherit; font-weight: 400; text-align: left; cursor: pointer; }
.menu-lista button:hover { background: var(--surface-2); }
.menu-lista button[disabled] { opacity: .5; cursor: not-allowed; }
.opcion { display: inline-flex; align-items: center; gap: var(--s-2); min-height: 44px; padding: 0 var(--s-3); border: 1px solid var(--border-strong); border-radius: var(--r-md); cursor: pointer; }
.opcion input { accent-color: var(--primary); width: 20px; height: 20px; }
/* Ritmo vertical del cuerpo de Ayuda: título, párrafo y lista de definiciones se separan
   solos. Se salta los cuerpos con `.pila`, que ya llevan su propio `gap` de rejilla. */
.dialogo-cuerpo:not(.pila) > * + * { margin-top: var(--s-3); }
.dialogo-cuerpo:not(.pila) > dl + h3 { margin-top: var(--s-6); }
/* Listas de definición del cuerpo de Ayuda: ritmo en grid en vez de la sangría por defecto del navegador */
.dialogo-cuerpo dl { display: grid; gap: var(--s-2) var(--s-4); grid-template-columns: max-content minmax(0, 1fr); margin: 0; }
.dialogo-cuerpo dt { font-weight: 600; }
.dialogo-cuerpo dd { margin: 0; color: var(--muted-fg); }
@media (max-width: 640px) {
  .dialogo-cuerpo dl { grid-template-columns: minmax(0, 1fr); }
}
/* ---------- `.acciones`: fila de botones (H14 T2 fix ronda 1) ---------- */
/* Base para una página COMPLETA, fuera de un modal (el wizard del portal, cero CSS nuevo por
   plantilla): mismo flex/gap que `.modal-form .acciones` de abajo, sin lo que es específico de
   vivir pegado al fondo de un `<dialog>` que scrollea (sticky/fondo/borde/márgenes negativos).
   Especificidad menor que `.modal-form .acciones`: donde SÍ es un modal, esa regla sigue mandando. */
.acciones { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s-3); }
@media (max-width: 480px) {
  .acciones { flex-direction: column-reverse; }
  .acciones .btn { width: 100%; }
}

/* ---------- Formularios en modal (H1 T3): GET fragmento → #modal-cuerpo, POST 204/422 ---------- */
/* Un formulario largo (alta de cliente, editor de checklist) scrollea dentro del modal: el
   <dialog> ya viene acotado por el navegador y sin `overflow` los botones quedaban fuera de la
   pantalla, sin forma de llegar a ellos. Las acciones se quedan pegadas abajo; el `padding-bottom`
   del formulario es 0 y lo pone la propia barra, para que no quede una franja de contenido
   asomándose por debajo de ella. */
/* El ancho del modal lo decide el cascarón (.dialogo de arriba): el form solo lo llena. Sin
   max-height propio: como item flex de #modal-cuerpo se encoge al espacio disponible
   (min-height 0) y scrollea él solo -- la barra sticky de .acciones queda siempre a la vista. */
.modal-form { width: 100%; display: grid; gap: var(--s-4); padding: var(--s-5) var(--s-5) 0; min-height: 0; overflow: auto; }
.modal-form .acciones { display: flex; justify-content: flex-end; gap: var(--s-3); position: sticky; bottom: 0; margin: 0 calc(var(--s-5) * -1); padding: var(--s-3) var(--s-5) var(--s-5); background: var(--surface); border-top: 1px solid var(--border); }
@media (max-width: 480px) {
  .modal-form .acciones { flex-direction: column-reverse; }
  .modal-form .acciones .btn { width: 100%; }
}

/* ---------- Picker de mapa (Leaflet + geocodificador por proxy) ---------- */
/* `isolation: isolate` (H28b): Leaflet pone sus paneles en z-index 400. Sin esto compiten con el resto de la página y el
   mapa se pintaba ENCIMA de la barra pegajosa Guardar/Cancelar de los modales (a 390) y de la lista de sugerencias de
   dirección; así los 400 quedan encerrados en el mapa. */
.mapa-lienzo { height: 320px; border-radius: var(--r-md); border: 1px solid var(--border-strong); background: var(--surface-2); isolation: isolate; }
/* auto-fit (Joe, 2026-08-31): con el slot del radio son 3 cajas parejas al tercio; sin él,
   lat/lon a la mitad. En angosto caen solas a una columna -- ya sin media query. */
.mapa-coords { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--s-3); margin-top: var(--s-3); }
/* `.hint` solo vestía dentro de un `.campo` (componentes.css): el picker (la línea del mapa) y la sección "otro lugar" de
   `/r/` lo usan fuera y salía a 15 px y color de cuerpo. Mismas declaraciones, selectores acotados: sin regla global. */
.mapa-picker .hint, .terminos .hint { color: var(--muted-fg); font-size: var(--t-xs); }

/* Sugerencias del geocodificador: listbox flotante bajo el campo de dirección */
.sugerencias { position: relative; }
.sugerencias ul { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; right: 0; margin: 0; padding: var(--s-1); list-style: none; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--sh-card-h); max-height: 260px; overflow: auto; }
.sugerencias li { padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); cursor: pointer; font-size: var(--t-sm); }
.sugerencias li:hover, .sugerencias li[aria-selected="true"] { background: var(--primary-bg); color: var(--primary); }

/* ---------- Píldoras de filtro ---------- */
.pildoras { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.pildora { display: inline-flex; align-items: center; gap: var(--s-1); min-height: 44px; padding: 0 var(--s-3); border-radius: var(--r-pill); border: 1px solid var(--border-strong); background: var(--surface); color: var(--fg); font-size: var(--t-sm); font-weight: 500; cursor: pointer; }
.pildora[aria-pressed="true"] { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); }
/* Cubre aria-current="true" (filtros) Y "date" (el día activo de la agenda, revisión final H3
   I4): son valores ARIA distintos para el mismo "esto es lo activo", con el mismo estilo. */
.pildora[aria-current]:not([aria-current="false"]) { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); }
/* Píldora "Solicitudes" (H14): mismos tokens --info que .chip-info/.aviso.info -- pierde contra
   [aria-current] cuando está seleccionada (toda píldora activa se ve igual, sin importar su
   categoría; la especificidad del selector de arriba ya gana sobre esta sola clase). */
.pildora-info { color: var(--info); background: var(--info-bg); border-color: var(--info-border); }
/* Botón-píldora "Registrar causa" (H15 T3, R9): mismo trato de color que `.chip-warn`, pero
   clicable -- el rojo sin causa es una alerta a resolver, no un filtro seleccionado. */
.pildora-warn { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); }

/* ---------- Acciones mini alineadas a la derecha dentro de una fila de tabla ---------- */
.tabla .acciones { display: flex; justify-content: flex-end; gap: var(--s-2); }

/* ---------- Buscador (input con icono) ---------- */
.buscador { position: relative; }
.buscador input { font: inherit; width: 100%; min-height: 44px; padding: 0 var(--s-3) 0 var(--s-8); border-radius: var(--r-md); border: 1px solid var(--border-strong); background: var(--surface); color: var(--fg); }
.buscador input:focus { outline: none; border-color: var(--ring); box-shadow: var(--ring-shadow); }
.buscador::before { content: "🔍"; position: absolute; left: var(--s-3); top: 50%; transform: translateY(-50%); font-size: var(--t-sm); opacity: .6; pointer-events: none; }

/* ---------- Paginación ---------- */
.paginacion { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; font-size: var(--t-sm); color: var(--muted-fg); }
.paginacion .botones { display: flex; gap: var(--s-2); }

/* ---------- Toasts ---------- */
.toasts { position: fixed; right: var(--s-4); bottom: calc(var(--s-4) + env(safe-area-inset-bottom) / var(--zoom)); display: grid; gap: var(--s-2); z-index: 50; max-width: min(100% - 2 * var(--s-4), 380px); }
.toast { padding: var(--s-3) var(--s-4); border-radius: var(--r-md); background: var(--fg); color: var(--bg); box-shadow: var(--sh-card-h); font-size: var(--t-sm); }
.htmx-indicator { opacity: 0; transition: opacity var(--dur-rapido) var(--ease); }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }

/* ---------- Píldora de filtro con casilla real (H1 T4) ----------
   La casilla es un <input> de verdad (se manda en el formulario del buscador y la lee el
   lector de pantalla); la píldora es su etiqueta. `:has()` la pinta cuando está prendida. */
.pildora input[type="checkbox"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.pildora:has(input:checked) { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); }
.pildora:focus-within { outline: 2px solid var(--ring); outline-offset: 2px; }

/* ---------- Barra de filtros de una lista ---------- */
.filtros { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.filtros .buscador { flex: 1 1 240px; min-width: 0; }

/* Filtros de Reportes (H9 T2, R5): 6 campos (periodo + cliente/sucursal/proveedor/técnico) más
   los botones -- una rejilla que se acomoda sola en vez de un `.filtros` de una sola fila, que a
   este ancho de campos se hubiera visto apretado en cualquier tamaño intermedio (tablet). */
.filtros-reportes { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--s-3); align-items: end; }
.filtros-reportes-acciones { justify-content: end; }
.filtros-reportes-acciones .fila { gap: var(--s-2); }

/* ---------- Secciones de una ficha (cliente, proveedor, orden) ---------- */
.secciones { gap: var(--s-8); }
.seccion { display: grid; gap: var(--s-4); min-width: 0; }
.cabecera-seccion { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; border-bottom: 1px solid var(--border); padding-bottom: var(--s-2); }

/* Rejilla de tarjetas que se acomoda sola (sucursales) */
.rejilla { display: grid; gap: var(--s-4); grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); }

/* Lista de renglones con acciones a la derecha (responsables, puntos del checklist) */
.lista-plana { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); min-width: 0; }
.lista-plana > li { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; padding-bottom: var(--s-3); border-bottom: 1px solid var(--border); }
.lista-plana > li:last-child { border-bottom: 0; padding-bottom: 0; }
/* Pasada con detalle de campo (H4): el <li> trae dos hijos (resumen + <details>) y el flex
   space-between los ponía lado a lado en tablet (cierre H4, Playwright 820); en bloque se apilan. */
.lista-plana > li.apilada { display: block; }
.lista-plana > li.apilada > .fila { margin-bottom: var(--s-2); }

/* Desglose de conceptos bajo una visita (H27b, D240): reusa `.fila` para nombre + monto, pero sin
   la carcasa de `.lista-plana li` (border/padding), para que no se lea como otra tabla.
   `:not(.lista-plana)` en la segunda regla (revision ronda 1): `campo/_conceptos.html` combina
   AMBAS clases (`class="lista-plana lista-conceptos"`) para heredar el `display:grid` + separadores
   de `.lista-plana` -- sin el `:not()`, `.lista-conceptos li` empataba en especificidad con
   `.lista-plana > li` y, por ir despues en el archivo, ganaba en TODAS las propiedades compartidas
   (display/padding/border), borrando los separadores de fila de la tarjeta de campo en silencio. */
.lista-conceptos { list-style: none; margin: 0; padding: 0; }
.lista-conceptos:not(.lista-plana) li { display: block; padding: 0; border: 0; }
.lista-conceptos .fila { justify-content: space-between; flex-wrap: nowrap; width: 100%; }
/* El monto nunca se parte del nombre (Playwright 1440: en la celda Tipo de la liquidacion el
   `.fila` heredado con wrap ponia "Revision" en una linea y "$550.00" en la siguiente). */
.lista-conceptos .fila .mono { white-space: nowrap; }

/* Datos de una ficha en dos columnas; a 640 se apilan */
.datos { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-2) var(--s-4); margin: 0; }
.datos dt { font-size: var(--t-sm); color: var(--muted-fg); font-weight: 600; }
.datos dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 640px) { .datos { grid-template-columns: minmax(0, 1fr); gap: var(--s-1) 0; } .datos dd { margin-bottom: var(--s-2); } }

/* ---------- Filas repetibles de un formulario (editor de checklist) ---------- */
.filas { display: grid; gap: var(--s-3); min-width: 0; }
.fila-editable { display: grid; gap: var(--s-2); padding: var(--s-3); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2); min-width: 0; }

/* ---------- Píldora de filtro de opción única (H1 T5: tipo de proveedor) ----------
   Mismo trato que la píldora con casilla: el <input> real se manda en el formulario del buscador
   y lo lee el lector de pantalla; la píldora es su etiqueta y `:has(input:checked)` la pinta. */
.pildora input[type="radio"] { position: absolute; width: 1px; height: 1px; opacity: 0; }
/* Un placeholder tampoco es un dato: dentro de una celda numérica se queda en la tipografía
   de UI, igual que el rótulo de su cabecera (`th.num`). */
.tabla .num .muted { font-family: var(--font-ui); }
/* Una fila de tabla con varias acciones (técnicos: Editar, permiso de costo, Desactivar): en
   escritorio envuelven en vez de desbordar, y en modo tarjeta (≤640) se llevan su propio renglón
   completo alineadas a la izquierda, para que ningún botón quede fuera de la pantalla. */
.tabla .acciones { flex-wrap: wrap; }
@media (max-width: 640px) {
  .tabla td.acciones { grid-column: 1 / -1; justify-content: flex-start; margin-top: var(--s-2); }
}

/* ---------- Configuración (H1 T6) ----------
   Tarjeta que ES un enlace (índice de Configuración): hereda el color del texto para que el
   párrafo no se pinte de acento, y el título carga el color de marca. Sin subrayado: el borde y
   la sombra ya dicen que es un destino. */
.tarjeta-enlace { display: grid; gap: var(--s-2); color: var(--fg); text-decoration: none; }
.tarjeta-enlace h2 { color: var(--primary); }
.tarjeta-enlace:hover { border-color: var(--border-strong); }
/* Texto largo capturado por una persona (términos y condiciones): respeta sus saltos de línea
   y parte las palabras kilométricas en vez de estirar la tarjeta. */
.texto-largo { white-space: pre-wrap; overflow-wrap: anywhere; }
/* Dato que puede venir sin espacios y más ancho que su columna (un correo, un error de SMTP). */
.rompe { overflow-wrap: anywhere; }
/* El historial de términos y el resto de bloques plegables: el resumen es el área táctil. */
details > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 600; }
details[open] > summary { margin-bottom: var(--s-3); }
/* Acciones de un renglón: el flex vive en un div DENTRO de la celda, no en la celda misma. Una
   celda con `display: flex` deja de ser celda de tabla y se encoge a lo que miden los botones:
   con un renglón alto (nombre, usuario y correo) su borde inferior queda 16 px arriba del resto y
   la línea de la fila se ve cortada. Con el div, la celda sigue siendo celda. */
.tabla td.acciones-celda { vertical-align: middle; }
.acciones-fila { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s-2); }
/* Una tabla con varias acciones por renglón se vuelve tarjetas ANTES que las demás: a 820 px
   (tablet) seis columnas más tres botones dejan cada botón en su propio renglón dentro de una
   celda de 120 px. Son las mismas reglas que `.tabla` aplica a 640, adelantadas a 900. */
@media (max-width: 900px) {
  .tabla-densa thead { display: none; }
  .tabla-densa, .tabla-densa tbody { display: block; width: 100%; }
  .tabla-densa tr { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-1) var(--s-4); align-items: center; border: 1px solid var(--border); border-radius: var(--r-md); margin-bottom: var(--s-2); background: var(--surface); padding: var(--s-3) var(--s-4); }
  .tabla-densa td { display: block; border: 0; padding: 0; min-width: 0; }
  /* Dos columnas que SÍ se encogen (`minmax(0, 1fr)`) en vez de las `auto` de `.tabla`: una
     celda ancha (tres chips de estado) dejaba la columna de al lado en cero y su texto se
     encimaba con el de la vecina. Con dos pistas iguales, el contenido envuelve. */
  .tabla-densa td:first-child { grid-column: 1 / -1; }
  /* Ver eliminados de Configuración, Usuarios: "Eliminado" lleva el motivo (texto libre, a veces una URL) y en media
     pista la tarjeta lo apretaba a ~150 px. Acotado a esa tabla (`.tabla-eliminados`): la de equipos no cambia. */
  .tabla-eliminados td[data-col="Eliminado"] { grid-column: 1 / -1; }
  .tabla-densa td[data-col]:not(:first-child)::before { content: attr(data-col); display: block; font-size: var(--t-xs); color: var(--muted-fg); font-family: var(--font-ui); }
  .tabla-densa .num { text-align: left; }
  .tabla-densa td.acciones-celda { grid-column: 1 / -1; margin-top: var(--s-2); }
  /* Un <input> tiene ancho intrinseco (~20ch) y no se encoge con `minmax(0,1fr)`: en el modal
     Conceptos a 390 el Monto se salia del modal (Playwright H27). Dentro de la tarjeta ocupa su pista. */
  .tabla-densa td input:not([type=checkbox]):not([type=radio]) { width: 100%; max-width: 100%; }
  .tabla-densa .acciones-fila { justify-content: flex-start; }
}

/* Portal y enlace: cita, progreso, términos, confirmación (Diseño §8) */
.cita { display: grid; grid-template-columns: auto 1fr; gap: var(--s-5); align-items: center; padding: var(--s-5); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); margin-bottom: var(--s-4); }
.cita .datos { display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; min-width: 0; }  /* pisa la rejilla de 2 columnas de `.datos` (dl) */
.cita .datos h2 { font-size: var(--t-lg); }
/* H30b: el bloque Espera del proveedor vive dentro de `.cita .datos`, que es una sola columna: su `dl` vuelve a la
   rejilla de dos columnas de `.datos` y a 640 se apila (Diseño de la Tanda 3, §6). Clase propia y no `.datos`: la
   regla de arriba la aplanaría. */
.cita dl.espera { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-1) var(--s-4); margin: 0; }
.cita dl.espera dt { font-size: var(--t-sm); color: var(--muted-fg); font-weight: 600; }
.cita dl.espera dd { margin: 0; min-width: 0; }
@media (max-width: 640px) { .cita dl.espera { grid-template-columns: minmax(0, 1fr); gap: 0; } .cita dl.espera dd { margin-bottom: var(--s-2); } }
.progreso { display: flex; align-items: center; gap: var(--s-3); font-size: var(--t-sm); color: var(--muted-fg); margin: var(--s-4) 0; }
.progreso .pista { flex: 1; height: 6px; border-radius: var(--r-pill); background: var(--surface-2); overflow: hidden; }
.progreso .avance { display: block; height: 100%; background: var(--primary); width: 0; transition: width .2s; }
/* Selectores compuestos (`.progreso .avance-N`), no `.avance-N` a secas: la especificidad de un
   selector con dos clases (0,2,0) SIEMPRE gana sobre una sola clase (0,1,0), así que la anchura
   de `.progreso .avance` (width: 0) le ganaba a `.avance-50` cuando competían por especificidad
   igual en el orden del archivo. Con el ancestro `.progreso` en los dos lados, gana el ancho real. */
.progreso .avance-0 { width: 0%; }
.progreso .avance-10 { width: 10%; } .progreso .avance-20 { width: 20%; } .progreso .avance-30 { width: 30%; } .progreso .avance-40 { width: 40%; } .progreso .avance-50 { width: 50%; }
.progreso .avance-60 { width: 60%; } .progreso .avance-70 { width: 70%; } .progreso .avance-80 { width: 80%; } .progreso .avance-90 { width: 90%; } .progreso .avance-100 { width: 100%; }
.lista { display: grid; gap: var(--s-2); }
.terminos { padding: var(--s-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); margin-top: var(--s-4); }
.terminos summary { cursor: pointer; font-weight: 600; min-height: 44px; display: flex; align-items: center; }
.terminos .texto { max-width: 65ch; color: var(--muted-fg); font-size: var(--t-sm); margin-top: var(--s-3); }
.confirmar { display: grid; gap: var(--s-3); padding: var(--s-5); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); margin-top: var(--s-4); }
.firma-legal { font-size: var(--t-xs); color: var(--muted-fg); max-width: 65ch; }
@media (max-width: 640px) { .cita { grid-template-columns: minmax(0, 1fr); } }

/* Pasos del modal de orden (Diseño §9, "Pasos") */
.pasos { list-style: none; margin: 0 0 var(--s-4); padding: 0 var(--s-5); display: flex; gap: var(--s-4); font-size: var(--t-sm); color: var(--muted-fg); flex-wrap: wrap; }
.pasos li { display: flex; align-items: center; gap: var(--s-2); }
.pasos .n { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: var(--surface-2); font-family: var(--font-mono); font-size: var(--t-xs); }
.pasos li[aria-current="step"] { color: var(--primary); font-weight: 600; }
/* Tinta del número sobre el círculo teal: `var(--bg)`, NO un `#fff` fijo (barrido H18, B4).
   En oscuro `--primary` se voltea a un teal claro (#6CC2BC) y el blanco encima daba 2.08:1;
   con el token del fondo son 5.6:1 en claro y 8.4:1 en oscuro, sin un override por tema. */
.pasos li[aria-current="step"] .n { background: var(--primary); color: var(--bg); }
.vehiculos-editor { border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-3); display: grid; gap: var(--s-2); }
.vehiculos-editor legend { font-weight: 600; padding: 0 var(--s-1); }
/* Tarjeta de unidad v2 (H13 T4, Diseño §3): mismo tratamiento que `.fila-editable` (borde, fondo
   -2, radio) pero con sus propios renglones internos (`.fila`) en vez de una sola fila de columnas
   fijas -- reemplaza las reglas v1 `.vehiculos-editor .fila-editable` (ya sin uso: la tarjeta v2
   no anida `.fila-editable`). `.fila-campos` crece parejo los `.campo` de un renglón (unidad Y
   paso 2 de plantilla); el resto de `.fila` ya resuelve el wrap responsivo. */
.unidad-tarjeta { display: grid; gap: var(--s-3); padding: var(--s-4); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2); min-width: 0; }
.fila-campos > .campo { flex: 1 1 180px; }
/* Un renglón de campos alinea por ARRIBA (fix 2026-08-31, cazado por Joe con el modal ya al 80%:
   el `align-items: center` de .fila dejaba a Placa, al checkbox y al NIV cada quien a su altura
   -- el hint del NIV y el checkbox sin etiqueta descuadran los centros). La .opcion baja el alto
   de una etiqueta de .campo (t-sm por lh-base más el gap) para quedar hombro a hombro con los
   inputs de 44px. */
.fila-campos { align-items: flex-start; }
.fila-campos > .opcion { margin-top: calc(var(--t-sm) * var(--lh-base) + var(--s-1)); }
.aviso-propulsion > div { display: grid; gap: var(--s-2); }
/* Compacto (Joe, 2026-08-31: "el mensaje está enorme"): es un aviso de una vez, no un contrato --
   texto xs acotado a 65ch y el checkbox al tamaño de .opcion (20px), no los 28px del check de
   contratos. El renglón del "Entiendo" conserva sus 44px de área táctil. */
.aviso-propulsion { font-size: var(--t-xs); }
.aviso-propulsion p { max-width: 65ch; margin: 0; }
.aviso-propulsion .contrato-check { font-size: var(--t-sm); }
.aviso-propulsion .contrato-check input { width: 20px; height: 20px; }
.folio { font-family: var(--font-mono); font-size: var(--t-xl); font-weight: 500; letter-spacing: .02em; }

/* Detalle de orden: tarjetas (cita, sucursal, vehículos, checklist) a la izquierda, bitácora y
   envíos a la derecha; se apila con el resto de la carcasa (misma quiebra que `.shell`). */
.grid2 { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: var(--s-4); align-items: start; }
@media (max-width: 1024px) { .grid2 { grid-template-columns: minmax(0, 1fr); } }

/* Visita activa del detalle de la orden (H3): tarjeta con proveedor/técnico/costo y sus manos. */
.visita-activa h2 { display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-lg); }
.manos { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border); }
.manos h3 { font-size: var(--t-sm); color: var(--muted-fg); margin: 0 0 var(--s-2); }

/* Carcasa `/proveedor`, visitas (H3 T5): la hora grande de una tarjeta de visita fuera del `.sello`
   con grid (la lista las apila una tras otra, no en la rejilla de dos columnas de la cita). */
.hora-grande { display: block; font-family: var(--font-mono); font-weight: 500; font-size: var(--t-hora);
  line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.tarjeta-visita { display: flex; align-items: center; gap: var(--s-4); }
.tarjeta-visita .cuando { min-width: 84px; text-align: right; line-height: 1.2; }
/* `grid-template-columns` explícito (fix 2026-08-31, cazado por Joe en /proveedor): el `.datos`
   genérico de los <dl> (rejilla de DOS columnas, max-content|1fr) se colaba aquí -- esta regla
   pisaba display/gap pero no las columnas, y la tarjeta pintaba título+chip en la col 1 con el
   tipo brincado a la col 2 y el pago descolgado. Mismo remedio que ya usaba `.cita .datos`. */
.tarjeta-visita .datos { flex: 1; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-1); }
/* "Gestacore te paga: $X" (D25, la tarjeta y el detalle SIEMPRE la enseñan): revisión final H3
   M5, el dinero no es un estado de éxito, así que se saca de la familia `--ok` (ya la usa
   `chip-ok` para "Confirmada", a 2 cm de distancia en la misma tarjeta) y pasa a teal
   (`--primary`), la familia de "esto es OnSite" (§3). La cifra va en `.mono` (I3, §4: el mono es
   para el dato, no para la etiqueta), envuelta en la propia plantilla. */
.pago-etiqueta { display: inline-flex; align-items: center; gap: 6px; min-height: 26px; padding: 0 10px;
  border-radius: var(--r-sm); font-size: var(--t-sm); font-weight: 700; color: var(--primary);
  background: var(--primary-bg); border: 1px solid color-mix(in srgb, var(--primary) 40%, transparent);
  width: fit-content; }
@media (max-width: 640px) { .tarjeta-visita { flex-wrap: wrap; } .tarjeta-visita .cuando { text-align: left; } }
/* Carcasa `/campo` (H3 T6): "Pendientes de aceptar" resalta con borde ámbar, misma familia de
   tokens que `.chip-warn` (RN: es a lo que el técnico tiene que responder primero). */
.tarjeta-visita.pendiente { border-color: var(--warn-border); }
/* Volver al detalle desde "Mis visitas" (topbar de campo, mismo trato que el molde del mockup). */
.topbar .atras { min-height: 44px; display: inline-flex; align-items: center; gap: 6px; color: var(--primary); font-weight: 600; text-decoration: none; }
/* "Cómo llegar" / "Llamar": mismo ancho, uno junto al otro; envuelve y se encoge (M8) para que
   una etiqueta más larga (o una sucursal con nombre largo en el botón) no desborde en horizontal. */
.fila-contacto { display: flex; gap: var(--s-3); flex-wrap: wrap; }
.fila-contacto > * { flex: 1; min-width: 0; }

/* ---------- Temporizador de espera (H4 T8, Diseño §5 y §8) ----------
   Único elemento que se mueve solo (Diseño §6, "movimiento: 2"): la cuenta regresiva y la barra
   que se vacía. `campo-sitio.mjs` la corrige con la hora del servidor y pinta `--progreso` con
   `el.style.setProperty` (JS, no plantilla: el guardián solo revisa `style=` en HTML). */
.temporizador { padding: var(--s-4); border-radius: var(--r-md); background: var(--surface); border: 1px solid var(--border); display: grid; gap: var(--s-2); }
.temporizador .resta { font-family: var(--font-mono); font-size: var(--t-2xl); font-weight: 500; font-variant-numeric: tabular-nums; line-height: 1; }
.barra-espera { --progreso: 0%; height: 8px; border-radius: var(--r-pill); background: var(--surface-2); overflow: hidden; }
.barra-espera span { display: block; height: 100%; width: var(--progreso); background: var(--primary); transition: width var(--dur-lento) var(--ease); }
@media (prefers-reduced-motion: reduce) { .barra-espera span { transition: none; } }
/* Tanda 3 (R1/R2, Diseño §1 "ámbar = esperó de más"): vencida, la barra queda llena en `warn`; con el
   servicio iniciado el reloj se cambia por el sello y, si aplica, la línea de los minutos de más. */
.temporizador[data-vencida] .barra-espera span { background: var(--warn); }
.temporizador .sello-inicio { font-size: var(--t-lg); font-weight: 600; }
.temporizador .de-mas { color: var(--warn); }

/* Tarjeta de "No se hizo" (ida en falso, RN12): el mismo canto rayado que `.kpi-falso`. */
.tarjeta-falso { position: relative; overflow: hidden; border-color: var(--falso-border); }
.tarjeta-falso::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 8px; background: var(--rayado); }
.tarjeta-falso { padding-left: calc(var(--s-4) + 8px); }

/* Tarjeta de "Te están pasando N equipos" (H5 T2, campo/stock): acento teal a la izquierda, sin
   recurrir al rayado (ese es solo para "ida en falso"). Padding compensado, mismo criterio que
   `.tarjeta-falso`: sin él el borde le come 4px al padding normal de `.tarjeta` de ese lado. */
.tarjeta-teal { border-left: 4px solid var(--primary); padding-left: calc(var(--s-4) + 4px); }

/* Código de traspaso de 6 dígitos, grande y en mono (H5 T2, campo/stock/codigo). */
.codigo-grande { font-family: var(--font-mono); font-size: var(--t-hora); font-weight: 600; letter-spacing: .2em; text-align: center; font-variant-numeric: tabular-nums; }

/* ---------- Lista de unidades de una visita en sitio (H4 T8, Diseño §8) ---------- */
.unidades { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.unidades > li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px var(--s-3); align-items: center; padding: var(--s-3) var(--s-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); min-height: 56px; }
.unidades .placa { grid-column: 1; grid-row: 1; font-size: var(--t-md); }
.unidades .chip { grid-column: 2; grid-row: 1 / span 2; justify-self: end; }
.unidades .modelo { grid-column: 1; grid-row: 2; }
.unidades .miniaturas { grid-column: 1 / -1; display: flex; gap: var(--s-1); margin-top: var(--s-1); }
.unidades .miniaturas img { width: 40px; height: 40px; border-radius: var(--r-sm); object-fit: cover; border: 1px solid var(--border); }
.unidades .acciones-linea { grid-column: 1 / -1; margin-top: var(--s-1); }

/* ---------- Galería de evidencia (panel/proveedor), H4 T9 ----------
   Miniaturas/`<video>` más grandes que las de `.unidades .miniaturas` (esas son de campo, de un
   vistazo rápido en el celular; aquí Gestacore/el proveedor revisan la instalación con calma). */
.galeria { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-2); }
.evidencia { display: grid; gap: var(--s-1); width: 128px; }
.evidencia img, .evidencia video { width: 128px; height: 96px; border-radius: var(--r-md); object-fit: cover; border: 1px solid var(--border); background: var(--surface-2); }
.evidencia .btn { width: 100%; }

/* Form "Agregar evidencia" (multipart por línea, H4 T9): mismo `.campo`/`.btn` de siempre, solo se
   apila y se acota de ancho para no estirarse a lo tonto dentro de la tarjeta de la unidad.
   R9 (H9 T7): igual que `.pila`, sin `grid-template-columns` la columna implícita es "auto" y
   el `<input type="file">` (ancho mínimo nativo grande) se la salta -- desbordó orden-detalle
   a 390px pese al `max-width:320px` de la caja (el max-width topa la CAJA, no la columna de
   adentro). `minmax(0, 1fr)` topa también la columna. */
.form-evidencia { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-2); max-width: 320px; margin-top: var(--s-2); }

/* Chips de marcas de GPS de la llegada (SOLO panel, R2/D63): envuelven en su propia fila, no
   pegados al texto de la hora de arriba. */
.marca-gps { display: flex; flex-wrap: wrap; gap: var(--s-1); }

/* ---------- Línea de tiempo del portal (usted), H4 T9 ----------
   Mismo lenguaje visual que `.bitacora` (punto + línea conectora), pero sin el detalle interno de
   quién/canal: el portal cuenta la visita como una historia de tres o cuatro hechos, no un log. */
.linea-tiempo { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.linea-tiempo li { display: grid; grid-template-columns: 20px 1fr; gap: 0 var(--s-3); padding-bottom: var(--s-5); position: relative; }
.linea-tiempo .nodo { position: relative; }
.linea-tiempo .nodo::before { content: ""; position: absolute; left: 9px; top: 18px; bottom: -4px; width: 2px; background: var(--primary); }
.linea-tiempo li:last-child .nodo::before { display: none; }
.linea-tiempo .nodo::after { content: ""; position: absolute; left: 4px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 0 3px var(--surface); }
.linea-tiempo .cuando { font-size: var(--t-xs); color: var(--muted-fg); }
.linea-tiempo .que { font-weight: 600; }
.linea-tiempo img { max-width: 240px; border-radius: var(--r-md); border: 1px solid var(--border); margin-top: var(--s-2); display: block; }
/* Ola final de H30a (barrido B, menor 1): cada renglón del cuerpo de un hito lleva el aire de la casa, el mismo de la foto
   y del marco de la firma. "Firmó: …" iba pegado a la foto y "Descargar constancia" a su línea de arriba (0 px). */
.linea-tiempo li > div > p { margin-top: var(--s-2); }

/* ---------- Firma sobre papel (H30a, B1/P1/P2) ----------
   La firma es un PNG de trazo sobre fondo transparente y el color del trazo depende de CUÁNDO se guardó: las que
   hay en producción traen el crema del tema oscuro del celular y las nuevas, la tinta oscura #232222
   (`campo-firma.mjs`). Crudas, una de las dos desaparece sobre un fondo claro o sobre uno oscuro, y abiertas solas
   en una pestaña el visor les pone SU fondo (Chrome, un gris claro: ahí se pierde la crema, medido) y no hay CSS
   que las rescate. Por eso el SERVIDOR las entrega ya sobre papel: un PNG opaco, la tinta oscura sobre `PAPEL` (`evidencia/firma.py::png_sobre_papel`, servido desde
   `web/routes/comunes.py::respuesta_archivo`), igual en los dos temas y en la pestaña suelta.
   `.firma-papel` es solo el MARCO de esa imagen en la página: el mismo papel (`--papel-firma`, base.css; el mismo
   color que `PAPEL` y que el papel del PDF, vigilado en `tests/test_evidencia_firma.py`) con su padding, su borde y
   su radio. SIN `filter`: sobre la imagen ya opaca un `brightness(0)` la volvería negra entera (y antes, con el
   papel de fondo en el mismo `<img>` del filtro, salía un rectángulo negro: medido en Chromium).
   Estas reglas van DESPUÉS de `.evidencia img` y `.linea-tiempo img` (misma especificidad: gana la que cae después)
   porque desarman su recorte, su borde y su fondo. La tarjeta de la galería se ensancha (`.evidencia-firma`): a
   128px el trazo de 2.5px de la firma quedaría en medio píxel. `tests/test_firma_a_la_vista.py` vigila todo esto. */
.firma-papel { display: block; width: 100%; max-width: 280px; padding: var(--s-2) var(--s-3); background: var(--papel-firma); border: 1px solid var(--border); border-radius: var(--r-md); }
.firma-papel img { display: block; width: 100%; max-width: none; height: auto; max-height: 120px; margin: 0; border: 0; border-radius: 0; background: none; object-fit: contain; }
.linea-tiempo .firma-papel { margin: var(--s-2) 0; }
.evidencia.evidencia-firma { width: 280px; max-width: 100%; }

/* ---------- Ranura de captura (foto/video/no_se_hizo), H4 T8 ----------
   `<label>` envuelve el `input[type=file]` (oculto: la ranura completa es el área táctil). */
.ranura { display: flex; align-items: center; gap: var(--s-3); min-height: 64px; padding: var(--s-2) var(--s-3); border: 1px dashed var(--border-strong); border-radius: var(--r-md); background: var(--surface); cursor: pointer; }
.ranura:hover { border-color: var(--primary); }
.ranura-vista { width: 48px; height: 48px; border-radius: var(--r-sm); background: var(--surface-2); display: grid; place-items: center; overflow: hidden; flex-shrink: 0; }
.ranura-vista img { width: 100%; height: 100%; object-fit: cover; }
.ranura-icono { font-size: var(--t-lg); }
.ranura-texto { display: grid; gap: 2px; min-width: 0; }
.ranura-etiqueta { font-size: var(--t-sm); font-weight: 600; }
.ranura-estado-texto { font-size: var(--t-xs); color: var(--muted-fg); }
.ranura[data-estado="listo"] { border-style: solid; border-color: var(--ok-border); }
.ranura[data-estado="rechazado"] { border-style: solid; border-color: var(--bad-border); }
.ranura[data-estado="rechazado"] .ranura-estado-texto { color: var(--bad); }
/* Ranura sin `<input>` (H4 T8, revisión ronda 1, Importante #3: solo lectura o ya tiene una
   evidencia aceptada): sin el cursor de "aquí se toca" ni el hover que invita a intentarlo. */
.ranura-bloqueada { cursor: default; }
.ranura-bloqueada:hover { border-color: var(--border-strong); }
/* Fix ronda 1 de la Task 4: lo que no cupo del lote, dicho DENTRO del diálogo de "No se hizo" (el toast de la
   página queda debajo del fondo del modal). Vacío no ocupa nada; con texto, aire arriba y el color de aviso. */
.aviso-lote { color: var(--warn); font-size: var(--t-sm); line-height: var(--lh-sm); }
.aviso-lote:not(:empty) { margin-top: var(--s-2); }
/* Ola final de H30a (#13, barrido A, fila C10): las 5 ranuras de la ida en falso (el diálogo y la tarjeta ya
   registrada) en columnas iguales: dos por fila a 390 y tres a 820. En `.fila` cada una medía lo de su texto. Las de
   un renglón, del mismo alto (`stretch`): la de "Lista para enviar" ocupa dos líneas y las demás se veían corridas. */
.fila[data-lote] { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 140px), 1fr)); align-items: stretch; }

/* Contador "Fotos: N de mínimo M" (H16 T1, R1; M = `reglas.MINIMO_FOTOS`): el rebote SOLO se dispara al intentar cerrar sin
   el mínimo (campo-unidad.mjs, sobre `campo:accion-error` de `guardar_unidad`) -- se retira solo
   al terminar la animación, así puede repetirse en un segundo intento fallido. */
.fotos-contador { margin: var(--s-2) 0 0; }
.fotos-contador.rebote { color: var(--bad); animation: rebote .4s ease; }
@keyframes rebote {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(3px); }
}

/* ---------- Zona de arrastrar y soltar (Importar CSV, H5 T6) ----------
   `<label>` envuelve el `input[type=file]` (oculto: la zona completa es el área táctil, igual
   criterio que `.ranura`). SIN JS sigue funcionando: tocar la etiqueta abre el selector nativo
   del navegador -- es comportamiento HTML puro, no depende de `panel-importar.mjs`. */
.zona-soltar { display: flex; flex-direction: column; align-items: center; gap: var(--s-2); min-height: 96px; padding: var(--s-5); border: 2px dashed var(--border-strong); border-radius: var(--r-md); background: var(--surface); cursor: pointer; text-align: center; }
.zona-soltar:hover { border-color: var(--primary); }
.zona-soltar.arrastrando { border-color: var(--primary); background: var(--primary-bg); }
.zona-soltar-texto { font-weight: 600; }
.zona-soltar .hint { color: var(--muted-fg); font-size: var(--t-xs); }  /* nombre del archivo elegido; `.hint` normal solo aplica dentro de `.campo` */

/* ---------- Pasos con contenido (Documentar unidad, H4 T8) ----------
   Reusa `.pasos` (la numeración) pero apilada: aquí cada paso ES una sección del formulario, a
   diferencia del breadcrumb chico del modal de nueva orden. */
.pasos-unidad { flex-direction: column; align-items: stretch; padding: 0; gap: var(--s-4); counter-reset: paso; }
.pasos-unidad > li { flex-direction: column; align-items: stretch; gap: var(--s-3); padding: var(--s-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); counter-increment: paso; }
/* Revisión ronda 1, Menor #11: el número lo cuenta el CSS (`counter-increment` en cada `<li>`
   REALMENTE en el DOM), no un `{{ '3' if requiere_equipo else '1' }}` a mano por paso -- cuando el
   tipo no requiere equipo y Jinja omite los pasos de IMEI/ICC, el conteo se recorre solo porque ya
   no hay esos dos `<li>` que contar, sin tocar los demás pasos. */
.pasos-unidad > li .paso-cab .n::before { content: counter(paso); }
.paso-cab { display: flex; align-items: center; gap: var(--s-2); font-weight: 600; color: var(--fg); }
.paso-cuerpo { display: grid; gap: var(--s-2); }
.paso-cuerpo.fila { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* ---------- Firma en canvas (Cerrar visita, H4 T8) ---------- */
canvas[data-firma] { width: 100%; max-width: 600px; aspect-ratio: 600 / 240; touch-action: none; border: 1px solid var(--border-strong); border-radius: var(--r-md); background: var(--surface); }
/* Tanda 3 (Diseño §4): lo que firma el cliente en la ida en falso, en grande y en usted. */
.firma-declaracion { font-size: var(--t-md); line-height: var(--lh-md); }

/* Versión de la app en el pie del nav del panel (discreta, alineada con perfil/salir) */
.pie .version { display: block; margin-top: var(--s-1); font-size: var(--t-xs); color: var(--muted-fg); }

/* Documentos de la visita: cada uno como bloque, con los botones en su propia fila abajo
   (antes iban en una columna estrecha de tabla y se apilaban feos; Joe, 2026-08-25). */
.lista-docs { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.lista-docs .doc { display: grid; gap: var(--s-2); padding: var(--s-3); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); }
.lista-docs .doc-datos { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1) var(--s-3); }
.lista-docs .doc-acciones { flex-wrap: wrap; gap: var(--s-2); }

/* Fila de evidencia ZIP (H16 T3, R13): pieza compartida portal+panel, mismo lenguaje visual que
   `.lista-docs .doc` -- borde+radio+superficie, texto a la izquierda y el botón a la derecha.
   `.fila-atenuada` (ya existe, H11 T5) pinta el estado "candado" (caducó/se invalidó). */
.fila-zip { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-3); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); }
.fila-zip p { margin: 0; }

/* Detalle de orden balanceado (Joe, 2026-08-25: "no puede ser algo balanceado?"). Las tarjetas
   chicas de metadata (cita, sucursal, vehiculos, relojes, cobro) fluyen en flexbox con crecimiento:
   a diferencia de un grid auto-fit, aqui el ULTIMO renglon SIEMPRE se estira para llenar el ancho
   -- una tarjeta que quede sola crece en vez de dejar un hueco a la derecha. El contenido largo
   (checklist, visita, visitas anteriores, bitacora, envios) va a todo el ancho debajo. */
.tarjetas-info { display: flex; flex-wrap: wrap; gap: var(--s-4); align-items: stretch; }
.tarjetas-info > * { flex: 1 1 280px; margin: 0; }
/* H28b (re-revisión de la ola final): una tarjeta `.pila` (rejilla) se estira a la altura de la vecina más alta
   y la rejilla repartía ese alto entre sus renglones (el botón flotaba a media fila): arriba, como las demás. */
.tarjetas-info > .pila { align-content: start; }
.ancho-total, .tarjetas-info + * { margin-top: var(--s-4); }
.ancho-total + .ancho-total { margin-top: var(--s-4); }

/* ---------- Combo con búsqueda (H11 T3, RN2-60, D182) ----------
   Progressive enhancement de `combo.mjs` sobre <select data-combo>: el select real se queda (con
   `.sr-solo` -- el mismo truco que ya usa el <input type=file> requerido de
   equipos/_form_importar.html -- oculto a la vista pero SIGUE renderizado, así que un `required`
   no se sale de la validación nativa del formulario). Cerrado se ve igual a un <select> de
   `.campo` con su caret; abierto: cajita de búsqueda + lista flotante (max-height 320px, sombra
   `--sh-panel`). Sin animación (Diseño §6: mejor no animarlo). */
.combo { position: relative; min-width: 0; }
.combo-input {
  font: inherit; width: 100%; min-width: 0; min-height: 44px;
  padding: 0 var(--s-3);
  /* !important, igual que `[hidden]` arriba (línea ~82): `.campo input:not(...):not(...)` es MÁS
     específico que una sola clase (sus dos :not() suman) y le gana la cascada al padding de acá
     -- el resto de sus propiedades ya trae los MISMOS valores que las de abajo (por eso se
     repiten, no por accidente), solo el hueco para el caret es genuinamente distinto. */
  padding-right: var(--s-8) !important;
  border-radius: var(--r-md); border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--fg);
}
.combo-input:focus { outline: none; border-color: var(--ring); box-shadow: var(--ring-shadow); }
.combo-input[aria-invalid="true"] { border-color: var(--bad); }   /* H28a: lo marca formularios.mjs */
.combo-input:disabled { opacity: .6; background: var(--surface-2); cursor: not-allowed; }
.combo-caret { position: absolute; top: 50%; right: var(--s-3); width: 10px; height: 10px; transform: translateY(-65%); pointer-events: none; }
.combo-caret::before { content: ""; position: absolute; left: 0; top: 4px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid var(--muted-fg); }
.combo-panel { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; right: 0; padding: var(--s-1); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--sh-panel); }
.combo-lista { list-style: none; margin: 0; padding: 0; max-height: 320px; overflow: auto; }
.combo-opcion { display: flex; align-items: center; min-height: 44px; padding: 0 var(--s-3); border-radius: var(--r-sm); cursor: pointer; font-size: var(--t-sm); }
.combo-opcion:hover, .combo-opcion[aria-selected="true"] { background: var(--primary-bg); color: var(--primary); }
.combo-resalte { background: none; color: var(--primary); font-weight: 700; }
.combo-vacio { margin: 0; padding: var(--s-3); font-size: var(--t-sm); color: var(--muted-fg); }

/* ---------- Contrato (H12 T3, RN2-41/RN2-43) ----------
   Página BLOQUEANTE (Diseño: sin app detrás, el servidor es el candado) sobre `publico/base.html`.
   `.contrato-cab` pone el título y "Cerrar sesión" en la misma fila (la salida honesta de una
   pantalla sin nav). `.contrato-cuerpo` es el propio candado visual: 65ch de lectura cómoda,
   scroll propio (nunca el de la página) para que el sentinela de `contrato.mjs` mida contra ESE
   scroll, y `white-space: pre-line` pinta los párrafos del cuerpo (`\n\n` de la BD) sin meter un
   filtro de Jinja nuevo. */
.contrato-cab { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--s-3); flex-wrap: wrap; }
.contrato-salir { margin: 0; }
.contrato-cuerpo {
  max-width: 65ch;
  max-height: min(calc(60vh / var(--zoom)), 34rem);
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-4);
  background: var(--surface);
  white-space: pre-line;
}
.contrato-cuerpo:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
.contrato-check { display: flex; align-items: center; gap: var(--s-3); min-height: 44px; font-weight: 600; }
.contrato-check input { width: 28px; height: 28px; flex-shrink: 0; }
.contrato-check input:disabled { opacity: .5; }

/* ---------- Avance del servicio: tramos por actor (H15 T4, RN2-25/26, Diseño v2 §3) ----------
   `comunes/_avance_servicio.html`, panel y portal. Barra proporcional en `--primary-bg`; la más
   larga (`.mas-largo`) en `--warn-bg` -- NUNCA rayada (el rayado es exclusivo de la ida en falso).
   `.en-curso` (tramo todavía corriendo) cambia la pista a hueca con borde punteado, sin relleno.
   `.no-aplica` (tramo cortado al cancelar la orden, R10) esconde la pista entera -- "No aplica" es
   el único dato, mostrar una barra ahí confundiría "cuánto tardó" con "hasta dónde alcanzó a
   correr". Anchos por clase de decena (`.avance-0`..`.avance-100`, mismo patrón que
   `.progreso .avance-N`): CERO `style=` inline (guardián `test_sin_style_ni_title_inline_en_plantillas`).
   Móvil: la pista ya vive DEBAJO del texto por default (sin media query -- fluido en todo el rango). */
.tramos-lista { display: grid; gap: var(--s-3); list-style: none; margin: 0; padding: 0; }
.tramo-fila { display: grid; gap: 4px; }
.tramo-cabeza { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; }
.tramo-titulo { font-size: var(--t-sm); font-weight: 600; }
.tramo-duracion { font-size: var(--t-sm); color: var(--muted-fg); white-space: nowrap; }
.tramo-pista { height: 8px; border-radius: var(--r-pill); background: var(--surface-2); overflow: hidden; }
.tramo-pista > span { display: block; height: 100%; background: var(--primary-bg); }
.tramo-fila.mas-largo .tramo-pista > span { background: var(--warn-bg); }
.tramo-fila.en-curso .tramo-pista { background: none; border: 1.5px dashed var(--border-strong); }
.tramo-fila.en-curso .tramo-pista > span { display: none; }
/* Selectores compuestos (mismo motivo que `.progreso .avance-N`, ver ese comentario): el ancestro
   `.tramo-pista` en los dos lados evita que el `width` de una regla más específica le gane a estas. */
.tramo-pista .avance-0 { width: 0%; }
.tramo-pista .avance-10 { width: 10%; } .tramo-pista .avance-20 { width: 20%; } .tramo-pista .avance-30 { width: 30%; } .tramo-pista .avance-40 { width: 40%; } .tramo-pista .avance-50 { width: 50%; }
.tramo-pista .avance-60 { width: 60%; } .tramo-pista .avance-70 { width: 70%; } .tramo-pista .avance-80 { width: 80%; } .tramo-pista .avance-90 { width: 90%; } .tramo-pista .avance-100 { width: 100%; }
.tramos-detalle { margin-top: var(--s-3); }
.tramos-detalle summary { cursor: pointer; font-weight: 600; min-height: 44px; display: flex; align-items: center; }
.tramos-ronda + .tramos-ronda { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border); }
.tramos-ronda > p { margin: 0 0 var(--s-2); }

/* ---------- Verificar (H16 T2, R12): tira de miniaturas por unidad + candado en prosa ---------- */
.unidad-curar .tira-miniaturas { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-2); }
.miniatura { position: relative; display: block; width: 96px; height: 96px; border-radius: var(--r-md);
  border: 1px solid var(--border); overflow: hidden; padding: 0; cursor: pointer; background: var(--surface-2); }
.miniatura img, .miniatura video { width: 100%; height: 100%; object-fit: cover; display: block; }
.miniatura-skeleton { display: grid; place-items: center; cursor: default; }
/* Badge de decisión SOBRE la miniatura: reusa `.chip`/`.chip-ok`/`.chip-bad`/`.chip-warn` (cero
   tokens nuevos), solo se posiciona encima -- el texto es lo que distingue el estado (R11: "badges
   con texto, no solo color"), nunca únicamente el tono. */
.badge-curacion { position: absolute; left: 4px; right: 4px; bottom: 4px; justify-content: center; min-height: 20px;
  padding: 1px 6px; font-size: 10px; white-space: normal; }
.candado-verificar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); margin-top: var(--s-5); }
.candado-verificar .hint { flex-basis: 100%; margin: 0; }

/* ---------- Visor de curación (H16 T2, R11): overlay a pantalla completa, JS plano ---------- */
.visor-curacion { position: fixed; inset: 0; width: calc(100vw / var(--zoom)); height: calc(100dvh / var(--zoom)); max-width: calc(100vw / var(--zoom)); max-height: calc(100dvh / var(--zoom));
  margin: 0; padding: 0; border: 0; background: var(--surface); color: var(--fg); display: grid;
  grid-template-rows: auto 1fr auto; }
/* Fondo = el `::backdrop` de `.dialogo` de la casa (Diseño §3): mismos valores, cero literal nuevo. */
.visor-curacion::backdrop { background: rgb(35 34 34 / .45); backdrop-filter: blur(2px); }
.visor-header { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--border); }
.visor-nombre { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
.visor-contador { color: var(--muted-fg); font-size: var(--t-sm); }
.visor-central { position: relative; display: grid; place-items: center; overflow: hidden; background: var(--surface-2); min-height: 0; }
.visor-media { width: 100%; height: 100%; display: grid; place-items: center; }
.visor-media img, .visor-media video { max-width: 100%; max-height: 100%; object-fit: contain; }
.visor-flecha { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface); color: var(--fg); font-size: 1.5rem; line-height: 1; cursor: pointer; }
.visor-flecha-prev { left: var(--s-3); }
.visor-flecha-next { right: var(--s-3); }
.visor-footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); padding: var(--s-3) var(--s-4); border-top: 1px solid var(--border); }
.visor-pildoras { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.visor-contador-vivo { color: var(--muted-fg); font-size: var(--t-sm); }
.visor-decidir { display: flex; gap: var(--s-2); margin-left: auto; }
@media (max-width: 640px) {
  .visor-footer { flex-direction: column; align-items: stretch; }
  .visor-decidir { margin-left: 0; }
  .visor-nombre { font-size: var(--t-sm); }
}

/* ---------- "Le falta para operar" (H20, D227) ---------- */
/* Mismos tokens `--warn*` que `.aviso.warn` y `.chip-warn`: claro y oscuro salen gratis. */
.le-falta { display: grid; gap: var(--s-3); padding: var(--s-4); border: 1px solid var(--warn-border); border-left-width: 4px; border-radius: var(--r-md); background: var(--warn-bg); }
.le-falta h2 { color: var(--warn); }
.le-falta .titulo { font-weight: 600; }
.le-falta .por-que { color: var(--muted-fg); }
.le-falta .lista-plana > li { border-bottom-color: var(--warn-border); }
/* El chip de la lista SÍ envuelve (`.chip` es `nowrap`): "Le falta: administrador, sucursal,
   responsable, principal, ubicación" desbordaba la celda a 390 px. */
.chip-falta { white-space: normal; text-align: left; }

/* ---------- Escoba del barrido H18 (B8 y B9) ----------
   Reglas NUEVAS al final de la hoja donde vive cada familia (Ruling N del plan H18b). */
/* B8: el <select> de un filtro con combo queda `.sr-solo` (fuera de flujo) y el ancho del `.campo`
   pasa a decidirlo el <input> del combo, que mide su tamaño intrínseco (~20 caracteres): a 390 px
   el combo se veía a ~60 % al lado de selects nativos a 100 %. `:has()` para no tocar los `.campo`
   que no llevan combo (los suyos ya llenan el renglón al encogerse). */
@media (max-width: 640px) { .filtros .campo:has(.combo) { flex: 1 1 100%; } }
/* B9: dos `.seccion` hermanas necesitan aire ENTRE ellas -- el `gap` de `.seccion` separa a sus
   hijos, no a las hermanas. Solo donde el padre no es ya una rejilla de la casa con su propio
   hueco (`.pila`/`.pila secciones` del panel, `.cuerpo` de campo): en el Portal las secciones
   cuelgan directo del <main>, y "Anteriores" quedaba pegado al botón de la visita de arriba. */
:not(.pila):not(.cuerpo) > .seccion + .seccion { margin-top: var(--s-6); }

/* ---------- Ola de arreglos de H29a (barridos B y C) ----------
   Reglas NUEVAS al final de la hoja (mismo criterio que la escoba de H18): todas opt-in por clase o acotadas a su
   pieza, ninguna cambia otra vista. Las prueban en el navegador los barridos; `test_guardianes_ui.py` fija las que
   cuidan una falla Alta o el área táctil. */
/* B3: un enlace suelto de texto en una celda o en un dato ("Acta", "Constancia", "Descargar comprobante") medía 16 a
   19 px de alto en el celular. Mismo remedio que el folio de la tarjeta (`.tabla td:first-child a`): 44 px de área
   táctil sin volverlo botón. Ola 2: `min-width` también, porque "Acta" medía 28 px de ancho a 390 (área de 44x44, el
   texto sigue a la izquierda). */
.enlace-tactil { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }
/* F2: en Pagos del proveedor, de 901 a 1440 px, la tabla en auto-layout partía el folio ("OS-" / "0020") y la fecha en
   dos renglones para darle ancho al motivo. Solo las celdas que llevan la clase: las demás tablas no cambian. */
.tabla td.sin-partir { white-space: nowrap; }
/* B1: Por cobrar y Pagadas. En la rejilla de `.filtros-reportes` los dos botones (~210 px) no caben en una pista de
   ~176 px (820 y 920): "Filtrar" quedaba encima de "Quitar filtros" y más arriba que los campos. Aquí los campos
   crecen parejo y los botones van juntos, al pie de los campos; si no caben en el renglón, bajan juntos al siguiente.
   Base de 140 px: con tres campos y los dos botones (~230 px) el renglón cabe desde ~770 px de pantalla. Los botones
   se encogen solo si van solos en su renglón y aun así no caben (320 px): entonces se envuelven adentro, sin salirse. */
.filtros-en-linea { display: flex; flex-wrap: wrap; align-items: flex-end; }
.filtros-en-linea > .campo { flex: 1 1 140px; }
.filtros-en-linea > .filtros-reportes-acciones { flex: 0 1 auto; margin-left: auto; }
/* B3 del bloque C: bajo el tipo de una visita, el motivo y el desglose de conceptos eran dos bloques pegados (0 px). */
.tabla td > .lista-conceptos { margin-top: var(--s-1); }
/* F1/F2 del bloque C, `/campo/pagos`: la tarjeta usaba `.datos` (la rejilla `max-content | 1fr` de los <dl>): arriba de
   640 px un motivo largo se quedaba con la primera columna en un solo renglón y sacaba la etiqueta y el monto de la
   tarjeta. Una columna en todos los anchos, la composición que ya tenía a 390. */
.pago-datos { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-1); min-width: 0; }
/* F3 del bloque C (de H27b): el modal Conceptos (lista + alta) scrollea por su `.dialogo-cuerpo`. El `.modal-form` de
   adentro ya no es scroller ni repite el padding del cuerpo, y su barra deja de ser pegajosa: la del modal es "Cerrar". */
.dialogo-cuerpo .modal-form { padding: 0; overflow: visible; }
.dialogo-cuerpo .modal-form .acciones { position: static; margin: 0; padding: 0; border-top: 0; background: none; }

/* ---------- Ola final de H30b (barridos A y B) ---------- */
/* B1: el bloque "Cobro adicional por espera" del detalle de la orden quedaba pegado (0 px) al botón Conceptos de arriba y a
   la línea de abajo, en la tarjeta de la visita activa y en las anteriores. */
.cobro-espera-bloque { margin: var(--s-4) 0; }
/* B3: "Ir a la orden" (`.enlace-tactil`, inline-flex de 44 px) dentro de una razón inflaba su línea de texto a 820 y 1440 px:
   va en su propia línea y su área táctil se queda. */
.enlace-tactil.linea-propia { display: flex; width: fit-content; }
