/* ==========================================================================
   Agrospot — Design system
   Portado del de MGA (Linear-style, denso, monocromático) con UNA diferencia:
   MGA es blanco y negro por decisión ("sin colores de acento"); Agrospot tiene
   marca. El teal #13615b es la "A" del logo y ya aparece 134 veces en la
   platform — lo que nunca existió es el token, y por eso hay cuatro verdes casi
   idénticos tipeados de memoria (#13615b, #25685b, #01635b, #0f4f47).

   La regla: los neutros son los de MGA, intactos — son los que hacen legible
   una lista densa. El teal entra SÓLO en el botón primario, el ítem activo del
   sidebar, el foco, los links y el badge de producto. Filas, texto, iconos y
   fondos siguen grises. Un token nuevo de marca se agrega acá o no se agrega.

   La sexta y única excepción, anotada acá porque la regla se lee acá: la
   BARRITA DE AVANCE (`.viajes-barra`), el único teal que mide algo en vez de
   marcar una acción. Vive en los encabezados de las dos pestañas que listan
   equipos y en ningún otro lado — la fila de la lista de negocios muestra su
   avance con el número pelado. Que siga siendo la única es lo que la sostiene.
   ========================================================================== */

:root {
  /* ── Marca (la del logo, por fin declarada) ──────────────────────────────── */
  --brand:        #13615b;   /* la "A" del isotipo: primario, activo, foco */
  --brand-hover:  #0f4f47;   /* su hover */
  --brand-ink:    #002c36;   /* el wordmark: teal casi negro */
  --brand-soft:   #f0f6f5;   /* fondo del badge de producto */
  --brand-line:   #d1e7e4;   /* su borde */

  /* Los dos que sólo usa la pública, y que vienen del kit de marca —no de una
     paleta inventada acá. `--papel` es el verde pálido del panel del hero y el
     mismo del PDF del contrato (`negocios/pdf.py`), así que el sitio y el papel
     que se firma son del mismo color. `--menta` es la «A» sobre las fotos. */
  --papel:        #e8ebe4;
  --menta:        #a9d4a0;

  /* Superficies */
  --bg:            #ffffff;   /* contenido principal */
  --bg-sidebar:    #fbfbfa;   /* sidebar, un pelo más cálido */
  --bg-hover:      #f4f4f3;   /* hover de ítems / filas */
  --bg-active:     #ececeb;   /* ítem de sidebar seleccionado */
  --bg-sunken:     #f7f7f6;   /* inputs, badges */

  /* Texto */
  --text:          #08090a;   /* casi negro */
  --text-2:        #62646a;   /* secundario */
  --text-3:        #9a9ca1;   /* terciario / placeholders */
  --text-inv:      #ffffff;

  /* Líneas */
  --border:        #e9e9e7;
  --border-strong: #d6d6d4;

  /* Forma */
  --radius:    6px;
  --radius-lg: 8px;

  /* Sombra (mínima, solo overlays) */
  --shadow-modal: 0 12px 48px rgba(8, 9, 10, .16), 0 0 0 1px var(--border);

  /* Alerta / destructivo. Un solo rojo para toda la app: el de .tag-danger. */
  --danger: #a32d2d;

  --sidebar-w: 240px;
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  /* ── Escala de z-index ─────────────────────────────────────────────────────
     Las capas de la app, declaradas UNA vez y en el orden en que se tapan. Todo
     lo que flota sale de acá: un número suelto en el archivo es un empate
     esperando pasar (la isla y el backdrop del modal eran los dos 50, así que
     ganaba el orden del DOM y la isla quedaba iluminada sobre la penumbra).

     El orden es el de la interacción — lo que abrís último tapa lo anterior:

       fila          (sin z-index: es el flujo normal de la página)
       sticky        lo que se queda quieto mientras la página scrollea por
                     debajo (el panel de propiedades del detalle): tapa al
                     contenido, y nada más
       isla          flota sobre la lista, pero por debajo de todo lo que abras
       popover       los menús anclados a un control (estado, vistas, filtro,
                     acciones del detalle, picker de ícono, datepicker, usuario)
       menú de fila  el popover flotante del kebab y del click derecho: se abre
                     DESDE una fila y tiene que tapar a la isla de la selección
       modal-fondo   la penumbra apaga la pantalla entera: tapa todo lo de arriba
       modal         la caja, sobre su propia penumbra
       toast         se ve incluso con un modal abierto (un rechazo se avisa
                     igual, y el toast no roba el foco)

     Los saltos son de 100 para que quede lugar entre capas sin volver a tocar
     esta escala. Un dropdown que vive DENTRO del modal (p. ej. el TomSelect del
     import) no entra acá: el modal ya es su contexto de apilado, y su z-index
     sólo compite con sus hermanos. */
  --z-sticky:       10;
  --z-isla:        100;
  --z-popover:     200;
  --z-menu-fila:   300;
  --z-modal-fondo: 400;
  --z-modal:       410;
  --z-toast:       500;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: "cv05" 1, "cv08" 1, "ss01" 1;
}

a { color: inherit; text-decoration: none; }

/* ── App shell (estilo Linear) ──────────────────────────────────────────────
   El sidebar es el FONDO de toda la ventana. El contenido (.main) flota por
   encima como una "hoja" con bordes redondeados y un margen sutil, dejando
   asomar el fondo del sidebar arriba, abajo y a la derecha. */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  height: 100vh;
  overflow: hidden;
  background: var(--bg-sidebar);
}

/* ── Layout limpio (sin sidebar): consent OAuth, etc. ────────────────────── */
.blank { background: var(--bg-sunken); }
.blank-shell {
  min-height: 100vh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 20px; padding: 24px;
}
.blank-brand { display: flex; align-items: center; gap: 9px; }
.blank-brand .ws-logo {
  width: 26px; height: 26px; border-radius: 6px;
  background: var(--text); color: var(--text-inv);
  display: flex; align-items: center; justify-content: center;
  font-weight: 600; font-size: 14px;
}
.blank-brand .ws-name { font-weight: 600; font-size: 15px; }

.card {
  width: 100%; max-width: 420px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 24px;
  box-shadow: 0 1px 2px rgba(8, 9, 10, .04);
}
.card .card-title { font-size: 17px; font-weight: 600; margin: 0 0 12px; }
.card p { margin: 0 0 8px; }
.card .muted { color: var(--text-2); font-size: 13px; }
.card code {
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 4px; padding: 1px 5px; font-size: 12.5px;
}

/* ── El círculo de una persona (ADR-0011) ──────────────────────────────────
   UNA sola clase para toda la app: donde haya una persona dibujada sale de acá,
   siempre vía el partial `templates/_avatar.html`. La usan el sidebar
   (MGA-185), los tres paneles de detalle y las filas de cesiones y movimientos
   (MGA-187) y las filas de lista de Pedidos y Órdenes (MGA-188, la celda
   `.m-autor` de más abajo): TODAS tal cual, sin una segunda variante ni un
   modificador de tamaño. Están las cinco superficies que ADR-0011 enumera, así
   que no falta ninguna — la lista de Acopios no lleva círculo a propósito.

   Monocromo, sin color por persona. No es una limitación del design system que
   haya que disculpar: se verificó en el DOM de Linear que sus dos avatares de
   iniciales, de personas distintas, comparten el mismo fondo gris. Copiar al
   referente y respetar la línea 1 de este archivo son lo mismo.

   20px es el tamaño que se probó contra las filas reales antes de decidirlo. El
   `inline-flex` (y no `flex`) es para que entre en un renglón de texto sin
   romperlo: en el panel el círculo acompaña al nombre.

   Va ACÁ —antes del Sidebar, con el resto del chrome— y no adentro de ninguna
   sección de pantalla, porque no es de ninguna: este archivo ordena primero lo
   que vale para toda la app y después las pantallas (Perfil, Detalle, Modal,
   Importar). El círculo es un átomo compartido, así que si vive partiendo la
   sección del Sidebar, el próximo que agregue algo al menú de usuario lo escribe
   del otro lado y la sección se parte de nuevo. */
/* El tamaño salió de mirarlo, no de calcularlo: la primera versión (20px, letras
   de 9.5px) se leía barata —el disco quedaba chico y las dos iniciales tocaban
   el borde—, así que el círculo subió a 24px y la letra a 10.5px. Que las dos
   cosas crecieran juntas es la decisión: agrandar SÓLO el círculo deja más
   anillo vacío y el mismo dibujo adentro, y lo que faltaba no era vacío sino
   presencia. El tracking acompaña (.35px) porque dos mayúsculas juntas en Inter
   se empastan al crecer.

   24px es el techo, y lo fija la fila: `.lin-row` mide 44px de alto y la columna
   del autor en las grillas de meta mide 32px (subió de 28 con este cambio), así
   que el círculo entra con 4px de aire de cada lado. Un tamaño más grande no es
   un ajuste de esta regla, es rediseñar la fila. */
.avatar-p {
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--bg-sunken); border: 1px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 600; color: var(--text-2);
  letter-spacing: .35px; flex-shrink: 0; user-select: none;
}

/* ── Sidebar ─────────────────────────────────────────────────────────────── */
.sidebar {
  background: var(--bg-sidebar);
  display: flex;
  flex-direction: column;
  padding: 0 8px 8px;
  overflow-y: auto;
  user-select: none;
}

/* Header del sidebar: vive sobre el fondo, sin línea (la hoja flotante ya
   marca la separación). Mismo alto que antes para que respire igual. */
.workspace {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 48px;
  flex-shrink: 0;
  margin: 0 -2px;
  padding: 0 8px;
  border-radius: var(--radius);
  cursor: pointer;
}
.workspace:hover { background: var(--bg-hover); }
/* 24px, como TODOS los marcadores de identidad de la app (ADR-0011). El sidebar
   tenía tres —workspace 20, cliente 16, persona 24— y ninguno por una razón. */
.workspace .ws-logo {
  width: 24px; height: 24px; border-radius: 6px;
  background: var(--brand); color: var(--text-inv);
  font-weight: 700; font-size: 13px; letter-spacing: -.02em;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; overflow: hidden;
}
.workspace .ws-logo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.workspace .ws-name { font-weight: 600; font-size: 13.5px; color: var(--brand-ink); }
.workspace .ws-caret { margin-left: auto; color: var(--text-3); display: flex; }

.nav { margin-top: 4px; display: flex; flex-direction: column; gap: 1px; }

.nav-section {
  font-size: 11px;
  font-weight: 500;
  color: var(--text-3);
  padding: 14px 8px 4px;
  letter-spacing: .01em;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 8px;
  border-radius: var(--radius);
  font-size: 13.5px;
  color: var(--text-2);
  cursor: pointer;
  white-space: nowrap;
}
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
.nav-item.active { background: var(--brand-soft); color: var(--brand); font-weight: 500; }
.nav-item.is-stub { cursor: default; }
.nav-item .icon { width: 16px; height: 16px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.nav-item .icon svg { width: 16px; height: 16px; stroke: currentColor; }
.nav-item .count {
  margin-left: auto; font-size: 11px; color: var(--text-2);
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 10px; padding: 0 7px; line-height: 16px;
}
/* La píldora de «lo que todavía no está». Era una regla anidada bajo `.nav-item`
   —el sidebar era su único usuario— y pasó a ser genérica cuando la barra de
   etapas del negocio la necesitó: adentro de una pestaña heredaba nada y salía
   como texto pelado, que es por qué la barra no entraba en una línea. */
.badge-soon {
  font-size: 10px; font-weight: 500; letter-spacing: .02em;
  color: var(--text-3); background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 10px; padding: 0 7px; line-height: 15px; text-transform: none;
}
.nav-item .badge-soon { margin-left: auto; }

/* Cliente (team) colapsable con <details>/<summary> */
.client { }
.client > summary { list-style: none; }
.client > summary::-webkit-details-marker { display: none; }
.client > summary .icon { color: var(--text-3); }
.client > summary .caret { transition: transform .12s; }
.client[open] > summary .caret { transform: rotate(90deg); }
/* La sigla del cliente: 24px, el mismo tamaño que el círculo de la persona y que
   el logo del workspace. El nombre `-sm` quedó de cuando era el chico de los
   tres; hoy no hay grande ni chico, hay UN tamaño.

   Lo que sí cambia con el tamaño es la fila. La altura de un `.nav-item` sale de
   su contenido más 6px de padding arriba y abajo: con texto de 13.5px la línea
   mide ~20px y la fila 32px, que es el ritmo de TODO el nav. Un marcador de 24px
   la empujaría a 36 y la lista de Clientes dejaría de leerse pareja con la de
   Workspace justo arriba — así que esta fila lleva 4px de padding y vuelve a
   medir 32 exacto. El padding compensa al marcador; no es una excepción de
   estilo, es la misma altura sostenida con otro número. */
.client .ws-logo-sm {
  width: 24px; height: 24px; border-radius: 6px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 600; color: var(--text-2);
  letter-spacing: .35px; flex-shrink: 0;
}
.client > summary.nav-item { padding-top: 4px; padding-bottom: 4px; }
.client-children { padding-left: 25px; display: flex; flex-direction: column; gap: 1px; }
.client-children .nav-item { font-size: 13px; color: var(--text-3); }
.client-children a.nav-item:hover { color: var(--text); }

.link { color: var(--text-2); text-decoration: underline; }
.link:hover { color: var(--text); }

.sidebar-footer { margin-top: auto; padding-top: 8px; }

/* Menú de usuario: el nombre es un <summary> clickeable; al abrirlo despliega
   un popover hacia arriba con el email, "Perfil" y "Cerrar sesión". */
.user-menu { position: relative; }
.user-row {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px; border-radius: var(--radius);
  font-size: 12.5px; color: var(--text-2); cursor: pointer;
  list-style: none;
}
.user-row::-webkit-details-marker { display: none; }
.user-row:hover { background: var(--bg-hover); }
.user-menu[open] > .user-row { background: var(--bg-hover); }
/* El `.user-row .avatar` de una letra que vivía acá se fue: era el mismo círculo
   dibujado aparte, y ahora es `.avatar-p` como el de todos los demás. */
.user-row .nombre { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-row .user-caret { margin-left: auto; color: var(--text-3); display: flex; flex-shrink: 0; }

.user-pop {
  position: absolute; bottom: calc(100% + 6px); left: 0; right: 0; z-index: var(--z-popover);
  padding: 4px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-modal);
}
/* El email, ahora que la fila muestra el nombre. Es un dato, no una acción: no
   tiene hover ni cursor, y la línea lo separa de los ítems que sí lo son
   ("Perfil" y "Cerrar sesión"). */
.user-pop-email {
  padding: 6px 8px 8px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
  font-size: 12px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.user-pop-item {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 8px; border-radius: var(--radius);
  font-size: 13px; color: var(--text-2); cursor: pointer; white-space: nowrap;
}
.user-pop-item:hover { background: var(--bg-hover); color: var(--text); }
.user-pop-item svg { width: 16px; height: 16px; stroke: currentColor; flex-shrink: 0; }

/* ── Main: la "hoja" flotante (estilo Linear) ───────────────────────────────
   Tarjeta blanca por encima del fondo del sidebar. El margen deja asomar el
   fondo arriba/abajo/derecha; el borde redondeado + sombra fina la despegan. */
.main {
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg);
  margin: 8px 8px 8px 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 3px rgba(8, 9, 10, .05);
}

.content { flex: 1; overflow-y: auto; padding: 22px 26px; }
.content-inner { max-width: none; margin: 0; }

/* Cabecera de cada pantalla: título a la izquierda, acciones a la derecha.
   (El comentario que vivía acá decía que Linear no tiene migas de pan y que por
   eso la app tampoco. Es falso —Linear tiene `MGA › Issues`— y el ADR-0004 §6 lo
   derogó: las migas volvieron, ver .migas.) */
.page-head {
  display: flex; align-items: flex-start; gap: 16px;
  margin-bottom: 18px;
}
.page-head h1 { font-size: 18px; font-weight: 600; margin: 0 0 2px; }
/* El subtítulo, en cualquier lado. Era una regla anidada bajo `.page-head` y
   `.section-head`, o sea que un `.sub` fuera de esos dos contenedores no estaba
   estilado: heredaba el color del body y su tamaño, y salía como un párrafo
   negro de 14px que se lee igual que el contenido. Pasaba en las cuatro
   pestañas de Configuración, que usan `.sec-head`.

   Sólo color y tamaño acá: el margen lo sigue poniendo cada contexto, porque no
   es el mismo abajo de un `<h1>` que adentro de un modal. */
.sub { color: var(--text-2); font-size: 13px; }
.page-head .sub { margin: 0; }
.page-head .head-actions { margin-left: auto; flex-shrink: 0; display: flex; gap: 8px; }

/* ── Toolbar ─────────────────────────────────────────────────────────────── */
.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
.toolbar .grow { flex: 1; }

.search {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0 10px; background: var(--bg);
  height: 32px; color: var(--text-3);
}
.search:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(19,97,91,.08); }
.search svg { width: 15px; height: 15px; stroke: currentColor; flex-shrink: 0; }
.search input {
  border: none; outline: none; background: transparent;
  font: inherit; font-size: 13.5px; color: var(--text);
  width: 100%; padding: 0;
}
.search input::placeholder { color: var(--text-3); }

/* ── Inputs / botones ────────────────────────────────────────────────────── */
input, select, button { font: inherit; }

/* El atributo [hidden] debe ganarle al display de componentes (.btn usa
   inline-flex, que si no lo pisa y el elemento no se oculta al setear .hidden). */
[hidden] { display: none !important; }

.btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 11px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); color: var(--text);
  font-size: 13px; font-weight: 500; cursor: pointer;
  white-space: nowrap; transition: background .1s, border-color .1s;
}
.btn:hover { background: var(--bg-hover); }
.btn svg { width: 15px; height: 15px; stroke: currentColor; }

.btn-primary { background: var(--brand); color: var(--text-inv); border-color: var(--brand); }
.btn-primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); }

.btn-ghost { border-color: transparent; background: transparent; color: var(--text-2); height: 28px; padding: 0 8px; }
.btn-ghost:hover { background: var(--bg-hover); color: var(--text); }

.btn-danger { border-color: transparent; background: transparent; color: var(--text-2); height: 28px; padding: 0 8px; }
.btn-danger:hover { background: var(--bg-hover); color: var(--text); }

.btn-sm { height: 28px; font-size: 12.5px; }

/* ── Lista (estilo Linear: plana, full-width) ────────────────────────────── */
.table-wrap { border: none; border-radius: 0; overflow: visible; }
table { width: 100%; border-collapse: collapse; }

/* Header de columnas: transparente, solo una línea fina abajo. */
thead th {
  text-align: left; font-size: 12px; font-weight: 400;
  color: var(--text-3); letter-spacing: 0;
  padding: 8px 12px; background: transparent;
  border-bottom: 1px solid var(--border);
}
thead th.col-meta { text-align: right; }
/* La celda va con su encabezado: la regla del `<th>` estaba sola porque hasta
   Configuración ninguna tabla tenía una columna de meta con contenido. */
tbody td.col-meta { text-align: right; }

/* Filas altas, separador sutil, toda la fila clickeable. */
tbody td {
  padding: 0 12px; height: 44px;
  border-bottom: 1px solid var(--border);
  font-size: 13.5px; vertical-align: middle;
}
tbody tr { transition: background .08s; cursor: pointer; }
tbody tr:hover { background: var(--bg-hover); }
tbody tr.is-empty { cursor: default; }
tbody tr.is-empty:hover { background: transparent; }

/* El checkbox y el kebab no son columnas de datos: existen sólo para colgar los
   dos controles que se revelan en hover (ADR-0004 §2, «sin una columna
   permanente»). Sin un ancho fijo, el layout automático de la tabla les da el
   ancho de una columna de verdad y corre el contenido ~40px a la derecha —
   justo lo que la regla venía a evitar.
   Los números salen de .lin-row, para que una tabla de catálogo y una lista de
   documentos arranquen en la MISMA vertical: 8px de inset, checkbox de 14px,
   10px hasta el contenido. Por eso la celda que sigue al checkbox pierde su
   padding izquierdo: ya lo puso la del checkbox. */
thead th.col-check, tbody td.col-check { width: 14px; padding: 0 10px 0 8px; }
thead th.col-check + th, tbody td.col-check + td { padding-left: 0; }
thead th.col-kebab, tbody td.col-kebab { width: 24px; padding: 0 8px 0 0; }

/* El «seleccionar todo» del header SÍ se revela en hover, al revés de lo que
   fijaba MGA-143 ("una puerta invisible no se encuentra"). Lo que hace que el
   argumento no aplique acá es DÓNDE se engancha el hover: no al `<th>` —que
   sería una puerta de 14px que hay que adivinar— sino a **la tabla entera**. El
   pointer en cualquier parte de la lista lo enciende, así que se encuentra igual
   que antes, y la cabecera queda limpia cuando nadie está por seleccionar nada.

   Sigue visible, obviamente, cuando hay algo tildado: si no, no habría con qué
   destildar. */
.table-wrap:hover thead th.col-check .lin-check input { opacity: 1; }

/* Banner sobre la tabla (variante B del prototipo). Confiesa que «todo» son las
   filas cargadas y no las que matchean el filtro; en la etapa 2 suma acá el link
   que ofrece el resto. Vive entre el toolbar y la tabla, y lo dibuja el JS: sólo
   existe como consecuencia de un click, nunca sale del server. */
.sel-banner {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 9px 12px; margin-bottom: -1px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 12.5px; color: var(--text-2);
}
.sel-banner b { color: var(--text); font-weight: 500; }

/* Pie que cierra la cuenta (el desglose del acopio por plata, las cesiones):
   la última fila es el total y por eso es la única que pesa. */
tfoot td {
  padding: 0 12px; height: 36px;
  font-size: 13px; color: var(--text-2); vertical-align: middle;
}
tfoot tr:first-child td { border-top: 1px solid var(--border); }
tfoot tr:last-child td { color: var(--text); }
tfoot .muted { font-size: 12.5px; }

/* El mismo pie, pero FUERA de la tabla (el desglose del acopio por plata desde
   MGA-160): salió del <tfoot> para dejarle lugar al renglón de carga entre la
   última fila de material y el cierre de la cuenta (ADR-0010).

   Se ve idéntico al <tfoot> que reemplaza: mismo alto de renglón, mismo cuerpo,
   y el monto contra el borde derecho con el mismo inset de 12px que la columna
   Monto, así la cuenta cierra en la misma vertical que los números que suma. No
   lleva borde arriba: la última fila del <tbody> ya trae el suyo, que es el que
   antes se fusionaba con el del <tfoot> (border-collapse). */
.pie-cuenta .pie-fila {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 0 12px; height: 36px;
  font-size: 13px; color: var(--text-2);
}
.pie-cuenta .pie-total { color: var(--text); }
/* El total es el único renglón que pesa, y pesa ENTERO: adentro de la tabla el
   monto lo tomaba de `tfoot tr:last-child td`, que le ganaba a `.cell-meta` por
   especificidad. Afuera, `.cell-meta` le declara el color al span y una
   declaración propia siempre le gana a una heredada, así que el número hay que
   pintarlo acá o el total queda con la etiqueta negra y el monto gris. */
.pie-cuenta .pie-total .cell-meta { color: var(--text); }
/* Las aclaraciones ("recibido + en vuelo") se leen un tono más claro que su
   etiqueta. Adentro de la tabla eso lo hacía `td .muted`; sin <td>, va acá. */
.pie-cuenta .muted { font-size: 12.5px; color: var(--text-3); }

/* Nombre con ícono a la izquierda. Es un <span> casi siempre, pero en las
   filas que navegan a un detalle propio (plantillas, ADR-0014/MGA-208) es un
   <a>: mismo look, sin el azul ni el subrayado de link por default. */
.cell-name { display: flex; align-items: center; gap: 10px; font-weight: 450; color: var(--text); }
a.cell-name { text-decoration: none; }
a.cell-name:hover { text-decoration: underline; }
.cell-name .row-icon {
  width: 16px; height: 16px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; color: var(--text-3);
}
.cell-name .row-icon svg { width: 16px; height: 16px; stroke: currentColor; }
/* Ícono cambiable (estilo Linear) + ID del acopio en la lista. */
.cell-name .icono-cell { position: relative; display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; }
.icono-btn {
  width: 24px; height: 24px; flex-shrink: 0; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: 6px; color: var(--text-3);
}
.icono-btn:hover { background: var(--bg-hover); color: var(--text); }
.icono-btn svg { width: 16px; height: 16px; stroke: currentColor; }
.cell-name .row-id {
  flex-shrink: 0; font-variant-numeric: tabular-nums;
  font-size: 11px; font-weight: 500; color: var(--text-3); letter-spacing: .02em;
}
/* Los mismos dos, ahora dentro de una fila de lista: Acopios dejó de ser una
   tabla (MGA-110) y se llevó puesto el ícono cambiable y el ID. Las reglas de
   arriba cuelgan de .cell-name, que es la celda de una tabla y ahí ya no está. */
.lin-row .icono-cell { position: relative; display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; }
.lin-row .row-id {
  flex-shrink: 0; font-variant-numeric: tabular-nums;
  font-size: 11px; font-weight: 500; color: var(--text-3); letter-spacing: .02em;
}

/* Popover del picker. Pre-renderizado y oculto; el botón lo muestra al instante. */
.icono-pop { position: absolute; top: 100%; left: 0; z-index: var(--z-popover); display: none; }
.icono-cell.open .icono-pop { display: block; }
.icono-picker {
  margin-top: 6px; width: 220px; padding: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px; box-shadow: var(--shadow-modal);
}
.icono-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 2px; }
.icono-opt {
  aspect-ratio: 1; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: 6px; color: var(--text-2);
}
.icono-opt:hover { background: var(--bg-hover); color: var(--text); }
.icono-opt.is-active { background: var(--bg-active); color: var(--text); }
.icono-opt svg { width: 17px; height: 17px; stroke: currentColor; }
/* En el modal de editar el acopio el picker vive dentro de un `.field`, y ahí
   `.field label { display: block }` le gana en especificidad al inline-flex de
   arriba (el ícono se iba a la esquina en vez de centrarse). Las columnas `1fr`,
   además, están calibradas para el popover angosto de la fila: en el modal de
   400px estiraban cada celda a 65px. Se fija el tamaño de la celda. */
.field .icono-grid { grid-template-columns: repeat(6, 34px); }
.field .icono-opt { display: inline-flex; margin-bottom: 0; }

/* Metadata alineada a la derecha (rubro, unidad…). */
.cell-meta { text-align: right; color: var(--text-2); white-space: nowrap; }
.cell-meta .meta { display: inline-flex; align-items: center; gap: 14px; justify-content: flex-end; }
.cell-meta .tag {
  color: var(--text-2); font-size: 12.5px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 5px; padding: 1px 8px;
}
td .muted { color: var(--text-3); }

/* Progreso del acopio: cuánto queda (texto) + cuánto se usó (barra). */
.cell-meta .progress {
  display: inline-flex; align-items: center; gap: 10px;
}
/* La misma barra en la meta de una fila de lista: Acopios dejó de ser una tabla
   (MGA-110) y se la llevó puesta. */
.lin-meta .progress {
  display: inline-flex; align-items: center; gap: 10px;
}
.progress-text { font-size: 12.5px; color: var(--text); font-variant-numeric: tabular-nums; }
.progress-track {
  width: 88px; height: 6px; border-radius: 999px;
  background: var(--bg-active); overflow: hidden; flex-shrink: 0;
}
.progress-fill {
  display: block; height: 100%; border-radius: 999px;
  background: var(--text); transition: width .2s ease;
}
.progress-fill.has-value { min-width: 3px; }
/* Sobregirado (MGA-91): el saldo negativo va en rojo y la barrita se llena de
   rojo entera. Va en la PISTA y no en el tramo porque un acopio se puede
   sobregirar sin nada entregado (todo cedido, o todo en vuelo), y ahí el tramo
   mide 0 y no habría rojo que ver. */
.progress.is-sobregiro .progress-text { color: var(--danger); font-weight: 500; }
.progress.is-sobregiro .progress-track { background: var(--danger); }
.progress.is-sobregiro .progress-fill { display: none; }

.empty { text-align: center; color: var(--text-2); padding: 56px 16px; font-size: 13.5px; }
.empty strong { color: var(--text); font-weight: 500; }

/* Borrar dentro del modal: discreto, abajo a la izquierda. */
.modal-actions .delete-link {
  margin-right: auto; border-color: transparent; background: transparent;
  color: var(--text-2);
}
.modal-actions .delete-link:hover { background: var(--bg-hover); color: var(--text); }

/* Badge de estado (abierto / cerrado). */
.badge {
  display: inline-flex; align-items: center;
  font-size: 12px; font-weight: 500;
  border-radius: 5px; padding: 1px 8px;
  border: 1px solid var(--border);
}
.badge-abierto { color: var(--text); background: var(--bg-sunken); }
.badge-cerrado { color: var(--text-3); background: transparent; }

/* Estados de pedido. Tonos suaves, coherentes con el resto. */
.badge-pendiente      { color: #92560a; background: #fdf2e2; border-color: #f3d9b0; }
.badge-confirmado     { color: #0c447c; background: #e8f1fb; border-color: #bcd6f0; }
.badge-cargar_sistema { color: #4527a0; background: #efeafa; border-color: #d4c8f0; }
.badge-solicitado     { color: #0c447c; background: #e8f1fb; border-color: #bcd6f0; }
.badge-en_camino      { color: #27500a; background: #ecf6e6; border-color: #c5e2b3; }
.badge-recibido       { color: #6a1b9a; background: #f6ecfa; border-color: #e0c8ee; }
.badge-con_problema   { color: #a32d2d; background: #fbebeb; border-color: #f0c4c4; }

/* Estados de OC (creada → enviada → presupuesto → confirmada → recibida;
   presupuesto/negociación solo cuenta corriente). */
.badge-creada      { color: #0c447c; background: #e8f1fb; border-color: #bcd6f0; }
.badge-enviada     { color: #27500a; background: #ecf6e6; border-color: #c5e2b3; }
.badge-presupuesto { color: #8a5300; background: #fbf1e0; border-color: #f0dcba; }
.badge-confirmada  { color: #1b5e51; background: #e4f4ef; border-color: #b7e0d4; }
.badge-recibida    { color: #6a1b9a; background: #f6ecfa; border-color: #e0c8ee; }

/* Dropdown de cambio de estado (badge clickeable → menú de opciones). */
.estado-menu { position: relative; display: inline-block; }
.estado-trigger {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
}
.estado-trigger::-webkit-details-marker { display: none; }
.estado-caret { display: inline-flex; opacity: .6; }
.estado-caret svg { width: 12px; height: 12px; stroke: currentColor; }
.estado-pop {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: var(--z-popover);
  min-width: 170px; padding: 4px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-modal);
}
.estado-opt {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 6px 8px; border: none; background: transparent;
  border-radius: 5px; cursor: pointer; text-align: left;
  font-size: 13px; color: var(--text);
}
.estado-opt:hover { background: var(--bg-hover); }
.estado-opt.is-active { font-weight: 500; }
.estado-opt .dot {
  width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid currentColor; box-sizing: border-box;
}
/* Colores de punto por estado (mismos que la lista). */
.dot.dot-pendiente      { color: #f9a825; }
.dot.dot-confirmado     { color: #1565c0; }
.dot.dot-cargar_sistema { color: #7b1fa2; }
.dot.dot-solicitado     { color: #1565c0; }
.dot.dot-en_camino      { color: #2e7d32; background: currentColor; }
.dot.dot-recibido       { color: #7b1fa2; background: currentColor; }
.dot.dot-con_problema   { color: #c62828; background: currentColor; }
/* Orden de compra: creada (hueco) → enviada → presupuesto → confirmada →
   recibida (lleno). Presupuesto/negociación es intermedio (hueco ámbar). */
.dot.dot-creada      { color: #1565c0; }
.dot.dot-enviada     { color: #2e7d32; background: currentColor; }
.dot.dot-presupuesto { color: #e08a00; }
.dot.dot-confirmada  { color: #00897b; background: currentColor; }
.dot.dot-recibida    { color: #7b1fa2; background: currentColor; }
/* Acopio: abierto = punto lleno; cerrado = punto hueco tenue. */
.dot.dot-abierto { color: var(--text); background: currentColor; }
.dot.dot-cerrado { color: var(--text-3); }

/* ── Panel de propiedades (compartido acopio + pedido) ───────────────────── */
.props-group { display: flex; flex-direction: column; gap: 4px; }
.props-group + .props-group { margin-top: 16px; }
.props-group-title {
  font-size: 11px; font-weight: 500; color: var(--text-3);
  letter-spacing: .02em; padding: 0 0 4px;
}
.prop {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 5px 0; min-height: 28px;
}
.prop-label { flex: 0 0 70px; font-size: 12.5px; color: var(--text-2); padding-top: 2px; }
.prop-value { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--text); overflow-wrap: anywhere; }
/* El tratamiento de un valor vacío del panel (AGR-322): hasta acá los seis
   campos de `.props-box` salían de FKs obligatorias y nunca estaban vacíos,
   así que no había regla que lo cubriera. `--text-3` y no `--text-2` porque es
   el mismo tono que ya usa `.props-group-title` para lo secundario del panel,
   no el de un dato que sí está pero pesa menos. */
.prop-value.is-vacia { color: var(--text-3); }
/* La razón social de cada punta linkea a la ficha de la empresa. En toda la app
   `a` es `color: inherit; text-decoration: none`, así que sin esto el link no se
   distingue del valor de al lado y no lo encuentra nadie. El subrayado en hover
   es la misma señal que traía ese mismo link cuando vivía en la tab Carga
   (`.traspaso-parte-nombre a:hover`, borrado por AGR-323). */
.prop-value a:hover { text-decoration: underline; }
.prop-link { display: inline-flex; align-items: center; gap: 6px; color: var(--text); }
.prop-link:hover { color: var(--text); text-decoration: underline; }
.prop-link svg { width: 14px; height: 14px; stroke: var(--text-3); }
.prop-empty { font-size: 12.5px; }
.props-obras { display: flex; flex-wrap: wrap; gap: 5px; }

/* Variantes de .lin-row para la lista de materiales del detalle. */
.lin-row .lin-icon {
  width: 16px; height: 16px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; color: var(--text-3);
}
.lin-row .lin-icon svg { width: 15px; height: 15px; stroke: currentColor; }
.lin-qty { font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* Fila de pedido vencido + tag de peligro. */
.row-vencida { box-shadow: inset 3px 0 0 #c62828; }
.tag-danger { color: #a32d2d; background: #fbebeb; border-color: #f0c4c4; }
/* Chip «Aporte a {cliente}»: OC cuya obra es de otro cliente (MGA-98). Forma de
   pastilla como .tag, pegado a la obra; el tono de alerta lo pone .tag-danger. */
.tag-aporte {
  display: inline-block; font-size: 12px; line-height: 1.4;
  border: 1px solid; border-radius: 5px; padding: 1px 8px; margin-left: 6px;
}

/* ── Lista estilo Linear (pedidos) ───────────────────────────────────────── */
.lin-list { display: flex; flex-direction: column; }
.lin-row {
  display: flex; align-items: center; gap: 10px;
  height: 44px; padding: 0 8px;
  border-bottom: 1px solid var(--border);
  color: var(--text); cursor: pointer;
  transition: background .08s;
}
.lin-row:hover { background: var(--bg-hover); }

/* Checkbox de selección múltiple (borrado en bloque, MGA-67). Zona de click
   generosa para no pifiarle al cuadradito dentro de la fila-link. Estilo Linear:
   aparece on hover de la fila (o si está tildado); si no, queda invisible pero
   ocupando su lugar para que el layout no salte. */
.lin-check { display: flex; align-items: center; flex-shrink: 0; padding: 6px; margin: -6px; cursor: pointer; }
/* El checkbox se dibuja entero acá, con `appearance: none`.
   El nativo lo pinta el sistema operativo: en macOS el borde del no-tildado es
   casi negro, y en una lista de veinte filas eso son veinte manchas oscuras
   compitiendo con el texto. Con `accent-color` sólo se puede cambiar el relleno
   del tildado, no el borde del vacío — que es justo el que se ve siempre. */
.lin-check input {
  appearance: none; -webkit-appearance: none;
  width: 14px; height: 14px; margin: 0; cursor: pointer; flex-shrink: 0;
  border: 1px solid var(--border-strong); border-radius: 4px;
  background: var(--bg) no-repeat center; background-size: 11px;
  opacity: 0; transition: opacity .1s, background-color .1s, border-color .1s;
}
.lin-check input:hover { border-color: var(--text-3); }
.lin-check input:checked, .lin-check input:indeterminate {
  background-color: var(--brand); border-color: var(--brand);
}
.lin-check input:checked { background-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2012'><path%20d='M2.6%206.2%204.9%208.5%209.4%203.7'%20fill='none'%20stroke='%23fff'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'/></svg>"); }
/* Indeterminado: hay filas tildadas pero no todas. No es un estado que el HTML
   sepa escribir (no es un atributo), lo prende el JS de base.html. */
.lin-check input:indeterminate { background-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2012'><path%20d='M3.2%206h5.6'%20fill='none'%20stroke='%23fff'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'/></svg>"); }
.lin-check input:focus-visible {
  outline: none; box-shadow: 0 0 0 3px rgba(19, 97, 91, .16);
}
/* El reveal en hover vale también para las tablas de catálogo, que siguen siendo
   tablas (ADR-0004 §1) y ganan selección múltiple sin una columna permanente de
   checkboxes: por eso el hover se escribe contra .lin-row Y contra <tr>. */
.lin-row:hover .lin-check input,
tr:hover .lin-check input,
.lin-check input:checked,
.lin-check input:indeterminate,
.lin-check input:focus-visible { opacity: 1; }

/* Kebab de la fila (ADR-0004 §2): el único hogar de las acciones de una fila.
   Se revela en hover, como el checkbox, y queda prendido mientras su menú está
   abierto. Ocupa lugar siempre (opacity, no display) para que nada salte. */
.lin-kebab {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; flex-shrink: 0; margin-left: 2px;
  border: none; background: transparent; border-radius: 5px;
  color: var(--text-3); cursor: pointer;
  opacity: 0; transition: opacity .1s, background .1s;
}
.lin-row:hover .lin-kebab,
tr:hover .lin-kebab,
.lin-kebab.is-open,
.lin-kebab:focus-visible { opacity: 1; }
.lin-kebab:hover, .lin-kebab.is-open { background: var(--bg-active); color: var(--text); }
.lin-kebab svg { width: 15px; height: 15px; stroke: currentColor; }

/* La fila cuyo menú está abierto (por el kebab o por click derecho) queda
   marcada: con el menú flotando al lado, sin esto no se sabe sobre cuál actúa. */
.lin-row.is-ctx, .lin-row.is-ctx:hover, tr.is-ctx { background: var(--bg-active); }

/* ── Isla de acciones flotante (estilo Linear, MGA-67) ─────────────────────── */
/* Fija abajo-centro, aparece con animación al tildar filas. */
.action-island {
  position: fixed; left: 50%; bottom: 12px; z-index: var(--z-isla);
  display: flex; align-items: center; gap: 10px;
  padding: 7px 8px 7px 14px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 10px; box-shadow: 0 8px 28px rgba(0, 0, 0, .16);
  font-size: 13px; color: var(--text-2);
  /* Estado oculto: corrido hacia abajo + transparente; .is-visible lo sube. */
  transform: translate(-50%, 12px); opacity: 0;
  transition: transform .18s cubic-bezier(.2, .8, .3, 1), opacity .18s ease;
}
.action-island.is-visible { transform: translate(-50%, 0); opacity: 1; }
.action-island-count { font-weight: 500; color: var(--text); white-space: nowrap; }
.action-island-hint { font-size: 12px; color: var(--text-3); white-space: nowrap; }
.action-island-sep { width: 1px; align-self: stretch; background: var(--border); margin: 2px 2px; }
.action-island .btn-eliminar { display: inline-flex; align-items: center; gap: 5px; }
.action-island .btn-eliminar svg { width: 14px; height: 14px; stroke: currentColor; }
.action-island-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: none; background: none; border-radius: 6px;
  color: var(--text-3); cursor: pointer;
}
.action-island-close:hover { background: var(--bg-hover); color: var(--text); }
.action-island-close svg { width: 15px; height: 15px; stroke: currentColor; }

/* Botón de acción destructiva (tono de .tag-danger). */
.btn-eliminar { color: #a32d2d; border-color: #f0c4c4; }
.btn-eliminar:hover { background: #fbebeb; color: #a32d2d; }

/* Punto de estado a la izquierda (como el ícono de status de Linear).

   `display: inline-block` no es decorativo: sin él, `width` y `height` **no
   aplican**, porque un `<span>` es `inline` y en inline se ignoran. Adentro de
   `.lin-row` se veía bien igual, de rebote —es un contenedor flex y los ítems
   flex se blockifican solos—, pero en el detalle el mismo punto vive dentro de un
   `<p>` y de un `.prop-value` (que es ítem flex, no contenedor) y ahí quedaba
   inline: se veía sólo el borde redondeado colapsado, un arco verde sin tamaño.

   `vertical-align` lo sienta sobre la línea de texto: al lado de una palabra, un
   inline-block se apoya en la baseline y el círculo queda flotando alto. */
.lin-status {
  display: inline-block;
  width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid currentColor; box-sizing: border-box;
  vertical-align: -1px;
}
.lin-status.dot-pendiente      { color: #f9a825; }
.lin-status.dot-confirmado     { color: #1565c0; }
.lin-status.dot-cargar_sistema { color: #7b1fa2; }
.lin-status.dot-solicitado     { color: #1565c0; }
.lin-status.dot-en_camino      { color: #2e7d32; background: currentColor; }
.lin-status.dot-recibido       { color: #7b1fa2; background: currentColor; }
.lin-status.dot-con_problema   { color: #c62828; background: currentColor; }
.lin-status.dot-creada         { color: #1565c0; }
.lin-status.dot-enviada        { color: #2e7d32; background: currentColor; }
.lin-status.dot-presupuesto    { color: #e08a00; }
.lin-status.dot-confirmada     { color: #00897b; background: currentColor; }
.lin-status.dot-recibida       { color: #7b1fa2; background: currentColor; }

/* ID monoespaciado tenue, como "MGA-36" en Linear. */
.lin-id {
  flex-shrink: 0; font-variant-numeric: tabular-nums;
  font-size: 12px; font-weight: 500; color: var(--text-3);
  min-width: 38px;
}

/* Título: ocupa el espacio del medio, recorta con … si no entra. */
.lin-title {
  flex: 1 1 auto; min-width: 0;
  font-size: 13.5px; font-weight: 450;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lin-title .lin-client { font-weight: 400; }

/* La nota del pedido en la fila (MGA-213). Ocupa EXACTAMENTE el hueco que le
   dejan el título y los labels, ni un pixel más.

   El `width: 0` es lo que lo consigue, y no es decorativo: es lo que hace que la
   nota NO cuente su texto cuando el navegador mide cuánto quiere ocupar la fila.
   Sin él, una nota larga se cobraba el ancho de su prosa y aplastaba lo de al
   lado: medido en el browser a 1040px, los labels pasaban de 82px y 105px a
   22px y 22px —«Al», «T»— para hacerle lugar. Al revés de lo que hay que
   sacrificar. Con `width: 0` la nota no reclama nada, el título y los labels se
   miden a su ancho de siempre, y `flex-grow` le da a la nota el sobrante, donde
   recorta con `…`.

   El orden de sacrificio queda entonces: primero la nota, y recién después
   —cuando la fila ya no entra ni sin ella— labels y título, igual que antes de
   este cambio. Es el orden correcto: el título es identidad (de qué obra es) y
   los labels son una clasificación cerrada; la nota es prosa, y su texto entero
   sigue estando en el `title`. En una fila muy apretada la nota se reduce a
   nada, y está bien.

   Lo que sí cuesta es la separación: una fila CON nota gasta los 8px que la
   despegan del título, y en una ventana angosta eso puede recortar el título
   unos pixeles antes de lo que lo recortaría sin nota. Es el precio de mostrarla
   al lado y no hay forma de no pagarlo; las filas sin nota no pagan nada.

   Tenue y 12.5px a propósito: tiene que poder ignorarse de un vistazo cuando
   escaneás la columna de obras, y aparecer cuando la buscás. */
.lin-nota {
  width: 0; flex: 1 1 auto; min-width: 0;
  font-size: 12.5px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Título + nota, en un solo hijo de la fila (MGA-213).

   Existe por dos razones, las dos medidas en el browser:

   1. LA NOTA SE LEE PEGADA AL TÍTULO. Quien se come el espacio libre de la fila
      es el título (`flex: 1 1 auto`): por eso la meta termina contra el borde
      derecho. Con la nota como hermana y el título creciendo igual, la nota
      quedaba escupida al otro extremo del renglón, a un palmo de la obra que
      describe, leyéndose como otra columna en vez de como su aclaración. Acá
      adentro el que crece es `.lin-head`, el título vuelve al ancho de su texto
      y la nota arranca donde termina la obra.

   2. EL PEDIDO SIN NOTA NO PAGA NADA. `.lin-row` tiene `gap: 10px`, y el gap se
      cobra por hijo, no por pixel: una nota de ancho cero le costaba igual 10px
      al renglón y recortaba títulos que antes entraban. Con las dos piezas
      adentro de un hijo, la fila cuenta uno solo, como antes de este cambio.

   El `gap: 8px` de acá adentro sólo aparece cuando hay nota, porque sin nota
   `.lin-head` tiene un solo hijo y no hay entre qué separar. */
.lin-head {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: baseline; gap: 8px;
}
.lin-head .lin-title { flex: 0 1 auto; }

/* Chips de label de la fila de pedido (ADR-0007): al lado del título, antes de
   la meta. No es una columna del grid de la derecha —un pedido puede tener cero,
   uno o varios— así que se lleva lo que el título le deje y no empuja nada: con
   muchos labels se recorta la tira, no la fila. El título ya tiene `flex: 1 1
   auto`, así que la tira cede primero. */
.lin-labels {
  flex: 0 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 4px;
  overflow: hidden;
}
.lin-label {
  max-width: 140px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Metadatos a la derecha: pills + fecha + estado. */
.lin-meta {
  flex-shrink: 0; display: flex; align-items: center; gap: 10px;
  white-space: nowrap;
}
/* El pill es un átomo, no una pieza de la fila: estaba escrito como
   `.lin-meta .pill`, así que los pills de afuera de la meta (los del panel de
   propiedades, los del título de una línea, y los «estimado» del detalle de la
   OC) se veían como texto pelado. Misma pinta, ahora en todos lados. */
.pill {
  font-size: 12px; color: var(--text-2);
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 20px; padding: 2px 10px;
}
.lin-date { font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.lin-date.is-overdue { color: #c62828; font-weight: 500; }
/* Total de la OC en la vista "Por proveedor" (≈ = estimado / incompleto). */
.lin-total {
  font-size: 12.5px; font-weight: 500; color: var(--text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.lin-total.muted { font-weight: 400; color: var(--text-3); }

/* La celda del autor (MGA-188 / ADR-0011). Es la ÚNICA columna que las dos
   metas comparten idéntica —mismo ancho de track (32px), mismo contenido (un
   círculo de 24px y nada más)— así que se escribe una vez: son la misma columna
   en dos listas, y duplicarla es invitar a que se separen sin que nadie lo
   decida. Las demás celdas sí van scopeadas por meta, porque cada lista tiene
   las suyas.

   `display: flex` deja el círculo centrado en su track sin depender de la línea
   base del texto, y —clave— hace que el salto de línea que deja el include NO
   cuente como contenido: sin autor la celda está y no dibuja nada, ni un
   espacio. El fantasma punteado de Linear se rechazó a propósito (es un botón
   para asignarte; el nuestro no va a ser clickeable nunca). */
.lin-meta .m-autor {
  justify-self: center;
  display: flex; align-items: center;
}

/* Meta de la fila de OC: GRID de anchos fijos (ADR-0004 §5, y la V1 del
   prototipo docs/prototypes/0004-ui-fila.html). La meta era un flexbox suelto:
   cada pieza medía lo que medía el dato, así que las fechas y los montos no se
   alineaban entre filas y no se podían comparar de un vistazo.
   Columnas: obra · CC · fecha · total · estado. La del total solo existe donde
   hay total (.con-total, la vista "Por proveedor"): la lista plana no calcula
   ningún precio. Es de Órdenes y no de .lin-meta a secas porque las otras listas
   tienen otras columnas; cuando les toque, cada una define la suya. */
/* obra · CC · autor · fecha · total · estado. La columna del total está SIEMPRE
   (las cuatro superficies de una OC lo muestran), aunque la OC no tenga precio:
   la celda dice "sin precio" y sostiene la alineación de la de al lado.
   El autor (32px, MGA-188) va TERCERO y no en una punta: obra y CC son dos
   pills que se leen como un bloque con el proveedor del título, y fecha/total
   son dos números alineados a la derecha que se comparan de arriba abajo. El
   círculo es el corte natural entre esas dos mitades; meterlo antes de la obra
   despegaría las pills del título, y meterlo al final lo dejaría discutiendo el
   borde derecho con el badge de estado. */
.lin-meta.meta-orden {
  display: grid;
  grid-template-columns: 176px 30px 32px 82px 116px 100px;
  align-items: center; gap: 10px;
}
.meta-orden .m-obra { min-width: 0; justify-self: start; }
/* La OC de varias obras ("QM101, QM102, QM103") no puede ensanchar su columna:
   se recorta con … y el nombre entero queda en el title. */
.meta-orden .m-obra .pill {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meta-orden .m-cc { justify-self: center; }
.meta-orden .m-cc .pill { padding: 2px 7px; font-size: 11px; }
/* Los números pegados al borde derecho de su columna: así se comparan de arriba
   abajo (con .lin-date/.lin-total ya en tabular-nums, las cifras caen en línea). */
.meta-orden .m-fecha, .meta-orden .m-total { justify-self: end; }
/* El badge ocupa toda su columna: los estados forman un bloque parejo, que es lo
   que hace escaneable la última columna. */
.meta-orden .m-estado { justify-self: stretch; }
.meta-orden .m-estado .badge { width: 100%; justify-content: center; }

/* Meta de la fila de PEDIDO: el mismo grid de anchos fijos (ADR-0004 §5), con
   las columnas que tiene esta lista — autor · fecha de necesidad · estado.
   Cada celda se emite siempre, aunque venga vacía (ver pedidos/_row.html): si no,
   un pedido sin fecha correría el estado a la primera columna y la columna
   dejaría de ser una columna.

   El autor (32px, MGA-188 / ADR-0011) DEROGA lo que decía este comentario, que
   era "el autor NO va en la lista: vive en el panel de propiedades (MGA-146)".
   El motivo de aquella decisión era que competía con el label, y era cierto de
   un TEXTO ("Francisco" al lado de "Reposición" son dos pastillas peleando el
   mismo lugar); un círculo de 24px es otro átomo visual y no compite. Va primero
   porque es lo único de la meta que no es un número: fecha y estado se comparan
   de arriba abajo pegados al borde derecho, y el círculo abre la meta del lado
   de los labels, que es de donde viene leyendo el ojo. */
.lin-meta.meta-pedido {
  display: grid;
  grid-template-columns: 32px 132px 100px;
  align-items: center; gap: 10px;
}
/* La fecha, pegada al borde derecho de su columna: así se comparan de arriba
   abajo (el badge de urgencia y la pill de fecha caen en la misma línea). */
.meta-pedido .m-fecha { justify-self: end; }
/* El badge ocupa toda su columna, igual que en Órdenes: los estados forman un
   bloque parejo, que es lo que hace escaneable la última columna. */
.meta-pedido .m-estado { justify-self: stretch; }
.meta-pedido .m-estado .badge { width: 100%; justify-content: center; }

/* Vencida: barrita roja a la izquierda de la fila. */
.lin-row.is-vencida { box-shadow: inset 2px 0 0 #c62828; }

.lin-empty { text-align: center; color: var(--text-2); padding: 56px 16px; font-size: 13.5px; }
.lin-empty strong { color: var(--text); font-weight: 500; }
.lin-load-more { padding: 10px 8px; }

/* Fila de carga continua al pie de la lista de materiales, siempre visible (sin
   modal): material + cantidad + "+" en el pedido, y con la OBRA adelante en la
   orden de compra, donde la obra es un dato de la línea (ADR-0010). */
.lin-add {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 8px;
  border-bottom: 1px solid var(--border);
}
.lin-add-obra { flex: 0 0 190px; min-width: 0; }
.lin-add-material { flex: 1 1 auto; min-width: 0; }
.lin-add-cantidad { flex: 0 0 110px; }
.lin-add-cantidad input { width: 100%; }
.lin-add .btn { flex-shrink: 0; }
/* El mismo renglón al pie de la lista de materiales del acopio (MGA-161), donde
   además de la cantidad va el precio. El precio pide más lugar que una cantidad:
   son pesos con centavos, no bolsas. */
.lin-add-precio { flex: 0 0 130px; }
.lin-add-precio input { width: 100%; }

/* El renglón del acopio crece hacia abajo cuando el alta no entró (MGA-161): el
   motivo aterriza ahí, en el ancho entero, debajo de los campos.

   `row-gap: 0` para que, cuando no hay nada que decir, el bloque vacío no le
   sume aire al renglón: el margen lo pone el propio error cuando existe. */
#item-inline { flex-wrap: wrap; row-gap: 0; }
#item-renglon-error { flex: 0 0 100%; }
.lin-add-error { margin-top: 8px; }

/* El renglón de la OC crece hacia abajo cuando tiene algo que decir sobre la
   línea que se está tecleando (MGA-163): el aviso del aporte cross-cliente —que
   se confirma con el Enter siguiente— o el motivo por el que no entró. Ocupa el
   ancho entero, debajo de los tres campos.

   `row-gap: 0` para que, cuando no hay nada que decir, el bloque vacío no le
   sume 8px de aire al renglón: el margen lo pone el propio aviso cuando existe. */
#orden-linea-inline { flex-wrap: wrap; row-gap: 0; }
#orden-renglon-aviso { flex: 0 0 100%; }
.lin-add-aviso, .lin-add-error { margin-top: 8px; }
.lin-add-confirmar { margin: 6px 0 0; font-size: 12.5px; color: var(--text-2); }

/* Ítems del pedido agrupados por obra. */
.obra-group { margin-bottom: 16px; }
.obra-group:last-child { margin-bottom: 0; }
.obra-group-head {
  display: flex; align-items: center; gap: 10px;
  font-size: 12.5px; font-weight: 600; color: var(--text-2);
  margin-bottom: 6px; padding-left: 2px;
}
.obra-group-obra { flex: 1 1 auto; min-width: 0; }

/* La plata de la OC en su detalle (MGA-122): el subtotal por obra en la cabecera
   del grupo, el precio unitario y el subtotal en cada línea, y el total al pie.
   Los tres son de la misma familia tipográfica (tabular, alineados a la derecha)
   para que la columna se lea de arriba abajo: lo que se ve, sumado, da el total. */
.obra-group-total {
  flex-shrink: 0; display: flex; align-items: center; gap: 6px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.lin-precio {
  flex: 0 0 96px; display: flex; align-items: center; gap: 3px;
  justify-content: flex-end;
  font-size: 12.5px; color: var(--text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.lin-precio-sign { color: var(--text-3); }
.lin-precio-val { overflow: hidden; text-overflow: ellipsis; }
.lin-precio-input {
  width: 76px; padding: 2px 6px; text-align: right;
  font-size: 12.5px; font-variant-numeric: tabular-nums;
  border: 1px solid var(--border); border-radius: 4px;
  background: var(--bg); color: var(--text);
}
.lin-precio-input:focus { outline: none; border-color: var(--border-strong); }

/* El subtotal manda sobre el unitario: es la plata de esa línea. */
.lin-subtotal {
  flex: 0 0 104px; text-align: right;
  font-size: 12.5px; font-weight: 500; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.orden-total-row {
  display: flex; align-items: center; gap: 8px;
  margin-top: 12px; padding: 10px 8px 0;
  border-top: 1px solid var(--border);
  font-size: 13px;
}
/* En la OC el renglón de carga se mete entre las filas y el pie (ADR-0010): el
   borde de abajo del renglón ya es la raya que separa, no van dos. */
.lin-add + #orden-total .orden-total-row { margin-top: 0; border-top: 0; }
.orden-total-label { font-weight: 600; }
.orden-total-val {
  margin-left: auto; font-weight: 600;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.orden-total-val.muted { font-weight: 400; color: var(--text-3); }

/* ── Migas de pan (ADR-0004 §6) ──────────────────────────────────────────── */
/* El camino por el que se entró: `Acopios › Bendito · CQ › OC #32`. La última
   miga es la pantalla actual (no es link); las de antes son el volver. */
.migas {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  font-size: 13px; margin-bottom: 10px;
}
.migas .miga { color: var(--text-2); }
.migas .miga:hover { color: var(--text); }
.migas .miga-sep { color: var(--text-3); }
.migas .miga-actual { color: var(--text); }

/* ── La pantalla del perfil (ADR-0011) ─────────────────────────────────────
   Dos inputs y un dato. Todo lo demás lo pone `.field`, que ya existía para los
   modales pero nunca estuvo scopeado a ellos: reusarlo es lo que hace que esta
   pantalla se lea igual que cualquier formulario de la app, y por eso acá abajo
   hay tres reglas y no un formulario nuevo.

   El ancho tope es la decisión visible: un input estirado a lo ancho de la hoja
   dice "escribí un párrafo", y acá se escribe un nombre. 420px es el mismo orden
   de magnitud que un modal (.modal tiene 460), que es el ancho al que el equipo
   ya está acostumbrado a cargar datos cortos. */
.perfil { max-width: 420px; }
.perfil .field { margin-bottom: 22px; }

/* El email NO es un input deshabilitado: un campo gris apagado se lee como
   "esto se podría editar y algo lo impide", y lo cierto es lo contrario — la
   identidad la emite Auth0 y no hay nada que editar. Se dibuja como dato. */
.perfil-fijo { font-size: 13.5px; color: var(--text); padding: 2px 0; }

/* ── Detalle (acopio) ───────────────────────────────────────────────────── */
.detail-head { margin-bottom: 24px; }
.detail-head .back {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--text-2); font-size: 13px; margin-bottom: 10px;
}
.detail-head .back:hover { color: var(--text); }
.detail-head .back svg { width: 15px; height: 15px; stroke: currentColor; }
.detail-head .detail-title-row { display: flex; align-items: center; gap: 16px; margin: 0 0 8px; }
.detail-head .detail-title-row .btn { margin-left: auto; flex-shrink: 0; }
.detail-head h1 { font-size: 20px; font-weight: 600; margin: 0; }
.detail-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }

.section { margin-bottom: 28px; }
.section-head {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 12px;
}
.section-head h2 { font-size: 14px; font-weight: 600; margin: 0; }
.section-head .btn { margin-left: auto; }
.section-head .sub { margin: 0; color: var(--text-3); font-size: 12.5px; }

.monto-big { font-size: 28px; font-weight: 600; letter-spacing: -.02em; }
.monto-big .muted { font-size: 15px; font-weight: 400; }

/* ── Los tres detalles: contenido a la izquierda, propiedades a la derecha ─── */
/* El panel tiene COLUMNA PROPIA. Antes flotaba (`float: right` + `sticky`), y esa
   combinación no se puede arreglar: un float sólo corre el texto que fluye a su
   lado, y una tabla no se acomoda alrededor — le pasa por debajo. Mientras el
   panel estaba quieto no se notaba, pero al pegarse viajaba hacia abajo, sobre la
   lista de materiales. Ponerle fondo y z-index lo hizo opaco: dejó de ser texto
   sobre texto y pasó a TAPAR las filas, que es igual de malo. Dos columnas no
   comparten espacio, así que el bug no puede volver a existir (user story 36). */
.detail-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 260px;
  gap: 28px;
}
/* Las tabs del acopio que no son Resumen no muestran panel: el contenido se queda
   con el ancho entero (decisión 2 de la etapa 3). */
.detail-cols.sin-props { grid-template-columns: minmax(0, 1fr); }
.detail-main { min-width: 0; }
/* La columna se estira a lo alto de la fila; lo que se pega es esto, así el panel
   tiene por dónde viajar sin salirse nunca de su columna. */
.props-sticky { position: sticky; top: 16px; }
/* El panel de la derecha son DOS cajas: arriba los responsables (que colapsan),
   abajo la identidad del negocio. El borde de 1px las separa sin necesidad de un
   título que explique que son dos cosas distintas, y le da al bloque colapsado
   una forma propia en vez de dejarlo como un renglón suelto arriba de una lista.

   Es un borde y NADA más —sin fondo, sin sombra—: el panel es una columna de
   datos para leer, y una caja con relleno se lee como un control. */
.props-box {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

/* Título del detalle de acopio (fuera de .detail-head, junto al float). */
.detail-title-row { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; }
.detail-title-row h1 { font-size: 20px; font-weight: 600; margin: 0; margin-right: auto; }
.detail-title-row .btn { flex-shrink: 0; }
.detail-notas { color: var(--text-2); margin: 0 0 16px; }

/* Título editable del detalle de una plantilla (ADR-0014, MGA-208): un
   <input> con la misma tipografía que el <h1> de los otros tres detalles,
   sin borde propio — el fondo al pasar el mouse es lo único que lo delata
   como editable, y al enfocarlo se ve como cualquier campo. */
.plantilla-nombre-input {
  font-size: 20px; font-weight: 600; border: none; background: transparent;
  color: var(--text); padding: 2px 6px; margin: -2px 0 6px -6px;
  border-radius: var(--radius); width: 100%; max-width: 480px;
}
.plantilla-nombre-input:hover { background: var(--bg-hover); }
.plantilla-nombre-input:focus { background: var(--bg-sunken); outline: none; }

/* La cantidad de una línea de plantilla: mismo alto que las filas de la
   tabla, chico y alineado a la derecha con el resto de .col-meta. */
.plantilla-cantidad-input {
  width: 72px; height: 26px; padding: 0 8px; text-align: right;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-sunken); color: var(--text); font-size: 13px;
}

/* Botón cuadrado sólo-ícono (ej: el "…" de más acciones). */
.btn-icon { padding: 0; width: 28px; justify-content: center; }
.btn-icon svg { width: 16px; height: 16px; }

/* Barra de acciones sobre el panel de propiedades: el botón "…" alineado a la
   derecha, arriba de las boxes. */
.props-actions { display: flex; justify-content: flex-end; margin-bottom: 10px; }

/* Menú de acciones (los 3 puntos): dropdown nativo <details> que abre un
   popover hacia abajo/izquierda, alineado al borde derecho del botón. */
.acciones-menu { position: relative; }
.acciones-trigger { list-style: none; }
.acciones-trigger::-webkit-details-marker { display: none; }
.acciones-menu[open] > .acciones-trigger { background: var(--bg-hover); }
.acciones-pop {
  position: absolute; top: calc(100% + 4px); right: 0; z-index: var(--z-popover);
  min-width: 160px; padding: 3px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-modal);
}
.acciones-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 6px 8px; border: none; background: transparent;
  border-radius: 5px; text-align: left;
  font-size: 12.5px; color: var(--text-2); cursor: pointer;
  text-decoration: none; white-space: nowrap;
}
.acciones-item:hover { background: var(--bg-hover); color: var(--text); }
.acciones-item svg { width: 15px; height: 15px; stroke: currentColor; flex-shrink: 0; }
.acciones-sep { height: 1px; background: var(--border); margin: 4px 2px; }
/* Eliminar, en el menú: el mismo tono que el botón destructivo del modal. */
.acciones-item.is-peligro, .acciones-item.is-peligro:hover { color: #a32d2d; }
.acciones-item.is-peligro:hover { background: #fbebeb; }

/* ── El vínculo a mano de un negocio suelto (AGR-414) ────────────────────
   Las dos listas de candidatas del modal de «Vincular a oportunidad»: la
   oportunidad en el paso 1, la orden en firme en el paso 2. Misma anatomía
   que `.ubi-opt` (nombre + secundario en la misma fila, hover parejo) pero
   sin el posicionamiento flotante de `.ubi-res`: acá la lista vive adentro
   del cuerpo del modal, no de un campo de formulario. */
.vincular-lista {
  display: flex; flex-direction: column; gap: 2px;
  max-height: 320px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 9px; padding: 4px;
}
.vincular-opt {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 8px 10px; border: 0; border-radius: 6px;
  background: transparent; text-align: left; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--text);
}
.vincular-opt:hover { background: var(--bg-hover); }
.vincular-opt-nombre { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vincular-opt-sec { color: var(--text-3); font-size: 12px; white-space: nowrap; flex-shrink: 0; }
/* El mismo rojo que `.field .err`, para el rechazo del paso 1 (otra
   oportunidad no es la que el negocio necesita — nunca debería pasar desde
   la lista, que ya filtra, pero un POST directo sí puede mandarla). */
.vincular-error {
  color: var(--danger); background: rgba(163, 45, 45, .06);
  border-radius: 5px; padding: 6px 9px; font-size: 12.5px; margin-bottom: 8px;
}

/* ── La ficha de una empresa ─────────────────────────────────────────────
   Dos bloques —contactos y ubicaciones— con la misma anatomía que el bloque de
   traspaso del cierre: quién a la izquierda, qué hacer a la derecha. Comparten
   la forma a propósito; son la misma pregunta («¿a quién le escribo?») en dos
   pantallas. */
.ficha-titulo { display: flex; align-items: center; gap: 12px; min-width: 0; }
.ficha-titulo .emp-logo { flex: none; }

.ficha-bloque { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border); }
.ficha-bloque-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 10px;
}
.ficha-bloque-head h2 { font-size: 15px; font-weight: 600; margin: 0; }

.ficha-filas { display: flex; flex-direction: column; }
.ficha-fila {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 10px 8px; margin: 0 -8px; border-radius: var(--radius);
}
/* El separador va entre filas y no debajo de la última: el borde del bloque
   siguiente ya cierra la lista, y dos líneas juntas se leen como un error. */
.ficha-fila + .ficha-fila { border-top: 1px solid var(--border); border-radius: 0; }
.ficha-fila:hover { background: var(--bg-hover); }

.ficha-fila-quien { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ficha-fila-nombre {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--text); min-width: 0;
}
/* La meta es una fila de datos separados por un punto medio dibujado en CSS: sin
   esto hay que decidir en el template cuál lleva separador y cuál no, y el
   template termina con un `{% if not forloop.last %}` por cada dato opcional. */
.ficha-fila-meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px;
  font-size: 12.5px; color: var(--text-2); min-width: 0;
}
.ficha-fila-meta > * + *::before { content: "·"; margin-right: 8px; color: var(--text-3); }

.ficha-fila-acciones { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* El kebab de estas filas se revela en hover como el de cualquier lista
   (ADR-0004 §2), pero acá la fila no es un `<tr>` ni un `.lin-row`, así que hay
   que repetir la regla sobre este contenedor. */
.ficha-fila .lin-kebab { opacity: 0; }
.ficha-fila:hover .lin-kebab, .ficha-fila .lin-kebab:focus-visible,
.ficha-fila .lin-kebab.is-open { opacity: 1; }

/* La papelera de una fila de archivo (AGR-304): el mismo hover-reveal que el
   kebab de arriba, y por el mismo motivo (ADR-0004 §2) — con el mouse afuera
   de la fila no hay razón para mostrar una acción destructiva al lado de
   "Bajar". Roja recién en su PROPIO hover, no en el de la fila: la fila entera
   se pone roja sería leer como que TODA la fila es la acción de borrar. */
.ficha-fila-borrar { opacity: 0; }
.ficha-fila:hover .ficha-fila-borrar, .ficha-fila-borrar:focus-visible { opacity: 1; }
.ficha-fila-borrar:hover { background: #fbebeb; color: #a32d2d; }

/* La fila se vuelve su propia confirmación (AGR-304) y no el modal genérico:
   mismo `.ficha-fila` — mismo alto, mismo padding, no salta la lista — con el
   texto a la izquierda donde antes iba el nombre del archivo. */
/* Mismo recorte que `.archivo-nombre`, y por el mismo motivo: la fila mide
   428px, los dos botones 145px y el gap 16px, así que a la pregunta le
   quedan 267px. Un nombre de teléfono ("WhatsApp Image 2026-08-28 at
   14.05.32.jpeg") mide 285px él solo -sin la pregunta alrededor-, así que sin
   recorte la frase envuelve, la fila crece de 48 a 68px y empuja a sus
   hermanas. `min-width: 0` es imprescindible: este div es un hijo flex de
   `.ficha-fila`, y sin eso el `overflow: hidden` no recorta nada -un ítem
   flex no baja de su ancho de contenido-. El `?` de cierre se pierde en el
   nombre largo -el `…` lo reemplaza-, igual que hoy se pierde la extensión de
   un nombre de archivo recortado: es el mismo trade-off que ya acepta esta
   pantalla, no uno nuevo. */
.ficha-fila-confirmar-texto {
  font-size: 13px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}

/* El vacío explica PARA QUÉ sirve el bloque, no sólo que está vacío: es la
   primera vez que alguien lo ve y no es obvio que de acá sale el botón de
   WhatsApp de la pantalla de carga. */
.ficha-vacio {
  padding: 14px 0; font-size: 12.5px; color: var(--text-3); line-height: 1.6;
}
.ficha-vacio strong { color: var(--text-2); font-weight: 500; }

/* ── El bloque de una etapa, dentro del documento ─────────────────────────
   «Carga» vive abajo del contrato y en la misma columna. Se separa con un borde
   y no con una tarjeta flotante: es otra sección del MISMO documento, y una caja
   con sombra la haría leer como un widget pegado encima. */
.doc-etapa { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border); }
/* La primera etapa arranca pegada al subtítulo: su borde superior estaría a un
   renglón del encabezado y se leería como un subrayado del título, no como un
   separador entre secciones. */
.doc-etapa.is-primera { margin-top: 20px; padding-top: 0; border-top: none; }
.doc-etapa-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 14px;
}
.doc-etapa-head h2 { font-size: 15px; font-weight: 600; margin: 0; }
.doc-etapa-quien {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--text-2);
}

/* El aviso de «falta tu teléfono»: es el único aviso con una acción adentro, y
   por eso necesita alinear el botón a la derecha. El resto de los `.aviso` son
   sólo texto. */
.aviso-perfil { display: flex; align-items: center; gap: 12px; }
.aviso-perfil .aviso-texto { flex: 1 1 auto; }
.aviso-perfil .btn { flex: none; }

/* ── Las etapas del negocio, como pestañas ────────────────────────────────
   Reusa `.tab-bar` / `.tab` de Configuración tal cual: es el mismo mecanismo
   (links con `?tab=`, el server manda el panel) y tiene que verse igual. Lo único
   que se suma es el dot del estado adentro del link. */
/* La barra de ETAPAS del negocio, más apretada que la de Configuración: allá son
   cinco pestañas de una palabra, acá son seis con dot, avatar y badge. Con el
   padding de 12px la barra medía 864px y se partía en dos líneas en cualquier
   notebook de 1280. Es una clase propia y no un override de `.tab` para no
   angostar las pestañas de Configuración, que no tienen el problema. */
.tab-etapas { gap: 0; flex-wrap: wrap; row-gap: 0; }
.tab-etapas .tab { padding-left: 7px; padding-right: 7px; gap: 5px; }
.tab-etapas .avatar-p { width: 16px; height: 16px; font-size: 8px; }
/* El badge de «todavía no» en una pestaña es un reloj, no texto: es lo que hace
   que la barra entre en una línea (ver `_tabs_items.html`). Sin fondo ni borde —
   a este tamaño la píldora es más ruido que el ícono. */
.badge-icono {
  display: inline-flex; align-items: center;
  padding: 0; background: none; border: none; line-height: 0;
}
.badge-icono svg { width: 13px; height: 13px; stroke: currentColor; }
.tab { display: inline-flex; align-items: center; gap: 6px; }
.tab .lin-status { width: 9px; height: 9px; }
/* El círculo de quién tiene la etapa, más chico que en una fila de lista: acá
   convive con el texto de la pestaña y a 24px la dominaba. */
.tab .avatar-p { width: 18px; height: 18px; font-size: 8.5px; letter-spacing: .2px; }

/* Las etapas que todavía no tienen pantalla. NO son links y se nota: sin cursor
   de mano, sin hover, y con el mismo badge «Próximamente» que el sidebar usa para
   las secciones que faltan. Se ven para que la barra cuente el proceso entero;
   no se abren para no prometer una pantalla que no existe.

   Igual muestran el círculo del responsable, porque a una etapa se le puede
   asignar alguien antes de que exista su panel — y saber quién va a ver la
   liquidación es justamente lo que el cierre bien armado tiene que dejar dicho. */
.tab.is-stub { color: var(--text-3); cursor: default; }
.tab.is-stub:hover { color: var(--text-3); border-bottom-color: transparent; }
/* En una pestaña va más chica que en el sidebar: son cuatro seguidas, y a tamaño
   de sidebar la barra se pasa de una línea. */
.tab .badge-soon { margin-left: 1px; font-size: 9px; padding: 0 5px; line-height: 13px; }

/* Los dos hechos de los que sale el estado, arriba del panel de la etapa. Es una
   grilla y no dos renglones sueltos para que la etiqueta y el valor caigan en la
   misma vertical que el panel de propiedades de la derecha. */
.etapa-datos {
  display: flex; flex-wrap: wrap; gap: 8px 32px;
  padding-bottom: 16px; margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
/* En la Carga el bloque de traspaso ya no abre el panel: lo abre la lista de
   equipos, y esto arranca abajo. El aire lo pone la coordinación y no la lista,
   porque `.viajes` es la misma pieza que usa la Descarga y ahí no hay nada
   debajo de lo que separarse. */
.viajes + .etapa-datos { margin-top: 24px; }
/* `flex: none` para que no se encojan: son ítems de un contenedor flex y por
   default se achican hasta el mínimo, y ahí el selector de responsable —que
   trunca con ellipsis— mostraba «Ca...» teniendo lugar de sobra para «Carlota».
   Si no entran los dos, que envuelvan (el contenedor es `wrap`), no que se
   recorten. */
.etapa-dato { display: flex; align-items: center; gap: 10px; min-height: 28px; flex: none; }
.etapa-dato-k { font-size: 12.5px; color: var(--text-3); flex: none; }
/* `min-width: max-content` porque este span es ítem flex de `.etapa-dato` y, como
   tal, se encoge por debajo de su contenido: el selector de responsable trunca
   con ellipsis y mostraba «Ca...» con lugar de sobra para «Carlota». El ellipsis
   sigue existiendo para el caso real que lo justifica —«Santiago García
   Castellanos»— y por eso hay un tope, en vez de sacarlo. */
.etapa-dato-v { font-size: 13px; color: var(--text); flex: none; min-width: max-content; }
.etapa-datos .asignar-trigger { max-width: 240px; }
/* El motivo por el que «Cerrar descarga» está deshabilitado, al lado del
   botón — mismo tono apagado que `.etapa-dato-k`. */
.descarga-cierre-motivo { font-size: 12.5px; color: var(--text-3); }

/* El estado de la etapa, con la fecha que lo produce al lado. Van pegados a
   propósito: el estado no se elige, sale de la fecha. */
.carga-estado { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; }
.carga-estado-txt { color: var(--text-2); }
.carga-fecha input {
  border: 1px solid transparent; background: transparent;
  border-radius: 5px; padding: 2px 4px; margin: -2px 0;
  font-family: inherit; font-size: 12.5px; color: var(--text-2);
  font-variant-numeric: tabular-nums; cursor: pointer;
}
.carga-fecha input:hover { border-color: var(--border); background: var(--bg); }
.carga-fecha input:focus { outline: none; border-color: var(--brand); background: var(--bg); }

/* Los estados de la carga. La convención es la de toda la app: HUECO = todavía
   no pasó, LLENO = pasó. Sin asignar es gris porque no está pasando nada;
   asignada es ámbar porque hay alguien esperando hacer algo; coordinada es el
   teal de la marca y va llena — el acuerdo con el vendedor ya ocurrió.

   En curso es el verde que la app ya usa para «en camino» y «enviada», y no un
   color nuevo: es literalmente lo mismo —hay un camión moviéndose— y aprender
   dos verdes para la misma idea no le sirve a nadie.

   Completada es el púrpura de «recibida» y «recibido», y va LLENO. La app tiene
   dos convenciones para lo terminal y se parten por significado: púrpura lleno
   es «la escalera llegó al final» —el cierre de `creada › enviada › presupuesto
   › confirmada › recibida`, acá abajo—, y hueco apagado es «esto ya no está
   vivo» —`cerrada` de Oportunidades, `cerrado` de Acopio—. Ésta es una escalera
   de avance con final, no una de vida y muerte, así que le toca la primera; y la
   regla de arriba manda igual: completada PASÓ, o sea que va llena.

   El gris hueco de Acopio queda descartado por algo concreto: `sin_asignar` ya
   es ese mismo gris hueco, así que los dos extremos de esta escalera se verían
   idénticos en la misma lista. */
.lin-status.dot-sin_asignar { color: var(--text-3); }
.lin-status.dot-asignada    { color: #e08a00; }
.lin-status.dot-coordinada  { color: var(--brand); background: currentColor; }
.lin-status.dot-en_curso    { color: #2e7d32; background: currentColor; }
.lin-status.dot-completada  { color: #7b1fa2; background: currentColor; }

/* Los tres botones de contacto de la etapa, en una sola fila (AGR-324,
   variante E1 del prototipo, CSS literal). El bloque de aviso al responsable
   —`.traspaso-aviso`, con su propia línea de texto— y las dos puntas sueltas
   de AGR-323 vivían separados; ahora las tres acciones —a quién le hablo—
   comparten un único renglón.

   `flex-wrap: wrap` viene de `.traspaso-partes` (AGR-323), que lo puso a
   propósito porque «en pantalla angosta dos botones con texto no siempre entran
   en el mismo renglón», y se perdió al renombrar el contenedor. La fila mientras
   tanto creció a tres botones más la línea divisoria más el ícono de copiar: el
   motivo de tenerlo es más fuerte ahora que cuando se escribió, no más débil. El
   prototipo no lo define, pero su mock tampoco vive en el ancho real de la
   pestaña. */
.carga-acciones { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* La línea que separa «Copiar el aviso» —acción secundaria y tenue, sin
   texto visible, sólo el ícono— de los tres botones de contacto, que son la
   acción principal de la barra. */
.carga-acciones-sep { width: 1px; height: 18px; background: var(--border); margin: 0 2px; flex: 0 0 auto; }
/* El «+» de un botón degradado va a 13px y no a los 15px que `.btn svg` le da
   al ícono de WhatsApp: es un signo, no un logo, y así queda del mismo tamaño
   que el «+» del popover de «Asignar responsable» (`.asignar-vacio svg`), que
   es el otro degradado de la misma fila. El prototipo lo baja igual, con un
   `style` inline sobre el `<svg>`. */
.carga-accion-falta svg { width: 13px; height: 13px; }

/* El tercer botón, cuando no hay responsable asignado, es el popover del panel
   de la derecha reusado tal cual (AGR-325), y ahí su disparador es un valor de
   una columna de datos —discreto a propósito, ver `.asignar-trigger`— mientras
   que acá es el tercero de tres botones. Se le da la caja de un `.btn-sm
   .btn-ghost` —los mismos 28px de alto, el mismo padding lateral y el mismo
   radio— para que la fila no tenga dos botones y una tercera cosa, que es lo
   que dibujan los estados 3.3 y 3.5 del prototipo.

   El `margin: -3px -6px` de `.asignar-trigger` allá es correcto —sangra la zona
   de hover para que el nombre quede alineado con los demás valores de la
   columna— y acá se le comía 6px al `gap: 10px` de cada lado, dejando al tercer
   botón visiblemente más pegado que los otros dos. Se va entero y no sólo el
   horizontal: el vertical estaba compensando el padding de allá, y acá la altura
   es explícita. */
.carga-acciones .asignar-trigger {
  margin: 0; height: 28px; padding: 0 8px; border-radius: var(--radius);
}
.carga-acciones .asignar-vacio { color: var(--text-2); }
.carga-acciones .asignar-trigger:hover .asignar-vacio { color: var(--text); }
.traspaso-camion {
  display: flex; align-items: center; gap: 6px;
  margin-top: 12px; font-size: 12.5px; color: var(--text-2);
}
.traspaso-camion svg { width: 15px; height: 15px; stroke: currentColor; }

/* ── Los equipos que salieron ──────────────────────────────────────────────
   El bloque de viajes dentro de la pestaña Carga. En pantalla se dice «equipo»
   —la palabra del negocio— y la tabla se llama `Viaje`; las clases siguen al
   schema, que es lo que hace que el CSS se pueda encontrar desde el modelo.

   La fila NO es una clase nueva: es `.lin-row`, la misma de todas las listas de
   la app, y ahora **sin ninguna variante**. Tenía un `cursor: default` puesto a
   propósito mientras no hubiera nada que abrir; desde que la fila entera abre la
   corrección, ese override decía lo contrario de lo que la fila hace. El
   `cursor: pointer` y el hover salen de `.lin-row`, y el kebab que aparece al
   pasar por encima, de `.lin-row:hover .lin-kebab`. */
.viajes { margin-top: 24px; }
/* Los 24px de arriba son para despegarse de lo que venga antes. En la Carga la
   lista abre el panel —los equipos son lo primero— y no hay de qué despegarse:
   ahí el aire queda de más y el que separa lo pone la coordinación de abajo.

   Va contra `.tab-panel-carga` y no contra `:first-child` a secas para no tocar
   la Descarga, donde la lista también abre el panel pero el aire de arriba es el
   que ya tenía y se deja como estaba. */
.tab-panel-carga > .viajes { margin-top: 0; }
.viajes-head {
  display: flex; align-items: center; gap: 12px;
  padding-bottom: 8px; border-bottom: 1px solid var(--border);
}
.viajes-titulo { font-size: 13px; font-weight: 600; color: var(--text); }
/* El conteo, cuando lo hay. Sin equipos no se dibuja: un cero es una promesa
   incumplida —promete una lista que no existe— repetida en cada pantalla. */
.viajes-conteo { margin-left: 5px; font-weight: 400; color: var(--text-3); }
.viajes-head .btn { margin-left: auto; }

/* `tabular-nums` en los tres números de la fila, y es lo que hace la lista.
   Con cifras de ancho variable, cinco cartas de porte y cinco pesos no se
   comparan de un vistazo: hay que leerlos uno por uno. */
.viaje-cp { font-variant-numeric: tabular-nums; font-size: 13px; }
/* Un equipo sin documento se ve COMPLETO, no roto: «Sin carta de porte» es prosa y
   no un número, así que pierde el tabular y toma el gris del dato ausente. Es
   el caso mayoritario, no el excepcional. */
.viaje-cp.is-vacia { color: var(--text-3); font-variant-numeric: normal; }
.viaje-meta { font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.viaje-derecha { margin-left: auto; display: flex; align-items: center; gap: 16px; }
/* La fecha reserva su ancho por el mismo motivo que los kilos: casi ningún equipo
   la trae, y sin el `min-width` los kilos de las filas sin fecha se corren 16px a
   la derecha y las dos columnas dejan de alinearse — que es justo lo que la lista
   viene a permitir, comparar cinco equipos sin leerlos. */
.viaje-fecha { min-width: 42px; text-align: right; }
/* El `min-width` alinea la columna de kilos aunque un equipo diga «sin kilos» y
   el de al lado «30.150 kg»: sin él, la coma flota de fila en fila. */
.viaje-kg { font-size: 13px; font-variant-numeric: tabular-nums; min-width: 78px; text-align: right; }
.viaje-kg.is-vacia { color: var(--text-3); }
.viajes-vacio { padding: 26px 8px; font-size: 13px; color: var(--text-3); }

/* ── El documento del camión ───────────────────────────────────────────────
   El bloque de archivos del modal del equipo y el clip de la fila. Se apoya
   entero en `.ficha-bloque` / `.ficha-fila` / `.ficha-vacio`, que ya son la
   anatomía del bloque de contactos de la ficha de empresa. Ningún token nuevo. */

/* El clip dice que ese camión ya tiene el documento, con el conteo al lado. Sólo se
   dibuja cuando hay algo: un clip en gris para «no hay» sería el cero que esta
   misma lista decidió no dibujar en el encabezado. Toma el tamaño y el color del
   ícono de la fila (`.lin-row .lin-icon`) para que no compita con la carta de
   porte, que es lo que se lee primero. */
.viaje-clip {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums;
}
.viaje-clip svg { width: 14px; height: 14px; stroke: currentColor; }

/* `.ficha-bloque` nace para una ficha, que respira: 28px de margen y 20 de
   padding. Adentro de un modal de 460 eso es medio bloque de aire antes del
   título, y empuja el pie fuera de la pantalla en un notebook. La línea de
   arriba se queda —es lo que separa el documento de los campos del viaje— y el aire
   se achica a la mitad. */
.ficha-bloque-modal { margin-top: 18px; padding-top: 14px; }

/* En el PASO DEL DOCUMENTO (AGR-298) el bloque no separa nada de nada: es lo único
   que hay en la pantalla, ya adentro de su sección y pegado a su título. La
   línea de arriba y el aire existen para despegarlo de los campos del viaje, y
   ahí no hay campos de los que despegarse — dejarlos dibuja un separador entre
   un título y su propio contenido. */
.ficha-bloque-documento { margin-top: 0; padding-top: 0; border-top: 0; }

/* El control de adjuntar es UNO: el botón. El input de archivo nativo —que mide
   ~240px con su «Sin archivos seleccionados» y no entra al lado del título de un
   modal de 460— ya no se dibuja, porque tampoco hay nada que mostrar: elegir el
   archivo lo sube, así que el estado «elegido pero no subido» no existe.

   Se esconde con el mismo `clip-path` que `.grano input` y por el mismo motivo:
   sigue siendo el input que el teclado alcanza y el que los lectores de pantalla
   anuncian. Con `display:none` o con tamaño cero se cae del orden de tabulación
   y del árbol de accesibilidad, y el único control quedaría siendo un botón que
   no es el que sube. */
.archivo-elegir { display: flex; align-items: center; }
.archivo-input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* Y el foco se ve sobre el botón, que es lo único dibujado: sin esto, quien
   navega con el teclado tabula hasta un elemento invisible y el anillo no
   aparece en ningún lado. Mismo recurso que `.grano input:focus-visible`. */
.archivo-input:focus-visible + .btn { outline: 2px solid var(--brand); outline-offset: 2px; }

/* El ícono del archivo, del mismo tamaño y del mismo gris que el del camión en
   la lista de equipos (`.lin-row .lin-icon`): son la misma clase de dato —qué es
   esta fila— y verlos con dos pesos distintos en la misma pantalla se lee como
   un descuido. */
.archivo-icono {
  width: 16px; height: 16px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; color: var(--text-3);
}
.archivo-icono svg { width: 15px; height: 15px; stroke: currentColor; }
/* El nombre viene del teléfono y puede ser `WhatsApp Image 2026-08-30 at
   11.42.07 (1).jpeg`: sin el corte, una fila estira el modal y desalinea el
   resto. Se corta al final, que es donde está la parte que no identifica nada. */
.archivo-nombre { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* El rechazo del archivo es el MISMO rojo del error de un campo (`.field .err`)
   porque es la misma clase de hecho: algo que se mandó y no entró. La regla se
   repite en vez de reusarse porque aquélla cuelga de un `.field`, y en este
   bloque no hay ninguno — no hay label, no hay input con borde que teñir. */
.archivo-err {
  color: var(--danger); background: rgba(163, 45, 45, .06);
  border-radius: 5px; padding: 6px 8px; font-size: 12px; margin-bottom: 8px;
}

/* El estado de la lectura del documento (AGR-299, AGR-301): leyendo, leyó, falló,
   no está configurada, o trae varios camiones juntos. Una sola caja para los
   cinco, porque los cinco contestan la misma pregunta —qué pasó con el
   archivo que acabo de subir— y darles cinco formas distintas obligaría a
   leer la forma antes que el texto.

   Lo que cambia entre uno y otro es sólo el COLOR del ícono, no la caja: el
   fondo hundido es el mismo, y así el bloque no salta de tamaño cuando el
   estado cambia solo debajo del cursor. */
.lectura {
  display: flex; align-items: center; gap: 8px; margin-top: 10px;
  padding: 8px 10px; border-radius: 7px;
  background: var(--bg-sunken); color: var(--text-2); font-size: 12.5px;
}
.lectura svg { width: 15px; height: 15px; flex-shrink: 0; stroke: currentColor; }
.lectura-listo svg { color: var(--brand); }
/* El mismo ámbar del `.campo-aviso`, y por el mismo motivo: los tres dicen
   «algo no cargó y se puede seguir igual». En rojo se leerían como un
   rechazo, y acá no hay nada rechazado — `lectura-falla` guardó el archivo
   igual, y `lectura-varios` (AGR-301) ni siquiera falló: leyó bien y avisa
   que hay que cargar a mano, lo mismo que dice `lectura-off`. */
.lectura-falla svg, .lectura-off svg, .lectura-varios svg { color: #a06a12; }
/* El botón de reintentar se va a la derecha, que es donde vive la acción de una
   fila en toda la app (`.ficha-fila-acciones`). */
.lectura .btn { margin-left: auto; flex-shrink: 0; }

/* El giro mientras el modelo contesta. Reusa el `ubi-giro` del buscador de
   ubicaciones —misma espera, misma respuesta visual— en vez de declarar un
   segundo keyframe que dice lo mismo. A diferencia de aquél éste NO cuelga de
   `.htmx-request`: acá el elemento entero existe sólo mientras se está leyendo,
   así que gira siempre que está en pantalla. */
.lectura-giro {
  width: 13px; height: 13px; flex-shrink: 0; border-radius: 50%;
  border: 1.5px solid var(--border-strong); border-top-color: var(--brand);
  animation: ubi-giro .6s linear infinite;
}

/* ── Lo que agrega la mitad de DESTINO ─────────────────────────────────────
   La pestaña Descarga lista las mismas filas con las columnas del otro lado, así
   que reusa todo lo de arriba y suma cinco reglas. Ningún token nuevo. */

/* El avance en toneladas, a la derecha del encabezado. Ocupa el lugar que en la
   Carga tiene el botón «Nuevo equipo»: la Descarga no crea, completa. */
.viajes-avance {
  margin-left: auto; display: flex; align-items: center; gap: 9px;
  font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums;
}
.viajes-avance b { color: var(--text); font-weight: 600; }
.viajes-barra { width: 84px; height: 4px; border-radius: 2px; background: var(--bg-sunken); overflow: hidden; }
/* La excepción anotada en el encabezado de este archivo: el único teal que mide
   algo en vez de marcar una acción. El ancho lo pone el server —recortado a 100
   por `_avance_barra()`— porque es un dato, no una decisión de estilo. */
.viajes-barra span { display: block; height: 100%; background: var(--brand); }
/* La mitad de origen adentro de la fila: contra qué se compara el ticket que se
   está cargando. Un punto más chica que el resto de la fila y en el gris del dato
   de fondo, para que se lea cuando se la busca y no compita con los kilos. */
.viaje-origen { font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; }
/* La humedad reserva su ancho por lo mismo que la fecha y los kilos: sin el
   `min-width`, un «sin dato» corre la columna de los kilos de esa fila. */
.viaje-hum { font-size: 12.5px; color: var(--text-2); font-variant-numeric: tabular-nums; min-width: 46px; text-align: right; }
.viaje-hum.is-vacia { color: var(--text-3); }
/* La marca de la cola de trabajo. Es gris y no un badge de color a propósito: un
   equipo sin ticket todavía no es un problema, es lo normal el día que salió. */
.viaje-falta { font-size: 12.5px; color: var(--text-3); }
.viajes-vacio-accion { margin-top: 9px; }

/* El neto a liquidar, al pie de «Calidad» en el modal del ticket. Es lo único que
   la app CALCULA en toda la pantalla de Descarga, así que no se ve como un campo:
   ocupa el ancho entero, no tiene caja de input y el número va en negrita.

   La cuenta —«30.040 − 410»— va en gris y a la izquierda del resultado, que es el
   orden en que se lee: primero de dónde sale, después cuánto da. Que esté a la
   vista es lo que permite controlarla de un vistazo en vez de tener que confiar.

   `tabular-nums` por lo mismo que en las listas: los kilos cambian mientras se
   tipea, y sin ancho fijo de dígito el número salta a cada tecla. */
.neto-liquidar {
  display: flex; align-items: baseline; gap: 8px;
  margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border);
}
.neto-liquidar-label { font-size: 12.5px; color: var(--text-2); }
.neto-liquidar-cuenta {
  margin-left: auto; font-size: 12.5px; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.neto-liquidar-total {
  font-size: 13.5px; font-weight: 600; font-variant-numeric: tabular-nums;
}

/* El desglose de la Cámara, al pie de «Calidad» y arriba del neto a liquidar.
   Es la misma familia que `.neto-liquidar` —ancho entero, sin caja de input,
   `tabular-nums` porque los números cambian a cada tecla— pero son tres renglones
   y no uno, así que en vez de un flex es una grilla de tres columnas: el
   concepto a la izquierda, el porcentaje y los kilos alineados a la derecha.

   Las mismas filas que muestra la calculadora de la Cámara, que es lo que este
   pie reemplaza. Los dos conceptos van separados porque discutir con la planta
   pide poder señalar cuál de los dos no cierra.

   Va ARRIBA del neto a liquidar y no abajo porque se lee en ese orden: de dónde
   salió la merma, y recién después los kilos por los que se paga. El borde
   superior lo trae este pie y el del neto queda como separador entre los dos. */
.merma-camara {
  margin-top: 10px; padding-top: 9px; border-top: 1px solid var(--border);
}
.merma-camara-titulo { font-size: 12.5px; color: var(--text-2); margin-bottom: 3px; }
.merma-camara-fila {
  display: grid; grid-template-columns: 1fr auto auto; gap: 8px;
  align-items: baseline; font-size: 12.5px; color: var(--text-3);
}
.merma-camara-concepto { color: var(--text-2); }
.merma-camara-pct, .merma-camara-kg { font-variant-numeric: tabular-nums; }
.merma-camara-kg { min-width: 58px; text-align: right; }
/* El total no repite el porcentaje —sumarlos no significa nada— así que su celda
   de kilos se salta la columna del medio y queda alineada con las de arriba. */
.merma-camara-total { margin-top: 2px; }
.merma-camara-total .merma-camara-concepto,
.merma-camara-total .merma-camara-kg { color: var(--text); font-weight: 600; }
.merma-camara-total .merma-camara-kg { grid-column: 3; }

/* La frase que reemplaza a los tres ceros cuando la humedad no llegó al umbral
   de recibo. Va en el tono del resto del pie y no en ámbar: no es un aviso, es el
   resultado — la cuenta corrió y no corresponde descontar nada. */
.merma-camara-tolerancia { font-size: 12.5px; color: var(--text-2); }

/* El aviso de un campo, que **no es un error**: `.err` es rojo y significa que no
   se puede guardar; esto es ámbar y significa que se puede guardar igual. Que se
   vean distinto es la mitad del mensaje — el caso que lo estrena es el bruto
   menos la tara que no da el neto, donde un documento mal transcripto tiene que poder
   entrar mientras se averigua cuál de los tres números está mal.

   El ámbar NO es el `#e08a00` del dot de «Asignada», y la diferencia tiene motivo:
   ese valor está elegido para un círculo de 7px sobre fondo claro y como texto de
   12px no llega al contraste mínimo. Éste es el mismo ámbar oscurecido hasta que
   se lee. Son dos usos distintos del mismo concepto —«esto espera algo»— y por eso
   son dos valores; si algún día se declara un token de ámbar, van los dos juntos. */
.campo-aviso {
  margin-top: 5px; font-size: 12px; color: #a06a12;
  display: flex; gap: 5px; align-items: flex-start;
}
.campo-aviso svg {
  width: 13px; height: 13px; stroke: currentColor; flex: none; margin-top: 2px;
}

/* Un teléfono es un valor atómico: se corta entero a la línea siguiente o no se
   corta, pero nunca por la mitad. Sin esto «+54 9 351 513-4266» se parte en
   cuatro renglones apenas la tabla se angosta, y deja de leerse como un número. */
.cell-tel { white-space: nowrap; }

/* El hueco del responsable en la lista de Cargas. `_avatar.html` no dibuja nada
   sin email (ADR-0011 rechazó el fantasma para el AUTOR, que es inmutable), pero
   en Cargas el responsable vacío es justo lo que hay que ver: sin un círculo
   punteado, las filas sin asignar se pierden entre las que tienen una inicial.
   Del mismo tamaño que `.avatar-p` para que la columna no se mueva. */
.avatar-vacio {
  display: inline-block; width: 24px; height: 24px;
  border-radius: 50%; border: 1px dashed var(--border-strong);
}

/* El conteo al lado de cada opción del selector de Cargas: es lo que lo vuelve
   un tablero y no sólo un filtro. `tabular-nums` para que las cifras se alineen
   de arriba abajo en el menú abierto. */
.view-menu-conteo {
  margin-left: auto; padding-left: 10px;
  font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums;
}
.view-menu-opt.is-active .view-menu-conteo { color: var(--text-2); }

/* ── Asignar responsable ──────────────────────────────────────────────────
   El control de asignación reusa el popover del kebab (`.acciones-*`) y sólo
   aporta lo que no existía: un disparador que se ve como un VALOR y no como un
   botón, y el estado "sin asignar", que acá sí se dibuja.

   Por qué el disparador es discreto: vive adentro de un `.prop-value`, entre
   Vendedor y CUIT, y si tuviera borde y fondo sería el único botón de una
   columna de datos — se leería como la acción principal de la pantalla, cuando
   la acción principal es bajar el PDF. Se revela en hover, que es el mismo
   gesto con el que la app revela el checkbox y el kebab de una fila. */
.asignar-trigger {
  display: inline-flex; align-items: center; gap: 7px;
  margin: -3px -6px; padding: 3px 6px;
  border-radius: 5px; cursor: pointer;
  max-width: 100%; min-width: 0;
}
.asignar-trigger:hover { background: var(--bg-hover); }
/* `min-width: 0` es lo que habilita el ellipsis: sin él un ítem flex se niega
   a encogerse por debajo de su contenido y el nombre desborda en vez de cortarse. */
.asignar-nombre { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* El hueco SÍ se dibuja, al revés que el círculo del autor (ADR-0011 rechazó el
   fantasma ahí porque el autor es inmutable). Acá el hueco es la invitación. */
.asignar-vacio {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--text-3); font-size: 12.5px;
}
.asignar-vacio svg { width: 13px; height: 13px; stroke: currentColor; }
.asignar-trigger:hover .asignar-vacio { color: var(--text-2); }

/* Hereda el `right: 0` de `.acciones-pop`, o sea que crece hacia la IZQUIERDA
   desde el borde derecho del disparador. Es lo que corresponde acá y no una
   copia por inercia: el panel de propiedades es la última columna de la pantalla,
   así que un popover anclado a la izquierda del disparador crece hacia afuera del
   viewport y se corta. Con `max-width` además, porque `.acciones-item` es
   `nowrap` y un nombre largo («Santiago García Castellanos») estiraba la caja
   hasta salirse igual. */
.asignar-pop { min-width: 210px; max-width: 260px; }
.acciones-item.is-active { color: var(--text); font-weight: 500; }
.asignar-check { margin-left: auto; display: flex; color: var(--brand); }
.asignar-check svg { width: 14px; height: 14px; }
.asignar-nadie {
  padding: 8px; font-size: 12.5px; color: var(--text-3); line-height: 1.5;
  max-width: 220px;
}
.asignar-nadie a { color: var(--brand); }

/* La misma lista, en el modal de la fila. Es más alta y muestra el teléfono:
   acá hay lugar, y saber si esa persona tiene WhatsApp cargado es parte de la
   decisión de a quién asignarle. */
.asignar-sub { margin: 0 0 12px; }
.asignar-lista { display: flex; flex-direction: column; gap: 2px; }
.asignar-opcion {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px; border: none; background: transparent;
  border-radius: var(--radius); text-align: left; cursor: pointer;
  font-size: 13px; color: var(--text); font-family: inherit;
}
.asignar-opcion:hover { background: var(--bg-hover); }
.asignar-opcion.is-active { background: var(--brand-soft); }
.asignar-opcion svg { width: 15px; height: 15px; stroke: currentColor; flex-shrink: 0; }
.asignar-opcion-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.asignar-tel { font-size: 11.5px; color: var(--text-3); }
.asignar-opcion.is-quitar { color: var(--text-2); margin-top: 4px; }
.asignar-opcion.is-quitar:hover { color: var(--danger); }

/* ── Configuración › Avisos: el estado de cada canal ─────────────────────────
   Filas y no cajas, como el resto de Configuración. Un canal apagado no se
   esconde ni se pinta de rojo: se apaga y dice qué variable falta — no es un
   error, es que en este entorno no está configurado. */
.canales { display: flex; flex-direction: column; gap: 2px; }
.canal {
  display: flex; align-items: center; gap: 12px; padding: 12px 0;
  font-size: 13px;
}
.canal + .canal { border-top: 1px solid var(--border); }
.canal-icono {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex-shrink: 0;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-sunken); color: var(--text-2);
}
.canal-icono svg { width: 15px; height: 15px; stroke: currentColor; }
.canal.is-off .canal-icono { color: var(--text-3); }
.canal-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.canal-nombre { color: var(--text); font-weight: 500; }
.canal-detalle { font-size: 12px; color: var(--text-2); }
.canal-detalle code {
  font-size: 11.5px; padding: 1px 4px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 4px;
}
/* El desvío en ámbar y con triángulo: significa que NADA llega a su
   destinatario real, y es lo que alguien deja prendido sin querer. */
.canal-desvio {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; color: #e08a00;
}
.canal-desvio svg { width: 13px; height: 13px; stroke: currentColor; }
.canal-estado {
  margin-left: auto; flex-shrink: 0;
  font-size: 12px; color: var(--text-2); white-space: nowrap;
}

/* El resumen de envíos, que **es el filtro**: cada caja recorta la lista a su
   estado. Cajas y no píldoras porque el número es el dato —se lee de lejos— y una
   píldora lo dejaría al tamaño de su etiqueta. */
.resumen { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
.resumen-caja {
  display: flex; flex-direction: column; gap: 1px;
  min-width: 104px; padding: 9px 12px;
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  color: var(--text-2);
}
.resumen-caja:hover { border-color: var(--border-strong); }
/* El elegido se marca con el borde y no con un fondo: son cuatro cajas en fila y
   un relleno de color convierte el resumen en una barra de botones. */
.resumen-caja.is-activo { border-color: var(--text); color: var(--text); }
.resumen-n { font-size: 19px; font-weight: 500; line-height: 1.15; color: var(--text); }
.resumen-k { font-size: 11.5px; }
/* El número toma el tono de lo que cuenta, y sólo el número: la caja entera de
   color haría que «2 fallaron» grite más fuerte que el resto de la pantalla
   incluso cuando es cero. */
.resumen-caja.tono-malo .resumen-n { color: var(--danger); }
.resumen-caja.tono-tibio .resumen-n { color: #e08a00; }
.resumen-caja.tono-bien .resumen-n { color: var(--brand); }

/* La sección de referencia (el catálogo de eventos), colapsada. Contesta «¿este
   evento existe?», que se pregunta una vez cada tanto — abierta y en el medio era
   lo que más lugar ocupaba para lo que menos se mira. */
.ajustes-ref > .sub { margin: 4px 0 12px; }
summary.ajustes-ref-head {
  display: flex; align-items: center; gap: 6px;
  padding: 4px 0; cursor: pointer; list-style: none;
  font-size: 14px; font-weight: 500; color: var(--text);
}
summary.ajustes-ref-head::-webkit-details-marker { display: none; }
summary.ajustes-ref-head:hover { color: var(--text-2); }
details.ajustes-ref[open] > summary .props-grupo-caret { transform: rotate(90deg); }

/* ── El aviso: «¿le avisamos?» y el rastro de lo que salió ───────────────────
   El modal muestra el texto ENTERO y sin maquillar —los asteriscos de la
   negrita de WhatsApp incluidos—, porque el motivo de que este aviso lo dispare
   una persona y no el sistema es justamente que quiere leer lo que se le va a
   mandar a otro. Un preview estilizado sería una versión, no el mensaje. */
.aviso-preview {
  margin: 0 0 14px; padding: 12px 14px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font); font-size: 12.5px; line-height: 1.55;
  color: var(--text-2); white-space: pre-wrap; word-break: break-word;
  max-height: 260px; overflow-y: auto;
}

.aviso-canales { display: flex; flex-direction: column; gap: 2px; }
.aviso-canal {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 8px; border-radius: var(--radius); cursor: pointer; font-size: 13px;
}
.aviso-canal:hover { background: var(--bg-hover); }
.aviso-canal input { margin-top: 2px; accent-color: var(--brand); }
/* Un canal que no se puede usar **se muestra igual**, apagado y con el motivo al
   lado. Esconderlo dejaría al que asigna sin saber que a esa persona no se le
   puede avisar por WhatsApp — que es justo el dato que lo haría ir a cargarle el
   teléfono. */
.aviso-canal.is-off { cursor: default; opacity: .6; }
.aviso-canal.is-off:hover { background: transparent; }
.aviso-canal-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.aviso-canal-nombre { color: var(--text); }
.aviso-canal-dest { font-size: 11.5px; color: var(--text-3); }
.aviso-canal-motivo { font-size: 11.5px; color: #e08a00; }

/* ── La bandeja de avisos ────────────────────────────────────────────────────
   El badge del sidebar. Sólo existe con algo sin leer: un contador en cero es
   una promesa incumplida en cada pantalla. */
.nav-badge {
  margin-left: auto; flex-shrink: 0;
  min-width: 18px; padding: 0 5px;
  border-radius: 9px; background: var(--brand); color: var(--text-inv);
  font-size: 10.5px; font-weight: 600; line-height: 18px; text-align: center;
}

/* La fila de la bandeja. Reusa `.lin-row` —es una lista más de la app— y sólo
   agrega el punto de leído/sin leer y las dos líneas de texto. */
.bandeja-fila { display: flex; align-items: center; gap: 12px; padding: 11px 12px; }
/* Hueco = leído, lleno = sin leer. La misma convención que los dots de estado de
   Órdenes y Acopios, que es lo que hace que una lista se lea de un vistazo sin
   leer las etiquetas. */
.bandeja-dot {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid var(--border-strong);
}
.bandeja-fila.is-nuevo .bandeja-dot { background: var(--brand); border-color: var(--brand); }
.bandeja-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bandeja-titulo {
  font-size: 13px; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Lo NO leído pesa más. Es la única diferencia visual entre las dos mitades de
   la lista, y alcanza: sin ella hay que leer los puntos uno por uno. */
.bandeja-fila.is-nuevo .bandeja-titulo { color: var(--text); font-weight: 500; }
.bandeja-sub {
  font-size: 12px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bandeja-cuando {
  margin-left: auto; flex-shrink: 0;
  font-size: 12px; color: var(--text-3); white-space: nowrap;
}

/* La actividad, al fondo del panel de la etapa y en cuerpo chico: es el registro
   de lo que ya pasó, no algo con lo que se trabaje. Mismo lugar y mismo peso que
   el log de actividad de un issue de Linear.

   Una tira vertical con línea a la izquierda y no una tabla — una tabla promete
   que se va a leer entera. */
.actividad {
  margin-top: 28px; padding-top: 14px;
  border-top: 1px solid var(--border);
}
.actividad-titulo {
  font-size: 11px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-3); margin-bottom: 8px;
}
.actividad-fila {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 0 4px 10px;
  border-left: 1px solid var(--border);
  font-size: 12px; color: var(--text-3);
}
.actividad-icono { display: flex; flex-shrink: 0; }
.actividad-icono svg { width: 12px; height: 12px; stroke: currentColor; }
.actividad-txt { min-width: 0; }
.actividad-txt strong { color: var(--text-2); font-weight: 500; }
/* La nota es el «por qué» —el motivo, el desvío— y va un tono más apagada que la
   frase: se lee cuando hace falta, no de entrada. */
.actividad-nota { color: var(--text-3); }
.actividad-cuando { margin-left: auto; flex-shrink: 0; white-space: nowrap; }
/* Lo que no salió NO se apaga: es el único renglón de este bloque que pide algo.
   Rojo cuando falló, ámbar cuando fue el modo consola —que no es un error, es que
   no hay proveedor— y en los dos casos el punto es el mismo: esa persona no se
   enteró. */
.actividad-fila.is-fallido { color: var(--danger); }
.actividad-fila.is-fallido strong,
.actividad-fila.is-tibio strong { color: inherit; }
.actividad-fila.is-tibio { color: #e08a00; }
.actividad-reintentar {
  flex-shrink: 0; border: none; background: transparent; cursor: pointer;
  padding: 2px 6px; border-radius: 5px;
  font-family: inherit; font-size: 12px; color: inherit;
  text-decoration: underline; text-underline-offset: 2px;
}
.actividad-reintentar:hover { background: var(--bg-hover); }

/* El bloque de responsables del panel de la derecha. La etiqueta es el nombre de
   la etapa, así que es más larga que las de Propiedades («Procedencia», «CUIT») y
   necesita su propio ancho — con los 70px de `.prop-label`, «Liquidación» se
   partía en dos líneas. */
#negocio-responsables { display: flex; flex-direction: column; gap: 4px; }
#negocio-responsables .prop-label { flex: 0 0 88px; }

/* Colapsable, con el mismo caret que los grupos de la lista (ADR-0004 §4), y
   **cerrado por default**: quién tiene cada etapa ya se ve en la barra de
   pestañas, así que abierto repetía esa información en media columna. El estado
   vive en el `<details>`, que queda afuera del OOB — ver `_responsables.html`.

   Misma caja que `.props-box`: son dos bloques del mismo panel y tienen que
   verse como hermanos, no como un bloque y su encabezado. */
.props-grupo {
  margin-bottom: 10px;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
/* Abierto necesita el aire de abajo; cerrado es una sola línea y con el mismo
   padding queda una tira demasiado alta. */
details.props-grupo[open] { padding-bottom: 12px; }
summary.props-grupo-head {
  display: flex; align-items: center; gap: 6px;
  padding: 2px 0;
  cursor: pointer; list-style: none;
}
summary.props-grupo-head::-webkit-details-marker { display: none; }
/* El caret va INLINE y no colgando hacia el margen. Colgaba para alinear el
   título con «Propiedades» cuando los dos vivían en la misma caja; ahora son dos
   cajas con su propio padding, y un caret que se sale del borde se lee como un
   error de maquetado. */
.props-grupo-caret {
  display: flex; color: var(--text-3); flex-shrink: 0;
  transition: transform .12s ease;
}
.props-grupo-caret svg { width: 14px; height: 14px; stroke: currentColor; }
details.props-grupo[open] > summary .props-grupo-caret { transform: rotate(90deg); }
.props-grupo-head .props-group-title { margin-bottom: 0; padding-bottom: 0; }
details.props-grupo[open] #negocio-responsables { margin-top: 8px; }
.props-grupo-head:hover .props-group-title { color: var(--text-2); }
/* El disparador ocupa la fila entera: es el área clickeable de una acción que se
   hace seguido, y media fila muerta al lado del nombre es medio blanco donde el
   click no hace nada. */
#negocio-responsables .asignar-trigger { width: 100%; }

/* El mismo popover, pero flotando: es el menú de la fila (uno solo para toda la
   página, en base.html), que abren el kebab y el click derecho. El JS lo ubica
   con left/top, así que no cuelga de ningún ancestro. */
.acciones-pop.acciones-flotante {
  position: fixed; top: 0; left: 0; right: auto; z-index: var(--z-menu-fila); min-width: 200px;
}

/* ── Tabs (estilo Linear; la tab abierta la decide el server, vive en ?tab=) ── */
.tab-bar {
  display: flex; align-items: center; gap: 4px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 20px;
}
.tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; margin-bottom: -1px;
  font-size: 13.5px; font-weight: 500; color: var(--text-2);
  border-bottom: 2px solid transparent;
  cursor: pointer; white-space: nowrap;
}
.tab:hover { color: var(--text); }
.tab-count {
  font-size: 11px; font-weight: 500; color: var(--text-3);
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 10px; padding: 0 6px; line-height: 16px;
}
/* El server renderiza SOLO el panel de la tab abierta (y sólo en Resumen, el
   panel de propiedades): no hay nada que esconder con CSS. */
.tab.is-active { color: var(--text); border-bottom-color: var(--text); }

/* Aviso en línea (no es un toast: se queda). Hoy lo usa el resumen del acopio
   cuando el saldo en plata no se pudo calcular entero porque hay materiales sin
   precio congelado (MGA-92). Tono ámbar: no es un error, es un dato faltante. */
.aviso {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px; margin-bottom: 16px;
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 13px; line-height: 1.5;
}
.aviso-icon { display: flex; flex-shrink: 0; margin-top: 1px; }
.aviso-icon svg { width: 16px; height: 16px; stroke: currentColor; }
.aviso-warn { background: #fdf6e7; color: #8a5d10; border-color: #f0dcae; }
.aviso-error { background: #fdecea; color: #8c2f28; border-color: #f0c4c4; }
.aviso-texto strong { font-weight: 600; }
/* === AGR-430 (QA) === Un `.aviso` con varios `<p>` (la lista de errores de
   «Revisá los datos») es `display: flex` sin dirección: sin esto cada
   párrafo se hace columna angosta y el texto se corta palabra por palabra. */
.aviso-lista { flex-direction: column; gap: 4px; }

/* Confirmación DENTRO de un aviso (aporte cross-cliente, MGA-85): el checkbox
   que habilita el botón de crear. Va abajo del texto, separado, para que se lea
   como "esto hay que tildarlo" y no como una nota más. */
.aviso-check {
  display: flex; align-items: flex-start; gap: 8px;
  margin-top: 10px; font-weight: 600; cursor: pointer;
}
.aviso-check input { margin-top: 2px; flex-shrink: 0; }

/* El aviso de pedido repetido del alta (MGA-227): un renglón por label con
   antecedentes y, debajo, los pedidos de esa obra que ya lo llevan. Reusa el
   `.aviso .aviso-warn` de arriba —el ámbar de "no es un error, es un dato que
   hay que mirar", que es exactamente el tono de este aviso, que no bloquea— y
   sólo agrega la lista, que es lo único que ese aviso todavía no tenía.

   Sin `flex` en el <li> a propósito: cada renglón es una frase con partes
   heterogéneas (link, estado, fecha, círculo + nombre de la persona) y el flujo
   inline las envuelve solo cuando el modal se angosta; con flex, cada corrida
   de texto se vuelve un ítem y los `·` se despegan de lo que separan. */
.aviso-antecedente { margin-bottom: 8px; }
.aviso-pedidos { list-style: none; margin: 3px 0 0; padding: 0; }
.aviso-pedidos li { padding: 1px 0; line-height: 1.6; }
.aviso-pedidos a { font-weight: 600; text-decoration: underline; }
/* El resto contado ("y 7 más") es el mismo aviso, un tono más bajo: no es un
   pedido más de la lista, es cuántos quedaron afuera del tope. */
.aviso-mas { opacity: .75; }

/* ── Resumen: hero con saldo grande + barra de progreso ───────────────────── */
.resumen-hero {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 32px; flex-wrap: wrap;
  padding: 20px; margin-bottom: 24px;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.resumen-hero-num { display: flex; flex-direction: column; gap: 2px; }
.resumen-hero-label { font-size: 12px; color: var(--text-2); }
.resumen-hero-value {
  font-size: 30px; font-weight: 600; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.resumen-hero-sub { font-size: 12.5px; color: var(--text-3); }
.resumen-bar { flex: 1 1 220px; min-width: 200px; }
/* El riel: la pista más, si el acopio está sobregirado, el tramo que se pasó.
   Existe para que el exceso pueda dibujarse FUERA de la pista sin escaparse del
   layout: la pista se encoge, el rojo la sigue de largo. MGA-91. */
.resumen-bar-riel { display: flex; align-items: center; }
.resumen-bar-track {
  position: relative;
  height: 10px; border-radius: 999px;
  background: var(--bg-active); overflow: hidden;
  /* Crece 100 contra el flex-grow del tramo sobregirado (su % de exceso sobre
     el total): así la pista sigue midiendo el acopio entero y el rojo que sale
     de ella está a escala (20% de sobregiro = un quinto de la pista). */
  flex: 100 0 0; min-width: 0;
}
/* Tramo entregado: sólido, anclado a la izquierda. */
.resumen-bar-fill {
  position: absolute; top: 0; left: 0; height: 100%;
  background: var(--text); transition: width .2s ease;
}
/* Si hay algo (>0) pero el % es minúsculo, garantizamos un sliver visible. */
.resumen-bar-fill.has-value { min-width: 3px; }
/* Tramo comprometido (en vuelo): rayado, apilado a continuación del entregado. */
.resumen-bar-fill.is-comprometido {
  background: var(--text-3);
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(255, 255, 255, .45) 0, rgba(255, 255, 255, .45) 3px,
    transparent 3px, transparent 6px
  );
}
/* Tramo cedido (plata que salió sin material): gris claro SÓLIDO, y va último.
   Sólido porque ya ocurrió (la regla: rayado = todavía en vuelo). El gris es un
   punto más oscuro que un gris claro puro porque, al ir último, limita con el
   tramo vacío de la barra y tiene que despegarse de él. MGA-84. */
.resumen-bar-fill.is-cedido { background: #bcbec2; }
.resumen-bar-legend {
  display: flex; flex-wrap: wrap; gap: 14px;
  margin-top: 8px; font-size: 12px; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-swatch {
  width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0;
  background: var(--text);
}
.legend-swatch.is-comprometido {
  background: var(--text-3);
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(255, 255, 255, .45) 0, rgba(255, 255, 255, .45) 2px,
    transparent 2px, transparent 4px
  );
}
.legend-swatch.is-cedido { background: #bcbec2; }

/* ── Sobregiro: lo único rojo del resumen (MGA-91) ──────────────────────────
   El design system es monocromático, y sigue siéndolo: el rojo acá es
   SEMÁNTICO ("esto está mal"), no decorativo. Por eso lo usa el sobregiro y no
   el Cedido, que es una operación normal. Es el --danger de siempre, el único
   rojo de la app: un segundo rojo sería un acento. */
.resumen-bar-exceso {
  height: 10px;
  /* El flex-grow lo pone el template: el % de exceso sobre el total, contra el
     100 de la pista. Se topa el ancho DIBUJADO, no el número: un acopio
     consumido al 400% no puede empujar el layout, pero la leyenda igual dice
     "400,0% del acopio". */
  max-width: 30%;
  /* Sale de la pista y sigue: pegado a ella (sin redondeo a la izquierda) y
     cerrado a la derecha, para que se lea como desborde y no como otro tramo. */
  border-radius: 0 999px 999px 0;
  background: var(--danger);
}
.legend-item.is-sobregiro { color: var(--danger); font-weight: 500; }
.legend-swatch.is-sobregiro { background: var(--danger); }
.resumen-hero-value.is-sobregiro,
.resumen-hero-sub.is-sobregiro,
.cell-meta.is-sobregiro { color: var(--danger); }

/* ── Modal (rediseñado al estilo Linear) ─────────────────────────────────────
   Tres cambios sobre el modal que venía de MGA, y los tres van en la misma
   dirección: que el marco pese menos que el contenido.

   1. **El encabezado es una cápsula de contexto, no un título.** Linear no
      escribe "Nuevo cierre" en 15px semibold arriba de todo: escribe
      «Cierres › Nuevo cierre» en el mismo cuerpo que el resto, con el ícono de
      la sección adelante. Se sabe dónde estás sin que el marco grite.
   2. **Los campos no son cajas.** El borde permanente de un input multiplica las
      líneas de la pantalla por la cantidad de campos: doce campos son doce cajas
      compitiendo. Acá el campo es una superficie hundida, sin borde, y el borde
      (teal) aparece SOLO al enfocar — que es cuando dice algo.
   3. **El pie enseña el atajo.** «⌘↵ para guardar» a la izquierda: es como se
      carga un formulario sin soltar el teclado, y es lo que separa una pantalla
      de administración de una herramienta.
   ────────────────────────────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed; inset: 0; z-index: var(--z-modal-fondo);
  background: rgba(8, 9, 10, .38);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 10vh 0 48px;
  /* El overlay scrollea: si el modal es más alto que el viewport, el operador se
     desplaza hasta la barra de acciones. El overflow va acá y NO en .modal ni en
     .modal-body, que quedan en visible para que los popovers (Tom Select,
     datepicker) floten fuera de la caja sin recortarse. */
  overflow-y: auto;
  animation: modal-fondo .12s ease-out;
}
@keyframes modal-fondo { from { opacity: 0 } to { opacity: 1 } }

.modal {
  position: relative; z-index: var(--z-modal);
  background: var(--bg); border-radius: 12px;
  width: 460px; max-width: calc(100vw - 32px);
  box-shadow: 0 16px 60px rgba(8, 9, 10, .18), 0 0 0 1px var(--border);
  /* visible (no hidden) para que los popovers —datepicker, Tom Select— floten
     fuera de la caja del modal en vez de quedar recortados. El redondeo de la
     barra inferior se hace en .modal-actions. */
  overflow: visible;
  animation: modal-entra .14s cubic-bezier(.2, .8, .3, 1);
}
@keyframes modal-entra {
  from { opacity: 0; transform: translateY(-6px) scale(.99) }
  to   { opacity: 1; transform: none }
}

/* El encabezado: la cápsula a la izquierda, la × a la derecha. */
.modal-head {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 12px 12px 16px;
}
.modal-crumb {
  display: flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--text-3); min-width: 0;
}
.modal-crumb-icon { display: flex; color: var(--text-3); }
.modal-crumb-icon svg { width: 14px; height: 14px; stroke: currentColor; }
.modal-crumb-ctx { color: var(--text-3); }
.modal-crumb-sep { display: flex; color: var(--text-3); opacity: .65; }
.modal-crumb-sep svg { width: 12px; height: 12px; stroke: currentColor; }
.modal-crumb-actual {
  color: var(--text); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.modal-x {
  margin-left: auto; flex-shrink: 0;
  width: 26px; height: 26px; padding: 0; display: flex;
  align-items: center; justify-content: center;
  border: 0; border-radius: 6px; background: transparent;
  color: var(--text-3); cursor: pointer;
}
.modal-x:hover { background: var(--bg-hover); color: var(--text); }
.modal-x svg { width: 15px; height: 15px; stroke: currentColor; }

.modal-body { padding: 4px 16px 18px; }

/* ── Campos ──────────────────────────────────────────────────────────────── */
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field label {
  display: block; font-size: 12px; font-weight: 500;
  color: var(--text-2); margin-bottom: 5px;
}
.field input, .field select, .field textarea {
  width: 100%; padding: 0 10px;
  /* Sin borde: la superficie hundida ya separa el campo del fondo. El borde
     aparece al enfocar (abajo), que es cuando hay algo que señalar. */
  border: 1px solid transparent; border-radius: 7px;
  background: var(--bg-sunken); color: var(--text); font-size: 13.5px;
  transition: background .1s ease, border-color .1s ease, box-shadow .1s ease;
}
.field input, .field select { height: 34px; }
.field textarea {
  padding: 8px 10px; font-family: inherit; resize: vertical; line-height: 1.45;
}
.field input:hover, .field select:hover, .field textarea:hover { background: var(--bg-hover); }

/* El campo que llenó la lectura del documento y no una persona (AGR-299).

   Es un FONDO TENUE y nada más: sin borde, sin ícono, sin badge y sin texto.
   Un borde en teal habría abierto la segunda excepción a la regla de color del
   proyecto —hoy hay una sola, la barrita de avance— y encima los campos de esta
   app no tienen borde por decisión: aparece sólo al enfocar. Y son hasta seis
   campos marcados a la vez en un modal chico: seis íconos o seis badges serían
   más ruido que los seis números.

   **El fondo se pinta con una `box-shadow` `inset` y no con `background`**, que
   es lo que deja las dos capas separadas: el `background` sigue siendo el
   idioma del HOVER, así que un campo marcado se sigue sintiendo al pasarle por
   encima en vez de quedar congelado. El `inset` no mueve un píxel de geometría
   y sigue solo el `border-radius` del campo.

   **El `:focus` la apaga sin una línea de más.** El foco reemplaza la
   `box-shadow` entera por su anillo, así que tocar el campo borra la marca — que
   es exactamente lo que tiene que pasar: desde que alguien lo tocó, el número ya
   no es «lo que dijo el documento». Por eso las dos reglas van ACÁ, arriba de
   `:focus` y con su misma especificidad (una clase y un elemento, sin `.field`
   adelante): así el foco gana por orden, sin `!important` y sin `:not()`. Mover
   estas líneas abajo del `:focus`, o agregarles el `.field`, deja la marca
   pintada sobre un campo enfocado. */
input.leido { box-shadow: inset 0 0 0 100px var(--brand-soft); }
input.leido:hover { box-shadow: inset 0 0 0 100px var(--brand-line); }

.field input::placeholder, .field textarea::placeholder { color: var(--text-3); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; background: var(--bg);
  border-color: var(--brand); box-shadow: 0 0 0 3px rgba(19, 97, 91, .10);
}

/* Campo checkbox: label al lado de un toggle switch, no el input estirado. */
.field-check { display: flex; align-items: center; gap: 10px; flex-direction: row; }
.field-check label { margin: 0; font-size: 13.5px; color: var(--text); order: 2; }
.field-check input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 34px; height: 20px; padding: 0; margin: 0; flex-shrink: 0;
  border-radius: 20px; background: var(--border-strong); border: none;
  position: relative; cursor: pointer; transition: background .15s;
}
.field-check input[type="checkbox"]::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 16px; height: 16px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2);
  transition: transform .15s;
}
.field-check input[type="checkbox"]:checked { background: var(--brand); }
.field-check input[type="checkbox"]:checked::after { transform: translateX(14px); }
.field-check input[type="checkbox"]:focus-visible { box-shadow: 0 0 0 3px rgba(19,97,91,.16); }

/* El error del campo, en el rojo de la app y no en gris: un rechazo que se ve
   igual que un texto de ayuda no se lee como rechazo. */
.field .err {
  color: var(--danger); background: rgba(163, 45, 45, .06);
  border-radius: 5px; padding: 4px 8px; font-size: 12px; margin-top: 5px;
}
.field .err + input, .field input.is-error { border-color: var(--danger); }

/* Los errores de un campo obligatorio que marca `static/js/validacion.js` en
   vez del cartel del navegador (QA de AGR-453): el mismo rojo que `.field
   .err`, en cualquier formulario —el diálogo público no usa `.field`—, y una
   línea arriba del formulario con cuántos datos faltan. */
.err-validacion {
  color: var(--danger); background: rgba(163, 45, 45, .06);
  border-radius: 5px; padding: 4px 8px; font-size: 12px; margin-top: 5px;
}
.aviso-validacion {
  color: var(--danger); background: rgba(163, 45, 45, .06);
  border-radius: 8px; padding: 8px 10px; font-size: 13px;
}

/* ── Secciones del formulario ──────────────────────────────────────────────
   Doce campos seguidos son una pared. Agrupados en «Las partes», «La operación»
   y «Los términos» son tres decisiones, que es como se piensa un cierre. El
   separador es una línea con el título encima, no una caja: sumar bordes es
   exactamente lo que el campo sin borde vino a evitar. */
.form-sec + .form-sec { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); }
.form-sec-title {
  font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-3); margin-bottom: 12px;
}

/* ── Pie ─────────────────────────────────────────────────────────────────── */
.modal-actions {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 16px; border-top: 1px solid var(--border);
  background: var(--bg-sidebar); border-radius: 0 0 12px 12px;
}
.modal-botones { margin-left: auto; display: flex; gap: 8px; }
.modal-hint { font-size: 11.5px; color: var(--text-3); display: flex; align-items: center; gap: 3px; }
.modal-hint kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 4px;
  border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 4px;
  background: var(--bg); font: inherit; font-size: 10.5px; color: var(--text-2);
}

/* Feedback de "cargando" al enviar: HTMX marca el <form> con .htmx-request
   mientras el POST está en vuelo. Contra una DB remota el guardado puede tardar,
   así que atenuamos la primaria para que el operador no vuelva a clickear. */
.modal form.htmx-request .btn-primary { opacity: .55; pointer-events: none; }
.modal form.htmx-request { cursor: progress; }

/* ── Modal de confirmación (ADR-0005 §2) ─────────────────────────────────── */
/* El cuerpo dice qué se pierde: la frase principal, y abajo el colateral (gris,
   secundario) o el bloqueo (rojo, con el botón de confirmar deshabilitado). */
.confirm-msg { margin: 0; font-size: 13.5px; line-height: 1.5; }
.confirm-aviso {
  margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--text-2);
}
.confirm-bloqueo {
  margin: 12px 0 0; padding: 8px 10px; border-radius: var(--radius);
  background: #fbebeb; border: 1px solid #f0c4c4; color: #a32d2d;
  font-size: 12.5px; line-height: 1.5;
}
/* Cuáles bloquean (MGA-130): un renglón por OC, con la misma forma que la lista
   de OC del modal de armado (.mat-confirm-row). El estado va a la derecha, en
   columna: es lo que decide qué hacer con el pedido. El número es un link y
   tiene que verse como tal — el reset global (`a { color: inherit;
   text-decoration: none }`) lo dejaría igual que el texto de al lado. */
.confirm-bloqueo-lista {
  list-style: none; margin: 8px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
}
.confirm-bloqueo-item {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; background: var(--bg-sunken);
  border: 1px solid var(--border); border-radius: 8px;
  font-size: 12.5px; line-height: 1.4;
}
.confirm-bloqueo-link {
  color: var(--text-2); text-decoration: underline;
  font-variant-numeric: tabular-nums; flex-shrink: 0;
}
.confirm-bloqueo-link:hover { color: var(--text); }
/* Lo que bloquea pero no tiene pantalla propia (una obra, una plantilla): mismo
   lugar que el link, sin el subrayado que prometería un click. Va en el color
   del texto principal y NO en el del link: son renglones sin detalle ni estado,
   y en gris claro la caja se lee como un input vacío con un placeholder. */
.confirm-bloqueo-link-muerto {
  color: var(--text); font-variant-numeric: tabular-nums; flex-shrink: 0;
}
.confirm-bloqueo-detalle { color: var(--text); }
.confirm-bloqueo-estado { margin-left: auto; color: var(--text-2); white-space: nowrap; }
/* Cuál de los seleccionados encabeza el grupo, en el borrado masivo (MGA-159).
   Va sobre su lista y no adentro: es el objeto que hay que destildar, no uno más
   de los que bloquean. */
.confirm-bloqueo-grupo {
  margin: 14px 0 0; font-size: 12.5px; font-weight: 600;
  color: var(--text); line-height: 1.4;
}
/* El «y N más» del tope: un renglón de la lista, pero sin la caja — no es un
   documento que se pueda abrir, es la confesión de que la lista está cortada. */
.confirm-bloqueo-mas {
  padding: 2px 10px; font-size: 12px; color: var(--text-2);
}
/* Rojo sólo para lo que destruye. Cerrar un acopio también se confirma acá, y
   se confirma con el botón primario: congela precios, no borra nada. */
.btn-peligro { background: #a32d2d; border-color: #a32d2d; color: #fff; }
.btn-peligro:hover { background: #8f2727; }
.btn:disabled { opacity: .45; pointer-events: none; }

/* ── Importar pedido de WhatsApp ─────────────────────────────────────────── */
/* El review necesita más ancho: la grilla muestra texto crudo + cantidad +
   selector de material por fila. */
.modal-md { width: 620px; max-width: calc(100vw - 32px); }
.modal-wide { width: 920px; max-width: calc(100vw - 32px); }
/* OJO: nada de overflow en el modal-body — recortaría el dropdown de Tom Select
   del material y daría la sensación de que "no abre". Si hay muchas filas, deja
   que scrollee la página de fondo. */
.modal-wide .modal-body { overflow: visible; }

.field textarea {
  width: 100%; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); color: var(--text); font-size: 13.5px;
  font-family: inherit; resize: vertical; line-height: 1.45;
}
.field textarea:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(19,97,91,.10);
}

.field-label { display: block; font-size: 12.5px; color: var(--text-2); margin-bottom: 6px; }

/* Grilla tipo hoja de cálculo: una fila por material, celdas con bordes. */
.imp-grid-wrap {
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: visible;  /* visible: los dropdowns de Tom Select flotan fuera */
}
/* table-layout: fixed → los anchos de columna mandan y el contenido (incluido
   el Tom Select del material) se ajusta a la celda en vez de estirarla. */
.imp-grid { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 13px; }
.imp-grid thead th {
  text-align: left; font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .03em; color: var(--text-2);
  padding: 7px 10px; background: var(--bg-sidebar);
  border-bottom: 1px solid var(--border);
}
.imp-grid td {
  padding: 0; border-bottom: 1px solid var(--border); vertical-align: middle;
}
.imp-grid tbody tr:last-child td { border-bottom: none; }
.imp-grid .imp-col-raw  { width: 38%; }
.imp-grid .imp-col-qty  { width: 90px; }
.imp-grid .imp-col-mat  { width: auto; }   /* la más ancha: lo que importa */
.imp-grid .imp-col-x    { width: 40px; text-align: center; }

/* El texto crudo del mensaje, como celda de planilla (truncado, sin wrap). */
.imp-raw {
  display: block; padding: 0 10px; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.imp-raw-manual { font-style: italic; }

/* Inputs embebidos en la celda: sin borde propio, ocupan toda la celda (look
   de hoja de cálculo). El borde lo pone la celda. */
.imp-grid .imp-qty,
.imp-grid .imp-mat {
  border: none; border-radius: 0; background: transparent;
  height: 34px; width: 100%;
}
.imp-grid .imp-qty {
  padding: 0 10px; text-align: right; color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* Sin flechitas: que escriban el número directo (look de planilla). */
.imp-grid .imp-qty::-webkit-outer-spin-button,
.imp-grid .imp-qty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.imp-grid .imp-qty { -moz-appearance: textfield; appearance: textfield; }
.imp-grid .imp-qty:focus {
  outline: none; box-shadow: inset 0 0 0 1px var(--border-strong); background: var(--bg);
}
/* Tom Select dentro de la celda: sin borde, ocupa el ancho de la celda. OJO: nada
   de overflow:hidden en la celda ni en el control — recorta el dropdown y parece
   que "no abre". El truncado del texto largo se hace solo en el .item. */
.imp-grid .ts-wrapper {
  width: 100%; max-width: 100%; box-sizing: border-box;
}
.imp-grid .ts-wrapper .ts-control {
  border: none; border-radius: 0; background: transparent; min-height: 34px;
  flex-wrap: nowrap; cursor: pointer;
}
/* Material ya elegido: se ve sutil, como texto de la planilla (no como un input
   activo). El affordance de "tocá para buscar otro" aparece en hover/focus. */
.imp-grid .ts-wrapper .ts-control > .item {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text);
}
.imp-grid .ts-wrapper:not(.focus) .ts-control:hover { background: var(--bg-hover); }
/* Selección sutil: un borde fino gris, no el marco negro de 2px. */
.imp-grid .ts-wrapper.focus .ts-control {
  box-shadow: inset 0 0 0 1px var(--border-strong); background: var(--bg);
}
/* El dropdown del material por encima de la barra de acciones del modal. No sale
   de la escala de :root a propósito: vive DENTRO del modal, que ya es su contexto
   de apilado, así que sólo compite con sus hermanos (la barra de acciones). */
.imp-grid .ts-dropdown { z-index: 60; }

/* Fila sin material identificado: marca roja a la izquierda + tinte; bloquea el
   guardado hasta que el usuario elija un material del catálogo. */
.imp-row-bad td { background: var(--bg-sunken); }
.imp-row-bad .imp-col-raw { box-shadow: inset 3px 0 0 var(--danger); }

.imp-row-empty td { padding: 14px 10px; color: var(--text-2); text-align: center; }

.imp-del {
  border: none; background: transparent; cursor: pointer;
  color: var(--text-2); font-size: 18px; line-height: 1;
  width: 28px; height: 28px; border-radius: var(--radius);
}
.imp-del:hover { background: var(--bg-hover); color: var(--text); }

.imp-add { margin-top: 10px; }

/* La importación dejó de ser un modal y pasó a ser una pantalla de dos pasos
   (MGA-116 / ADR-0005 §1). El paso 1 es una columna angosta —hay un solo
   textarea y no gana nada estirándose—; el paso 2 usa el ancho de la página
   completo, que es justo lo que la grilla fila-por-fila pedía y el modal de
   920px nunca le pudo dar. */
/* Columna centrada: es la pantalla entera (miga, título y form), no sólo el
   form. Una columna angosta pegada al borde izquierdo de una página por lo
   demás vacía se lee como un error de maquetado. */
.imp-page { max-width: 720px; margin: 0 auto; }
.imp-head-row { display: flex; gap: 16px; align-items: flex-start; }
.imp-head-row .field { flex: 1; min-width: 0; }
/* La barra de acciones del modal ahora es el pie de la pantalla: misma
   gramática (secundaria a la izquierda, primaria a la derecha), separada del
   cuerpo por una línea en vez de por el borde del modal. */
.imp-page-actions {
  display: flex; justify-content: flex-end; align-items: center; gap: 8px;
  margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border);
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); border: 0;
}

/* ── Empty-state de páginas stub ─────────────────────────────────────────── */
.stub {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; height: 60vh; color: var(--text-2);
}
.stub .stub-icon {
  width: 40px; height: 40px; border-radius: var(--radius-lg);
  border: 1px solid var(--border); background: var(--bg-sidebar);
  display: flex; align-items: center; justify-content: center; margin-bottom: 14px;
}
.stub .stub-icon svg { width: 20px; height: 20px; stroke: var(--text-3); }
.stub h2 { margin: 0 0 4px; font-size: 15px; font-weight: 600; color: var(--text); }
.stub p { margin: 0; font-size: 13px; }

.htmx-indicator { opacity: 0; transition: opacity .2s; }
.htmx-request .htmx-indicator { opacity: 1; }

/* ── Tom Select (dropdowns con búsqueda, estilo Linear) ──────────────────── */
.ts-wrapper { font-size: 13.5px; }
/* Deshabilitado (ej: acopio mientras no se eligió proyecto). */
.ts-wrapper.disabled .ts-control { background: var(--bg-sunken); cursor: not-allowed; opacity: .6; }
.ts-control {
  min-height: 34px; height: 34px; padding: 0 10px;
  display: flex; align-items: center; flex-wrap: nowrap;
  border: 1px solid var(--border) !important; border-radius: var(--radius);
  background: var(--bg); color: var(--text);
  box-shadow: none;
}
/* Item seleccionado + input de búsqueda en una sola línea, sin crecer. */
.ts-wrapper.single .ts-control .item {
  background: transparent; padding: 0; margin: 0; line-height: 1;
}
.ts-control input { flex: 1 1 auto; min-width: 0; }
.ts-wrapper.single.has-items .ts-control input { width: 0; min-width: 0; flex: 0 0 0; }
/* Picker multi (labels del pedido, ADR-0007). El .ts-control de arriba está
   pensado para el single de la grilla: alto fijo y una sola fila. Un multi mete
   varios chips, así que acá el control crece hacia abajo (los chips fluyen a la
   fila siguiente) y CADA chip se queda en una línea —un label largo se recorta
   con ellipsis, no se parte en dos como venía pasando al salir del campo. */
.ts-wrapper.multi .ts-control {
  height: auto; min-height: 34px;
  flex-wrap: wrap; gap: 4px;
  padding-top: 4px; padding-bottom: 4px;
}
.ts-wrapper.multi .ts-control > .item {
  max-width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ts-wrapper.focus .ts-control {
  border-color: var(--text) !important;
  box-shadow: 0 0 0 3px rgba(8,9,10,.06);
}
.ts-control input, .ts-control input::placeholder { color: var(--text-3); }
.ts-control input { color: var(--text); }
.ts-dropdown {
  margin-top: 4px;
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  font-size: 13.5px; overflow: hidden;
}
.ts-dropdown .option {
  padding: 7px 10px; color: var(--text-2); cursor: pointer;
}
.ts-dropdown .option.active { background: var(--bg-hover); color: var(--text); }
.ts-dropdown .optgroup-header {
  font-size: 11px; font-weight: 500; color: var(--text-3);
  padding: 8px 10px 4px; letter-spacing: .01em;
}
.ts-dropdown .no-results { padding: 8px 10px; color: var(--text-3); }
.ts-wrapper.single .ts-control:after { border-color: var(--text-3) transparent transparent; }
/* Dropdown vacío: no mostrar cajas fantasma. */
.ts-dropdown:empty, .ts-dropdown .ts-dropdown-content:empty { display: none; }
/* Opción "Agregar…": destacada y consistente con el tema. */
.ts-dropdown .create {
  padding: 7px 10px; color: var(--text-2); cursor: pointer;
}
.ts-dropdown .create.active { background: var(--bg-hover); color: var(--text); }
.ts-dropdown .create strong { color: var(--text); font-weight: 600; }
/* Spinner de carga remota: chico, del alto de una línea, sin agrandar nada. */
.ts-wrapper .spinner {
  display: block; width: 14px; height: 14px; margin: 9px auto;
  border: 2px solid var(--border-strong); border-top-color: var(--text-2);
  border-radius: 50%; animation: ts-spin .6s linear infinite;
}
.ts-wrapper .spinner::before, .ts-wrapper .spinner::after { content: none; }
@keyframes ts-spin { to { transform: rotate(360deg); } }

/* --- Datepicker propio ---------------------------------------------------- */
.dp-wrap { position: relative; }
.dp-input { cursor: pointer; }
.dp-pop {
  display: none; position: absolute; top: calc(100% + 6px); left: 0; z-index: var(--z-popover);
  width: 252px; padding: 10px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-modal);
}
.dp-wrap.dp-open .dp-pop { display: block; }
/* Flip hacia arriba cuando no hay espacio abajo (ej: campo cerca del footer). */
.dp-wrap.dp-up .dp-pop { top: auto; bottom: calc(100% + 6px); }
.dp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.dp-title { font-size: 13px; font-weight: 500; color: var(--text); }
.dp-nav {
  width: 26px; height: 26px; padding: 0; cursor: pointer; font-size: 16px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: var(--radius); color: var(--text-2);
}
.dp-nav:hover { background: var(--bg-hover); color: var(--text); }
.dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dp-dow { font-size: 10.5px; color: var(--text-3); text-align: center; padding: 2px 0 4px; }
.dp-cell { aspect-ratio: 1; }
.dp-day {
  width: 100%; height: 100%; padding: 0; cursor: pointer; font-size: 12.5px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: var(--radius); color: var(--text);
}
.dp-day:hover { background: var(--bg-hover); }
.dp-day.is-today { font-weight: 600; box-shadow: inset 0 0 0 1px var(--border-strong); }
.dp-day.is-sel { background: var(--text); color: var(--text-inv); }
.dp-day.is-sel:hover { background: var(--text); }
.dp-foot { margin-top: 8px; display: flex; justify-content: flex-end; }
.dp-today {
  padding: 4px 10px; cursor: pointer; font-size: 12px;
  border: 1px solid var(--border); background: var(--bg); border-radius: var(--radius); color: var(--text-2);
}
.dp-today:hover { background: var(--bg-hover); color: var(--text); }

/* ── Toggle de vistas en la lista de pedidos ──────────────────────────────── */
/* Reusa el look de .tab pero como links navegables (no radios). */
.tab-bar .tab.is-active { color: var(--text); border-bottom-color: var(--text); }

/* Barra unificada estilo Linear: tabs a la izquierda, controles a la derecha,
   todo sobre una misma línea con un único borde inferior. */
.list-bar {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; border-bottom: 1px solid var(--border); margin-bottom: 20px;
}
/* Dentro de la barra unificada la tab-bar ya no dibuja su propio borde ni
   margen: los pone .list-bar (así el subrayado activo corre sobre la misma
   línea que los controles de la derecha). */
.list-bar .tab-bar { border-bottom: none; margin-bottom: 0; }
.list-tools {
  display: flex; align-items: center; gap: 8px; flex-shrink: 0;
  padding-bottom: 6px;
}
/* Sin tabs a la izquierda (ej. Órdenes de compra): los controles van a la derecha. */
.list-bar > .list-tools:only-child { margin-left: auto; }
/* Search compacto a la derecha (como los filtros de Linear), no full-width. */
.list-tools .search { height: 30px; width: 200px; }
.list-tools .search input { font-size: 13px; }

/* Selector de layout de la lista de pedidos (Por estado / Lista / Calendario):
   dropdown estilo Linear (<details>/<summary>), no un segmented control. El
   botón muestra el modo activo; el menú despliega las tres opciones con motion. */
.view-menu { position: relative; flex-shrink: 0; }
.view-menu-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 8px 0 10px;
  font-size: 13px; font-weight: 500; color: var(--text);
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); cursor: pointer; white-space: nowrap;
  list-style: none;
}
.view-menu-btn::-webkit-details-marker { display: none; }
.view-menu-btn:hover { background: var(--bg-hover); }
.view-menu[open] > .view-menu-btn { background: var(--bg-hover); border-color: var(--border-strong); }
.view-menu-btn > svg { width: 15px; height: 15px; stroke: currentColor; color: var(--text-2); flex-shrink: 0; }
.view-menu-caret {
  display: inline-flex; color: var(--text-3); margin-left: 1px;
  transition: transform .15s ease;
}
.view-menu-caret svg { width: 14px; height: 14px; stroke: currentColor; }
.view-menu[open] > .view-menu-btn .view-menu-caret { transform: rotate(180deg); }

/* Popover: aparece con fade + leve slide-down + scale (motion tipo Linear). */
.view-menu-pop {
  position: absolute; top: calc(100% + 5px); right: 0; z-index: var(--z-popover);
  min-width: 184px; padding: 4px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-modal);
  transform-origin: top right;
  animation: view-menu-in .14s cubic-bezier(.2, .8, .3, 1);
}
@keyframes view-menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.view-menu-opt {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 8px; border-radius: 6px;
  font-size: 13px; color: var(--text-2); cursor: pointer;
  text-decoration: none; white-space: nowrap;
}
.view-menu-opt > span:first-of-type { flex: 1; }
.view-menu-opt:hover { background: var(--bg-hover); color: var(--text); }
.view-menu-opt.is-active { color: var(--text); font-weight: 500; }
.view-menu-opt svg { width: 15px; height: 15px; stroke: currentColor; flex-shrink: 0; }
.view-menu-opt > svg:first-child { color: var(--text-2); }
.view-menu-check { display: inline-flex; color: var(--text); }
.view-menu-opt.is-active > svg:first-child { color: var(--text); }

/* ── Filtro de la lista (ADR-0005 §4) ────────────────────────────────────────
   El botón dice "Filtrar" con el ícono al lado, SIEMPRE: un botón que muta es un
   botón que hay que volver a aprender. Es el mismo átomo visual que el .view-menu
   que va al lado en la misma barra.

   El popover trae los DOS menús ya renderizados —el de campos y el de valores de
   cada campo, con sus conteos— y el JS de base.html solo elige cuál se ve
   (.filtro-pane[hidden]): los conteos los calcula el server (core.facetas), así
   que nunca son de mentira. */
.filtro-menu { position: relative; flex-shrink: 0; }
.filtro-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 10px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); color: var(--text-2); font-size: 13px;
  cursor: pointer; list-style: none; white-space: nowrap; user-select: none;
}
.filtro-btn::-webkit-details-marker { display: none; }
.filtro-btn:hover { background: var(--bg-hover); color: var(--text); }
.filtro-menu[open] > .filtro-btn { background: var(--bg-hover); border-color: var(--border-strong); }
.filtro-btn > svg { width: 15px; height: 15px; stroke: currentColor; flex-shrink: 0; }

.filtro-pop {
  position: absolute; top: calc(100% + 5px); left: 0; z-index: var(--z-popover);
  min-width: 220px; padding: 4px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-modal);
  animation: view-menu-in .14s cubic-bezier(.2, .8, .3, 1);
}
.filtro-sec {
  font-size: 11px; color: var(--text-3);
  padding: 6px 8px 3px; letter-spacing: .03em;
}
.filtro-opt {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 8px; border: 0; border-radius: 6px; background: transparent;
  font-size: 13px; color: var(--text-2); text-align: left; cursor: pointer;
  white-space: nowrap;
}
.filtro-opt:hover { background: var(--bg-hover); color: var(--text); }
.filtro-opt.is-active { color: var(--text); font-weight: 500; }
.filtro-opt .filtro-label { flex: 1; }
.filtro-opt > svg { width: 14px; height: 14px; stroke: currentColor; color: var(--text-3); flex-shrink: 0; }
/* El tilde ocupa su lugar aunque no esté: si no, la fila salta al marcarla. */
.filtro-check { display: inline-flex; width: 14px; flex-shrink: 0; color: var(--text); }
.filtro-check svg { width: 14px; height: 14px; stroke: currentColor; }
.filtro-n { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
/* «Creado por» es el único campo cuyos valores son PERSONAS, así que es el único
   que lleva el círculo (MGA-193). El círculo mide 24px y no se toca (ADR-0011:
   un solo tamaño para todos los marcadores de identidad), así que el costo se
   paga en el padding —igual que en el `.nav-item` del sidebar—: se lo achica
   sólo en esta fila para que el círculo entre sin inflar la altura por encima
   de las demás filas del menú. */
.filtro-pane[data-campo="creado_por"] .filtro-opt { padding-top: 4px; padding-bottom: 4px; }

/* Las píldoras: lo filtrado, arriba de la barra. El cuerpo REABRE el menú de ese
   campo (se cambian, no solo se borran); la × saca el campo entero. */
.filtro-pills { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.filtro-pill {
  display: inline-flex; align-items: stretch; height: 28px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg); overflow: hidden;
}
.filtro-pill-body {
  display: inline-flex; align-items: center; gap: 5px; padding: 0 9px;
  border: 0; background: transparent; cursor: pointer;
  font-size: 12.5px; color: var(--text); white-space: nowrap;
}
.filtro-pill-body:hover { background: var(--bg-hover); }
.filtro-pill-k { color: var(--text-3); }
.filtro-pill-x {
  display: inline-flex; align-items: center; justify-content: center; width: 24px;
  border: 0; border-left: 1px solid var(--border); background: transparent;
  color: var(--text-3); cursor: pointer;
}
.filtro-pill-x:hover { background: var(--bg-hover); color: var(--text); }
.filtro-pill-x svg { width: 13px; height: 13px; stroke: currentColor; }
.filtro-clear {
  border: 0; background: transparent; color: var(--text-3);
  font-size: 12.5px; cursor: pointer; padding: 0 6px;
}
.filtro-clear:hover { color: var(--text); text-decoration: underline; }

/* Vista "Por estado": secciones por estado calculado; Entregados es un
   <details> colapsable (mismo patrón caret que .client en el sidebar). */
.estado-grupo { margin-bottom: 20px; }
.estado-grupo-head {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px; font-size: 13px; font-weight: 600; color: var(--text);
  background: var(--bg-sunken); border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}
.estado-grupo-head .lin-status { width: 10px; height: 10px; }
/* Ícono del proveedor en la vista "Por proveedor" de órdenes. */
.estado-grupo-head > svg { width: 15px; height: 15px; stroke: currentColor; color: var(--text-2); flex-shrink: 0; }
summary.estado-grupo-head { cursor: pointer; list-style: none; }
summary.estado-grupo-head::-webkit-details-marker { display: none; }
.estado-grupo-head .caret {
  display: inline-flex; color: var(--text-3); transition: transform .12s;
}
.estado-grupo-head .caret svg { width: 14px; height: 14px; stroke: currentColor; }
details.estado-grupo[open] > summary .caret { transform: rotate(90deg); }

/* ── Materiales pendientes de comprar (armado de OC) ──────────────────────── */
/* Mismo look que la vista "Por estado" de pedidos: sin recuadro ni bordes
   laterales, solo un header con fondo y las filas debajo. */
.mat-pendientes { display: flex; flex-direction: column; gap: 20px; }
.mat-grupo-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 7px 8px; background: var(--bg-sunken); border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}
.mat-grupo-prov { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; }
.mat-grupo-prov svg { width: 15px; height: 15px; color: var(--text-2); stroke: currentColor; }
.mat-grupo-total {
  margin-left: 6px; font-size: 12.5px; font-weight: 500; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* Fila de material estilo Pedidos: total siempre visible; sin expandible. La
   fila no navega (no es link), pero el checkbox on-hover la selecciona. */
.mat-row { border-bottom: 1px solid var(--border); }
.mat-row:last-child { border-bottom: none; }
.mat-row .lin-title { white-space: normal; }

/* Precio acopio / cuenta corriente como tags chicos en la meta de la fila. */
.mat-precio-tag {
  font-size: 12px; color: var(--text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.mat-precio-tag .mat-saldo { display: inline; margin-left: 4px; }
.mat-saldo { font-size: 11px; color: var(--text-3); font-weight: 400; }
.mat-saldo-corto { color: #b4382f; }
/* "Sin precio": el proveedor lo eligió una persona y dejó de tener precio para
   ese material (MGA-205). Ocupa el lugar del número, así que se lee como una
   falta y no como un estado más de la fila. */
.mat-sin-precio { color: #b4382f; font-style: italic; }

/* La fila cuyo proveedor decidió una persona (MGA-204 / ADR-0013). No lleva
   color ni fondo: lo que la distingue de una sugerida es que TIENE cosas que la
   otra no —el círculo del autor y el botón de volver al sugerido—, y eso ya se
   ve de un vistazo escaneando la columna. Pintarla además sería decir dos veces
   lo mismo en una mesa que ya usa el gris para "sin proveedor". */
.mat-volver {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0;
  background: none; border: none; border-radius: 4px;
  color: var(--text-3); cursor: pointer;
}
/* `stroke: currentColor` no es opcional: los íconos de `_icons.html` vienen sin
   atributo `stroke`, así que sin esta regla el SVG se dibuja invisible y el
   botón queda como una caja vacía (se vio en el QA de dev, no lo ve ningún
   test). Todas las reglas de ícono de esta hoja lo llevan junto al tamaño. */
.mat-volver svg { width: 13px; height: 13px; stroke: currentColor; }
.mat-volver:hover { background: var(--bg-sunken); color: var(--text); }

/* El tramo de un reparto se ve IGUAL que cualquier fila (.mat-row); no lleva
   estilo propio. El badge de proveedor se usa en el modal de confirmación. */
.mat-badge-prov {
  font-size: 11.5px; font-weight: 500; color: var(--text);
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 20px; padding: 2px 10px; white-space: nowrap;
}

/* Bloque de acciones de selección dentro de la isla (se oculta sin selección). */
.mat-island-sel { display: inline-flex; align-items: center; gap: 8px; }

/* Modal de confirmación de armado: lista de OC por proveedor. */
.mat-confirm-list { display: flex; flex-direction: column; gap: 8px; }
.mat-confirm-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 10px; background: var(--bg-sunken);
  border: 1px solid var(--border); border-radius: 8px;
}
.mat-confirm-detalle { font-size: 12.5px; font-variant-numeric: tabular-nums; }

/* Modal de reparto (dividir / reasignar). */
.mat-modal { max-width: 460px; }
.mat-modal-total { margin: 0 0 12px; }
.mat-tramos { display: flex; flex-direction: column; gap: 8px; }
.mat-tramo { display: flex; align-items: center; gap: 8px; }
.mat-tramo-cant {
  width: 90px; padding: 6px 8px; text-align: right;
  font-variant-numeric: tabular-nums; font-size: 13px;
  border: 1px solid var(--border); border-radius: 6px; background: var(--bg);
}
.mat-tramo-prov { flex: 1; }
.mat-tramo-del {
  border: none; background: none; color: var(--text-3);
  font-size: 18px; line-height: 1; cursor: pointer; padding: 0 4px;
}
.mat-tramo-del:hover { color: #a32d2d; }
.mat-tramo-add { margin-top: 10px; }
.mat-tramo-resto { margin: 10px 0 0; font-size: 12.5px; color: var(--text-2); }
.mat-tramo-resto.is-ok { color: #2e7d32; }
.mat-tramo-resto.is-err { color: #a32d2d; }

/* =========================================================================
   Motion — transiciones suaves de navegación (hx-boost) y micro-interacciones.
   Todo puro CSS, enganchado al ciclo de vida de htmx. Se desactiva entero si
   el sistema del usuario pide menos movimiento.
   ========================================================================= */

/* El contenido nuevo entra con un fade + empujoncito hacia arriba. Solo anima
   .content-inner, así el sidebar queda quieto al cambiar de pestaña. */
.content-inner { animation: page-in .18s ease both; }
@keyframes page-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Durante el swap de htmx, desvanecemos lo viejo apenas para que no haya un
   corte seco entre página y página. */
.content-inner.htmx-swapping { opacity: 0; transition: opacity .1s ease; }

/* Micro-interacciones consistentes en los elementos clickeables. */
.nav-item { transition: background .12s ease, color .12s ease; }
.btn, button.btn, .pill { transition: background .12s ease, border-color .12s ease, color .12s ease; }

@media (prefers-reduced-motion: reduce) {
  .content-inner,
  .content-inner.htmx-swapping,
  .nav-item, .btn, button.btn, .pill {
    animation: none !important;
    transition: none !important;
  }
}

/* Toast de error global (red de seguridad HTMX): un 403/500 o una caída de red
   nunca deben ser silencio total. Fijo arriba a la derecha, se apilan y se
   auto-ocultan a los ~6s (ver base.html). Tono de alerta como .tag-danger. */
#toast-host {
  position: fixed; top: 16px; right: 16px; z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: 8px;
  max-width: min(360px, calc(100vw - 32px)); pointer-events: none;
}
.toast-error {
  background: #fbebeb; color: #a32d2d; border: 1px solid #f0c4c4;
  border-radius: var(--radius); padding: 10px 12px; font-size: 13px;
  box-shadow: var(--shadow-modal); pointer-events: auto;
}
/* Toast de éxito (verde): el único caso positivo del ADR-0005 §5 — el resultado
   no está a la vista (las OC armadas se crean en otra pantalla) o la acción tocó
   N filas de un saque y el toast dice cuántas. Llega por el canal `toastOk`
   (HX-Trigger). Mismo formato que el de error, en tono positivo. */
.toast-ok {
  background: #eaf7ee; color: #1e7a3d; border: 1px solid #c6e6cf;
  border-radius: var(--radius); padding: 10px 12px; font-size: 13px;
  box-shadow: var(--shadow-modal); pointer-events: auto;
}

/* Nuevo pedido: fecha + solicitante en una fila de dos columnas. */
.np-head-row { display: flex; gap: 12px; }
.np-head-row .field { flex: 1; min-width: 0; }

/* --- Logos de proveedores -------------------------------------------------
   Imagen redonda con object-fit para no deformar. Tres tamaños: fila de la
   lista de proveedores (base), pill de la fila de OC (-sm) y preview en el
   modal de edición (-lg). Sin logo, cae al ícono de camión de siempre. */
.prov-logo {
  width: 18px; height: 18px; border-radius: 50%;
  object-fit: cover; display: block; flex-shrink: 0;
  background: var(--bg-sunken);
}
.pill-proveedor { display: inline-flex; align-items: center; gap: 6px; }
.pill-proveedor .prov-logo.prov-logo-sm { width: 14px; height: 14px; }
.logo-actual { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.logo-actual .prov-logo.prov-logo-lg { width: 48px; height: 48px; border-radius: 10px; }
.logo-actual .muted { color: var(--text-2); font-size: 12.5px; }
.field .hint { color: var(--text-3); font-size: 12px; margin-top: 4px; }

/* Ubicación de la obra (MGA-78): buscador Places + mapa con pin arrastrable. */
.obra-ubic-buscar { width: 100%; margin-bottom: 8px; }
.obra-ubic-map { width: 100%; height: 240px; border-radius: 10px; margin-bottom: 6px; background: var(--surface-2, #f2f2f2); }
.obra-ubic-resumen { font-size: 12.5px; }
.obra-ubic-resumen .muted { color: var(--text-2); }

/* ── Cesiones: la fila expande y muestra sus movimientos (MGA-137) ────────────
   Patrón nuevo para esta app, y se acepta a sabiendas: ADR-0004 avisa que «la
   inconsistencia que duele no es que haya dos patrones: es que el patrón se
   elegía sin regla». Acá la regla es la del prototipo — se probaron las tres
   formas con los datos de Bendito y ésta fue la única que muestra el detalle sin
   tapar el contexto contra el que hay que juzgarlo. */
thead th.col-chevron, tbody td.col-chevron { width: 16px; padding: 0 6px 0 8px; }
.cesion-chevron {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 5px; color: var(--text-3);
}
.cesion-chevron:hover { background: var(--bg-hover); color: var(--text); }
.cesion-chevron svg { width: 14px; height: 14px; stroke: currentColor; }
.cesion-ente.is-abierta > td { border-bottom-color: transparent; }
.cesion-ente.is-abierta { background: var(--bg-hover); }

/* Los movimientos, indentados bajo su ente y en un tono más liviano: son el
   detalle de la fila de arriba, no filas hermanas de las otras cesiones. */
.mov-row { cursor: default; }
.mov-row:hover { background: transparent; }
.mov-row > td { height: 36px; font-size: 13px; color: var(--text-2); border-bottom: none; }
/* El desplegado cierra con una línea, y la pone SIEMPRE su última fila —la de
   «Agregar movimiento», que es la que siempre está—. Con `:last-of-type` la
   línea caía en el último <tr> del tbody, que sólo es el correcto cuando la
   cesión abierta es la de más abajo. */
.mov-row.is-agregar > td { border-bottom: 1px solid var(--border); }
.mov-fecha { padding-left: 26px; white-space: nowrap; }
.mov-row.is-agregar > td:not(.col-chevron) { padding-left: 26px; }
.mov-acciones { display: inline-flex; align-items: center; gap: 2px; opacity: 0; }
.mov-row:hover .mov-acciones, .mov-acciones:focus-within { opacity: 1; }
.icono-btn.is-peligro:hover { background: #fbebeb; color: #a32d2d; }


/* ── Agrospot: el badge del producto ─────────────────────────────────────────
   El único lugar de una FILA donde entra el teal. Es el mismo par que ya usaba
   la platform (#f0f6f5 sobre #d1e7e4), ahora con nombre. */
.pill-producto {
  background: var(--brand-soft);
  border-color: var(--brand-line);
  color: var(--brand);
  font-weight: 500;
}

/* ==========================================================================
   Agrospot — lo que la app suma sobre el design system de MGA
   ========================================================================== */

/* ── Meta de la fila de CIERRE (ADR-0004 §5) ────────────────────────────────
   El mismo grid de anchos fijos que las listas de MGA, con las columnas de esta
   lista: producto · toneladas · ruta · autor · fecha. Cada celda se emite
   siempre, aunque venga vacía — la columna vacía es lo que sostiene la
   alineación de la de al lado. No hay columna de estado: el negocio todavía no
   tiene, y se va a derivar de las descargas. */
/* Seis tracks donde antes había cinco, y **el total es el mismo**: la ruta cede
   126px para que entre el estado. Eso no es aritmética por prolijidad — es lo que
   mantiene el círculo y la fecha en la MISMA vertical que Oportunidades y
   Cotizaciones (arrancan en 436px y 478px en las tres), que es la propiedad que
   hace que las listas se lean como una sola app. Lo que se angosta es la ruta,
   que ya venía con ellipsis y con el valor entero en su `title`. */
.lin-meta.meta-negocio {
  display: grid;
  grid-template-columns: 78px 96px 126px 96px 32px 84px;
  align-items: center; gap: 10px;
}
.meta-negocio .m-estado {
  min-width: 0; font-size: 12.5px; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meta-negocio .m-producto { min-width: 0; justify-self: start; }
.meta-negocio .m-producto .pill {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Las toneladas pegadas al borde derecho de su columna: así se comparan de
   arriba abajo (tabular-nums las alinea dígito con dígito). */
.meta-negocio .m-tn {
  justify-self: end; font-variant-numeric: tabular-nums;
  font-size: 12.5px; color: var(--text-2); white-space: nowrap;
}
/* El avance entra en esta MISMA columna y no en un track propio: el total de la
   meta —562px— es lo que mantiene el círculo y la fecha en la misma vertical que
   Oportunidades y Cotizaciones, y un track más lo paga la ruta, que a 56px deja
   de decir nada. El peor caso de ancho, «1.150/2.300 tn», mide 91 de los 96.

   Lo descargado va en el negro del texto y lo pactado en el gris de la columna,
   que es la misma jerarquía que usa `.viajes-avance b` en el encabezado de la
   pestaña: el número que cambia adelante, el que no cambia atrás. */
.meta-negocio .m-tn b { color: var(--text); font-weight: 600; }
.meta-negocio .m-ruta {
  min-width: 0; font-size: 12.5px; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meta-negocio .m-fecha { justify-self: end; }

/* El pie de la lista: cuántos cierres y cuántas toneladas hay a la vista. Es lo
   que el string "200 tn aprox" de la platform hacía imposible preguntar, y por
   eso vive acá y no en un dashboard aparte. */
.lista-pie {
  padding: 10px 14px; font-size: 12.5px; color: var(--text-3);
  border-top: 1px solid var(--border); font-variant-numeric: tabular-nums;
}

/* ── Grupos colapsables (ADR-0004 §4) ───────────────────────────────────── */
.lin-group > summary { list-style: none; }
.lin-group > summary::-webkit-details-marker { display: none; }
.lin-group-head {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 14px; cursor: pointer;
  background: var(--bg-sunken); border-bottom: 1px solid var(--border);
  font-size: 12.5px; color: var(--text-2); user-select: none;
}
.lin-group-head:hover { background: var(--bg-hover); }
.lin-group-caret { display: flex; transition: transform .12s ease; color: var(--text-3); }
.lin-group-caret svg { width: 14px; height: 14px; stroke: currentColor; }
.lin-group[open] > summary .lin-group-caret { transform: rotate(90deg); }
.lin-group-title { font-weight: 500; color: var(--text); }
.lin-group-n {
  min-width: 20px; padding: 0 6px; border-radius: 999px;
  background: var(--bg-active); color: var(--text-2); font-size: 11px; text-align: center;
}
.lin-group-tn { margin-left: auto; font-variant-numeric: tabular-nums; }

/* ── El detalle como DOCUMENTO ──────────────────────────────────────────────
   Los términos comerciales son prosa: precio, forma de pago, condición de
   recibo y cupo se pactan escribiéndolos, y así van al contrato. Por eso el
   cuerpo del detalle no es una tabla de campos — es el texto, con su etiqueta
   arriba en chico. Los datos que SÍ son campos (las partes, el producto, las
   toneladas) viven en el panel de propiedades de la derecha. */
.doc { margin-top: 18px; display: flex; flex-direction: column; gap: 2px; }
.doc-bloque { padding: 12px 0; border-bottom: 1px solid var(--border); }
.doc-bloque:last-child { border-bottom: 0; }
.doc-k {
  font-size: 11px; font-weight: 500; letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-3); margin-bottom: 4px;
}
.doc-v { font-size: 14px; line-height: 1.55; color: var(--text); max-width: 62ch; }

/* El cierre no describe el contrato: lo MUESTRA (AGR-316). Antes el fondo era
   el gris del visor nativo de PDF del browser; ahora es el de la app. El alto
   del iframe sigue la proporción A4 del documento (negocios/pdf.py usa A4) en
   vez de un 75vh arbitrario, para que el visor nativo no tenga que dejar
   margen: a lo ancho completo, como antes, sin recuadrar el documento. */
.embed-pdf {
  margin-top: 18px; border: 1px solid var(--border-strong); border-radius: 10px;
  overflow: hidden; background: var(--bg);
}
.embed-pdf iframe { display: block; width: 100%; aspect-ratio: 210 / 297; min-height: 520px; border: 0; }
/* La liquidación es una A4 apaisada (ver `negocios/pdf_liquidacion.py`): con el
   alto de una A4 vertical, el frame dejaría media hoja de gris debajo. */
.embed-pdf--apaisado iframe { aspect-ratio: 297 / 210; min-height: 0; }
.embed-pdf-foot {
  display: flex; align-items: center; justify-content: space-between;
  padding: 9px 14px; background: var(--bg); border-top: 1px solid var(--border);
  font-size: 12.5px; color: var(--text-2);
}
/* La marca «Precio editado a mano» (AGR-354): en el encabezado del panel de
   Liquidación, arriba de las tres ramas (documento, formulario o nada para
   liquidar todavía), en el mismo ámbar de `.campo-aviso` — avisa, no
   rechaza. */
.liquidacion-marca { margin: 0 0 14px; color: #a06a12; font-size: 12px; }

/* Las etapas que todavía no existen se anuncian en el documento y no en un menú
   escondido: es donde el usuario va a preguntar "¿y ahora qué?" con el cierre
   ya cargado. */
.doc-proximo {
  margin-top: 24px; padding: 14px 16px;
  background: var(--brand-soft); border: 1px solid var(--brand-line);
  border-radius: var(--radius-lg);
}
.doc-proximo-k {
  font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--brand); margin-bottom: 5px;
}
.doc-proximo-v { font-size: 13px; line-height: 1.5; color: var(--text-2); max-width: 62ch; }

/* ── Formulario del cierre: doce campos en dos columnas ─────────────────────
   Sigue siendo modal (ADR-0005 §1: no es de más de un paso, no es una grilla de
   revisión, no necesita ver la pantalla de atrás). Lo que necesita es ancho. */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 13px 14px; }
.form-grid .field-ancho { grid-column: 1 / -1; }
.form-grid .field { margin: 0; }

/* Los roles como checkboxes en línea: "Opera como ☑ Vendedor ☑ Comprador". Una
   empresa que hace las dos cosas es UNA ficha — el checkbox es lo que lo dice. */
/* `flex-wrap: wrap` (default nowrap): `roles` y `medios_pago` son listas
   cortas que nunca llenan el ancho ni en mobile, así que en desktop no se
   nota. Pero AGR-407 sumó listas más largas (`cultivos`, `tipos_negocio`,
   `prioridades`) al mismo componente, y a 375px una fila de nowrap se corre
   ~337px afuera del viewport en vez de acomodarse en varias líneas. */
.field-roles > div { display: flex; flex-wrap: wrap; gap: 10px 18px; }
/* Revisión de grupo de AGR-407: `.field-roles label` alcanzaba cualquier
   <label> adentro de `.field-roles`, y eso incluye el rótulo del CAMPO
   entero ("Cultivos que siembra"), no sólo el de cada opción ("Girasol") —
   los dos son <label> ahí adentro. Con la misma especificidad que
   `.field label` pero declarada después, esta regla le ganaba, y el rótulo
   del campo salía negro, 13.5px y en flex: el dibujo de una opción, no el de
   un rótulo. El rótulo del campo es un <label> hijo directo de `.field-roles`;
   las opciones viven dentro del `<div>` del widget (`.field-roles > div`),
   así que acotar el selector a `.field-roles > div label` alcanza sólo a las
   opciones y deja al rótulo del campo con el estilo normal de `.field label`. */
.field-roles > div label { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 13.5px; color: var(--text); cursor: pointer; }
/* Los roles usan el MISMO checkbox que las filas (sin el reveal en hover, que
   acá no tiene sentido): una app con dos dibujos de checkbox tiene dos. */
/* `padding: 0` deshace el `.field input { padding: 0 10px }` general (arriba,
   §Campos): ese padding no se anula en ningún otro lado de esta regla, y con
   `box-sizing: border-box` el navegador no deja que el ancho declarado (15px)
   sea menor que padding + borde (10+10+1+1 = 22px) — el checkbox terminaba
   apaisado (22×15) en vez de cuadrado. Sin altura de por medio (el padding
   vertical del campo ya es 0), el alto sí se respetaba: por eso se notaba
   ancho y no alto. */
.field-roles input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 15px; height: 15px; margin: 0; padding: 0; flex-shrink: 0; cursor: pointer;
  border: 1px solid var(--border-strong); border-radius: 4px;
  background: var(--bg) no-repeat center; background-size: 11px;
  transition: background-color .1s, border-color .1s;
}
.field-roles input[type="checkbox"]:hover { border-color: var(--text-3); }
.field-roles input[type="checkbox"]:checked {
  background-color: var(--brand); border-color: var(--brand);
  background-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2012'><path%20d='M2.6%206.2%204.9%208.5%209.4%203.7'%20fill='none'%20stroke='%23fff'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'/></svg>");
}
.field-roles input[type="checkbox"]:focus-visible {
  outline: none; box-shadow: 0 0 0 3px rgba(19, 97, 91, .16);
}

/* El CUIT en tabular para que las columnas de dígitos se lean de arriba abajo,
   y sin cortar: partido en dos renglones deja de ser un número. */
.col-mono { font-variant-numeric: tabular-nums; color: var(--text-2); white-space: nowrap; }

/* Los roles en una sola línea: el acopio lleva los dos y no puede por eso medir
   el doble de alto que las demás filas. */
.col-roles { white-space: nowrap; }
.col-roles .pill { margin-right: 4px; }

/* ── Configuración ─────────────────────────────────────────────────────────
   Pantalla propia y no modal: son dos formularios que conviven y uno de ellos
   es un texto legal de quince renglones. */
.ajustes { display: flex; flex-direction: column; gap: 28px; max-width: 720px; margin-top: 20px; }
/* Sin caja: la línea de la app es plana y a todo el ancho (ver `.table-wrap`).
   El recuadro venía de cuando Configuración era un formulario solo; con tres
   pestañas, una caja adentro de una pestaña es un borde de más. */
.ajustes-sec { padding: 0; }
.ajustes-sec + .ajustes-sec { margin-top: 34px; }
.ajustes-sec h2 { font-size: 15px; font-weight: 600; margin: 0 0 2px; }
.ajustes-sec .sub { margin: 0 0 16px; }
.ajustes-sec .field { margin-bottom: 14px; }
.ajustes-actions { display: flex; justify-content: flex-end; margin-top: 4px; }
.ajustes-hist { margin-top: 14px; font-size: 12.5px; color: var(--text-3); }

/* El modal ancho (el alta de un cierre: doce campos) no puede empezar a 12vh:
   con esa sangría la barra de acciones queda siempre abajo del pliegue y hay
   que scrollear el overlay para llegar a Guardar. El overlay sigue scrolleando
   —es lo que salva al modal más alto que la pantalla— pero arranca más arriba. */
.modal-backdrop:has(.modal-wide) { padding-top: 5vh; }

/* La aclaración de un campo: el porqué que el label no puede decir sin volverse
   una oración. Va debajo del control, no arriba: primero se ve el control. */
.field-nota { margin-top: 6px; font-size: 11.5px; color: var(--text-3); }

/* La aclaración de TODA una sección ("Sólo interno: no se publica."), no de un
   campo puntual: `.field-nota` sola trae 6px de margen y el mismo tamaño que
   un hint, y el ojo la lee pegada al primer campo en vez de leerla como lo
   que es. Revisión de grupo de AGR-407, sobre «Perfil del vendedor». */
.field-nota.field-nota-seccion { margin: 2px 0 16px; font-size: 12px; }

/* El subtítulo de un bloque adentro de una sección ("Qué hace", "Cómo
   vende"): tiene que pesar MENOS que `.form-sec-title` ("PERFIL DEL
   VENDEDOR") — si pesara igual, competiría con los títulos de sección reales
   por ese lugar, y dejaría de leerse como parte de la misma sección.
   Minúscula, no versalita como `.form-sec-title`, un tono más oscuro que ese
   título pero lejos del negro de un rótulo. Revisión de grupo de AGR-407. */
.form-subsec-title { font-size: 12px; font-weight: 600; color: var(--text-2); margin: 18px 0 10px; }
.form-subsec-title:first-of-type { margin-top: 0; }

/* El modal ancho, en pantallas chicas, deja de ser de dos columnas: dos campos
   de 200px cada uno son peores que uno de 420. */
@media (max-width: 720px) {
  .form-grid { grid-template-columns: 1fr; }
}

/* ── El modal de confirmación ───────────────────────────────────────────────
   La excepción a la cápsula de contexto: acá el título pesa, porque es lo único
   de la app que destruye algo. El ícono lo antecede y se pinta rojo cuando la
   acción es destructiva — que es la mitad de la advertencia, antes de leer. */
.modal-head-confirmar { align-items: flex-start; gap: 10px; padding: 16px 12px 8px 16px; }
.modal-head-confirmar h2 { margin: 0; font-size: 14.5px; font-weight: 600; line-height: 1.45; }
.modal-head-icono {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 22px; height: 22px; border-radius: 6px;
  background: var(--bg-sunken); color: var(--text-2);
}
.modal-head-icono.is-peligro { background: rgba(163, 45, 45, .09); color: var(--danger); }
.modal-head-icono svg { width: 14px; height: 14px; stroke: currentColor; }
.modal-confirmar .modal-body { padding-left: 48px; }

/* ── Tom Select y el date, adentro del modal ────────────────────────────────
   Los dos controles que el navegador (o la librería) dibuja por su cuenta
   tenían caja blanca con borde mientras el resto de los campos son superficies
   hundidas sin borde. En un formulario de doce campos eso se ve como dos clases
   de campo, y no las hay: son todos lo mismo. Se alinean acá, en el modal, para
   no tocar el .ts-control de la grilla de la OC, que sí quiere borde. */
.modal .ts-control {
  border-color: transparent !important;
  background: var(--bg-sunken);
  border-radius: 7px;
  transition: background .1s ease, border-color .1s ease, box-shadow .1s ease;
}
.modal .ts-wrapper:not(.focus) .ts-control:hover { background: var(--bg-hover); }
.modal .ts-wrapper.focus .ts-control {
  background: var(--bg);
  border-color: var(--brand) !important;
  box-shadow: 0 0 0 3px rgba(19, 97, 91, .10);
}

/* El nativo `input[type=date]` trae su propio ícono de calendario y, en Chrome,
   su propio fondo: sin esto queda blanco entre campos grises. */
.modal .field input[type="date"] { background: var(--bg-sunken); }
.modal .field input[type="date"]:hover { background: var(--bg-hover); }
.modal .field input[type="date"]:focus { background: var(--bg); }
.modal .field input[type="date"]::-webkit-calendar-picker-indicator {
  opacity: .45; cursor: pointer;
}
.modal .field input[type="date"]:hover::-webkit-calendar-picker-indicator { opacity: .8; }

/* ── El control de ubicación ────────────────────────────────────────────────
   Dos estados en el mismo hueco: buscador cuando no hay nada elegido, chip
   cuando sí. No conviven — un buscador al lado de lo ya elegido invita a
   buscar de nuevo, y lo que hay que invitar es a seguir con el formulario. */
.ubi { position: relative; }

.ubi-elegida {
  display: flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 6px 0 10px;
  border: 1px solid transparent; border-radius: 7px;
  background: var(--bg-sunken); font-size: 13.5px;
}
.ubi-pin { display: flex; color: var(--brand); flex-shrink: 0; }
.ubi-pin svg { width: 14px; height: 14px; stroke: currentColor; }
.ubi-nombre { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ubi-prov { color: var(--text-3); font-size: 12.5px; white-space: nowrap; }
.ubi-x {
  margin-left: auto; flex-shrink: 0;
  width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 5px; background: transparent; color: var(--text-3); cursor: pointer;
}
.ubi-x:hover { background: var(--bg-active); color: var(--text); }
.ubi-x svg { width: 13px; height: 13px; stroke: currentColor; }

.ubi-buscador { position: relative; }
.ubi-input {
  width: 100%; height: 34px; padding: 0 10px;
  border: 1px solid transparent; border-radius: 7px;
  background: var(--bg-sunken); color: var(--text); font-size: 13.5px;
  transition: background .1s ease, border-color .1s ease, box-shadow .1s ease;
}
.ubi-input::placeholder { color: var(--text-3); }
.ubi-input::-webkit-search-cancel-button { display: none; }
.ubi-input:hover { background: var(--bg-hover); }
.ubi-input:focus {
  outline: none; background: var(--bg);
  border-color: var(--brand); box-shadow: 0 0 0 3px rgba(19, 97, 91, .10);
}

/* El giro mientras Mapbox contesta. Sin esto, tipear una localidad que no está
   cargada parece que no hizo nada durante medio segundo. htmx enciende
   `.htmx-request` en el contenedor (hx-indicator). */
.ubi-spinner {
  position: absolute; right: 10px; top: 10px;
  width: 13px; height: 13px; border-radius: 50%;
  border: 1.5px solid var(--border-strong); border-top-color: var(--brand);
  opacity: 0; transition: opacity .1s ease;
}
.ubi.htmx-request .ubi-spinner { opacity: 1; animation: ubi-giro .6s linear infinite; }
@keyframes ubi-giro { to { transform: rotate(360deg) } }

/* Los resultados flotan: adentro del flujo empujarían los campos de abajo cada
   vez que se tipea una letra, y el formulario saltaría solo. */
.ubi-res:not(:empty) {
  position: absolute; z-index: var(--z-popover); left: 0; right: 0; top: 38px;
  padding: 4px; border: 1px solid var(--border); border-radius: 9px;
  background: var(--bg); box-shadow: 0 10px 32px rgba(8, 9, 10, .14);
  max-height: 288px; overflow-y: auto;
}

/* El encabezado de cada grupo. Es lo que hace visible la diferencia entre
   reusar un lugar y crear uno — la decisión que evita cinco «Villa María». */
/* El encabezado de cada grupo. Es lo que hace visible la diferencia entre
   reusar un lugar y crear uno — la decisión que evita cinco «Villa María».

   Hereda de `.filtro-sec`, el encabezado de sección del menú de filtros, y NO
   de `.form-sec-title`. Los dos son "títulos de sección", pero son de dos
   familias distintas: `.form-sec-title` vive en el cuerpo de un modal, donde
   ordena bloques grandes y puede darse el lujo de las mayúsculas y el semibold;
   esto vive en un popover de 288px arriba de una lista, donde lo que tiene que
   pesar son los resultados. En mayúsculas y en 600 competía con ellos, que es
   por qué se leía "puesto por default": no era falta de estilo, era el estilo
   equivocado. */
.ubi-grupo {
  display: flex; align-items: baseline; gap: 6px;
  padding: 6px 9px 3px;
  font-size: 11px; font-weight: 400; letter-spacing: .03em;
  color: var(--text-3);
}
/* La aclaración va todavía más callada que su propio encabezado: dice algo que
   sólo importa la primera vez, y después es paisaje. */
.ubi-grupo-nota { font-size: 10.5px; color: var(--text-3); opacity: .7; }
.ubi-grupo:not(:first-child) { margin-top: 4px; border-top: 1px solid var(--border); padding-top: 8px; }

.ubi-opt {
  width: 100%; display: flex; align-items: center; gap: 8px;
  padding: 7px 9px; border: 0; border-radius: 6px;
  background: transparent; text-align: left; cursor: pointer;
  font: inherit; font-size: 13px; color: var(--text-2);
}
/* Una sola pinta para el hover del mouse y para la marca del teclado: si el
   teclado resaltara distinto que el mouse serían dos idiomas para "esta es la
   que se va a elegir". */
.ubi-opt:hover, .ubi-opt.is-marcada { background: var(--bg-hover); color: var(--text); }
.ubi-opt-pin { display: flex; color: var(--brand); flex-shrink: 0; }
.ubi-opt-pin svg { width: 14px; height: 14px; stroke: currentColor; }
.ubi-opt-nombre {
  color: var(--text); flex-shrink: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Lo tipeado, adentro del nombre. Sin fondo: un highlight amarillo en una lista
   de seis renglones es más ruido que ayuda; alcanza con el peso. */
.ubi-opt-nombre mark { background: none; color: var(--brand); font-weight: 600; }
.ubi-opt-sec {
  color: var(--text-3); font-size: 12px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* El pin apagado y el + a la derecha: las dos señales de que esto todavía no
   existe y elegirlo lo crea. */
.ubi-opt.is-nueva .ubi-opt-pin { color: var(--text-3); }
.ubi-opt-mas { margin-left: auto; flex-shrink: 0; display: flex; color: var(--text-3); }
.ubi-opt-mas svg { width: 13px; height: 13px; stroke: currentColor; }
.ubi-opt.is-marcada .ubi-opt-mas, .ubi-opt:hover .ubi-opt-mas { color: var(--brand); }

.ubi-vacio { padding: 9px 10px; font-size: 12.5px; color: var(--text-3); line-height: 1.45; }
.ubi-vacio code {
  font-size: 11.5px; padding: 1px 4px; border-radius: 4px;
  background: var(--bg-sunken); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* ── El modal de descarga del contrato ──────────────────────────────────────
   Dos opciones, cada una con su explicación, y arriba una maqueta de las hojas
   que van a salir. La explicación no es adorno: «incluir condiciones» y
   «mostrar comisión» cambian lo que se firma, y el label solo no alcanza
   para decidir. La maqueta tampoco: la segunda hoja se apaga al destildar, así
   que la consecuencia se ve antes de tomar la decisión. */
.modal-pdf { width: 420px; }
.modal-pdf .modal-body { padding-top: 2px; }

/* Las hojitas, en el color de papel del contrato de verdad (#E8EBE4): la
   maqueta y el PDF tienen que verse de la misma familia o no es una maqueta. */
.hojas {
  display: flex; gap: 10px; justify-content: center;
  padding: 14px 0 18px;
}
.hoja {
  position: relative;
  width: 62px; height: 88px; padding: 9px 8px 0;
  display: flex; flex-direction: column; gap: 4px;
  border-radius: 4px; background: #e8ebe4;
  box-shadow: 0 1px 3px rgba(8, 9, 10, .1), 0 0 0 1px rgba(37, 104, 91, .14);
  transition: opacity .12s ease, box-shadow .12s ease, background .12s ease;
}
.hoja-linea { height: 2px; border-radius: 1px; background: rgba(37, 104, 91, .3); }
.hoja-linea.is-titulo { height: 4px; width: 60%; margin: 0 auto 4px; background: rgba(26, 58, 58, .45); }
.hoja-linea.is-parrafo { height: 9px; border-radius: 2px; background: rgba(37, 104, 91, .16); }
.hoja-n {
  position: absolute; left: 0; right: 0; bottom: -18px;
  text-align: center; font-size: 10.5px; color: var(--text-3);
}

/* La hoja 2, apagada: fondo vacío y borde punteado. No desaparece — el hueco es
   parte de la respuesta ("el contrato va a tener una sola hoja"). */
.sin-condiciones .hoja-condiciones {
  background: transparent; opacity: .5;
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
.sin-condiciones .hoja-condiciones .hoja-linea { background: var(--border-strong); }

.opciones { display: flex; flex-direction: column; gap: 2px; }
.opt {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 9px 10px; border-radius: 7px; cursor: pointer;
}
.opt:hover { background: var(--bg-hover); }
.opt:has(input:disabled) { cursor: default; opacity: .55; }
.opt:has(input:disabled):hover { background: transparent; }
.opt-static { cursor: default; }
.opt-static:hover { background: transparent; }
.opt input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  width: 15px; height: 15px; margin: 1px 0 0; flex-shrink: 0; cursor: pointer;
  border: 1px solid var(--border-strong); border-radius: 4px;
  background: var(--bg) no-repeat center; background-size: 11px;
  transition: background-color .1s, border-color .1s;
}
.opt input[type="checkbox"]:checked {
  background-color: var(--brand); border-color: var(--brand);
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20viewBox%3D%270%200%2012%2012%27%3E%3Cpath%20d%3D%27M2.6%206.2%204.9%208.5%209.4%203.7%27%20fill%3D%27none%27%20stroke%3D%27%23fff%27%20stroke-width%3D%271.9%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27/%3E%3C/svg%3E");
}
.opt input[type="checkbox"]:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(19,97,91,.16); }
/* El texto en su propia columna del flex: sin esto los dos <span> de adentro
   dependen de que el label sea flex, y cualquier reset que lo vuelva inline
   apelmaza título y descripción en un solo párrafo corrido. */
.opt-txt { display: flex; flex-direction: column; min-width: 0; }
.opt-k { font-size: 13.5px; color: var(--text); line-height: 1.35; }
.opt-d { font-size: 12px; color: var(--text-3); margin-top: 2px; line-height: 1.45; }

/* ── La puerta ──────────────────────────────────────────────────────────────
   La pantalla de login usa `base_blank.html`: sin sidebar. Mostrarle la
   navegación entera a alguien que todavía no entró es enseñarle el mapa de una
   casa cerrada — y de paso le canta qué secciones existen. */
.card-login { padding: 26px 28px 28px; text-align: left; }
.card-login h1 { margin: 0 0 4px; font-size: 17px; font-weight: 600; }
.card-login .sub { margin: 0 0 20px; font-size: 13px; color: var(--text-2); line-height: 1.5; }
.card-login .btn { width: 100%; height: 36px; justify-content: center; font-size: 13.5px; }

/* ── Quién está adentro, en el pie del sidebar ────────────────────────────── */
.sb-usuario {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px; margin-bottom: 2px; border-radius: 6px;
  font-size: 13px; color: var(--text-2);
}
.sb-usuario:hover { background: var(--bg-hover); }
.sb-usuario-nombre {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sb-salir {
  margin-left: auto; flex-shrink: 0;
  width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
  border-radius: 5px; color: var(--text-3);
}
.sb-salir:hover { background: var(--bg-active); color: var(--text); }
.sb-salir svg { width: 15px; height: 15px; stroke: currentColor; }
.lista-pie-nota { color: var(--text-3); opacity: .8; }

/* ── Oportunidades (el libro de compras) ─────────────────────────────────── */

/* Los estados de una orden, sobre el dot que ya define `.lin-status`. El relleno
   distingue lo que está VIVO de lo que no: activa es el único que le contesta a
   una cotización, así que es el único pintado lleno. El resto queda en aro. */
.lin-status.dot-activa    { color: #2e7d32; background: currentColor; }
.lin-status.dot-pausada   { color: #e08a00; }
.lin-status.dot-borrador  { color: var(--text-3); }
.lin-status.dot-revision  { color: #1565c0; }
.lin-status.dot-cerrada   { color: #7b1fa2; }
.lin-status.dot-rechazada { color: #c62828; }

/* La meta de la fila (ADR-0004 §5): producto · toneladas · precio · autor ·
   fecha.

   **Las mismas medidas que `.meta-negocio`**, y eso es lo que importa: los tracks
   son px fijos y `.lin-meta` es `flex-shrink: 0`, así que la meta siempre arranca
   en el mismo x y las columnas caen en la misma vertical en todas las filas — y
   además en la misma que Cierres, con lo cual las tres listas se leen como una.

   Tenía un `minmax(0, 1fr)` en el medio para que el precio se llevara el resto, y
   era el bug: dentro de una caja que se encoge al contenido, un track `1fr` mide
   lo que mide su texto, así que la grilla cambiaba de ancho por fila y el pill de
   producto arrancaba en un x distinto en cada renglón. Lo que varía de largo se
   recorta con ellipsis, no estira la columna. */
.lin-meta.meta-oportunidad {
  display: grid;
  grid-template-columns: 78px 96px 232px 32px 84px;
  gap: 10px;
  align-items: center;
}
.meta-oportunidad .m-producto { min-width: 0; justify-self: start; }
.meta-oportunidad .m-producto .pill {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meta-oportunidad .m-tn { justify-self: end; font-variant-numeric: tabular-nums; }
.meta-oportunidad .m-precio { min-width: 0; justify-self: start; }
.meta-oportunidad .m-precio .pill {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meta-oportunidad .m-fecha { justify-self: end; }

/* La columna de números, sobre la tabla que ya tiene la app. Es el único hueco
   que le faltaba: `tabular-nums` es lo que hace que dos precios se puedan
   comparar de un vistazo, porque las cifras caen en la misma vertical. */
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* La nota al pie de un bloque del documento: con qué pizarra y con qué dólar se
   resolvió el precio de arriba. Va abajo y en chico porque es la procedencia del
   número, no el número. */
.doc-nota { font-size: 12px; color: var(--text-3); margin: 8px 0 0; max-width: 62ch; }

/* ── Cotizaciones ────────────────────────────────────────────────────────── */

/* El estado que muestra la fila es el de la última CORRIDA, no del pedido: el
   pedido no tiene estado. `lista` va lleno porque es el único que significa que
   la persona ya tiene su respuesta. */
.lin-status.dot-lista     { color: #2e7d32; background: currentColor; }
.lin-status.dot-corriendo { color: #1565c0; }
.lin-status.dot-fallida   { color: #c62828; background: currentColor; }

/* Los mismos tracks que Cierres y Oportunidades, con el de autor absorbido por el
   del medio (una cotización no tiene autor: entra de afuera). 232 + 10 de gap + 32
   = 274, así el borde derecho cae exactamente donde en las otras dos. */
.lin-meta.meta-cotizacion {
  display: grid;
  grid-template-columns: 78px 96px 274px 84px;
  gap: 10px;
  align-items: center;
}
.meta-cotizacion .m-producto { min-width: 0; justify-self: start; }
.meta-cotizacion .m-producto .pill {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.meta-cotizacion .m-tn,
.meta-cotizacion .m-mejor,
.meta-cotizacion .m-fecha { justify-self: end; font-variant-numeric: tabular-nums; }

/* La tabla de la respuesta: la que hoy sólo existe adentro de un mail. Diez
   columnas no entran en un celular, así que scrollea ella y no la página. */
.tabla-wrap { overflow-x: auto; margin: 4px 0 10px; }
.tabla-respuesta { min-width: 820px; }
.tabla-respuesta th, .tabla-respuesta td { white-space: nowrap; }
.tabla-respuesta .fuerte { font-weight: 600; }
/* Las filas de esta tabla no llevan a ninguna parte: el cursor de la tabla de la
   app promete un click que acá no existe. */
.tabla-respuesta tbody tr { cursor: default; }
/* Lo que no le gana a Rosario se muestra igual pero apagado: la pregunta
   «¿por qué a este no le apareció tal comprador?» se contesta viéndolo. */
.tabla-respuesta .fila-apagada td { color: var(--text-3); }
.tabla-respuesta .fila-benchmark td {
  background: var(--bg-hover); font-weight: 500; border-bottom: 0;
}
.sub-celda { display: block; font-size: 11px; color: var(--text-3); margin-top: 2px; }
.txt-bien { color: #2e7d32; }
.txt-peligro { color: #c62828; }


/* ── El titular de la respuesta ───────────────────────────────────────────── */

/* La respuesta en una oración, arriba de la tabla. La cifra es lo único grande
   de la pantalla a propósito: es el número que el operador repite por teléfono,
   y hasta ahora había que reconstruirlo leyendo diez columnas.

   Sin caja ni fondo: lo que la separa de la tabla es el aire y el peso del
   número, no un recuadro. Un panel de color acá competiría con la fila del
   benchmark, que es lo único que ya usa fondo en esta pantalla. */
.titular { margin: 2px 0 20px; }
.titular-cifra {
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.titular-unidad {
  font-size: 15px;
  font-weight: 400;
  color: var(--text-3);
  letter-spacing: 0;
  margin-left: 1px;
}
.titular-detalle {
  font-size: 13.5px;
  color: var(--text-2);
  line-height: 1.55;
  max-width: 62ch;
  margin-top: 5px;
}
.titular-detalle strong { color: var(--text); font-weight: 500; }

/* El scroller de la tabla ancha. `min-width: 0` es lo que hace que funcione:
   sin él, el `min-width` de la tabla se propaga hacia arriba por el grid y en vez
   de scrollear la tabla se aplasta la columna entera (con la respuesta —neto y
   diferencia— fuera de pantalla, que era justo lo que había que ver). */
.tabla-wrap { min-width: 0; }

/* La línea que separa la respuesta (neto y diferencia) de cómo se llegó a ella.
   Es lo que hace legible que la tabla se corte por ahí: lo de la derecha es
   verificación, no resultado. */
.tabla-respuesta .sep-calculo { border-left: 1px solid var(--border); padding-left: 12px; }

/* ── Configuración con pestañas ───────────────────────────────────────────── */

/* El encabezado de una sección que además tiene una acción a la derecha
   («Traer ahora»). Es el `.page-head` en chico: mismo reparto, un nivel abajo. */
.sec-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; margin-bottom: 14px;
}
.sec-head .sub { margin: 0; }
.sec-head h2 { margin-bottom: 2px; }

/* La carga a mano va detrás de un `<details>`, cerrado por default: es la salida
   de emergencia para cuando la fuente falla, no el camino normal. Abrirla siempre
   invitaría a tipear precios que el sistema ya trae solo. */
.ajustes-detalle { margin-top: 16px; }
.ajustes-detalle > summary {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-size: 13px; color: var(--text-2); padding: 4px 0; width: fit-content;
  list-style: none;
}
.ajustes-detalle > summary::-webkit-details-marker { display: none; }
.ajustes-detalle > summary::before {
  content: "+"; display: inline-flex; justify-content: center;
  width: 14px; font-size: 14px; line-height: 1; color: var(--text-3);
}
.ajustes-detalle[open] > summary::before { content: "−"; }
.ajustes-detalle > summary:hover { color: var(--text); }
.ajustes-detalle[open] > summary { margin-bottom: 12px; }

/* El formulario de carga: campos en grilla y el botón abajo a la izquierda, no
   a la derecha como en el form de la pestaña General. Ahí el botón cierra una
   pantalla entera; acá cierra un bloque de tres campos. */
.form-inline { padding: 12px 14px; background: var(--bg-sunken); border-radius: var(--radius); }
.form-inline .form-grid { margin-bottom: 4px; }

.lista-simple { list-style: none; margin: 0; padding: 0; font-size: 13.5px; }
.lista-simple li { padding: 5px 0; border-bottom: 1px solid var(--border); color: var(--text-2); }
.lista-simple li:last-child { border-bottom: 0; }

/* El renglón de curva del tarifario: no es un tramo, es una tirada de tramos de
   un kilómetro resumida. Se distingue en tono para que no se lea como un escalón
   cargado a mano. */
.fila-curva td { color: var(--text-2); }
.fila-curva { cursor: default; }

/* Un aviso de que algo salió bien. Faltaba la variante: los mensajes de éxito
   salían en ámbar, que es el color de «ojo con esto». */
.aviso-ok { background: #eef7ef; color: #2e6b34; border-color: #cfe6d2; }

/* ── La superficie pública ──────────────────────────────────────────────────
   El formulario de cotización que abre cualquiera, sin login. Usa los mismos
   tokens que la app —el teal de la marca, los neutros de MGA, Inter— y no una
   paleta de landing: es la misma empresa y es la misma tabla de resultados. Lo
   único que cambia es la escala, porque acá el que lee está solo y con el
   teléfono en la mano, no adentro de una grilla densa.

   Lo que NO se reusa de la app: el shell. Nada de sidebar, nada de modales,
   nada de islas de selección. Una columna centrada y listo. */
/* `height: auto` pisa el `height: 100%` de `html, body` de la app: con el body
   del alto de la ventana, la navbar `sticky` se iba con el scroll apenas se
   bajaba una pantalla, porque lo fijo no sale de su padre (QA de AGR-348). */
.pub {
  background: var(--bg-sunken);
  height: auto; min-height: 100vh;
  display: flex; flex-direction: column;

  /* Las curvas del sitio público, una sola vez. Las de CSS (`ease`,
     `ease-out`) arrancan tibias; éstas llegan rápido y se asientan, que es lo
     que hace que algo que entra se sienta contestado y no arrastrado. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}
/* La navbar del sitio: la MISMA en la landing y en el cotizador. Es lo que hace
   que el formulario no se sienta una pantalla colgada en otro dominio.

   No es el topbar de la app: no es la tapa de un sidebar, no dice en qué sección
   estás y no lleva a nadie logueado. Es un logo, tres links y la acción. Y es
   `sticky`, porque en una página larga la acción tiene que seguir estando. */
/* Todo con prefijo `pub-`: `.nav`, `.nav-item` y compañía ya son la navegación
   del sidebar de la app en esta misma hoja, y sin el prefijo esta barra heredaba
   su `flex-direction: column`. */
.pub-nav {
  display: block;
  position: sticky; top: 0; z-index: 40;
  background: rgba(255, 255, 255, .92);
  backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--border);
}
/* A sangre y no dentro del contenedor de 1180 centrado: el logo va contra el
   borde de la pantalla, que es donde se lo busca. Alinearlo con la columna del
   contenido lo deja flotando en el medio en un monitor ancho. */
.pub-nav-in {
  width: 100%;
  padding: 11px 30px;
  display: flex; align-items: center; gap: 30px;
}
.pub-nav-logo { display: flex; align-items: center; }
.pub-nav-logo img { display: block; height: 48px; width: auto; }
.pub-nav-links { display: flex; align-items: center; gap: 26px; margin-left: 14px; }
.pub-nav-links a {
  font-size: 14px; font-weight: 500; color: var(--text-2); text-decoration: none;
}
.pub-nav-links a:hover { color: var(--brand); }
/* Las dos acciones de la derecha. El `margin-left: auto` es del GRUPO y no del
   botón: desde que «Ingresar» se le puso al lado, ponerlo en `.pub-nav-cta`
   metía el aire entre los dos en vez de entre los links y el par. */
.pub-nav-acciones { margin-left: auto; display: flex; align-items: center; gap: 18px; }
.pub-nav-cta {
  height: 38px; padding: 0 20px; border-radius: 999px; font-size: 14px;
}
/* «Sumate como comprador de granos» (AGR-433): la etiqueta entera es larga y
   en una barra que ya tiene cuatro links y «Ingresar» no puede partirse en dos
   renglones ni empujar al resto. Abajo de 1200 se acorta a «Sumate como
   comprador» y abajo de 900 la barra ya esconde los links, así que queda sola.
   El texto largo vive en el HTML: el CSS sólo lo tapa (`font-size: 0`) y pone
   el corto con `::after`, que es lo mismo que hace `.pub-nav-ingresar`. */
.pub-nav-cta-comprador { white-space: nowrap; }
@media (max-width: 1200px) {
  .pub-nav-cta-comprador { font-size: 0; }
  .pub-nav-cta-comprador::after { content: "Sumate como comprador"; font-size: 14px; }
}
@media (max-width: 520px) {
  .pub-nav-cta-comprador::after { content: "Soy comprador"; }
}
/* La puerta del equipo: presente en todas las públicas, y deliberadamente
   más callada que «Cotizar». Quien entra por acá ya sabe que existe; quien
   viene a vender granos no tiene nada que buscar del otro lado. */
.pub-nav-ingresar {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13.5px; font-weight: 500; color: var(--text-3); text-decoration: none;
  white-space: nowrap;
}
.pub-nav-ingresar svg { width: 15px; height: 15px; flex: none; }
.pub-nav-ingresar:hover { color: var(--brand); }

@media (max-width: 900px) {
  /* El rótulo entero no entra al lado de «Cotizar», pero el ícono solo sí: el
     link no se va, se encoge. El nombre queda en el `aria-label` para que un
     lector de pantalla siga leyendo lo mismo que se leía en desktop. */
  .pub-nav-ingresar { font-size: 0; gap: 0; }
  .pub-nav-ingresar svg { width: 18px; height: 18px; }
}

@media (max-width: 720px) {
  /* Los links se van y queda logo + acción. Un menú hamburguesa para tres anclas
     de la misma página es una pieza con estado —abierto, cerrado, foco atrapado—
     para no ganar nada: los links siguen existiendo en el pie. */
  .pub-nav-in { padding: 9px 16px; gap: 12px; }
  .pub-nav-links { display: none; }
  .pub-nav-acciones { gap: 14px; }
  .pub-nav-logo img { height: 40px; }
}

.pub-shell {
  flex: 1;
  display: flex; justify-content: center;
  /* El margen lateral de la pública, en una variable porque no lo usa sólo
     este padding: la pasarela de la landing (AGR-396) se sale del margen con
     un margen negativo para llegar al vidrio, y ese negativo tiene que ser
     exactamente éste. Con el 16px que tenía escrito a mano se pasaba 2px de
     cada lado a 375 y la página scrolleaba de costado. */
  --pub-gutter: 20px;
  padding: 32px var(--pub-gutter) 48px;
}
.pub-pie {
  padding: 20px; text-align: center;
  font-size: 12.5px; color: var(--text-3);
  border-top: 1px solid var(--border);
}

/* Dos anchos: la tarjeta del formulario y la pantalla de resultados, que tiene
   que dar lugar a ocho columnas sin apretarlas. */
.pub-card {
  width: 100%; max-width: 460px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 28px;
  box-shadow: 0 1px 2px rgba(8, 9, 10, .04);
  align-self: flex-start;
}
.pub-ancho { width: 100%; max-width: 940px; }

.pub-h1 {
  margin: 0 0 6px; font-size: 21px; font-weight: 600;
  letter-spacing: -.01em; line-height: 1.3;
}
.pub-sub { margin: 0 0 22px; font-size: 14px; color: var(--text-2); line-height: 1.55; }
.pub-sub-centrada { text-align: center; }
.pub-encabezado { margin-bottom: 18px; }

/* El botón de la pública es ancho y alto: es la única acción de la pantalla. */
.pub-btn { width: 100%; height: 40px; justify-content: center; font-size: 14px; margin-top: 6px; }
.pub-nota { margin: 10px 0 0; font-size: 12.5px; color: var(--text-3); text-align: center; }

/* El teaser: el conteo grande, y la cifra tapada al lado. La cifra es un adorno
   —el número nunca llega al template, ver `publico/views.py:_teaser`—, así que
   acá no hay nada que un inspector de elementos pueda destapar. */
.pub-teaser {
  display: flex; align-items: center; gap: 16px;
  padding: 16px; margin-bottom: 4px;
  background: var(--brand-soft); border: 1px solid var(--brand-line);
  border-radius: var(--radius);
}
.pub-teaser-n {
  font-size: 34px; font-weight: 600; line-height: 1;
  color: var(--brand); font-variant-numeric: tabular-nums;
}
.pub-teaser-txt { font-size: 13.5px; line-height: 1.5; color: var(--text); }
.pub-tapado {
  text-align: center; padding: 18px 0 6px;
  color: var(--text-3); user-select: none;
}
.pub-tapado-cifra { font-size: 30px; font-weight: 600; letter-spacing: .02em; }
.pub-tapado-unidad { font-size: 15px; }

/* El cierre de la pantalla de resultados: la única salida que tiene el vendedor,
   y va a Agrospot y no al comprador. */
.pub-cta {
  margin-top: 22px; padding: 18px 20px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap;
}
.pub-cta-titulo { font-size: 15px; font-weight: 600; }
.pub-cta-txt { font-size: 13.5px; color: var(--text-2); margin-top: 2px; }
.pub-cta-tel { font-size: 15px; font-weight: 600; color: var(--brand); }

/* En el teléfono la tarjeta ocupa todo y el titular baja un punto: la tabla de
   resultados ya scrollea sola dentro de `.tabla-wrap`. */
@media (max-width: 640px) {
  .pub-shell { --pub-gutter: 14px; padding: 20px var(--pub-gutter) 36px; }
  .pub-card { padding: 22px 18px; }
  .pub-h1 { font-size: 19px; }
  .pub-teaser { gap: 12px; padding: 14px; }
  .pub-teaser-n { font-size: 28px; }
}

/* ── La empresa dibujada: logo o monograma ───────────────────────────────────
   El monograma es el estado NORMAL y no el de error: la mayoría de las empresas
   no va a cargar logo nunca, así que tiene que verse como una decisión de diseño
   y no como una imagen que faltó. Por eso lleva color propio —derivado del CUIT,
   estable en el tiempo— y no un gris de placeholder. */
.emp-logo {
  flex: none;
  width: 22px; height: 22px; border-radius: 5px;
  display: inline-flex; align-items: center; justify-content: center;
  overflow: hidden;
  font-size: 9.5px; font-weight: 600; letter-spacing: .02em;
  color: var(--text-inv);
}
/* `contain` y no `cover`: un logo recortado es una marca mal usada. El fondo
   blanco es el que asumen casi todos los logos que nos van a mandar. */
.emp-logo img { width: 100%; height: 100%; object-fit: contain; background: #fff; }
.emp-logo-lg { width: 48px; height: 48px; border-radius: 10px; font-size: 17px; }
.emp-logo-xl { width: 40px; height: 40px; border-radius: 9px; font-size: 15px; }

/* Los seis tonos. Todos suficientemente oscuros para leer blanco encima, y todos
   dentro del mundo de la marca: nada de un arcoíris de avatares de app. */
.mono-0 { background: #13615b; }
.mono-1 { background: #002c36; }
.mono-2 { background: #4a6b2a; }
.mono-3 { background: #8a4b2a; }
.mono-4 { background: #2f5d7c; }
.mono-5 { background: #6b5b2a; }

/* El campo del logo en el formulario de empresa. */
.field-logo .logo-fila { display: flex; align-items: center; gap: 14px; }
.field-logo .logo-controles { min-width: 0; flex: 1; }
.field-logo input[type="file"] { font-size: 12.5px; color: var(--text-2); max-width: 100%; }
.field-logo .hint { margin-top: 4px; }
.logo-quitar {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 8px; font-size: 12.5px; color: var(--text-2);
}

/* ── La landing ──────────────────────────────────────────────────────────────
   Poppins para los títulos porque es la del logo, Inter para el texto corrido
   porque es la que ya está afinada para leer. La regla que ordena todo lo demás:
   el teal es de las ACCIONES (el botón, los links) y el papel es de los BLOQUES.
   Si el fondo grita, el botón deja de ser lo único que se ve. */
/* El hero ocupa la pantalla entera, siempre: lo que se ve al abrir agrospot.com.ar
   es la promesa y el botón, y nada más. «La pantalla» es el viewport MENOS lo que
   ya ocupan la navbar y la franja de pizarra, que van arriba y en el flujo —sin
   restarlas el hero mide una pantalla completa pero empieza 100 px abajo, y su
   último renglón queda cortado justo al abrir, que es el único momento que esta
   sección tiene que resolver bien.

   `--pub-cabeza` lo mide `publico.js` (las dos barras cambian de alto: la
   franja se va cuando no hay pizarra, y la navbar crece si los links envuelven).
   El valor de acá es el que vale sin JS, y es el caso normal: 70 de navbar + 36
   de franja.

   `svh` y no `vh`: en el teléfono `vh` es la pantalla con las barras del
   navegador ESCONDIDAS, así que al abrir —cuando están a la vista— el hero mide
   más que la pantalla y se come el botón, que es exactamente lo que no puede
   pasar. `svh` es la pantalla chica, la del primer momento. */
.hero {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 24px; align-items: stretch;
  /* El fallback va en el `var()` y NO como `--pub-cabeza` propia de `.hero`:
     declararla acá la haría ganar sobre la que `publico.js` pone en `:root`, y
     la medición real no llegaría nunca. */
  min-height: calc(100svh - var(--pub-cabeza, 106px));
  padding: 12px 24px 8px;
  max-width: 1180px; margin: 0 auto; width: 100%;
}
.hero-panel {
  background: var(--papel);
  border-radius: 26px;
  padding: 44px 40px 36px;
  display: flex; flex-direction: column; align-items: flex-start;
  /* Con el hero alto el contenido se centra contra la foto en vez de colgar
     del techo y dejar medio panel vacío abajo. */
  justify-content: center;
}
.hero-kicker {
  margin: 0 0 18px; font-size: 12.5px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--brand);
}
.hero-h1 {
  font-family: "Poppins", var(--font);
  margin: 0 0 16px; font-size: 40px; line-height: 1.12; font-weight: 600;
  letter-spacing: -.02em; color: var(--brand-ink);
}
/* La mitad que importa de la promesa, en el teal de la marca. Es un `em` y no un
   `span`: gramaticalmente ES un énfasis. */
.hero-h1 em { font-style: normal; color: var(--brand); }
.hero-sub {
  margin: 0 0 26px; font-size: 15.5px; line-height: 1.6;
  color: var(--brand-ink); opacity: .8; max-width: 46ch;
}
/* La flecha se corre al pasar el mouse: es lo que pidió Santi cuando dijo que
   el botón «tiene que invitar a apretarlo». Agrandarlo más sólo lo hace
   pesado; el movimiento hacia adelante se lee como avanzar. */
.hero-cta {
  height: 50px; padding: 0 28px; font-size: 15.5px; border-radius: 999px;
  display: inline-flex; align-items: center; gap: 10px;
}
.hero-cta svg { width: 16px; height: 16px; transition: transform .15s ease; }
@media (hover: hover) and (pointer: fine) {
  .hero-cta:hover svg { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) { .hero-cta:hover svg { transform: none; } }
.hero-nota { margin: 12px 0 0; font-size: 12.5px; color: var(--brand-ink); opacity: .6; }

/* Los granos que se operan. Al pie del panel porque contestan «¿me sirve a mí?»
   después de la promesa, no antes. */
.hero-granos {
  list-style: none; margin: 28px 0 0; padding: 22px 0 0;
  border-top: 1px solid rgba(0, 44, 54, .12);
  display: flex; flex-wrap: wrap; gap: 22px; width: 100%;
}
.hero-granos li {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 500; color: var(--brand-ink);
}
/* Los cuatro íconos vienen del kit de marca y NO son cuadrados —el maíz es
   276×412, la soja 368×463—, así que se fija el alto y el ancho sale solo. Con
   `width` y `height` iguales quedaban achatados, que en un dibujo de línea se
   nota como un error de imprenta. */
.hero-granos img { display: block; height: 24px; width: auto; opacity: .85; }

/* Las tres fotos del hero: el trigo ancho arriba, maíz y soja abajo. Una pieza
   que manda y dos que acompañan —no tres iguales—, que es lo que hace que se
   lea como una composición y no como una grilla de stock.

   **Por filas y no por columnas, y el motivo es el recorte.** Con el trigo
   alto a la izquierda, las otras dos quedaban en cajas verticales (236×381) y
   las tres fotos son apaisadas: `cover` recortaba una tira de ~370 px de los
   1400 del original y la estiraba a 472 CSS px, o sea la agrandaba dos veces y
   media. Maíz quedaba en cuatro granos gigantes y borrosos. Así las dos chicas
   son casi cuadradas, el recorte usa 840 px de ancho del archivo en vez de
   370, y la imagen se ACHICA para entrar -que es lo que la deja nítida-.

   `min-height` y no alto fijo: el hero ya mide una pantalla y la columna se
   estira sola con él (`align-items: stretch`). Los 420 px son el piso para el
   monitor bajito, donde si no las dos de abajo quedan en nada. */
.hero-fotos {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 2fr 1fr;
  gap: 12px;
  min-height: 420px;
}
/* El trigo, las dos columnas. Es el único de los tres con horizonte —una foto
   de campo y no un primer plano de granos sueltos—, y leído chico no se
   entiende; maíz y soja son texturas y aguantan el recorte. */
.hero-foto-grande { grid-column: 1 / -1; }
.hero-foto { border-radius: 20px; overflow: hidden; min-height: 0; }
/* La grande lleva el radio del panel de al lado (26). Las chicas miden la mitad
   de alto, y con 26 px el redondeo se les come la esquina. */
.hero-foto-grande { border-radius: 26px; }
.hero-foto img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── Las secciones de abajo ──────────────────────────────────────────────── */
/* Las secciones de la landing comparten el ancho del hero (1180 menos su
   padding). `.pub-ancho` mide 940 porque es el ancho de LECTURA de la pantalla de
   resultados —una lista de ofertas—, y ahí está bien; acá 940 debajo de un hero de
   1180 se ve como una columna que no cierra con la de arriba. */
.pub-shell-ancha { padding-top: 56px; }
.pub-shell-ancha .pub-ancho { max-width: 1132px; }
.seccion { margin-bottom: 64px; }
.seccion-h2 {
  font-family: "Poppins", var(--font);
  margin: 0 0 24px; font-size: 24px; font-weight: 600;
  letter-spacing: -.015em; color: var(--brand-ink);
}

/* Los cinco pasos del recorrido real (AGR-433), con ícono en vez del número.
   Cinco en una fila de 1180 px son celdas de ~212: el `minmax(0, 1fr)` es lo
   que impide que una tarjeta con un título largo estire a las otras cuatro.
   Abajo de 1000 pasan a tres y a dos: cinco columnas en una tablet son cinco
   tiras de texto vertical. */
.pasos {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 14px;
}
@media (max-width: 1000px) { .pasos { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.pasos li {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 20px 18px;
}
/* El ícono reemplaza al número (`.paso-n`): la lista es `<ol>`, así que el
   orden ya lo dice el marcado y un «3» en un cuadrito era decir lo mismo dos
   veces. El dibujo, en cambio, dice de qué se trata el paso de un vistazo. */
.paso-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--brand-soft); border: 1px solid var(--brand-line);
  color: var(--brand);
}
/* Los SVG de `_icons.html` traen `fill="none"` y NINGÚN `stroke`: el color lo
   pone el contenedor, igual que en `.pub-grano-ico`. Sin esta línea los cinco
   pasos son cinco cuadraditos vacíos. */
.paso-ico svg { width: 17px; height: 17px; stroke: currentColor; }
.pasos h3 {
  font-family: "Poppins", var(--font);
  margin: 13px 0 5px; font-size: 15px; font-weight: 600;
  color: var(--brand-ink); line-height: 1.3;
}
.pasos p { margin: 0; font-size: 13px; line-height: 1.55; color: var(--text-2); }

/* «Información a tener en cuenta» (AGR-433). Dos avisos en dos columnas, con
   el ícono al costado y no arriba: son párrafos para leer, no tarjetas para
   escanear como los pasos, y el ícono al lado deja el texto arrancando todo a
   la misma altura. Sin caja: son advertencias, y encajonarlas las convierte en
   dos alertas amarillas que nadie lee. */
.info-lista {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px;
}
@media (max-width: 800px) { .info-lista { grid-template-columns: 1fr; gap: 22px; } }
.info-lista li { display: flex; gap: 14px; align-items: flex-start; }
.info-ico {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand);
}
.info-ico svg { width: 17px; height: 17px; stroke: currentColor; }
.info-lista h3 {
  font-family: "Poppins", var(--font);
  margin: 5px 0 6px; font-size: 15px; font-weight: 600; color: var(--brand-ink);
}
.info-lista p { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--text-2); }

.proposito {
  background: var(--brand-ink); color: #fff;
  border-radius: 26px; padding: 44px 40px;
  text-align: center;
}
.proposito-k {
  margin: 0 0 12px; font-size: 11.5px; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; color: var(--menta);
}
.proposito-txt {
  font-family: "Poppins", var(--font);
  margin: 0 auto 36px; font-size: 26px; line-height: 1.3; font-weight: 500;
  max-width: 22ch; letter-spacing: -.015em;
}
.numeros {
  list-style: none; margin: 0; padding: 0;
  display: flex; justify-content: center; gap: 56px; flex-wrap: wrap;
}
.numeros li { display: flex; flex-direction: column; gap: 2px; }
.numeros strong {
  font-family: "Poppins", var(--font);
  font-size: 30px; font-weight: 600; color: var(--menta);
}
.numeros span { font-size: 12.5px; opacity: .75; }

.cierre {
  text-align: center; padding: 8px 0 8px;
}
.cierre h2 {
  font-family: "Poppins", var(--font);
  margin: 0 0 8px; font-size: 24px; font-weight: 600; color: var(--brand-ink);
  letter-spacing: -.015em;
}
.cierre p { margin: 0 0 22px; font-size: 14.5px; color: var(--text-2); }
.cierre .btn { height: 44px; padding: 0 26px; font-size: 14.5px; border-radius: 999px; }

/* ── El pie ──────────────────────────────────────────────────────────────── */
.pub-pie-ancho {
  border-top: 1px solid var(--border); background: var(--bg);
  padding: 40px 24px; text-align: left;
}
.pie-cols {
  max-width: 1000px; margin: 0 auto;
  display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 32px;
}
.pie-logo { height: 26px; width: auto; display: block; }
.pie-nota { margin: 12px 0 0; font-size: 12.5px; color: var(--text-3); max-width: 30ch; }
.pie-titulo {
  font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-3); margin-bottom: 10px;
}
.pie-cols a, .pie-cols span:not(.pie-titulo) {
  display: block; font-size: 13px; color: var(--text-2);
  text-decoration: none; margin-bottom: 6px;
}
.pie-cols a:hover { color: var(--brand); }

@media (max-width: 900px) {
  /* Las fotos se van en el teléfono, y ahora con más razón: eran 162 KB y son
     329 entre las tres. Descargar eso para decorar arriba de la promesa es
     justo lo que hace que una landing tarde en abrir en el campo, con una barra
     de señal. El panel de papel ya tiene la identidad. */
  .hero { grid-template-columns: 1fr; padding: 8px 14px; }
  .hero-fotos { display: none; }
  .hero-panel { padding: 32px 24px 28px; border-radius: 20px; }
  .hero-h1 { font-size: 30px; }
  .hero-sub { font-size: 14.5px; }
  .hero-cta { width: 100%; justify-content: center; }
  .hero-granos { gap: 16px; margin-top: 22px; padding-top: 18px; }

  .pub-shell-ancha { padding-top: 36px; }
  .seccion { margin-bottom: 44px; }
  .seccion-h2, .cierre h2 { font-size: 20px; }
  /* Dos y no una (AGR-433): con cinco pasos, una columna son cinco tarjetas
     altas que empujan todo lo de abajo media pantalla más lejos. */
  .pasos { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .pasos li { padding: 18px 16px; }
  .proposito { padding: 32px 22px; border-radius: 20px; }
  .proposito-txt { font-size: 21px; margin-bottom: 28px; }
  .numeros { gap: 24px; justify-content: space-between; }
  .numeros strong { font-size: 24px; }
  .pie-cols { grid-template-columns: 1fr 1fr; gap: 24px; }
}

/* ── El formulario público ───────────────────────────────────────────────────
   Los controles de la app miden 32px de alto y están afinados para un mouse
   sobre una grilla densa. Acá los mismos controles se llenan con el pulgar, una
   sola vez en la vida, así que todo crece: 46px de alto, 15px de tipografía y el
   label como una pregunta y no como una etiqueta de columna. */
.pub-label {
  display: block; margin-bottom: 8px;
  font-size: 14.5px; font-weight: 500; color: var(--brand-ink);
}
.pub-card .field { margin-bottom: 20px; }
.pub-card input[type="text"],
.pub-card input[type="email"],
.pub-card input[type="number"],
.pub-card input[type="search"],
.pub-card select {
  height: 46px; font-size: 15px; border-radius: 10px;
  padding-left: 13px; padding-right: 13px;
}
.pub-card .hint { font-size: 12.5px; margin-top: 6px; }

/* El picker de ubicación, agrandado a la misma escala. No se cambia su template:
   el control es el mismo que usa la app y acá sólo se lo escala, así que el día
   que cambie el buscador cambia en los dos lados. */
.pub .ubi-input { height: 46px; font-size: 15px; border-radius: 10px; }
.pub .ubi-elegida { min-height: 46px; border-radius: 10px; padding: 8px 12px; }
.pub .ubi-nombre { font-size: 15px; }
.pub .ubi-res { border-radius: 12px; }
.pub .ubi-res .ubi-op { padding: 11px 12px; font-size: 14.5px; }

/* Las tarjetas de grano. Reemplazan al `<select>`: cinco opciones caben a la
   vista y en el teléfono no abren la rueda nativa del sistema. */
.granos {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
}
.granos .grano { cursor: pointer; }
/* El gluten del cotizador, sólo con un grano que se compra por gluten
   (AGR-453): el radio de trigo lleva `data-gluten`. */
.campo-gluten { display: none; }
form:has(.granos input[data-gluten]:checked) .campo-gluten,
.campo-gluten.is-fijo { display: block; }
/* El Grado Cámara del cotizador, sólo con un grano con grado (29/9): el
   radio de maíz lleva `data-grado`. */
.campo-grado { display: none; }
form:has(.granos input[data-grado]:checked) .campo-grado,
.campo-grado.is-fijo { display: block; }
/* El input se esconde pero sigue existiendo para el teclado y para los lectores
   de pantalla, y el `:checked` del CSS sigue funcionando.

   Y se esconde con `clip-path`, no con `width: 0; height: 0`: un elemento de
   tamaño cero lo saltean varios lectores de pantalla y algunos navegadores lo
   dejan afuera del árbol de accesibilidad —verificado acá: con tamaño cero los
   cinco radios no aparecían en el árbol—, así que el grupo quedaba invisible
   justo para quien más lo necesita. Éste es el idioma estándar de «oculto a la
   vista, presente para todo lo demás». */
.grano input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.grano-caja {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 14px 4px 11px;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--bg);
  transition: border-color .12s, background .12s;
}
@media (hover: hover) and (pointer: fine) {
  .grano-caja:hover { border-color: var(--border-strong); background: var(--bg-sunken); }
}
/* El disco de fondo del ícono es `.pub-grano-ico` (AGR-341, definido más abajo
   con `--pub-grano-fondo`): soja y trigo son casi blancos y se perdían acá con
   apenas un `opacity: .8` sobre el papel (mismo defecto, punto 6 de AGR-363).
   `.grano-ico` sólo pone el tamaño de ESTE selector; el círculo, el fondo y el
   filtro a blanco del dibujo salen de `.pub-grano-ico`, igual que en el
   catálogo. */
.grano-ico { width: 40px; height: 40px; opacity: .82; }
.grano-ico img { height: 22px; }
.grano-ico svg { width: 20px; height: 20px; }
.grano-nombre { font-size: 12.5px; font-weight: 500; color: var(--text-2); }

/* Elegido: papel de fondo y el anillo teal. El ícono NO cambia de color —son
   PNG de una tinta— así que la selección la marcan el fondo y el borde, que es
   lo que se ve de reojo igual. */
.grano input:checked + .grano-caja {
  background: var(--brand-soft);
  border-color: var(--brand);
  box-shadow: inset 0 0 0 1px var(--brand);
}
.grano input:checked + .grano-caja .grano-ico { opacity: 1; }
.grano input:checked + .grano-caja .grano-nombre { color: var(--brand); font-weight: 600; }
.grano input:focus-visible + .grano-caja { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ── El muro: el fondo fantasma y la tarjeta encima ──────────────────────────
   El fondo es MÁS ANCHO que la tarjeta —920 contra 460— y eso es todo el truco:
   con los dos del mismo ancho la tarjeta lo tapa entero y no se ve nada de lo que
   hay atrás, que era justamente el punto. Asomando por los costados se lee lo que
   tiene que leerse: la respuesta está, y está a un formulario de distancia. */
.muro { position: relative; width: 100%; max-width: 920px; }
.muro-fondo {
  position: absolute; inset: 0;
  /* Centradas verticalmente: con cuatro filas y una tarjeta más alta, arriba
     quedaban pegadas al borde y abajo quedaba un vacío que se leía como que la
     lista se cortó. Centradas, el fondo enmarca la tarjeta. */
  display: flex; flex-direction: column; justify-content: center; gap: 10px;
  /* El blur va sobre un ESQUELETO, no sobre datos: los precios y los nombres no
     llegan al HTML de esta pantalla (ver `publico/views.py:_teaser`). Es lo que
     hace que el muro no se pueda sacar con clic derecho. */
  filter: blur(3px); opacity: .5;
  pointer-events: none; user-select: none;
}
.fantasma {
  display: flex; align-items: center; gap: 12px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 12px; padding: 16px 18px;
}
.fantasma-top { border-color: var(--brand-line); background: var(--brand-soft); }
.fantasma-logo { flex: none; width: 26px; height: 26px; border-radius: 7px; background: #d8d8d5; }
.fantasma-nombre { height: 11px; border-radius: 3px; background: #dededb; }
.fantasma-km { font-size: 12px; color: var(--text-3); margin-left: auto; }
.fantasma-precio { flex: none; width: 82px; height: 15px; border-radius: 3px; background: #cfcfcb; }

/* `z-index` porque el fondo va DESPUÉS en el DOM —lo necesita el teléfono, donde
   fluye debajo de la tarjeta— y sin esto, en el escritorio, un absoluto posterior
   se dibuja encima: el muro quedaría borroso y la respuesta nítida atrás. */
.muro-card { position: relative; z-index: 1; margin: 0 auto; }
.muro-cifra { display: flex; align-items: baseline; gap: 10px; margin-bottom: 14px; }
.muro-n {
  font-family: "Poppins", var(--font);
  font-size: 40px; font-weight: 600; line-height: 1; color: var(--brand);
}
.muro-n-txt { font-size: 13.5px; color: var(--text-2); line-height: 1.4; }
.muro-h1 { font-size: 19px; }

/* ── La pantalla de «buscando» ───────────────────────────────────────────────
   Entra con `animation-delay`: si el server contesta antes de los 200 ms, nadie
   la ve. No hay temporizador que sostenga la espera — la espera se muestra, no
   se fabrica. */
.cargando {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: rgba(247, 247, 246, .92);
  opacity: 0;
  animation: cargando-entra .25s var(--ease-out) 200ms forwards;
}
@keyframes cargando-entra { to { opacity: 1; } }
.cargando-caja { display: flex; align-items: center; gap: 16px; padding: 0 24px; }
.cargando-anillo {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  border: 2.5px solid var(--brand-line); border-top-color: var(--brand);
  animation: cargando-gira .7s linear infinite;
}
@keyframes cargando-gira { to { transform: rotate(360deg); } }
.cargando-txt { display: flex; flex-direction: column; gap: 3px; }
.cargando-txt strong { font-size: 15px; color: var(--brand-ink); }
.cargando-txt span { font-size: 13px; color: var(--text-2); }

/* Quien pidió menos movimiento no ve girar nada: el anillo queda quieto y el
   panel aparece de una, sin fundido. Pero aparece recién a los 200 ms, como
   para todos: sin la demora, una respuesta rápida le prendía y apagaba el
   panel en la cara, que es justo lo que esa preferencia pide evitar. */
@media (prefers-reduced-motion: reduce) {
  .cargando { animation: cargando-entra 1ms linear 200ms forwards; }
  .cargando-anillo { animation: none; }
}

@media (max-width: 720px) {
  /* En el teléfono la tarjeta ocupa el ancho entero, así que un fondo detrás no
     se ve: pasa a estar DEBAJO, en el flujo normal. Se lee igual de bien —«hay
     más acá abajo y está tapado»— y no se pierde el efecto por el que existe. */
  .muro-fondo {
    position: static; inset: auto;
    margin-top: 14px;
    filter: blur(3px); opacity: .45;
  }
}

@media (max-width: 640px) {
  /* Cinco tarjetas de grano en 375px dan 60px cada una: entra el dibujo pero el
     nombre se corta. Tres por fila y el pulgar no falla. */
  .granos { grid-template-columns: repeat(3, 1fr); gap: 7px; }
  .grano-caja { padding: 12px 4px 9px; }
  .grano-ico { width: 32px; height: 32px; }
  .grano-ico img { height: 17px; }
  .grano-ico svg { width: 16px; height: 16px; }
  .muro-n { font-size: 34px; }
}

/* ── Las ofertas: una marcación, dos formas ──────────────────────────────────
   Cada oferta es un `<li>` en grilla. En el escritorio las columnas son de ancho
   FIJO —no `auto`— y eso es lo que hace que los números queden alineados entre
   filas, que es lo único que una tabla hacía mejor. En el teléfono la misma
   grilla pasa a una columna y el `<li>` se lee como una tarjeta. */
.res-head { margin-bottom: 20px; }
/* Seis columnas piden más que los 940 de lectura: con el ancho de la landing la
   columna del comprador se comía el logo y el nombre caía en cuatro renglones.
   Sigue siendo una lista y no una tabla —la fila es alta y respirada—, sólo que
   ahora la cuenta entera entra de un vistazo. */
.pub-ancho-resultados { max-width: 1000px; }
.ofertas { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
/* El orden es el de la cuenta, de izquierda a derecha: quién compra → de qué
   precio sale → qué le corre encima → cuánto se lleva Agrospot → qué cuesta el
   flete → cuánto queda, con el plazo y la humedad de recepción adentro de esa
   última columna (AGR-427: antes el plazo era una columna aparte y la comisión
   no se mostraba). Las columnas del medio son de ancho FIJO, que es lo que
   hace que los números queden alineados entre filas: la única cosa que una
   tabla hacía mejor. */
.oferta {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 104px 116px 108px 92px 216px;
  align-items: center; gap: 14px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: 14px; padding: 18px 20px;
}
/* La mejor lleva el papel de la marca y el borde teal. No lleva la cifra más
   grande: si cada fila se dibuja distinto, comparar deja de ser posible. */
.oferta-mejor { background: var(--brand-soft); border-color: var(--brand-line); }
.oferta-cinta {
  position: absolute; top: -9px; left: 18px;
  background: var(--brand); color: var(--text-inv);
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px;
}
/* La referencia no es una oferta: gris, sin acciones y al final. */
.oferta-ref { background: var(--bg-sunken); }
.oferta-ref .of-cifra { color: var(--text-2); }

.of-emp { display: flex; align-items: center; gap: 12px; min-width: 0; }
.of-emp-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* Envuelve en dos renglones, no recorta con puntos suspensivos: «Cerealera Río
   Ceb…» esconde justo el dato por el que alguien abrió el link. Una fila un poco
   más alta es un precio barato al lado de eso. */
.of-nombre {
  font-size: 15px; font-weight: 600; color: var(--brand-ink);
  line-height: 1.3;
}
.of-donde { font-size: 12.5px; color: var(--text-2); }

.of-dato { display: flex; flex-direction: column; gap: 3px; }
/* Las etiquetas van enteras: «Descuento contra flete» abreviado a «Desc. flete»
   ahorra doce píxeles y le saca el sentido —contra QUÉ es el descuento es la
   mitad del dato—. Como alguna siempre va a caer en dos renglones, se le reserva
   la altura de dos a todas: así los valores arrancan a la misma altura en las
   seis columnas y siguen alineados entre filas. */
.of-k {
  font-size: 10.5px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
  line-height: 1.25; min-height: 26px;
}
/* Los dos `line-height` son explícitos y no heredados: la altura reservada abajo
   se calcula sobre ellos, y con `normal` la cuenta depende de la métrica de la
   fuente que haya cargado —con Inter todavía sin bajar, el número y el subrenglón
   miden otra cosa y la reserva queda corta. */
.of-v { font-size: 13.5px; line-height: 1.4; color: var(--text); font-variant-numeric: tabular-nums; }
/* Y el mismo truco abajo: el flete lleva un subrenglón con los km y ninguna otra
   columna lo lleva, así que sin reservarlo su bloque quedaba más alto y —al ir
   todos centrados— su número subía cinco píxeles respecto del de al lado. Con el
   renglón reservado en las cuatro, las cuatro miden igual y quedan alineadas. */
.of-dato .of-v { min-height: 34px; }
.of-v small { display: block; font-size: 11.5px; line-height: 1.3; color: var(--text-3); }

.of-neto { text-align: right; display: flex; flex-direction: column; gap: 2px; }
.of-neto .of-k { min-height: 0; margin-bottom: 1px; }
.of-cifra {
  font-family: "Poppins", var(--font);
  font-size: 21px; font-weight: 600; color: var(--brand-ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.of-cifra small { font-size: 12px; font-weight: 500; color: var(--text-3); }
/* Dos renglones y no uno: «+3.000 vs Rosario» obligaba a elegir entre los pesos
   y el porcentaje, y son dos preguntas distintas —con los pesos se decide esta
   venta, con el porcentaje se la compara contra la de la semana pasada—. La
   etiqueta va apagada y el número con el color del signo, así el ojo baja por la
   columna de números sin tropezarse con el texto. */
.of-dif {
  display: flex; justify-content: flex-end; gap: 10px;
  font-size: 12.5px; font-weight: 500; font-variant-numeric: tabular-nums;
}
.of-dif-k { font-weight: 400; color: var(--text-3); }
.of-total { font-size: 11.5px; color: var(--text-3); margin-top: 3px; }
/* AGR-427: plazo y humedad, adentro de `.of-neto` y no como columnas propias
   —son dato de cierre, no parte de la resta que llega al neto—. Lado a lado
   y a la derecha, como el resto de la columna. */
/* Una grilla de dos filas —etiquetas arriba, valores abajo— que cada dato
   comparte con `subgrid` (QA de AGR-453): con el gluten en el medio, una
   etiqueta de un renglón («Gluten») entre dos de dos renglones dejaba cada
   valor a otra altura. Las etiquetas se apoyan abajo, contra su valor. */
.of-neto-pie {
  display: grid; grid-auto-flow: column; grid-template-rows: auto auto;
  justify-content: end; column-gap: 14px;
  margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border);
}
.of-neto-pie .of-dato {
  display: grid; grid-row: span 2; grid-template-rows: subgrid; row-gap: 3px;
  text-align: right;
}
.of-neto-pie .of-k { align-self: end; }
/* La regla de gluten de un comprador con escalera (QA de AGR-453, variante
   A): una banda del ancho de la oferta, debajo de la cuenta, con un tramo por
   escalón —rango, precio en planta y neto— y el de la cuenta marcado. En la
   tarjeta del teléfono los tramos pasan a dos por dos. */
.of-regla {
  grid-column: 1 / -1; display: flex; flex-direction: column; gap: 8px;
  border-top: 1px solid var(--border); padding-top: 12px;
}
.of-regla-cab { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.of-regla-cab .of-k { min-height: 0; }
.of-regla-nota { font-size: 12.5px; color: var(--text-3); }
.of-regla-tramos {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
}
.of-regla-tramos li {
  position: relative; display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px; font-variant-numeric: tabular-nums;
  box-shadow: -1px 0 0 var(--border);
}
.of-regla-tramos li.is-marca { background: rgba(19, 97, 91, .07); box-shadow: inset 0 0 0 1.5px var(--brand); }
.of-regla-rango {
  font-size: 11px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--text-3);
}
.of-regla-precio { font-size: 15px; font-weight: 600; color: var(--text); }
.of-regla-neto { font-size: 12px; color: var(--text-2); }
.of-regla-tag { position: absolute; top: 8px; right: 10px; font-size: 10.5px; font-weight: 600; color: var(--brand); }
.of-cifra .of-desde { font-size: 13px; font-weight: 500; color: var(--text-2); }
.res-nota { text-align: left; margin-top: 14px; }
/* El aviso de simulación, una sola vez arriba de la lista (AGR-427, AC4):
   tono neutro —no es un error ni una advertencia, es la aclaración de que el
   número todavía no está firmado. */
.aviso-simulacion { background: var(--bg-sunken); color: var(--text-2); }

/* El corte es 1000 y no 720 como el resto de la pública, y lo fija la aritmética
   de la fila: las seis columnas de números suman 680px con sus separaciones, y
   abajo de ~980 lo que sobra para el comprador baja de 215px —el logo más el
   nombre más largo— y «Agro Sarmiento SA» se parte en tres renglones. Antes de
   llegar ahí conviene pasar a la tarjeta, que en 900px se lee mejor que una fila
   apretada. */
@media (max-width: 1000px) {
  /* La tarjeta. Se lee en el mismo orden que la fila del escritorio y que el
     HTML (AGR-427): quién compra, la cuenta en dos columnas de a dos —pizarra,
     descuento, comisión, flete—, el neto que sale de esa cuenta y, al final,
     las acciones. Nada se reordena con `order`: la grilla pasa a dos y los
     cuatro datos caen solos en 2×2. */
  .oferta {
    grid-template-columns: 1fr 1fr;
    gap: 14px 12px; padding: 18px 16px;
  }
  .of-emp { grid-column: 1 / -1; }
  .of-neto {
    grid-column: 1 / -1; text-align: left;
    /* La raya va arriba: separa la cuenta del resultado de la cuenta. */
    padding-top: 14px; border-top: 1px solid var(--border);
  }
  .oferta-mejor .of-neto { border-top-color: var(--brand-line); }
  .of-cifra { font-size: 26px; }
  /* Alineadas a la izquierda como el resto de la tarjeta, pero la etiqueta y el
     número separados a las dos puntas: en 375px el par pegado se lee como una
     sola frase rota. El tope de ancho es para el otro extremo del rango — en una
     tarjeta de 900px, «Dif/tn vs Rosario» y su número quedaban en dos puntas
     distintas de la pantalla y dejaban de leerse como un par. */
  .of-neto .of-dif { justify-content: space-between; max-width: 340px; }
  /* Plazo y humedad, uno al lado del otro y alineados a la izquierda como el
     resto de la tarjeta: acá no hay columna a la que quedarle pegados a la
     derecha. */
  .of-neto-pie { justify-content: flex-start; max-width: 340px; }
  .of-neto-pie .of-dato { text-align: left; }
  /* Acá cada etiqueta tiene el ancho de media tarjeta y ninguna envuelve, así
     que la altura reservada para dos renglones es aire. Vuelve más abajo, en el
     único ancho donde hace falta. */
  .of-k { min-height: 0; }
  /* En la fila Rosario la casilla del descuento va vacía (ahí la pizarra ES
     el precio), y en una corrida anterior al desglose también la comisión y
     el flete: en la tarjeta no se dibujan huecos. */
  .oferta-ref .of-dato:empty { display: none; }
}

/* «Descuento contra flete» necesita 151px en un renglón, y media tarjeta mide
   `(ancho − 84) / 2`: abajo de ~390px no entra y envuelve. Ahí —y sólo ahí— hay
   que volver a reservarle los dos renglones a las cuatro etiquetas, o el valor
   de esa columna baja un renglón respecto del de la de al lado. */
@media (max-width: 400px) {
  .of-k { min-height: 26px; }
  .of-neto .of-k { min-height: 0; }
}

/* ── La sección del neto: la cuenta dicha como cuenta ────────────────────── */
.neto { display: grid; grid-template-columns: 1.15fr .85fr; gap: 40px; align-items: center; }
.neto-txt p { margin: 0 0 14px; font-size: 15px; line-height: 1.65; color: var(--text-2); }
.neto-txt p:last-child { margin-bottom: 0; }
.neto-txt strong { color: var(--brand-ink); }

.formula {
  background: var(--papel); border-radius: 20px; padding: 26px 24px;
  display: flex; flex-direction: column; align-items: stretch; gap: 6px;
}
.formula-fila {
  background: #fff; border-radius: 10px; padding: 13px 16px;
  font-size: 13.5px; color: var(--brand-ink);
}
.formula-op, .formula-igual {
  text-align: center; font-size: 15px; font-weight: 600; color: var(--brand);
  line-height: 1; padding: 1px 0;
}
.formula-igual { font-size: 17px; padding: 4px 0 2px; }
.formula-neto {
  background: var(--brand); color: var(--text-inv); font-weight: 600;
}

/* ── Qué operamos ────────────────────────────────────────────────────────── */
.granos-tira {
  list-style: none; margin: 0 0 16px; padding: 0;
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px;
}
.granos-tira li {
  background: var(--bg); border: 1px solid var(--border); border-radius: 14px;
  padding: 22px 10px 16px;
  display: flex; flex-direction: column; align-items: center; gap: 12px;
}
/* Sin `opacity`: los dibujos son line art de trazo fino (0,7 px a este tamaño)
   en `--brand-ink`, y bajarles la opacidad los deja del gris del fondo. Lo que
   los hacía invisibles era el color con el que estaban GUARDADOS —soja y trigo
   venían en `#e6ede3`, sorgo en `#01635b`, contra el `#002c36` del maíz—, así
   que los cuatro PNG se repintaron a la tinta del maíz y acá no queda nada que
   los apague. */
.granos-tira img { display: block; height: 40px; width: auto; }
/* El genérico de `_icons.html` viene con `fill="none"` y sin trazo: sin
   `stroke` la espiga de Girasol queda en blanco en la tira de la landing. */
.grano-ico-svg { color: var(--brand-ink); }
.grano-ico-svg svg { stroke: currentColor; }
.grano-ico-tira { width: 36px; height: 40px; }
/* El `stroke-width` del SVG se baja de 1,5 a 0,9: el ícono se dibuja a 40px de
   alto desde un `viewBox` de 24, o sea escalado 1,67×, así que 1,5 rendereaba
   2,5px de trazo al lado de los 0,7px de los PNG. Girasol se leía como de otro
   set. A 0,9 son 1,5px: sigue siendo el más grueso —un SVG hairline se
   desvanece donde un PNG con antialias aguanta—, pero ya es la misma familia. */
.grano-ico-tira svg { width: 36px; height: 40px; stroke-width: .9; }
.granos-tira span { font-size: 13.5px; font-weight: 500; color: var(--brand-ink); }
.granos-nota { margin: 0; font-size: 13.5px; color: var(--text-2); }

.cierre-otros { margin: 22px 0 0; font-size: 13.5px; color: var(--text-2); }
.cierre-otros a { color: var(--brand); font-weight: 500; }

@media (max-width: 900px) {
  .neto { grid-template-columns: 1fr; gap: 26px; }
  .granos-tira { grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .granos-tira li { padding: 16px 6px 12px; gap: 9px; }
  .granos-tira img { height: 32px; }
  .granos-tira span { font-size: 12.5px; }
  .formula { padding: 20px 16px; }
}

/* === AGR-401 === */
/* La franja de pizarra y dólar del día, debajo de la navbar en las tres
   pantallas públicas (publico/templatetags/pizarra_publica.py +
   publico/templates/publico/_franja_pizarra.html). Reemplaza el bloque ancho
   de AGR-342, que vivía sólo en la landing entre "compradores" y "el neto".

   Pensada para un renglón: en desktop todos los ítems entran en una fila: si
   no entran (celular chico) `.pub-franja-pizarra-scroll` se scrollea de costado en
   vez de partir la franja en varios renglones altos o empujar el ancho de la
   página (decisión de AGR-401).

   El "dato viejo" sigue en `--pub-tierra`, el token de la pública que declara
   la sección AGR-341. */
.pub-franja-pizarra {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--papel);
  border-bottom: 1px solid var(--border);
  padding: 7px 24px;
}

.pub-franja-pizarra-scroll {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.pub-franja-pizarra-scroll::-webkit-scrollbar { display: none; }

/* El riel: los ítems en una fila, con el gap que antes estaba en el scroll.
   Existe para poder moverlos juntos —el scroll no se puede animar sin pelearle
   al usuario— y para poder estirarlos cuando sobra lugar. */
.pub-franja-pizarra-riel {
  display: flex;
  align-items: center;
  gap: 20px;
  flex: 0 0 auto;
}

/* Sobra lugar: los ítems se reparten parejo de borde a borde en vez de
   apelmazarse a la izquierda con medio renglón vacío a la derecha. La clase la
   pone `publico.js`, que es quien puede medir si entran. */
.pub-franja-pizarra-riel.is-holgado {
  flex: 1 1 auto;
  justify-content: space-between;
  gap: 12px;
}

/* No entran: loop continuo, el mismo gesto que la pasarela de «quién está
   comprando hoy» —de derecha a izquierda, sin parar, con fade en los bordes y
   frenando con el mouse encima—. Recorre exactamente un set y vuelve a 0: no se
   nota porque detrás del set va su clon, que pone `publico.js`.

   Sin JS o con movimiento reducido no se llega acá y queda el scroll a mano. */
@keyframes pub-franja-loop {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(-1 * var(--franja-set, 0px)), 0, 0); }
}
.pub-franja-pizarra-scroll.en-loop {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
}
.pub-franja-pizarra-scroll.en-loop .pub-franja-pizarra-riel {
  will-change: transform;
  animation: pub-franja-loop var(--franja-dur, 60s) linear infinite;
}
@media (hover: hover) and (pointer: fine) {
  /* Frena para poder leer un precio. Es una tira de datos, no un adorno: si no
     se puede detener, el dato que alguien vino a buscar se le escapa. */
  .pub-franja-pizarra-scroll.en-loop:hover .pub-franja-pizarra-riel { animation-play-state: paused; }
}

.pub-franja-pizarra-item {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex: 0 0 auto;
  white-space: nowrap;
}

/* Un dato que no es de hoy, en el tono tierra del diseño (decisión 8 de
   AGR-342, que sigue vigente): nunca se oculta, sólo se distingue del precio
   fresco. */
.pub-franja-pizarra-item.pub-franja-pizarra-viejo .pf-v { color: var(--pub-tierra); }
.pub-franja-pizarra-item.pub-franja-pizarra-viejo .pf-fecha { color: var(--pub-tierra); }

.pf-k {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pf-v {
  font-family: var(--font-t, inherit);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--brand-ink);
  font-variant-numeric: tabular-nums;
}

.pf-v small { font-size: 10.5px; font-weight: 500; color: var(--text-3); }

.pf-var { font-size: 11.5px; font-weight: 600; color: var(--brand); }
.pf-var-baja { color: #a13a3a; }

.pf-fecha { font-size: 10.5px; color: var(--text-3); }

/* La cita a la Cámara Arbitral, adentro del scroll, liderando el grupo de
   granos -no el dólar, que sale del Banco Nación y lleva su propia fuente en el
   `title` de su ítem (review de AGR-401)-. El `title` de esta cita lleva la
   aclaración del issue -antes un párrafo visible-, así que el cursor de ayuda
   es la única pista de que hay algo para leer ahí. */
.pub-franja-pizarra-fuente {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  cursor: help;
}

.pub-franja-pizarra-fuente img { display: block; height: 16px; width: auto; opacity: .85; }

@media (max-width: 640px) {
  .pub-franja-pizarra { padding: 6px 16px; gap: 10px; }
  .pub-franja-pizarra-scroll { gap: 14px; }
}

/* El QR de confirmación de teléfono (perfiles/_mi_qr.html). El SVG viene sin
   ancho propio, así que el tamaño se fija acá; el fondo blanco es obligatorio y
   no estético: un QR sobre fondo oscuro no lo lee ninguna cámara. */
.qr-caja {
  display: flex;
  justify-content: center;
  padding: 16px;
  margin: 4px 0 12px;
  background: #fff;
  border: 1px solid var(--linea, #e5e5e5);
  border-radius: 8px;
}
.qr-caja svg { width: 190px; height: 190px; display: block; }
.qr-token { text-align: center; margin-bottom: 12px; }

/* === AGR-341 === */
/* El catálogo público de /compradores/ y su card de oportunidad, variante C
   «ficha compacta» del prototipo aprobado
   (publico/templates/publico/catalogo.html + _oportunidad.html).

   ── Los tokens de la pública ───────────────────────────────────────────────
   Este ticket es su primer consumidor, así que los declara; las demás
   pantallas públicas los usan desde acá. Ningún color nuevo: el tierra ya era
   `.mono-3` y el papel hondo es `--papel` un punto más apagado. Todos con
   prefijo `pub-`, como el resto de lo público, para no pisar nada de la app. */
:root {
  /* Dos tonos. `--pub-papel-hondo` es el fondo de lo que no tiene imagen
     (foto vacía, mapa); `--pub-tierra` es el dato que no es de hoy —el sello
     «precio de ayer»—, y es el único uso semántico de ese tono. No es rojo a
     propósito: un precio de ayer no es un error, es un dato con fecha. */
  --pub-papel-hondo: #dfe3da;
  --pub-tierra:      #8a4b2a;
  /* El fondo del ícono de grano (defecto del QA de AGR-341): soja y trigo
     salen casi blancos del kit de marca (RGB medio ~230,237,227) y se pierden
     sobre un chip claro o una card blanca.

     Decisión 22 de la tanda CREA: el primer disco, gris claro (#b9c2b0), daba
     1,5:1 contra ese blanco. Éste es OSCURO, y el dibujo se pinta de blanco
     encima (`.pub-grano-ico img`, abajo), como ya hacía el chip encendido:
     un disco oscuro solo se comía a maíz y sorgo, que son oscuros. Así el
     contraste es uno para los cinco granos: 7,2:1 contra el blanco y 6,1:1
     contra el color original de soja y trigo. Un verde campo apagado y no el
     teal de marca, que queda para lo que se toca. Lo fija un test
     (`IconoDeGrano` en publico/test_catalogo.py). */
  --pub-grano-fondo: #4f5a48;

  /* Dos niveles de sombra y no hay un tercero. El 1 es lo que apoya (card,
     tarjeta de formulario, pin); el 2 es sólo lo que flota (barra fija, pie de
     un overlay). */
  --pub-sombra-1: 0 1px 2px rgba(8, 9, 10, .04);
  --pub-sombra-2: 0 8px 24px rgba(0, 44, 54, .10);

  /* Los dos radios que la hoja tipeaba a mano. El 6 y el 8 siguen siendo
     `--radius` y `--radius-lg`. */
  --pub-radio-card:  10px;
  --pub-radio-panel: 26px;

  /* La escala tipográfica: cinco tamaños, con su valor de teléfono acá y el
     de pantalla ancha abajo. Poppins para cifras y títulos (es la del logo),
     Inter para el resto. El H1 del hero queda afuera: es la cifra estirada. */
  --pub-fuente-t:     "Poppins", var(--font);
  --pub-t-cifra:      27px;
  --pub-t-titulo:     20px;
  --pub-t-subtitulo:  15px;
  --pub-t-cuerpo:     13.5px;
  --pub-t-etiqueta:   10.5px;
}
@media (min-width: 1000px) {
  :root {
    --pub-t-titulo: 24px;
    --pub-t-cuerpo: 15px;
  }
}

/* ── La página ──────────────────────────────────────────────────────────────
   Gutter de 16 en el teléfono, como el marco del wireframe; ancho máximo de
   1100 para cerrar con tres columnas de card. */
.pub-catalogo { flex: 1; padding: 24px 16px 48px; }
.pub-catalogo-in {
  max-width: 1100px; margin: 0 auto;
  display: flex; flex-direction: column; gap: 14px;
  min-width: 0;
}
.pub-catalogo-cab { display: flex; flex-direction: column; gap: 4px; }
/* La barra de filtros (AGR-348): el origen y los chips en un contenedor
   propio, para que en desktop se puedan fijar juntos. Acá sólo repite el
   `gap` de la columna, así en el teléfono se ve igual que cuando eran dos
   hijos sueltos de `.pub-catalogo-in`. El `min-width: 0` deja que el carril
   de chips haga scroll en vez de ensanchar la página. Lo fijo está en el
   bloque de desktop, al final de AGR-346. */
.pub-filtros { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.pub-catalogo-h1 {
  margin: 0;
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-titulo); font-weight: 600;
  letter-spacing: -.015em; line-height: 1.25; color: var(--brand-ink);
}
.pub-catalogo-sub { margin: 0; font-size: var(--pub-t-cuerpo); line-height: 1.55; color: var(--text-2); }
.pub-catalogo-sub b { color: var(--brand-ink); font-weight: 600; }

/* ── El ícono del grano, con fondo ──────────────────────────────────────────
   Comparte esta base el chip, el subtítulo de un grupo de «Todos» y el título
   de la card: los tres envuelven el ícono (el `.png` del grano, o el genérico
   de `_icons.html` cuando el grano no tiene uno propio —hoy, Girasol) en un
   `<span class="pub-grano-ico">`, y cada contexto sólo ajusta el tamaño. */
.pub-grano-ico {
  display: inline-flex; flex: none; align-items: center; justify-content: center;
  border-radius: 999px; background: var(--pub-grano-fondo);
}
/* El dibujo, en blanco sobre el disco oscuro (decisión 22): los `.png` no se
   editan, se pintan. */
.pub-grano-ico img { display: block; width: auto; filter: brightness(0) invert(1); }
.pub-grano-ico svg { display: block; stroke: var(--text-inv); }

/* ── Los chips de grano ─────────────────────────────────────────────────────
   Un solo nivel y scroll lateral: con seis granos no entran en 375 y envolver
   en dos renglones hace parecer que hay dos filtros. El scroll es del carril y
   no de la página, así que la página nunca se corre de costado. */
.pub-chips {
  display: flex; gap: 8px;
  overflow-x: auto; overscroll-behavior-x: contain;
  margin: 0 -16px; padding: 2px 16px 4px;
  scrollbar-width: none;
}
.pub-chips::-webkit-scrollbar { display: none; }
.pub-chip-grano {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  height: 34px; padding: 0 13px; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--bg);
  font-size: 13px; font-weight: 500; color: var(--text-2);
  white-space: nowrap; text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .pub-chip-grano:hover { border-color: var(--brand-line); color: var(--brand-ink); }
}
.pub-chip-grano:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.pub-chip-grano .pub-grano-ico { width: 20px; height: 20px; }
.pub-chip-grano .pub-grano-ico img { height: 13px; }
.pub-chip-grano .pub-grano-ico svg { width: 12px; height: 12px; }
.pub-chip-grano.is-on {
  background: var(--brand); border-color: var(--brand); color: var(--text-inv);
}
/* En el chip encendido el disco se apaga: el ícono ya es blanco (decisión 22,
   arriba) y sobre el color de marca hay contraste de sobra; un segundo fondo
   se vería como una mancha detrás del ícono. */
.pub-chip-grano.is-on .pub-grano-ico { background: transparent; }
.pub-chip-grano.is-on .pub-grano-ico img { filter: brightness(0) invert(1); opacity: .95; }
.pub-chip-grano.is-on .pub-grano-ico svg { stroke: var(--text-inv); }

.pub-conteo {
  margin: 0;
  font-size: 12px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* ── La franja de invitar a cotizar (AGR-397) ──────────────────────────────
   Oscura y no de marca: no es un CTA del comprador, es del stand. `--text`
   (casi negro) y `--text-inv` (blanco), los mismos que ya arma el logo del
   layout sin sidebar (`.blank-brand .ws-logo`), y no una tercera pareja de
   colores nueva. */
.pub-franja {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 12px;
  background: var(--text); border-radius: var(--pub-radio-card);
  padding: 14px 16px;
}
.pub-franja-p {
  margin: 0; flex: 1 1 260px; min-width: 0;
  font-size: var(--pub-t-cuerpo); font-weight: 500; line-height: 1.4;
  color: var(--text-inv);
}
.pub-franja-btn { flex: none; height: 40px; padding: 0 16px; font-size: 13.5px; }

/* ── Los grupos de «Todos» y la grilla ─────────────────────────────────────
   1 / 2 / 3 columnas en los dos cortes que ya usa la hoja pública, 640 y 1000.
   `align-items: start` para que una card más alta no estire a su vecina. */
.pub-grupo { display: flex; flex-direction: column; gap: 10px; }
.pub-grupo + .pub-grupo { margin-top: 14px; }
.pub-grupo-k {
  margin: 0; padding-top: 4px;
  display: flex; align-items: center; gap: 7px;
  font-size: 11px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-2);
}
.pub-grupo-k .pub-grano-ico { width: 22px; height: 22px; }
.pub-grupo-k .pub-grano-ico img { height: 14px; }
.pub-grupo-k .pub-grano-ico svg { width: 13px; height: 13px; }
.pub-grupo-k span { color: var(--text-3); font-variant-numeric: tabular-nums; }

.pub-oportunidades {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start;
}
@media (min-width: 640px) {
  .pub-oportunidades { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .pub-oportunidades { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
}

/* ── La card ────────────────────────────────────────────────────────────────
   Calcada de la variante C. Clases propias (`pub-op-*`) y no las del
   prototipo: `.neto`, `.titulo` y `.grano-ico` ya existen en esta hoja para la
   landing y el cotizador, y heredarlas rompía las dos cosas. */
.pub-oportunidad {
  position: relative;
  min-width: 0;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--pub-radio-card); box-shadow: var(--pub-sombra-1);
  padding: 13px 14px;
  display: flex; flex-direction: column; gap: 9px;
}
@media (hover: hover) and (pointer: fine) {
  .pub-oportunidad:hover { border-color: var(--brand-line); background: var(--brand-soft); }
}
/* El link vacío estirado sobre la card entera: toda la card es zona de toque,
   y el anillo de foco rodea la card y no un pedazo de texto. */
.pub-op-link { position: absolute; inset: 0; border-radius: var(--pub-radio-card); }
.pub-op-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.pub-op-cabeza { display: flex; align-items: center; gap: 9px; min-width: 0; }
/* El bloque de marca: el monograma de `_empresa_avatar.html` al tamaño de la
   card. Sin foto es el estado normal, no el de error. */
.pub-op-marca { width: 34px; height: 34px; border-radius: var(--radius-lg); font-size: 13px; }
.pub-op-quien { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pub-op-titulo {
  margin: 0;
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-subtitulo); font-weight: 600;
  line-height: 1.25; letter-spacing: -.01em; color: var(--text);
  text-wrap: balance;
}
.pub-op-grano {
  width: 18px; height: 18px;
  vertical-align: -5px; margin-right: 6px;
}
.pub-op-grano img { height: 13px; }
.pub-op-grano svg { width: 12px; height: 12px; }
.pub-op-empresa {
  font-size: 12px; color: var(--text-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pub-op-chev { flex: none; width: 16px; height: 16px; margin-left: auto; color: var(--text-3); }

/* El único ganador visual: la cifra. Nada más en la card pasa de 15px. */
.pub-op-precio { display: flex; flex-direction: column; gap: 1px; }
/* Pizarra y contraflete (AGR-386): dos líneas chicas arriba del rótulo, sin
   competirle a la cifra ni al rótulo mayúscula de `.pub-op-condicion`. */
.pub-op-pizarra {
  font-size: 11px; color: var(--text-3); line-height: 1.3;
}
.pub-op-condicion,
.pub-op-k {
  font-size: 10px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
}
.pub-op-cifra {
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-cifra); font-weight: 600;
  line-height: 1.12; letter-spacing: -.02em; color: var(--brand-ink);
  font-variant-numeric: tabular-nums;
}
.pub-op-cifra small {
  font-family: var(--font); font-size: 13px; font-weight: 500;
  letter-spacing: 0; color: var(--text-3);
}

.pub-op-fila {
  /* AGR-392: el cupo bajó al pie, así que la fila ya sólo lleva Plazo,
     Humedad y Distancia — hasta tres datos de texto, ninguno con el ancho
     variable que tenía el cupo (QA de AGR-387). Columnas iguales alcanzan. */
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  padding-top: 9px; border-top: 1px solid var(--border);
}
.pub-op-dato { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pub-op-v {
  font-size: 13px; color: var(--text); font-variant-numeric: tabular-nums;
  overflow: hidden; overflow-wrap: anywhere;
}
/* El cupo de la orden (AGR-387): punto lleno para «Disponible» —el mismo
   verde que ya usa la app para «en camino»— y aro tierra para «Consultar
   cupos», que es el default y no una alarma. Ya no es texto libre de la
   planta, así que no necesita el clamp de dos renglones que tenía antes. */
.pub-op-cupo {
  display: flex; align-items: center; gap: 5px;
  /* AGR-392: en la fila el cupo era `.pub-op-v pub-op-cupo` y heredaba de ahí
     los 13 px. En el pie ya no lleva `.pub-op-v`, así que sin esto se iba a
     los 14 px de `body` y quedaba como el texto más grande de la card, al
     lado de un sello de 11,5 px. Sigue midiendo lo que medía. */
  font-size: 13px;
}
.pub-op-punto {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
}
.pub-op-cupo-disponible .pub-op-punto { background: #2e7d32; }
.pub-op-cupo-consultar .pub-op-punto {
  background: none; border: 1.5px solid var(--pub-tierra);
}

.pub-op-pie { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.pub-sello { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.pub-sello-viejo { color: var(--pub-tierra); font-weight: 500; }

/* ── El estado vacío ────────────────────────────────────────────────────────
   Borde punteado sobre fondo hundido: se lee como «acá iría algo» y no como
   una card rota. */
.pub-vacio {
  border: 1px dashed var(--border-strong); border-radius: var(--radius-lg);
  background: var(--bg-sunken);
  padding: 20px 18px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 9px;
}
.pub-vacio-k { font-size: var(--pub-t-etiqueta); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--text-3); }
.pub-vacio-h {
  margin: 0;
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-subtitulo); font-weight: 600;
  line-height: 1.3; color: var(--brand-ink);
}
.pub-vacio-p { margin: 0; max-width: 60ch; font-size: var(--pub-t-cuerpo); line-height: 1.6; color: var(--text-2); }
.pub-vacio-acciones { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
.pub-vacio-acciones .btn { height: 44px; padding: 0 18px; font-size: 14px; }

/* === AGR-343 === */
/* El origen del productor en el catálogo: el campo «¿Desde dónde vendés?» y la
   nota de la card que no se pudo llevar a neto
   (publico/templates/publico/catalogo.html + _oportunidad.html).

   El campo es el picker de ubicaciones de la app (`.ubi-*`), agrandado para el
   pulgar: 46 de alto como en el diseño, y 16px de letra, que es lo que evita
   que Safari en el teléfono haga zoom al enfocar el buscador. */
.pub-origen { display: flex; flex-direction: column; gap: 6px; max-width: 420px; }
.pub-origen-k {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600; color: var(--text-2);
}
.pub-origen-k svg { width: 15px; height: 15px; stroke: var(--brand); }
.pub-origen .ubi-input,
.pub-origen .ubi-elegida {
  height: 46px; font-size: 16px;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.pub-origen .ubi-input { padding: 0 38px 0 12px; }
/* Lleno se ve como un dato, no como un campo por llenar: fondo de card y el
   pin en marca (el pin ya es `--brand` en `.ubi-pin`). */
.pub-origen .ubi-elegida { background: var(--bg); padding: 0 3px 0 12px; }
.pub-origen .ubi-prov { font-size: 13px; }
/* «Cambiar» es la única acción del campo lleno: 40 de lado, zona de toque de
   un dedo y no de un mouse. */
.pub-origen .ubi-x { width: 40px; height: 40px; border-radius: var(--radius); }
.pub-origen .ubi-x svg { width: 15px; height: 15px; }
.pub-origen .ubi-spinner { top: 16px; right: 14px; }
.pub-origen .ubi-res:not(:empty) { top: 50px; }
/* Mientras se recalcula. La primera visita con un origen nuevo rutea una vez
   por planta y puede tardar unos segundos: la lista de abajo se apaga (htmx
   pone `.htmx-request` en el formulario que pidió) para que se vea que algo
   está pasando, sin tapar lo que había. */
.pub-origen.htmx-request { cursor: progress; }
.pub-origen.htmx-request ~ *,
/* Desde AGR-348 el form vive en `.pub-filtros` y la lista ya no es hermana
   suya: sin la segunda regla sólo se apagaban los chips. */
.pub-filtros:has(.pub-origen.htmx-request) ~ * { opacity: .5; transition: opacity 150ms var(--ease-out); pointer-events: none; }

/* Por qué una card con origen sigue en precio en planta. Gris y chica, debajo
   de la cifra: explica el número, no compite con él. No va en tierra, que es
   sólo para el dato que no es de hoy. */
.pub-op-nota {
  margin: -4px 0 0;
  font-size: 12px; line-height: 1.45; color: var(--text-2);
}

/* El origen en el mapa no tapa los precios (QA final de la tanda CREA). Con
   origen la lista va por neto, y el mejor neto es el de las plantas más cerca
   del campo: los mejores pins caen amontonados alrededor del punto del origen.
   Mapbox apila los marcadores en el orden en que se agregan (`mapa.js`), y el
   origen va último, así que su rótulo quedaba arriba de la mejor oferta; y
   entre pins, el de la card de abajo tapaba al de la card de arriba.
   - Todo pin arriba del origen, que no fija z-index.
   - El pin de la primera card arriba de los demás. El elegido lo sube
     `mapa.js` con `z-index: 2` en línea: empata, y gana por venir después.
   - La leyenda, arriba de cualquier pin: es la condición de los números. */
.pub-mapa-pin { z-index: 1; }
.pub-mapa-pin:first-of-type { z-index: 2; }
.pub-mapa-cuerpo .pub-mapa-leyenda { z-index: 3; }
.pub-mapa-cuerpo .pub-mapa-leyenda-viejo { z-index: 3; }

/* === AGR-345 === */
/* La sección «Quién está comprando hoy» de la landing: mismo `.seccion` que
   el resto de la página, cabecera con el título y el botón secundario, y la
   grilla de adelanto. La card es `.pub-oportunidad` (AGR-341) sin tocar. */
.pub-sec-cab {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 12px 16px;
  margin-bottom: 4px;
}
.pub-sec-cab .seccion-h2 { margin: 0; }
.pub-sec-nota { margin: 0 0 18px; font-size: 14px; color: var(--text-2); line-height: 1.55; }

/* El botón secundario de la cabecera: mismo `.btn` que el resto del sitio
   (ver `.pub-vacio-acciones` arriba), un poco más alto para pesar tanto como
   el título al lado. */
.pub-ver-todos { height: 38px; padding: 0 16px; font-size: 13.5px; flex: none; }

@media (max-width: 640px) {
  .pub-sec-cab { flex-direction: column; align-items: flex-start; }
  .pub-ver-todos { width: 100%; justify-content: center; }
}

/* === AGR-434 / AGR-435: las filas por provincia ===
   Reemplazan la pasarela única en loop de AGR-396/402 (una fila del grano
   líder, girando a 9 s por tarjeta, con fade en los bordes y arrastre). Ahora
   hay una fila por provincia, QUIETA, y las tarjetas son chicas: la referencia
   aprobada es la fila de Airbnb —foto cuadrada, sin caja, tres renglones de
   texto pelado debajo— y lo que la hace liviana es justamente que no hay
   borde, ni sombra, ni fondo.

   El loop y sus `@keyframes` se fueron enteros. Lo que se anima ahora es el
   `scroll-behavior: smooth` de las flechas, que es del navegador. */

.pub-fila { margin-top: 26px; }
.pub-fila:first-of-type { margin-top: 18px; }

.pub-fila-cab {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 12px;
}
/* El título ES el link, con la flecha pegada al texto: el patrón de «Stay in
   Algarrobo →». La flecha se corre 2px en hover — el único movimiento que
   queda en la sección. */
.pub-fila-h3 {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 0; text-decoration: none;
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-subtitulo);
  font-weight: 600; letter-spacing: -.01em; color: var(--brand-ink);
}
.pub-fila-h3 svg { width: 15px; height: 15px; transition: transform .15s ease; }
@media (hover: hover) and (pointer: fine) {
  .pub-fila-h3:hover svg { transform: translateX(2px); }
}
.pub-fila-h3:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 4px; }

/* Las flechas. Las muestra `publico.js` sólo cuando la fila no entra: con dos
   tarjetas no hay nada a donde ir y dos botones apagados son ruido. Por eso
   el `hidden` viene puesto en el HTML y no al revés — sin JS no aparecen, y
   la fila se scrollea a mano igual. */
.pub-fila-flechas { display: flex; gap: 8px; flex: none; }
.pub-fila-flechas[hidden] { display: none; }
.pub-fila-flecha {
  width: 30px; height: 30px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg); color: var(--brand-ink);
  border: 1px solid var(--border); border-radius: 50%;
  cursor: pointer; transition: border-color .15s ease, opacity .15s ease;
}
.pub-fila-flecha svg { width: 14px; height: 14px; }
@media (hover: hover) and (pointer: fine) {
  .pub-fila-flecha:hover { border-color: var(--brand-line); }
}
/* Al principio y al final de la fila el botón que no lleva a ningún lado se
   apaga, pero NO se esconde: si desapareciera, el otro saltaría de lugar. */
.pub-fila-flecha:disabled { opacity: .3; cursor: default; }

/* El margen negativo sale de `--pub-gutter`, el mismo que `.pub-shell` usa de
   padding: escrito a mano se desfasaba del margen real —14px en el teléfono,
   20px arriba— y de menos dejaba un escalón, de más hacía scrollear la página
   de costado. */
.pub-pasarela {
  margin-inline: calc(-1 * var(--pub-gutter, 20px));
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; scroll-behavior: smooth;
  padding: 2px var(--pub-gutter, 20px) 4px;
  /* El mismo margen que el padding: con `scroll-snap-align: start` a secas la
     tarjeta snapea contra el borde del scrollport y se come el aire lateral,
     así que a 375 px la primera queda pegada al vidrio. */
  scroll-padding-inline: var(--pub-gutter, 20px);
  scrollbar-width: none;
}
.pub-pasarela::-webkit-scrollbar { display: none; }
.pub-pasarela:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
@media (min-width: 1000px) {
  .pub-pasarela { margin-inline: 0; padding: 2px 2px 4px; scroll-padding-inline: 2px; }
}
@media (prefers-reduced-motion: reduce) {
  .pub-pasarela { scroll-behavior: auto; }
}

.pub-pasarela-riel { display: flex; gap: 14px; align-items: start; }

/* La tarjeta: foto cuadrada y tres renglones. SIN caja —ni borde, ni sombra,
   ni fondo—: en una fila de siete, siete rectángulos dibujados compiten entre
   sí y con la foto, que es lo único que tiene que mirarse.

   `align-items: start` en el riel y no `stretch`: sin caja no hay nada que
   estirar, y dejarlas estirar hacía que una tarjeta con nombre de dos
   renglones alargara a las seis de al lado. */
.pub-pasg {
  position: relative;
  flex: none; scroll-snap-align: start;
  width: 172px; min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
}
@media (min-width: 640px)  { .pub-pasg { width: 196px; } }
@media (min-width: 1000px) { .pub-pasg { width: 212px; } }

/* El marco cuadrado. El radio es el grande de la pública y no el de card:
   sin borde que lo sostenga, un radio chico se lee como una foto sin
   redondear. */
.pub-pasg-foto-marco {
  position: relative;
  aspect-ratio: 1 / 1; width: 100%;
  margin-bottom: 8px;
  background: var(--pub-papel-hondo, var(--brand-soft));
  border-radius: 14px; overflow: hidden;
}
.pub-pasg-foto { display: block; width: 100%; height: 100%; object-fit: cover; }
/* El mapa `light-v11` es casi blanco y contra el papel no se ve dónde termina
   la foto. Una línea de adentro, sólo para él. */
.pub-pasg-mapa { box-shadow: inset 0 0 0 1px var(--border); }
.pub-pasg-sinfoto {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0 14%;
  text-align: center; font-family: var(--pub-fuente-t);
  font-size: 13px; font-weight: 500; color: var(--text-3); line-height: 1.3;
}
/* El logo, abajo a la izquierda y ADENTRO de la foto (antes colgaba del borde
   con `bottom: -16px`, que con una tarjeta sin caja quedaba flotando sobre el
   título). `pointer-events: none` porque está por encima del link que cubre la
   tarjeta: sin eso es una zona muerta de 36 px. No es interactivo, no se
   pierde nada. */
.pub-pasg-marca {
  position: absolute; left: 8px; bottom: 8px; z-index: 2;
  pointer-events: none;
  width: 36px; height: 36px; border-radius: 10px;
  border: 2px solid var(--bg); font-size: 13px;
  box-shadow: var(--pub-sombra-1);
}

/* El link cubre la tarjeta entera, como en Airbnb. Va sobre la foto Y sobre el
   texto (`inset: 0` de un hijo posicionado), y por eso no hay ningún botón
   adentro: el «Conocé más» vive en la tarjeta del catálogo, que es la densa. */
.pub-pasg-link { position: absolute; inset: 0; z-index: 1; border-radius: 14px; }
.pub-pasg-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
@media (hover: hover) and (pointer: fine) {
  .pub-pasg:hover .pub-pasg-foto { transform: scale(1.03); }
  .pub-pasg-foto { transition: transform .35s ease; }
}
@media (prefers-reduced-motion: reduce) {
  .pub-pasg:hover .pub-pasg-foto { transform: none; }
}

/* Los tres renglones. Tamaños chicos y una sola jerarquía: el primero en
   `--brand-ink`, los otros dos en gris. */
/* Un renglón y elipsis: «Maíz en Colonia Caroya, Córdoba» en 172 px son dos
   líneas, y dos líneas en una tarjeta y una en la de al lado desalinean los
   precios de toda la fila. El nombre completo queda en el `aria-label` del
   link y en la ficha. */
.pub-pasg-titulo {
  margin: 0;
  font-family: var(--pub-fuente-t); font-size: 13.5px; font-weight: 600;
  letter-spacing: -.01em; color: var(--brand-ink); line-height: 1.35;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Dos renglones fijos, siempre. La condición comercial entera —«Pizarra
   Rosario del día de la descarga − $ 30.000»— no entra en un renglón de 212 px,
   y una tarjeta que parte en dos al lado de una que entra en uno deja el pie
   («30 días · Seco») a distinta altura en cada una: la fila se ve desprolija
   justo en la línea que se lee de corrido. Con el alto fijado, el pie cae
   siempre en el mismo lugar aunque arriba haya uno o dos renglones. */
.pub-pasg-precio {
  margin: 0; font-size: 13px; line-height: 1.35; color: var(--text-1);
  min-height: 2.7em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.pub-pasg-pie { margin: 0; font-size: 12.5px; color: var(--text-2); }

/* === AGR-389 === */
/* El muro «Nuestros compradores»: 2 columnas en móvil, 3 en tablet, 4 en
   desktop (issue: "grilla responsive 2/3/4 columnas"). Mismo `.emp-logo` /
   `.emp-mono` de siempre para el logo o el monograma. */
.compradores-grilla {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
}
@media (min-width: 640px) {
  .compradores-grilla { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
}
@media (min-width: 1000px) {
  .compradores-grilla { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
}
.comprador-tile {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 18px 12px; text-align: center; text-decoration: none;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-lg);
  transition: border-color .15s ease;
}
.comprador-tile:hover { border-color: var(--brand-line); }
.comprador-tile-avatar { width: 48px; height: 48px; font-size: 16px; }
.comprador-tile-nombre { font-size: 13.5px; font-weight: 600; color: var(--brand-ink); line-height: 1.3; }
.comprador-tile-donde { font-size: 12px; color: var(--text-2); }
.comprador-tile-estado {
  display: flex; align-items: center; gap: 5px; margin-top: 2px;
  font-size: 11.5px; color: var(--text-3);
}
.comprador-tile-punto {
  width: 7px; height: 7px; border-radius: 50%; background: #2e7d32; flex-shrink: 0;
}
/* Inactiva: gris, logo desaturado —"Sin orden hoy" ya lo dice el texto. */
.comprador-tile-inactivo .comprador-tile-avatar,
.comprador-tile-inactivo .emp-logo img { filter: grayscale(1); opacity: .55; }
.comprador-tile-inactivo .comprador-tile-nombre { color: var(--text-2); }

/* === AGR-344 === */
/* El perfil público del comprador, /compradores/<slug>/
   (publico/templates/publico/perfil*.html). Usa los tokens `pub-` de AGR-341
   y no declara ninguno: la card es la del catálogo tal cual.

   Teléfono: una columna en el orden del issue y la primaria fija abajo, con
   el WhatsApp como texto gris al lado —el teal es de la primaria y de nadie
   más—. Pantalla ancha: grilla 1fr / 340 y «Cómo paga» con las acciones en la
   columna del costado, sticky; ahí no hay barra fija. */
.pub-perfil { flex: 1; padding: 24px 16px 112px; }
.pub-perfil-in {
  max-width: 1100px; margin: 0 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 24px;
}
.pub-perfil-grid { display: flex; flex-direction: column; gap: 24px; min-width: 0; }
.pub-perfil-sec { display: flex; flex-direction: column; gap: 12px; min-width: 0; }

.pub-perfil-h2 {
  margin: 0;
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-titulo); font-weight: 600;
  letter-spacing: -.015em; line-height: 1.25; color: var(--brand-ink);
}
.pub-perfil-h3 {
  margin: 0;
  font-family: var(--pub-fuente-t); font-size: var(--pub-t-subtitulo); font-weight: 600;
  line-height: 1.3; color: var(--brand-ink);
}
.pub-perfil-p { margin: 0; font-size: var(--pub-t-cuerpo); line-height: 1.55; color: var(--text-2); }
.pub-perfil-p b { color: var(--brand-ink); font-weight: 600; }
.pub-perfil-hint { font-size: 12px; line-height: 1.45; color: var(--text-3); font-variant-numeric: tabular-nums; }
.pub-perfil-k {
  font-size: var(--pub-t-etiqueta); font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
}
.pub-perfil-caja {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 16px;
}

/* ── Cabecera ── */
.pub-perfil-cab { display: flex; flex-direction: column; gap: 16px; }
.pub-perfil-foto {
  display: block; width: 100%; height: 184px; object-fit: cover;
  border-radius: var(--pub-radio-card); background: var(--pub-papel-hondo);
}
.pub-perfil-marca { display: flex; gap: 16px; align-items: flex-start; min-width: 0; }
.pub-perfil-avatar { width: 56px; height: 56px; border-radius: 12px; font-size: 20px; }
.pub-perfil-quien { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pub-perfil-h1 {
  margin: 0; overflow-wrap: anywhere;
  font-family: var(--pub-fuente-t); font-size: 21px; font-weight: 600;
  letter-spacing: -.015em; line-height: 1.2; color: var(--brand-ink);
}
.pub-perfil-donde { font-size: 13px; color: var(--text-2); }
.pub-perfil-anios {
  align-self: flex-start; margin-top: 4px;
  font-size: 12px; font-weight: 600; color: var(--brand-ink);
  background: var(--papel); border-radius: 999px; padding: 4px 10px;
}
.pub-perfil-dedica {
  margin: 4px 0 0; font-size: 14px; line-height: 1.4; color: var(--text);
}

/* ── Mosaico de fotos (AGR-390): grande + 2 chicas, UNA en el teléfono ── */
.pub-perfil-mosaico { display: flex; gap: 8px; }
.pub-perfil-foto-grande { flex: 2; }
.pub-perfil-foto-chicas {
  flex: 1; display: flex; flex-direction: column; gap: 8px; min-width: 0;
}
/* En el teléfono va una sola chica, del alto de la grande (184): dos de 88 al
   lado de una de 184 son dos estampillas que no se leen. La segunda vuelve a
   partir de 1000 px, abajo. */
.pub-perfil-foto-chica { height: 184px; }
.pub-perfil-foto-chica ~ .pub-perfil-foto-chica { display: none; }

/* ── Plantas y pagos: pares clave/valor en grilla, nunca tabla ── */
.pub-perfil-planta-cab { display: flex; flex-direction: column; gap: 4px; }
.pub-perfil-caja .pub-perfil-dl { margin-top: 12px; }
.pub-perfil-dl {
  margin: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 16px;
}
.pub-perfil-dt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pub-perfil-dt dt {
  font-size: var(--pub-t-etiqueta); font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
}
.pub-perfil-dt dd { margin: 0; font-size: 13.5px; line-height: 1.4; color: var(--text); overflow-wrap: anywhere; }

/* ── Calidad: frases, una por grano (AGR-390) ── */
.pub-perfil-calidad-lista {
  margin: 0; list-style: none;
  display: flex; flex-direction: column; gap: 8px;
}
.pub-perfil-calidad-lista li { font-size: 13.5px; line-height: 1.4; color: var(--text); }

/* ── Historia: prosa por párrafos ── */
.pub-perfil-historia { display: flex; flex-direction: column; gap: 12px; }
.pub-perfil-historia p { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text); }

/* ── Reseñas de vendedores (AGR-391): citas cargadas a mano ── */
.pub-perfil-resenas-lista {
  margin: 0; list-style: none;
  display: flex; flex-direction: column; gap: 16px;
}
.pub-perfil-resenas-lista li + li { padding-top: 16px; border-top: 1px solid var(--border); }
.pub-perfil-resenas-lista blockquote {
  margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text); font-style: italic;
}
.pub-perfil-resena-pie { margin: 6px 0 0; font-size: 12.5px; color: var(--text-2); }

/* ── Viajes ── */
.pub-perfil-viajes { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.pub-perfil-viajes .pub-perfil-caja { display: flex; flex-direction: column; gap: 4px; }
.pub-perfil-cifra {
  font-family: var(--pub-fuente-t); font-size: 24px; font-weight: 600;
  letter-spacing: -.02em; color: var(--brand-ink); font-variant-numeric: tabular-nums;
}

/* ── Galería: una tira, no un carrusel ── */
.pub-perfil-galeria { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.pub-perfil-galeria img {
  display: block; width: 100%; height: 84px; object-fit: cover;
  border-radius: var(--radius-lg); background: var(--pub-papel-hondo);
}

/* ── Acciones: fijas abajo en el teléfono ── */
.pub-perfil-acciones {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  /* Lleva un solo botón, «Cotizá HOY» (`.pub-perfil-comparar`), que ocupa
     el ancho entero de la barra. */
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px; row-gap: 6px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  /* Translúcida, no opaca (AC de AGR-398): la ficha se sigue leyendo por
     debajo mientras se scrollea, y la barra se lee como algo que flota sobre
     la página y no como su final. Mismo tratamiento que `.pub-nav`, la otra
     barra flotante del sitio público. */
  background: rgba(255, 255, 255, .88);
  backdrop-filter: saturate(140%) blur(8px);
  border-top: 1px solid var(--border);
  box-shadow: var(--pub-sombra-2);
}
/* === AGR-431 === */
/* El botón único «Cotizá HOY» del perfil: la primaria, al cotizador general. */
.pub-perfil-comparar { flex: 1; flex-basis: 100%; justify-content: center; height: 44px; font-size: 14px; font-weight: 600; }

/* ── El diálogo de orden en firme (AGR-398) ── */
.pub-dialog-firme {
  width: min(440px, calc(100vw - 32px)); max-height: min(88vh, 720px);
  padding: 0; border: 0; border-radius: var(--radius-lg);
  background: var(--bg); box-shadow: var(--pub-sombra-2);
}
.pub-dialog-firme::backdrop { background: rgba(8, 9, 10, .45); }
.pub-dialog-firme-form { display: flex; flex-direction: column; gap: 14px; padding: 18px 18px 20px; }
.pub-dialog-firme-cab { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.pub-dialog-firme-titulo { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--text); }
.pub-dialog-firme-cerrar {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border: 0; background: transparent; color: var(--text-3);
  border-radius: var(--radius); cursor: pointer;
}
.pub-dialog-firme-cerrar:hover { background: var(--bg-hover); color: var(--text); }
.pub-dialog-firme-cerrar svg { width: 16px; height: 16px; stroke: currentColor; }
.pub-dialog-firme-calidad { margin: 0; font-size: 12.5px; line-height: 1.4; color: var(--text-2); }
.pub-dialog-firme-nota { margin: 0; font-size: 12.5px; line-height: 1.4; color: var(--text-3); }
/* La cuenta de referencia (AGR-419), renglón por renglón: el concepto a la
   izquierda y el número a la derecha, con el neto cerrando la cuenta. Sin
   esto el `<dl>` cae en el estilo del navegador —el `<dd>` sangrado abajo de
   su `<dt>`—, en el perfil y en cada oferta de los resultados (AGR-420). */
.pub-dialog-firme-cuenta { display: flex; flex-direction: column; gap: 6px; margin: 0; }
.pub-dialog-firme-dt { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.pub-dialog-firme-dt dt { font-size: 13px; color: var(--text-2); }
.pub-dialog-firme-dt dd { margin: 0; font-size: 13.5px; color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pub-dialog-firme-dt-total { padding-top: 8px; border-top: 1px solid var(--border); }
.pub-dialog-firme-dt-total dt { color: var(--brand-ink); font-weight: 600; }
.pub-dialog-firme-dt-total dd { font-weight: 600; font-size: 15px; }
.pub-dialog-firme-grilla { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.pub-dialog-firme-campo { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pub-dialog-firme-campo label {
  font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-3);
}
.pub-dialog-firme-campo input {
  height: 38px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 13.5px; color: var(--text); background: var(--bg);
}
.pub-dialog-firme-acciones { display: flex; justify-content: flex-end; margin-top: 4px; }
@media (max-width: 480px) {
  .pub-dialog-firme-grilla { grid-template-columns: minmax(0, 1fr); }
}

@media (min-width: 1000px) {
  .pub-perfil { padding: 32px 24px 64px; }
  .pub-perfil-in { gap: 32px; }
  .pub-perfil-h1 { font-size: 30px; letter-spacing: -.02em; }
  .pub-perfil-avatar { width: 72px; height: 72px; border-radius: 14px; font-size: 26px; }
  .pub-perfil-foto { height: 280px; }
  /* Las dos chicas, y su columna mide lo mismo que la grande: 2 × 136 + 8 de
     gap = 280. Va DESPUÉS de `.pub-perfil-foto` a propósito: misma
     especificidad, gana la última, y sin esto cada chica se estiraba a 280 y
     el mosaico quedaba el doble de alto que la foto grande. */
  .pub-perfil-foto-chica { height: 136px; }
  .pub-perfil-foto-chica ~ .pub-perfil-foto-chica { display: block; }
  /* Con una sola chica (empresa con 2 fotos) la columna se estira igual a la
     grande: media columna dibujada y media vacía es el hueco que el ticket
     no quiere. En el teléfono no hace falta, ahí ya mide lo mismo. */
  .pub-perfil-foto-chica:only-child { height: 280px; }

  /* Filas de alto automático y sin gap: la columna del costado ocupa muchas
     filas, y un gap de grilla dejaría huecos por cada fila vacía que abarca.
     El aire entre secciones lo da el margen. */
  .pub-perfil-grid {
    display: grid; grid-template-columns: minmax(0, 1fr) 340px;
    column-gap: 32px; row-gap: 0; align-items: start;
  }
  .pub-perfil-grid > * { grid-column: 1; margin-bottom: 32px; }
  .pub-perfil-grid > .pub-perfil-panel {
    grid-column: 2; grid-row: 1 / span 6;
    position: sticky; top: 88px;
  }
  .pub-perfil-grid .pub-oportunidades { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pub-perfil-dl { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .pub-perfil-panel .pub-perfil-dl { grid-template-columns: minmax(0, 1fr); }
  .pub-perfil-viajes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pub-perfil-galeria { grid-template-columns: 2fr 1fr 1fr; gap: 12px; }
  .pub-perfil-galeria img { height: 200px; }

  .pub-perfil-acciones {
    position: static; z-index: auto;
    flex-direction: column; align-items: stretch; gap: 4px;
    padding: 0; border: 0; box-shadow: none;
    background: none; backdrop-filter: none;
  }
  /* `flex: 1` da `flex-basis: 0`, que en este eje columna pisa el
     `height: 44px` de base: el botón quedaba aplastado a 23 px (hallazgo del
     QA final). Acá no hace falta crecer —el ancho ya lo da
     `align-items: stretch`—, así que se apaga el grow/shrink y gana el
     `height` de siempre. */
  .pub-perfil-comparar { flex: none; }
}

/* ── Mapa de sus plantas (AGR-346, enganchado acá por `mapa_perfil.js`) ──────
   Una caja más de la columna, con el orden del issue: nunca overlay ni
   columna fija como en el catálogo. El componente (`.pub-mapa`, AGR-346) no
   trae alto propio —lo pone quien lo usa—, así que se lo da acá.

   `[hidden]` necesita `!important`: sin él, `.pub-perfil-sec { display: flex }`
   de arriba (misma especificidad, pero de autor) le gana a la regla del
   navegador para `hidden` y la sección se vería igual, vacía, mientras el
   mapa carga o si nunca lo hace —el mismo defecto que ya tiene resuelto
   `.pub-mapa[hidden]` de AGR-346 más abajo. */
.pub-perfil-mapa[hidden] { display: none !important; }
.pub-perfil-mapa .pub-mapa {
  display: block;
  height: 320px;
  border: 1px solid var(--border); border-radius: var(--pub-radio-card);
  overflow: hidden; box-shadow: var(--pub-sombra-1);
}
.pub-perfil-mapa .pub-mapa-panel { height: 100%; }
@media (min-width: 1000px) {
  .pub-perfil-mapa .pub-mapa { height: 420px; }
}

/* === AGR-347 === */
/* El cotizador precargado y los resultados enlazados.

   El banner de contexto («Cotizando con … · Maíz · desde …») va con el papel
   de la marca, como la mejor oferta: es lo que ya sabemos, no un aviso. El
   «cambiar» es un link y no un botón, y lleva alto de dedo en el teléfono. */
.pub-contexto {
  margin: 0 0 20px; padding: 10px 14px;
  background: var(--brand-soft); border: 1px solid var(--brand-line);
  border-radius: var(--pub-radio-card);
}
.pub-contexto-fila { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.pub-contexto-txt { margin: 0; font-size: 14px; line-height: 1.45; color: var(--text); }
.pub-contexto-txt strong { font-weight: 600; color: var(--brand-ink); }
.pub-contexto-cambiar {
  flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px;
  font-size: 13px; font-weight: 500; color: var(--brand);
  text-decoration: underline; text-underline-offset: 2px;
}
.pub-contexto-cambiar:hover { color: var(--brand-hover); }
.pub-contexto-cambiar:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius); }
.pub-contexto-nota { margin: 2px 0 4px; font-size: 12.5px; line-height: 1.45; color: var(--text-2); }

/* El comprador desde el que se cotizó: borde teal entero y un rótulo encima
   del nombre. No cambia el tamaño de nada: las filas se siguen comparando. */
.oferta-elegida { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.of-elegida {
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--brand);
}
.res-marcado-ausente { text-align: left; margin: 0 0 12px; color: var(--text-2); }

/* El comprador precargado que pierde contra Rosario (AGR-363, variante B): se
   lista igual que cualquier oferta pero atenuada, y en tierra —no en rojo—
   porque no es un error, es una oferta que no alcanza (mismo criterio que
   `.pub-mapa-pin.is-viejo`). */
.oferta-ausente { opacity: .62; }
.oferta-ausente .of-cifra { color: var(--text-2); }
.oferta-ausente-marca {
  position: absolute; top: -9px; left: 18px;
  background: var(--pub-tierra); color: var(--text-inv);
  font-size: 10.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 9px; border-radius: 999px;
}

/* Cuándo se calculó: pegado a la bajada, y en `--text-2` y no en el gris de
   `.pub-nota`, que no llega a 4,5:1 sobre blanco y esto es un dato. */
.res-cuando { text-align: left; margin: -14px 0 0; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* El nombre linkea al perfil: se lee como el nombre, y el subrayado aparece
   al pasar o al enfocar. */
a.of-nombre { text-decoration: none; }
a.of-nombre:hover { text-decoration: underline; text-underline-offset: 2px; }
a.of-nombre:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius); }

/* Las dos acciones de cada oferta, como celdas de la grilla de la fila: el
   WhatsApp («¿Tenés dudas?») en la columna del comprador y la orden en firme
   (AGR-420, abre `perfil_orden_firme_dialog.html`) de la pizarra al neto, así
   quedan alineadas con los datos de arriba. Misma altura y mismo radio; el
   WhatsApp sin fondo y con borde claro, para que la primaria mande. */
.of-firme, .of-dudas { height: 44px; justify-content: center; font-size: 14px; }
.of-firme { grid-column: 2 / -1; }
.of-dudas {
  grid-column: 1; background: transparent;
  border-color: var(--border); color: var(--text-2); text-decoration: none;
}
.oferta-mejor .of-dudas { border-color: var(--brand-line); }
.of-dudas svg { color: var(--brand); }
.of-dudas:hover { background: var(--bg-hover); color: var(--brand-ink); }
.oferta-mejor .of-dudas:hover { background: rgba(19, 97, 91, .06); }
.of-firme:focus-visible, .of-dudas:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Sin la otra (una orden sin precio resoluble, o sin teléfono en
   Configuración), la que queda ocupa la fila entera. */
.of-accion-sola { grid-column: 1 / -1; }
/* En el teléfono, apiladas a todo lo ancho y la primaria arriba. `order`
   sólo acá: en el HTML van en el orden del escritorio (de izquierda a
   derecha), y en un teléfono no hay tabulador que se desordene. */
@media (max-width: 1000px) {
  .of-firme, .of-dudas { grid-column: 1 / -1; }
  .of-firme { order: 1; }
  .of-dudas { order: 2; }
}

/* === AGR-346 === */
/* El mapa del catálogo: la columna de la derecha en desktop, el overlay de
   «Ver mapa» en el teléfono, y los pins (publico/templates/publico/_mapa.html,
   static/js/mapa.js y mapa_catalogo.js). El componente es genérico: el mapa de
   plantas del perfil (AGR-344) usa las mismas clases.

   Todo lo que depende de que el mapa cargue lo prende el script con una clase
   (`.con-mapa`, `.is-abierto`) o sacando un `hidden`. Sin script, o con el
   mapa caído, ninguna de estas reglas cambia el catálogo. */

/* `hidden` gana siempre: las reglas de abajo le ponen `display` al mapa y al
   botón, y sin esto un mapa que no cargó ocuparía su lugar vacío. */
.pub-mapa[hidden],
.pub-mapa-pie[hidden],
.pub-ver-mapa[hidden] { display: none !important; }

/* ── «Ver mapa», al lado del conteo ────────────────────────────────────────
   Un link de texto en marca, como en el diseño, con zona de toque de un dedo:
   44 de alto, y el margen negativo para que no empuje la fila. */
.pub-conteo-fila {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.pub-ver-mapa {
  display: inline-flex; align-items: center; gap: 5px;
  height: 44px; margin: -12px -8px -12px 0; padding: 0 8px;
  border: 0; border-radius: var(--radius); background: transparent;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--brand);
  cursor: pointer; flex: none;
}
.pub-ver-mapa svg { width: 16px; height: 16px; stroke: currentColor; }
.pub-ver-mapa:hover { background: var(--brand-soft); }
.pub-ver-mapa:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ── Los pins ───────────────────────────────────────────────────────────────
   Píldora blanca con la cifra, como el prototipo. Mapbox posiciona el botón
   (le pone `position: absolute` y un `transform`), así que acá no se toca
   ninguna de las dos cosas. */
.pub-mapa-pin {
  display: inline-flex; align-items: center;
  height: 28px; padding: 0 10px; border-radius: 999px;
  background: var(--bg); border: 1px solid var(--border-strong);
  box-shadow: var(--pub-sombra-1);
  font-family: var(--font); font-size: 12px; font-weight: 600; line-height: 1;
  color: var(--brand-ink); font-variant-numeric: tabular-nums; white-space: nowrap;
  cursor: pointer;
}
/* La zona de toque llega a 44 aunque la píldora mida 28. */
.pub-mapa-pin::before { content: ""; position: absolute; inset: -8px -2px; }
@media (hover: hover) and (pointer: fine) {
  .pub-mapa-pin:hover { border-color: var(--brand-line); box-shadow: var(--pub-sombra-2); }
}
.pub-mapa-pin:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* Una card con origen que no llegó a neto: su número es precio en planta, y
   al lado de netos se tiene que ver distinto. Borde punteado, como el estado
   vacío: «esto no es lo mismo que lo de al lado». */
.pub-mapa-pin.is-planta { border-style: dashed; color: var(--text-2); }
/* Un precio que no es de hoy, en tierra, como el sello de su card. */
.pub-mapa-pin.is-viejo { color: var(--pub-tierra); }
.pub-mapa-pin.is-sel,
.pub-mapa-pin.is-sel:hover {
  background: var(--brand); border-color: var(--brand); color: var(--text-inv);
  box-shadow: var(--pub-sombra-2);
}

/* El origen del productor: un punto, no una píldora, para que no se confunda
   con un precio. */
.pub-mapa-origen { display: inline-flex; align-items: center; gap: 6px; pointer-events: none; }
.pub-mapa-origen-punto {
  width: 14px; height: 14px; border-radius: 999px;
  background: var(--brand-ink); box-shadow: 0 0 0 3px var(--bg), var(--pub-sombra-2);
}
.pub-mapa-origen-k {
  padding: 3px 7px; border-radius: 999px;
  background: rgba(255, 255, 255, .94);
  font-size: 11.5px; font-weight: 600; color: var(--brand-ink); white-space: nowrap;
}

/* ── El mapa ────────────────────────────────────────────────────────────── */
.pub-mapa-panel { display: flex; flex-direction: column; background: var(--bg); min-height: 0; }
.pub-mapa-cuerpo {
  position: relative; flex: 1; min-height: 0; background: var(--pub-papel-hondo);
  display: flex;
}
/* El lienzo toma el tamaño estirándose en el flex, no con `position`: Mapbox
   le pone la clase `.mapboxgl-map`, cuya hoja le fija `position: relative`.
   Con `position: absolute; inset: 0` acá, la que llegaba última ganaba y el
   lienzo quedaba con alto 0: un mapa en blanco con la leyenda sola (QA de
   AGR-346). Así el tamaño no depende de quién gana esa propiedad. */
.pub-mapa-lienzo { flex: 1 1 auto; min-width: 0; }
/* La condición de los números, siempre a la vista arriba del mapa. Las dos
   —la del texto que llena el script y, en el catálogo, la del color
   (AGR-363)— apilan en la misma esquina: la segunda no empuja bajo la
   primera con un `top` a mano, sigue por ser hermana con `gap`. */
.pub-mapa-leyenda,
.pub-mapa-leyenda-viejo {
  position: absolute; top: 10px; left: 10px; right: 56px; z-index: 1;
  width: fit-content; max-width: calc(100% - 66px);
  margin: 0; padding: 6px 10px; border-radius: var(--pub-radio-card);
  background: rgba(255, 255, 255, .94); box-shadow: var(--pub-sombra-1);
  font-size: 12px; font-weight: 500; line-height: 1.35; color: var(--brand-ink);
  pointer-events: none;
}
.pub-mapa-leyenda:empty { display: none; }
.pub-mapa-leyenda:not(:empty) ~ .pub-mapa-leyenda-viejo { top: 54px; }

.pub-mapa-leyenda-viejo { display: flex; flex-direction: column; gap: 4px; }
.pub-mapa-leyenda-fila { display: flex; align-items: center; gap: 6px; }
/* La muestra lleva la clase `.pub-mapa-pin` en el HTML —fondo, borde, forma
   y el color de `.is-viejo`— para ser el mismo objeto que el pin, no una
   aproximación con otra forma. Achicada y sin cursor ni zona de toque, que
   acá no hacen nada. */
.pub-mapa-leyenda-muestra {
  height: 20px; padding: 0 7px; font-size: 11px; cursor: default; flex: none;
}

/* La barra de «Volver a la lista» y el scrim son del overlay. */
.pub-mapa-scrim,
.pub-mapa-top { display: none; }

/* La card que resalta un pin. Más que el hover, para que se distinga de la
   card que está bajo el mouse. */
.pub-oportunidad.is-mapa {
  border-color: var(--brand); background: var(--brand-soft);
  box-shadow: 0 0 0 1px var(--brand), var(--pub-sombra-2);
}

/* ── Teléfono: el overlay ─────────────────────────────────────────────────
   Casi a pantalla completa: la franja de arriba es scrim, y tocarla cierra.
   Nunca lista y mapa compartiendo la pantalla. */
html.pub-mapa-bloqueo { overflow: hidden; }
@media (max-width: 999.98px) {
  .pub-mapa.is-abierto {
    position: fixed; inset: 0; z-index: var(--z-modal);
    display: block;
  }
  .pub-mapa.is-abierto .pub-mapa-scrim {
    display: block;
    position: absolute; inset: 0;
    background: rgba(0, 44, 54, .5);
  }
  .pub-mapa.is-abierto .pub-mapa-panel {
    position: absolute; top: 40px; right: 0; bottom: 0; left: 0;
    border-radius: 16px 16px 0 0; overflow: hidden;
    box-shadow: var(--pub-sombra-2);
  }
  .pub-mapa.is-abierto .pub-mapa-top {
    display: flex; align-items: center; gap: 8px;
    flex: none; height: 52px; padding: 0 12px 0 4px;
    border-bottom: 1px solid var(--border);
  }
  .pub-mapa-volver {
    display: inline-flex; align-items: center; gap: 6px;
    height: 44px; padding: 0 10px; border: 0; border-radius: var(--radius);
    background: transparent; font: inherit; font-size: 14px; font-weight: 600;
    color: var(--brand-ink); cursor: pointer;
  }
  .pub-mapa-volver svg { width: 16px; height: 16px; }
  .pub-mapa-volver:hover { background: var(--bg-hover); }
  .pub-mapa-volver:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
  .pub-mapa-titulo {
    margin-left: auto; font-size: 12px; color: var(--text-2);
    font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  /* La card del pin tocado, al pie: flota sobre el mapa (sombra 2). */
  .pub-mapa-pie {
    flex: none; max-height: 45vh; overflow-y: auto;
    padding: 10px 12px 14px; border-top: 1px solid var(--border);
    background: var(--bg); box-shadow: var(--pub-sombra-2);
  }
  /* Sube como una hoja y, al cerrarla, baja por el mismo camino: algo que
     entra deslizándose y sale teletransportado no dice adónde se fue. La
     salida es más corta que la entrada; el que cierra ya terminó de mirar. */
  @media (prefers-reduced-motion: no-preference) {
    .pub-mapa.is-abierto .pub-mapa-panel { animation: pub-mapa-sube 240ms var(--ease-drawer); }
    .pub-mapa.is-abierto .pub-mapa-scrim { animation: pub-mapa-aparece 240ms var(--ease-drawer); }
    .pub-mapa.is-abierto.is-cerrando .pub-mapa-panel { animation: pub-mapa-baja 160ms var(--ease-out) forwards; }
    .pub-mapa.is-abierto.is-cerrando .pub-mapa-scrim { animation: pub-mapa-se-va 160ms var(--ease-out) forwards; }
  }
  /* Con menos movimiento no hay desplazamiento, pero tampoco un corte: el
     mapa aparece y se va con un fundido. */
  @media (prefers-reduced-motion: reduce) {
    .pub-mapa.is-abierto .pub-mapa-panel,
    .pub-mapa.is-abierto .pub-mapa-scrim { animation: pub-mapa-aparece 160ms ease; }
    .pub-mapa.is-abierto.is-cerrando .pub-mapa-panel,
    .pub-mapa.is-abierto.is-cerrando .pub-mapa-scrim { animation: pub-mapa-se-va 120ms ease forwards; }
  }
  /* Mientras se va, no se toca: un segundo toque sobre el scrim no tiene
     nada que cerrar. */
  .pub-mapa.is-cerrando { pointer-events: none; }
}
@keyframes pub-mapa-sube { from { transform: translateY(24px); opacity: .6; } }
@keyframes pub-mapa-aparece { from { opacity: 0; } }
@keyframes pub-mapa-baja { to { transform: translateY(24px); opacity: 0; } }
@keyframes pub-mapa-se-va { to { opacity: 0; } }

/* ── Desktop: lista a la izquierda, mapa a la derecha ─────────────────────
   Como la búsqueda con mapa de Airbnb (AGR-348): el marco deja el tope de
   1100 y va a todo el ancho de la ventana. La lista va siempre a dos columnas
   y el mapa se queda con todo lo que sobra, como una card grande con aire
   alrededor. En un monitor grande la lista no crece: más ancho de card no dice
   nada más, y más mapa sí.

   El scroll es de la PÁGINA, no de un panel con `overflow` propio: la rueda,
   el teclado, «volver» del navegador y el `scrollIntoView` del pin funcionan
   como en cualquier página. Lo quieto (el mapa y la barra de filtros) es
   `sticky`, y el mapa se despega solo al terminar el marco, con el footer.

   Todo cuelga de `.con-mapa` (AGR-346): sin mapa, o con un grano sin
   compradores, el catálogo es el de siempre, centrado a tres columnas. */
@media (min-width: 1000px) {
  .pub-ver-mapa { display: none !important; }
  .pub-catalogo-marco.con-mapa {
    /* El alto de la navbar (logo de 38 + 11 arriba y abajo + el borde), que es
       el techo de todo lo fijo. Y el de la barra de filtros, que sólo usa el
       `scroll-margin` de las cards: el alto real depende del origen elegido y
       del ancho, así que es el del caso más alto, medido en el QA. */
    --pub-nav-alto: 61px;
    --pub-filtros-alto: 146px;
    /* Las proporciones de la búsqueda con mapa de Airbnb, medidas a 2000 de
       ancho (QA de AGR-348): el mismo margen a los costados y entre la lista
       y el mapa (48 en un monitor grande, 24 en una notebook), y la lista y
       el mapa casi mitad y mitad. */
    --pub-margen: clamp(24px, 2.5vw, 48px);
    /* El aire del mapa contra la navbar y el pie. Arriba es el mismo que el
       de la lista, así el mapa arranca a la altura del título. */
    --pub-mapa-aire: 24px;
    /* Anula el padding de `.pub-catalogo`: los márgenes los pone el marco. */
    margin: -24px -16px -48px;
    padding: 0 var(--pub-margen);
    display: grid;
    /* La lista, siempre a dos columnas, con el 48 % del ancho. En una
       notebook eso deja cards muy angostas, así que ahí toma hasta 560 (sin
       pasar del 55 %: el mapa no baja del 40 %). En un monitor muy ancho, la
       lista para en 1000 y el resto va al mapa. Todo continuo: cambiar el
       ancho de la ventana no hace saltar columnas. */
    grid-template-columns: clamp(min(560px, 55%), 48%, 1000px) minmax(0, 1fr);
    column-gap: var(--pub-margen); align-items: start;
  }
  .pub-catalogo-marco.con-mapa > .pub-catalogo-in {
    max-width: none; margin: 0;
    padding: var(--pub-mapa-aire) 0 48px;
  }
  .pub-catalogo-marco.con-mapa .pub-oportunidades {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 24px;
  }
  /* Como en Airbnb, el mapa no va a sangre: flota con aire arriba, a la
     derecha y abajo, con las esquinas redondeadas. */
  .pub-catalogo-marco.con-mapa > .pub-mapa {
    display: block;
    position: sticky; top: calc(var(--pub-nav-alto) + var(--pub-mapa-aire));
    margin: var(--pub-mapa-aire) 0;
    height: calc(100vh - var(--pub-nav-alto) - 2 * var(--pub-mapa-aire)); min-height: 420px;
  }
  .pub-catalogo-marco.con-mapa .pub-mapa-panel {
    height: 100%;
    border: 1px solid var(--border); border-radius: 16px;
    overflow: hidden; box-shadow: var(--pub-sombra-1);
  }

  /* La barra de filtros, fija debajo de la navbar. Fondo opaco, del color de
     la página: las cards pasan por debajo y no se ven a través. El margen
     negativo la estira sobre el aire de la lista (el gutter de la izquierda y
     el hueco hasta el mapa), para que una card no asome por los costados; no
     llega al mapa, así que no tapa ni la leyenda ni los controles.
     El `z-index` la pone arriba de las cards (que son `position: relative`) y
     debajo de la navbar (40); el desplegable del buscador de origen
     (`--z-popover`) queda adentro de su contexto y abre sobre las cards.
     El `margin-top` negativo se come parte del `gap` de la columna: sin
     fijar se ve igual que antes, y fija deja 10 de aire bajo la navbar. */
  .pub-catalogo-marco.con-mapa .pub-filtros {
    position: sticky; top: var(--pub-nav-alto); z-index: 30;
    margin: -10px calc(-1 * var(--pub-margen)) 0;
    padding: 10px var(--pub-margen) 12px;
    background: var(--bg-sunken);
  }
  /* Un pin trae su card con `scrollIntoView` (`mapa_catalogo.js`): sin esto la
     card quedaba justo debajo de la barra fija, tapada. */
  .pub-catalogo-marco.con-mapa .pub-oportunidad {
    scroll-margin-top: calc(var(--pub-nav-alto) + var(--pub-filtros-alto) + 12px);
    scroll-margin-bottom: 12px;
  }
}

/* === Movimiento del sitio público === */

/* ── Respuesta al toque ─────────────────────────────────────────────────
   En el stand casi todo se toca con el dedo, y ahí no hay hover: sin esto,
   tocar una card o un botón no da ninguna señal hasta que llega la página
   siguiente, y en la red del stand eso puede tardar. Se hunde apenas —0,97;
   la card, que es grande, 0,985— y vuelve sola al soltar. */
.pub .btn {
  transition: background .12s ease, border-color .12s ease, color .12s ease,
              transform 160ms var(--ease-out);
}
.pub .btn:active:not(:disabled) { transform: scale(0.97); }

.pub-oportunidad {
  transition: background-color 150ms ease, border-color 150ms ease,
              transform 160ms var(--ease-out);
}
.pub-oportunidad:active { transform: scale(0.985); }

.pub-chip-grano {
  transition: border-color 120ms ease, color 120ms ease,
              transform 160ms var(--ease-out);
}
.pub-chip-grano:active { transform: scale(0.97); }

.grano-caja { transition: border-color .12s, background .12s, transform 160ms var(--ease-out); }
.grano:active .grano-caja { transform: scale(0.97); }

/* Quien pidió menos movimiento se queda con el cambio de color y sin el
   hundimiento. Hace falta decirlo: la regla global de `.btn` le saca la
   transición, así que el `scale` saltaría de golpe. */
@media (prefers-reduced-motion: reduce) {
  .pub .btn:active:not(:disabled),
  .pub-oportunidad:active,
  .pub-chip-grano:active,
  .grano:active .grano-caja { transform: none; }
}

/* ── Las ofertas de los resultados ──────────────────────────────────────
   Es la pantalla que el productor vino a buscar y la ve una vez por
   cotización: la lista entra escalonada, la mejor primero, que además es el
   orden en que hay que leerla. 50 ms entre filas y tope a la sexta: con doce
   ofertas la última no puede tardar medio segundo. No frena nada: los links
   funcionan mientras entran. */
@media (prefers-reduced-motion: no-preference) {
  .ofertas > .oferta { animation: pub-oferta-entra 300ms var(--ease-out) both; }
  .ofertas > .oferta:nth-child(2) { animation-delay: 50ms; }
  .ofertas > .oferta:nth-child(3) { animation-delay: 100ms; }
  .ofertas > .oferta:nth-child(4) { animation-delay: 150ms; }
  .ofertas > .oferta:nth-child(5) { animation-delay: 200ms; }
  .ofertas > .oferta:nth-child(n+6) { animation-delay: 250ms; }
}
@media (prefers-reduced-motion: reduce) {
  .ofertas > .oferta { animation: pub-oferta-aparece 200ms ease both; }
}
@keyframes pub-oferta-entra { from { opacity: 0; transform: translateY(8px); } }
@keyframes pub-oferta-aparece { from { opacity: 0; } }

/* ── El catálogo después de filtrar ────────────────────────────────────
   Un chip o un origen reemplazan el catálogo entero. Lo nuevo entra desde
   el mismo 50 % en que quedó atenuado lo viejo mientras se recalculaba, así
   que las dos cosas se ven como una sola transición y no como un corte.
   Sólo opacidad: es una lista para comparar precios, no se mueve.
   `htmx-added` la pone htmx sobre lo que acaba de insertar y la saca al
   asentarlo, así que la primera carga de la página no anima. */
.pub-catalogo-in > .pub-conteo-fila,
.pub-catalogo-in > .pub-grupo,
.pub-catalogo-in > .pub-vacio { transition: opacity 150ms var(--ease-out); }
.pub-catalogo-in.htmx-added > .pub-conteo-fila,
.pub-catalogo-in.htmx-added > .pub-grupo,
.pub-catalogo-in.htmx-added > .pub-vacio { opacity: .5; }

/* ── La cuenta del neto, en la landing ─────────────────────────────────
   Se arma en cuatro tiempos —precio, menos flete, menos comisión, igual—
   cuando llega a la pantalla. `is-listo` y `is-visto` los pone
   `publico.js`: sin JS la fórmula está entera desde el principio. Cada
   signo entra con la fila que lo sigue. */
.formula.is-listo > * {
  opacity: 0; transform: translateY(6px);
  transition: opacity 350ms var(--ease-out), transform 350ms var(--ease-out);
}
.formula.is-listo.is-visto > * { opacity: 1; transform: none; }
.formula.is-listo.is-visto > :nth-child(2),
.formula.is-listo.is-visto > :nth-child(3) { transition-delay: 90ms; }
.formula.is-listo.is-visto > :nth-child(4),
.formula.is-listo.is-visto > :nth-child(5) { transition-delay: 180ms; }
.formula.is-listo.is-visto > :nth-child(6),
.formula.is-listo.is-visto > :nth-child(7) { transition-delay: 270ms; }

/* === AGR-355 === La lista «Para revisar antes de mandar» arriba del
   documento de la pestaña Liquidación (variante C del prototipo AGR-353). */
.revisar { border: 1px solid var(--border); border-radius: var(--radius-lg); margin-bottom: 14px; }
.revisar-head { padding: 10px 14px; font-size: 13px; font-weight: 600; border-bottom: 1px solid var(--border); display: flex; gap: 8px; align-items: center; }
.revisar-head:last-child { border-bottom: 0; }
.revisar-head .dim { font-weight: 400; color: var(--text-3); }
.revisar-item { display: flex; align-items: center; gap: 12px; padding: 9px 14px; border-bottom: 1px solid var(--border); font-size: 13px; }
.revisar-item:last-child { border-bottom: 0; }
.revisar-item .txt { flex: 1; min-width: 0; }
.revisar-item .sub-l { display: block; font-size: 12px; color: var(--text-2); margin-top: 2px; }
.revisar-ok { padding: 12px 14px; font-size: 13px; color: var(--text-2); }

/* === AGR-357 === La comisión de cada punta, debajo del documento: interna,
   no va en el PDF. Borde punteado (mismo lenguaje que `.pub-vacio`): se lee
   como un dato aparte del documento, no como parte de él. */
.comision-box {
  margin-top: 14px; padding: 12px 14px;
  border: 1px dashed var(--border-strong); border-radius: var(--radius-lg);
}
.comision-box-titulo { font-size: 12px; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: .04em; }
.comision-box-linea { margin-top: 4px; font-size: 13.5px; font-weight: 500; }
.comision-box-sep { color: var(--text-3); margin: 0 2px; }
.comision-box-nota { margin-top: 4px; font-size: 12px; color: var(--text-3); }

/* === AGR-356 === El formulario que completa el precio de un negocio viejo,
   en el lugar del documento de la pestaña Liquidación (variante C del
   prototipo AGR-353, escenario «viejo»). */
.viejo-intro { font-size: 13.5px; color: var(--text-2); margin: 0 0 16px; max-width: 70ch; line-height: 1.5; }
.viejo { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 24px; align-items: start; }
@media (max-width: 900px) { .viejo { grid-template-columns: minmax(0, 1fr); } }
.orig-papel { background: var(--papel); color: #1a3a3a; padding: 14px 16px; border-radius: var(--radius-lg); font-size: 12px; }
.orig-papel .k { font-size: 9px; color: #25685b; text-transform: uppercase; font-weight: 700; letter-spacing: .03em; }
.orig-papel p { margin: 2px 0 8px; }
.orig-papel .resalta { background: rgba(37, 104, 91, .13); border-radius: 3px; padding: 1px 3px; font-weight: 600; }
/* Los campos del otro `precio_tipo` (ver `_precio_tipo_js.html`): `hidden`
   a secas pierde contra cualquier `display` que le ponga una regla. */
[data-precio-grupo][hidden] { display: none; }
.preview-precio { font-size: 12.5px; color: var(--text-2); background: var(--bg-sunken); border-radius: 6px; padding: 8px 10px; margin-top: 13px; }
.viejo-botones { display: flex; justify-content: flex-end; gap: 6px; margin-top: 14px; }

/* === AGR-359 === «Corregir una pizarra»: el botón va a la derecha de la
   cabecera de «Para revisar antes de mandar». */
.revisar-head .revisar-head-accion { margin-left: auto; font-weight: 400; }

/* === AGR-411: el estado de la orden en firme, deducido === */
/* El rótulo del estado. Las acciones (AGR-409) ya no cuelgan al lado: viven
   todas en el kebab de la fila (`.lin-kebab`, arriba). */
.firme-estado {
  font-size: 12px; color: var(--text-2); white-space: nowrap;
  background: var(--bg-sunken); border: 1px solid var(--border);
  border-radius: 20px; padding: 2px 10px;
}
.firme-estado-cerrada { color: var(--text); font-weight: 500; }
.firme-estado-vencida, .firme-estado-descartada { color: var(--text-3); }

/* === AGR-430 === La orden en firme con «¿Sos cliente?», el resumen de las
   condiciones del comprador y la calidad que declara el vendedor. */
.pub-dialog-firme-cliente, .pub-dialog-firme-alta, .pub-dialog-firme-humedad {
  margin: 0; padding: 0; border: 0; min-width: 0;
}
.pub-dialog-firme-cliente legend, .pub-dialog-firme-alta legend, .pub-dialog-firme-humedad legend {
  padding: 0; margin-bottom: 6px;
  font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-3);
}
.pub-dialog-firme-opciones { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.pub-dialog-firme-opciones label,
.pub-dialog-firme-check label {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13.5px; font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--text);
  cursor: pointer;
}
/* Radios y checkbox dibujados acá, con `appearance: none`: el nativo pinta el
   borde del vacío casi negro en macOS, y `accent-color` sólo cambia el relleno
   del marcado (ver `.lin-check`). El borde es el de los campos de al lado. */
.pub-dialog-firme-opciones input, .pub-dialog-firme-check input {
  appearance: none; -webkit-appearance: none;
  flex-shrink: 0; width: 16px; height: 16px; margin: 0; padding: 0; cursor: pointer;
  border: 1px solid var(--border-strong); background: var(--bg) no-repeat center;
  transition: background-color .1s, border-color .1s, box-shadow .1s;
}
.pub-dialog-firme-opciones input[type="radio"] { border-radius: 50%; }
.pub-dialog-firme-check input[type="checkbox"] { border-radius: 4px; background-size: 12px; }
.pub-dialog-firme-opciones input:hover, .pub-dialog-firme-check input:hover { border-color: var(--text-3); }
.pub-dialog-firme-opciones input[type="radio"]:checked {
  border-color: var(--brand); box-shadow: inset 0 0 0 4px var(--bg); background-color: var(--brand);
}
.pub-dialog-firme-check input[type="checkbox"]:checked {
  border-color: var(--brand); background-color: var(--brand);
  background-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2012'><path%20d='M2.6%206.2%204.9%208.5%209.4%203.7'%20fill='none'%20stroke='%23fff'%20stroke-width='1.9'%20stroke-linecap='round'%20stroke-linejoin='round'/></svg>");
}
.pub-dialog-firme-opciones input:focus-visible, .pub-dialog-firme-check input:focus-visible {
  outline: none; box-shadow: 0 0 0 3px rgba(19, 97, 91, .16);
}
.pub-dialog-firme-opciones input[type="radio"]:checked:focus-visible {
  box-shadow: inset 0 0 0 4px var(--bg), 0 0 0 3px rgba(19, 97, 91, .16);
}
/* «¿Sos cliente?» como dos botones parejos (el video del stand del CREA): el
   radio sigue adentro de la etiqueta —teclado, lector y `required` intactos—
   pero no se ve; la marca es la etiqueta entera en el verde de la marca. */
.pub-dialog-firme-botones { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pub-dialog-firme-botones label {
  position: relative; justify-content: center; min-height: 40px; padding: 8px 12px;
  border: 1px solid var(--border-strong); border-radius: var(--radius); background: var(--bg);
  font-weight: 500; text-align: center;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.pub-dialog-firme-botones label:hover { background: var(--bg-hover); }
.pub-dialog-firme-botones input {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer;
}
.pub-dialog-firme-botones label:has(input:checked) {
  background: var(--brand-soft); border-color: var(--brand); color: var(--brand);
}
.pub-dialog-firme-botones label:has(input:focus-visible) {
  outline: 2px solid var(--brand); outline-offset: 2px;
}
.pub-dialog-firme-check, .pub-dialog-firme-ancho { grid-column: 1 / -1; }
.pub-dialog-firme-campo select, .pub-dialog-firme-campo textarea {
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius);
  font: inherit; font-size: 13.5px; color: var(--text); background: var(--bg);
}
.pub-dialog-firme-campo select { height: 38px; padding-block: 0; }
.pub-dialog-firme-campo textarea { resize: vertical; min-height: 60px; }
.pub-dialog-firme-sub {
  margin: 4px 0 0; font-size: 13px; font-weight: 600; color: var(--text);
}
/* El resumen es prosa larga (la recepción, la forma de pago): a la derecha,
   pero partiendo renglones, no en uno solo como los números de la cuenta. */
.pub-dialog-firme-condiciones { display: flex; flex-direction: column; gap: 6px; margin: 0; }
.pub-dialog-firme-condiciones dd { white-space: normal; text-align: right; }
/* El alta sólo con «Alta de cliente», y el % sólo con «Húmedo». Sin
   `:has()` se ven siempre: la vista igual ignora el alta de un cliente y el
   % de un grano seco. */
.pub-dialog-firme-form:has(.pub-dialog-firme-cliente input[value="alta"]:not(:checked)) .pub-dialog-firme-alta,
.pub-dialog-firme-grilla:has(.pub-dialog-firme-humedad input[value="humedo"]:not(:checked)) .pub-dialog-firme-humedad-pct {
  display: none;
}
/* La lista interna: el alta debajo del nombre, y la calidad que parte
   renglones en vez de estirar la tabla. */
.firme-alta { font-size: 12px; margin-top: 2px; white-space: normal; }
td.col-calidad { font-size: 12.5px; color: var(--text-2); min-width: 220px; max-width: 320px; white-space: normal; }
/* El precio no puede partirse en tres renglones cuando la columna de al lado
   (Calidad declarada) le gana el ancho (QA de AGR-430). */
td.firme-precio { white-space: nowrap; }
/* La condición comercial (vista/del día de la descarga) al lado de la
   expresión de «Cotizá HOY»: tenue, es una aclaración, no el dato principal. */
.pub-perfil-cond-pizarra { color: var(--text-3); }

/* ── Por grano (AGR-452): pestañas por producto en la ficha del comprador,
   CSS puro y sin JS. Los radios van fuera del form real (sin `name` de
   dato), ocultos con `.sr-only` — visibles al lector de pantalla y al
   teclado, invisibles a la vista — y la etiqueta hace de pestaña clickeable
   (mismo patrón que «¿Sos cliente?» en el diálogo de orden en firme, acá
   arriba). ─────────────────────────────────────────────────────────────── */
.grano-tabs { display: flex; flex-wrap: wrap; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 14px; }
.grano-tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; margin-bottom: -1px;
  font-size: 13.5px; font-weight: 500; color: var(--text-2);
  border-bottom: 2px solid transparent;
  cursor: pointer; white-space: nowrap;
}
.grano-tab:hover { color: var(--text); }
.grano-tab-vacio { font-size: 11px; color: var(--text-3); font-weight: 400; margin-left: 4px; }
/* La pestaña activa la decide el radio, no un `.is-active` que el server
   tendría que recalcular en cada request — acá no hay vista de por medio. */
.grano-tab-radio:checked + .grano-tab { color: var(--text); border-bottom-color: var(--text); }
.grano-tab-radio:focus-visible + .grano-tab { outline: 2px solid var(--brand); outline-offset: 1px; }

/* Un panel por grano, en el mismo orden que sus pestañas: el radio y el
   panel son hermanos de `.por-grano`, así que `:has()` los conecta sin
   pedirles compartir una `id`/`value` en común — nth-of-type cuenta cada
   tipo de hijo por separado (los radios entre las labels, los paneles
   adentro de `.grano-paneles`), así que el radio N-ésimo prende el panel
   N-ésimo pase lo que pase con cuántos productos haya. */
.grano-panel { display: none; }
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(1):checked) .grano-paneles .grano-panel:nth-of-type(1),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(2):checked) .grano-paneles .grano-panel:nth-of-type(2),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(3):checked) .grano-paneles .grano-panel:nth-of-type(3),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(4):checked) .grano-paneles .grano-panel:nth-of-type(4),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(5):checked) .grano-paneles .grano-panel:nth-of-type(5),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(6):checked) .grano-paneles .grano-panel:nth-of-type(6),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(7):checked) .grano-paneles .grano-panel:nth-of-type(7),
.por-grano:has(.grano-tabs .grano-tab-radio:nth-of-type(8):checked) .grano-paneles .grano-panel:nth-of-type(8) {
  display: block;
}
.grano-vacio {
  font-size: 13px; color: var(--text-2); background: var(--bg-sunken);
  border-radius: 8px; padding: 10px 12px; margin-bottom: 12px;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
/* El botón «Cargar X» es un `<label>` que apunta al primer campo del grano
   (la humedad): clickearlo enfoca el input sin JS, mismo patrón que
   cualquier `<label for>` del form. */
.grano-cargar-btn { flex-shrink: 0; }

/* El PH mínimo de maíz se completa solo según el grado elegido (AC de
   AGR-452): el template dibuja un `span[data-grado]` por cada par de
   `productos.calidad.PH_POR_GRADO` —el NÚMERO no se repite acá, sólo el
   identificador del grado («Grado 1»…), que hace falta para que `:has()`
   sepa cuál mostrar según qué `<option>` quedó marcada en el select de
   ESTE panel. */
.ph-valor { display: none; }
.grano-panel:has(select[name$="_grado_camara"] option[value="Grado 1"]:checked) .ph-valor[data-grado="Grado 1"],
.grano-panel:has(select[name$="_grado_camara"] option[value="Grado 2"]:checked) .ph-valor[data-grado="Grado 2"],
.grano-panel:has(select[name$="_grado_camara"] option[value="Grado 3"]:checked) .ph-valor[data-grado="Grado 3"] {
  display: inline;
}
.grano-panel:has(select[name$="_grado_camara"] option[value="Grado 1"]:checked) .ph-sin-grado,
.grano-panel:has(select[name$="_grado_camara"] option[value="Grado 2"]:checked) .ph-sin-grado,
.grano-panel:has(select[name$="_grado_camara"] option[value="Grado 3"]:checked) .ph-sin-grado,
.grano-panel:has(select[name$="_grado_camara"] option[value="Norma propia"]:checked) .ph-sin-grado {
  display: none;
}
/* Con norma propia no hay PH (AC de AGR-452): el detalle libre reemplaza al
   select fijo de arriba, oculto salvo que sea justo ese el grado elegido. */
.campo-detalle-norma { display: none; }
.grano-panel:has(select[name$="_grado_camara"] option[value="Norma propia"]:checked) .campo-detalle-norma {
  display: block;
}

/* Las bases de soja: tabla fija, chica, de sólo lectura — no es `.tabla-wrap`
   de una lista paginada, es la misma norma para todos los compradores. */
.tabla-bases-soja { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 4px; }
.tabla-bases-soja th, .tabla-bases-soja td { padding: 5px 8px; border-bottom: 1px solid var(--border); text-align: left; }
.tabla-bases-soja th { color: var(--text-2); font-weight: 500; }
.tabla-bases-soja td:not(:first-child) { color: var(--text-2); white-space: nowrap; }

/* El editor de la escalera de gluten del trigo (AGR-453, QA del 29/9): un
   renglón por escalón, «Desde [17] de gluten → $ [305000] /tn», el rango que
   resulta y la cruz. Los inputs son los de `.field`, con el ancho de la
   grilla y no el 100% de siempre. */
.escalera-ed { display: flex; flex-direction: column; gap: 6px; }
.escalera-fila {
  display: grid; align-items: center; gap: 6px;
  grid-template-columns: auto 72px auto auto auto minmax(110px, 160px) auto 1fr auto;
}
.escalera-fila .err { grid-column: 1 / -1; margin-top: 0; }
.field .escalera-fila label { display: inline; margin: 0; }
.escalera-k { font-size: 12.5px; color: var(--text-2); white-space: nowrap; font-weight: 400; }
.escalera-flecha { color: var(--text-3); padding-inline: 2px; }
.escalera-rango {
  font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums;
  padding-left: 6px; white-space: nowrap;
}
.escalera-quitar {
  width: 28px; height: 28px; border: 0; border-radius: var(--radius);
  background: transparent; color: var(--text-3); cursor: pointer;
  display: grid; place-items: center;
}
.escalera-quitar:hover { background: var(--bg-hover); color: var(--text); }
.escalera-quitar svg { width: 14px; height: 14px; stroke: currentColor; }
.escalera-mas { align-self: flex-start; margin-top: 8px; }
.escalera-mas svg { width: 14px; height: 14px; stroke: currentColor; }
@media (max-width: 560px) {
  /* Desde, su gluten, el precio y la cruz en un renglón; el rango abajo. */
  .escalera-fila { grid-template-columns: auto 64px minmax(0, 1fr) auto; grid-auto-flow: dense; }
  .escalera-flecha, .escalera-fila .escalera-k:not(:first-child) { display: none; }
  .escalera-rango { grid-column: 1 / -1; padding-left: 0; }
}

/* Los campos de secada sólo aparecen con «cobra secada» tildado (AC de
   AGR-452): mismo mecanismo que arriba, sobre el checkbox de este panel. */
.campo-secada { display: none; }
.grano-panel:has(input[name$="_cobra_secada"]:checked) .campo-secada { display: block; }

/* === AGR-454 === La oportunidad nueva arranca con la ficha del comprador.

   Lo precargado se ve con fondo tenue, el mismo criterio que la marca del
   campo leído en la carga (`input.leido`, más arriba): una sombra `inset`
   que no mueve geometría. Acá no se apaga con el foco — el rótulo «de la
   ficha» sigue diciendo de dónde vino hasta que el valor cambia, y ahí el
   navegador lo pasa a «cambiado», sin fondo (`ordenes/_marca_origen_js.html`). */
.field.de-ficha input:not([type="checkbox"]),
.field.de-ficha select,
.field.de-ficha .ts-control { box-shadow: inset 0 0 0 100px var(--brand-soft); }
.field-roles.de-ficha > div { background: var(--brand-soft); border-radius: var(--radius); padding: 6px 8px; }
.field .origen { font-weight: 400; font-size: 11px; color: var(--brand); margin-left: 6px; }
.field.cambiado .origen { color: #9a6700; }

/* «Sin bichos, sin olor y sin granos podridos»: una línea fija, no un campo. */
.bajada-recepcion {
  grid-column: 1 / -1; font-size: 13px; color: var(--text);
  background: var(--bg-sunken); border-radius: 7px; padding: 8px 10px;
}
.bajada-recepcion b { font-weight: 500; }
.bajada-recepcion span { color: var(--text-3); font-size: 11.5px; margin-left: 6px; }

/* Calidad por grano en la oportunidad, con el mismo mecanismo que la pestaña
   del grano en la ficha (AGR-452): el PH de maíz lo elige `:has()` según el
   grado marcado, el detalle de la norma aparece sólo con «Norma propia», y
   los puntos libres y el costo sólo con la secada tildada. Sirve igual en la
   edición, que no vuelve a pedir el form al server. */
.calidad-orden:has(select[name="grado_camara"] option[value="Grado 1"]:checked) .ph-valor[data-grado="Grado 1"],
.calidad-orden:has(select[name="grado_camara"] option[value="Grado 2"]:checked) .ph-valor[data-grado="Grado 2"],
.calidad-orden:has(select[name="grado_camara"] option[value="Grado 3"]:checked) .ph-valor[data-grado="Grado 3"] {
  display: inline;
}
.calidad-orden:has(select[name="grado_camara"] option[value="Grado 1"]:checked) .ph-sin-grado,
.calidad-orden:has(select[name="grado_camara"] option[value="Grado 2"]:checked) .ph-sin-grado,
.calidad-orden:has(select[name="grado_camara"] option[value="Grado 3"]:checked) .ph-sin-grado {
  display: none;
}
.calidad-orden:has(select[name="grado_camara"] option[value="Norma propia"]:checked) .campo-detalle-norma,
.calidad-orden:has(input[name="cobra_secada"]:checked) .campo-secada {
  display: block;
}

/* El borde rojo de un campo marcado por `static/js/validacion.js`. Va al final
   del archivo a propósito: gana por orden a los bordes de cada formulario
   (`.field input`, `.pub-dialog-firme-campo input`), que tienen su misma
   especificidad. */
input.is-invalido, select.is-invalido, textarea.is-invalido { border-color: var(--danger); }
