/* Nada Menos — compara tu carrito. Sistema de diseño según
   "Nada Menos/DESIGN.md". Solo tokens semánticos --nm-* (tokens.css): ningún
   hex suelto. Modo claro principal y oscuro de primera clase.

   Dos acentos con roles que no se cruzan:
   · tinta (--nm-primario) = todo control sólido: el botón primario es
     tinta maciza con texto hueso, y hueso con texto carbón en oscuro.
     Ningún botón es dorado.
   · maíz (--nm-acento) = el ÚNICO color de la página y UN SOLO oro por
     modo (v3.1), para todos sus usos —texto, nudos, foco—, y solo en: el
     logo, el monto del ahorro, el nudo y el monto ganador, el nudo focal
     del quipu-chart, el subrayado de la pestaña activa y el anillo de
     foco.
   Todo lo demás es acromático: la página lee 95% papel y tinta. */

/* Acá solo lo propio de esta web; los tokens de marca están en tokens.css.
   `--nm-cabecera` se mudó al interruptor de forma de tokens.css (109 px en
   forma web, 119 en la forma app): su valor depende de la forma, no de esta hoja, y
   con el alto acá el bloque de forma perdía por orden de fuente — que es
   exactamente el defecto que producía la banda donde la cabecera envolvía
   pero el token seguía declarando 108. */
:root {
  --nm-ancho: 1180px;      /* medida de la caja de contenido */
  /* Una columna sola no se estira hasta 1 180: a partir de 1 000 px las
     pestañas de una sola columna —Ofertas, Semana, Compras, Ajustes— se
     centran en 1 160 (PANTALLAS §2.2). El chasis de dos columnas sí usa
     `--nm-ancho` entero, porque ahí el ancho se reparte. */
  --nm-ancho-app: 1160px;
}

*, *::before, *::after { box-sizing: border-box; }

/* La cabecera es pegajosa: todo salto de scroll —`scrollIntoView` al
   generar los carritos, un ancla, el foco que entra a un campo fuera de
   pantalla— tiene que descontarla o el destino aterriza debajo de ella.
   Va en el documento y no en cada destino porque la regla es una sola.

   Abajo pasa lo mismo con el cromo de la forma app: la barra de pestañas y
   la barra de compromiso son fijas y tapan las últimas filas (el error
   visible en Fitia). Los dos términos valen 0 px en forma web, así que la
   cuenta es una sola para las dos formas y no hace falta preguntar. El
   `safe-area-inset-bottom` se suma UNA vez: la barra se apoya en las
   pestañas y las pestañas en el inset. */
html {
  scroll-padding-top: calc(var(--nm-cabecera) + var(--nm-esp-3));
  scroll-padding-bottom: calc(var(--nm-barra-tabs) + var(--nm-barra) + env(safe-area-inset-bottom));
}
/* El mismo motivo que el `scrollbar-gutter: stable` de `.carritos` más
   abajo, pero a nivel de página (Gise, revisión del 2026-09-28): abrir
   Buscar en la columna agrega resultados debajo de la barra sticky, la
   página cruza el alto de la ventana y el scrollbar VERTICAL del
   documento aparece — como la grilla de dos columnas se centra con
   `margin: auto`, ese scrollbar corre TODA la columna (y el campo
   adentro) unos px cada vez que el alto de la página cruza ese borde.
   Reservar el sitio siempre es la misma solución, un nivel más arriba —
   pero SOLO en el chasis par: puesto en `html` a secas le robaba 15 px a
   TODAS las pantallas, móvil incluido, y ahí rompía anchos medidos a
   pixel (R9 de Ofertas, `test_carritos_fix2_ui`). */
:root:not([data-forma="app"]):has([data-chasis="par"]) {
  scrollbar-gutter: stable;
}

/* Cinturón, no tirante: el ::before a sangre completa de .nm-banda y
   .nm-franja-indigo (tokens.css) ya no desborda —usa el truco
   `50% + margen -50vw`, exactamente 100vw— pero clip queda igual como
   respaldo ante cualquier otro desborde lateral futuro. `clip` y no
   `hidden`: clip no convierte al raíz en contenedor de scroll horizontal y
   el scroll vertical queda intacto (verificado en vivo el 2026-08-30:
   scrollTo instantáneo llega a donde se le pide con el clip puesto). */
html { scroll-behavior: smooth; overflow-x: clip; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* El home llena la pantalla: la página nunca termina a media altura
   dejando un vacío bajo el pie. El cuerpo es una columna de 100svh y el
   contenido empuja al pie hasta abajo — el aire es composición, no sobra. */
body {
  margin: 0;
  min-height: 100svh;
  display: flex; flex-direction: column;
  background: var(--nm-fondo);
  color: var(--nm-texto);
  font-family: var(--nm-cuerpo);
  font-size: var(--nm-b2);
  line-height: var(--nm-lh-cuerpo);
  -webkit-font-smoothing: antialiased;
  transition: background-color var(--nm-mov-entrada) var(--nm-curva-suave),
              color var(--nm-mov-entrada) var(--nm-curva-suave);
}

button, input, select, textarea { font-family: inherit; }

/* Táctil. Dos cosas que el navegador móvil hace por su cuenta y que acá
   estorban:

   - El doble toque para hacer zoom obliga a Safari y a Chrome a esperar unos
     300 ms antes de disparar el clic, por si venía un segundo toque. En una
     lista de compras eso son 300 ms en CADA chip, casilla y botón de
     cantidad. `touch-action: manipulation` renuncia al doble toque (el zoom
     por pellizco sigue intacto, y el <meta viewport> tampoco lo bloquea) y el
     clic sale al instante.

   - El realce de toque por omisión es un bloque gris o azul del sistema, que
     no es de la marca y que además desborda el radio de los chips. No se
     apaga del todo: sin ningún `:active` en toda la hoja, dejarlo
     transparente quitaría la única señal de que el dedo aterrizó. Se cambia
     por un velo del color de foco, que sí es nuestro. */
a, button, label, summary, input, select, textarea,
[role="button"], [role="option"], [role="tab"] {
  touch-action: manipulation;
  -webkit-tap-highlight-color: color-mix(in srgb, var(--nm-foco) 24%, transparent);
}

/* La escala .nm-d*, .nm-b*, .nm-l1 y el wordmark vienen de tokens.css */
.nm-d2 { text-wrap: balance; }

/* Cifras comparables en columna */
.nm-num, .tienda__precio, .tienda__unitario, .tienda__dif,
.alternativa__unitario, .alternativa__ahorro,
.carrito__total, .carrito__item-precio, .carrito__item-meta, .carrito__pie,
.item-fila__cantidad, .item-fila__unidad, .reparto__total-cifra, .reparto__cuenta,
.comparacion__meta, .lista__resumen-total, .ficha__identidad,
.qlabels, .lista__resumen { font-variant-numeric: tabular-nums; }

.sr-solo {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.saltar {
  position: absolute; left: var(--nm-esp-4); top: -60px; z-index: 100;
  background: var(--nm-superficie-1); color: var(--nm-texto);
  padding: var(--nm-esp-3) var(--nm-esp-4); border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-md); text-decoration: none;
  transition: top var(--nm-mov-base) var(--nm-curva-salida);
}
.saltar:focus { top: var(--nm-esp-4); }

:focus-visible {
  outline: 2px solid var(--nm-foco);
  outline-offset: 2px;
  border-radius: var(--nm-radio-sm);
}

/* ───────────── Cabecera (56 px) ─────────────

   El cromo del rediseño 2026 (PANTALLAS §1 regla 2): una sola fila de 56 px
   con el título grande a la izquierda y, a la derecha, el «···» de la
   pestaña y el avatar. El aspecto lo pone `.nm-cabecera` de
   `componentes.css`; acá va solo el layout: qué se ve en cada forma y el
   velo pegajoso, que es de esta pantalla y no del componente.

   Se fueron de acá el selector de tema (a Ajustes), la línea de estadísticas
   (a la portada y al pie) y el buscador (a su pantalla completa). */

.cabecera {
  position: sticky; top: 0; z-index: 40;
  background: var(--nm-velo-nav);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--nm-borde-sutil);
}
/* La cabecera es sticky y ocupa todo el ancho, así que en un teléfono con
   muesca —y sobre todo en horizontal— el título quedaba debajo del recorte
   de la pantalla. Los insets suman al padding en vez de reemplazarlo: en
   cualquier equipo sin muesca `env()` vale 0 y el aire es el de siempre.

   El alto es exacto y no mínimo: `--nm-cabecera` lo leen el
   `scroll-padding-top` del documento y el `top` de la columna del reparto, y
   una cabecera que mide dos píxeles más que su token deja cada ancla tapada
   (pasó una vez con 69 px de diferencia). */
.cabecera__interior {
  max-width: var(--nm-ancho); margin: 0 auto;
  /* Menos el filete: la caja pegajosa entera —el interior más el borde de
     abajo de `.cabecera`— tiene que medir `--nm-cabecera`, que es lo que el
     documento descuenta en `scroll-padding-top`. En forma app el filete lo
     lleva el interior (ver el bloque de forma) y ahí el alto es el completo. */
  height: calc(var(--nm-cabecera) - 1px); min-height: 0;
  padding: 0 calc(var(--nm-esp-5) + env(safe-area-inset-right))
           0 calc(var(--nm-esp-5) + env(safe-area-inset-left));
  display: flex; align-items: center; gap: var(--nm-esp-4);
}
/* El logo es un <a> al inicio: sin subrayado ni color de enlace — sigue
   siendo el wordmark, no un link de texto. */
.marca {
  display: flex; align-items: center; gap: 10px; flex: none;
  color: var(--nm-texto); text-decoration: none;
}
.marca__simbolo { width: 26px; height: 26px; flex: none; }
.marca__texto { font-size: 20px; }

/* El título de la vista. En forma web no se pinta: el lugar lo dicen el logo
   y la pestaña marcada, y repetirlo gastaría el ancho que el campo necesita. */
.cabecera__titulo { display: none; flex: 1; min-width: 0; }

/* «Tu lista» abre las listas guardadas (QA del rediseño 2026, R19), así que
   tiene que verse tocable sin dejar de ser el título: el cursor, el foco y un
   chevroncito de hoja. No es un botón relleno — el único primario de la
   pantalla está en otro lado (PANTALLAS §1 regla 1). El control de verdad es
   un `<button>` que `vistas.js` cuelga adentro del `<h1>` (auditoría de
   accesibilidad 2026-09-24: `role="button"` sobre el `<h1>` le borraba el
   encabezado), así que el aspecto tocable se resetea desde cero y el foco y
   el hover viven en el botón, no en el título entero. */
.cabecera__titulo--puerta { cursor: pointer; }
.cabecera__titulo--puerta::after {
  content: '⌄'; margin-left: var(--nm-esp-2);
  font-size: 0.7em; color: var(--nm-texto-3); vertical-align: 0.15em;
}
.cabecera__titulo__boton {
  background: none; border: none; padding: 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer; text-align: inherit;
}
.cabecera__titulo__boton:focus-visible {
  outline: 2px solid var(--nm-foco); outline-offset: 3px;
  border-radius: var(--nm-radio-sm);
}
@media (hover: hover) and (pointer: fine) {
  .cabecera__titulo--puerta:hover::after { color: var(--nm-texto-1); }
}

.cabecera__acciones { margin-left: auto; flex: none; display: flex; align-items: center; gap: var(--nm-esp-2); }
/* El correo no se escribe en el cromo de 56 px: la inicial es la señal de «estoy
   en una cuenta» y su `aria-label` dice el correo completo (pintarSesion). En
   1280 px la línea «tu cuenta de gonzalo@…» se salía de la ventana. */
.sesion__texto { display: none; }
.cabecera__mas {
  width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--nm-radio-full); background: transparent;
  color: var(--nm-texto-2); font-size: 20px; line-height: 1; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .cabecera__mas:hover { background: var(--nm-superficie-2); color: var(--nm-texto); }
}

/* El campo de la cabecera en forma web (Gonzalo, 2026-09-28: en TODAS las
   vistas, no solo donde había ancho de sobra — la Lista lo tenía colgado
   de la columna de Carritos, y lo quiere arriba, donde ya vive en las
   demás). Es un slot vacío: `vistas.js` (`actualizarBuscarBarra`) le
   cuelga DENTRO el `.buscar__barra` de verdad —el mismo `<form>` con `#q`
   que en forma app vive dentro de `#buscar`—, así que nunca hay un campo
   decorativo aparte diciendo lo mismo que el de verdad. */
/* `flex: 0 1 420px`, NUNCA `flex: 1`: con flex-grow el campo compite por el
   espacio que sueltan sus hermanos de la fila —la pestaña activa pierde su
   `font-weight: 600` en cuanto Buscar se abre y ninguna queda marcada
   (`mostrarVista`), y ese medio píxel que las pestañas dejan libre lo
   heredaba el campo, midiendo distinto abierto que cerrado (Gise,
   verificación del 2026-09-29: 828 px vs 829 px en `x`). Con flex-grow en
   cero el campo solo encoge si de verdad no entra 420px —nunca crece de
   más—, así que ninguna fluctuación de sus vecinos lo mueve. */
.cabecera__buscar { flex: 0 1 420px; min-width: 0; }
/* Colgado acá, el campo deja de medir su ancho por su contenido —el
   `max-width` + `margin: 0 auto` que lo centra solo en la pantalla
   completa de Buscar— y pasa a llenar el slot que le reserva la cabecera. */
.cabecera__buscar .buscar__barra { max-width: none; margin: 0; }
/* El campo NUNCA cambia de tamaño ni de sitio (Gise, revisión del
   2026-09-28, y de nuevo el 2026-09-29 al subir el campo a la cabecera):
   la ✕ y «Cerrar» entrando y saliendo del flujo corrían el campo o lo
   hacían medir distinto abierto que cerrado. Acá los dos reservan su sitio
   con `visibility` en vez de irse del flujo con `display: none`, así que
   la caja de `#q` (lo que miden las pruebas) es SIEMPRE la misma, escriba
   o no escriba, esté Buscar abierto o cerrado.

   La ✕ ya la decide `:placeholder-shown` en la hoja general (mira el valor
   real del campo); acá solo se le cambia CÓMO se esconde. «Cerrar» no
   tiene ninguna señal de CSS propia —vive en la cabecera, lejos de
   `#buscar`—, así que `vistas.js` (`actualizarBuscarBarra`) prende la
   clase `cabecera__buscar--abierto` en el propio slot cuando Buscar está
   abierto, la misma idea que `carritos--buscando` un poco más abajo. */
:root:not([data-forma="app"]) .cabecera__buscar .buscador__campo:placeholder-shown ~ .buscador__limpiar {
  display: flex; visibility: hidden; pointer-events: none;
}
.cabecera__buscar .buscar__cerrar { visibility: hidden; pointer-events: none; }
.cabecera__buscar--abierto .buscar__cerrar { visibility: visible; pointer-events: auto; }

/* ───────────── Tab bar y botón de Buscar ─────────────

   Cinco pestañas en pastilla más el botón circular aparte (PANTALLAS §2.1).
   En forma web la misma <nav> se queda en la cabecera junto al logo, plana y
   en una hilera; en forma app se despega y flota abajo. El subrayado ocre de
   la pestaña activa se retiró: el oro no marca navegación (§8). */
.tabs { flex: none; }
/* Plana en la cabecera: la pastilla es la cara de la forma app. */
.tabs__pastilla { height: auto; padding: 0; gap: var(--nm-esp-4); background: none; border: 0; box-shadow: none; }
.tab { flex-direction: row; gap: 7px; min-width: 0; padding: 8px 12px; font-size: var(--nm-b2); }
.tab .tab__icono { width: 18px; height: 18px; }
/* La activa se distingue en negrita (`componentes.css`,
   `.nm-tab[aria-current="page"]`), pero eso cambia el ANCHO del texto —y
   desde que el campo de Buscar vive en la MISMA fila que las pestañas
   (2026-09-29), la pestaña que pierde su negrita al abrir Buscar (ninguna
   queda marcada, `abrirBuscar`) corría el campo un píxel: medía distinto
   abierto que cerrado (Gise, verificación del 2026-09-29). Acá se pisa el
   `font-weight` de vuelta a 500 y la negrita se simula con `text-shadow`:
   mismo peso visual, cero cambio de métrica, porque una sombra no ocupa
   espacio en el flujo. */
.tab[aria-current="page"] {
  font-weight: 500; text-shadow: 0 0 0.35px currentColor, 0 0 0.35px currentColor;
}
/* El círculo de Buscar es de la forma app: en web el campo de arriba hace su
   trabajo y con ancho para escribir. */
.buscar-abrir { display: none; }

/* ───────────── Buscar, a pantalla completa ─────────────
   Ocupa toda la pantalla, hasta arriba, con el campo en la primera línea y
   nada de la lista detrás (PANTALLAS §3 P2). */
.buscar {
  position: fixed; inset: 0; z-index: 50;
  background: var(--nm-fondo); color: var(--nm-texto);
  overflow: auto; overscroll-behavior: contain;
  padding: calc(var(--nm-esp-3) + env(safe-area-inset-top))
           calc(var(--nm-esp-5) + env(safe-area-inset-right))
           calc(var(--nm-esp-8) + env(safe-area-inset-bottom))
           calc(var(--nm-esp-5) + env(safe-area-inset-left));
}
/* En forma web el campo vive en la cabecera —siempre visible, sticky,
   `z-index: 40`— y ya no viaja con esta pantalla: acá arranca DEBAJO de
   ella, no encima, así que el campo en el que la persona está mirando
   nunca queda tapado por sus propios resultados. En forma app, sin
   cabecera con campo, sigue siendo la pantalla completa de siempre. */
:root:not([data-forma="app"]) .buscar { top: var(--nm-cabecera); }
.buscar__barra {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  max-width: var(--nm-ancho); margin: 0 auto var(--nm-esp-4);
}
.buscar__barra .buscador__caja { flex: 1; min-width: 0; }
.buscar__cerrar { flex: none; }
.buscar > .panel-resultados, .buscar > .bienvenida, .buscar > .buscar__reemplazo {
  max-width: var(--nm-ancho); margin: 0 auto;
}
/* El encabezado de «reemplazar producto» (ver `buscador.js`,
   `abrirBuscarParaReemplazar`): una línea entre la barra y los resultados,
   sin caja ni fondo propio — es contexto, no una tarjeta más. */
.buscar__reemplazo {
  margin-top: calc(-1 * var(--nm-esp-2)); margin-bottom: var(--nm-esp-3);
  color: var(--nm-texto-2); font-size: var(--nm-b3);
}
.buscar__reemplazo strong { color: var(--nm-texto); font-weight: 600; }
.buscar-volver-arriba {
  position: fixed; z-index: 5;
  right: calc(var(--nm-esp-5) + env(safe-area-inset-right));
  bottom: calc(var(--nm-esp-5) + env(safe-area-inset-bottom));
}
.buscador__campo {
  flex: 1; min-width: 0; border: 0; background: transparent; color: var(--nm-texto);
  /* 16 px: ni un pixel menos, o iOS hace zoom al enfocar. */
  font: 400 16px / 1 var(--nm-cuerpo); outline: none;
}
.buscador__campo::placeholder { color: var(--nm-texto-3); }
.buscador__campo::-webkit-search-cancel-button { display: none; }
.buscador__lupa { color: var(--nm-texto-3); flex: none; }
.buscador__limpiar, .buscador__escanear { cursor: pointer; }
/* La ✕ ya no la apaga `hidden` desde JS (2026-09-28): la decide
   `:placeholder-shown`, que mira el valor real del campo sin que nadie
   tenga que sincronizarlo a mano. Fuera de la columna se comporta igual
   que antes (`display: none`, sin sitio reservado); la columna la
   reserva con `visibility` (ver «El campo nunca cambia de tamaño» más
   abajo), porque ahí SÍ importa que aparecer/desaparecer no mueva nada. */
.buscador__campo:placeholder-shown ~ .buscador__limpiar { display: none; }

/* ───────────── Las pestañas vacías del chasis ───────────── */
.carritos, .ofertas { padding-top: var(--nm-esp-4); }
.vista__titulo { margin: 0 0 var(--nm-esp-4); }

/* ───────────── Layout ───────────── */

/* Trampa de HTML que costó una portada desordenada y, después, un botón
   "Salir" que no se escondía: el atributo `hidden` equivale a
   `display: none` con la especificidad más baja que existe, así que
   cualquier `.clase { display: flex }` lo pisa y el elemento se queda en
   pantalla aunque el JS lo haya "ocultado".

   Va con `!important` y sin acotar a `.contenido`: acotarla dejó fuera la
   cabecera, donde `.sesion` y `.sesion__accion` traen display propio — y
   ahí "Salir" seguía visible en el modo de dueño fijo, que no tiene cookie
   que borrar, o sea un botón que no podía hacer nada. Un elemento marcado
   `hidden` no se muestra, y punto: si algo necesita mostrarse, se le quita
   el atributo. */
[hidden] { display: none !important; }

.contenido {
  flex: 1 0 auto;
  width: 100%; max-width: var(--nm-ancho); margin: 0 auto;
  padding: var(--nm-esp-6) var(--nm-esp-5) var(--nm-esp-8);
  display: flex; flex-direction: column;
}

.meta-linea {
  display: flex; flex-wrap: wrap; gap: var(--nm-esp-2);
  margin: 0; min-height: 1.4em;
  color: var(--nm-texto-3); font-size: var(--nm-b3);
}
.meta-linea b { color: var(--nm-texto-2); font-weight: 600; }
.meta-linea__sep { color: var(--nm-borde-fuerte); }

/* Tooltip de tiendas: el conteo de la cabecera lleva el detalle escondido
   detrás del hover/foco, no un enlace ni un modal, porque es un dato de
   contexto y no una acción. */
.tiendas-tip { position: relative; cursor: default; border-bottom: 1px dotted var(--nm-borde-fuerte); }
/* El botón que abre el detalle (`tiendasTip`, lista.js) es un `.enlace-boton`
   por el foco y el clic, pero no se lee como enlace: el subrayado de
   `.enlace-boton` sumado al punteado de `.tiendas-tip` daba dos líneas, y su
   tinta sacaba el conteo del tono de la meta-línea. Queda UNA marca —el
   punteado— y el color de quien lo lleva. Dos clases para ganarle a
   `.enlace-boton`, que vive más abajo en la hoja. */
.tiendas-tip > .tiendas-tip__boton { color: inherit; text-decoration: none; }
.tiendas-tip__lista {
  /* abre hacia abajo: hacia arriba queda debajo de la cabecera fija
     (z-index 40), que la tapa por completo al estar tan cerca del borde
     superior */
  position: absolute; z-index: 41; top: calc(100% + var(--nm-esp-2)); left: 0;
  /* width en vez de min-width: el shrink-to-fit del absoluto lo dejaría al
     ancho del span padre, pero min-width: max-content ganaba sobre max-width
     y con la lista larga el tooltip —aun oculto— ensanchaba el área de
     scroll: la portada entera quedaba paneable de lado en móvil */
  width: max-content; max-width: min(60ch, calc(100vw - var(--nm-esp-5) * 2));
  background: var(--nm-superficie-1); color: var(--nm-texto-2);
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-md);
  box-shadow: var(--nm-sombra-2); padding: var(--nm-esp-3) var(--nm-esp-4);
  font-size: var(--nm-b3); line-height: 1.5; white-space: normal;
  opacity: 0; visibility: hidden; transform: translateY(-2px);
  transition: opacity var(--nm-mov-micro) var(--nm-curva-salida),
              transform var(--nm-mov-micro) var(--nm-curva-salida);
}
.tiendas-tip:focus-within .tiendas-tip__lista { opacity: 1; visibility: visible; transform: translateY(0); }
@media (hover: hover) and (pointer: fine) {
  .tiendas-tip:hover .tiendas-tip__lista {   opacity: 1; visibility: visible; transform: translateY(0); }
}
/* En pantallas angostas el conteo cae a mitad de línea y un tooltip colgado
   de la palabra se sale del viewport por la derecha: se ancla a la línea de
   meta completa y ocupa su ancho. Las otras líneas de meta que llevan el
   tooltip («en N tiendas» de la fila elegida y del buscador) se anclan
   igual.

   Va en el umbral de forma y no en 640: a 645 px el tooltip desbordaba la
   página HOY (medido: `document.scrollWidth` 656 contra un `clientWidth` de
   645, con `.tiendas-tip__lista` llegando a 656 px de borde derecho). A 768
   ya no desborda, así que 767 es exactamente lo que hacía falta. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .meta-linea, :root:not([data-forma="web"]) .item-fila__unidad, :root:not([data-forma="web"]) .comparacion__meta { position: relative; }
  :root:not([data-forma="web"]) .tiendas-tip { position: static; }
  :root:not([data-forma="web"]) .tiendas-tip__lista { left: 0; right: 0; width: auto; }
}
:root[data-forma="app"] .meta-linea, :root[data-forma="app"] .item-fila__unidad, :root[data-forma="app"] .comparacion__meta { position: relative; }
:root[data-forma="app"] .tiendas-tip { position: static; }
:root[data-forma="app"] .tiendas-tip__lista { left: 0; right: 0; width: auto; }


/* ───────────── Bienvenida ───────────── */

/* El hero se estira hasta llenar lo que quede del viewport y reparte su
   aire entre titular y sugerencias, en vez de amontonarse arriba. El alto
   sale de la cadena flex (body 100svh → main → hero), no de un cálculo:
   restar a mano cabecera, metadatos y pie dejaba la página 89px larga y
   obligaba a un scroll que no debería existir. */
.bienvenida {
  flex: 1 0 auto;
  padding-top: var(--nm-esp-4);
  /* arriba, no centrado: centrado dejaba un hueco de ~90 px entre la línea
     de estado y la primera frase, como si algo no hubiera cargado */
  display: flex; flex-direction: column; justify-content: flex-start;
  gap: var(--nm-esp-7);
}

/* R34: la instrucción del primer producto, una sola vez y arriba de los
   chips — no es un tour ni un tooltip flotante, es texto del propio estado
   vacío. Mismo ritmo vertical que el resto de `.bienvenida` (el `gap` del
   padre ya separa esto de los chips de abajo). */
.bienvenida__instruccion { display: flex; flex-direction: column; gap: var(--nm-esp-2); }
.bienvenida__instruccion-titulo {
  font: 700 22px / 1.2 var(--nm-cuerpo); letter-spacing: -0.01em; margin: 0;
}
.bienvenida__instruccion-linea { margin: 0; color: var(--nm-texto-2); max-width: var(--nm-medida); }

.sugerencias { margin-top: 0; }
.sugerencias__intro { margin: 0 0 var(--nm-esp-4); color: var(--nm-texto-2); }
.sugerencias__lista { display: flex; flex-wrap: wrap; gap: var(--nm-esp-2); }
.sugerencia {
  padding: 7px var(--nm-esp-4);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde-tarjeta);
  border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2);
  font-size: var(--nm-b2);
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-base) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .sugerencia:hover {   border-color: var(--nm-borde-fuerte); color: var(--nm-texto); background: var(--nm-superficie-2); }
}

/* El chip con cifra: el nombre manda y el precio va detrás, en tinta plena
   y con cifras tabulares, para que la fila de chips se lea como una tabla
   de básicos y no como ocho botones con ruido al costado. El precio NO va
   en maíz: el oro de la app está reservado para el ahorro del optimizador
   —un chip no señala ninguna ganadora— y ocho aciertos de oro en pantalla
   le quitarían el suyo. */
.sugerencia--con-precio { display: inline-flex; align-items: baseline; gap: var(--nm-esp-2); }
.sugerencia__precio { color: var(--nm-texto); font-weight: 600; }
.sugerencia__unidad { color: var(--nm-texto-3); font-weight: 400; font-size: var(--nm-b3); }
/* «desde» no es número: va con el peso y el color del rótulo, no del precio */
.sugerencia__desde { color: var(--nm-texto-3); font-weight: 400; font-size: var(--nm-b3); }

/* ───────────── Ofertas · «Bajaron hoy» (PANTALLAS §3 P2b) ─────────────
   La pestaña del hábito diario, armada con `.nm-fila` de la hoja de
   componentes: foto, nombre con «Metro · antes S/ 23.50» debajo, y a la
   derecha el precio de hoy en tinta con la bajada debajo. El «+» de borde
   cierra la fila.

   La bajada «−S/ 11.80» va en el ORO DE TEXTO y el porcentaje va entre
   paréntesis al lado del precio de hoy, en la misma línea y en terciario
   («S/ 11.70 (−50 %)», pedido de Gonzalo del 2026-09-16): los dos colores
   salen de `.nm-fila__bajada` y de `.nm-fila__pct` en `componentes.css`,
   así que acá no se vuelve a decidir. Es la decisión de
   PANTALLAS §8 del 2026-09-15 —la bajada entra a la lista del maíz— y
   revierte lo que esta hoja decía hasta la fase 3, que la pintaba en tinta
   citando DESIGN.md:184. Lo que el oro marca sigue siendo lo mismo: el dato
   por el que la pantalla existe, que acá es cuánto menos cuesta y no cuánto
   cuesta.

   Sin tachado y sin badge rojo: ése es el vocabulario de las cadenas y
   convierte el dato en promoción. El precio de antes se dice con palabras,
   en la meta de la fila. */
/* El título grande vive en el cromo de 56 px (PANTALLAS §1 regla 2), así que
   el `<h2>` de la sección es el de la FORMA WEB, donde esa cabecera no pinta
   título. En app se esconde, igual que el de Carritos: sin esto «Bajaron hoy»
   se leía dos veces, una debajo de la otra. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .ofertas > .vista__titulo { display: none; }
}
:root[data-forma="app"] .ofertas > .vista__titulo { display: none; }

/* El tab bar flota sobre el final de la pantalla, igual que en Compras: sin
   este aire el botón «Ver N más» (encargo del 2026-09-23: `/api/ofertas`
   trae hasta ~24 por categoría, no las diez de la vitrina) quedaba tapado
   por la barra y no se podía tocar. */
.bajadas {
  padding-bottom: calc(var(--nm-esp-5) + var(--nm-barra-tabs) + env(safe-area-inset-bottom));
}
.bajadas__intro { margin: 0 0 var(--nm-esp-3); }
/* La tira de categorías se desplaza de lado, y el último chip quedaba
   cortado contra el borde («Cu…») sin ningún indicio de que se pudiera
   correr (QA M10). Dos arreglos: sangra hasta el borde de la pantalla y
   vuelve a entrar con su propio padding —así el chip cortado se ve como un
   chip que SIGUE, no como un chip roto— y cada chip se alinea al soltar. */
.bajadas__chips {
  margin: 0 0 var(--nm-esp-3);
  margin-inline: calc(var(--nm-esp-4) * -1);
  padding-inline: var(--nm-esp-4);
  scroll-snap-type: x proximity; scroll-padding-left: var(--nm-esp-4);
  overscroll-behavior-x: contain;
}
.bajadas__chips .nm-chip { scroll-snap-align: start; white-space: nowrap; }
/* Las categorías de Ofertas no se desplazan de lado: son pocas y entran en
   dos renglones. El carril cortaba el último chip contra el borde («Cuid…»)
   sin barra ni degradado que dijera que había más, y un degradado no alcanza
   porque los chips traen su propio fondo (QA M10). Envueltas, se ven todas y
   no hay nada que descubrir. */
#ofertas-chips {
  flex-wrap: wrap; overflow-x: visible;
  padding-right: var(--nm-esp-4); margin-right: 0;
  scroll-snap-type: none;
}
.bajadas__lista {
  list-style: none; margin: 0; padding: 0 var(--nm-esp-3);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde-tarjeta);
  border-radius: var(--nm-radio-lg);
}
/* La foto es puerta a la ficha, igual que el nombre, así que es un botón; lo
   que se ve sigue siendo la miniatura y nada más. */
.bajada__puerta {
  border: 0; padding: 0; background: transparent; cursor: pointer;
  flex: 0 0 auto; display: flex; border-radius: var(--nm-radio-md);
}
.bajada__mas { flex: 0 0 auto; margin-left: var(--nm-esp-2); }

/* El nombre del producto se lee en DOS renglones y no con puntos suspensivos.
   La hoja de componentes recorta a uno porque en la lista y en los resultados
   la fila tiene menos columnas; acá compiten la foto, el precio, la bajada
   con su porcentaje y el «+», y a 390 px «Agua Micelar Beauty Care Aroma
   Rosas 400 ml» quedaba en «Agua Micela…», que no identifica nada. Dos
   renglones dejan la fila en ~90 px, dentro de los 60–104 de PANTALLAS §1. */
.bajada .nm-fila__nombre {
  white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* A lo ancho del teléfono la fila se aprieta para que «antes S/ 32.90» entre
   entero: es la mitad del dato que la pantalla promete. Es una regla de FORMA
   y por eso va dos veces: con la guarda dentro de la media query, y con
   `data-forma="app"` afuera, para el cascarón de Capacitor en una tableta
   ancha o con puntero fino, donde la media query no aplica
   (`tests/test_forma_app.py`). */
@media (max-width: 767px) {
  :root:not([data-forma="web"]) .bajadas__lista { padding: 0 var(--nm-esp-2); }
  :root:not([data-forma="web"]) .bajada.nm-fila { gap: 10px; }
  :root:not([data-forma="web"]) .bajada .nm-fila__foto { width: 40px; height: 40px; }
  :root:not([data-forma="web"]) .bajada__mas { margin-left: var(--nm-esp-1); }
}
:root[data-forma="app"] .bajadas__lista { padding: 0 var(--nm-esp-2); }
:root[data-forma="app"] .bajada.nm-fila { gap: 10px; }
:root[data-forma="app"] .bajada .nm-fila__foto { width: 40px; height: 40px; }
:root[data-forma="app"] .bajada__mas { margin-left: var(--nm-esp-1); }
/* «S/ 11.70 (−50 %)» es una sola cosa y no se parte: el espacio entre la
   cifra y el porcentaje es tipográfico, así que lo sostiene la hoja y no un
   `&nbsp;` en el marcado, que además viajaría literal a las pruebas de Node. */
.bajada .nm-fila__precio,
.nm-fila__bajada { white-space: nowrap; }

/* SIN ORO DE "MEJOR OFERTA". Hasta el 2026-09-23 solo la primera fila —la
   de mayor bajada en soles— llevaba su cifra en oro, como si fuera la
   "mejor oferta" del día; con las filas ahora ordenadas por % de ahorro
   (encargo del dueño de producto) ese destaque de una sola card se veía
   todavía más raro, así que se quita: todas las filas se tratan igual, en
   tinta secundaria (`.nm-fila__pct`, en `componentes.css`, también es
   tinta terciaria en las tres, pareja). Ningún elemento de la lista de
   Ofertas queda en oro (QA M8: diez filas en ocre son una franja, no
   "unos pocos destellos"; y una sola fila en oro entre iguales es el
   destaque que se pidió quitar). */
.bajadas__lista .nm-fila__bajada { color: var(--nm-texto-2); }

/* Cargando: tres filas de esqueleto con la geometría de las de verdad, para
   que la pantalla no salte cuando lleguen (DESIGN.md, Components). */
.ofertas__esqueleto { display: grid; gap: 1px; }
.ofertas__esqueleto-fila {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  padding: var(--nm-esp-3) 0;
}
.ofertas__esqueleto-foto { width: 44px; height: 44px; flex: 0 0 auto; }
.ofertas__esqueleto-linea { height: 14px; flex: 1; }
.ofertas__esqueleto-cifra { width: 72px; height: 18px; flex: 0 0 auto; }

/* Arranque: la puerta de entrada a la compra de ejemplo —el producto
   explicándose solo—. Va sólido (hueso sobre índigo, vía el remapeo de la
   franja): como fantasma se perdía contra el fondo oscuro. La regla de un
   primario cuenta por franja: este es el de la portada y «Genera tus
   carritos» el de la banda de trabajo. */
.arranque {
  display: flex; flex-wrap: wrap; gap: var(--nm-esp-3);
  margin-top: var(--nm-esp-5);
}
/* El primario nace a lo ancho del panel de reparto; acá se ciñe al texto.
   Va con el contenedor delante porque .boton-primario aparece más abajo en
   la hoja y con la misma especificidad le ganaba el width: 100%. */
.arranque .arranque__boton { width: auto; margin-top: 0; padding: 11px var(--nm-esp-5); }
/* Aviso de la compra de ejemplo: la salida explícita para no dejar a nadie
   creyendo que esa lista es la suya. */
.demo-aviso {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--nm-esp-4);
}
.demo-aviso .boton-fantasma { flex: none; }

/* Aviso de la lista que salió del planificador: el mismo cuerpo que el de
   la demo, con sus dos salidas (volver a la semana, vaciar). */
.semana-aviso__acciones { display: flex; flex-wrap: wrap; align-items: center; gap: var(--nm-esp-3); }

/* ───────────── Mi semana (PANTALLAS §3 P5) ─────────────
   Planear la semana y volcarla a la lista. Lo que se ve al entrar es el
   nombre de la semana, el subtítulo tocable («4 personas · 69 platos») y la
   grilla 7×6: un nudo por plato, punteada cuando está vacía. La grilla es el
   vistazo —revisar la semana no puede ser abrir setenta platos— y lo que se
   edita es el detalle del día elegido, debajo, donde cada plato muestra para
   cuántos es.

   Cero oro en toda la pestaña (PANTALLAS §8, censo por pantalla): acá no hay
   ningún dato que se haya ganado todavía. Un solo primario, «Armar la
   lista», en la barra de compromiso. */

/* Una sola línea de identidad, no un bloque de título (QA G21): «Mi semana ▾
   · 4 personas ▾ · 0 platos». El título de la pestaña vive donde el de las
   otras cuatro, en el cromo de 56 px, y por eso ya NO se apaga acá. */
.semana__cabecera { padding: var(--nm-esp-2) 0 var(--nm-esp-3); }
/* El «···» de la semana, como el del reparto: vive en el cromo y se pinta
   solo con su pestaña delante. */
.semana__mas { display: none; }
body:has(#semana:not([hidden])) .cabecera__acciones > .semana__mas { display: flex; }
/* El nombre de la semana abre la hoja para cambiar de semana: se ve como
   texto fuerte y el «▾» dice que hay algo detrás. */
.semana__cambiar {
  display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
  padding: 0; background: none; border: 0; cursor: pointer;
  color: var(--nm-texto); font: 600 var(--nm-b2) / 1.3 var(--nm-cuerpo);
  text-align: left;
}
.semana__cambiar > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.semana__chevron { width: 18px; height: 18px; color: var(--nm-texto-3); }
.semana__sub {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin: 2px 0 0; color: var(--nm-texto-3);
}
.semana__sub-accion {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 0; background: none; border: 0; color: inherit; font: inherit; cursor: pointer;
}
.semana__sub-accion .semana__chevron { width: 15px; height: 15px; }
.semana__estado { margin: 0; min-height: 1.2em; color: var(--nm-texto-3); }
/* Las hileras de deshacer (ESPEC_UI §6bis): la de Mi semana (`menu.js`). */
.semana__deshacer { margin: var(--nm-esp-2) 0 0; }

.semana__modos { margin-bottom: var(--nm-esp-4); }

/* `.semana__franja` junta el segmentado (Planificador | Recetas) y la barra
   de compromiso en un solo padre (encargo 2026-09-23: los botones «Armar mi
   semana»/«Armar la lista» quedaban al pie de la grilla, fuera del primer
   viewport en escritorio). En forma app/móvil no hace nada —el segmentado
   sigue su flujo normal y `.semana-barra` se saca sola con `position: fixed`,
   como siempre—; en forma web pasa a fila y la barra se corre junto al
   segmentado, arriba. */
.semana__franja { margin-bottom: var(--nm-esp-4); }
.semana__franja .semana__modos { margin-bottom: 0; }
/* La negación exacta de la @media de abajo (767/768, con el mismo punto de
   corte que `tokens.css`): forma web es todo lo que NO es «forma app,
   forzada por el atributo» ni «angosto o de dedo». */
@media (min-width: 768px) and (not ((pointer: coarse) and (hover: none))) {
  :root:not([data-forma="app"]) .semana__franja {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--nm-esp-4); flex-wrap: wrap;
  }
  /* `.semana-barra` también lleva `nm-barra` (componentes.css): el fondo,
     el filete de arriba y el alto/padding fijos de una barra de acciones
     FIJA al pie de la pantalla, que es lo que sigue siendo en móvil y en
     la app. Acá ya no es fija —vive en la fila del segmentado—, así que se
     le apaga esa caja: si no, quedaba un recuadro propio alrededor de solo
     los dos botones, que ninguna otra pantalla web tiene (Carritos, la
     barra de Lista a dos columnas). Reportado por Gonzalo, 2026-09-23. */
  :root:not([data-forma="app"]) .semana-barra {
    margin-top: 0; background: transparent; border-top: 0; height: auto; padding: 0;
  }
  :root:not([data-forma="app"]) .semana-barra__resumen { display: none; }
}

/* La barra de compromiso de la semana. Es la suya y no la de la lista
   (`.barra-compromiso`, que enciende `html.con-barra` desde `lista.js` y
   habla del otro total): dos barras que se disputan el mismo borde son dos
   capas fijas de más. En forma app es fija sobre el tab bar; en forma web va
   en `.semana__franja`, junto al segmentado, y el resumen se apaga ahí
   porque `.semana__sub` de la cabecera ya dice cuántos platos tiene la
   semana — repetirlo a un palmo habría sido la misma cifra dos veces. */
.semana-barra {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--nm-esp-3); margin-top: var(--nm-esp-4);
}
.semana-barra__resumen { margin: 0; }
/* «Armar mi semana» y «Armar la lista», lado a lado: cuál de los dos es el
   único primario (de tinta) lo decide `menu.js` según el estado. */
.semana-barra__acciones { display: flex; align-items: center; gap: var(--nm-esp-2); flex: none; }
/* En angosto (teléfono y app) el resumen («17 platos · 4 adultos») y los dos
   botones ya no compiten por la misma fila: eso es lo que los apretaba en 3-4
   líneas ilegibles (reportado 2026-09-24). El resumen sube a su propia línea
   y los botones bajan a una fila propia, a todo lo ancho — el mismo criterio
   que ya usa la barra de Carritos (`.carritos__barra-resumen` en columna,
   junto a un solo botón). Por eso `.semana-barra` cambia de fila a columna
   solo acá, sin tocar la versión de escritorio (arriba, junto al
   segmentado) ni el texto que pinta `menu.js`. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .semana-barra {
    position: fixed; left: 0; right: 0;
    bottom: calc(var(--nm-barra-tabs) + env(safe-area-inset-bottom));
    z-index: 39; margin: 0; height: auto;
    flex-direction: column; align-items: stretch; justify-content: center;
    gap: var(--nm-esp-1);
    padding: var(--nm-esp-2) calc(var(--nm-esp-5) + env(safe-area-inset-right))
             calc(var(--nm-esp-2) + env(safe-area-inset-bottom)) calc(var(--nm-esp-5) + env(safe-area-inset-left));
  }
  :root:not([data-forma="web"]) .semana-barra__resumen {
    text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  :root:not([data-forma="web"]) .semana-barra__acciones { width: 100%; }
  :root:not([data-forma="web"]) .semana-barra__acciones .nm-boton { flex: 1 1 0; min-width: 0; padding-left: 12px; padding-right: 12px; }
  :root:not([data-forma="web"]) .planificador { padding-bottom: var(--nm-semana-barra-alto); }
  :root:not([data-forma="web"]) .nm-banda:has(.semana-barra) { z-index: 1; }
}
:root[data-forma="app"] .semana-barra {
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--nm-barra-tabs) + env(safe-area-inset-bottom));
  z-index: 39; margin: 0; height: auto;
  flex-direction: column; align-items: stretch; justify-content: center;
  gap: var(--nm-esp-1);
  padding: var(--nm-esp-2) calc(var(--nm-esp-5) + env(safe-area-inset-right))
           calc(var(--nm-esp-2) + env(safe-area-inset-bottom)) calc(var(--nm-esp-5) + env(safe-area-inset-left));
}
:root[data-forma="app"] .semana-barra__resumen {
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
:root[data-forma="app"] .semana-barra__acciones { width: 100%; }
:root[data-forma="app"] .semana-barra__acciones .nm-boton { flex: 1 1 0; min-width: 0; padding-left: 12px; padding-right: 12px; }
:root[data-forma="app"] .planificador { padding-bottom: var(--nm-semana-barra-alto); }
:root[data-forma="app"] .nm-banda:has(.semana-barra) { z-index: 1; }

/* ----- la grilla ----- */

/* El componente es `.nm-grilla` (componentes.css); acá va solo el ancho de
   la columna de rótulos, que en el teléfono se achica para que los siete
   días entren sin apretarse. Las celdas miden 40 px y llevan nudos, no
   nombres: por eso los siete días caben en 390 px y la grilla ya no se
   desliza de costado. */
.semana-grilla { margin-bottom: var(--nm-esp-5); }
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .semana-grilla .nm-grilla { --nm-grilla-rotulo: 74px; }
}
:root[data-forma="app"] .semana-grilla .nm-grilla { --nm-grilla-rotulo: 74px; }

/* ----- el detalle del día ----- */

.dia__titulo {
  display: flex; align-items: baseline; gap: var(--nm-esp-2);
  margin: 0 0 var(--nm-esp-2); font-size: var(--nm-d4); font-weight: 700;
  letter-spacing: var(--nm-d4-tracking); color: var(--nm-texto);
}
.dia__fila {
  display: flex; align-items: flex-start; gap: var(--nm-esp-3);
  padding: var(--nm-esp-3) 0; border-bottom: 1px solid var(--nm-borde-sutil);
  min-height: 60px;
}
.dia__fila:last-child { border-bottom: 0; }
/* El rótulo del momento es el botón de plegar: toda la fila del rótulo, con
   la flecha delante, para que el affordance no dependa de un chevron suelto
   (QA G-R7b). Sin fondo ni borde: sigue leyéndose como rótulo. */
.dia__momento {
  flex: 0 0 84px; padding: 6px 0 0; text-align: left;
  display: flex; align-items: baseline; gap: 4px;
  background: transparent; border: 0; cursor: pointer;
  font: inherit; font-size: var(--nm-b3); line-height: 1.2; color: var(--nm-texto-3);
}
.dia__fila--plegada { min-height: 0; align-items: baseline; }
.dia__fila--plegada .dia__platos { padding-top: 6px; }
.semana-plegar__flecha { color: var(--nm-texto-3); }
/* Lo que queda de una fila plegada en la grilla: cuántos platos tiene ese
   momento en la semana, ocupando las siete columnas de los días. */
/* El rótulo de la grilla también es botón desde que se puede plegar; el
   componente lo define como celda de texto, así que acá va el reset. */
.semana-grilla .nm-grilla__momento {
  gap: 4px; text-align: left; font: inherit; font-size: var(--nm-b3);
  color: var(--nm-texto-2); background: transparent; border: 0; cursor: pointer;
}
.semana-grilla__plegada {
  grid-column: span 7; display: flex; align-items: center;
  height: 24px; font-size: var(--nm-b3); color: var(--nm-texto-3);
}
.nm-grilla__momento--plegado, .dia__momento--plegado { color: var(--nm-texto-2); }
@media (hover: hover) and (pointer: fine) {
  .nm-grilla__momento:hover, .dia__momento:hover { color: var(--nm-texto); }
}
.dia__platos { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.dia__fila--fuera .dia__platos { color: var(--nm-texto-3); }
.dia__fuera-marca {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--nm-b3); color: var(--nm-texto-3);
}
.dia__fuera-marca .nm-ico { width: 15px; height: 15px; }
.dia__poner {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 32px; height: 32px; margin-top: 2px;
  background: transparent; border: 1px dashed var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); cursor: pointer;
}
.dia__poner .nm-ico { width: 16px; height: 16px; }
@media (hover: hover) and (pointer: fine) {
  .dia__poner:hover { border-color: var(--nm-texto); color: var(--nm-texto); }
}

/* Un plato del día: el nombre es el protagonista, «×4» va siempre a la
   vista (pedido de Gonzalo: revisar la semana no puede ser abrir setenta
   platos) y «Quitar» lleva ícono, que es como se encuentra una acción. */
.plato {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  min-height: 32px; color: var(--nm-texto);
}
.plato__nombre {
  /* `flex-basis: 0` y no `auto` (reportado 2026-09-24, ancho junto a la
     barra de Mi semana): en un contenedor `flex-wrap` como el de abajo, un
     `flex-basis: auto` hace que el navegador decida si el renglón alcanza
     usando el ancho MÁXIMO del texto sin partir — con un nombre largo eso
     ya no entraba junto a «cambiar», y empujaba «quitar» solo a una segunda
     línea. Con `0` el nombre entra al cálculo del renglón como si no
     pidiera nada y crece después con lo que sobra, así que «cambiar» y
     «quitar» se quedan siempre juntos en la primera línea y es el nombre
     el que, si hace falta, parte en dos. En escritorio (una sola línea, sin
     wrap) el resultado es idéntico: sigue siendo el único elástico de la
     fila. */
  flex: 1 1 0; min-width: 0; text-align: left;
  padding: 0; background: none; border: 0; color: inherit;
  font: 500 var(--nm-b2) / 1.3 var(--nm-cuerpo); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .plato__nombre:hover { text-decoration: underline; }
}
/* atenuado cuando es el número de la casa; en tinta y con borde cuando es
   una excepción («lonchera para 2»), porque es lo que no hay que perder */
.plato__personas {
  flex: none; padding: 1px 6px;
  border: 1px solid transparent; border-radius: var(--nm-radio-full);
  color: var(--nm-texto-3); font-size: var(--nm-b3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.plato__personas--propia { color: var(--nm-texto); border-color: var(--nm-texto); font-weight: 600; }
/* «Cambiar»: el mismo tamaño y trato que «Quitar», con su propio ícono
   (Gonzalo, 2026-09-23, «Armar mi semana» §7bis) — una acción que la
   persona tiene que encontrar lleva ícono reconocible, no una palabra en
   texto terciario. Llama a `/cambiar` sobre ESE plato puntual y nunca pisa
   los demás de la casilla. */
.plato__cambiar {
  flex: none; display: grid; place-items: center; width: 28px; height: 28px;
  background: transparent; border: 0; border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); cursor: pointer;
}
.plato__cambiar .nm-ico { width: 16px; height: 16px; }
@media (hover: hover) and (pointer: fine) {
  .plato__cambiar:hover { background: var(--nm-superficie-2); color: var(--nm-texto); }
}
.plato__quitar {
  flex: none; display: grid; place-items: center; width: 28px; height: 28px;
  background: transparent; border: 0; border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); cursor: pointer;
}
.plato__quitar .nm-ico { width: 17px; height: 17px; }
@media (hover: hover) and (pointer: fine) {
  .plato__quitar:hover { background: var(--nm-superficie-2); color: var(--nm-texto); }
}
/* En palabras, nunca abreviado (Gonzalo, 2026-09-23: «4A» o «2A · 1N» era
   críptico): «2 adultos · 1 niño» entra holgado en escritorio, en la misma
   fila. En una pantalla angosta puede no entrar junto al nombre y los dos
   íconos — ahí se manda a su propia línea completa, debajo del nombre, en
   vez de apretarlo o volver a abreviar. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .plato { flex-wrap: wrap; }
  :root:not([data-forma="web"]) .plato__nombre { order: 1; }
  :root:not([data-forma="web"]) .plato__cambiar { order: 2; }
  :root:not([data-forma="web"]) .plato__quitar { order: 3; }
  :root:not([data-forma="web"]) .plato__personas { order: 4; flex: 1 1 100%; margin-top: 2px; }
}
:root[data-forma="app"] .plato { flex-wrap: wrap; }
:root[data-forma="app"] .plato__nombre { order: 1; }
:root[data-forma="app"] .plato__cambiar { order: 2; }
:root[data-forma="app"] .plato__quitar { order: 3; }
:root[data-forma="app"] .plato__personas { order: 4; flex: 1 1 100%; margin-top: 2px; }

/* ----- el estado vacío ----- */

/* La grilla punteada arriba, la instrucción debajo y la línea punteada que
   señala la primera celda (Cooklist `oth_gv0of`). Sin párrafo. */
.semana-vacia { position: relative; padding-bottom: var(--nm-esp-6); }
.semana-vacia__texto { text-align: center; padding: var(--nm-esp-4) 0 0; }
.semana-vacia__hilo {
  display: block; width: 120px; height: 64px; margin: var(--nm-esp-2) auto 0;
  color: var(--nm-borde-fuerte);
}
/* «Armar mi semana» en el estado vacío: el atajo de un toque, antes de las
   semanas modelo y «Desde cero» (Gonzalo, 2026-09-23, §7bis). Si todavía no
   hay semana, `menu.js` la crea y la guarda primero. */
.semana-vacia__cta { margin-top: var(--nm-esp-5); }
.semana-vacia__modelos { margin-top: var(--nm-esp-4); }

/* estado vacío del planificador: dos semanas modelo para empezar */
.modelos { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--nm-esp-3); }
.modelo {
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  padding: var(--nm-esp-4);
  background: var(--nm-superficie-1); border: 1px solid var(--nm-borde-tarjeta); border-radius: var(--nm-radio-lg);
  color: var(--nm-texto-2); text-align: left; font: inherit; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .modelo:hover { border-color: var(--nm-borde-fuerte); }
}
.modelo strong { color: var(--nm-texto); font-size: var(--nm-b1); font-weight: 700; }
.modelo span { font-size: var(--nm-b3); color: var(--nm-texto-3); }

/* ----- las hojas de la semana ----- */

.semana-hoja__filas { display: flex; flex-direction: column; }
.semana-item {
  display: flex; align-items: center; gap: var(--nm-esp-3); width: 100%;
  padding: var(--nm-esp-3) 0; border: 0; border-bottom: 1px solid var(--nm-borde-sutil);
  background: transparent; color: var(--nm-texto); font: inherit; text-align: left; cursor: pointer;
}
.semana-item:last-child { border-bottom: 0; }
.semana-item b { font-weight: 600; }
.semana-item__texto { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.semana-item[aria-current="true"] b { text-decoration: underline; text-underline-offset: 4px; }
.semana-hoja__rotulo { margin: var(--nm-esp-4) 0 4px; }

.personas__ayuda { margin: 0 0 var(--nm-esp-3); color: var(--nm-texto-3); }
/* Adultos y niños en las mismas filas del onboarding —es el mismo hogar— y
   el total debajo, que es el número con el que se cocina (QA R20). */
.personas__total { margin: var(--nm-esp-3) 0 0; text-align: center; }
.personas__stepper { display: flex; margin: 0 auto; }

.picker__puestos { display: flex; flex-direction: column; gap: 4px; margin-bottom: var(--nm-esp-3); }
.picker__caminos { margin-bottom: var(--nm-esp-3); }
/* La casilla nativa se pinta con el color del sistema —en oscuro sale un
   cuadrito mostaza, que es justo el oro que esta pestaña no tiene—: va a tinta. */
.picker__todas { display: flex; align-items: center; gap: var(--nm-esp-2); margin: var(--nm-esp-3) 0; color: var(--nm-texto-3); }
.picker__todas input { accent-color: var(--nm-texto); }
.picker__lista { max-height: 44vh; overflow: auto; border-top: 1px solid var(--nm-borde); }
.picker__item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--nm-esp-3);
  width: 100%; padding: var(--nm-esp-3) var(--nm-esp-2);
  background: transparent; border: 0; border-bottom: 1px solid var(--nm-borde-sutil);
  color: var(--nm-texto); font: inherit; text-align: left; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .picker__item:hover { background: var(--nm-superficie-2); }
}
.picker__item small { color: var(--nm-texto-3); font-size: var(--nm-b3); white-space: nowrap; }
.picker__pie { margin: var(--nm-esp-3) 0 0; color: var(--nm-texto-3); }
.picker__fuera { margin-top: var(--nm-esp-4); }
.picker__fuera .nm-boton[aria-pressed="true"] { border-color: var(--nm-texto); color: var(--nm-texto); }

.plato-hoja { margin: 0; }
.plato-hoja__fila {
  display: flex; align-items: center; justify-content: space-between; gap: var(--nm-esp-3);
  padding: var(--nm-esp-3) 0; border-bottom: 1px solid var(--nm-borde-sutil);
}
/* Los chips de día en su propia fila, DEBAJO del rótulo «Días»: con «Días»
   al lado (como «Momento» y «Personas») le robaba ~50px al ancho y los
   siete no entraban en 390px —la «D» sola caía a una segunda fila (Gonzalo,
   2026-09-23, con captura)—. El rótulo arriba le deja el ancho completo a
   `#plato-dias`, donde los siete (36px de alto, el área táctil no baja de
   40px con el `::before` de más abajo) sí entran en una fila. */
.plato-hoja__fila--dias { flex-direction: column; align-items: flex-start; gap: var(--nm-esp-2); flex-wrap: wrap; }
.plato-hoja__fila--dias .plato-hoja__valor { width: 100%; }
/* Adultos y niños por separado (2026-09-23) ya no entran en la fila plana
   de «Momento»: dos filas de stepper apiladas, mismo motivo que «Días»
   arriba — el rótulo se sube y el valor se estira a lo ancho. */
.plato-hoja__fila--personas { flex-direction: column; align-items: flex-start; gap: var(--nm-esp-2); }
.plato-hoja__fila--personas .plato-hoja__valor { width: 100%; }
.plato-hoja__fila--personas .personas__total { margin-top: var(--nm-esp-2); text-align: left; }
.plato-hoja dt { margin: 0; }
.plato-hoja__valor { margin: 0; color: var(--nm-texto); font-weight: 600; }
.plato-hoja__valor .nm-chip { font-weight: 500; }
/* El momento es de la CASILLA, no de la receta: un `<select>` nativo —sin
   modal encima del panel— que hereda el mismo look que el resto de «Poner
   en la semana» (CLAUDE.md: nada de hex sueltos, solo tokens `--nm-*`). */
.plato-hoja__momento {
  border: 1px solid transparent; border-radius: var(--nm-radio-md); background: transparent;
  color: var(--nm-texto); font: inherit; font-weight: 600; text-align: right;
  padding: 4px 6px; cursor: pointer; max-width: 60vw;
}
@media (hover: hover) and (pointer: fine) {
  .plato-hoja__momento:hover { border-color: var(--nm-borde); }
}
/* En oscuro el desplegable nativo hereda el fondo de la página si no se le
   dice otra cosa (`.orden__select`, arriba): mismo arreglo acá. */
.plato-hoja__momento option { background: var(--nm-campo); color: var(--nm-texto); }

/* La intro de una pantalla, la primera vez que se entra (Cooklist
   `oth_astdx`): un ícono lineal, el título, una línea y «Empezar». */
.semana-intro {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--nm-esp-3); width: min(92vw, 22rem); padding: var(--nm-esp-6) var(--nm-esp-5);
}
.semana-intro { position: relative; }
/* La X, dentro del modal y sobre su propio borde (QA G22). */
.semana-intro__cerrar {
  position: absolute; top: var(--nm-esp-3); right: var(--nm-esp-3);
}
.semana-intro__ico { width: 40px; height: 40px; color: var(--nm-texto-2); }
.semana-intro h2 { margin: 0; }
.semana-intro p { margin: 0; }


/* ----- recetario ----- */

/* El buscador chico, el «+» circular de «Nueva receta» y los chips por
   momento en un carril: el primario de la pantalla es «Armar la lista», así
   que crear una receta es un circular y no un botón relleno más. */
.recetario__cabecera {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--nm-esp-3);
  align-items: center; margin-bottom: var(--nm-esp-4);
}
.recetario__buscar { min-width: 0; }
.recetario__filtros { grid-column: 1 / -1; }
.recetario__nueva { justify-self: end; }

/* Filas compactas, no tarjetas: el nombre es la puerta a la receta y el
   «+» la pone en la semana. */
.recetas { display: flex; flex-direction: column; }
.receta {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  padding: var(--nm-esp-3) 0; border-bottom: 1px solid var(--nm-borde-sutil);
  min-height: 60px;
}
.receta:last-child { border-bottom: 0; }
.receta__ico { display: flex; color: var(--nm-texto-3); }
.receta__cuerpo { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.receta__nombre {
  padding: 0; background: none; border: 0; color: var(--nm-texto); text-align: left;
  font: 600 var(--nm-b1) / 1.25 var(--nm-cuerpo); cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .receta__nombre:hover { text-decoration: underline; }
}
.receta__poner {
  flex: 0 0 auto; display: grid; place-items: center; width: 36px; height: 36px;
  background: transparent; border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .receta__poner:hover { border-color: var(--nm-texto); background: var(--nm-superficie-2); }
}

/* ----- el buscador de recetas: estantes (variante A) y lista densa (B) -----

   La maqueta «estantes» de Gonzalo (2026-09-22, variante A aprobada,
   `rm/maqueta.html`): sin buscar ni filtrar, la banda es estantes
   horizontales con foto; buscando o con un filtro de momento puesto, cambia
   a lista densa. Sin costo —ni «Más baratas hoy», ni un monto por plato—:
   Gonzalo lo dejó en el backlog el mismo día. Los tokens son solo `--nm-*`
   (DESIGN.md). */

/* la foto: placeholder digno con los mismos anillos que usaba la maqueta,
   nunca un hueco gris. Cuadrada en el estante y en la fila; 16:9 en la
   ficha de detalle (`.receta-hoja__foto`, más abajo). */
.receta-foto {
  position: relative; display: block; aspect-ratio: 1 / 1; overflow: hidden;
  border-radius: var(--nm-radio-lg); background: var(--nm-fondo);
  border: 1px solid var(--nm-borde-sutil);
}
.receta-foto img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* En el estante y en la fila densa, la foto ES la puerta a la ficha —el
   mismo `data-ver` que el nombre, `tabindex="-1"` y `aria-hidden` en el
   marcado (menu.js) para no sumar una parada de tabulación—, así que acá
   solo se le borra el vestido de `<button>` nativo y se le pone la mano. */
button.receta-foto {
  padding: 0; margin: 0; font: inherit; color: inherit; text-align: inherit;
  cursor: pointer; appearance: none; -webkit-appearance: none;
}
.receta-foto--vacia {
  display: flex; align-items: center; justify-content: center;
  color: var(--nm-texto-3);
}
.receta-foto--vacia .nm-ico { width: 32%; height: 32%; }

/* ---- estantes ---- */
.receta-estantes { display: flex; flex-direction: column; }
.receta-estante { margin-bottom: var(--nm-esp-5); }
.receta-estante:last-child { margin-bottom: 0; }
.receta-estante__cab {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: var(--nm-esp-2);
}
.receta-estante__titulo { margin: 0; font: 700 17px/1.2 var(--nm-cuerpo); letter-spacing: -0.01em; }
.receta-estante__vertodas {
  padding: var(--nm-esp-1) 0; background: none; border: 0; cursor: pointer;
  font-size: var(--nm-b3); font-weight: 600; color: var(--nm-texto-2);
}
@media (hover: hover) and (pointer: fine) {
  .receta-estante__vertodas:hover { color: var(--nm-texto); }
}
.receta-estante__carril {
  display: flex; gap: var(--nm-esp-3); overflow-x: auto; scrollbar-width: none;
  margin: 0 calc(var(--nm-esp-4) * -1); padding: 0 var(--nm-esp-4) var(--nm-esp-1);
  scroll-snap-type: x mandatory; scroll-padding: 0 var(--nm-esp-4);
}
.receta-estante__carril::-webkit-scrollbar { display: none; }
.receta-tarjeta { flex: 0 0 112px; scroll-snap-align: start; position: relative; }
.receta-tarjeta__foto { width: 100%; }
.receta-tarjeta__mas {
  position: absolute; top: 6px; right: 6px; z-index: 1;
  width: 32px; height: 32px; border-radius: var(--nm-radio-full);
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--nm-borde); background: var(--nm-superficie-2);
  color: var(--nm-texto); cursor: pointer; box-shadow: var(--nm-sombra-1);
}
.receta-tarjeta__mas .nm-ico { width: 16px; height: 16px; }
.receta-tarjeta__nombre {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; margin: var(--nm-esp-2) 0 2px; min-height: 35px;
  font: 600 14px/1.25 var(--nm-cuerpo); background: none; border: 0; padding: 0;
  text-align: left; cursor: pointer; color: var(--nm-texto);
}
@media (hover: hover) and (pointer: fine) {
  .receta-tarjeta__nombre:hover { text-decoration: underline; }
}
.receta-tarjeta__meta { display: block; font-size: 12px; color: var(--nm-texto-3); }

/* ---- lista densa ---- */
.receta-lista__volver { margin: 0 0 var(--nm-esp-3); }
.receta-lista__cuenta { margin: 0 0 var(--nm-esp-2); }
.receta-lista { list-style: none; margin: 0; padding: 0; }
.receta-fila {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  padding: var(--nm-esp-2) 0; border-bottom: 1px solid var(--nm-borde-sutil);
}
.receta-fila:last-child { border-bottom: 0; }
.receta-fila__foto { flex: 0 0 auto; width: 48px; border-radius: var(--nm-radio-md); }
.receta-fila__cuerpo { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.receta-fila__nombre {
  padding: 0; background: none; border: 0; color: var(--nm-texto); text-align: left;
  font: 600 var(--nm-b1) / 1.25 var(--nm-cuerpo); cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .receta-fila__nombre:hover { text-decoration: underline; }
}
.receta-fila__meta b { font-weight: 600; color: var(--nm-texto); }
.receta-fila__mas {
  flex: 0 0 auto; display: grid; place-items: center; width: 36px; height: 36px;
  background: transparent; border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto); cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .receta-fila__mas:hover { border-color: var(--nm-texto); background: var(--nm-superficie-2); }
}

/* ---- el «+»: puntito en los días que ya tienen algo puesto ---- */
/* El chip mide 36px a la vista (el tamaño de siempre, `.nm-chip`) pero el
   dedo lo alcanza en 40px: el `::before` completa el área táctil sin
   engordar la fila (mismo truco que `.plato__quitar`, arriba). No se achica
   el chip para que los siete entren en 390px — eso lo resuelve
   `.plato-hoja__fila--dias` dándole el ancho completo, no reducir el
   tamaño del blanco. */
#plato-dias .nm-chip { position: relative; }
#plato-dias .nm-chip::before { content: ''; position: absolute; inset: -2px; }
#plato-dias .nm-chip--ocupado::after {
  content: ''; position: absolute; left: 50%; bottom: -5px;
  width: 4px; height: 4px; margin-left: -2px; border-radius: 50%;
  background: var(--nm-texto-3);
}

/* ---- la ficha de detalle (cara «alta», como la ficha de producto) -----

   Vive DENTRO de `.hoja__cuerpo`, que ya trae su padding horizontal
   (`componentes.css`): la foto no sangra al borde —como la ficha de
   producto, cuya foto es un cuadrado de 168 px centrado y no un borde a
   borde— para no tocar ninguna clase compartida (`.hoja__rotulo` la usan
   todas las hojas de la app). */
.receta-hoja__foto { width: 100%; aspect-ratio: 16 / 9; margin-top: var(--nm-esp-2); }
.receta-hoja__cuerpo { padding-top: var(--nm-esp-4); }
.receta-hoja__nombre {
  margin: 0; font: 700 var(--nm-d3) / 1.15 var(--nm-cuerpo); letter-spacing: var(--nm-d3-tracking);
  text-wrap: balance;
}
.receta-hoja__meta { margin: var(--nm-esp-1) 0 0; }
.receta-hoja__porciones {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--nm-esp-2);
  margin-top: var(--nm-esp-3); padding-top: var(--nm-esp-3); border-top: 1px solid var(--nm-borde-sutil);
}
/* Adultos y niños por separado (2026-09-23): dos steppers chicos con su
   rótulo pegado, en vez de uno solo que sumaba y no decía quiénes eran. */
.receta-hoja__personas { display: flex; align-items: center; gap: var(--nm-esp-1); flex-wrap: wrap; }
.receta-hoja__ingredientes, .receta-hoja__pasos { margin: 0 0 var(--nm-esp-4); }
.receta-hoja__ingredientes {
  columns: 2; column-gap: var(--nm-esp-4); font-size: var(--nm-b3); color: var(--nm-texto-2); list-style: none; padding: 0;
}
.receta-hoja__ingredientes li { padding: 2px 0; break-inside: avoid; }
.receta-hoja__pasos { color: var(--nm-texto-2); font-size: var(--nm-b3); line-height: var(--nm-lh-cuerpo); padding-left: 1.1em; }
.receta-hoja__pasos li { padding: 2px 0; }
.receta-hoja__enlace { margin: 0 0 var(--nm-esp-6); }

/* ----- diálogos (el perfil) y las capas de la semana ----- */
.dialogo {
  width: min(92vw, 34rem); max-height: 88vh;
  padding: var(--nm-esp-5);
  background: var(--nm-superficie-1); color: var(--nm-texto);
  border: 1px solid var(--nm-borde-tarjeta); border-radius: 16px;
  /* la sombra la declara el bloque del velo, más abajo: es la misma
     propiedad y con dos fuentes se pierde una de las dos capas */
}
/* Picker, «para cuántos» y editor de recetas viven bajo la hoja
   (`menu.js`, ESPEC_UI §6 #33). Cerrados, esperan en `#semana-hojas`, que
   está siempre oculto. El editor no usa la cabecera de la hoja —trae la
   suya—, así que le devuelve el aire de arriba que esa cabecera daba. Y es
   un formulario de dos columnas de campos: en la forma web la hoja se abre
   al ancho que tenía el diálogo; en la app `--nm-hoja-ancho` ya es el ancho
   entero y `max()` lo deja así. */
.semana-hoja--editor { padding-top: var(--nm-esp-5); }
.hoja__panel:has(.semana-hoja--editor) { width: max(var(--nm-hoja-ancho), min(94vw, 44rem)); }

/* ───────────── El velo de las capas ─────────────
   El velo NO lo pinta `::backdrop`: lo pinta la propia capa, y el
   `::backdrop` queda transparente.

   Esto refuta por escrito el comentario de `visor.js` («no uso <dialog>
   porque su ::backdrop no toma los tokens del tema»). El motivo que daba
   sigue siendo cierto: `::backdrop` pasó a heredar del elemento originante
   —y con eso a ver los custom properties— solo en Chrome 122, Safari 17.4 y
   Firefox 120, y nuestro piso está debajo por los dos lados
   (`movil/android/variables.gradle` declara `minSdkVersion = 24`, y el último
   Chrome/WebView de Android 7 es el 119; el proyecto de iOS declara
   `IPHONEOS_DEPLOYMENT_TARGET = 15.0`, y en iOS el WebView es el del
   sistema). Un `var(--nm-*)` dentro de `::backdrop` queda sin valor ahí y el
   velo cae al negro del navegador. Lo que cambia es DÓNDE se pinta: un
   elemento normal del árbol hereda `--nm-*` de `:root` en todos los
   navegadores, en los dos modos, y reacciona al cambio de `data-theme` sin
   una línea de JS. Con un solo mecanismo no hay dos que mantener.

   En `.dialogo` el velo es una sombra de extensión enorme porque el panel
   sigue siendo el propio `<dialog>` (no tiene un hijo-panel todavía). Un
   diálogo modal vive en la top layer y se posiciona contra el viewport, así
   que la extensión lo cubre entero y no hay scroll que se le escape; una
   sombra no crea área desplazable. `max(100vw, 100vh)` y no `100vmax`: en
   vertical `vmax` toma el ALTO de la pantalla, que es el mismo error que
   costó un desborde lateral real en `.nm-banda`. La técnica no es nueva acá:
   el recorte del tour (`.tour__foco`) pinta su velo con la misma sombra de
   extensión desde antes. */
.dialogo { box-shadow: var(--nm-sombra-2), 0 0 0 max(100vw, 100vh) var(--nm-velo); }
.dialogo::backdrop { background: transparent; }
/* Defensa para un navegador sin `<dialog>` (iOS 15.0–15.3): `showModal()`
   tira excepción, no se abre nada, y el elemento no puede quedarse pintado
   en medio de la página. */
dialog:not([open]) { display: none; }
.dialogo__cabecera { display: flex; align-items: center; justify-content: space-between; gap: var(--nm-esp-3); margin-bottom: var(--nm-esp-4); }
.dialogo__titulo { margin: 0; }
.dialogo__cerrar {
  width: 32px; height: 32px;
  background: transparent; border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); font: inherit; cursor: pointer;
}
.dialogo__acciones { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--nm-esp-2); margin-top: var(--nm-esp-5); }
.dialogo__acciones .boton-primario { width: auto; margin: 0; }
.enlace-boton { background: none; border: 0; padding: 0; color: var(--nm-texto); font: inherit; text-decoration: underline; cursor: pointer; }

.editor__base { margin: 0 0 var(--nm-esp-3); color: var(--nm-texto-3); }
.editor__campos { display: flex; flex-wrap: wrap; gap: var(--nm-esp-3); }
.editor__campo { display: flex; flex-direction: column; gap: 4px; color: var(--nm-texto-3); }
.editor__campo--nombre { flex: 1 1 18ch; }
.editor__numero { width: 7ch; text-align: center; font-variant-numeric: tabular-nums; }
.editor__momentos { margin: var(--nm-esp-4) 0 0; padding: 0; border: 0; }
.editor__momentos legend { padding: 0; margin-bottom: var(--nm-esp-2); color: var(--nm-texto-3); }
.editor__chips { display: flex; flex-wrap: wrap; gap: var(--nm-esp-1); }
.editor__ingredientes { margin-top: var(--nm-esp-4); }
.editor__ingredientes-cabecera { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--nm-esp-3); margin-bottom: var(--nm-esp-2); color: var(--nm-texto-3); }
.editor__ayuda { color: var(--nm-texto-3); }
.editor__filas { list-style: none; margin: 0 0 var(--nm-esp-3); padding: 0; display: flex; flex-direction: column; gap: var(--nm-esp-2); }
.ingrediente { display: grid; grid-template-columns: minmax(0, 1fr) 6ch minmax(8ch, auto) auto; gap: var(--nm-esp-2); align-items: center; position: relative; }
.ingrediente__nombre { min-width: 0; }
.ingrediente__cantidad { text-align: right; font-variant-numeric: tabular-nums; }
.ingrediente__cantidad:disabled { opacity: .5; }
/* El selector de la medida de cocina (taza, cda, diente...): mismo estilo
   que cualquier otro `<select>` del front (`.lista__selector`), con el
   ancho chico que necesitan sus opciones. */
.ingrediente__unidad { font-size: var(--nm-b3); padding: 6px var(--nm-esp-2); }
.ingrediente__quitar {
  flex: none; display: grid; place-items: center; width: 28px; height: 28px;
  background: transparent; border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto-3); font: inherit; cursor: pointer;
}
.ingrediente__quitar .nm-ico { width: 16px; height: 16px; }
@media (hover: hover) and (pointer: fine) {
  .ingrediente__quitar:hover { color: var(--nm-texto); }
}
/* «≈ 300 g · se compra por kilo»: la ayuda de la fila, en su propio
   renglón bajo los cuatro campos (contrato de medidas, 2026-09-22). */
.ingrediente__ayuda { grid-column: 1 / -1; margin: -4px 0 0; }
.ingrediente__opciones {
  position: absolute; z-index: 5; left: 0; right: 0; top: 100%;
  margin: 2px 0 0; padding: 0; list-style: none;
  background: var(--nm-superficie-1); border: 1px solid var(--nm-borde-tarjeta); border-radius: var(--nm-radio-md);
  box-shadow: var(--nm-sombra-2);
  max-height: 14rem; overflow: auto;
}
.ingrediente__opcion { width: 100%; padding: var(--nm-esp-2) var(--nm-esp-3); background: transparent; border: 0; color: var(--nm-texto); font: inherit; font-size: var(--nm-b3); text-align: left; cursor: pointer; }
.ingrediente__opcion:focus { background: var(--nm-superficie-2); }
@media (hover: hover) and (pointer: fine) {
  .ingrediente__opcion:hover {   background: var(--nm-superficie-2); }
}
.ingrediente__opcion small { color: var(--nm-texto-3); }
.ingrediente--pendiente .ingrediente__nombre { border-style: dashed; }
.editor__estado { margin: var(--nm-esp-3) 0 0; min-height: 1.2em; color: var(--nm-texto-3); }

/* ───────────── Tarjeta de filas ─────────────
   La unidad del producto es la fila, no la tarjeta: un contenedor con
   divisores de 1px en vez de tarjetas dentro de tarjetas. */

/* `.resultados` YA NO entra acá. Esta regla es para listas cuya unidad es la
   FILA: un solo contenedor con divisores internos. Los resultados del
   buscador dejaron de ser eso cuando su unidad pasó a ser la fila de
   comparación —un producto y, dentro, una línea por tienda—, así que cada
   `.comparacion` es su propia tarjeta y este contenedor le quedaba encima:
   tarjeta dentro de tarjeta, justo lo que el comentario de arriba dice que
   no se hace. El fondo y el borde del `<ul>` dibujaban un marco que unía
   visualmente las doce tarjetas y el ojo no encontraba dónde termina un
   producto y empieza el siguiente. */
.resultados--filas, .alternativas, .carrito__items {
  list-style: none; margin: 0; padding: 0;
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde-tarjeta);
  border-radius: var(--nm-radio-lg);
  overflow: hidden;
  transition: background-color var(--nm-mov-entrada) var(--nm-curva-suave),
              border-color var(--nm-mov-entrada) var(--nm-curva-suave);
}

/* ───────────── Resultados: filas de comparación ─────────────
   Un producto por tarjeta y, dentro, una línea por tienda ordenada de
   menor a mayor. El precio más bajo se marca con nudo de oro y monto en
   maíz tostado; si varias tiendas empatan en ese precio se marcan todas. */

.panel-resultados { margin-bottom: var(--nm-esp-6); }
.panel-resultados__contador { margin: var(--nm-esp-4) 0 var(--nm-esp-3); color: var(--nm-texto-3); }

/* 24 y no 12 entre tarjetas. Doce resultados a 12px de separación, con la
   tarjeta a 6.8 puntos de luminancia del fondo (medido: 249.1 contra 242.3)
   y sin sombra, se leían como UNA sola losa continua: el ojo no encontraba
   dónde termina un producto y empieza el siguiente. El aire es lo que
   convierte doce rectángulos pegados en doce objetos. */
.resultados { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--nm-esp-5); }

/* La misma clase la usan dos listas distintas y solo una lleva contenedor: el
   buscador incremental (`#resultados`, cuyos hijos ya son tarjetas
   `.comparacion` completas) y la página `/buscar` servida en HTML, cuyos
   hijos son filas planas `.resultado` que SÍ necesitan una caja que las
   agrupe. Por eso la segunda se marca `--filas`: sin el modificador,
   quitarle el contenedor al buscador se lo quitaba también a ella y sus
   filas quedaban sueltas sobre el fondo de la página. */
.resultados--filas { display: block; }

/* Fila de /buscar, la versión servida en HTML de los resultados. Es un
   enlace de verdad —no el botón del buscador incremental— porque esa página
   existe para que un rastreador llegue a las fichas siguiendo enlaces. */
.resultado__enlace {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: var(--nm-esp-2) var(--nm-esp-4);
  align-items: baseline;
  padding: var(--nm-esp-4) var(--nm-esp-5);
  border-top: 1px solid var(--nm-borde-sutil);
  color: inherit;
  text-decoration: none;
}
.resultado:first-child .resultado__enlace { border-top: 0; }
@media (hover: hover) and (pointer: fine) {
  .resultado__enlace:hover {   background: var(--nm-superficie-2); }
}
/* Las cuatro celdas van colocadas a mano: el orden del DOM es nombre, meta,
   precio y unitario, pero la meta baja a la segunda fila. Con posiciones
   parciales (solo grid-row o solo grid-column) la fase explícita corre antes
   que la automática y el precio se quedaba con la columna del nombre. */
.resultado__nombre { grid-area: 1 / 1; font-weight: 500; }
.resultado__precio { grid-area: 1 / 2; font-weight: 600; white-space: nowrap; }
.resultado__unitario { grid-area: 1 / 3; color: var(--nm-texto-3); font-size: var(--nm-b3); white-space: nowrap; }
.resultado__meta { grid-area: 2 / 1; color: var(--nm-texto-3); font-size: var(--nm-b3); }

.comparacion {
  background: var(--nm-superficie-1);
  /* `--nm-borde` y no `--nm-borde-tarjeta`: el DESIGN define la tarjeta con
     `outline` (#DDD6C9) y reserva `outline-subtle` (#EAE4D9) para los
     divisores INTERNOS — que acá son las líneas entre tiendas. La tarjeta
     estaba usando el valor del divisor, así que su contorno pesaba lo mismo
     que las líneas de adentro y el bloque entero se leía plano. */
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-lg);
  /* La sombra tenue y cálida que el DESIGN da para la elevación en claro. En
     oscuro el token es casi negro y la profundidad la dan las capas de
     carbón más el borde, como manda la regla del modo oscuro. */
  box-shadow: var(--nm-sombra-1);
  overflow: hidden;
  transition: background-color var(--nm-mov-entrada) var(--nm-curva-suave),
              border-color var(--nm-mov-entrada) var(--nm-curva-suave);
}
/* La cabecera del grupo toma la geometría de `fila` (§5.2): relleno de 8 px
   verticales contra los 16 de antes, la foto de 44 px que ya tenía, el
   nombre con tope de DOS renglones y la meta en una sola línea con puntos
   suspensivos. Doce resultados de tres renglones eran tres pantallas solo de
   cabeceras. */
.comparacion__cabecera { padding: var(--nm-esp-2) var(--nm-esp-4); }
.comparacion__nombre {
  margin: 0;
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: var(--nm-d4);
  letter-spacing: var(--nm-d4-tracking); line-height: 1.25;
}
/* El tope de dos renglones va en el BOTÓN, que es quien contiene el texto:
   el `<h2>` lo envuelve como caja inline atómica y clamparlo ahí no recorta
   nada. Su `overflow: hidden` no se lleva puesta el área pinchable de
   `.comparacion__abrir::after`: el bloque contenedor de ese pseudo es
   `.comparacion__cabecera` —posicionada más abajo en este archivo—, que es
   ancestro del botón y no descendiente, así que el recorte del botón no le
   aplica. */
.comparacion__abrir {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden;
}
.comparacion__meta {
  margin: 2px 0 0; color: var(--nm-texto-3); font-size: var(--nm-b3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.comparacion__empate { margin: 2px 0 0; color: var(--nm-texto-2); font-size: var(--nm-b3); }

.comparacion__tiendas { list-style: none; margin: 0; padding: 0; }

/* El pie del grupo: «Ver las otras 4 tiendas».

   Lo que gasta la densidad de esta pantalla no son las tarjetas sino las
   LÍNEAS DE TIENDA —un producto en seis tiendas son seis líneas, y el precio
   que la persona vino a buscar está en la primera—, así que por defecto se
   pintan las dos más baratas y el resto se despliega acá. Es un control del
   grupo y no una opción del listbox: va en su propio `<li>` de presentación,
   en registro terciario, sin borde ni relleno que compitan con las líneas de
   precio que tiene encima. */
.comparacion__pie { border-top: 1px solid var(--nm-borde-sutil); }
.comparacion__mas {
  display: block; width: 100%;
  /* 44 px: es lo que devuelve las otras tiendas, así que es un objetivo de
     dedo como cualquier otro y no un enlace de pie de página. */
  min-height: 44px;
  padding: 10px var(--nm-esp-4);
  background: transparent; border: 0;
  color: var(--nm-texto-3); font: inherit; font-size: var(--nm-b3);
  text-align: left; cursor: pointer;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-base) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .comparacion__mas:hover {   color: var(--nm-texto); background: var(--nm-superficie-2); }
}
.comparacion__mas:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -2px; }

/* Columnas: tienda · precio · por unidad · diferencia. Con tabular-nums las
   cifras caen alineadas y la comparación se lee de un vistazo. */
.tienda {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 13ch) minmax(0, 9ch);
  /* `column-gap` y `row-gap` por separado, nunca el `gap` corto: el sello con
     tarjeta va en una segunda fila, y con `gap: 16px` esos 16 px se abrían
     también entre el precio y el sello — la fila de Wong con Prime quedaba
     más alta que la de Metro en escritorio. Los 2 px valen en todo ancho. */
  column-gap: var(--nm-esp-4);
  row-gap: 2px;
  align-items: baseline;
  width: 100%;
  padding: 10px var(--nm-esp-5);
  background: transparent;
  border: 0; border-top: 1px solid var(--nm-borde-sutil);
  color: inherit; font-size: var(--nm-b2); text-align: left; cursor: pointer;
  transition: background-color var(--nm-mov-base) var(--nm-curva-salida);
}
.tienda[aria-selected="true"] { background: var(--nm-superficie-2); }
@media (hover: hover) and (pointer: fine) {
  .tienda:hover {   background: var(--nm-superficie-2); }
}
.tienda:focus-visible { outline-offset: -2px; }
.tienda__nombre { font-weight: 500; }
.tienda__precio {
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: var(--nm-b1);
  letter-spacing: -.01em; white-space: nowrap; text-align: right;
}
.tienda__unitario { color: var(--nm-texto-3); font-size: var(--nm-b3); white-space: nowrap; text-align: right; }
.tienda__dif { color: var(--nm-texto-3); font-size: var(--nm-b3); white-space: nowrap; text-align: right; }
/* El precio con tarjeta, en su propio renglón y DEBAJO DEL PRECIO: es un
   precio, no un unitario, y bajo la columna del unitario se leía como otro
   «por kilo» (Gonzalo lo señaló el 2026-09-09). Abarca las columnas de la
   tienda y del precio y se pega a la derecha, así su borde cae en el borde
   del precio sin ensanchar esa columna; dos sellos en Wong se apilan ahí
   mismo. Vacío no ocupa. */
.tienda__sello {
  grid-column: 1 / 3; display: flex; justify-content: flex-end; flex-wrap: wrap;
  gap: 2px var(--nm-esp-4); margin-top: -2px;
}
.tienda__sello:empty { display: none; }

/* La ganadora: nudo de oro (relleno --nm-nudo) y monto en el oro de texto
   — el oro marca dónde gana tu plata. Los demás montos van en tinta. */
.tienda--gana .tienda__nombre::before {
  content: ''; display: inline-block; vertical-align: middle;
  width: 9px; height: 9px; margin-right: var(--nm-esp-2);
  background: var(--nm-nudo); border-radius: 50%;
}
.tienda--gana .tienda__nombre { font-weight: 600; }
.tienda--gana .tienda__precio { color: var(--nm-acento); }
/* Agotada: el precio baja a terciario, como un dato que hoy no se puede
   usar, y el chip «Hoy sin stock» —el mismo de la ficha— va pegado al
   nombre de la tienda, que es de quien es el dato. `inline-block` para que
   no herede el subrayado del nombre. Sin tachar y sin rojo: no es un
   error, es que hoy no está. */
.tienda--sin-stock .tienda__precio { color: var(--nm-texto-3); font-weight: 400; }
.tienda__nombre .etiqueta {
  display: inline-block; margin-left: var(--nm-esp-2); vertical-align: middle;
  font-family: var(--nm-cuerpo);
}

/* La fila de comparación a dos columnas: es forma, no ajuste. Cabe a cuatro
   columnas desde ~386 px, así que 620 nunca fue un umbral de que algo no
   quepa — es la decisión de cuánta información recibe un teléfono (ESPEC_UI
   §4: «en escritorio se muestra más, no menos»). */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .tienda { grid-template-columns: minmax(0, 1fr) auto; }
  :root:not([data-forma="web"]) .tienda__unitario { grid-column: 1; }
  :root:not([data-forma="web"]) .tienda__dif { grid-column: 2; text-align: right; }
}
:root[data-forma="app"] .tienda { grid-template-columns: minmax(0, 1fr) auto; }
:root[data-forma="app"] .tienda__unitario { grid-column: 1; }
:root[data-forma="app"] .tienda__dif { grid-column: 2; text-align: right; }

/* ───────────── Escáner de código de barras (PANTALLAS §3 P2a) ─────────────

   La cámara ocupa la pantalla entera: es lo que la persona está mirando, y
   cualquier cromo que le robe alto le roba encuadre. Sobre el video van tres
   cosas y nada más —el título con la ✕, el visor, y la pista con la puerta al
   campo—, y el veredicto llega en `.escaner-hoja`, que sube desde abajo sin
   apagar la cámara.

   **Es una superficie oscura en los dos modos**, como la franja índigo: lo
   que hay debajo es una foto de la cámara y no una superficie nuestra. Así
   que en vez de repintar cada pieza, la sección REMAPEA los tokens
   semánticos a la escala nocturna —el mismo recurso que `tokens.css` usa en
   `.nm-franja-indigo`— y todo lo que caiga adentro (`.nm-boton`, `.nm-vacio`,
   `.nm-campo`) se adapta solo. El oro de adentro es siempre maíz, porque el
   fondo es oscuro en los dos modos, y el único destello es la cifra del
   mejor precio.

   El tab bar sigue abajo, a la vista, invertido a oscuro: la pantalla es una
   capa por debajo de él (z-index 35 contra 40) y no una pestaña. */

.escaner,
.escaner-hoja {
  --nm-fondo:         var(--nm-p-profundo);
  --nm-superficie-1:  var(--nm-p-noche);
  --nm-superficie-2:  var(--nm-p-cerro);
  --nm-superficie-3:  var(--nm-p-cumbre);
  --nm-texto:         var(--nm-p-hueso);
  --nm-texto-2:       var(--nm-p-niebla);
  --nm-texto-3:       var(--nm-p-pizarra-osc);
  --nm-borde-sutil:   var(--nm-p-linea-osc);
  --nm-borde:         var(--nm-p-linea-osc);
  --nm-borde-fuerte:  var(--nm-p-linea-fuerte-osc);
  --nm-borde-tarjeta: var(--nm-p-linea-osc);
  --nm-primario:       var(--nm-p-helada);
  --nm-primario-tinta: var(--nm-p-profundo);
  --nm-primario-hover: var(--nm-p-helada-hover);
  --nm-acento:        var(--nm-p-maiz);
  --nm-acento-hover:  var(--nm-p-maiz-palido);
  --nm-nudo:          var(--nm-p-maiz);
  --nm-foco:          var(--nm-p-maiz);
  --nm-campo:         var(--nm-p-profundo);
  color: var(--nm-texto);
}

.escaner {
  position: fixed; inset: 0; z-index: 35;
  background: var(--nm-fondo);
  overflow: hidden;
}
.escaner:focus { outline: none; }

/* La cámara, a sangre y detrás de todo. */
.escaner__camara { position: absolute; inset: 0; }
.escaner__video { display: block; width: 100%; height: 100%; object-fit: cover; }

/* El cromo flota sobre el video y no le roba el toque: solo sus controles lo
   reciben. */
.escaner__cromo {
  position: absolute; inset: 0; pointer-events: none;
  display: grid; grid-template-rows: auto 1fr auto; gap: var(--nm-esp-4);
  padding: calc(var(--nm-esp-4) + env(safe-area-inset-top))
           calc(var(--nm-esp-5) + env(safe-area-inset-right))
           calc(var(--nm-esp-5) + var(--nm-barra-tabs) + env(safe-area-inset-bottom))
           calc(var(--nm-esp-5) + env(safe-area-inset-left));
}
.escaner__barra, .escaner__pie { pointer-events: auto; }

.escaner__barra {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--nm-esp-4); min-height: 56px;
}
.escaner__titulo {
  margin: 0; max-width: 18ch;
  font: 700 var(--nm-d4) / 1.2 var(--nm-cuerpo); letter-spacing: var(--nm-d4-tracking);
}
/* La ✕ es la salida, y sobre una foto necesita su propio fondo para leerse. */
.escaner__cerrar {
  flex: none; width: 40px; height: 40px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--nm-superficie-1); border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full); color: var(--nm-texto); cursor: pointer;
}

/* El visor: cuatro esquinas en hueso y una línea de lectura en maíz. Los
   colores salen de las primitivas y no de los semánticos porque lo que tienen
   debajo es una foto y no una superficie nuestra — el mismo motivo por el que
   la miniatura usa cal cruda. */
.escaner__mira {
  position: relative; place-self: center;
  width: min(76%, 300px); aspect-ratio: 3 / 2;
}
/* Sin cámara no hay a qué apuntar, y el visor se va (QA P1). Vivía fuera de
   `#escaner-camara`, así que en el camino de «tu navegador no lee códigos de
   barras» —Safari de iOS, o permiso negado— la pantalla pedía TECLEAR los 13
   dígitos y al mismo tiempo dibujaba el retículo de apuntado con su línea de
   lectura sobre un vacío negro. Dos instrucciones contrarias a la vez. */
#escaner-camara[hidden] + .escaner__cromo .escaner__mira { display: none; }
.escaner__esquina {
  position: absolute; width: 28px; height: 28px;
  border: 3px solid var(--nm-p-hueso);
}
.escaner__esquina:nth-child(1) { top: 0; left: 0; border-right: 0; border-bottom: 0; border-top-left-radius: var(--nm-radio-md); }
.escaner__esquina:nth-child(2) { top: 0; right: 0; border-left: 0; border-bottom: 0; border-top-right-radius: var(--nm-radio-md); }
.escaner__esquina:nth-child(3) { bottom: 0; left: 0; border-right: 0; border-top: 0; border-bottom-left-radius: var(--nm-radio-md); }
.escaner__esquina:nth-child(4) { bottom: 0; right: 0; border-left: 0; border-top: 0; border-bottom-right-radius: var(--nm-radio-md); }
.escaner__linea {
  position: absolute; left: 6px; right: 6px; top: 50%;
  height: 2px; border-radius: 2px; background: var(--nm-p-maiz);
}

.escaner__pie { display: grid; justify-items: start; gap: var(--nm-esp-3); }
.escaner__pista {
  margin: 0; min-height: 1.2em;
  color: var(--nm-texto-2); font-size: var(--nm-b2);
}
/* La puerta al campo: quien puede apuntar no necesita que nada le tape el
   visor, y quien no puede la encuentra en la primera línea de abajo. */
.escaner__escribir {
  padding: var(--nm-esp-2) 0; border: 0; background: none;
  color: var(--nm-texto); font: 600 var(--nm-b2) / 1 var(--nm-cuerpo);
  text-decoration: underline; text-decoration-color: var(--nm-borde-fuerte);
  text-underline-offset: 4px; cursor: pointer;
}
.escaner__manual { width: 100%; max-width: 420px; }
.escaner__fila { display: flex; align-items: center; gap: var(--nm-esp-2); }
.escaner__campo, .escaner-anaquel__campo {
  flex: 1; min-width: 0;
  padding: 11px var(--nm-esp-4);
  background: var(--nm-campo);
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto); font-family: var(--nm-cuerpo); font-size: 16px;
}
.escaner__campo:focus-visible, .escaner-anaquel__campo:focus-visible {
  outline: 2px solid var(--nm-foco); outline-offset: 2px; border-color: transparent;
}
.escaner__enviar {
  flex: none; height: 44px; padding: 0 18px;
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  background: transparent; color: var(--nm-texto);
  font: 700 var(--nm-b2) / 1 var(--nm-cuerpo); cursor: pointer;
}
.escaner__error { margin: var(--nm-esp-2) 0 0; color: var(--nm-p-error-osc); }
.escaner__error:empty { display: none; }

/* La pantalla previa al permiso y el respaldo sin `<dialog>`: centrados sobre
   la cámara, que en ese momento todavía no está prendida. */
.escaner__resultado {
  position: absolute; inset: 0; z-index: 2;
  display: grid; align-content: center; justify-items: center;
  padding: calc(var(--nm-esp-6) + env(safe-area-inset-top)) var(--nm-esp-5)
           calc(var(--nm-esp-6) + var(--nm-barra-tabs) + env(safe-area-inset-bottom));
}
.escaner__resultado:empty { display: none; }

/* ── la hoja del veredicto ─────────────────────────────────
   Sube sobre la cámara viva y no la apaga. El segundo escaneo reemplaza su
   contenido sin cerrarla (PANTALLAS §9.3). */
/* No toma toda la altura: la cámara tiene que seguir viéndose arriba, o la
   hoja deja de leerse como una capa sobre lo que la persona está apuntando. */
.escaner-hoja {
  background: var(--nm-superficie-1);
  max-height: min(74dvh, calc(100dvh - 48px));
}
.escaner-hoja__cuerpo { display: grid; gap: var(--nm-esp-4); }
.escaner-hoja__cuerpo:focus { outline: none; }
.escaner-hoja__titulo { margin: 0; }
.escaner-hoja__texto { margin: 0; color: var(--nm-texto-2); font-size: var(--nm-b2); }

/* Qué se escaneó: la foto chica y el nombre, para poder decir «sí, es esto». */
.escaner-hallazgo { display: flex; align-items: center; gap: var(--nm-esp-3); }
.escaner-hallazgo__texto { min-width: 0; }
.escaner-hallazgo__nombre { margin: 0; font: 700 var(--nm-d4) / 1.2 var(--nm-cuerpo); letter-spacing: var(--nm-d4-tracking); }
/* El nombre ES la puerta a la ficha, y la única. Tiene que VERSE que lo es:
   enlazarlo con `color: inherit` y sin subrayado —como quedó en el primer
   intento— es lo mismo que no enlazarlo, porque nadie prueba a hacerle clic a
   un título. */
.escaner-hallazgo__enlace {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--nm-borde-fuerte);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida),
              text-decoration-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.escaner-hallazgo__enlace:focus-visible {
  outline: 2px solid var(--nm-foco); outline-offset: 3px; border-radius: var(--nm-radio-sm);
}
/* La foto se abre en grande, igual que en la ficha. */
.escaner-hallazgo .ficha__foto { flex: none; width: 52px; }
.escaner-hallazgo .ficha__foto img, .escaner-hallazgo .miniatura { width: 52px; height: 52px; }
.escaner-hallazgo__meta { margin: var(--nm-esp-1) 0 0; color: var(--nm-texto-3); font-size: var(--nm-b3); }

/* El veredicto, en tres líneas. La cifra es el ÚNICO oro de la pantalla y el
   único momento serif: es el dato que la persona vino a buscar. */
.escaner-veredicto { display: grid; gap: 2px; }
.escaner-veredicto__titular { margin: 0; font-size: var(--nm-b1); font-weight: 600; }
.escaner-veredicto__cifra {
  margin: 0;
  font: 400 var(--nm-precio) / 1.05 var(--nm-serif);
  letter-spacing: var(--nm-precio-tracking);
  color: var(--nm-acento);
}
.escaner-veredicto__resto { margin: 0; color: var(--nm-texto-3); font-size: var(--nm-b3); }
/* El veredicto contra el anaquel: la frase en tinta y solo el monto en oro. */
.escaner-veredicto__monto { color: var(--nm-acento); font-weight: 700; }

/* Las demás tiendas: una fila por tienda, el precio a la derecha en tabular. */
.escaner-tiendas__rotulo { margin: 0 0 calc(var(--nm-esp-2) * -1); }
.escaner-tiendas { list-style: none; margin: 0; padding: 0; }
.escaner-tienda + .escaner-tienda { border-top: 1px solid var(--nm-borde-sutil); }
.escaner-tienda__enlace {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--nm-esp-4); align-items: baseline;
  padding: 10px 0;
  color: inherit; text-decoration: none;
}
.escaner-tienda__nombre { font-size: var(--nm-b2); }
.escaner-tienda__precio {
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: var(--nm-b1);
  letter-spacing: -.01em; white-space: nowrap; text-align: right;
}
.escaner-tienda__nota { grid-column: 1 / -1; color: var(--nm-texto-3); }
.escaner-tienda__sello { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: flex-end; }
.escaner-tienda__sello .sello-beneficio { margin-left: 0; }

/* El precio del anaquel: el único dato que nosotros no tenemos. La pregunta
   no supone dónde está parada la persona — el escáner no lo sabe. */
.escaner-anaquel { display: grid; gap: var(--nm-esp-2); }
.escaner-anaquel__etiqueta { color: var(--nm-texto-2); font-size: var(--nm-b3); }
.escaner-anaquel__fila { display: flex; align-items: center; gap: var(--nm-esp-2); }
.escaner-anaquel__moneda { flex: none; color: var(--nm-texto-3); font-size: var(--nm-b2); }
.escaner-anaquel__veredicto { margin: 0; font-size: var(--nm-b2); font-weight: 600; }
.escaner-anaquel__veredicto:empty { display: none; }

/* Un solo primario —«Agregar a la lista»— y «Otro» de borde, que cierra la
   hoja y deja la cámara lista para el siguiente sin navegar. Los dos se
   quedan pegados abajo: con ocho tiendas la lista es larga y el primario
   caía fuera de la pantalla (visto a 390×844). */
.escaner-acciones { display: flex; flex-wrap: wrap; align-items: center; gap: var(--nm-esp-3); }
.escaner-hoja .escaner-acciones {
  position: sticky; bottom: -24px;
  margin: 0 -20px -24px; padding: var(--nm-esp-3) var(--nm-esp-5) var(--nm-esp-6);
  background: var(--nm-superficie-1); border-top: 1px solid var(--nm-borde);
}
.escaner-acciones .nm-boton { flex: 1 1 auto; }
.escaner-acciones__estado { flex: 1 0 100%; margin: 0; color: var(--nm-texto-2); }
.escaner-acciones__estado:empty { display: none; }

/* Antes del permiso del sistema, una pantalla nuestra (ESPEC_UI §2bis #9):
   ícono, título, una frase y dos salidas. «Permitir cámara» es el único
   relleno de la vista; «Ahora no» es terciario. */
.escaner-permiso {
  display: grid; justify-items: start; gap: var(--nm-esp-3);
  max-width: 420px;
  padding: var(--nm-esp-6) var(--nm-esp-5);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde-tarjeta);
  border-radius: var(--nm-radio-lg);
}
.escaner-permiso__icono { width: 40px; height: 40px; color: var(--nm-texto-2); }
.escaner-permiso__titulo { margin: 0; font: 700 var(--nm-d3) / 1.2 var(--nm-cuerpo); letter-spacing: var(--nm-d3-tracking); }
.escaner-permiso__texto { margin: 0; max-width: var(--nm-medida); color: var(--nm-texto-2); font-size: var(--nm-b2); }
.escaner-permiso__acciones {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--nm-esp-3);
  margin-top: var(--nm-esp-2);
}

/* El código que no tenemos y el error de red, en el `.nm-vacio` de siempre. */
.escaner-vacio { padding: var(--nm-esp-5) 0; }
.escaner-vacio .nm-vacio__texto { max-width: 34ch; }

/* La cabecera de la app se aparta mientras la cámara está abierta: el escáner
   trae su propio título y su propia salida, y el velo opaco de la cabecera le
   tapaba los dos (visto a 390×844). No se le baja el `z-index` porque el tab
   bar vive DENTRO de ella y tiene que quedarse: se le quita el fondo, se
   esconde lo que no es el tab bar, y se le devuelven los toques al video. */
:root:has(.escaner:not([hidden])) .cabecera {
  background: none; border-bottom-color: transparent; pointer-events: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
:root:has(.escaner:not([hidden])) .cabecera__interior > *:not(.tabs) { visibility: hidden; }
:root:has(.escaner:not([hidden])) .tabs { pointer-events: auto; }

/* El tab bar, invertido mientras la cámara está abierta: sigue a la vista
   —salir de la pantalla no es un callejón— pero deja de ser la superficie
   clara que le robaba el ojo al visor. */
:root:has(.escaner:not([hidden])) .tabs__pastilla {
  background: var(--nm-p-noche); border-color: var(--nm-p-linea-osc);
}
:root:has(.escaner:not([hidden])) .tab { color: var(--nm-p-pizarra-osc); }
:root:has(.escaner:not([hidden])) .tab[aria-current="page"] {
  background: var(--nm-p-cerro); color: var(--nm-p-hueso);
}
:root:has(.escaner:not([hidden])) .buscar-abrir {
  background: var(--nm-p-noche); border-color: var(--nm-p-linea-osc); color: var(--nm-p-hueso);
}

@media (hover: hover) and (pointer: fine) {
  .escaner__cerrar:hover, .escaner__enviar:hover { background: var(--nm-superficie-2); }
  .escaner__escribir:hover { text-decoration-color: currentColor; }
  .escaner-hallazgo__enlace:hover { color: var(--nm-acento); text-decoration-color: currentColor; }
  .escaner-tienda__enlace:hover { color: var(--nm-acento); }
}

/* ───────────── Detalle ───────────── */

.detalle { padding-top: var(--nm-esp-2); }
.detalle:focus { outline: none; }

/* La ficha (PANTALLAS §3 P3). Arriba del pliegue: «‹ Buscar» y el enlace a
   la tienda en una línea de cromo, la foto GRANDE al centro, el nombre, la
   línea de identidad —«Primor · 1.8 l · desde S/ 8.61 el litro»— y la
   comparación por tienda. La cifra grande de antes se retiró: el precio de
   ESTA tienda es una fila más de la comparación, y tenerlo dos veces hacía
   que la ficha contestara dos preguntas distintas en el mismo bloque. */
.ficha { margin-bottom: var(--nm-esp-7); padding-bottom: var(--nm-barra-max); }

.ficha__cromo {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--nm-esp-3); flex-wrap: wrap;
  margin-bottom: var(--nm-esp-5);
}
.ficha__volver { text-decoration: none; }

.ficha__encabezado {
  display: grid; justify-items: center; text-align: center;
  gap: var(--nm-esp-3);
  margin-bottom: var(--nm-esp-6);
}
.ficha__nombre { margin: 0; max-width: 28ch; }
.ficha__identidad { margin: 0; color: var(--nm-texto-2); }
.ficha__identidad strong { color: var(--nm-texto); font-weight: 700; }

/* La ruta «Inicio › Genérico › Producto»: solo aparece cuando el producto
   tiene genérico. Una línea de tinta terciaria, el último tramo (el producto
   mismo) se corta con puntos suspensivos para no partir la línea en móvil. */
.ficha__ruta { margin: calc(var(--nm-esp-2) * -1) 0 var(--nm-esp-4); color: var(--nm-texto-3); }
.ficha__ruta ol {
  display: flex; align-items: baseline; flex-wrap: nowrap;
  gap: var(--nm-esp-2); margin: 0; padding: 0; list-style: none;
}
.ficha__ruta li { flex: 0 0 auto; white-space: nowrap; }
.ficha__ruta li + li::before { content: "›"; margin-right: var(--nm-esp-2); }
.ficha__ruta li:last-child { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ficha__ruta a { color: var(--nm-texto-2); text-underline-offset: 2px; }
/* «Ver todas las opciones de …», bajo la comparación: la salida hacia el
   genérico, que compara todas las tiendas y marcas. */
.ficha__todas { margin: var(--nm-esp-4) 0 0; }

/* El puente hacia la lista («¿Y el resto de tu compra?»,
   `seo.puente_a_la_lista`): una tarjeta en el flujo, justo debajo de «Dónde
   está más barato», donde termina la lectura mediana de quien llega desde
   Google. Sin marcador de sección —no es una sección de la ficha, es la
   salida hacia la app— y sin oro: las cifras van en tinta. El botón es el
   secundario en añil; el único primario sigue siendo «Agregar a la lista». */
.ficha-puente {
  display: grid; justify-items: start; gap: var(--nm-esp-3);
  padding: var(--nm-esp-5); margin: 0 0 var(--nm-esp-8);
}
.ficha-puente__titulo { margin: 0; }
.ficha-puente__texto { margin: 0; max-width: 60ch; color: var(--nm-texto-2); }
.ficha-puente__cifra { margin: 0; max-width: 60ch; color: var(--nm-texto-2); }
.ficha-puente__cifra strong { color: var(--nm-texto); font-weight: 700; }
/* El enlace en tinta y no en el azul del navegador (DESIGN.md: nada de azul
   eléctrico); el subrayado es lo que lo dice enlace. */
.ficha-puente__cifra .nm-enlace {
  white-space: nowrap; color: var(--nm-texto); font-weight: 600;
  text-underline-offset: 2px;
}
.ficha-puente__accion {
  margin-top: var(--nm-esp-2); text-decoration: none;
  height: auto; min-height: 44px; padding-block: 10px;
  white-space: normal; text-align: center; line-height: 1.25;
}
/* Angosto de verdad —el ancho del teléfono—, no la forma app: es la medida
   del botón, no el chasis. */
@media (max-width: 599px) {
  .ficha-puente { padding: var(--nm-esp-4); }
  .ficha-puente__accion { width: 100%; }
}

/* «¿Un dato mal? Avísanos», al final y en gris: es una salida, no una
   invitación. */
.ficha__reporte { margin: var(--nm-esp-6) 0 0; color: var(--nm-texto-3); }
.ficha__reporte a { color: var(--nm-texto-3); text-underline-offset: 2px; }

/* La barra fija de la ficha: el mejor precio a la izquierda y el único
   primario a la derecha. La pinta solo `/api/producto/<id>` —la página
   servida sin JavaScript no la lleva (ver `seo.cuerpo_producto`)— y su
   chasis es el `--nm-barra-display` de la forma: fija en app, en su sitio
   en web. */
.ficha__barra-dato { display: grid; }
.ficha__barra .agregar-lista { margin-top: 0; margin-left: auto; }
/* En web la barra no es fija: la ficha entra entera en la pantalla y una
   capa pegada abajo sería cromo de más. En forma app sí, igual que la barra
   de compromiso de la lista, un punto debajo del tab bar. */
.ficha__barra { margin-top: var(--nm-esp-6); border-radius: var(--nm-radio-lg); }
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .ficha__barra {
    position: fixed; left: 0; right: 0;
    bottom: calc(var(--nm-barra-tabs) + env(safe-area-inset-bottom));
    z-index: 39; margin: 0; border-radius: 0;
  }
}
:root[data-forma="app"] .ficha__barra {
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--nm-barra-tabs) + env(safe-area-inset-bottom));
  z-index: 39; margin: 0; border-radius: 0;
}

/* La fecha de verificación pesa un poco más: es la que le dice a quien llega
   desde un buscador —o a quien cite la página— de cuándo es el precio. */
.ficha__actualizado {
  margin: var(--nm-esp-4) 0 0;
  color: var(--nm-texto-3);
  max-width: 62ch;
}
.ficha__actualizado strong { color: var(--nm-texto-2); font-weight: 600; }

/* Agregar este producto a una lista. Es la única acción de la ficha —todo lo
   demás es lectura— así que se lleva el botón primario de la pantalla, y el
   menú de listas se abre ahí mismo en vez de mandar a la persona a la otra
   pestaña a buscar el producto de nuevo. */
.agregar-lista { margin-top: var(--nm-esp-5); }
/* .boton-primario es de ancho completo porque nació en el panel del reparto;
   acá el botón va en línea, a la medida de su texto. */
.boton-primario.agregar-lista__boton { display: inline-flex; width: auto; margin-top: 0; }

/* El menú de listas es una VARIANTE DE `hoja` (§5.1), no un popover anclado.
   Hasta el 2026-09-11 este bloque declaraba
   `position: absolute; z-index: 20; top: calc(100% + …)` contra
   `.agregar-lista`, y eso dejó de funcionar cuando el marcado pasó a
   `<dialog>` + `showModal()`: en la top layer el bloque contenedor del
   dialog es la VENTANA, así que ese `top` colgaba del borde del viewport y no
   del botón. Ahora no queda nada de eso —ni posición, ni z-index, ni relleno,
   ni fondo, ni sombra, ni `min-width`—: el chasis, el velo, la capa y la
   geometría por forma los da `.hoja` (abajo, «Los cuatro componentes»), que
   en forma web centra un panel de `min(92vw, 26rem)` y en forma app lo sube
   desde abajo a pantalla completa.

   No lleva `.hoja--anclada`: esa cara la enciende `hoja.js` cuando escribe
   `--hoja-x`/`--hoja-y` desde el rect del ancla, y este menú lo abre
   `buscador.js` con `showModal()` directo. Sin esas coordenadas el panel
   anclado caería en `top: 50%; left: 50%` sin centrar, o sea descolgado hacia
   abajo y a la derecha; centrado es lo que §4 pide para la hoja en forma web.

   Lo único que este bloque sigue debiendo es el RELLENO del contenido:
   `.hoja__panel` va con `padding: 0` porque sus hijos habituales
   (`.hoja__cabecera`, `.hoja__cuerpo`, `.hoja__pie`) traen el suyo, y acá
   adentro va el formulario de listas, que no. */
.agregar-lista__menu .hoja__panel { padding: var(--nm-esp-4); }

.agregar-lista__titulo { margin: 0 0 var(--nm-esp-2); color: var(--nm-texto-3); }

.agregar-lista__listas {
  list-style: none; margin: 0 0 var(--nm-esp-3); padding: 0;
  /* contain: al tocar fondo, el scroll se queda acá y no arrastra la
     página por debajo del menú. */
  max-height: 40vh; overflow-y: auto; overscroll-behavior: contain;
}
.agregar-lista__opcion {
  display: block; width: 100%;
  padding: var(--nm-esp-2) var(--nm-esp-3);
  background: transparent; border: 0; border-radius: var(--nm-radio-md);
  color: var(--nm-texto); text-align: left; cursor: pointer;
  transition: background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .agregar-lista__opcion:hover {   background: var(--nm-superficie-2); }
}
.agregar-lista__nombre { display: block; font-weight: 600; font-size: 14px; }
.agregar-lista__meta { display: block; color: var(--nm-texto-3); }

/* La lista nueva se crea desde acá: quien todavía no tiene ninguna no
   debería tener que ir a armarla a otra pantalla para poder guardar el
   producto que está mirando. */
.agregar-lista__nueva {
  display: flex; gap: var(--nm-esp-2);
  padding-top: var(--nm-esp-3);
  border-top: 1px solid var(--nm-borde-sutil);
}
.agregar-lista__listas:empty + .agregar-lista__nueva { border-top: 0; padding-top: 0; }
.agregar-lista__campo {
  flex: 1; min-width: 0;
  padding: 9px var(--nm-esp-3);
  background: var(--nm-campo);
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-md);
  color: var(--nm-texto); font-size: 14px;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.agregar-lista__campo:focus { border-color: var(--nm-borde-fuerte); }
@media (hover: hover) and (pointer: fine) {
  .agregar-lista__campo:hover {   border-color: var(--nm-borde-fuerte); }
}
.agregar-lista__campo::placeholder { color: var(--nm-texto-3); }

.agregar-lista__estado { margin: var(--nm-esp-3) 0 0; min-height: 1.4em; color: var(--nm-texto-3); }
.agregar-lista__ver {
  padding: 0; background: none; border: 0;
  color: var(--nm-texto-2); font: inherit; text-decoration: underline;
  text-underline-offset: 3px; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .agregar-lista__ver:hover {   color: var(--nm-texto); }
}

@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  /* El ancho del menú en móvil ya no se corrige acá: `--nm-hoja-ancho` lo
     pone en 100% en forma app, que es lo que reemplazó al parche de los
     280 px anclados a la izquierda del botón. */
  :root:not([data-forma="web"]) .boton-primario.agregar-lista__boton { display: flex; width: 100%; }
}
:root[data-forma="app"] .boton-primario.agregar-lista__boton { display: flex; width: 100%; }

/* ---------- historial de precios ---------- */

/* El gráfico se dibuja en el servidor (canasta_precios/grafico.py) y se
   inyecta como SVG en línea, así que estas reglas lo alcanzan igual que a
   cualquier otro elemento y hereda el tema claro/oscuro sin trabajo extra. */

/* Tipografía mayor (§6 #11): el veredicto es la respuesta de la sección —«hoy
   está en su precio más bajo de las dos semanas»— y se leía en el mismo
   cuerpo que la letra chica de abajo. Sube a B1; las cifras, a B2 con la
   cifra en tinta. Es todo lo que el historial cambia: el gráfico, su ventana
   de catorce días y el `<details>` día por día se quedan como están. */
.historial__veredicto { margin: 0 0 var(--nm-esp-4); max-width: 62ch; font-size: var(--nm-b1); }

.historial {
  padding: var(--nm-esp-5);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde-sutil);
  border-radius: var(--nm-radio-lg);
}
/* Los números que hay que poder leer sí o sí van en HTML, no adentro del
   SVG: el SVG escala con su columna y en un teléfono sus rótulos quedarían
   en 8 px. */
.historial__cifras {
  display: flex; flex-wrap: wrap; gap: var(--nm-esp-2) var(--nm-esp-5);
  margin: 0 0 var(--nm-esp-4); color: var(--nm-texto-3);
  font-size: var(--nm-b2);
}
.historial__cifras strong { color: var(--nm-texto); font-weight: 600; }

/* Llena el ancho de la tarjeta del historial, en escritorio y en móvil: un
   tope de 780 px (`--nm-ancho` es 1 180) dejaba un vacío a la derecha en
   cualquier pantalla más ancha que eso —reportado en el sitio de desarrollo,
   2026-09-25, con la tarjeta ocupando solo 2/3 de su ancho—. El viewBox
   escala proporcional (ver `grafico.svg`), así que ensanchar el SVG también
   le sube el alto: no hay texto que se deforme, solo un gráfico más grande
   con series de pocos puntos, que es preferible al vacío. */
.gp { display: block; width: 100%; height: auto; overflow: visible; }

/* Rejilla: hairline sólida, un paso sobre el fondo. Ni punteada (se lee como
   "proyección") ni marcada (compite con el dato). */
.gp__rejilla { stroke: var(--nm-borde-sutil); stroke-width: 1; }
.gp__tick { fill: var(--nm-texto-3); font-size: 14px; font-variant-numeric: tabular-nums; }

/* La línea es tinta, no oro: el DESIGN reserva el maíz para el dato que
   gana, y una curva entera dorada es decoración (v4.0). El relleno es una
   sombra apenas insinuada del propio texto. */
.gp__area { fill: var(--nm-texto); opacity: .05; }
.gp__linea {
  fill: none; stroke: var(--nm-texto-2); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
}
/* El anillo del color de la superficie —y no un borde— es lo que mantiene
   legible un punto cuando cae encima de la línea o de otro punto. Los
   puntos son nudos: terciarios de serie y tinta los rotulados.

   El mínimo TAMBIÉN es de tinta, y es un cambio del 2026-09-12. Era el nudo
   maíz del gráfico —uno de los sitios que `DESIGN.md` le reserva al oro—,
   pero en la ficha el gráfico comparte pantalla con la cifra del ahorro y
   con la fila ganadora: medido por color computado a 375×812, la ficha
   llegaba a SIETE destellos de oro donde el presupuesto es 2–3 contando el
   logo. De todo lo que brillaba, este punto es el que menos dice: el mínimo
   ya está escrito con todas sus letras dos renglones más arriba («Mínimo
   S/ 6.80»), así que el oro estaba repitiendo un dato, y el oro que repite
   es el que sobra. Se queda más grande que los de serie —es `--clave`—,
   que es lo que lo distingue del resto de la serie. */
.gp__punto { fill: var(--nm-texto-3); stroke: var(--nm-superficie-1); stroke-width: 2; }
.gp__punto--clave { r: 5; fill: var(--nm-texto); }
.gp__punto--minimo { fill: var(--nm-nudo); }
.gp__valor {
  fill: var(--nm-texto); font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Hover en CSS puro: cada día tiene una franja invisible del ancho de su
   columna (no del punto de 8 px) que enciende la cruz y el globo. Sin
   JavaScript, así funciona igual en el HTML servido que en el repintado. */
.gp__franja { cursor: crosshair; }
.gp__cruz, .gp__punto--activo, .gp__globo { opacity: 0; pointer-events: none; }
@media (hover: hover) and (pointer: fine) {
  .gp__franja:hover .gp__cruz, .gp__franja:hover .gp__punto--activo, .gp__franja:hover .gp__globo {   opacity: 1; }
}
.gp__cruz { stroke: var(--nm-borde); stroke-width: 1; }
.gp__globo {
  fill: var(--nm-texto); font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums; paint-order: stroke;
  stroke: var(--nm-superficie-1); stroke-width: 4; stroke-linejoin: round;
}

.historial__tabla { margin-top: var(--nm-esp-4); }
.historial__tabla summary {
  cursor: pointer; color: var(--nm-texto-2); font-size: var(--nm-b2);
}
.historial__tabla table { border-collapse: collapse; margin-top: var(--nm-esp-3); }
.historial__tabla th, .historial__tabla td {
  text-align: left; padding: var(--nm-esp-2) var(--nm-esp-5) var(--nm-esp-2) 0;
  border-bottom: 1px solid var(--nm-borde-sutil); font-size: var(--nm-b2);
}
.historial__tabla th { color: var(--nm-texto-3); font-weight: 500; }
.historial__tabla td { font-variant-numeric: tabular-nums; }

/* En pantallas angostas el SVG entero encoge y sus rótulos con él. Se
   esconden en vez de dejarlos ilegibles: los mismos números están arriba en
   .historial__cifras y abajo en la tabla, así que no se pierde ningún dato.

   Refutación escrita, porque borra una decisión razonada: el bloque de
   ajustes de teléfono subía estos dos a 20/22 px «para que en 340px no
   queden en 7px», y a 340 px están escondidos por esta misma regla. El
   aumento solo estaba vivo en la banda 601–640 y por debajo era CSS muerto,
   así que se fue con la consolidación. Gana el esconder, que trae su propia
   justificación verificable. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .gp__tick, :root:not([data-forma="web"]) .gp__valor { display: none; }
}
:root[data-forma="app"] .gp__tick, :root[data-forma="app"] .gp__valor { display: none; }

.seccion { margin-bottom: var(--nm-esp-8); }
/* El marcador de sección (cuerda + nudo) va sobre el H2 y reemplaza al
   filete de abajo: el motivo es estructura, no adorno (Quipu 26Q4). */
.seccion__cabecera {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--nm-esp-4); flex-wrap: wrap;
  margin-bottom: var(--nm-esp-4);
}
.seccion__cabecera .nm-marcador { flex-basis: 100%; margin-bottom: 0; }
.seccion__titulo { margin: 0; }
.seccion__nota { margin: 0; color: var(--nm-texto-3); max-width: 62ch; }
.seccion__ayuda { margin: var(--nm-esp-3) 0 var(--nm-esp-4); color: var(--nm-texto-3); max-width: var(--nm-medida); }

.etiqueta { font-weight: 500; font-size: var(--nm-l1); letter-spacing: .12em; text-transform: uppercase; color: var(--nm-texto-3); }
/* Sin rojo de "error": el aviso se resuelve con contorno y peso. */
.etiqueta--alerta {
  color: var(--nm-texto-2);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full);
  padding: 1px 8px;
}

.aviso {
  margin-bottom: var(--nm-esp-4);
  padding: var(--nm-esp-4) var(--nm-esp-5);
  background: var(--nm-superficie-1);
  border: 1px dashed var(--nm-borde);
  border-radius: var(--nm-radio-lg);
  color: var(--nm-texto-3);
}
.aviso p { margin: 0 0 var(--nm-esp-2); max-width: var(--nm-medida); }
.aviso p:last-child { margin-bottom: 0; }
.aviso strong { color: var(--nm-texto); font-weight: 600; }
.aviso em { font-style: normal; color: var(--nm-texto-2); }
/* El aviso de tienda que cambió sola (reparto de Gonzalo, 2026-09-28):
   persistente hasta que la persona lo cierre o vuelva a armar el carrito —
   nunca un toast que se desvanece solo. Mismo `.aviso` sin oro ni verde,
   con su «Entendido» debajo del texto en vez de al lado. */
.aviso--cambio-tienda { margin-top: var(--nm-esp-3); }
.aviso--cambio-tienda .carrito__deshacer { margin-top: var(--nm-esp-1); }

.enlace-tienda {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--nm-texto-3); font-size: var(--nm-b3); text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida), border-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .enlace-tienda:hover {   color: var(--nm-texto); border-bottom-color: var(--nm-borde-fuerte); }
}
/* El enlace que LLEVA LA CIFRA (§2bis #11): «Ver en Plaza Vea · S/ 47.99 ·
   1 kg ↗». Es la regla de las cifras corroborables hecha componente, y por
   eso pesa más que un enlace de texto: secundario con borde, nunca relleno
   —el único primario de la ficha es «Agregar a mi lista»— y sin una gota de
   oro, que está reservado al dato que celebra. */
.enlace-tienda--ir {
  padding: 9px var(--nm-esp-4);
  /* `border` completo y no solo el de abajo: `.enlace-tienda` declara un
     `border-bottom` transparente para su subrayado al hover, y el atajo lo
     reemplaza entero. */
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); font-size: var(--nm-b2);
  font-variant-numeric: tabular-nums;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .enlace-tienda--ir:hover {   color: var(--nm-texto); border-color: var(--nm-borde-fuerte); }
}
/* En la ficha es su propia franja de acción, debajo de los metadatos. */
.ficha > .enlace-tienda--ir { margin-top: var(--nm-esp-4); }

/* Alternativas — «Otros productos parecidos» y «Otras presentaciones».

   Estrena `.fila`, el componente de solo lectura del §5.2, y es su primer
   consumidor: de ahí salen el alto mínimo, el divisor, el `gap` y la
   tipografía del nombre, escritos una sola vez. Lo propio de la alternativa
   son sus dos columnas de plata —el par, y el ahorro contra el producto que
   se está mirando— y que el sello cuelga del monto, como en todas partes.

   El selector es `.alternativa.fila` y no `.alternativa` a secas por la
   cascada: `.fila` se declara mil líneas más abajo —vive con los componentes
   del §5— y con la misma especificidad ganaría, dejando a la alternativa con
   las tres pistas del componente base en vez de sus cuatro. */
.alternativa.fila {
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  column-gap: var(--nm-esp-4);
  row-gap: 2px;
  align-items: baseline;
  width: 100%;
  padding: var(--nm-esp-2) var(--nm-esp-5);
  background: transparent;
  border: 0; border-top: 1px solid var(--nm-borde-sutil);
  color: inherit; font-size: var(--nm-b2);
  text-align: left; cursor: pointer;
  /* es un <a> desde el 2026-09-08: el subrayado es solo del nombre, abajo */
  text-decoration: none;
  transition: background-color var(--nm-mov-base) var(--nm-curva-salida);
}
.alternativas > li:first-child .alternativa,
.alternativas > .alternativa:first-child { border-top: 0; }
@media (hover: hover) and (pointer: fine) {
  .alternativa:hover {   background: var(--nm-superficie-2); }
}
.alternativa:focus-visible { outline-offset: -2px; }
.alternativa > .miniatura { grid-column: 1; grid-row: 1 / -1; align-self: center; }
.alternativa__ident { grid-column: 2; grid-row: 1; min-width: 0; }
.alternativa__par {
  grid-column: 3; grid-row: 1;
  display: flex; align-items: baseline; gap: var(--nm-esp-3);
}
.alternativa__sello { grid-column: 3; grid-row: 2; justify-self: start; }
.alternativa__sello:empty { display: none; }
.alternativa__nombre {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
  overflow: hidden;
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: var(--nm-b1);
  letter-spacing: -.01em; line-height: 1.3;
}
.alternativa__meta { margin-top: 2px; color: var(--nm-texto-3); font-size: var(--nm-b3); }
.alternativa__monto {
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: var(--nm-b1);
  letter-spacing: -.01em; white-space: nowrap; text-align: right;
}
.alternativa__unitario { color: var(--nm-texto-3); font-size: var(--nm-b3); white-space: nowrap; text-align: right; }
.alternativa__ahorro {
  grid-column: 4; grid-row: 1;
  color: var(--nm-texto-3); font-size: var(--nm-b3);
  white-space: nowrap; text-align: right; min-width: 11ch;
}
.alternativa__ahorro strong { color: var(--nm-texto-2); font-weight: 600; }

/* El layout propio de móvil (§6 #13): con cuatro columnas de plata a 375 px
   el nombre se queda sin ancho y envuelve a cuatro renglones. En forma app
   la fila se pliega en dos: arriba el nombre y el ahorro, abajo el par con
   su sello, y la foto cruzando los dos.

   Y el ahorro deja de ser una sola línea: «+S/ 11.38 (107% más)» son 170 px
   sin envolver, o sea más de la mitad de lo que queda después de la foto, y
   el nombre terminaba cortado en «Leche Reconstitui…» con dos renglones
   vacíos al lado. Envolviendo cuesta un renglón de B3 —que la fila ya tiene,
   porque la identidad son dos— y le devuelve 90 px al nombre. El tope de
   13ch es lo que obliga a envolver: una pista `auto` se queda con el
   max-content cuando entra, así que sin tope la celda seguía midiendo 150 px
   aunque pudiera partirse.

   El relleno lateral y el `column-gap` bajan a esp-3 por la misma razón que
   en la fila de la lista: a 375 px, `.contenido` ya paga 16 px de cada lado y
   cada píxel que la fila se guarda se lo lleva el nombre, que es lo que la
   persona lee. Con esp-5 y gap esp-4 el nombre medía 122 px. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .alternativa { grid-template-columns: auto minmax(0, 1fr) auto;
    padding-left: var(--nm-esp-3); padding-right: var(--nm-esp-3); column-gap: var(--nm-esp-3); }
  :root:not([data-forma="web"]) .alternativa__par { grid-column: 2 / -1; grid-row: 2; justify-content: space-between; }
  :root:not([data-forma="web"]) .alternativa__sello { grid-column: 2 / -1; grid-row: 3; }
  :root:not([data-forma="web"]) .alternativa__ahorro { grid-column: 3; grid-row: 1; min-width: 0; max-width: 13ch; white-space: normal; }
}
:root[data-forma="app"] .alternativa { grid-template-columns: auto minmax(0, 1fr) auto;
  padding-left: var(--nm-esp-3); padding-right: var(--nm-esp-3); column-gap: var(--nm-esp-3); }
:root[data-forma="app"] .alternativa__par { grid-column: 2 / -1; grid-row: 2; justify-content: space-between; }
:root[data-forma="app"] .alternativa__sello { grid-column: 2 / -1; grid-row: 3; }
:root[data-forma="app"] .alternativa__ahorro { grid-column: 3; grid-row: 1; min-width: 0; max-width: 13ch; white-space: normal; }

/* ───────────── Lista de compras ───────────── */

.lista { padding-top: 0; }
/* El hero «Arma tu lista» en franja índigo se retiró con el chasis de 2026
   (PANTALLAS §4): la cabecera de 56 px ya dice en qué pestaña estás, y dentro
   de la app no hay franjas. Las reglas que le cedían la pantalla en el
   teléfono se fueron con él. */

/* Barra de listas guardadas */
/* La cuenta, en la cabecera. Deliberadamente discreta: es una oferta
   ("llevate tus listas"), no un portón, así que no compite con el buscador
   ni con la marca. El texto se recorta antes que empujar nada. */
.sesion {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  min-width: 0;
}
/* La inicial de la cuenta: la única señal de "estoy en una cuenta" que no
   se recorta ni se esconde (ver la media query de abajo). Reusa los
   tokens del botón primario —tinta, no oro— porque es lo más cerca que
   este sistema tiene de "un círculo sólido de marca" sin inventar un
   color nuevo para una insignia. */
/* Dejó de ser un adorno el 2026-09-08: es el acceso al perfil, que es
   donde la gente lo busca. Como ahora es <button>, hay que apagarle el
   chrome del navegador o deja de leerse como bolita. */
.sesion__avatar {
  display: flex; align-items: center; justify-content: center;
  flex: none; width: 24px; height: 24px; border-radius: 50%;
  background: var(--nm-texto); color: var(--nm-fondo);
  font: 700 12px/1 var(--nm-cuerpo);
  border: 0; padding: 0; cursor: pointer;
  transition: opacity var(--nm-mov-base) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .sesion__avatar:hover {   opacity: .85; }
}
/* Sin sesión (R51): el disco de tinta se queda —es la puerta a Ajustes
   siempre, con o con cuenta— pero en vez de una inicial lleva la silueta
   genérica, en el mismo color de fondo que usaba la inicial. */
.sesion__avatar--vacio::before {
  content: '';
  width: 14px; height: 14px; background: var(--nm-fondo);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-5 0-9 2.5-9 6v2h18v-2c0-3.5-4-6-9-6Z'/%3E%3C/svg%3E") center / 14px no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-5 0-9 2.5-9 6v2h18v-2c0-3.5-4-6-9-6Z'/%3E%3C/svg%3E") center / 14px no-repeat;
}
.sesion__texto {
  margin: 0; min-width: 0;
  color: var(--nm-texto-3); font-size: var(--nm-b3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sesion__accion {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px;
  border: 1px solid var(--nm-borde-sutil); border-radius: var(--nm-radio-md);
  background: none; color: inherit;
  font: inherit; font-size: var(--nm-b3); line-height: 1;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: background-color var(--nm-mov-entrada) var(--nm-curva-suave),
              border-color var(--nm-mov-entrada) var(--nm-curva-suave);
}
@media (hover: hover) and (pointer: fine) {
  .sesion__accion:hover {   background: var(--nm-superficie-2); }
}
.sesion__g { width: 14px; height: 14px; flex: none; }
/* «Entrar» sin sesión: secundario (el primario de la lista es «Ver dónde pagar
   menos»), a la izquierda de la silueta y a la altura de un toque cómodo. */
#sesion-entrar {
  order: -1; min-height: 32px; padding: 0 12px; gap: 8px;
  font-weight: 600; color: var(--nm-texto);
  border-color: var(--nm-borde);
}

/* En pantallas chicas la cabecera ya va apretada: se deja el botón, que es
   lo accionable, y se esconde el texto — el correo se puede consultar
   tocándolo. */
/* Va al umbral de forma junto con el resto de la cabecera: en tres números
   distintos era lo que producía la banda donde la cabecera ya envolvía pero
   el correo seguía ocupando sitio. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
}

/* El bloque de guardado —Mis listas, el nombre y «Guardar»— salió del flujo
   y vive dentro de la hoja del «···» (PANTALLAS §4). Acá va como el primer
   bloque de esa hoja, separado del resto por un filete. */
.lista-hoja__guardar {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--nm-esp-2);
  padding-bottom: var(--nm-esp-3);
  border-bottom: 1px solid var(--nm-borde);
}
.lista__barra-acciones {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--nm-esp-2);
}
.lista__selector,
.lista__nombre {
  padding: 9px var(--nm-esp-3);
  background: var(--nm-campo);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-md);
  color: var(--nm-texto);
  font-size: 14px;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-entrada) var(--nm-curva-suave);
}
@media (hover: hover) and (pointer: fine) {
  .lista__selector:hover, .lista__nombre:hover {   border-color: var(--nm-borde-fuerte); }
}
.lista__selector:focus, .lista__nombre:focus { outline: none; border-color: var(--nm-borde-fuerte); }
.lista__selector:focus-visible, .lista__nombre:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -1px; border-color: transparent; }
.lista__nombre::placeholder { color: var(--nm-texto-3); }
/* sin min-height: en el panel, una línea vacía siempre reservada empuja al
   reparto sin decir nada; el aviso aparece y empuja solo cuando habla */
.lista__barra-estado { margin: 0; color: var(--nm-texto-3); }
/* un dato, no una alarma: el mismo gris que el estado, sin color de aviso */
.lista__sin-guardar { color: var(--nm-texto-3); }
.lista__barra-estado:empty { display: none; }

/* Aviso de resultado viejo: va sobre los carritos, con el borde punteado
   del resto de avisos — señala sin gritar y sin gastar oro. */
.aviso--viejo { margin-bottom: var(--nm-esp-4); color: var(--nm-texto-2); }
.lista__conteo { margin: 0; color: var(--nm-texto-3); }

/* ───────────── La lista (PANTALLAS §3 P1) ─────────────

   La unidad es la FILA, no la tarjeta: sin borde por fila, sin tarjeta por
   categoría, divisores de 1 px y 72–88 px de alto. Lo que la arma son las
   clases `nm-*` de `componentes.css` (`.nm-fila`, `.nm-categoria`,
   `.nm-stepper`); acá va solo el layout propio de esta pantalla. */

.items-lista {
  list-style: none; margin: 0 0 var(--nm-esp-4); padding: 0;
}

/* «Plegar todas» a la vista, arriba de la lista y en las dos formas (R4).
   El botón queda a la derecha, en terciario: es un acomodo de la vista, no
   una acción de la compra, así que no compite con el único primario. El
   total de productos va a la izquierda, en la misma fila: es la cifra que
   faltaba antes de entrar a cada categoría plegada. */
.lista__plegado {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: var(--nm-esp-1);
}
.lista__plegado-total { color: var(--nm-texto-3); }

/* La cabecera de categoría es una fila tocable a todo el ancho: nombre,
   conteo y chevrón que gira. Nada se pliega solo.

   Pegajosa bajo el cromo de 56 px (R56): con una lista larga, hacer scroll
   dentro de una categoría perdía de vista en cuál se estaba. `position:
   sticky` con `top: var(--nm-cabecera)` cede sola a la siguiente cabecera
   cuando llega —es lo que hace `sticky`, sin JS—, y como `.lista` no abre
   su propio contenedor de scroll (solo `.carritos` lo hace en dos
   columnas, PANTALLAS §2.2), el mismo `top` sirve en app y en web: el
   documento es lo único que se desplaza. El fondo no puede ser transparente
   —se pega justo bajo la cabecera y el texto de las filas pasaría detrás—
   así que toma `--nm-banda`, el mismo fondo que ya pinta la franja donde
   vive la lista. Por debajo del cromo (z-index 40) y del menú del
   desplegable de producto (30).

   El sticky va en el BOTÓN, no en el `<li>`: el `<li>` es la sección
   entera —cabecera + sus filas, en `.items-lista__categoria-filas`— y es
   su alto el que le da al botón dónde recorrer antes de ceder a la
   cabecera siguiente. Un `<li>` que envolviera solo el botón (44 px) no le
   daría margen de recorrido y la cabecera se quedaría pegada para
   siempre. */
.items-lista__categoria { margin: 0; }
.items-lista__categoria .categoria {
  position: sticky; top: var(--nm-cabecera); z-index: 20; background: var(--nm-banda);
}
.items-lista__categoria-filas { list-style: none; margin: 0; padding: 0; }
.categoria { gap: var(--nm-esp-2); color: var(--nm-texto); font-weight: 600; font-size: var(--nm-b2); }
.categoria__titulo { min-width: 0; margin-right: auto; }
.items-lista__cuantos { color: var(--nm-texto-3); font-size: var(--nm-b3); font-weight: 400; margin-right: var(--nm-esp-2); }

/* `hidden` sobre una fila que es flex */
.items-lista .item-fila[hidden] { display: none; }

/* La caja de agregar del pie de cada categoría: control terciario, sin caja
   propia que compita con los productos. */
.items-lista__agregar { list-style: none; }
.items-lista__agregar-boton {
  width: 100%;
  padding: var(--nm-esp-3) 0;
  background: none; border: 0;
  color: var(--nm-texto-3); font-size: var(--nm-b3);
  text-align: left; cursor: pointer;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .items-lista__agregar-boton:hover { color: var(--nm-texto); }
}
.items-lista__agregar-boton:focus-visible { outline-offset: -2px; }

/* La fila con producto elegido. `position: relative` es el ancla de
   `.item-fila__zona`, el botón que cubre el hueco de la fila y abre la ficha
   para editar: va DEBAJO de todo (`z-index: 0` contra el `1` de los hijos),
   así que el nombre, el stepper y Quitar siguen recibiendo su propio toque. */
.item-fila--elegida { position: relative; gap: var(--nm-esp-2); }
/* La caja libre («Escribe y elige…») no lleva `nm-fila`, que es quien daba el
   `display: flex`: sin él el tacho caía en su propio renglón debajo de la
   caja, huérfano. Caja a todo el ancho y Quitar al lado, como en toda fila. */
.item-fila--libre { display: flex; align-items: center; gap: var(--nm-esp-2); }
.item-fila__zona {
  position: absolute; inset: 0; z-index: 0;
  border: 0; background: transparent; cursor: pointer;
  border-radius: var(--nm-radio-md);
}
.item-fila__zona:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -2px; }
/* Lo que sube por encima de la zona son los CONTROLES, no todos los hijos.
   Levantar `.item-fila__producto` entero le daba contexto de apilamiento
   propio, y eso AÍSLA el grupo de composición: la foto, que se funde con el
   `mix-blend-mode: multiply` de `.miniatura`, pasaba a multiplicarse contra
   la nada y salía blanca sobre la banda (lo vio `test_miniaturas_ui`). Suben
   el enlace del nombre, el paso de cantidad y Quitar; el resto de la fila
   —foto y meta— es superficie de la ficha. */
.item-fila--elegida :is(.item-fila__nombre a, .paso-cantidad, .item-fila__quitar,
    .item-fila__deshacer, .item-fila__cantidad-unidad--boton) {
  position: relative; z-index: 1;
}
/* El bloque del producto —foto y textos— es SUPERFICIE de la ficha, no un
   control: deja pasar el toque a la zona que tiene debajo, y el nombre lo
   recupera porque él sí es la puerta a la ficha del producto. Se hace con
   `pointer-events` y no levantándolo con `z-index`, que es lo que rompía la
   fusión de la foto. */
.item-fila--elegida .item-fila__producto { pointer-events: none; }
.item-fila--elegida .item-fila__nombre a { pointer-events: auto; }
.item-fila__producto { display: flex; align-items: center; gap: var(--nm-esp-3); min-width: 0; flex: 1; }
.item-fila__datos { min-width: 0; flex: 1; }
/* El nombre es la puerta a la ficha y va en tinta, SIN subrayado (§4): el
   subrayado por fila hacía de la lista una página de enlaces. */
/* Dos renglones, no uno con elipsis (QA X5): la fila medía 73 px contra un
   techo de 104 y aun así «Suavizante de rop…» y «Papel higiénico ul…» se
   cortaban. El techo estaba ahí sin usarse. */
.item-fila__nombre {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; overflow-wrap: anywhere; line-height: 1.2;
}
/* Con dos renglones de nombre y dos de meta la fila se iba a 115 px y el techo
   de PANTALLAS §1.5 es 104: el aire de la hoja de componentes es el de una
   fila de dos renglones, y acá hay hasta cuatro. Las filas cortas siguen en su
   piso de 72 por el `min-height` de `.nm-fila`. */
.item-fila.nm-fila { padding-top: 10px; padding-bottom: 10px; }
/* El nombre es la puerta a la ficha en la fila de la lista, en la card de
   Buscar y en el título de la hoja: mismo aspecto en las tres (2026-09-25). */
:is(.item-fila__nombre a, .resultado-fila__enlace, .ficha-hoja__nombre-enlace) { color: inherit; text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  :is(.item-fila__nombre a, .resultado-fila__enlace, .ficha-hoja__nombre-enlace):hover { text-decoration: underline; text-underline-offset: 3px; }
}
:is(.item-fila__nombre a, .resultado-fila__enlace, .ficha-hoja__nombre-enlace):focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; border-radius: 3px; }
.item-fila__nombre .etiqueta { font-family: var(--nm-cuerpo); }
/* La meta ENVUELVE a un segundo renglón en vez de elipsar (QA X5). Era una
   caja flex con la cifra fija y la marca encogiendo, y en un teléfono la pista
   mide 152 px: la cifra se llevaba 90 y la marca se cortaba SIEMPRE —«Tottus
   Fr… · desde S/ 5.49», y hasta «Gloria · 6 …», que es un caso corto—. El
   corte no había desaparecido: se había mudado del precio a la marca. */
.item-fila__unidad {
  margin-top: 2px;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; line-height: 1.35;
}
/* La cifra no se parte por dentro; el renglón se corta antes de ella. */
.item-fila__meta-cifra, .item-fila__meta-sep { white-space: pre; }
/* La copia de la meta que el teléfono pone junto al nombre (ver la fila en el
   teléfono, más abajo); en la forma web la meta sigue en su renglón. */
.item-fila__nombre-meta { display: none; }
.item-fila__aviso {
  margin: var(--nm-esp-1) 0 0;
  color: var(--nm-texto-3); font-size: var(--nm-b3); line-height: 1.45;
}
.item-fila__cantidad-grupo {
  flex: none; display: flex; align-items: center; gap: 4px;
}
/* La unidad del stepper, visible junto al número —antes solo vivía en el
   aria-label del «−» (hallazgo QA zona C #1)—. Texto terciario, no se
   envuelve ni empuja el ancho del stepper en móvil. */
.item-fila__cantidad-unidad {
  flex: none; color: var(--nm-texto-3); white-space: nowrap;
  /* Ancho fijo, el del chip «und ⇄»: con el rótulo suelto («kg») más angosto
     que el chip, los steppers de una lista mixta quedaban cada uno a otra
     altura del eje (Gonzalo, dev, 2026-09-25). Así forman una columna. */
  box-sizing: border-box; width: 3.75rem;
  /* Centrado en su espacio, como el chip «kg ⇄» que lo llena: pegado a la
     izquierda, el rótulo suelto se leía corrido respecto de los chips de las
     filas vecinas (Gonzalo, dev, 2026-09-25). */
  text-align: center; justify-content: center;
}
/* POR UNIDAD O POR KILO (2026-09-25): en un producto de la lista curada
   (papaya, palta, pollo entero…) la unidad deja de ser un rótulo mudo y
   pasa a ser el botón que alterna "und" ⇄ "kg" — mismo tamaño de blanco de
   toque que el resto del stepper, sin ensanchar la fila en 375 px. */
/* Con borde y más tinta que el rótulo mudo: sin caja se leía como el «kg» de
   siempre y nadie encontraba cómo cambiarlo (Gonzalo, dev, 2026-09-25). */
.item-fila__cantidad-unidad--boton {
  display: inline-flex; align-items: center; gap: 3px;
  min-height: 32px; padding: 0 var(--nm-esp-2);
  border: 1px solid var(--nm-borde); border-radius: 999px;
  background: var(--nm-superficie-1); color: var(--nm-texto-2);
  /* La letra del rótulo (`nm-b3`, 13 px), no la del padre: con `font: inherit`
     el chip heredaba 15 px, salía más grande que «kg» y «und ⇄» no entraba
     en su espacio del teléfono (Gonzalo, dev, 2026-09-26). */
  font-family: var(--nm-cuerpo); font-size: var(--nm-b3); line-height: 1.2;
  cursor: pointer;
}
.item-fila__icono-cambio { flex: none; width: 0.95em; height: 0.95em; }
.item-fila__cantidad-unidad--boton:focus-visible {
  outline: 2px solid var(--nm-foco); outline-offset: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .item-fila__cantidad-unidad--boton:hover { color: var(--nm-texto); border-color: var(--nm-texto-3); }
}
/* El selector de tamaño (PLAN_CANTIDADES.md, regla 6): la píldora con el
   tamaño que pide la fila, «de 390 g ▾», que en Buscar, en la fila y en la
   hoja es el mismo control. Un `<select>` nativo, invisible, cubre la píldora:
   el teléfono abre su selector, el teclado lo opera y el lector de pantalla
   lo lee como lo que es. La píldora dice lo corto; las opciones, lo largo. */
.selector-tamano {
  position: relative; flex: none; display: inline-flex; align-items: center; gap: 4px;
  min-height: 34px; box-sizing: border-box; padding: 0 var(--nm-esp-2) 0 12px;
  border: 1px solid var(--nm-borde); border-radius: 999px;
  background: var(--nm-superficie-1); color: var(--nm-texto-2);
  font-family: var(--nm-cuerpo); font-size: var(--nm-b3); line-height: 1.2;
  white-space: nowrap; cursor: pointer;
}
.selector-tamano__texto { font-variant-numeric: tabular-nums; }
.selector-tamano__chevron {
  flex: none; width: 15px; height: 15px; margin-right: -2px;
}
.selector-tamano__select {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer; font-size: 16px;
  -webkit-appearance: none; appearance: none;
}
.selector-tamano:has(.selector-tamano__select:focus-visible) {
  outline: 2px solid var(--nm-foco); outline-offset: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .selector-tamano:hover { color: var(--nm-texto); border-color: var(--nm-texto-3); }
}
/* En la fila del teléfono los controles van solos en su renglón, a la
   izquierda, bajo el nombre. */
.item-fila--elegida .item-fila__cantidad-grupo { justify-self: start; gap: 8px; }

/* "≈ 2.2 kg" bajo la meta de la fila, en unidades: el peso es dato
   secundario del pedido por pieza, nunca compite con la cantidad ni con el
   nombre. */
.item-fila__peso-aprox {
  margin: 1px 0 0; color: var(--nm-texto-3); white-space: nowrap;
}
/* Quitar: ícono reconocible y más tinta que el resto de los controles, que es
   la acción que hay que encontrar sin buscarla (DESIGN.md, y la memoria «una
   acción que se busca lleva ícono»). Se deshace en el mismo lugar. */
.item-fila__quitar {
  flex: none; width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--nm-radio-full);
  background: transparent; color: var(--nm-texto-2); cursor: pointer;
}
.item-fila__quitar svg { width: 19px; height: 19px; }
@media (hover: hover) and (pointer: fine) {
  .item-fila__quitar:hover { background: var(--nm-superficie-2); color: var(--nm-texto); }
}

/* Carritos, la línea de producto (PLAN_CANTIDADES.md, regla 4): lo que
   llega, con las palabras de la fila y la instrucción de empaque —«20 latas de
   410 g · 3 six packs + 2 sueltas»—, en tinta y con peso, porque es lo que
   se lee para comprar. «de tu lista: …» queda debajo en terciario. */
.carrito__item-entrega {
  margin: 2px 0 0; color: var(--nm-texto); font-weight: 600;
  font-size: var(--nm-b2); line-height: 1.35;
}

/* La fila ya comprada (Gonzalo, 2026-09-28): un carrito congelado no se
   rebalancea, así que su producto tampoco se edita ni se quita desde acá.
   Apagada un poco, como `.carrito__comprado--hecho`, y en el lugar del
   tacho, quién lo compró y dónde — terciario, sin oro ni verde. */
.item-fila--comprada { opacity: .7; }
.item-fila__comprado { margin: 1px 0 0; color: var(--nm-texto-2); }
/* El ✓ ocupa la misma casilla que el tacho (36 × 36): así la columna de
   steppers no se mueve entre filas compradas y pendientes. */
.item-fila__comprado-marca {
  flex: none; width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  color: var(--nm-texto-2);
}
.item-fila__comprado-marca .nm-ico { width: 18px; height: 18px; }

/* PLAN_CANTIDADES.md (2026-09-29): el control de la fila es «número + tamaño»
   («[− 20 +] [de 390 g ▾]», unos 220 px) y ya no cabe junto a la cifra en 375
   px. La fila sigue en DOS renglones, pero cambian de contenido: arriba el
   nombre con Quitar a su lado y, pegados al nombre en gris, los filtros y el
   piso («solo botella · desde S/ 4.60»); abajo los controles solos, a la
   izquierda. Lo que sigue es el comentario del arreglo de DOS renglones del
   2026-09-25, que este bloque conserva en su espíritu.

   La fila en el teléfono, en DOS renglones y no en tres (Gonzalo, 2026-09-25:
   «hazlo más eficiente»). Arriba el nombre a todo el ancho —no se parte—, y
   en gris a su lado la marca y la presentación («Papaya  5 marcas · a
   granel», que casi siempre caben: el nombre mide ~110 px de mediana sobre
   291), y abajo la cifra sola junto al stepper y Quitar. Antes eran tres renglones —nombre, meta
   y los controles solos abajo—, 114 px por fila y tres productos y medio por
   pantalla. Medido a 375 px: la pista de texto es de 291 px y los controles de
   dedo pedían 234, así que se compactan a ~200 (botones de 34 de ancho con
   sus 44 de alto, unidad de 44, foto de 32) para que la cifra quepa al lado.
   `display: contents` saca foto, nombre y meta de sus cajas para que la grilla
   de la fila los ubique. La meta no baja nunca de lo que mide la cifra
   (`minmax(min-content, …)`), así que la columna de steppers no se mueve. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .item-fila--elegida {
    display: grid; grid-template-columns: 32px minmax(min-content, 1fr) auto auto;
    grid-template-areas: "foto nombre nombre quitar" "foto cantidad cantidad cantidad";
    column-gap: 6px; row-gap: 2px; align-items: center;
  }
  :root:not([data-forma="web"]) .item-fila--elegida :is(.item-fila__producto, .item-fila__datos) {
    display: contents;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .miniatura {
    grid-area: foto; align-self: start; width: 32px; height: 32px;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__nombre {
    grid-area: nombre;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__unidad {
    display: none;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__meta-cifra {
    order: -2;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__meta-sep {
    display: none;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__meta-texto {
    display: none;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__nombre-meta {
    display: inline; margin-left: 0.45em; font: 400 var(--nm-b3) / 1.2 var(--nm-cuerpo); color: var(--nm-texto-3);
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__cantidad-grupo {
    grid-area: cantidad;
  }
  :root:not([data-forma="web"]) .item-fila--elegida > .item-fila__quitar {
    grid-area: quitar;
  }
  :root:not([data-forma="web"]) .item-fila--elegida :is(.item-fila__aviso, .item-fila__peso-aprox) {
    grid-column: 2 / -1; margin-top: 0;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .paso-cantidad__boton {
    width: 34px;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .paso-cantidad .item-fila__cantidad {
    width: 4ch;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__cantidad-unidad {
    width: 3.75em;
  }
  :root:not([data-forma="web"]) .item-fila--elegida .item-fila__cantidad-unidad--boton {
    padding: 0 4px; gap: 2px;
  }
}
:root[data-forma="app"] .item-fila--elegida {
  display: grid; grid-template-columns: 32px minmax(min-content, 1fr) auto auto;
  grid-template-areas: "foto nombre nombre quitar" "foto cantidad cantidad cantidad";
  column-gap: 6px; row-gap: 2px; align-items: center;
}
:root[data-forma="app"] .item-fila--elegida :is(.item-fila__producto, .item-fila__datos) {
  display: contents;
}
:root[data-forma="app"] .item-fila--elegida .miniatura {
  grid-area: foto; align-self: start; width: 32px; height: 32px;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__nombre {
  grid-area: nombre;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__unidad {
  display: none;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__meta-cifra {
  order: -2;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__meta-sep {
  display: none;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__meta-texto {
  display: none;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__nombre-meta {
  display: inline; margin-left: 0.45em; font: 400 var(--nm-b3) / 1.2 var(--nm-cuerpo); color: var(--nm-texto-3);
}
:root[data-forma="app"] .item-fila--elegida .item-fila__cantidad-grupo {
  grid-area: cantidad;
}
:root[data-forma="app"] .item-fila--elegida > .item-fila__quitar {
  grid-area: quitar;
}
:root[data-forma="app"] .item-fila--elegida :is(.item-fila__aviso, .item-fila__peso-aprox) {
  grid-column: 2 / -1; margin-top: 0;
}
:root[data-forma="app"] .item-fila--elegida .paso-cantidad__boton {
  width: 34px;
}
:root[data-forma="app"] .item-fila--elegida .paso-cantidad .item-fila__cantidad {
  width: 4ch;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__cantidad-unidad {
  width: 3.75em;
}
:root[data-forma="app"] .item-fila--elegida .item-fila__cantidad-unidad--boton {
  padding: 0 4px; gap: 2px;
}
/* La fila comprada en el teléfono (Gonzalo, 2026-09-28): el ✓ ocupa la
   casilla «quitar» del tacho, del mismo tamaño, y «Comprado en Plaza Vea»
   baja a un tercer renglón propio bajo la meta; el stepper sigue en su
   columna, apagado. */
:root:not([data-forma="web"]) .item-fila--comprada,
:root[data-forma="app"] .item-fila--comprada {
  grid-template-areas: "foto nombre nombre quitar" "foto cantidad cantidad cantidad" ". comprado comprado comprado";
}
:root:not([data-forma="web"]) .item-fila--comprada .item-fila__comprado,
:root[data-forma="app"] .item-fila--comprada .item-fila__comprado {
  grid-area: comprado;
}
:root:not([data-forma="web"]) .item-fila--comprada > .item-fila__comprado-marca,
:root[data-forma="app"] .item-fila--comprada > .item-fila__comprado-marca {
  grid-area: quitar;
}
/* La fila que acaba de aterrizar (Gonzalo, 2026-09-28, «dónde cayó lo
   agregado»): entra con un fade + slide corto —opacidad y `translateY`,
   nunca `top`/`margin`, para no disparar layout— y un resalte cal, sin oro
   (§8), que se sostiene y se apaga solo. `renderFilas` (lista.js) prende
   `--resaltada` al pintar, agrega `--apagando` a los 600 ms (dispara la
   transición de `background-color` de abajo) y quita las dos a los 900.
   300 ms es el tope de PANTALLAS §9.1 regla 6 fuera de la portada
   (`test_nada_dura_mas_de_300ms_fuera_de_la_portada`) — el fade quedó en
   ese tope en vez de en los 900 ms del primer borrador. */
.item-fila--resaltada {
  background: var(--nm-superficie-2);
  animation: item-fila-aterriza 200ms cubic-bezier(0.23, 1, 0.32, 1);
}
.item-fila--resaltada.item-fila--apagando {
  background: transparent;
  transition: background-color 300ms ease-out;
}
@keyframes item-fila-aterriza {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
/* Sin movimiento: se queda solo el resalte estático que se apaga —nunca el
   fade/slide de entrada, que ES el movimiento que este modo pide evitar. */
@media (prefers-reduced-motion: reduce) {
  .item-fila--resaltada { animation: none; }
}

/* La caja donde se escribe el producto que sigue. */
.item-fila__buscar { position: relative; flex: 1; min-width: 0; }
.item-fila__cantidad,
.item-fila__texto {
  padding: 9px var(--nm-esp-3);
  background: var(--nm-campo);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-md);
  color: var(--nm-texto); font: inherit; font-size: 16px;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.item-fila__texto { width: 100%; }
.item-fila__cantidad:focus, .item-fila__texto:focus { outline: none; border-color: var(--nm-texto); }
.item-fila__cantidad:focus-visible, .item-fila__texto:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -1px; border-color: transparent; }
.item-fila__texto::placeholder { color: var(--nm-texto-3); }

/* El stepper de la fila: es el `.nm-stepper` de la hoja de componentes, con
   el campo de número en vez del `<output>`. No anima el número —es el gesto
   más frecuente de la pantalla (§9.3)—, solo la presión de sus botones. */
.paso-cantidad { flex: none; }
.paso-cantidad__boton {
  width: 32px; min-height: 34px;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent;
  color: var(--nm-texto-2); font-size: var(--nm-b2); line-height: 1; cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .paso-cantidad__boton:hover { color: var(--nm-texto); }
}
.paso-cantidad__boton:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -2px; }
.paso-cantidad .item-fila__cantidad {
  width: 4.5ch; padding: 0 2px; text-align: center;
  border: 0; background: transparent; border-radius: 0;
  font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
/* En dedo, un campo editable de menos de 16px hace zoom a ciegas al
   enfocarlo en iOS (hallazgo QA zona C #3): el `ch` de `width` escala con la
   fuente, así que subir el tamaño no rompe el stepper. */
@media (pointer: coarse) {
  .paso-cantidad .item-fila__cantidad { font-size: 16px; }
}
.paso-cantidad .item-fila__cantidad::-webkit-outer-spin-button,
.paso-cantidad .item-fila__cantidad::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.paso-cantidad .item-fila__cantidad:focus-visible { outline-offset: -2px; }

.item-fila--quitada {
  display: flex; align-items: center; justify-content: space-between; gap: var(--nm-esp-3);
  min-height: 56px; padding: var(--nm-esp-3) 0;
  border-bottom: 1px solid var(--nm-borde);
  color: var(--nm-texto-3); font-size: var(--nm-b3);
}
.item-fila__quitada { margin: 0; min-width: 0; }
.item-fila__deshacer { font-size: var(--nm-b3); }

/* Fila con producto ya elegido.

   El interlineado es 1.2 y no 1.25: es el que `tokens.css` le da a la clase
   `.nm-d4`, o sea el que el sistema declara para un título de fila, y acá
   había una copia local con cinco centésimas de más. Son 1.05 px por renglón
   —70 px medidos sobre la compra de ejemplo, con dieciocho filas de dos
   renglones— y sobre todo es una divergencia entre la escala y su uso. */

.lista__acciones {
  display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap;
  gap: var(--nm-esp-4);
  margin-bottom: var(--nm-esp-6);
}

/* El primario del sistema. El rediseño 2026 movió los primarios nuevos a
   `.nm-boton.nm-boton--primario` (componentes.css) y con eso se perdió la
   regla base de `.boton-primario`, el nombre con el que nació en el
   producto: los cuatro que todavía lo usan —«Siguiente» del tour, la
   confirmación de listas, «Borrar compra» y «Guardar receta»— quedaron
   pintados como botones grises del sistema operativo, y ese gris es
   crema-ish, que DESIGN.md prohíbe. Es el MISMO componente que
   `.nm-boton--primario`: tinta de relleno, tipografía de la tinta, píldora.
   Nace a ancho completo porque su sitio natural es el panel del reparto; los
   sitios donde va en línea ya lo ciñen con su propio modificador. */
.boton-primario {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; margin-top: var(--nm-esp-4);
  padding: 11px var(--nm-esp-5);
  background: var(--nm-primario);
  color: var(--nm-primario-tinta);
  border: 1px solid transparent;
  border-radius: var(--nm-radio-full);
  font: 700 var(--nm-b2) / 1.2 var(--nm-cuerpo);
  cursor: pointer; white-space: nowrap;
  transition: background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .boton-primario:hover { background: var(--nm-primario-hover); }
}
.boton-primario[disabled], .boton-primario[aria-disabled="true"] { opacity: .45; pointer-events: none; }

/* El secundario del sistema: transparente, borde `outline`, texto
   secundario (`DESIGN.md`, Components). Se llama de dos maneras y es UNA
   sola: `boton-fantasma` es el nombre con el que nació en el producto y
   `boton-secundario` el que usa `ESPEC_UI` (§5.4, §5.5). Son alias a
   propósito y no dos componentes: dos botones secundarios que se ven
   distinto es exactamente lo que este sistema no tiene. */
.boton-fantasma, .boton-secundario {
  padding: 9px var(--nm-esp-5);
  background: transparent;
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2);
  font-weight: 500; font-size: 14px;
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida), color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .boton-fantasma:hover, .boton-secundario:hover {   border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
}

/* Las opciones del reparto, ahora dentro de la hoja del «···» de Carritos
   (PANTALLAS §3 P4). El registro es el mismo de siempre —casillas
   terciarias, sin bordes ni rellenos que compitan— porque las chips se
   probaron el 2026-08-26 y quedaron peor: sin sus explicaciones cortas, las
   opciones no se entendían. */
.carritos-hoja { display: grid; gap: var(--nm-esp-4); }
.carritos-hoja .opciones__estado { margin: 0; color: var(--nm-texto-3); }
.opcion__nota { margin: 0; color: var(--nm-texto-3); font-size: var(--nm-b3); line-height: 1.45; max-width: var(--nm-medida); }

.opcion {
  display: flex; align-items: flex-start; gap: var(--nm-esp-3);
  font-size: 14px; line-height: var(--nm-lh-ui); cursor: pointer;
}
/* texto terciario y no maíz: dos casillas marcadas en el mismo bloque
   serían dos acentos compitiendo */
.opcion input[type="checkbox"] {
  flex: none; width: 15px; height: 15px; margin-top: 3px;
  accent-color: var(--nm-texto-3); cursor: pointer;
}
.opcion small {
  display: block; margin-top: 2px;
  color: var(--nm-texto-3); font-size: var(--nm-b3); line-height: 1.45;
}
.opcion--select { align-items: center; justify-content: space-between; gap: var(--nm-esp-3); }
.opcion--select select {
  padding: 6px var(--nm-esp-2);
  background: var(--nm-campo);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-md);
  color: var(--nm-texto);
  font-size: var(--nm-b3);
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-entrada) var(--nm-curva-suave);
}
@media (hover: hover) and (pointer: fine) {
  .opcion--select select:hover {   border-color: var(--nm-borde-fuerte); }
}

/* "No compro en": mismo registro tranquilo que el resto de opciones —
   casillas terciarias, sin bordes ni rellenos. Dos columnas porque son
   hasta nueve tiendas y en una sola el panel se estira sin necesidad. */
.opcion-tiendas { border: 0; padding: 0; margin: 0; min-width: 0; }
.opcion-tiendas__titulo {
  padding: 0;
  font-size: 14px; line-height: var(--nm-lh-ui);
}
.opcion-tiendas__lista {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--nm-esp-1) var(--nm-esp-3);
  margin-top: var(--nm-esp-2);
}
.opcion-tienda {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  min-width: 0;
  color: var(--nm-texto-2); font-size: var(--nm-b3); line-height: var(--nm-lh-ui);
  cursor: pointer;
}
.opcion-tienda span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opcion-tienda input[type="checkbox"] {
  flex: none; width: 15px; height: 15px;
  accent-color: var(--nm-texto-3); cursor: pointer;
}
/* Tienda sin ruta de un clic, excluida por "Solo de un clic" (2026-09-28):
   marcada y sin poder destildarse. Sin oro — un asterisco es puntuación, no
   dato — el mismo terciario que ya usa la nota de abajo. */
.opcion-tienda:has(input:disabled) { cursor: not-allowed; color: var(--nm-texto-3); }
.opcion-tienda input:disabled { cursor: not-allowed; }
.opcion-tienda__asterisco { color: var(--nm-texto-3); margin-left: 1px; }
.opcion-tiendas__nota {
  display: block; margin-top: var(--nm-esp-2);
  color: var(--nm-texto-3); font-size: var(--nm-b3); line-height: 1.45;
}

/* ───────────── Carritos (PANTALLAS §3 P4) ─────────────

   La pestaña donde se ve dónde pagar menos y se sale a comprar. Lo que la
   arma son las clases `nm-*` de `componentes.css` —`.nm-trofeo`,
   `.nm-tienda`, `.nm-escalon`, `.nm-barra`—; acá vive solo el layout propio
   de la pantalla y lo que el quipu-chart necesita para dibujarse.

   El censo de oro de esta pantalla: el trofeo, el monto del colgante focal y
   su nudo. Nada más (DESIGN.md, «dónde vive el maíz»). */

.carritos__cabecera {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  min-height: 56px;
}
.carritos__cabecera .vista__titulo { margin: 0; flex: 1 1 auto; }
/* En forma app el título ya lo dice el cromo de 56 px de la cabecera, y el
   «···» también vive allá arriba (QA G12): acá no queda nada, así que la
   fila entera se va. En web la cabecera no pinta título y el de la sección
   es el que se ve. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .carritos__cabecera { display: none; }
}
:root[data-forma="app"] .carritos__cabecera { display: none; }

/* El «···» del reparto se pinta SOLO con la pestaña Carritos delante. Es la
   misma regla que `vistas.js` aplica a los otros dos con `hidden`, escrita
   en CSS para no tocar ese módulo: el estado ya está en el DOM (`#carritos`
   deja de estar `hidden` cuando la pestaña está puesta) y basta leerlo.
   En dos columnas se retira: ahí el único «···» de la fila es el de la
   lista, que lleva su título al lado (ver el bloque de `[data-chasis=par]`). */
.carritos__mas { display: none; }
body:has(#carritos:not([hidden])) .cabecera__acciones > .carritos__mas { display: flex; }
/* El `#principal` va en el selector a propósito: `:has()` toma la
   especificidad de su argumento más específico, así que sin el id esta regla
   pesaba menos que la de arriba —que lleva `#carritos` adentro— y no la
   apagaba nunca (se vio en la captura de 1280: dos «···» en la misma fila). */
body:has(#principal[data-chasis="par"]) .cabecera__acciones > .carritos__mas { display: none; }
.carritos__cuerpo {
  /* `minmax(0, 1fr)` y no `auto`: la tira de la escalera desborda de lado a
     propósito y con `auto` le dictaba a la columna su ancho mínimo, y toda la
     pestaña se salía del teléfono (visto en la captura del 2026-09-16). */
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--nm-esp-4);
}

/* La barra de compromiso de la pestaña: total, carritos y el único primario.
   No es la de la lista (`.barra-compromiso`, que enciende `html.con-barra`):
   esta la enciende `reparto.js` quitándole el `hidden` en cuanto hay lista, y
   se pega abajo, un punto por debajo del tab bar, como aquella. */
.carritos__barra { margin-top: var(--nm-esp-4); border-radius: var(--nm-radio-lg); }
.carritos__barra[hidden] { display: none; }
.carritos__barra-resumen { display: flex; flex-direction: column; margin: 0; min-width: 0; }
.carritos__barra .nm-boton { flex: none; margin-left: auto; }
/* Sin resumen (lista sin generar: `pintarBarraCarritos` esconde el `<p>` y
   deja solo el primario, R48 2026-09-25) el único hijo con `margin-left:
   auto` quedaba pegado a la derecha con un hueco vacío a la izquierda —la
   barra se leía como una tarjeta suelta, no como el chasis estándar de las
   otras barras de compromiso. Con un solo hijo, que ocupe todo el ancho. */
.carritos__barra-resumen[hidden] + .nm-boton { flex: 1; width: 100%; margin-left: 0; }
.carritos__desglose {
  display: flex; flex-wrap: wrap; gap: var(--nm-esp-2) var(--nm-esp-4);
  margin-top: var(--nm-esp-4);
}
.reparto__congelados { margin: var(--nm-esp-2) 0 0; }
.reparto__nota { margin: var(--nm-esp-4) 0 0; text-align: center; min-height: 1.2em; }
.reparto__ahorro { display: block; margin: var(--nm-esp-2) 0 0; color: var(--nm-texto-3); font-size: var(--nm-b3); }
.reparto__ahorro strong { color: var(--nm-texto-2); font-weight: 600; }

/* El trofeo: la única cifra en Bitter y en oro. Cuando el héroe deja de ser
   el ahorro y pasa a ser el total, va en tinta y en la sans — un total no es
   un premio. */
.nm-trofeo__cifra--tinta {
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: 34px;
  letter-spacing: -0.01em; color: var(--nm-texto);
}

/* Las tarjetas por tienda. El logo va en su color real y pequeño; sin logo,
   el monograma tonal.

   A COLOR y no en monocromo (QA P3): PANTALLAS §3 P4 los pide «en su color»
   porque son el dato de identidad de la tienda, y §8 solo los pasa a tinta
   «si el censo de color de la pantalla se pasa por los logos». Lo que se
   pasaba era el TAMAÑO: 36 px, cuando §8 los fija en 20–24. A 24 px cuatro
   logos seguidos son cuatro puntos y no cuatro manchas, y el censo vuelve a
   estar en el trofeo y el nudo. */
.carritos { list-style: none; margin: 0; padding: 0; }
.carritos .nm-tienda__logo { width: 24px; height: 24px; }
.carritos > li { margin: 0; }
.nm-tienda { width: 100%; text-align: left; font: inherit; color: inherit; }
.nm-tienda__logo--letra {
  display: flex; align-items: center; justify-content: center;
  background: var(--nm-superficie-2);
  border: 1px solid var(--nm-borde);
  color: var(--nm-texto-2); font-weight: 700; font-size: 15px;
}
.nm-tienda__chevron { width: 18px; height: 18px; color: var(--nm-texto-3); flex: 0 0 auto; }
/* La cobertura parte en vez de cortarse —«· asistido» tiene que leerse
   entero—, pero como TEXTO y no como caja flex: con `display: flex` el check
   era un ítem y la frase otro, así que el ✓ se quedaba solo en el primer
   renglón y «10 de 10 · envío S/ 9.90 · asistido» caía al segundo, montado
   sobre el logo (QA G13). Como `inline` el ✓ es una palabra más de la frase
   y la línea parte donde le toca. */
.nm-tienda__cobertura {
  display: block; white-space: normal; overflow: visible; text-overflow: clip;
}
.nm-tienda__cobertura .nm-ico {
  display: inline-block; vertical-align: -2px; margin-right: 4px; flex: none;
}
/* El cuerpo es el que cede ancho: sin `min-width: 0` una frase larga empuja
   al total fuera de la tarjeta en vez de partirse. */
.nm-tienda > .nm-fila__cuerpo { flex: 1 1 auto; min-width: 0; }
.nm-tienda--comprada { color: var(--nm-texto-2); }
/* R48: el check de la tarjeta comprada va en tinta plena, no en el gris
   apagado del resto de la tarjeta — es el indicador que dice "esta ya
   quedó". */
.carrito__check-comprado { display: inline-flex; flex: none; color: var(--nm-texto); margin-right: var(--nm-esp-2); }
.carrito__check-comprado .nm-ico { width: 18px; height: 18px; }
/* Tocar un colgante del quipu resalta su tarjeta: sube un escalón tonal, sin
   oro — el oro del bloque ya lo tiene el nudo. */
.nm-tienda--resaltada { background: var(--nm-superficie-2); border-color: var(--nm-borde-fuerte); }
.carritos-comprados { margin-top: var(--nm-esp-5); }
.carritos-comprados__titulo { margin: 0 0 var(--nm-esp-3); color: var(--nm-texto-3); }

/* El quipu-chart del reparto (DESIGN.md, uso 3 del motivo). Cuerda madre
   horizontal, un colgante por tienda proporcional al gasto con 24 px de
   mínimo, y el nudo de oro en el de MAYOR gasto. Máximo cinco; con más pasa
   a tabla, que a esa cantidad se lee mejor que un gráfico apretado. */
.qchart { margin-top: var(--nm-esp-5); }
.qchart svg { display: block; width: 100%; height: auto; max-width: 420px; margin: 0 auto; }
.qchart__colgante { cursor: pointer; }
.qchart__tienda { font: 500 12px var(--nm-cuerpo); fill: var(--nm-texto-3); }
.qchart__monto {
  font: 700 14px var(--nm-cuerpo); fill: var(--nm-texto);
  font-variant-numeric: tabular-nums;
}
.qchart__monto--focal { fill: var(--nm-acento); }
.qchart--tabla { margin-top: var(--nm-esp-4); }
.reparto__tabla { width: 100%; border-collapse: collapse; font-size: var(--nm-b3); }
.reparto__tabla th, .reparto__tabla td { padding: 7px 0; text-align: right; }
.reparto__tabla th[scope="row"], .reparto__tabla thead th:first-child { text-align: left; font-weight: 500; }
.reparto__tabla thead th {
  color: var(--nm-texto-3); font-size: var(--nm-l1); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  padding-bottom: var(--nm-esp-2); border-bottom: 1px solid var(--nm-borde-sutil);
}
.reparto__tabla tbody tr + tr th, .reparto__tabla tbody tr + tr td { border-top: 1px solid var(--nm-borde-sutil); }

/* La escalera: una TIRA horizontal debajo de las tarjetas, dentro de su
   misma franja. Nunca dice «ahorro»: se publica una sola cifra de ahorro y
   es la del trofeo. */
.escalera { margin-top: var(--nm-esp-5); }
.escalera__titulo { margin: 0 0 var(--nm-esp-1); }
.escalera__bajada { margin: 0 0 var(--nm-esp-3); max-width: var(--nm-medida); }
.escalera__cierre { margin: var(--nm-esp-4) 0 0; color: var(--nm-texto-3); font-size: var(--nm-b3); max-width: var(--nm-medida); }
/* La tira se desplaza de lado y TIENE que decirlo. Hasta el QA los peldaños
   crecían con su contenido, así que el último quedaba cortado a la mitad de
   una palabra —«S/ 45.69 m…», «otra cantida…»— y nada distinguía eso de un
   texto mal truncado (QA G14). Con un ancho fijo, el peldaño siguiente
   ASOMA entero por el borde: ese trozo de tarjeta es el indicio, que es como
   lo resuelven los carriles del inventario, y el `scroll-snap` deja cada
   peldaño alineado al soltarlo. */
.escalera__tira {
  display: flex; gap: var(--nm-esp-2);
  list-style: none; margin: 0; padding: 2px;
  overflow-x: auto; scrollbar-width: none;
  scroll-snap-type: x proximity; scroll-padding-left: 2px;
  overscroll-behavior-x: contain;
}
.escalera__tira::-webkit-scrollbar { display: none; }
.escalera__peldano {
  flex: 0 0 auto; display: flex; width: 232px; scroll-snap-align: start;
}
.escalera__peldano > .nm-escalon { width: 100%; }
.nm-escalon {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  text-align: left; font: inherit; color: inherit;
}
.peldano--elegido { border-color: var(--nm-texto); }
.peldano__minimo { display: block; margin-top: var(--nm-esp-2); }

/* ───────────── El carrito de una tienda, en push ─────────────

   Una capa sobre la pestaña: cabecera con «‹ Carritos», el nombre de la
   tienda y «Lo compré»; las filas con su producto real; y la barra fija con
   la cifra y el único primario de la capa. En escritorio es una columna
   centrada, que es lo que el push se vuelve donde hay ancho. */
.carrito-push {
  /* `min-width: 0`: `main` es flex y sin esto la capa toma el ancho mínimo
     de su fila más larga —la línea «de tu lista: …» no parte— y se sale del
     teléfono. */
  width: 100%; min-width: 0; box-sizing: border-box;
  max-width: 680px; margin: 0 auto;
  /* R53 (Gonzalo, 2026-09-17, Grave): la cabecera, el resumen y las filas no
     tenían un solo padding horizontal en común —cada uno era `padding: … 0`—
     así que la miniatura tocaba el borde izquierdo y el precio el derecho,
     sin el aire que sí tenía el botón «Armar en X» dentro de `.nm-barra` (que
     trae su propio `padding: 12px 20px`). El padding va acá, en el
     contenedor, para que cabecera/meta/filas/pasos compartan el mismo borde
     que la barra ve por dentro del suyo. Esto alinea la TARJETA de las filas
     (el `<ul>` con borde propio) con ese borde — no reemplaza el padding que
     `.carrito__item` necesita para su propio contenido, que vive donde está
     el borde de esa tarjeta (ver el comentario junto a `.carrito__item`). */
  padding: 0 var(--nm-esp-4) var(--nm-esp-6);
}
.push__cabecera {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  min-height: 56px;
}
.push__titulo { flex: 1 1 auto; margin: 0; }
.push__cabecera .carrito__comprado { flex: 0 0 auto; height: 40px; }
.push__meta { margin: 0 0 var(--nm-esp-3); }
.push__filas { list-style: none; margin: 0; padding: 0; }
.push__barra { margin-top: var(--nm-esp-4); border-radius: var(--nm-radio-lg); }
/* El primario de la capa. Es un `<a>` con una sola clase —así lo busca
   `test_enlaces_externos`— y por eso el aspecto de botón se escribe acá y no
   con las utilidades de `componentes.css`. */
.carrito__enlace {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 44px; padding: 0 20px;
  background: var(--nm-primario); color: var(--nm-primario-tinta);
  border: 0; border-radius: var(--nm-radio-full);
  font: 700 15px / 1 var(--nm-cuerpo); text-decoration: none; cursor: pointer;
  transition: transform var(--nm-t-toque, 120ms) var(--nm-curva-salida);
}
.carrito__enlace:active { transform: scale(0.97); }
.carrito__enlace:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }

/* La fila del carrito. El producto real arriba, «de tu lista: …» debajo en
   terciario, y «No lo encontré» como pastilla de borde con ícono: tiene que
   verse que se puede tocar (corrección de Gonzalo del 2026-09-15). */
.carrito__item { align-items: flex-start; }
.carrito__item > .ficha__foto, .carrito__item > .miniatura { align-self: flex-start; }
.carrito__item-etiquetas { margin-top: var(--nm-esp-1); }
.carrito__item-reemplazo { margin-top: 2px; color: var(--nm-texto-3); font-size: var(--nm-b3); font-style: italic; }
/* La tienda no tiene el tamaño de la fila: no es una explicación al margen
   sino lo que recibes, así que en tinta secundaria y sin cursiva. */
.carrito__item-reemplazo.carrito__item-tamano { color: var(--nm-texto-2); font-style: normal; }
.carrito__item-tarjeta.sello-beneficio { display: flex; margin: 2px 0 0; }
/* «No lo encontré» vive en la columna de la derecha, bajo el precio y el
   precio por unidad, y no a todo el ancho debajo del cuerpo: así la fila
   entra en los 60–104 px de PANTALLAS §1 en vez de los 290 px que medía el
   QA (G16). Sigue siendo la pastilla de borde con ícono de §P4. */
.carrito__item > .nm-fila__derecha {
  display: flex; flex-direction: column; align-items: flex-end; gap: 0;
}
.carrito__item .carrito__no-esta { margin-top: 2px; }
/* El nombre del producto se lee en DOS renglones y no con puntos
   suspensivos, igual que en Ofertas: la columna de la derecha se lleva el
   precio, el precio por unidad y la pastilla, y a 390 px «Manzana royal gala
   importada» quedaba en «Manzana royal g…», que no identifica nada. */
.carrito__item .carrito__item-nombre {
  white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* El primario de la barra del push no se parte en dos renglones: lo que cede
   ancho es la meta de la izquierda, que sí puede envolver (QA G17). */
.push__barra .barra-compromiso__resumen { min-width: 0; }
.push__barra .carrito__enlace { flex: 0 0 auto; white-space: nowrap; }
.carrito__no-esta, .carrito__deshacer { flex: 0 0 auto; }
/* `.nm-boton` fija `display: inline-flex` como regla de autor, así que el
   `[hidden]` nativo —misma especificidad, pero de la hoja del navegador—
   no le gana: sin esto, «No lo encontré» se veía igual antes y después de
   tocar el enlace de la tienda (corrección de Gonzalo, 2026-09-22). */
.carrito__no-esta[hidden] { display: none; }
.carrito__deshacer, .carrito__cambiar-texto {
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  background: transparent; color: var(--nm-texto-2);
  padding: 2px var(--nm-esp-3); font: 600 var(--nm-b3) / 1.8 var(--nm-cuerpo);
  cursor: pointer;
}
/* «Cambiar» de una fila «No lo encontré» reubicada (`filasReportadas`): la
   misma pastilla que «Deshacer», al lado, no encima — clase propia para que
   el delegado de `listas.js` (`.carrito__no-esta, .carrito__deshacer`) no la
   confunda con un «Deshacer» y desmarque el reporte en vez de reemplazar. */
.carrito__cambiar-texto { margin-right: var(--nm-esp-2); }
.carrito__comprado--hecho { opacity: .7; }
/* La fila que la persona sacó con «No lo encontré»: se queda en su sitio,
   apagada, diciendo a dónde pasó el producto y con su «Deshacer» al lado.
   Apagada y sin precio porque ya no se paga acá; presente porque marcar algo
   tiene que dejar rastro donde se marcó. */
/* Mientras el reparto se rearma, el carrito abierto se apaga: es lo único
   que la persona ve después de tocar «No lo encontré», y quieto entero
   parecía que el botón no había hecho nada. */
.carrito-push[aria-busy="true"] .push__filas { opacity: .55; }
.carrito__item--reportado .carrito__item-nombre { color: var(--nm-texto-2); }
.carrito__item--reportado .nm-fila__meta { color: var(--nm-texto-3); }
.aviso__reportados { margin: 0 0 var(--nm-esp-2); padding-left: var(--nm-esp-4); }
.aviso__reportados li { margin-bottom: 2px; }
.aviso__reportados .carrito__deshacer { margin-left: var(--nm-esp-2); }
.carrito__llevar { margin-top: var(--nm-esp-4); }
.carrito__enlace .nm-num { white-space: nowrap; }
.carrito__enlace-nota { margin: var(--nm-esp-2) 0 0; color: var(--nm-texto-3); font-size: var(--nm-b3); }
.carrito__envio-reglas {
  display: block; margin: var(--nm-esp-3) 0 0;
  color: var(--nm-texto-3); font-size: var(--nm-b3); line-height: 1.45;
}
.carrito__envio-reglas--alerta { color: var(--nm-texto); }
.lista__ayuda-enlaces { margin: var(--nm-esp-3) 0 0; color: var(--nm-texto-3); max-width: var(--nm-medida); }

/* El chasis de dos columnas del escritorio (PANTALLAS §2.2): a partir de
   1 000 px la lista queda a la izquierda y los carritos a la derecha, y
   entonces el push deja de tener barra fija — la columna entera se desplaza
   con la página. Por debajo de eso es una pestaña a pantalla completa y la
   barra se queda pegada abajo. */
/* En forma app las dos barras son capas fijas sobre el tab bar (PANTALLAS
   §1 regla 10: abajo, el tab bar y a lo sumo la barra de compromiso), y el
   contenido les reserva sitio para que la última fila no quede debajo. Va
   escrito dos veces, como todo lo de forma: la media query para el navegador
   y `data-forma="app"` para el cascarón. La base es la forma web, donde la
   barra se queda al final de su columna y se desplaza con la página; a
   partir de 1 000 px es el chasis de dos columnas (§2.2). */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) :is(.carritos__barra, .push__barra) {
    position: fixed; left: 0; right: 0; margin: 0; border-radius: 0;
    bottom: calc(var(--nm-barra-tabs, 0px) + env(safe-area-inset-bottom));
    z-index: 39;
  }
  :root:not([data-forma="web"]) :is(.carritos__cuerpo, .carrito-push) {
    padding-bottom: calc(var(--nm-barra-tabs, 0px) + 88px);
  }
}
:root[data-forma="app"] :is(.carritos__barra, .push__barra) {
  position: fixed; left: 0; right: 0; margin: 0; border-radius: 0;
  bottom: calc(var(--nm-barra-tabs, 0px) + env(safe-area-inset-bottom));
  z-index: 39;
}
:root[data-forma="app"] :is(.carritos__cuerpo, .carrito-push) {
  padding-bottom: calc(var(--nm-barra-tabs, 0px) + 88px);
}
/* ───────────── El escritorio a dos columnas (PANTALLAS §2.2) ─────────────

   Dos umbrales y no uno (ESPEC_UI §4): la FORMA cambia en 768 px —navegación,
   hoja, barra— y el CHASIS en 1 000. Entre 768 y 999 la app es la de una
   columna con la navegación arriba; a partir de 1 000 Lista y Carritos dejan
   de turnarse y se ven a la vez, que es la razón de ser del escritorio: armar
   la lista mirando lo que cuesta.

   El ancho no alcanza para decidirlo solo, porque las dos secciones se tapan
   con `hidden` y eso lo maneja el JS: `vistas.js` escribe `data-chasis="par"`
   en `<main>` cuando las destapa a las dos, y solo entonces esto se arma. El
   resto de las pestañas —y las dos columnas cuando una capa se abre encima—
   caen en `uno`, una columna centrada.

   El cascarón queda fuera por el atributo, igual que en el CSS de forma: una
   tableta ancha con mouse sigue siendo forma app y sigue teniendo su tab bar
   abajo, su barra de compromiso y una pantalla por vez. Por eso la guarda es
   `:root:not([data-forma="app"])` y no hay gemela que escribir: esto es
   exactamente lo que la forma app NO hace. */
@media (min-width: 1000px) {
  /* El push del carrito de una tienda abre como capa centrada y no como
     tercera columna: trae sus propias filas, su barra y su «‹ volver», y a
     420 px no cabe ninguna de las tres. */
  .carrito-push { max-width: 760px; }

  :root:not([data-forma="app"]) .contenido { max-width: var(--nm-ancho-app); }

  /* La izquierda toma lo que sobra y la derecha mide 420 px fijos: el panel
     lleva el trofeo, las tarjetas por tienda y el quipu-chart, que están
     dibujados para un ancho de teléfono y no ganan nada al estirarse.
     `align-items: start` para que el sticky de la derecha tenga contra qué
     pegarse: con el `stretch` de por defecto la columna mide todo el alto de
     la grilla y nunca se despega. */
  :root:not([data-forma="app"]) .contenido[data-chasis="par"] {
    display: grid; max-width: var(--nm-ancho);
    grid-template-columns: minmax(0, 1fr) 420px;
    gap: var(--nm-esp-6); align-items: start;
  }
  :root:not([data-forma="app"]) [data-chasis="par"] > .lista { min-width: 0; }
  /* Sticky, y con su propio desplazamiento SOLO cuando no cabe.
     El panel es un ítem de grilla con `align-items: start`, así que su bloque
     contenedor mide lo que mide él: se pega apenas la página arranca y no se
     despega nunca. Con el trofeo, las tarjetas por tienda, la escalera y el
     quipu, eso dejaba el final del panel fuera de la ventana para siempre —
     bajar la página movía la lista de la izquierda y el panel se quedaba
     quieto, cortado (QA del rediseño 2026, R3).
     `max-height` de ventana menos cromo + `overflow-y: auto` es lo que lo
     arregla sin reintroducir el defecto que cuida `test_reparto_movil`: el
     contenedor **solo** desplaza cuando su contenido no entra —si entra no hay
     barra y la rueda sigue siendo del documento— y `overscroll-behavior` queda
     en `auto`, que es la cadena al documento al llegar al borde. Lo que está
     prohibido es `contain`, que sí se tragaría la rueda. */
  :root:not([data-forma="app"]) [data-chasis="par"] > .carritos {
    position: sticky; top: calc(var(--nm-cabecera) + var(--nm-esp-5));
    min-width: 0; padding-top: 0;
    max-height: calc(100dvh - var(--nm-cabecera) - var(--nm-esp-5) * 2);
    overflow-y: auto; overscroll-behavior-y: auto;
    /* el sitio de la barra se reserva siempre: sin esto el panel cambia de
       ancho cuando el reparto pasa de caber a no caber */
    scrollbar-gutter: stable;
  }
  /* En la columna derecha la cabecera de la pestaña entera sobra. El título
     lo dice el trofeo mejor que dos palabras, y el «···» que quedaba solo
     contra el borde caía a la misma altura que el «···» de la lista: dos
     glifos idénticos en la misma línea, sin nada que dijera cuál abre qué
     (reportado el 2026-09-16). En par queda UNO, el de la lista, que es el
     que lleva su título al lado. Los ajustes del reparto siguen a un clic
     desde Ajustes → «Tus tiendas», que es el camino que PANTALLAS §3 P4 ya
     le reserva al escritorio; el botón sigue en el DOM, así que
     `perfil-ir-carritos` lo puede seguir tocando. */
  :root:not([data-forma="app"]) [data-chasis="par"] > .carritos .carritos__cabecera {
    display: none;
  }

  /* El total y el primario suben a la vista. En forma app la barra de la
     pestaña es una capa fija abajo y por eso siempre se ve; acá es el final de
     su columna, y con el trofeo, tres tarjetas, la escalera y el quipu por
     delante, «Empezar por Plaza Vea» caía tres pantallas abajo — o sea, la
     pantalla no tenía primario a la vista.

     La columna se vuelve una grilla y el cuerpo se borra como caja
     (`display: contents`) para que sus hijos sean hijos de ella: recién
     entonces `order` puede meter la barra entre el trofeo y las tarjetas, que
     es donde la pone el artboard de escritorio (PANTALLAS §6). El marcado no
     se toca. */
  :root:not([data-forma="app"]) [data-chasis="par"] > .carritos {
    display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--nm-esp-4);
  }
  :root:not([data-forma="app"]) [data-chasis="par"] > .carritos > .carritos__cuerpo { display: contents; }
  :root:not([data-forma="app"]) [data-chasis="par"] .carritos__cuerpo > .reparto__resumen { order: 1; }
  :root:not([data-forma="app"]) [data-chasis="par"] > .carritos > .carritos__barra { order: 2; margin-top: 0; }
  :root:not([data-forma="app"]) [data-chasis="par"] .carritos__cuerpo > div:not(.reparto__resumen) { order: 3; }
  :root:not([data-forma="app"]) [data-chasis="par"] .carritos__cuerpo > .reparto__nota { order: 4; }

  /* La cabecera de la columna izquierda: el título de la lista y su «···»,
     que `vistas.js` muda acá desde el cromo. Arriba la cabecera es del sitio
     —logo, pestañas, buscador—, así que el nombre de la pantalla vive sobre
     su columna. */
  :root:not([data-forma="app"]) [data-chasis="par"] .lista__cabecera {
    display: flex; align-items: center; gap: var(--nm-esp-3);
    min-height: 40px; margin-bottom: var(--nm-esp-4);
  }
  :root:not([data-forma="app"]) [data-chasis="par"] .lista__cabecera .cabecera__titulo {
    display: block; margin: 0;
  }
  :root:not([data-forma="app"]) [data-chasis="par"] .lista__cabecera .cabecera__mas { margin-left: auto; }

  /* En escritorio la barra de compromiso NO existe (ESPEC_UI §4, la tabla de
     las dos caras): el resumen y el único primario viven en la columna
     derecha. Dejarla habría puesto dos botones rellenos en la misma pantalla,
     que es lo que prohíbe la regla 1 de PANTALLAS §1. Generar los carritos lo
     pide «Ver dónde pagar menos» del panel, que hace el mismo submit. */
  :root:not([data-forma="app"]) [data-chasis="par"] .barra-compromiso { display: none; }
  :root:not([data-forma="app"]) [data-chasis="par"] .lista__form { padding-bottom: 0; }

  /* La banda de trabajo deja de sangrar. `.nm-banda::before` se estira a
     `100vw` porque en una columna la banda cruza la página de lado a lado; en
     dos columnas eso pintaba el fondo de la lista por DEBAJO del panel de la
     derecha y hasta el borde izquierdo de la ventana, y las dos columnas
     dejaban de leerse como dos. Acá la banda es de su columna: se recorta a su
     caja y se redondea como una tarjeta. */
  :root:not([data-forma="app"]) [data-chasis="par"] .banda-trabajo {
    border-radius: var(--nm-radio-lg); padding-left: var(--nm-esp-4);
    padding-right: var(--nm-esp-4); padding-bottom: var(--nm-esp-4);
  }
  :root:not([data-forma="app"]) [data-chasis="par"] .banda-trabajo::before {
    left: 0; right: 0; margin-left: 0; margin-right: 0;
    border-radius: var(--nm-radio-lg); border-left: 1px solid var(--nm-banda-linea);
    border-right: 1px solid var(--nm-banda-linea);
  }
}

/* ───── Buscar en la columna derecha (feedback de Gise, 2026-09-28) ─────

   Solo en forma web, y solo en Lista/Carritos con las dos columnas
   (`enColumnaBuscar`): `vistas.js` (`actualizarBuscarColumna`) cuelga
   `#buscar` DENTRO de `#carritos` en vez de dejarlo tapar la pantalla, así
   que la lista de la izquierda sigue a la vista. La columna ya es
   `position: sticky` con su propio scroll (el bloque `[data-chasis="par"]
   > .carritos` de arriba); acá solo se le quita a `.buscar` lo que traía
   para ser una capa a pantalla completa —`position: fixed`, el relleno con
   las `safe-area-inset` de un teléfono, su propio scroll— para que fluya
   como un hijo más de esa columna. El reparto no se recalcula ni se
   repinta: `carritos--buscando` solo esconde sus nodos de siempre mientras
   `#buscar` ocupa su lugar, y vuelven a verse tal cual `reparto.js` los
   había dejado en cuanto la clase se apaga.

   El CAMPO (`.buscar__barra`, con `#q`) ya no viaja con `#buscar`: desde
   2026-09-29 vive siempre en la cabecera del sitio (`.cabecera__buscar`,
   más abajo) mientras la forma sea web, sin importar la pestaña ni el
   ancho —así en TODAS las vistas está donde ya se lo esperaba, no solo
   donde había columna de sobra—, así que acá no hace falta excluirlo de
   nada: lo único que se mueve adentro y afuera de `#carritos` es el
   resultado (`#panel-resultados`, `#bienvenida`, el encabezado de
   reemplazo), que sigue con su propio `hidden` de siempre. Nada de esto
   corre en forma app —ahí Buscar sigue siendo su propia pantalla, con su
   propia barra adentro. */
:root:not([data-forma="app"]) #carritos.carritos--buscando > :not(.buscar) {
  display: none;
}
:root:not([data-forma="app"]) #carritos > .buscar {
  position: static; inset: auto; z-index: auto; min-width: 0;
  overflow: visible; padding: 0; background: transparent; color: inherit;
}
/* «Ver lista» sobra acá —la lista YA está a la vista, en la columna de al
   lado—, pero la columna sí perdió algo: Buscar reemplaza al reparto y se
   lleva su único primario, «Ver dónde pagar menos». La barra de Buscar
   (`#barra-compromiso-buscar`, `buscador.js`) vuelve a ser ese primario, y
   se queda clavada al pie DE LA COLUMNA: `sticky` dentro de `.buscar`, que
   es hijo de `#carritos`, la columna con su propio scroll. Ni flota sobre
   la lista (no es `fixed`) ni tapa el último resultado: sticky ocupa su
   lugar en el flujo, así que al final del scroll queda DEBAJO de él.
   Sin productos en la lista no se enciende (`con-barra-buscar`). */
:root:not([data-forma="app"]) #carritos > .buscar #barra-compromiso-buscar {
  display: none;
}
html.con-barra-buscar:not([data-forma="app"]) #carritos > .buscar #barra-compromiso-buscar.barra-compromiso--en-columna {
  display: flex;
  position: sticky; left: auto; right: auto; bottom: 0; z-index: 2;
  height: auto; min-height: 64px;
  /* A sangre dentro del panel: la franja opaca del color de la columna
     (el mismo de `#carritos > .buscar`, arriba) cubre todo el ancho hasta
     el borde de abajo, así que ni debajo ni a los lados de la barra se
     asoma nada de los resultados que pasan por detrás. */
  margin: var(--nm-esp-4) calc(-1 * var(--nm-esp-4)) calc(-1 * var(--nm-esp-4));
  padding: var(--nm-esp-3) var(--nm-esp-4);
  border: 0; border-top: 1px solid var(--nm-borde);
  border-radius: 0 0 var(--nm-radio-lg) var(--nm-radio-lg);
  background: var(--nm-p-cal);
}
/* Mismo prefijo que la regla de arriba (`html.con-barra-buscar…`): con menos
   especificidad el fondo claro le ganaba al oscuro y la franja salía blanca
   sobre la columna Noche. */
:root[data-theme="oscuro"].con-barra-buscar:not([data-forma="app"]) #carritos > .buscar #barra-compromiso-buscar.barra-compromiso--en-columna {
  background: var(--nm-p-noche);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]).con-barra-buscar:not([data-forma="app"]) #carritos > .buscar #barra-compromiso-buscar.barra-compromiso--en-columna {
    background: var(--nm-p-noche);
  }
}
html.con-barra-buscar:not([data-forma="app"]) #carritos > .buscar #barra-compromiso-buscar .barra-compromiso__resumen {
  display: flex;
}
html.con-barra-buscar:not([data-forma="app"]) #carritos > .buscar #barra-compromiso-buscar .boton-primario {
  flex: none; width: auto; margin: 0 0 0 auto;
}
/* «Volver arriba» medía el scroll de `#buscar`, que acá dejó de tener el
   suyo —scrollea la columna—: se apaga también por CSS para no depender de
   esa cuenta, que se queda clavada en 0. */
:root:not([data-forma="app"]) #carritos > .buscar .buscar-volver-arriba {
  display: none;
}

/* Fondo propio de la columna (Gonzalo, 2026-09-28: «cuando se abre el
   buscador a la derecha, el fondo debería ser de otro color, para que se
   distinga»). Solo en las dos columnas de escritorio —el guard de arriba—;
   la hoja de Buscar en forma app no cambia.

   Claro: Cal (`--nm-p-cal`, lo mismo que ya usan los popovers y lo anidado)
   contra la Piedra de la lista (`--nm-fondo`); con las tarjetas
   (`--nm-superficie-1`, Caliza) ya un paso más oscuras que Cal y su borde
   de 1px de siempre, no hace falta tocarlas.

   Oscuro: Cal/Cerro no se distinguen lo bastante del Profundo de la lista,
   así que acá manda Noche (`--nm-p-noche`, la MISMA superficie que ya usan
   las tarjetas) — eso las dejaría sin dónde pararse, así que dentro de la
   columna se redefine `--nm-superficie-1` a Cerro (`--nm-superficie-2`) para
   que cada tarjeta salte un escalón sin tocarlas una por una. Se aplica a
   los HIJOS (`> *`), nunca a `.buscar` mismo, para no repintar su propio
   fondo con el valor que se les está redefiniendo a ellos. */
/* Con fondo propio, la columna se lee como un panel —el mismo borde y
   radio de tarjeta que la lista de al lado— y lleva margen interior: sin él,
   el campo y los resultados quedaban pegados al canto del color. */
:root:not([data-forma="app"]) #carritos > .buscar {
  background: var(--nm-p-cal);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-lg);
  padding: var(--nm-esp-4);
}
:root[data-theme="oscuro"]:not([data-forma="app"]) #carritos > .buscar {
  background: var(--nm-p-noche);
}
:root[data-theme="oscuro"]:not([data-forma="app"]) #carritos > .buscar > * {
  --nm-superficie-1: var(--nm-superficie-2);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]):not([data-forma="app"]) #carritos > .buscar {
    background: var(--nm-p-noche);
  }
  :root:not([data-theme="claro"]):not([data-forma="app"]) #carritos > .buscar > * {
    --nm-superficie-1: var(--nm-superficie-2);
  }
}

/* ───────────── Estados ───────────── */

.vacio {
  padding: var(--nm-esp-6) var(--nm-esp-5);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde-tarjeta);
  border-radius: var(--nm-radio-lg);
  color: var(--nm-texto-3);
}
.vacio p { margin: 0 0 var(--nm-esp-2); max-width: var(--nm-medida); }
.vacio p:last-child { margin-bottom: 0; }
.vacio strong { color: var(--nm-texto); font-weight: 600; }
/* La celebración grande (DESIGN.md «Mascota: la Bolsa», «Todo comprado»):
   la ÚNICA fiesta del producto, centrada sobre el titular. `:has()` en vez
   de una clase en `.vacio` porque este bloque es el mismo de siempre —
   «no encontramos nada», «no se pudo calcular»— y solo cambia cuando trae
   la Bolsa adentro (`reparto.js`, `pintarResultadoLista`). */
.vacio:has(.reparto__bolsa-fiesta) { text-align: center; }
.vacio:has(.reparto__bolsa-fiesta) p { margin-inline: auto; }
.reparto__bolsa-fiesta { display: block; width: 240px; height: 240px; margin: 0 auto var(--nm-esp-3); }

.cargando {
  position: fixed; inset: 0 0 auto; height: 2px; z-index: 50;
  background: transparent; overflow: hidden;
}
.cargando__barra {
  display: block; height: 100%; width: 35%;
  background: var(--nm-texto);
  animation: deslizar 1s ease-in-out infinite;
}
@keyframes deslizar {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(385%); }
}
/* La regla global de prefers-reduced-motion (arriba) solo ACORTA la
   duración a .01ms; una animación `infinite` a esa duración parpadea en vez
   de quedar fija. Acá se apaga del todo y la barra queda fija a media
   opacidad — sigue anunciando que algo carga, sin movimiento. */
@media (prefers-reduced-motion: reduce) {
  .cargando__barra { width: 100%; animation: none; opacity: .5; }
}

/* Esqueleto: la carga de una superficie que se pinta en el cliente (DESIGN.md,
   Components: «Carga con esqueletos en surface-2, no spinners a pantalla
   completa»). Renglones de 12 px en superficie-2 —cal dentro de la banda,
   cerro de noche—, sin texto que leer y `aria-hidden` en el marcado: el
   estado lo anuncia quien lo pinta. Pulsa apagado, sin desplazar nada, y con
   `prefers-reduced-motion` se queda quieto: la regla global solo acorta la
   duración, y un pulso infinito de 0.01 ms parpadea, así que acá se apaga. */
.esqueleto { display: grid; gap: var(--nm-esp-3); }
.esqueleto__linea {
  display: block; height: 12px;
  background: var(--nm-superficie-2);
  border-radius: var(--nm-radio-sm);
  animation: esqueleto-pulso 1.6s var(--nm-curva-suave) infinite;
}
.esqueleto__linea:nth-child(3n + 2) { width: 72%; }
.esqueleto__linea:nth-child(3n) { width: 48%; }
@keyframes esqueleto-pulso { 50% { opacity: .5; } }
@media (prefers-reduced-motion: reduce) {
  .esqueleto__linea { animation: none; }
}

/* ───────────── Ajustes de teléfono (auditoría a 375px, 2026-09-06) ─────────────
   Lo que sobraba o faltaba al mirar cada pantalla a 375px, junto para que
   una sola media query cuente la historia. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .contenido { padding-bottom: var(--nm-esp-6); }

  /* Historial: la tarjeta cede padding al gráfico, que escala con su
     viewBox de 640. Los rótulos internos del SVG ya no suben de tamaño acá:
     en este mismo ancho están en `display: none` (ver .gp__tick/.gp__valor
     arriba), así que el aumento nunca se vio en el ancho para el que se
     escribió. */
  :root:not([data-forma="web"]) .historial { padding: var(--nm-esp-4) var(--nm-esp-3); }

  /* Carrito desplegado: cada línea trae hasta tres notas (qué pediste, por
     qué el ajuste, plan B). Se dejan todas —el plan B es una instrucción
     deliberadamente visible— pero con menos aire entre ellas y el precio
     pegado al nombre, no a una columna aparte. */
  :root:not([data-forma="web"]) .carrito__item { flex-wrap: wrap; gap: var(--nm-esp-1) var(--nm-esp-3); }
  /* base 0: el cuerpo toma lo que dejan la foto y el precio, así el precio
     nunca se cae a una línea propia debajo de la foto */
  :root:not([data-forma="web"]) .carrito__item-cuerpo { flex: 1 1 0; }
  :root:not([data-forma="web"]) .carrito__item-reemplazo { margin-top: 4px; line-height: 1.4; }
}
:root[data-forma="app"] .contenido { padding-bottom: var(--nm-esp-6); }
:root[data-forma="app"] .historial { padding: var(--nm-esp-4) var(--nm-esp-3); }
:root[data-forma="app"] .carrito__item { flex-wrap: wrap; gap: var(--nm-esp-1) var(--nm-esp-3); }
:root[data-forma="app"] .carrito__item-cuerpo { flex: 1 1 0; }
:root[data-forma="app"] .carrito__item-reemplazo { margin-top: 4px; line-height: 1.4; }

/* ───────────── Objetivos de toque ─────────────
   En un dedo, no en un puntero: todo control accionable llega a 44px de
   alto —lo que piden Apple y Android— sin engordar la interfaz de
   escritorio, donde el mouse apunta fino y la densidad importa. Se usa
   `pointer: coarse` y no un ancho: una tablet ancha también se toca. Los
   controles diminutos (la × de un plato) crecen por área de toque con un
   pseudoelemento y conservan su tamaño visual. Medidos en la auditoría a
   375px del 2026-09-06: × de plato 18px, «+ plato» 27px, flechas de día
   30px, chips 31px, «Abrir todas» 17px, botones fantasma 30px. */
@media (pointer: coarse) {
  .boton-fantasma, .boton-secundario, .carrito__comprado,
  .ajustes__momento,
  .receta__acciones .boton-fantasma,
  .marcas__resumen, .carrito__enlace, .carrito__no-esta, .carrito__deshacer,
  .sesion__accion, .item-fila--elegida > .item-fila__quitar, .item-fila__deshacer {
    min-height: 44px;
    display: inline-flex; align-items: center;
  }
  .semana__modos button, .lista__selector, .boton-primario, .item-fila__cantidad,
  .buscador__campo, .item-fila__texto,
  .buscador__escanear, .escaner__campo, .escaner-anaquel__campo { min-height: 44px; }
  .marcas__resumen { padding: 0 var(--nm-esp-2); margin-left: calc(-1 * var(--nm-esp-2)); }
  .item-fila__deshacer { padding: 0 var(--nm-esp-2); margin-right: calc(-1 * var(--nm-esp-2)); }
  .item-fila--elegida > :is(.item-fila__quitar, .item-fila__comprado-marca) { padding: 0 var(--nm-esp-3) 0 10px; margin-right: calc(-1 * var(--nm-esp-3)); }
  .item-fila__quitar, .item-fila__comprado-marca { width: 44px; height: 44px; }
  .paso-cantidad__boton { width: 44px; min-height: 44px; font-size: var(--nm-b2); }
  .buscador__limpiar { width: 40px; height: 40px; }
  .nm-segmentado button { min-height: 44px; }
  .pie__enlace { display: inline-block; padding: 8px 0; }
  .pie .pie__red { width: 44px; height: 44px; }
  /* La ✕ y el ícono de «Cambiar» siguen chicos a la vista (28px) y grandes
     al dedo (44px). */
  .plato__quitar, .plato__cambiar { position: relative; }
  .plato__quitar::before, .plato__cambiar::before { content: ""; position: absolute; inset: -8px; }
  /* Las celdas de la grilla miden 40px a la vista: el dedo las alcanza con
     el mismo truco, sin engordar la semana entera. */
  .nm-grilla__celda { position: relative; }
  .nm-grilla__celda::before { content: ""; position: absolute; inset: -2px; }
}

/* ───────────── Pie ───────────── */

/* El pie de la APP es una línea (PANTALLAS §3 P8): «un producto de Curaca» y
   los enlaces a privacidad y preguntas. El pie largo —cuatro columnas, la
   nota de las marcas de las cadenas y la fecha de los precios— es de la
   portada, que otra fase separa en su propio documento. Sin franja índigo:
   dentro de la app no hay franjas (§8).

   El relleno de abajo no se decide acá: la última palabra la tiene la regla
   de forma app, que le descuenta el tab bar y la barra de compromiso
   (tests/test_barra_pie_ui.py). */
.pie {
  flex: none;
  width: 100%; max-width: var(--nm-ancho); margin: 0 auto;
  padding: var(--nm-esp-5) calc(var(--nm-esp-5) + env(safe-area-inset-right))
           calc(var(--nm-esp-5) + env(safe-area-inset-bottom))
           calc(var(--nm-esp-5) + env(safe-area-inset-left));
  border-top: 1px solid var(--nm-borde-sutil);
  color: var(--nm-texto-3); font-size: var(--nm-b3);
}
.pie__cierre {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--nm-esp-2) var(--nm-esp-4); margin: 0;
}
/* La conexión con la casa matriz vive acá y en ningún otro lado: dentro del
   producto no entra ni el nombre ni el símbolo de Curaca. */
.pie__origen { color: var(--nm-texto-3); }
/* El hover de los enlaces sube a tinta, no a oro: el pie es estructura y el
   oro del sistema es puntuación del dato, no señal de navegación. */
.pie__enlace { color: var(--nm-texto-3); text-decoration: underline; text-underline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .pie__enlace:hover { color: var(--nm-texto); }
  .pie__red:hover { color: var(--nm-texto); }
}
/* Las redes de la marca: dos íconos en la tinta del pie, toque de 32 px. */
.pie__redes { display: inline-flex; gap: var(--nm-esp-1); align-self: center; }
.pie__red {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--nm-radio-md);
  color: var(--nm-texto-3);
}

/* ───────────── Responsive ─────────────

   EL CENSO ESTÁ CERRADO: cuatro umbrales de ancho y cuatro números, y nada
   más. Un bloque responsive nuevo elige uno de estos o no se escribe.

   · 767  — LA FORMA (app contra web): cromo, navegación, cuánta información
            recibe un teléfono. Se escribe siempre como
            `(max-width: 767px), (pointer: coarse) and (hover: none)`,
            porque un laptop de 683 px con mouse es forma web y un teléfono
            en horizontal de 915 px es forma app (ESPEC_UI §4). Lo que se
            puede expresar como valor vive en el interruptor de forma de
            `tokens.css` (`--nm-cabecera`, `--nm-barra-tabs`, `--nm-barra-*`,
            `--nm-hoja-*`), que es lo único que el cascarón puede ganar con
            `data-forma="app"`.
   · 899  — aprieta el botón de escanear en la cabecera. Era la mitad de un
            par con el `min-width: 900px` de la grilla de cinco de «Bajaron de
            precio»; esa grilla se fue cuando las bajadas pasaron a filas
            (2026-09-15) y con ella el único `min-width` de la hoja.
   · 1000 — el chasis de dos columnas, y el número es exacto, no herencia:
            593 (min-content medido de `ul.items-lista` en forma escritorio)
            + 320 (columna del reparto) + 24 (--nm-esp-5) + 48 (padding de
            .contenido) + 15 (barra de scroll) = 1000. A 768 px la columna
            izquierda quedaría en 361 px, 232 por debajo de su min-content, y
            volver fluida la derecha no salva nada: a 768 el ancho de
            contenido total es 705 px. Bajar el chasis exige antes que la
            fila compacta baje su min-content de 593 a ~390, y la
            herramienta correcta sería `@container` y no un ancho de
            viewport.
   · 560  — los pasos asistidos, el único umbral de CONTENIDO que sobrevive.

   Los `@media` de `prefers-reduced-motion`, `prefers-color-scheme`,
   `pointer: coarse` a secas y `hover: hover` no son de ancho y no cuentan.

   Dos notas para quien toque esto:
   · CSS no acepta `var()` en una media query, así que el censo vive en esta
     prosa y lo vigila una prueba, no el compilador.
   · Los números se reescriben EN SU SITIO. Juntar bloques del mismo ancho en
     uno solo los relocaliza en la cascada y cambia qué gana sin que ninguna
     prueba avise. */

/* ───────────── Cabecera y navegación de la forma app ─────────────

   En un teléfono la cabecera es lo que dice PANTALLAS §1 regla 2 y nada más:
   el título de la pestaña a la izquierda y, a la derecha, el «···» y el
   avatar. El logo y el campo de Buscar se quedan en la forma web, donde hay
   ancho; las cinco pestañas se despegan y flotan abajo como pastilla, que es
   donde un pulgar las espera, con el botón circular de Buscar aparte.

   Dos capas fijas abajo como máximo (§1 regla 10): el tab bar y, encima, la
   barra de compromiso cuando hay un total que comprometer. El FAB se retiró.

   Cada regla de acá se escribe DOS veces, que es el par que usan la fila
   compacta y la barra de compromiso: la media query con el selector
   `:root:not([data-forma="web"])` —para que el cascarón pueda pedir la forma
   web y ganarle al ancho— y un bloque gemelo `:root[data-forma="app"]` justo
   abajo —para que el cascarón pueda pedir la forma app y ganarle al puntero—.
   La duplicación es fea y no tiene salida en CSS puro: una media query no
   acepta un atributo y un selector no acepta un ancho. La vigila una prueba. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .cabecera__interior {
    gap: var(--nm-esp-2);
    padding: 0 calc(var(--nm-esp-4) + env(safe-area-inset-right))
             0 calc(var(--nm-esp-4) + env(safe-area-inset-left));
  }
  /* El logo y el campo de arriba son de la forma web. */
  :root:not([data-forma="web"]) .marca { display: none; }
  :root:not([data-forma="web"]) .cabecera__buscar { display: none; }
  :root:not([data-forma="web"]) .cabecera__titulo { display: block; }
  /* La cuenta: la inicial es la única señal permanente de «estoy en una
     cuenta» sin tener que tocar nada, y abre Ajustes.

     «Salir» no entra en la cabecera del teléfono, y con la cuenta abierta la
     inicial siempre está: Ajustes trae su propio «Salir». Sin cuenta, «Entrar»
     con su G va de pastilla corta al lado de la silueta. */
  :root:not([data-forma="web"]) .sesion__salir { display: none; }

  /* El tab bar flotante vive DENTRO de la cabecera —es el mismo marcado en
     las dos formas—, y por eso acá la cabecera no puede llevar
     `backdrop-filter`: un `filter`, un `transform` o un `backdrop-filter` en
     un ancestro lo convierte en bloque contenedor de sus descendientes
     `position: fixed`, y la pastilla se quedaba colgada arriba, tapando el
     título, en vez de flotar contra el borde de abajo del viewport (visto a
     390×844 el 2026-09-15). En la app la cabecera va con el fondo de la
     página, opaco: sin franjas y sin velo. */
  :root:not([data-forma="web"]) .cabecera { background: var(--nm-fondo); backdrop-filter: none; -webkit-backdrop-filter: none; }
  :root:not([data-forma="web"]) .tabs {
    position: fixed; left: var(--nm-esp-4); right: var(--nm-esp-4);
    bottom: calc(18px + env(safe-area-inset-bottom)); z-index: 40;
    display: flex; align-items: center; gap: var(--nm-esp-3);
  }
  :root:not([data-forma="web"]) .tabs__pastilla {
    flex: 1; display: flex; justify-content: space-around; align-items: center;
    height: 64px; padding: 6px; gap: 0;
    border-radius: var(--nm-radio-full);
    background: var(--nm-superficie-1); border: 1px solid var(--nm-borde);
    box-shadow: var(--nm-sombra-2);
  }
  /* El suelo del tab bar. La pastilla es opaca, pero flota: por el hueco
     entre ella y el botón de Buscar, y por encima de su borde, se leían
     medias filas —«S/ 11.30 (−33 %)» y un «60» partidos por la mitad (QA
     M7)—. La franja apaga el fondo bajo las dos piezas y deja la fila que
     viene detrás desvanecida, no cortada. No recibe toques. */
  :root:not([data-forma="web"]) .tabs::before {
    content: ''; position: absolute; z-index: -1; pointer-events: none;
    left: calc(var(--nm-esp-4) * -1); right: calc(var(--nm-esp-4) * -1);
    top: -40px;
    bottom: calc((18px + env(safe-area-inset-bottom)) * -1);
    background: linear-gradient(to bottom, transparent 0, var(--nm-fondo) 40px);
  }
  :root:not([data-forma="web"]) .tab {
    flex-direction: column; gap: 2px; padding: 6px 0; min-width: 0; flex: 1;
    border-radius: var(--nm-radio-full); font-size: 10px; font-weight: 500;
    -webkit-tap-highlight-color: transparent;
  }
  :root:not([data-forma="web"]) .tab .tab__icono { width: 21px; height: 21px; }
  /* La activa, en óvalo tonal: el subrayado ocre se retiró porque el oro no
     marca navegación (PANTALLAS §8). */
  :root:not([data-forma="web"]) .tab[aria-current="page"] { background: var(--nm-superficie-2); color: var(--nm-texto); font-weight: 600; }
  :root:not([data-forma="web"]) .tab:focus-visible { outline-offset: -3px; }
  :root:not([data-forma="web"]) .buscar-abrir { display: inline-flex; }

  :root:not([data-forma="web"]) .contenido { padding: var(--nm-esp-4) var(--nm-esp-4) var(--nm-esp-6); }
  /* R28 · el pie es de la portada y de la forma web ancha (PANTALLAS §2.2):
     un navegador angosto ya tiene el mismo cromo de pestañas que el
     cascarón —es la misma forma—, así que tampoco pinta el pie de la web al
     fondo de #lista/#carritos/#compras. Ya no reserva sitio para el tab bar
     porque no hay nada que reservarle sitio: no se pinta. */
  :root:not([data-forma="web"]) .pie { display: none; }
  /* La etiqueta de aviso, en 375 px: una de tres líneas con radio de
     píldora deja dos lentes enormes a los lados. */
  :root:not([data-forma="web"]) .etiqueta--alerta {
    letter-spacing: .04em;
    border-radius: var(--nm-radio-lg);
  }
}

/* El mismo bloque otra vez, para el cascarón. `data-forma="app"` lo estampa
   `empaquetar_web.py` en la etiqueta `<html>` del paquete y lo vuelve a poner
   `forma.js`. El porqué de cada regla está escrito una sola vez, arriba; acá
   van los valores pelados. Que las dos caras digan exactamente lo mismo lo
   vigila `tests/test_forma_app.py`, no la buena memoria de quien edite. */
:root[data-forma="app"] .cabecera__interior { gap: var(--nm-esp-2); padding: 0 calc(var(--nm-esp-4) + env(safe-area-inset-right)) 0 calc(var(--nm-esp-4) + env(safe-area-inset-left)); }
:root[data-forma="app"] .marca { display: none; }
:root[data-forma="app"] .cabecera__buscar { display: none; }
:root[data-forma="app"] .cabecera__titulo { display: block; }
:root[data-forma="app"] .sesion__salir { display: none; }
:root[data-forma="app"] .cabecera { background: var(--nm-fondo); backdrop-filter: none; -webkit-backdrop-filter: none; }
:root[data-forma="app"] .tabs { position: fixed; left: var(--nm-esp-4); right: var(--nm-esp-4); bottom: calc(18px + env(safe-area-inset-bottom)); z-index: 40; display: flex; align-items: center; gap: var(--nm-esp-3); }
:root[data-forma="app"] .tabs__pastilla { flex: 1; display: flex; justify-content: space-around; align-items: center; height: 64px; padding: 6px; gap: 0; border-radius: var(--nm-radio-full); background: var(--nm-superficie-1); border: 1px solid var(--nm-borde); box-shadow: var(--nm-sombra-2); }
:root[data-forma="app"] .tabs::before { content: ''; position: absolute; z-index: -1; pointer-events: none; left: calc(var(--nm-esp-4) * -1); right: calc(var(--nm-esp-4) * -1); top: -40px; bottom: calc((18px + env(safe-area-inset-bottom)) * -1); background: linear-gradient(to bottom, transparent 0, var(--nm-fondo) 40px); }
:root[data-forma="app"] .tab { flex-direction: column; gap: 2px; padding: 6px 0; min-width: 0; flex: 1; border-radius: var(--nm-radio-full); font-size: 10px; font-weight: 500; -webkit-tap-highlight-color: transparent; }
:root[data-forma="app"] .tab .tab__icono { width: 21px; height: 21px; }
:root[data-forma="app"] .tab[aria-current="page"] { background: var(--nm-superficie-2); color: var(--nm-texto); font-weight: 600; }
:root[data-forma="app"] .tab:focus-visible { outline-offset: -3px; }
:root[data-forma="app"] .buscar-abrir { display: inline-flex; }
:root[data-forma="app"] .contenido { padding: var(--nm-esp-4) var(--nm-esp-4) var(--nm-esp-6); }
/* R28 · el pie es de la portada y de la forma web (PANTALLAS §2.2): la app
   no tiene portada, así que en ninguna pestaña se pinta el pie de la web
   al fondo de #lista/#carritos/#compras. Antes solo se escondía con la
   lista vacía, que era la mitad de la regla. */
:root[data-forma="app"] .pie { display: none; }
:root[data-forma="app"] .etiqueta--alerta { letter-spacing: .04em; border-radius: var(--nm-radio-lg); }

/* El layout de teléfono de las alternativas se mudó: desde que la fila
   estrena `.fila` y tiene par y sello propios (2026-09-12) vive junto a su
   cara de escritorio, arriba, y en las DOS caras —esta copia estaba solo
   dentro de la media query de ancho, así que en el cascarón no existía. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .lista__barra-acciones { margin-left: 0; }
  :root:not([data-forma="web"]) .lista__acciones { flex-direction: column; align-items: stretch; }
  :root:not([data-forma="web"]) .lista__resumen-total { margin-left: 0; width: 100%; }
}
:root[data-forma="app"] .lista__barra-acciones { margin-left: 0; }
:root[data-forma="app"] .lista__acciones { flex-direction: column; align-items: stretch; }
:root[data-forma="app"] .lista__resumen-total { margin-left: 0; width: 100%; }

/* ---------- tour guiado de la compra de ejemplo ----------
   Enseña el circuito completo (lista → generar → ahorro → carrito → guardar)
   haciéndolo, no describiéndolo: cada paso recorta el trozo de pantalla del
   que habla y el paso 2 se cierra apretando el botón de verdad.

   El velo y el recorte no reciben clics a propósito. Un tour modal que
   bloquea la página tendría que replicar el botón "Genera tus carritos"
   dentro del globo, y entonces lo que se aprende es a usar el tour. */
.tour { position: fixed; inset: 0; z-index: 60; pointer-events: none; }
.tour[hidden] { display: none; }
.tour [hidden] { display: none; }

/* El velo es la sombra del recorte, no una capa aparte: así el agujero
   sigue al elemento con solo mover un div, sin máscaras ni cuatro rellenos. */
/* Sin transición sobre la posición a propósito: el recorte se recalcula en
   cada scroll, y una transición la reinicia en cada frame, así que el
   agujero viaja permanentemente por detrás del elemento que persigue. */
.tour__foco {
  position: absolute; border-radius: var(--nm-radio-lg); pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(13, 17, 32, .55);
  /* En TINTA y no en oro. El tour arranca solo con `?demo=1` sobre la Lista,
     y el censo de esa pantalla es CERO oro (PANTALLAS §8): lo primero que
     veía quien entraba por la portada eran dos anillos dorados (QA G23). La
     tinta contrasta con el hueco recortado en los dos modos —el hueco
     muestra la pantalla real— y no gasta la puntuación del dato. */
  outline: 2px solid var(--nm-texto); outline-offset: -1px;
}
:root[data-theme="oscuro"] .tour__foco { box-shadow: 0 0 0 9999px rgba(0, 0, 0, .72); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]) .tour__foco { box-shadow: 0 0 0 9999px rgba(0, 0, 0, .72); }
}

.tour__globo {
  position: absolute; pointer-events: auto;
  width: min(360px, calc(100vw - 24px));
  background: var(--nm-superficie-1); color: var(--nm-texto);
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-lg);
  box-shadow: var(--nm-sombra-2);
  padding: var(--nm-esp-4) var(--nm-esp-5) var(--nm-esp-4);
}
/* El globo se enfoca por programa al abrir el paso —es el diálogo, y el
   lector de pantalla tiene que caer adentro—, pero NO se le pinta el anillo
   de foco: es un contenedor, nadie lo alcanzó con el teclado, y el anillo
   global es de oro, así que arrancar el tour sobre la Lista encendía un
   marco dorado en una pantalla de censo cero oro (QA G23). Los controles de
   adentro conservan el suyo. */
.tour__globo:focus, .tour__globo:focus-visible { outline: none; }
.tour__conteo { margin: 0 0 var(--nm-esp-2); color: var(--nm-texto-3); }

/* El llamado al clic: el paso "genera tus carritos" le pone al botón un
   anillo que late —una onda que nace en el borde y se disuelve— para que no
   quede duda de que hay que apretarlo. En TINTA, como el recorte: el llamado
   cae sobre la Lista, y el censo de la Lista es cero oro (PANTALLAS §8, QA
   G23). El anillo va con `outline-offset` positivo, o sea sobre el fondo de
   la página y no sobre el añil del botón, que es donde se lee. Con
   reduced-motion la animación se apaga sola (regla global) y queda fijo. */
.tour-llamado {
  outline: 2px solid var(--nm-texto);
  outline-offset: 3px;
  animation: tour-latido 1.6s var(--nm-curva-suave) infinite;
}
@keyframes tour-latido {
  0%   { box-shadow: 0 0 0 0 var(--nm-texto); }
  70%  { box-shadow: 0 0 0 14px transparent; }
  100% { box-shadow: 0 0 0 14px transparent; }
}
/* La regla global solo acorta la duración a .01ms; un `infinite` a esa
   duración parpadea en vez de quedar fijo, al revés de lo que dice el
   comentario de arriba. El outline —que es el llamado real— no depende de
   la animación y se queda solo. */
@media (prefers-reduced-motion: reduce) {
  .tour-llamado { animation: none; }
}
.tour__titulo { margin: 0 0 var(--nm-esp-2); }
.tour__texto { margin: 0; color: var(--nm-texto-2); }
.tour__acciones {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--nm-esp-3); margin-top: var(--nm-esp-4);
}
/* .boton-primario nace a ancho completo porque su sitio natural es el panel
   de reparto; acá comparte fila, y sin esto se comía el ancho entero y
   partía "Saltar el tour" en tres líneas. */
.tour__acciones .boton-primario { width: auto; margin-top: 0; }
.tour__acciones button { flex: 0 0 auto; white-space: nowrap; }

/* ---------------------------------------------------------------------------
   Miniatura de producto

   Chip de 44px (dentro del rango 40–48 del brief) con la foto CONTENIDA, no
   recortada: a este tamaño cortarle los bordes a un envase alto —una botella
   de aceite, una caja de leche— le quita justo lo que lo hace reconocible.

   El maíz no entra acá: la miniatura no es un dato que celebre, y el oro está
   reservado al logo, el ahorro, el nudo y monto ganador, la pestaña activa y
   el foco.

   ── El fondo: placa cal en oscuro, NINGUNO en claro (ESPEC_UI §7) ──────────

   Hasta el 2026-09-15 este comentario defendía una placa cal con borde «en
   los DOS modos», con `--nm-p-cal` crudo, sobre tres premisas: (a) las fotos
   vienen horneadas sobre blanco; (b) un chip oscuro les dejaría un cuadrado
   claro flotando encima; (c) filtrarlas —invertirlas o atenuarlas— vuelve la
   foto un dato falso. Se refuta así, premisa por premisa:

   (a) y (b) son ciertas, pero solo prueban el modo OSCURO: sobre profundo,
   noche o cerro, el blanco de la foto es un cuadrado claro sí o sí, y la
   placa cal lo convierte en un chip deliberado. Ahí la placa se queda, y
   `multiply` no sirve: multiplicar contra un fondo oscuro apaga la foto
   entera, no solo su blanco.

   En CLARO la conclusión no se sigue de las premisas. La placa cal con borde
   ES el cuadrado que el comentario quería evitar: un recuadro dibujado sobre
   caliza y sobre piedra, repetido en cada fila. `mix-blend-mode: multiply`
   contra la superficie de abajo lo borra sin tocar el producto, porque el
   blanco multiplicado toma exactamente el tono de esa superficie.

   (c) `multiply` no es un filtro de color. Multiplica cada pixel por el
   fondo, y los fondos donde vive el chip son neutros casi acromáticos: cal
   (×1.00, no oscurece nada) o caliza (#F4F5F8, ×0.96: a lo sumo un 4 % de
   luminancia y ningún cambio de tono). Invertir o atenuar la foto sí la
   falsearía; esto no.

   ── Contra qué se multiplica ───────────────────────────────────────────────

   Contra la superficie donde está el chip, que siempre es un token opaco. El
   mismo producto no puede salir en tres tonos según la franja, así que se
   decide cuál toca a cada una:

   · cal (0 %): la lista y los carritos —la banda remapea superficie-1 a cal,
     tokens.css `.nm-banda`—, las bajadas, la hoja (`.hoja__panel` va en
     superficie-3), el desplegable y las compras.
   · caliza (~4 %): `.comparacion` y `.alternativas`, tarjetas sobre la base.
   · piedra (~9 %): SOLO el encabezado de la ficha y el hallazgo del escáner
     cuando se pinta en la página. Ahí no se multiplica contra la piedra: se
     le pone una placa de un nivel (superficie-1, la tarjeta sobre piedra que
     el sistema ya permite) y la foto queda en el 4 % de caliza.

   Resultado: 0 % o 4 %, nunca 9 %, y nunca los dos en la misma pantalla —el
   buscador vacío pinta bajadas en banda y no pinta resultados—.

   Dos trampas que decidieron cómo se escribe:
   · `isolation: isolate` va SOLO donde hay placa opaca. Sobre un chip
     transparente mata la mezcla: el grupo aislado se mezcla contra
     transparente y vuelve el cuadrado blanco.
   · Los valores no se guardan en custom properties de `:root`: un
     `var(--nm-superficie-1)` resuelto en `:root` no ve el remapeo de
     `.nm-banda`. Van como declaraciones con selectores de tema, igual que
     `.tour__foco`.

   Lo que no arregla: ~20 % de las fotos no traen fondo blanco (13 % casi
   blanco, 7 % de ambiente, ~12,000 imágenes). Siguen mostrando su propio
   fondo, igual que dentro del chip cal de antes; ninguna empeora.

   El `border-width` no cambia en claro —el borde queda transparente—, así
   que la caja sigue midiendo 44 px y `medir_ui.py` no se mueve. Lo vigila
   `tests/test_miniaturas_ui.py`, midiendo pixeles en Chromium.
   --------------------------------------------------------------------------- */
.miniatura {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  overflow: hidden;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--nm-radio-md);
}

/* La foto llena el chip por posicionamiento absoluto, no con height:100%:
   como ítem de grid ese porcentaje no resolvía y el navegador caía al
   aspect-ratio 1:1 de los atributos del <img> — una caja cuadrada dentro
   del chip apaisado del catálogo, con overflow:hidden, era la foto cortada
   exactamente a la mitad (feedback del 2026-08-26). En absoluto, el 100%
   es contra la caja del chip y `contain` centra sin recortar jamás. */
.miniatura { position: relative; }
.miniatura:not(.miniatura--vacia) img {
  position: absolute; inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Aire mínimo para que el radio del chip nunca afeite las esquinas de
     una foto que llega al borde: eso también se leía como «cortada». */
  padding: 2px;
  /* Claro: el blanco de la foto toma el tono de la superficie (ver arriba). */
  mix-blend-mode: multiply;
}

/* Piedra: la única superficie donde multiplicar oscurecería un 9 %. Placa de
   un nivel y grupo aislado, para que la foto se mezcle contra la placa y no
   contra la página. */
.ficha__encabezado .miniatura {
  background: var(--nm-superficie-1);
  isolation: isolate;
}

/* Oscuro, la franja índigo y el escáner —oscuros también en claro—: la placa
   cal se queda y la foto no se mezcla, porque `multiply` sobre índigo
   apagaría el producto. */
:root[data-theme="oscuro"] .miniatura,
.nm-franja-indigo .miniatura,
.escaner .miniatura,
.escaner-hoja .miniatura {
  background: var(--nm-p-cal);
  border-color: var(--nm-borde-sutil);
}
:root[data-theme="oscuro"] .miniatura img,
.nm-franja-indigo .miniatura img,
.escaner .miniatura img,
.escaner-hoja .miniatura img { mix-blend-mode: normal; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]) .miniatura {
    background: var(--nm-p-cal);
    border-color: var(--nm-borde-sutil);
  }
  :root:not([data-theme="claro"]) .miniatura img { mix-blend-mode: normal; }
}

/* Placeholder: el isotipo de la marca, a tamaño de firma y no de foto — es
   la marca diciendo "acá falta el dato", no una ilustración del producto.
   La versión clara funciona en los dos modos: en claro va sobre la superficie
   clara donde esté el chip, y en oscuro sobre la placa cal.
   Queda centrado por el grid del chip (excluido del llenado absoluto). */
.miniatura--vacia img { width: 24px; height: 24px; padding: 0; }
/* El hueco: el sitio de la foto, reservado y vacío, para que la columna de
   nombres no quede dentada cuando un producto no tiene foto o su foto no
   dice nada (la balanza del granel). Ver `HUECO_FOTO` en `lista.js`. */
/* El hueco no se ve NUNCA: gana con su propia especificidad porque en oscuro
   la regla de `.miniatura` le pone el fondo de cal —pensado para el chip con
   foto— y el sitio reservado se leía como un cuadrado BLANCO al lado de los
   productos a granel. */
.miniatura.miniatura--hueco,
:root[data-theme="oscuro"] .miniatura.miniatura--hueco {
  background: transparent; border-color: transparent;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]) .miniatura.miniatura--hueco {
    background: transparent; border-color: transparent;
  }
}

/* Buscador: la foto va en la cabecera del grupo, o sea una por producto
   físico. Una por línea de tienda repetiría la misma imagen seis veces en el
   mismo bloque y la pantalla dejaría de leerse acromática. */
.comparacion__cabecera {
  display: flex;
  /* Centrada y no `flex-start`: con el nombre topado en dos renglones el
     bloque de texto mide entre uno y dos, y con la foto clavada arriba la
     cabecera se leía descolgada cuando el nombre entraba en un renglón. */
  align-items: center;
  gap: var(--nm-esp-3);
}
.comparacion__titulo { min-width: 0; flex: 1; }

/* Al pasar el mouse la foto se agranda en el sitio, como popover anclado al
   chip: 44px alcanzan para reconocer la marca, no para leer la etiqueta.
   Solo con puntero de verdad (hover: hover) — en táctil un zoom que salta
   al rozar estorba — y solo cuando hay foto: agrandar el isotipo del
   placeholder no enseña nada. */
/* La guarda entera —`(hover: hover) and (pointer: fine)`— y no solo la
   mitad: un teléfono con un lápiz reporta `hover: hover` con puntero
   grueso, y ahí el popover de foto se queda pegado después del toque
   (PANTALLAS §9.1 regla 5). */
@media (hover: hover) and (pointer: fine) {
  /* La foto por ENCIMA del área pinchable del nombre (reparado el
     2026-09-15). `.comparacion__abrir::after` es `position: absolute;
     inset: 0` contra la cabecera y viene después en el documento, así que
     tapaba la miniatura: el `:hover` le llegaba al botón y nunca al chip, y
     el popover no se abría con el mouse.

     Se resuelve corriendo el borde izquierdo del pseudo hasta pasada la foto
     (relleno de la cabecera + los 44 px del chip + el hueco), y NO subiendo
     la miniatura con `z-index`: eso la vuelve un contexto de apilamiento,
     aísla su `multiply` contra transparente y la foto vuelve a pintar su
     cuadrado blanco sobre la caliza (§7; lo acusa
     `tests/test_miniaturas_ui.py`). Con el foco de teclado el pseudo vuelve a
     cubrir la cabecera entera, para que el anillo la rodee como antes.

     Con mouse, entonces, la foto se agranda al pasar y no abre la ficha: la
     puerta a la ficha es el nombre, y la foto es para verla en grande. En
     táctil —sin este bloque— el pseudo la sigue cubriendo y el toque abre el
     producto. Lo mide `tests/test_resultados_foto_ui.py`. */
  .comparacion__cabecera:has(> .miniatura:not(.miniatura--vacia)) .comparacion__abrir::after {
    left: calc(var(--nm-esp-4) + 44px + var(--nm-esp-3));
  }
  .comparacion__cabecera:has(> .miniatura:not(.miniatura--vacia)) .comparacion__abrir:focus-visible::after { left: 0; }
  .comparacion .miniatura:not(.miniatura--vacia) { cursor: zoom-in; }
  /* la tarjeta recorta a sus hijos (overflow: hidden por el radio): se
     suelta solo mientras el zoom está abierto, y sube sobre las tarjetas
     vecinas para que la foto grande no quede debajo de la siguiente */
  .comparacion:has(.miniatura:not(.miniatura--vacia):hover) {
    overflow: visible;
    position: relative;
    z-index: 42;
  }
  .comparacion .miniatura:not(.miniatura--vacia):hover {
    overflow: visible;
    z-index: 42;
  }
  .comparacion .miniatura:not(.miniatura--vacia):hover img {
    inset: auto;
    top: -8px;
    left: -8px;
    width: 200px;
    height: 200px;
    padding: 10px;
    background: var(--nm-p-cal);
    border: 1px solid var(--nm-borde);
    border-radius: var(--nm-radio-md);
    box-shadow: var(--nm-sombra-2);
    /* El popover pinta su propio fondo cal y flota sobre las tarjetas
       vecinas: con `multiply` a 200 px se vería el texto de abajo a través
       de la foto. */
    mix-blend-mode: normal;
  }
}

/* Fila de la lista: la miniatura entra DENTRO de la primera pista, no como
   columna nueva. La lista usa subgrid con seis pistas compartidas por todas
   las filas; agregar una séptima obligaría a tocar el track list del padre y
   a realinear los controles de cada fila. */
.item-fila__producto {
  display: flex;
  align-items: flex-start;
  gap: var(--nm-esp-3);
}
.item-fila__datos { min-width: 0; flex: 1; }

/* Ficha de producto: la miniatura acompaña al título, no lo reemplaza. Es la
   página DE ese producto, así que la foto tiene más presencia que en una
   lista, pero sigue siendo dato y no portada: el nombre manda.

   64px y no más: acá la foto sigue siendo dato de reconocimiento, no portada,
   y el nombre manda. Que el archivo ahora llegue a 600px no es motivo para
   agrandar el chip — es para que el visor tenga qué mostrar. Para verla
   grande se pincha.

   En la ficha del rediseño 2026 la foto sí es grande y va al centro
   (PANTALLAS §3 P3): es la página DE ese producto, y el reconocimiento es
   la mitad de la decisión. El chip de 64 px se quedó donde la foto
   acompaña —el escáner, el carrito—, no acá. */
.miniatura--ficha { width: min(200px, 56vw); height: min(200px, 56vw); }
.miniatura--ficha.miniatura--vacia img { width: 72px; height: 72px; }

/* En la ficha la foto se puede pinchar para verla en grande. El botón es
   transparente y del tamaño exacto del chip: lo que se ve sigue siendo la
   miniatura, con la lupa del cursor y el foco de siempre como única señal.
   Un borde o un fondo propios agregarían una caja dentro de otra. */
.ficha__foto {
  padding: 0;
  border: 0;
  background: none;
  border-radius: var(--nm-radio-md);
  cursor: zoom-in;
  line-height: 0;
}
.ficha__foto:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 3px; }

/* Visor de la foto -----------------------------------------------------------
   Velo sobre toda la ventana con la foto al doble de sus píxeles reales como
   techo (`--visor-ancho`, que pone app.js al cargar el archivo). El velo es
   profundo en los dos modos: se está mirando una foto horneada sobre blanco y
   un velo claro la dejaría sin contorno. Ahora sale de `--nm-velo-foto`, o
   sea de tokens.css, como cualquier otra superficie.

   El visor es la capa completa y el velo lo pinta él: eso es lo que le
   permite ser un `<dialog>` sin depender de que `::backdrop` vea los tokens
   (ver el bloque del velo, arriba). Las cuatro declaraciones de reset
   —`max-width`, `max-height`, `margin`, `border`— son las que hacen falta
   para que un `<dialog>` sea la capa y no una cajita centrada por el UA; con
   un `div` no estorban.

   Y como su contenido vive sobre un velo casi negro en los DOS modos, es una
   franja índigo: adentro el oro es maíz y el texto es hueso (DESIGN.md). El
   remapeo es lo que le quita a `.visor__cerrar` sus tres literales y lo que
   arregla un defecto real de contraste — su anillo de foco era ocre sobre
   casi negro en modo claro. */
.visor {
  --nm-texto:        var(--nm-franja-texto);
  --nm-texto-2:      var(--nm-franja-texto-2);
  --nm-texto-3:      var(--nm-franja-texto-3);
  --nm-superficie-1: var(--nm-franja-superficie);
  --nm-superficie-2: var(--nm-franja-superficie);
  --nm-borde:        var(--nm-franja-linea-fuerte);
  --nm-borde-sutil:  var(--nm-franja-linea);
  --nm-acento:       var(--nm-franja-acento);
  --nm-foco:         var(--nm-franja-nudo);
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: var(--nm-esp-6);
  border: 0;
  background: var(--nm-velo-foto);
  color: var(--nm-franja-texto);
}
.visor[hidden] { display: none; }
.visor::backdrop { background: transparent; }

.visor__marco {
  margin: 0;
  display: grid;
  justify-items: center;
  gap: var(--nm-esp-3);
  max-width: 100%;
}
.visor__foto {
  display: block;
  width: min(
    var(--visor-ancho, 320px),
    90vw,
    calc(80vh * var(--visor-aspecto, 1))
  );
  max-width: 100%;
  height: auto;
  background: var(--nm-p-cal);
  border-radius: var(--nm-radio-xl);
  box-shadow: var(--nm-sombra-2);
}
.visor__pie {
  margin: 0;
  max-width: 42ch;
  text-align: center;
  color: var(--nm-franja-texto);
}

.visor__cerrar {
  position: absolute;
  top: var(--nm-esp-4);
  right: var(--nm-esp-4);
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  /* Dentro de la franja los tokens ya están remapeados en `.visor`: el
     cerrar es una superficie de franja con texto hueso, y su anillo de foco
     es maíz. Antes eran tres literales de blanco translúcido y un foco ocre
     sobre casi negro. */
  color: var(--nm-franja-texto);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full);
  cursor: pointer;
}
.visor__cerrar svg { width: 18px; height: 18px; }
@media (hover: hover) and (pointer: fine) {
  .visor__cerrar:hover {   background: var(--nm-franja-linea); }
}
.visor__cerrar:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }

/* Mientras el visor está abierto la página de atrás no se desplaza: el gesto
   de rueda o de dedo sobre el velo movería el contenido oculto y al cerrar
   uno aparece en otro lado. */
/* Dos clases y no una: `con-visor` es la del visor de foto y `con-capa` la
   que va a poner el contador de capas de `hoja`. `showModal()` no garantiza
   el bloqueo del scroll en todos los motores, así que se sigue declarando. */
:root.con-visor body, :root.con-capa body { overflow: hidden; }


/* La lista de alternativas SÍ son productos distintos entre sí, así que acá
   la miniatura hace el trabajo que le toca: distinguirlos de un vistazo. */
.alternativa .miniatura { width: 44px; height: 44px; }

/* ---------------------------------------------------------------------------
   Qué se puede pinchar en los resultados del buscador

   Había dos problemas: al producto solo se llegaba pinchando una de sus
   tiendas —cuando lo primero que uno pincha es el producto—, y las filas de
   tienda no decían de ninguna manera que fueran pinchables hasta que el
   puntero pasaba por encima. Con teclado o en pantalla táctil, donde no hay
   hover, eso significaba que no había ninguna señal.

   Se resuelve con subrayado, no con color: el maíz está reservado al logo,
   el ahorro, el nudo y monto ganador, la pestaña activa y el foco, así que
   pintar los enlaces de oro gastaría el recurso escaso en algo que no es un
   dato que celebre. El subrayado en reposo va en `borde-fuerte` —presente
   pero callado— y sube a la tinta del texto al hover.
   --------------------------------------------------------------------------- */
.comparacion__cabecera { position: relative; }

/* Sin `transition` sobre `text-decoration-color`, a propósito: al cambiar de
   tema el color de la variable cambia pero la transición se queda pegada al
   valor viejo —medido: un elemento recién creado tomaba #423D38 y el mismo
   elemento con transición seguía en #C8BFAF— así que el subrayado conservaba
   el color del modo anterior hasta que la lista se repintaba. El salto de
   color al hover es instantáneo y a 1px no se nota; un subrayado con el color
   del tema equivocado sí. */
.comparacion__abrir {
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-align: left;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--nm-borde-fuerte);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.comparacion__abrir[aria-selected="true"] { text-decoration-color: currentColor; }
@media (hover: hover) and (pointer: fine) {
  .comparacion__abrir:hover {   text-decoration-color: currentColor; }
}

/* Recorriendo con las flechas, la opción activa se marca igual que una fila
   de tienda: el mismo escalón tonal, para que el teclado y el ratón cuenten
   la misma historia. Va en la cabecera y no en el botón, que solo ocupa el
   ancho del texto. */
.comparacion__cabecera:has(.comparacion__abrir[aria-selected="true"]) {
  background: var(--nm-superficie-2);
}

/* Área pinchable extendida a toda la cabecera, miniatura incluida: el botón
   sigue siendo UN solo elemento accesible —nadie tiene que tabular dos veces
   por lo mismo— pero el blanco de alrededor y la foto también responden, que
   es donde la gente pincha. */
.comparacion__abrir::after {
  content: '';
  position: absolute;
  inset: 0;
}
.comparacion__abrir:focus-visible { outline: none; }
.comparacion__abrir:focus-visible::after {
  outline: 2px solid var(--nm-acento);
  outline-offset: -2px;
  border-radius: var(--nm-radio-md);
}

/* La fila de tienda lleva a la ficha de ESA tienda. El subrayado va bajo el
   nombre de la tienda, que es lo que nombra el destino, y no bajo la fila
   entera. */
.tienda__nombre {
  text-decoration: underline;
  text-decoration-color: var(--nm-borde-fuerte);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.tienda[aria-selected="true"] .tienda__nombre { text-decoration-color: currentColor; }
@media (hover: hover) and (pointer: fine) {
  .tienda:hover .tienda__nombre {   text-decoration-color: currentColor; }
}

/* Misma señal en la lista de alternativas de la ficha: son filas pinchables
   con la misma forma que las del buscador, así que si allá el subrayado
   significa "esto lleva a algún lado", acá tiene que significar lo mismo.
   Media señal repartida sería peor que ninguna. */
.alternativa__nombre {
  text-decoration: underline;
  text-decoration-color: var(--nm-borde-fuerte);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
@media (hover: hover) and (pointer: fine) {
  .alternativa:hover .alternativa__nombre {   text-decoration-color: currentColor; }
}

/* En oscuro la tarjeta de resultado pierde la sombra: el DESIGN es explícito
   en que ahí la profundidad la dan las capas tonales de carbón más el borde
   de 1px, y las sombras quedan solo para lo que flota (menús, popovers). Una
   tarjeta en una lista no flota. La separación de 24px y el borde hacen todo
   el trabajo, que en carbón se ve incluso mejor que en papel. */
:root[data-theme="oscuro"] .comparacion { box-shadow: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]) .comparacion { box-shadow: none; }
}

/* Ver `pintarTema()` en app.js: se pone durante el cambio de tema para que
   ninguna transición se quede con el color del tema anterior. */
.sin-transiciones,
.sin-transiciones *,
.sin-transiciones *::before,
.sin-transiciones *::after {
  transition: none !important;
}

/* ---------------------------------------------------------------------------
   Barra de orden de los resultados

   Vive en la misma línea que el contador: es la fila que dice qué estás
   viendo, así que el control que lo cambia va ahí y no flotando aparte. En
   pantallas angostas el select pasa debajo, alineado a la izquierda con el
   contador, en vez de encogerse hasta no poder leer sus opciones.

   El control es secundario por definición —el protagonista son los precios—
   así que va en texto terciario y con el borde apagado hasta que lo tocas,
   igual que el selector de presentación de la lista. Sin maíz: el oro está
   reservado al dato que celebra, y un desplegable no lo es. El anillo de
   foco sí lleva acento, que es de los pocos usos permitidos.
   --------------------------------------------------------------------------- */
.panel-resultados__barra {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--nm-esp-2) var(--nm-esp-4);
  margin: var(--nm-esp-4) 0 var(--nm-esp-3);
}
.panel-resultados__barra .panel-resultados__contador { margin: 0; }

/* ───────────── La barra de resultados (§6 #9) ─────────────

   El contador y UN control, «Filtrar», que dice lo que está puesto.
   Hasta el 2026-09-15 esta barra también dibujaba tres pastillas —dos
   `<select>` y un `<details>` de tiendas— y la tira `.filtros` que iba a
   reemplazarlas; los controles se mudaron a la hoja (`.busqueda-hoja`, al
   final del archivo) y esas reglas, que ya no tocaban ningún nodo, se
   retiraron. */
.panel-resultados__barra { align-items: center; gap: var(--nm-esp-2); }

.orden {
  display: flex;
  align-items: baseline;
  gap: var(--nm-esp-2);
  min-width: 0;
}
.orden__etiqueta { color: var(--nm-texto-3); white-space: nowrap; }
.orden__select {
  max-width: 100%;
  padding: 4px 6px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--nm-radio-md);
  color: var(--nm-texto-2);
  font-family: inherit;
  font-size: var(--nm-b3);
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .orden__select:hover {   border-color: var(--nm-borde); color: var(--nm-texto); }
}
.orden__select:focus-visible {
  border-color: var(--nm-borde);
  outline: 2px solid var(--nm-acento);
  outline-offset: 2px;
}
/* En oscuro el desplegable nativo hereda el fondo de la página si no se le
   dice otra cosa, y las opciones quedan ilegibles. */
.orden__select option {
  background: var(--nm-campo);
  color: var(--nm-texto);
}

/* En forma app la barra no reparte contador y control a los extremos: se
   leen de corrido desde la izquierda. (El `.orden { flex-basis: 100% }` que
   vivía acá era de las pastillas; en la hoja `.orden` es hijo de una grilla.) */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .panel-resultados__barra { justify-content: flex-start; }
}
:root[data-forma="app"] .panel-resultados__barra { justify-content: flex-start; }

/* "Ver más" cierra la lista de resultados. Centrado y en botón fantasma: es
   una continuación, no una acción principal — el botón primario de la
   pantalla sigue siendo uno solo y está en la lista de compras. */
.ver-mas {
  display: flex;
  justify-content: center;
  margin-top: var(--nm-esp-5);
}
.ver-mas .boton-fantasma { min-width: 14rem; }

/* El filtro de categoría comparte la forma de los selects de la hoja. Se oculta cuando la
   búsqueda cae toda en una sola categoría: un desplegable de una opción es
   ruido que hay que leer para descubrir que no hace nada. */

/* Filtro de tiendas del buscador: chips conmutables (auditoría de
   accesibilidad 2026-09-24 — antes ocho casillas nativas, a diferencia de
   toda otra fila tocable de la app). El `.nm-chip` es el mismo que ya existe
   en `componentes.css`; la semántica de casilla se conserva con un
   `<input type="checkbox">` real dentro del `<label>`, oculto con
   `.sr-solo` (visible al lector de pantalla y al foco, no al ojo) y no con
   `display:none` — eso lo sacaría del tabulado. Como `<fieldset>` porque son
   varias casillas y un `<select>` no las puede mostrar todas marcadas a la
   vez. Se oculta con una sola tienda —no hay nada que filtrar— igual que la
   categoría. Vive en la hoja de filtros (`.busqueda-hoja`, que lo
   acomoda al final del archivo); lo que quedaba del `<details>` de antes
   —el marcador, el hover y el `[open]` del resumen— se retiró con él. */
.filtro-tiendas__lista {
  display: flex; flex-wrap: wrap;
  gap: var(--nm-esp-1) var(--nm-esp-2);
  position: absolute; z-index: 5; top: calc(100% + 4px); left: 0;
  width: max(20rem, 100%); max-width: 24rem;
  padding: var(--nm-esp-3);
  background: var(--nm-campo);
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-md);
  box-shadow: var(--nm-sombra-2);
}
/* El chip tonal, con la casilla nativa adentro: seleccionado es borde de
   tinta y fondo `superficie-2`, igual que cualquier otro `.nm-chip`, sin
   oro (DESIGN.md: "ni chips" en la lista de dónde no vive el maíz). */
.filtro-tiendas__lista .nm-chip--casilla:has(:checked) {
  border-color: var(--nm-texto); background: var(--nm-superficie-2); font-weight: 600;
}
.filtro-tiendas__lista .nm-chip--casilla:focus-within {
  outline: 2px solid var(--nm-foco); outline-offset: 2px;
}
/* Tienda sin ruta de un clic, excluida por "Solo de un clic": marcada
   (arriba, mismo selector `:has(:checked)`) y sin poder destildarse. El
   asterisco es terciario, no oro — es puntuación de la nota, no un dato. */
.filtro-tiendas__lista .nm-chip--casilla:has(input:disabled) { cursor: not-allowed; }
.filtro-tiendas__asterisco { color: var(--nm-texto-3); margin-left: 1px; }
/* La nota discreta de que esto también recorta el reparto (2026-09-24):
   `.nm-meta` (texto terciario) en su propia línea dentro del panel de chips. */
.filtro-tiendas__nota { flex-basis: 100%; margin: 2px 0 0; }

/* ───────────── Tus compras (ESPEC_COMPRAS) ───────────── */

/* El enlace del carrito y «Lo compré» comparten fila: son las dos acciones
   del bloque de llevar, una abre la tienda y la otra cierra el circuito. */
.carrito__acciones {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--nm-esp-3) var(--nm-esp-5);
}
/* En el teléfono el botón a la tienda va a lo ancho —es lo que se aprieta
   al terminar de mirar el carrito— y «Lo compré» baja a su propia línea. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .carrito__acciones .carrito__enlace { flex: 1 1 100%; }
}
:root[data-forma="app"] .carrito__acciones .carrito__enlace { flex: 1 1 100%; }
/* Borde de tinta y texto pleno, no el borde gris del chip: con el gris era
   «casi invisible» (Gonzalo, 2026-09-28). */
.carrito__comprado {
  padding: 6px var(--nm-esp-4);
  background: transparent;
  border: 1.5px solid var(--nm-texto);
  border-radius: var(--nm-radio-full);
  color: var(--nm-texto);
  font: inherit; font-size: var(--nm-b3); font-weight: 500;
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .carrito__comprado:hover {   background: var(--nm-superficie-2); }
  .carrito-push--armado .carrito__comprado--barra:hover { background: var(--nm-primario-hover); }
}
/* Marcado = chip seleccionado: tonal con borde de tinta, nunca de color. */
.carrito__comprado--hecho {
  border-color: var(--nm-texto);
  color: var(--nm-texto);
}
.carrito__comprado[disabled] { opacity: .6; cursor: default; }
/* «Lo compré» en la barra solo existe una vez armado el carrito en la
   tienda (`carrito-push--armado`, `aplicarEnlaceUsado` en reparto.js): ahí
   es EL primario —lo único que queda por hacer— y «Armar en X ↗» baja a
   borde para que la capa siga teniendo un solo primario. */
.carrito__comprado--barra { display: none; }
.carrito-push--armado .carrito__comprado--barra {
  display: inline-flex; align-items: center; justify-content: center;
  height: 44px; padding: 0 20px;
  background: var(--nm-primario); color: var(--nm-primario-tinta);
  border-color: var(--nm-primario);
  font: 700 15px / 1 var(--nm-cuerpo);
}
.carrito-push--armado .push__barra .carrito__enlace {
  background: transparent; color: var(--nm-texto);
  border: 1.5px solid var(--nm-borde-fuerte);
}
/* En el teléfono no caben el total y dos botones con texto: el enlace a la
   tienda queda como botón redondo «↗» de 44 px junto a «Lo compré». El texto
   «Armar en X ↗» sigue en el DOM (font-size 0), así que es su nombre
   accesible; la flecha la pinta el ::before. Una segunda fila quedaba tapada
   por la barra de pestañas. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .carrito-push--armado .push__barra .carrito__enlace {
    width: 44px; padding: 0; font-size: 0; flex: none;
  }
  :root:not([data-forma="web"]) .carrito-push--armado .push__barra .carrito__enlace::before {
    content: "↗"; font-size: 18px; line-height: 1;
  }
}
:root[data-forma="app"] .carrito-push--armado .push__barra .carrito__enlace {
  width: 44px; padding: 0; font-size: 0; flex: none;
}
:root[data-forma="app"] .carrito-push--armado .push__barra .carrito__enlace::before {
  content: "↗"; font-size: 18px; line-height: 1;
}
/* La Bolsa (DESIGN.md «Mascota: la Bolsa», «Lo compré»): un salto corto,
   una sola vez, junto al botón que acaba de confirmar (`reparto.js`,
   `pintarBotonComprado`). Chica y en línea, nunca empuja el layout más de
   lo que un chip nuevo ya movería. La entrada es la de la interfaz
   (120/200/320ms, DESIGN.md «Motion»): la pieza en sí ya trae su propio
   salto pintado a mano. */
.carrito__bolsa-comprado { width: 48px; height: 48px; vertical-align: middle; margin-left: var(--nm-esp-2); }
@media (prefers-reduced-motion: no-preference) {
  .carrito__bolsa-comprado { animation: nm-bolsa-entra 200ms var(--nm-curva-salida); }
}
@keyframes nm-bolsa-entra { from { opacity: 0; transform: scale(.8); } to { opacity: 1; transform: scale(1); } }

/* Sin hero y sin párrafo bajo el título (PANTALLAS §3 P6): la pantalla abre
   con el trofeo. El estado es una línea de servicio y solo ocupa sitio
   cuando dice algo. */
.compras__estado:empty { display: none; }
.compras__estado { margin: 0 0 var(--nm-esp-3); color: var(--nm-texto-3); }
/* El tab bar flota sobre el final de la pantalla: sin este aire la última
   compra del historial queda debajo y no se puede ni leer ni abrir. */
.compras__cuerpo {
  display: flex; flex-direction: column; gap: var(--nm-esp-4);
  padding-bottom: calc(var(--nm-esp-5) + var(--nm-barra-tabs) + env(safe-area-inset-bottom));
}
.compras__subtitulo {
  margin: 0; font: 700 17px / 1.2 var(--nm-cuerpo); letter-spacing: -0.01em;
}

/* EL TROFEO Y TU QUIPU, en la misma tarjeta: la cifra que se ganó y la
   cuerda donde se fue anudando. Es el único oro de la pantalla, más el nudo
   focal de la cuerda y los «menos» de las filas. */
.compras__trofeo .nm-trofeo__cifra { font-size: clamp(32px, 9vw, var(--nm-precio)); }
.compras__trofeo .nm-trofeo__cifra--tinta {
  color: var(--nm-texto); font-family: var(--nm-cuerpo); font-weight: 700;
}
.compras__trofeo > .nm-meta { margin: 0; }
/* Cuánto falta para el hito: la misma voz de la meta, separada una línea
   porque es otra frase y no la continuación de «en N compras desde …». */
.compras__trofeo > .compras__hito { margin-top: var(--nm-esp-2); }
/* La cuerda se sale de los bordes de la tarjeta: es el ancho de la tarjeta lo
   que la recorta, y el carril empieza y termina en el aire, no en un filo. */
.compras__quipu { margin: var(--nm-esp-4) calc(var(--nm-esp-4) * -1) 0; }
.compras__quipu .nm-quipu-personal__carril { padding: 0 var(--nm-esp-4); }

/* ----- la gráfica de gasto -----

   Barras en terciario con el período en curso en tinta, y la comparación que
   sirve dicha con palabras arriba: «Esta semana S/ 444.14 · promedio S/ 428».
   Sin color por tienda y sin nudo de oro — el censo de esta pantalla ya lo
   gastan el trofeo y el nudo focal del quipu (PANTALLAS §8). */

.gasto__cabecera {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--nm-esp-3);
}
.gasto__controles { display: flex; align-items: center; gap: var(--nm-esp-3); }
.gasto__modos { display: flex; gap: var(--nm-esp-2); }
.gasto__nav { display: flex; gap: var(--nm-esp-1); }
.gasto__flecha {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  background: transparent;
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); font-size: 15px; cursor: pointer;
}
.gasto__flecha[disabled] { opacity: .35; cursor: default; }
.gasto__resumen {
  margin: var(--nm-esp-3) 0 0;
  font-size: var(--nm-b2); color: var(--nm-texto-2);
}
/* position: relative — el tooltip del quipu (`.gasto__tip`) cuelga de esta
   caja, no del documento (restaurado de `ee50b83^`). */
.gasto__lienzo { margin-top: var(--nm-esp-3); position: relative; }
/* El quipu escala con su `viewBox` y conserva la proporción: la cuerda madre
   no se estira más que los colgantes (con `height` fija y
   `preserveAspectRatio: none`, que es como se pintaban las barras, un quipu
   angosto se deformaba). */
.gasto__svg { display: block; width: 100%; height: auto; }
.gasto__eje {
  fill: var(--nm-texto-3); font-family: var(--nm-cuerpo); font-size: 13px;
}
/* El resaltado por tienda (restaurado de `ee50b83^`): con una tienda
   previsualizada o fijada, `.gasto__svg--filtro` retrocede los segmentos que
   no son suyos a transparentes y `.gasto__seg--activa` sube los suyos —en
   TODOS los colgantes— a tinta plena, por encima del tono que le tocaba.
   Mismo recurso tonal de siempre (la rampa de `nm-quipu-gasto__hilo--tN`),
   nada de colores nuevos.

   El apagado se escribe en `stroke-opacity`/`fill-opacity` del hilo y del
   nudo, NUNCA en `opacity` del `<g>` que los envuelve: un `<g>` con opacidad
   compone como una sola capa, así que un nudo con `fill-opacity: 1` adentro
   de un grupo al 12 % igual sale al 12 % — y ESE nudo puede ser el maíz
   (`--nudo--focal`), que no se apaga nunca (PANTALLAS §8). Por eso el maíz
   queda afuera de las dos reglas con `:not()`. */
.gasto__svg--filtro .gasto__seg:not(.gasto__seg--activa) .nm-quipu-gasto__hilo,
.gasto__svg--filtro .gasto__seg:not(.gasto__seg--activa) .nm-quipu-gasto__nudo:not(.nm-quipu-gasto__nudo--focal) {
  stroke-opacity: .12; fill-opacity: .12;
}
.gasto__seg--activa .nm-quipu-gasto__hilo,
.gasto__seg--activa .nm-quipu-gasto__nudo:not(.nm-quipu-gasto__nudo--focal) {
  stroke-opacity: 1; fill-opacity: 1;
}
.gasto__tip {
  position: absolute; z-index: 5;
  transform: translate(-50%, -100%);
  padding: 5px 12px;
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-md);
  box-shadow: var(--nm-sombra-2);
  color: var(--nm-texto);
  font-size: var(--nm-b3);
  white-space: nowrap;
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--nm-mov-micro) var(--nm-curva-salida),
              transform var(--nm-mov-micro) var(--nm-curva-salida),
              display var(--nm-mov-micro) allow-discrete;
}
/* Antes era @keyframes: reiniciaba desde cero en cada `pointerover`, así
   que un segundo hover sobre el mismo segmento antes de que el primero
   terminara se veía saltar. `[hidden]` sigue siendo el interruptor (JS,
   compras.js) — la transición corre en `display` con `allow-discrete` y el
   estado de entrada lo pone `@starting-style`, como en `.nm-popover` /
   `.nm-hoja` / `.nm-modal` (componentes.css). */
@starting-style {
  .gasto__tip:not([hidden]) { opacity: 0; transform: translate(-50%, calc(-100% + 4px)); }
}
/* La transición del apagado/encendido vive en `.nm-quipu-gasto__hilo` y
   `.nm-quipu-gasto__nudo` (componentes.css), con su propio
   `prefers-reduced-motion`; acá solo la entrada del tooltip. */
@media (prefers-reduced-motion: reduce) {
  .gasto__tip { transition: none; }
}
/* R55: la leyenda de tiendas bajo el eje. Dice qué tienda es cada tono, con
   el logo a color y chico (PANTALLAS §8), y solo las de la ventana.

   Cada tienda es además un botón (restaurado de `ee50b83^`,
   `.gasto__tienda[data-tienda]`): fija el resaltado de esa tienda en el
   quipu de arriba (aria-pressed lo marca), y es la vía por teclado del
   mismo resaltado que el hover del quipu ofrece al puntero. */
.gasto__leyenda {
  display: flex; flex-wrap: wrap; gap: var(--nm-esp-2) var(--nm-esp-4);
  margin: var(--nm-esp-3) 0 0; padding: 0; list-style: none;
  color: var(--nm-texto-2);
}
.gasto__tienda {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--nm-radio-full);
  color: inherit;
  font: inherit; font-size: var(--nm-b3);
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida);
}
.gasto__tienda[aria-pressed="true"] { border-color: var(--nm-texto); color: var(--nm-texto); }
.gasto__leyenda .compra__logo { width: 16px; height: 16px; }
.gasto__detalle { margin-top: var(--nm-esp-4); }
.gasto__detalle summary {
  color: var(--nm-texto-2); font-size: var(--nm-b3); cursor: pointer;
}
.gasto__detalle .reparto__tabla { margin-top: var(--nm-esp-3); }
.gasto__detalle .reparto__tabla td:last-child { font-weight: 600; }
@media (hover: hover) and (pointer: fine) {
  .gasto__flecha:hover:not([disabled]) { border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
  .gasto__detalle summary:hover { color: var(--nm-texto); }
  .gasto__tienda:hover { border-color: var(--nm-borde); }
}
@media (prefers-reduced-motion: reduce) {
  .gasto__tienda { transition: none; }
}

/* ----- carritos comprados -----

   Una fila por carrito: el logo de la cadena en su color, la tienda, la
   fecha con el conteo, el total en tabular y lo que ese carrito ahorró, en
   oro. El chevrón abre el desglose. */

.compras__filas { margin: 0; padding: 0; list-style: none; }
.compra { display: block; }
.compra__detalle { min-width: 0; }
.compra__cabecera { list-style: none; cursor: pointer; }
.compra__cabecera::-webkit-details-marker { display: none; }
.compra__logo { width: 32px; height: 32px; }
.compra__logo--letra {
  display: grid; place-items: center;
  background: var(--nm-superficie-2); color: var(--nm-texto-2);
  font-weight: 700; font-size: var(--nm-b3);
}
.compra__cabecera .nm-fila__cuerpo { display: flex; flex-direction: column; gap: 2px; }
.compra__ahorro { display: block; margin-top: 2px; }
.compra__chevron {
  color: var(--nm-texto-3); flex: none;
  transition: transform var(--nm-mov-base) var(--nm-curva-salida);
}
.compra__detalle[open] .compra__chevron { transform: rotate(90deg); }
/* La compra que señaló un nudo del quipu: el mismo resalte tonal de la fila
   recién agregada, sin oro (PANTALLAS §8). */
.compra--senalada .compra__cabecera { background: var(--nm-superficie-2); }

/* Borrar, al pie del desglose: enlace de texto (`.enlace-boton`) en texto
   secundario, que gana tinta al hover. No es un botón de alarma: la hoja que
   abre dice la consecuencia antes de borrar. */
.compra__borrar {
  display: inline-block; margin-top: var(--nm-esp-3);
  color: var(--nm-texto-2); font-size: var(--nm-b3);
  text-decoration-color: var(--nm-borde-fuerte); text-underline-offset: 3px;
}
@media (hover: hover) and (pointer: fine) {
  .compra__borrar:hover {   color: var(--nm-texto); text-decoration-color: currentColor; }
}
.compra__cuerpo { padding: var(--nm-esp-3) 0 var(--nm-esp-2); }
.compra__items { margin: 0; padding: 0; list-style: none; }
.compra__item {
  display: grid; grid-template-columns: 1fr auto auto;
  column-gap: var(--nm-esp-4); align-items: baseline;
  padding: 5px 0;
  font-size: var(--nm-b2);
}
.compra__item + .compra__item { border-top: 1px solid var(--nm-borde-sutil); }
/* Con foto, la línea se centra en ella: a 32px ya no hay una línea base
   que compartir con el texto. */
.compra__item--con-foto { grid-template-columns: auto 1fr auto auto; align-items: center; }
.miniatura--compra { width: 32px; height: 32px; }
.miniatura--compra.miniatura--vacia img { width: 18px; height: 18px; }
.compra__item-cantidad { color: var(--nm-texto-3); text-align: right; max-width: 15em; }
.compra__pie { margin: var(--nm-esp-3) 0 0; color: var(--nm-texto-3); }

/* ---------------------------------------------------------------------
   Llevar el carrito paso a paso (Tottus y Freshmart)

   Estas dos tiendas no arman el carrito desde un enlace, así que en vez
   del enlace va una lista de lo que hay que agregar. Se diseña como lista
   de tareas y no como instructivo: la persona va y viene de la tienda, y
   lo único que necesita al volver es ver por dónde iba.

   Sin color propio: el paso a paso NO es un estado de alerta ni una
   promoción, es la misma acción de «llevar» con otra forma. Todo el peso
   lo llevan la superficie anidada y el número de cada paso; el oro no
   entra porque acá no hay ningún dato que puntuar (DESIGN.md §oro).
   --------------------------------------------------------------------- */
/* El mismo botón que «Armar este carrito»: es la misma acción con otra
   forma. Abierto, queda tonal como un chip elegido. */
.carrito__paso-abrir { font-family: inherit; cursor: pointer; }
.carrito__paso-abrir--abierto { background: var(--nm-superficie-2); }

.carrito__pasos {
  margin-top: var(--nm-esp-4);
  padding: var(--nm-esp-4);
  background: var(--nm-superficie-2);
  border-radius: var(--nm-radio-lg);
}
.carrito__pasos-intro { margin: 0 0 var(--nm-esp-3); color: var(--nm-texto-2); }

.carrito__pasos-lista { margin: 0; padding: 0; list-style: none; }
.paso {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  padding: var(--nm-esp-3) 0;
  border-top: 1px solid var(--nm-borde);
}
.paso:first-child { border-top: 0; }

/* El número es el control: se toca para marcar, así el blanco es grande
   en el teléfono y no hay una casilla diminuta al lado de un número. */
.paso__marca { position: relative; flex: none; cursor: pointer; }
.paso__hecho {
  position: absolute; inset: 0; margin: 0;
  opacity: 0; cursor: pointer;
}
.paso__num {
  display: grid; place-items: center;
  width: 28px; height: 28px;
  border: 1px solid var(--nm-borde-fuerte);
  border-radius: var(--nm-radio-full);
  font-size: var(--nm-b3); color: var(--nm-texto-2);
  transition: background var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida);
}
.paso__hecho:focus-visible + .paso__num {
  outline: 2px solid var(--nm-foco); outline-offset: 2px;
}
/* Marcado = tinta llena, el mismo lenguaje del chip seleccionado. */
.paso--hecho .paso__num {
  background: var(--nm-texto); border-color: var(--nm-texto);
  color: var(--nm-superficie-1);
}

.paso__que { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.paso__nombre { color: var(--nm-texto); }
.paso__cuanto { color: var(--nm-texto-2); }
/* Tachar el nombre y no la fila entera: la cantidad se sigue consultando
   después de marcar, porque es lo que se teclea en la tienda. */
.paso--hecho .paso__nombre { color: var(--nm-texto-3); text-decoration: line-through; }

.paso__abrir {
  flex: none;
  color: var(--nm-texto); font-size: var(--nm-b3); font-weight: 500;
  text-decoration: none;
  border-bottom: 1px solid var(--nm-borde);
}
@media (hover: hover) and (pointer: fine) {
  .paso__abrir:hover {   border-bottom-color: var(--nm-texto); }
}
.paso__sin-ficha { flex: none; color: var(--nm-texto-3); font-size: var(--nm-b3); }

.carrito__pasos-pie {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--nm-esp-3);
  margin: var(--nm-esp-3) 0 0;
  padding-top: var(--nm-esp-3);
  border-top: 1px solid var(--nm-borde);
  color: var(--nm-texto-2);
}
.carrito__pasos-canasta { color: var(--nm-texto); text-decoration: none; border-bottom: 1px solid var(--nm-borde); }
@media (hover: hover) and (pointer: fine) {
  .carrito__pasos-canasta:hover {   border-bottom-color: var(--nm-texto); }
}

/* «Cómo quieres comprar» se apila en vez de ir en fila como «Máximo de
   tiendas»: el panel de opciones es una columna angosta, y una etiqueta de
   tres palabras al lado de un <select> parte el renglón en tres. Apilado,
   además, el selector se lee entero, que importa cuando sus tres opciones
   son la disyuntiva misma y no un número. */
.opcion--apilada { flex-direction: column; align-items: stretch; gap: var(--nm-esp-2); }
.opcion--apilada select { width: 100%; }

/* La nota que explica el compromiso de «Cómo quieres comprar». Vive fuera
   del <label> porque cambia con la elección y leerla dentro del control la
   volvía parte del texto clicable, pero se pinta igual que el <small> de las
   otras opciones: es lo mismo que ellas y a tamaño de cuerpo se comía el
   panel, que tiene alto acotado y se corta a media frase en el teléfono. */
.opcion__nota {
  margin: var(--nm-esp-2) 0 0;
  color: var(--nm-texto-3);
  font-size: var(--nm-b3);
  line-height: 1.45;
}
/* El margen negativo es SOLO del panel del reparto, y esto se reparó el
   2026-09-12: la nota lo llevaba en su regla base para comerse el `gap` de
   12 px de `.opciones__cuerpo`, y cuando «Cómo quieres comprar» se mudó a
   Ajustes —donde no hay grilla con gap— los −8 px le montaron el texto
   encima del borde del <select>. Visto en la captura de móvil claro. */
.opciones__cuerpo > .opcion__nota { margin-top: calc(var(--nm-esp-2) * -1); }

/* ---------- Ajustes (P7) ----------

   Pantalla de ajustes: precisión, no espectáculo. Se entra en push desde el
   avatar, así que empieza con la vuelta y el título, y lo demás son bloques
   de filas (`.nm-ajustes`, la hoja de componentes) con el valor puesto
   debajo del nombre. Cero oro en toda la vista: acá no hay ningún dato
   ganado (PANTALLAS §8). Sin marcador de sección: los rótulos ya separan y
   el logo de la cabecera es la única aparición del motivo. */
.ajustes {
  max-width: 34rem; margin: 0 auto;
  /* el sitio de las dos capas fijas de abajo: la última fila no queda
     debajo del tab bar (PANTALLAS §1 regla 10) */
  padding: var(--nm-esp-4) var(--nm-esp-4)
           calc(var(--nm-barra-tabs) + 88px + env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: var(--nm-esp-3);
}
/* En la forma web Ajustes se lee como una hoja centrada (PANTALLAS §2.2):
   la misma columna sobre una superficie con borde, separada del cromo, y los
   bloques adentro un escalón más arriba (cal sobre caliza). En la forma app
   es la pantalla entera sobre el fondo, y por eso cada regla va con su gemela
   (`test_forma_app.py`). */
.ajustes--web-hoja {
  margin: var(--nm-esp-6) auto; padding: var(--nm-esp-5) var(--nm-esp-5) var(--nm-esp-6);
  background: var(--nm-superficie-1); border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-xl);
}
.ajustes--web-hoja .nm-ajustes { background: var(--nm-superficie-2); }
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .ajustes--web-hoja {
    margin: 0 auto; padding: var(--nm-esp-4) var(--nm-esp-4)
      calc(var(--nm-barra-tabs) + 88px + env(safe-area-inset-bottom));
    background: transparent; border: 0; border-radius: 0;
  }
  :root:not([data-forma="web"]) .ajustes--web-hoja .nm-ajustes { background: var(--nm-superficie-1); }
}
:root[data-forma="app"] .ajustes--web-hoja {
  margin: 0 auto; padding: var(--nm-esp-4) var(--nm-esp-4)
    calc(var(--nm-barra-tabs) + 88px + env(safe-area-inset-bottom));
  background: transparent; border: 0; border-radius: 0;
}
:root[data-forma="app"] .ajustes--web-hoja .nm-ajustes { background: var(--nm-superficie-1); }
.ajustes__volver { align-self: flex-start; padding: 4px 0; }
/* Sin cuenta no hay inicial que poner, y el disco de añil con un «?» adentro
   se leía como un control que pregunta algo (QA M15): no se pinta. La línea
   «Sin cuenta» y su explicación dicen todo lo que hay que decir. */
.nm-cuenta__avatar--vacio { display: none; }
.ajustes__titulo { margin: 0 0 var(--nm-esp-3); }
/* El título del cuerpo se ve SIEMPRE. Se había escondido en app para no
   repetir el del cromo, pero Ajustes es una capa en push y `abrirAjustes`
   deja la cabecera de 56 vacía a propósito: escondiéndolo también, la
   pantalla se quedaba sin nombre en el teléfono y con un `h1` hueco arriba
   (QA M15). Dos títulos no hay: arriba solo está el «‹ Lista». */
.ajustes__form { display: contents; }
.ajustes__rotulo { margin: var(--nm-esp-4) 0 0; }

/* «Tus tiendas», la subpantalla de Ajustes (QA R21). Los controles son los
   del reparto de siempre —no se reescriben, para no renombrar los ids `op-*`
   que `nucleo.js` busca—, así que acá se les pone el formato de las filas de
   Ajustes: mismo alto de aire, mismo separador, mismo ancho de canaleta. */
.ajustes-tiendas__grupo .carritos-hoja { display: block; }
.ajustes-tiendas__grupo .opciones__estado,
.ajustes-tiendas__grupo .opcion,
.ajustes-tiendas__grupo .opcion-tiendas,
.ajustes-tiendas__grupo .opcion__nota {
  padding: 12px 16px; margin: 0; border-top: 1px solid var(--nm-borde-sutil);
}
/* la nota de «Cómo quieres comprar» es la continuación de su control, no
   otra fila: va pegada arriba y sin línea de por medio */
.ajustes-tiendas__grupo .opcion__nota { border-top: 0; padding-top: 0; }
.ajustes-tiendas__grupo .opciones__estado:first-child { border-top: 0; }
.ajustes-tiendas__grupo .opcion--select { min-height: 60px; }
/* «Más ajustes» sale de la tarjeta: es una salida, no una fila */
.ajustes-tiendas__grupo #ir-ajustes {
  width: 100%; border-top: 1px solid var(--nm-borde-sutil); border-radius: 0;
}
.ajustes__cuenta { padding: 0 var(--nm-esp-2) var(--nm-esp-2); }
.ajustes__cuenta .nm-cuenta__quien { flex: 1 1 14rem; }
.ajustes__nota { margin: 4px 0 0; }
.ajustes__nombre { display: block; }
/* El nombre se edita en su sitio: se ve como el título que es y el campo
   solo aparece al enfocarlo. Lo que trae Google es un valor por defecto. */
.ajustes__nombre-campo {
  width: 100%; border: 0; border-bottom: 1px dashed var(--nm-borde-fuerte);
  background: transparent; color: var(--nm-texto); padding: 0 0 2px;
  font: inherit; font-size: 19px; font-weight: 700; letter-spacing: -0.01em;
}
.ajustes__nombre-campo:focus { outline: none; border-bottom-color: var(--nm-texto); }

.ajustes__barra { display: flex; align-items: center; gap: var(--nm-esp-3); }
.ajustes__estado { flex: 1; min-height: 1.2em; margin: 0; }

/* Las hojas de «Tu hogar» y «Tus tarjetas». La pieza es `.nm-hoja`; acá va
   solo lo que es de esta pantalla. */
.ajustes__hoja {
  width: min(100%, 34rem); margin: auto auto 0; padding: var(--nm-esp-5);
  display: flex; flex-direction: column; gap: var(--nm-esp-4);
  max-height: 88dvh; overflow-y: auto;
}
.ajustes__hoja-cabecera { display: flex; align-items: center; gap: var(--nm-esp-3); }
.ajustes__hoja-cabecera h2 { flex: 1; margin: 0; }
.ajustes__hoja p { margin: 0; color: var(--nm-texto-2); }
.ajustes__hogar { display: flex; flex-wrap: wrap; gap: var(--nm-esp-4); }
.ajustes__campo { display: flex; flex-direction: column; gap: var(--nm-esp-2); }
.ajustes__campo label { color: var(--nm-texto-3); }
.ajustes__numero {
  width: 4ch; min-width: 0; border: 0; background: transparent; color: var(--nm-texto);
  text-align: center; font: 600 17px / 1 var(--nm-cuerpo);
  -moz-appearance: textfield; appearance: textfield;
}
.ajustes__numero::-webkit-outer-spin-button,
.ajustes__numero::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Los momentos: una fila por momento con su interruptor, que es una casilla
   de verdad debajo. */
.ajustes__momentos { margin: 0; padding: 0; border: 0; min-width: 0; }
.ajustes__momentos legend { padding: 0; margin-bottom: var(--nm-esp-2); }
.ajustes__momentos-lista { display: flex; flex-direction: column; }
.ajustes__momento {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  min-height: 44px; cursor: pointer;
  border-bottom: 1px solid var(--nm-borde-sutil);
}
.ajustes__momento:last-child { border-bottom: 0; }
.ajustes__momento span { flex: 1; }
.ajustes__momento input { position: absolute; opacity: 0; width: 0; height: 0; }
.ajustes__momento i {
  display: block; width: 40px; height: 24px; border-radius: var(--nm-radio-full);
  background: var(--nm-superficie-2); border: 1px solid var(--nm-borde); position: relative;
  transition: background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.ajustes__momento i::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: var(--nm-radio-full); background: var(--nm-texto-3);
  transition: transform var(--nm-mov-base) var(--nm-curva-mover),
              background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.ajustes__momento input:checked + i { background: var(--nm-primario); border-color: var(--nm-primario); }
.ajustes__momento input:checked + i::after { transform: translateX(16px); background: var(--nm-primario-tinta); }
.ajustes__momento input:focus-visible + i { outline: 2px solid var(--nm-foco); outline-offset: 2px; }

/* Los chips de tarjeta: el logo de la cadena en su color, chico, y el borde
   de tinta cuando está marcada (nunca oro, nunca color de marca ajena). */
.ajustes__tarjeta input { position: absolute; opacity: 0; width: 0; height: 0; }
.ajustes__tarjeta:has(input:checked) {
  border-color: var(--nm-texto); background: var(--nm-superficie-2); font-weight: 600;
}
.ajustes__tarjeta:has(input:focus-visible) { outline: 2px solid var(--nm-foco); outline-offset: 2px; }
.ajustes__tarjetas-vacio { margin: 0; }

.ajustes__salida { display: flex; flex-direction: column; gap: var(--nm-esp-3); margin-top: var(--nm-esp-5); }
.ajustes__borrar { color: var(--nm-error); border: 1px solid var(--nm-error); }
.ajustes__resultado, .ajustes__version { margin: 0; text-align: center; }
.ajustes__version { margin-top: var(--nm-esp-4); }
.perfil__ayuda { margin: 0; color: var(--nm-texto-2); text-align: center; font-size: var(--nm-b3); }

/* El modal de «Borrar mis datos»: la pieza es `.nm-modal`. */
.ajustes__modal-acciones { display: flex; gap: var(--nm-esp-3); justify-content: flex-end; margin-top: var(--nm-esp-4); }
.ajustes__modal p { margin: var(--nm-esp-3) 0 0; color: var(--nm-texto-2); }


/* ───────────── El sello del precio con beneficio ───────────── */

/* La segunda cifra: lo que pagaría quien tiene esa tarjeta. Es un DATO
   secundario, no una etiqueta —nada de badge de color, nada de fondo—: el
   precio en tinta (el porqué está en la regla de la cifra, más abajo) y el
   nombre del beneficio en texto chico al lado. Aparece en la ficha, en los
   resultados, en las filas de la lista y en los pasos del reparto, y se ve
   igual en las cuatro: el mismo aviso no cambia de tono porque el contexto
   parezca más importante.

   `tabular-nums` como cualquier precio: van uno debajo del otro y las comas
   tienen que alinearse. */
.sello-beneficio {
  display: inline-flex; align-items: baseline; gap: .35em;
  color: var(--nm-texto-3);
  font-size: var(--nm-b3); line-height: var(--nm-lh-ui);
  font-style: normal;
}
/* La cifra va en tinta y no en oro: el oro es del monto ganador, uno por
   bloque, y un desplegable con varias líneas de Wong y Metro sellaba dos
   cifras por fila hasta que el censo se pasaba de «unos pocos destellos». */
.sello-beneficio__precio {
  color: var(--nm-texto-2);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
/* Casi siempre sigue a un precio en la misma línea: el aire lo pone él. */
.sello-beneficio { margin-left: .5em; }
.tienda__sello .sello-beneficio { margin-left: 0; }
/* En la fila de datos de la ficha el sello va pegado al precio dentro del
   mismo dato («Precio S/ 2.90 S/ 2.61 con Tarjeta Cencosud»): dos cifras
   seguidas se leen como una sola mal escrita. Un punto medio entre las dos,
   el mismo separador que ya usa esa fila, y el sello se lee como lo que es:
   la segunda cifra, condicionada. */
/* Lo mismo en la alternativa y en la cabecera de la ficha: el sello arranca
   en el borde del monto, así que no arrastra el medio em con el que se
   despega cuando va en línea detrás de una cifra. */
.alternativa__sello { display: grid; gap: 2px; justify-items: start; }
.alternativa__sello .sello-beneficio { margin-left: 0; }

/* ═════════════ Los cuatro componentes del rediseño ═════════════
   ESPEC_UI §5.1–5.3. `hoja`, `fila` y `barra`, cada uno en sus dos
   caras, y ninguno pregunta por su cuenta si la forma es app o web: todos
   leen el interruptor de forma de `tokens.css`. Van al final de la hoja a
   propósito, después de los bloques de forma app: son los componentes
   nuevos y ganan por orden, sin necesitar peso de selector.

   El censo de oro de estos tres es CERO. La barra lleva el único relleno
   sólido de la franja de trabajo —el `#optimizar-lista` que ya existe—; el
   FAB se retiró con el chasis nuevo (PANTALLAS §2.1) y su trabajo lo hace el
   botón circular de Buscar, que vive en el tab bar y no es una segunda
   acción primaria. El total de la barra va en tinta: un total no es un
   premio. */

/* ───────────── `hoja` (§5.1) ─────────────
   Un `<dialog>` con `showModal()`, así que el foco atrapado, Escape y la
   inercia de lo de atrás los da el navegador. El elemento es la CAPA
   completa y el panel visible es su hijo: eso es lo que le permite pintar su
   propio velo con un token (ver «El velo de las capas») y lo que hace que
   dos capas apiladas se puedan atenuar desde `:root`.

   Nada de `transform`, `filter` ni `backdrop-filter` en el elemento
   `dialog`: cualquiera de los tres lo convierte en bloque contenedor de sus
   descendientes fijos y rompe el velo de la capa de arriba. La animación
   vive en `.hoja__panel`. */
.hoja {
  position: fixed;
  inset: 0;
  z-index: 60;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  display: grid;
  align-items: var(--nm-hoja-alineado);
  justify-items: center;
  background: var(--nm-velo);
  color: var(--nm-texto);
  overflow: hidden;
}
.hoja::backdrop { background: transparent; }

.hoja__panel {
  display: flex;
  flex-direction: column;
  width: var(--nm-hoja-ancho);
  max-height: 85svh;
  margin: 0;
  padding: 0;
  background: var(--nm-superficie-3);
  color: var(--nm-texto);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-hoja-radio);
  box-shadow: var(--nm-sombra-2);
  overflow: hidden;
}
/* El panel es el destino de respaldo del foco cuando el elemento que abrió
   la hoja murió en un repintado (`lista.js` repinta con innerHTML), así que
   recibe foco por programa y necesita anillo. */
.hoja__panel:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -3px; }

/* El asa: la señal de que esto sube desde abajo. Solo en forma app. */
.hoja__asa {
  display: var(--nm-hoja-asa);
  width: 36px;
  height: 4px;
  margin: var(--nm-esp-2) auto 0;
  border-radius: var(--nm-radio-full);
  background: var(--nm-borde-fuerte);
}

.hoja__cabecera {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--nm-esp-3);
  padding: var(--nm-esp-4) var(--nm-esp-5) var(--nm-esp-3);
}
.hoja__titulo {
  margin: 0; min-width: 0;
  font-family: var(--nm-cuerpo); font-weight: 700; font-size: var(--nm-d4);
  letter-spacing: var(--nm-d4-tracking); line-height: 1.2;
}
/* «Listo» cierra y no compromete nada: es texto secundario, no un botón de
   relleno. El único relleno sólido de esta pantalla es el de la barra. */
.hoja__listo {
  flex: none; padding: var(--nm-esp-2);
  background: none; border: 0;
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b2); font-weight: 600;
  cursor: pointer;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .hoja__listo:hover {   color: var(--nm-texto); }
}

.hoja__cuerpo {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--nm-esp-5) calc(var(--nm-esp-5) + env(safe-area-inset-bottom));
}
.hoja__grupo { margin-top: var(--nm-esp-4); }
.hoja__rotulo {
  margin: 0 0 var(--nm-esp-2);
  color: var(--nm-texto-3);
  font-weight: 600; font-size: var(--nm-l1);
  letter-spacing: var(--nm-l1-tracking); text-transform: uppercase;
}
.hoja__chips { display: flex; flex-wrap: wrap; gap: var(--nm-esp-2); }

/* Chip de variante: tonal con borde de tinta cuando está elegido, como manda
   `chip-selected`. Ningún chip lleva oro. */
.hoja-chip {
  padding: 8px var(--nm-esp-4);
  background: transparent;
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b2);
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .hoja-chip:hover {   border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
}
.hoja-chip[aria-pressed="true"] {
  background: var(--nm-superficie-2);
  border-color: var(--nm-texto);
  color: var(--nm-texto);
}

/* «Envases que te sirven» (PLAN_CANTIDADES.md, regla 3): un pictograma de
   trazo fino con su palabra debajo, multiselección. Encendido = borde de
   tinta y fondo tonal, igual que cualquier chip elegido; apagado = tinta
   terciaria. Ningún envase lleva oro. */
.envases { display: flex; flex-wrap: wrap; gap: var(--nm-esp-2); }
.envase {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; min-width: 88px; min-height: 72px; padding: var(--nm-esp-2) var(--nm-esp-3);
  background: transparent; border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-lg);
  color: var(--nm-texto-3); font: inherit; font-size: var(--nm-b3); line-height: 1.2;
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.envase__icono { width: 28px; height: 28px; }
@media (hover: hover) and (pointer: fine) {
  .envase:hover { border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
}
.envase[aria-pressed="true"] {
  background: var(--nm-superficie-2); border-color: var(--nm-texto); color: var(--nm-texto);
  font-weight: 600;
}
.envase:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }
.envases__aviso { margin: var(--nm-esp-2) 0 0; color: var(--nm-texto-2); }
.envases__aviso[hidden] { display: none; }

/* «Del historial» (§5.4, `lista.js`): se cuelga dentro de la hoja de agregar
   y esconde el mosaico, así que pasa a ser lo primero del cuerpo y no le
   debe margen a nada de arriba. Sus chips son nombres de producto enteros
   —no variantes de dos palabras—, y un nombre largo se parte alineado a la
   izquierda en vez de centrarse. El estado («Trayendo tus compras…», el vacío
   y el error) es una frase de lectura en secundario. */
.hoja__mosaico[hidden] + .hoja-historial { margin-top: 0; }
.hoja-historial .hoja-chip { text-align: left; }
.hoja-historial__estado { margin: 0; max-width: var(--nm-medida); color: var(--nm-texto-2); }

/* El stepper de la hoja es el mismo `.paso-cantidad` de la fila, a tamaño de
   dedo y con su caja siempre visible: acá no compite con un nombre. */
.hoja__grupo--cantidad .paso-cantidad {
  align-self: start;
  border-color: var(--nm-borde);
}
/* La unidad, centrada bajo el stepper como su rótulo: al lado no cabía junto
   al primario en la hoja angosta de Buscar, y suelta caía a otra línea bajo el
   tacho (Gonzalo, dev, 2026-09-25). En lo que se pide por unidad o por kilo,
   ahí mismo va el chip «und ⇄» de la fila, con el peso de una pieza al lado
   (Gonzalo, 2026-09-28). */
.hoja__grupo--cantidad {
  flex: none; display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.hoja__cantidad-unidad { color: var(--nm-texto-3); line-height: 1.2; }
.hoja__cantidad-pie { display: flex; align-items: center; gap: var(--nm-esp-2); }
.hoja__cantidad-pie .item-fila__cantidad-unidad--boton { width: auto; color: var(--nm-texto-2); }
.hoja__cantidad-peso { color: var(--nm-texto-3); white-space: nowrap; }
.paso-cantidad--grande .paso-cantidad__boton { width: 48px; min-height: 48px; font-size: var(--nm-b1); }
.paso-cantidad--grande .item-fila__cantidad { font-size: var(--nm-b1); }

/* El mosaico 2×2 de acciones: ícono y verbo, borde sutil, cero relleno. */
.hoja__mosaico {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--nm-esp-2);
  margin-top: var(--nm-esp-5);
}
.hoja-accion {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  min-height: 52px; padding: var(--nm-esp-3) var(--nm-esp-4);
  background: transparent;
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-lg);
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b2);
  text-align: left; text-decoration: none; cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .hoja-accion:hover {   border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
}
.hoja-accion__icono { flex: none; width: 18px; height: 18px; }

/* Lo destructivo al final y separado por un filete, con su basurero: la
   misma regla de la fila —se encuentra sin buscarla y se puede deshacer—,
   sin mancha de relleno que la vuelva un botón de alarma. */
.hoja__pie {
  margin-top: var(--nm-esp-5);
  padding: var(--nm-esp-3) var(--nm-esp-5) calc(var(--nm-esp-4) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--nm-borde-sutil);
}
.hoja__quitar {
  display: inline-flex; align-items: center; gap: 6px;
  padding: var(--nm-esp-2) var(--nm-esp-3) var(--nm-esp-2) 10px;
  background: none; border: 0; border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b2); font-weight: 500;
  cursor: pointer;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
.hoja__quitar svg { width: 16px; height: 16px; flex: none; }
@media (hover: hover) and (pointer: fine) {
  .hoja__quitar:hover {   color: var(--nm-texto); background: var(--nm-superficie-2); }
}

/* La cara web anclada: el mismo `<dialog>` modal —el foco sigue atrapado y
   Escape sigue siendo del navegador— pero sin velo visible y con el panel
   colgado del ⋯ que lo abrió. Se lee como un menú, que es lo que §2bis
   corrige de la primera redacción: en escritorio esto no es un modal
   centrado. Las coordenadas las escribe `hoja.js` una sola vez, desde el
   rect del ancla y con volteo.

   Corregido el 2026-09-12: acá decía que «en forma app el CSS las ignora, así
   que JS no vuelve a decidir la forma», y es falso desde el paso 5. La cara
   anclada no la enciende una variable sino la CLASE `hoja--anclada`, que
   apaga el velo y saca el panel de abajo; si se pusiera en un teléfono, la
   hoja quedaría colgada de un rect en medio de la pantalla y sin velo. Quien
   decide es `hoja.js`, que lee `esFormaApp()` de `/js/forma.3c3e1697.js` —la misma
   fuente que el resto de la casa— y solo ancla en forma web. */
.hoja--anclada { background: transparent; }
.hoja--anclada .hoja__panel {
  position: absolute;
  top: var(--hoja-y, 50%);
  left: var(--hoja-x, 50%);
  width: min(22rem, 92vw);
  border-radius: var(--nm-radio-lg);
}

/* La entrada, por forma: en app sube desde abajo, en web aparece en el
   sitio. El bloque global de `prefers-reduced-motion` de arriba las deja en
   0.01 ms, así que acá no hace falta repetirlo: quien pide menos movimiento
   ve la hoja puesta y no una animación corta. */
@keyframes hoja-sube    { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes hoja-baja    { from { transform: translateY(0); } to { transform: translateY(100%); } }
@keyframes hoja-aparece { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } }
@keyframes hoja-se-va   { from { opacity: 1; } to { opacity: 0; } }
.hoja--entrando .hoja__panel { animation: hoja-sube var(--nm-t-hoja) var(--nm-curva-hoja) both; }
.hoja--saliendo .hoja__panel { animation: hoja-baja var(--nm-t-hoja-salida) var(--nm-curva-hoja) both; }
.hoja--anclada.hoja--entrando .hoja__panel { animation-name: hoja-aparece; }
.hoja--anclada.hoja--saliendo .hoja__panel { animation-name: hoja-se-va; }

/* Dos capas apiladas (la hoja abierta y el visor de foto que se abre desde
   ella) dan .45 sobre .85 y dejan la pantalla casi negra. El contador de
   capas de `:root` apaga el velo de la de abajo; no se resuelve con `:has()`
   sobre el dialog hermano porque iOS 15 no lo tiene. */
:root.con-capa-2 .hoja { background: transparent; }

/* El nombre sigue siendo la puerta a la ficha, y la única, pero el subrayado
   se retiró de la fila de la lista (PANTALLAS §4): en una lista de cincuenta
   productos cincuenta subrayados hacen una página de enlaces y el nombre deja
   de ser el protagonista. En las filas de SOLO LECTURA —el carrito, las
   alternativas— el subrayado se queda: ahí hay pocos y no compiten. */
a.fila__nombre {
  color: inherit;
  text-decoration: underline; text-decoration-color: var(--nm-borde-fuerte);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  a.fila__nombre:hover { text-decoration-color: var(--nm-acento); }
}

/* La fila de la lista se rediseñó (PANTALLAS §3 P1): su geometría vive
   arriba, en «La lista», y la mide `tests/test_geometria_ui.py`. Acá queda
   `.fila`, el hermano de SOLO LECTURA —las bajadas, el carrito, las
   alternativas—, que no tiene controles y por eso no se unificó con ella. */

/* `.fila` — el hermano de solo lectura, con lo compartido escrito una vez. */
.fila {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--nm-esp-3);
  min-height: 56px;
  padding: var(--nm-esp-2) var(--nm-esp-5);
  border-top: 1px solid var(--nm-borde-sutil);
}
.fila:first-child { border-top: 0; }
.fila__nombre {
  min-width: 0;
  font-family: var(--nm-cuerpo); font-weight: 600; font-size: var(--nm-b1);
  letter-spacing: -.01em; line-height: 1.25;
}
/* Sin foto llega a doce filas por pantalla, y hay productos donde la foto no
   aporta nada (granel, abarrotes básicos). */
.fila--sin-foto { grid-template-columns: minmax(0, 1fr) auto; min-height: 48px; }

/* ───────────── `barra` (§5.3) ─────────────
   La barra de compromiso no trae un botón nuevo: ENVUELVE el
   `#optimizar-lista` que ya existe. En forma web su chasis es
   `display: contents`, así que el botón se queda donde está hoy —arriba de la
   columna derecha— y el resumen no se pinta. En forma app el mismo elemento
   se vuelve la barra fija, con la cifra y el conteo a la izquierda y el único
   primario de la pantalla a la derecha. El aspecto lo pone `.nm-barra`; acá
   va el layout.

   **Solo se muestra cuando la lista tiene productos** (PANTALLAS §2.1): la
   enciende `html.con-barra`, que pone `lista.js`. Una barra vacía con «S/ 0»
   junto a un botón deshabilitado es una capa fija que no compromete nada.

   `position: fixed` resuelve contra el viewport porque ninguno de sus
   ancestros declara `transform`, `filter` ni `backdrop-filter` —verificado—.
   Si alguien anima esa columna, hay que volver a revisarlo.

   z-index 39: un punto DEBAJO del tab bar (40). */
.barra-compromiso {
  display: none;
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--nm-barra-tabs) + env(safe-area-inset-bottom));
  z-index: 39;
  height: var(--nm-barra-max);
  padding: var(--nm-esp-2) calc(var(--nm-esp-5) + env(safe-area-inset-right))
           var(--nm-esp-2) calc(var(--nm-esp-5) + env(safe-area-inset-left));
}
html.con-barra .barra-compromiso { display: var(--nm-barra-display); }
/* La gemela de Buscar (`#barra-compromiso-buscar`, `buscador.js`): mismo
   aspecto —comparte `.barra-compromiso.nm-barra`— pero un gatillo PROPIO,
   independiente de `html.con-barra`.

   `#buscar` es una capa a pantalla completa que se abre SIN esconder
   `#lista` por debajo (`.buscar { position: fixed; inset: 0 }` la tapa,
   nunca le pone `hidden`) — así que `html.con-barra` sigue encendido
   mientras se busca, y la regla de arriba (que solo mira la clase
   `.barra-compromiso`) prendía TAMBIÉN esta barra con «0 productos» apenas
   se abría Buscar, aunque `con-barra-buscar` siguiera apagado (hallazgo de
   Gonzalo, 2026-09-27). Por eso primero se apaga por id —que le gana en
   especificidad a la clase de arriba, sin importar el orden— y solo el
   gatillo propio la vuelve a encender. */
#barra-compromiso-buscar { display: none; }
html.con-barra-buscar #barra-compromiso-buscar {
  display: var(--nm-barra-display);
  /* Sin offset de tab bar: `.buscar` tapa el tab bar de la app (z-index 50
     contra 40, PANTALLAS §3 P2) y reservarle su alto de todos modos dejaba
     una franja vacía de ~80 px debajo de esta barra (Gonzalo, 2026-09-27).
     `#barra-compromiso`, la de la Lista, sí lo reserva porque ahí el tab
     bar se ve de verdad. */
  bottom: env(safe-area-inset-bottom);
}
/* El resumen solo existe en la cara app: en web el resumen es la columna
   derecha entera, y acá no se pinta. No puede compartir `--nm-barra-display`
   con el chasis —`contents` no esconde nada, empalma a los hijos en el flujo
   del padre—, así que se apaga en base (la base ES la forma web) y se
   enciende en los dos bloques de forma app. */
.barra-compromiso__resumen { display: none; flex-direction: column; margin: 0; min-width: 0; }
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .barra-compromiso__resumen { display: flex; }
}
:root[data-forma="app"] .barra-compromiso__resumen { display: flex; }
/* El total va en TINTA: el oro es del ahorro, y un total a pagar no es un
   premio (DESIGN.md, «el oro se gana, no se hereda»). */
.barra-compromiso__total { color: var(--nm-texto); }
/* Cada parte del conteo entera: si no cabe, parte entre «2 productos» y
   «1 tienda», nunca adentro de una (`conteoDeLaBarraHTML`). */
.barra-compromiso__parte { white-space: nowrap; }
/* `.boton-primario` nació de ancho completo en el panel del reparto; en la
   barra va a la medida de su texto, pegado a la derecha — y eso vale SOLO en
   forma app, donde la barra es una caja de verdad. `display: contents` borra
   la CAJA del chasis, no al chasis como ANCESTRO de un selector: con la regla
   suelta, en forma web le cambiaba la geometría al único primario del sitio
   sin que nadie lo pidiera. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .barra-compromiso .boton-primario { flex: none; width: auto; margin: 0 0 0 auto; }
}
:root[data-forma="app"] .barra-compromiso .boton-primario { flex: none; width: auto; margin: 0 0 0 auto; }
/* El pie no puede tapar la barra (reparado el 2026-09-15). `position: fixed`
   saca a la barra del flujo pero NO del contexto de apilamiento de su banda:
   `.nm-banda` declara `z-index: 0` para dejar su pintado a sangre completa
   detrás del contenido, así que el `z-index: 39` de la barra compite solo
   adentro de la banda. Se sube la banda que CONTIENE la barra un solo punto:
   queda por encima del pie (0) y sigue por debajo del cromo de la raíz. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .nm-banda:has(.barra-compromiso) { z-index: 1; }
}
:root[data-forma="app"] .nm-banda:has(.barra-compromiso) { z-index: 1; }

/* Lo que las capas fijas tapan. El `scroll-padding-bottom` de `html` arregla
   solo el scroll programático —el ancla, `subirAlTope`, el `focus()` del
   stepper—;

/* ───────────── `nota` (§5.6) ─────────────
   El dato a la vista y el porqué a demanda (regla 3 del rediseño): una línea
   con la cifra y un ⓘ que abre la hoja con la explicación. Sustituye a los
   párrafos de envío. La nota hereda el tamaño y el tono de quien la lleva
   —`.carrito__envio-reglas` ya es B3 en secundario, y su variante de alerta
   sube a tinta—, así que acá solo va la gramática del componente: la cifra
   en tinta y con números tabulares, que es lo que se compara, y el ⓘ en
   terciario, porque es servicio y no una acción que compita con el enlace
   del carrito. Ni la cifra ni el ⓘ llevan oro: un «te faltan S/ 105.80» no
   es ahorro (DESIGN.md, «el oro se gana, no se hereda»). */
.nota { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--nm-esp-1); }
.nota__cifra { color: var(--nm-texto); font-variant-numeric: tabular-nums; }
.nota__mas {
  position: relative;
  display: inline-grid; place-items: center; flex: none;
  width: 24px; height: 24px; padding: 0;
  background: none; border: 0; border-radius: var(--nm-radio-full);
  color: var(--nm-texto-3); font: inherit; line-height: 1;
  cursor: pointer;
  transition: color var(--nm-mov-micro) var(--nm-curva-salida);
}
.nota__mas svg { width: 16px; height: 16px; }
/* El ⓘ es un SVG en línea: en bloque, para que la línea de base no le
   agregue aire abajo y quede centrado en su círculo de 24 px. */
.nota__icono { display: block; flex: none; }
/* Lo que abre el ⓘ: la descripción del servidor, una frase por renglón
   (`abrirNota`, reparto.js). Lectura en secundario, a la medida del párrafo,
   con aire entre frases y sin margen después de la última. */
.nota__detalle { margin: 0 0 var(--nm-esp-2); max-width: var(--nm-medida); color: var(--nm-texto-2); }
.nota__detalle:last-child { margin-bottom: 0; }
@media (hover: hover) and (pointer: fine) {
  .nota__mas:hover {   color: var(--nm-texto); }
}
.nota__mas:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }

/* ───────────── La carga larga del reparto, honesta (R39, 2026-09-17) ─────────────
   Hasta acá había tres «pasos» que prometían un progreso que el cliente no
   podía afirmar: las tres fases del reparto pasan dentro de un solo POST,
   el servidor no avisa cuándo termina cada una, y el paso «actual» se
   quedaba lleno para siempre —Gonzalo lo reportó como R39: «siempre veo que
   se llena el primero solamente y me voy de frente a la pantalla con los
   carritos»—. Reemplazado por un indeterminado honesto (PANTALLAS.md §9):
   el trofeo y las tarjetas como esqueleto (`.nm-esqueleto`, el mismo pulso
   de opacidad de toda la app, ningún spinner) en el lugar exacto donde va a
   salir el resultado, más UNA frase con el único número real que el
   cliente sabe («Comparando N productos en M tiendas…»), sin pasos que
   fingir. */
/* La Bolsa haciendo malabares, chica y en línea con la frase — DESIGN.md
   «Mascota: la Bolsa», «Calculando carritos»: acompaña al esqueleto, no lo
   reemplaza. */
.reparto__bolsa-calculando {
  width: 40px; height: 40px; vertical-align: middle;
  margin: 0 var(--nm-esp-2) 0 0;
}
.reparto__esqueleto-frase { margin: 0 0 var(--nm-esp-3); font-variant-numeric: tabular-nums; display: flex; align-items: center; }
.carritos-comprados + .reparto__esqueleto-frase { margin-top: var(--nm-esp-4); border-top: 1px solid var(--nm-borde-sutil); padding-top: var(--nm-esp-4); }
.reparto__esqueleto-trofeo { display: grid; gap: var(--nm-esp-2); }
.reparto__esqueleto-tarjetas { display: grid; gap: var(--nm-esp-3); }
.reparto__esqueleto-tarjeta { display: grid; gap: var(--nm-esp-2); }
.reparto__esqueleto-linea { height: 14px; }
.reparto__esqueleto-linea--corta { width: 40%; }
.reparto__esqueleto-linea--titulo { width: 60%; }
.reparto__esqueleto-linea--cifra { height: 36px; width: 55%; }

/* ───────────── El mosaico de la hoja de fila (§2bis #6) ─────────────
   `.hoja__mosaico` y `.hoja-accion` están arriba, en `hoja`. `.hoja__accion`
   es el nombre fijado para esta vuelta: la celda del mosaico lo lleva junto a
   `.hoja-accion`, y se estiliza igual también sola, para que una superficie
   que solo ponga el nombre nuevo no quede como botón del navegador. La
   última celda impar ocupa las dos columnas: un mosaico de tres no deja un
   hueco al lado de la tercera. */
.hoja__accion {
  display: flex; align-items: center; gap: var(--nm-esp-2);
  min-height: 52px; padding: var(--nm-esp-3) var(--nm-esp-4);
  background: transparent;
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-lg);
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b2);
  text-align: left; text-decoration: none; cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .hoja__accion:hover {   border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
}
:is(.hoja-accion, .hoja__accion):focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }
.hoja__mosaico > :last-child:nth-child(odd) { grid-column: 1 / -1; }

/* ───────────── El control único de orden y filtro del buscador (§6 #9) ─────────────
   Un solo botón que DICE lo que está puesto («Más barato · Solo Lácteos ·
   3 de 9 tiendas») y abre la hoja con los controles. Pastilla secundaria
   —la misma forma que tenían las tres sueltas—, sin relleno de acción ni oro.
   Con un recorte puesto además del orden (`--filtrado`) se lee como el chip
   seleccionado del sistema: tonal con borde de tinta (DESIGN.md,
   `chip-selected`), para que un filtro activo no pase desapercibido.

   El resumen se corta con puntos suspensivos antes que partir la barra del
   contador: el texto entero sigue estando en la hoja. */
.busqueda-control {
  display: inline-flex; align-items: center; gap: 6px;
  min-width: 0; max-width: 100%; min-height: 34px;
  padding: 6px var(--nm-esp-3);
  background: transparent;
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-full);
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b3);
  cursor: pointer;
  transition: border-color var(--nm-mov-micro) var(--nm-curva-salida),
              color var(--nm-mov-micro) var(--nm-curva-salida),
              background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .busqueda-control:hover {   border-color: var(--nm-borde-fuerte); color: var(--nm-texto); }
}
.busqueda-control:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }
.busqueda-control__icono { flex: none; width: 16px; height: 16px; }
.busqueda-control__resumen { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.busqueda-control--filtrado {
  background: var(--nm-superficie-2);
  border-color: var(--nm-texto);
  color: var(--nm-texto);
}
/* Los controles dentro de la hoja: un control por renglón, con el rótulo
   encima, a lo ancho del panel. La lista de tiendas deja de ser un popover
   colgado de su pastilla —en la hoja no hay nada que tapar— y fluye. */
.busqueda-hoja { display: grid; gap: var(--nm-esp-4); padding-top: var(--nm-esp-2); }
.busqueda-hoja .orden { display: grid; gap: var(--nm-esp-2); margin: 0; padding: 0; border: 0; min-width: 0; }
.busqueda-hoja .orden__select {
  width: 100%; min-height: 40px;
  padding: var(--nm-esp-2) var(--nm-esp-3);
  background: var(--nm-campo);
  border-color: var(--nm-borde);
  color: var(--nm-texto); font-size: var(--nm-b2);
}
/* Categoría y Tiendas: dos plegables (`<details>`) hermanos, cada cabecera con
   el valor puesto. Foco visible; el chevron gira como el de `.fuera-panel`. */
.filtro-seccion { border: 0; padding: 0; margin: 0; min-width: 0; }
.filtro-seccion + .filtro-seccion { border-top: 1px solid var(--nm-borde-sutil); padding-top: var(--nm-esp-2); }
.filtro-seccion__resumen {
  display: flex; align-items: baseline; gap: var(--nm-esp-2); list-style: none;
  padding: 4px 2px; cursor: pointer; border-radius: var(--nm-radio-md);
}
.filtro-seccion__resumen::-webkit-details-marker { display: none; }
.filtro-seccion__resumen:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }
.filtro-seccion__valor { color: var(--nm-texto-2); flex: 1 1 auto; min-width: 0; }
.filtro-seccion__valor::before { content: "·"; margin-right: 6px; color: var(--nm-texto-3); }
.filtro-seccion__chevron {
  width: 16px; height: 16px; color: var(--nm-texto-3); flex: none; align-self: center;
  transition: transform var(--nm-mov-base) var(--nm-curva-salida);
}
.filtro-seccion[open] > .filtro-seccion__resumen .filtro-seccion__chevron { transform: rotate(90deg); }
.filtro-seccion__cuerpo { display: grid; gap: var(--nm-esp-2); padding: var(--nm-esp-2) 2px 0; }
.busqueda-hoja .filtro-tiendas__lista {
  position: static; width: auto; max-width: none;
  padding: 0; background: none; border: 0; box-shadow: none;
}

/* En forma web los mismos controles van EN LÍNEA, arriba de los resultados
   (R9): en una ventana de 1280 px sobra sitio y esconderlos detrás de un
   panel flotante al costado derecho era un gesto de teléfono fuera de lugar.
   Es el mismo nodo con los mismos ids y los mismos oyentes; lo único que
   cambia es que fluye en horizontal y no se esconde nunca. */
.busqueda-hoja--en-linea {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--nm-esp-3) var(--nm-esp-5);
  padding: 0 0 var(--nm-esp-3);
}
.busqueda-hoja--en-linea .orden__select { width: auto; min-width: 12rem; }
.busqueda-hoja--en-linea .filtro-seccion { flex: 1 1 22rem; min-width: 0; }
.busqueda-hoja--en-linea .filtro-tiendas__lista {
  display: flex; flex-wrap: wrap; gap: var(--nm-esp-1) var(--nm-esp-3);
}

/* En la COLUMNA derecha (forma web, dos columnas) el panel se abre en su
   sitio, debajo del chip y con el ancho de la columna, y empuja los
   resultados: nunca flota ni se sale de ella. */
.busqueda-hoja--en-columna {
  padding: var(--nm-esp-3);
  margin: 0 0 var(--nm-esp-3);
  background: var(--nm-superficie-1);
  border: 1px solid var(--nm-borde);
  border-radius: var(--nm-radio-lg);
  max-width: 100%; box-sizing: border-box;
}

/* Objetivos de toque de los componentes nuevos: por dedo y no por ancho,
   como el resto de la hoja. La ⓘ de la nota se queda en 24 px a la vista
   —crecer le robaría el renglón a la cifra— y gana los 44 al dedo con un
   pseudo, como la × del plato. */
@media (pointer: coarse) {
  .nota__mas::before { content: ""; position: absolute; inset: -10px; }
  :is(.semana__deshacer, .aviso__deshacer) .enlace-boton {
    display: inline-flex; align-items: center; min-height: 44px;
    padding: 0 var(--nm-esp-2); margin: 0 calc(-1 * var(--nm-esp-2));
  }
  .busqueda-control, .hoja__accion, .busqueda-hoja .orden__select { min-height: 44px; }
  .filtro-seccion__resumen { min-height: 44px; align-items: center; }
  .hoja-chip, .hoja__listo, .hoja__quitar, .hoja-accion { min-height: 44px; }
  .lista-hoja__item { min-height: 52px; }
  .compra__borrar { min-height: 44px; display: inline-flex; align-items: center; }
}

/* ───────────── Impresión ─────────────
   Una lista de compras que no se puede imprimir es un hueco propio (ESPEC_UI
   §7). Los colores los resuelve `tokens.css`, que en `print` vuelve a los
   semánticos del claro con papel cal aunque la pantalla esté de noche. Acá
   va lo que no es color: qué no tiene sentido en papel y qué no se parte.

   Se esconde el cromo —cabecera, pestañas, barra, FAB, tour, capas abiertas,
   las franjas índigo del hero y del pie— y todo control que en papel no se
   puede tocar: quitar, el ⋯, los pasos de cantidad, las acciones y los pasos
   de cada carrito, la escalera, el escáner y los resultados del buscador.
   Queda la lista con sus cantidades y, si se generó, el reparto con sus
   carritos.

   El pie solo se esconde: NUNCA se le escribe `padding` acá, porque
   `tests/test_barra_tabs.py` toma la última regla `.pie` que decide el
   relleno de abajo y exige que sea la de la barra de pestañas. */
@media print {
  .cabecera, .tabs, .barra-compromiso, .nm-barra, .tour, dialog, .nm-franja-indigo,
  .pie, .lista__barra, #aviso-demo, #aviso-semana,
  .item-fila__quitar, .item-fila__zona, .paso-cantidad__boton,
  .carrito__acciones, .carrito__pasos, #escalera, #panel-resultados, #escaner,
  .saltar, .cargando, .nota__mas, .reparto__esqueleto-frase, .reparto__esqueleto-trofeo, .reparto__esqueleto-tarjetas { display: none !important; }

  /* El papel: la superficie (cal en impresión, ver tokens.css) y no
     `--nm-fondo`, que sigue siendo el color de página del modo de pantalla.
     Las dos iniciales de cuenta pintan su letra con `--nm-fondo`; en papel
     van con el mismo cal. */
  body, body.nm-base { min-height: 0; background: var(--nm-superficie-1); }
  .nm-cuenta__avatar, .sesion__avatar { color: var(--nm-superficie-1); }
  html { scroll-padding: 0; }
  /* el sitio que la lista le reserva a la barra fija, que no se imprime */
  .lista__form { padding-bottom: 0; }

  .item-fila, .carrito, .carrito__item { break-inside: avoid; }
  /* El carrito ya no es un `<details>` que haya que abrir en papel: la
     tarjeta de tienda dice tienda, cobertura y total, y sus productos viven
     en el push, que se imprime tal cual está en pantalla. Lo que se retira
     es solo lo que en papel mentiría —un cheurón que no se puede apretar, el
     botón de la tienda, «Lo compré» y «No lo encontré»—. Lo mide
     `tests/test_impresion_carrito_ui.py`. */
  .nm-tienda__chevron, .carrito__enlace, .carrito__comprado, .carrito__no-esta,
  .nm-volver, .cabecera__mas { display: none !important; }

  /* La placa cal del modo oscuro no tiene por qué salir en papel blanco:
     el chip vuelve a ser transparente y la foto va tal cual —sobre cal,
     `multiply` y `normal` dan lo mismo, y la mezcla obliga a rasterizar—.
     Los selectores copian el peso de los del modo oscuro para ganarles por
     orden. */
  .miniatura,
  :root[data-theme="oscuro"] .miniatura,
  :root:not([data-theme="claro"]) .miniatura { background: transparent; border-color: transparent; }
  .miniatura:not(.miniatura--vacia) img,
  :root[data-theme="oscuro"] .miniatura img,
  :root:not([data-theme="claro"]) .miniatura img { mix-blend-mode: normal; }

  *, *::before, *::after { box-shadow: none !important; }
  .cabecera__interior, .barra-compromiso {
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
}

/* ───────────── La hoja del «···» de la lista (PANTALLAS §3 P1) ─────────────
   Filas de menú: ícono, nombre y una línea de ayuda que dice qué va a pasar.
   Sin primario: cada fila ES su acción, y la que no tiene vuelta —Eliminar—
   pide su confirmación de una línea aparte. */
.lista-hoja__item {
  display: flex; align-items: center; gap: var(--nm-esp-3);
  width: 100%; padding: 13px 0;
  border: 0; border-bottom: 1px solid var(--nm-borde);
  background: transparent; color: var(--nm-texto);
  font: inherit; text-align: left; cursor: pointer;
}
.lista-hoja__item:last-child { border-bottom: 0; }
.lista-hoja__item .nm-ico { color: var(--nm-texto-2); }
.lista-hoja__texto { display: grid; gap: 2px; min-width: 0; flex: 1; }
.lista-hoja__texto b { font-weight: 600; font-size: var(--nm-b2); }
@media (hover: hover) and (pointer: fine) {
  .lista-hoja__item:hover { color: var(--nm-texto); }
  .lista-hoja__item:hover .nm-ico { color: var(--nm-texto); }
}

/* ───────────── El estado vacío de la lista (PANTALLAS §3 P1) ─────────────
   La Bolsa mirando hacia la lupa (DESIGN.md «Mascota: la Bolsa», entró el
   2026-09-24 en lugar del hilo punteado), una instrucción y «Mira un
   ejemplo». Nada más: ni chips, ni ofertas. */
.lista-vacia {
  position: relative;
  justify-content: center;
  /* Lo que queda de pantalla bajo la cabecera y sobre el tab bar, dejando
     sitio para el pie: donde el pie SÍ se pinta —la forma web— el bloque no
     puede llenar la ventana entera o lo empuja fuera de cuadro. Donde el pie
     se esconde (las dos reglas de abajo) el bloque toma el alto completo y
     la página no desplaza. */
  min-height: calc(100dvh - var(--nm-cabecera) - var(--nm-barra-tabs) - 150px);
  padding: 32px 28px calc(var(--nm-barra-tabs) + 24px);
}
/* El alto COMPLETO solo donde el pie no se pinta, que son las mismas dos
   condiciones de la regla de abajo. Aplicado a todos los anchos empujaba el
   pie de la forma web fuera de cuadro y le regalaba 220 px de desplazamiento
   vacío a una pantalla que no tiene nada que desplazar (regresión de M5). */
/* Que quepa sin desplazar (2026-10-09): el bloque se centra entre la cabecera
   y el tab bar (el alto mínimo ya descuenta las dos), sin el relleno de abajo
   que contaba el tab bar dos veces, y la Bolsa y los huecos se achican con la
   ALTURA de la ventana (dvh: la barra de direcciones del navegador ya está
   descontada). A 740 o más queda la Bolsa de 140 px; a 600, 88. */
@media (max-width: 767px) {
  :root:not([data-forma="web"]) .lista-vacia {
    min-height: calc(100dvh - var(--nm-cabecera) - var(--nm-barra-tabs));
  }
  :root:not([data-forma="web"]) .lista-vacia { padding: var(--nm-esp-3) 20px var(--nm-esp-3); gap: clamp(6px, 1.6dvh, 14px); }
  :root:not([data-forma="web"]) .lista-vacia__bolsa { width: clamp(88px, 40dvh - 152px, 140px); height: clamp(88px, 40dvh - 152px, 140px); margin-bottom: 0; }
  :root:not([data-forma="web"]) .lista-vacia .nm-vacio__texto { max-width: 320px; }
  :root:not([data-forma="web"]) .lista-vacia__titulo { font-size: 20px; text-wrap: balance; }
  :root:not([data-forma="web"]) .lista-vacia__entradas > .nm-entrada { min-height: 88px; padding-block: clamp(8px, 1.8dvh, 16px); gap: clamp(4px, 1dvh, 8px); }
  :root:not([data-forma="web"]) .lista-vacia__entradas, :root:not([data-forma="web"]) .lista-vacia__entradas + .lista-vacia__demo { margin-top: clamp(4px, 1dvh, 12px); }
}
:root[data-forma="app"] .lista-vacia {
  min-height: calc(100dvh - var(--nm-cabecera) - var(--nm-barra-tabs));
}
:root[data-forma="app"] .lista-vacia { padding: var(--nm-esp-3) 20px var(--nm-esp-3); gap: clamp(6px, 1.6dvh, 14px); }
:root[data-forma="app"] .lista-vacia__bolsa { width: clamp(88px, 40dvh - 152px, 140px); height: clamp(88px, 40dvh - 152px, 140px); margin-bottom: 0; }
:root[data-forma="app"] .lista-vacia .nm-vacio__texto { max-width: 320px; }
:root[data-forma="app"] .lista-vacia__titulo { font-size: 20px; text-wrap: balance; }
:root[data-forma="app"] .lista-vacia__entradas > .nm-entrada { min-height: 88px; padding-block: clamp(8px, 1.8dvh, 16px); gap: clamp(4px, 1dvh, 8px); }
:root[data-forma="app"] .lista-vacia__entradas, :root[data-forma="app"] .lista-vacia__entradas + .lista-vacia__demo { margin-top: clamp(4px, 1dvh, 12px); }
.lista-vacia__titulo { text-wrap: balance; }
/* La Bolsa, encima de la frase (Agent Prompt Guide, «Estados vacíos»): un
   tamaño algo menor que el default de `.nm-bolsa` para no competir con el
   título en móvil. */
.lista-vacia__bolsa { width: 140px; height: 140px; margin-bottom: 4px; }
/* Las tres maneras de empezar (`.nm-entrada`, componentes.css): una grilla de
   tres columnas iguales —en móvil de 360 son 96 px cada una—, y sin la línea
   de apoyo donde no cabe. Con el escáner apagado queda solo «Busca
   productos»: ocupa el ancho de una tarjeta cómoda, centrada y en horizontal
   (ícono a la izquierda), nunca una de tres columnas con dos huecos. */
.lista-vacia__entradas {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  gap: var(--nm-esp-2); width: 100%; max-width: 480px; margin-top: var(--nm-esp-2);
}
html:not([data-escaner-listas="1"]) .lista-vacia__entradas > .nm-entrada {
  flex-direction: row; justify-content: center; gap: var(--nm-esp-3);
  width: 100%; max-width: 320px; justify-self: center; min-height: 64px; padding: var(--nm-esp-3) var(--nm-esp-4);
  text-align: left;
}
html:not([data-escaner-listas="1"]) .lista-vacia__entradas > .nm-entrada .nm-entrada__ico { width: 28px; height: 28px; }
html:not([data-escaner-listas="1"]) .lista-vacia__entradas > .nm-entrada .nm-entrada__apoyo { display: none; }
/* En móvil el texto de arriba cabe en 272 px y tres tarjetas no: la grilla
   sale 20 px a cada lado del texto (queda a 16 px del borde de la pantalla),
   y sin la línea de apoyo, que en 98 px no cabe. */
@media (max-width: 479px) {
  html[data-escaner-listas="1"] .lista-vacia__entradas { width: auto; align-self: stretch; margin-inline: -4px; }
  .lista-vacia__entradas > .nm-entrada .nm-entrada__apoyo { display: none; }
}
.lista-vacia__entradas + .lista-vacia__demo { margin-top: var(--nm-esp-3); }
/* La frase de arriba: con el escáner prendido invita a elegir; sin él (o
   mientras la app no sabe todavía si existe) queda la de siempre. Falla
   cerrado, igual que las entradas. */
.lista-vacia__texto-entradas { display: none; }
html[data-escaner-listas="1"] .lista-vacia__texto-entradas { display: inline; }
html[data-escaner-listas="1"] .lista-vacia__texto-base { display: none; }
.lista-vacia__demo { margin-top: var(--nm-esp-2); }

/* ───────────── La espera de una lista (`cargando.js`) ─────────────
   La Bolsa «cargando» (DESIGN.md «Mascota: la Bolsa») y una línea, donde van
   a aparecer las filas. Solo existe si la carga pasa de 300 ms; mientras está
   a la vista esconde el vacío y las filas viejas (`.lista--cargando`), para
   que sea la única figura de la pantalla. */
.lista-cargando {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--nm-esp-2); text-align: center;
  min-height: 46dvh; padding: var(--nm-esp-6) var(--nm-esp-4);
}
.lista-cargando[hidden] { display: none; }
.lista-cargando__bolsa { width: 160px; height: 160px; }
.lista-cargando__texto { margin: 0; color: var(--nm-texto-2); font-size: 1rem; }
.reparto--en-espera > .nm-vacio { display: none !important; }
.lista--cargando > .lista-vacia,
.lista--cargando #items-lista { display: none !important; }
/* El texto de esa misma bienvenida se reescribe por JS, no por CSS
   (hallazgo QA zona C #5): `test_la_bienvenida_es_una_instruccion_y_un_solo_boton`
   exige un solo `<p>` en el marcado, así que no hay dos redacciones que
   alternar acá — ver `lista.js` (`sincronizarTextoBienvenida`). */

/* El vacío gemelo de Carritos: en dos columnas no repite «Ir a mi lista»
   —lleva a la pantalla donde ya se está—, así que ahí cambia también el
   título y la frase, sin botón. Es `.nm-vacio` (flex en columna,
   componentes.css), por eso el «encendido» es `flex` y no `block`. */
.reparto-vacio--web { display: none; }
@media (min-width: 768px) and (hover: hover) and (pointer: fine) {
  :root:not([data-forma="app"]) .reparto-vacio--app { display: none; }
  :root:not([data-forma="app"]) .reparto-vacio--web { display: flex; }
}
:root[data-forma="web"] .reparto-vacio--app { display: none; }
:root[data-forma="web"] .reparto-vacio--web { display: flex; }

/* ───────────── Los avisos de origen (PANTALLAS §3 P1) ─────────────
   Una línea bajo el título con su acción, no una tarjeta. */
.lista-aviso {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--nm-esp-2) var(--nm-esp-3);
  margin: 0 0 var(--nm-esp-3);
  color: var(--nm-texto-2); font-size: var(--nm-b3); line-height: var(--nm-lh-ui);
}
.lista-aviso__texto { margin: 0; min-width: 0; }
.lista-aviso--viejo { color: var(--nm-texto-2); }
/* El error de validación del submit, bajo las filas y pegado al primario. Es
   el único lugar de la Lista donde entra `error` (PANTALLAS §8: validación de
   formulario), y por eso no lleva fondo ni ícono: una línea y nada más. */
.lista-aviso--error { color: var(--nm-error); margin: 0 0 var(--nm-esp-3); }
.semana-aviso__acciones { display: flex; flex-wrap: wrap; align-items: center; gap: var(--nm-esp-2); }

/* La lista le reserva abajo el sitio de las dos capas fijas: la barra de
   compromiso y el tab bar. El `scroll-padding-bottom` de `html` ya hace que
   el navegador no deje la última fila debajo de la barra al desplazarse al
   foco; esto es lo que hace que se pueda LLEGAR a ella con el dedo. */
.lista__form { padding-bottom: calc(var(--nm-barra) + var(--nm-barra-tabs) + var(--nm-esp-5)); }

/* El secundario que cierra la lista y el primario que la compromete NO se
   tocan. En forma web la barra es `display: contents` (`--nm-barra-display`),
   así que «Ver dónde pagar menos» cae en el flujo justo detrás de «+ Agregar
   producto» y quedaban pegados, uno debajo del otro, sin nada que dijera que
   son de dos pesos distintos (reportado el 2026-09-16). El aire es del
   secundario, para que valga también en forma app, donde el primario está en
   la barra fija y lo que se separa es el borde de la lista. */
.lista__acciones { margin-bottom: var(--nm-esp-6); }

/* Lo que se veía entre la barra de compromiso y el tab bar, y detrás del tab
   bar, era el contenido de la lista: las dos capas flotan y entre ellas
   quedaba una banda transparente (visto en `fase0-lista-390x844-claro.png`).
   La barra pinta el fondo hasta el borde de la ventana con un pseudoelemento
   —no crece, porque su alto lo leen `--nm-barra-max` y las pruebas—, y el
   tab bar se recorta contra ese fondo sólido. */
.barra-compromiso::after {
  content: ""; position: absolute; left: 0; right: 0; top: 100%;
  height: calc(var(--nm-barra-tabs) + env(safe-area-inset-bottom));
  background: var(--nm-fondo);
  pointer-events: none;
}

/* La banda de trabajo de la lista arranca pegada a la cabecera: los 64 px de
   aire de `.nm-banda` son de una página larga con secciones, y acá lo que
   sigue al título de la pestaña son los productos. */
.lista > .banda-trabajo { padding-top: var(--nm-esp-3); padding-bottom: 0; }
.lista > .banda-trabajo::before { border-top: 0; }

/* ───────────── La ficha del producto, a 5/6 (PANTALLAS §3 P2) ─────────────
   La misma hoja de siempre en su cara alta: ocupa cinco sextos de la pantalla,
   la ✕ arriba a la izquierda reemplaza al «Listo» —el primario vive abajo, en
   la barra fija— y el cuerpo se desplaza por dentro mientras esa barra se
   queda. El mismo molde al agregar y al editar. */
/* La ✕ se posiciona contra el PANEL y no contra el `<dialog>` (QA G3). Sin
   este `position: relative` el ancestro posicionado más cercano era `.hoja`,
   que ocupa el viewport entero: la ✕ quedaba en (12, 12) de la pantalla,
   flotando fuera de la hoja y encima del `h1` de la vista de atrás —en la
   Lista tapaba «Tu» de «Tu lista» y en Buscar la lupa del campo—. */
.hoja--alta .hoja__panel { position: relative; }
/* …salvo en la cara anclada de la web, donde el panel ya es `absolute` y eso
   también le sirve de ancla a la ✕. Sin esto el `relative` de arriba pisaba al
   `absolute` de `.hoja--anclada` (misma especificidad, regla posterior) y las
   coordenadas del ancla se leían como un CORRIMIENTO desde donde el panel ya
   estaba: en una pantalla de 1913 px la ficha de Buscar se abría casi entera
   fuera de la vista (Gonzalo, dev, 2026-09-25). */
.hoja--anclada.hoja--alta .hoja__panel { position: absolute; }
.hoja__cerrar {
  display: none;
  position: absolute; left: 12px; top: 12px; z-index: 2;
  width: 40px; height: 40px;
  align-items: center; justify-content: center;
  border: 0; border-radius: var(--nm-radio-full);
  background: var(--nm-superficie-2); color: var(--nm-texto);
  cursor: pointer;
}
.hoja--alta .hoja__cerrar { display: flex; }
.hoja--alta .hoja__panel {
  height: calc(100dvh / 6 * 5);
  display: flex; flex-direction: column;
  padding-top: var(--nm-esp-2);
}
/* El panel deja de desplazarse y el que se desplaza es el cuerpo: así la
   barra fija de abajo (`position: sticky`) se queda pegada al borde de la hoja
   mientras el contenido pasa por detrás, en vez de correrse con él. */
.hoja--alta .hoja__panel { overflow: hidden; padding-bottom: 0; }
/* El foco que la hoja pone en su panel al abrir es programático: el anillo de
   oro ahí se lee como un borde de la hoja, no como foco. Los controles de
   adentro conservan el suyo para el teclado. */
.hoja--alta .hoja__panel:focus-visible { outline: none; }
.hoja--alta .hoja__cuerpo {
  /* La ✕ (top 12 + 40) vive arriba de todo: el cuerpo arranca debajo de ella,
     así ni el contenido ni su desplazamiento pasan nunca por detrás. */
  margin-top: 36px;
  flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
  /* El cuerpo le RESERVA abajo el alto de la barra fija (QA G4). Un elemento
     `sticky` ocupa su sitio al final del flujo y se desplaza hacia arriba para
     quedarse a la vista, así que lo que tapa es el contenido que viene ANTES
     que él: en la ficha del aceite las chips de «No comprar en» quedaban
     debajo de [tacho · cantidad · +] + «Agregar a la lista» y no había cómo
     sacarlas de ahí. Con este sitio reservado, la última chip sube por encima
     de la barra al desplazarse, que es lo que PANTALLAS §P8 pide: nada queda
     detrás de una barra fija. El valor es el alto del pie —52 del control,
     más su aire de arriba y de abajo—, medido, no adivinado. */
  /* El alto exacto de la barra dejaba la última chip pegada al milímetro
     contra su borde —medido: chip hasta 755, barra desde 754—, o sea todavía
     tapada. Van además la hairline del borde superior y un respiro, para que
     lo último del cuerpo se lea COMPLETO y con aire, no al ras. */
  padding-bottom: calc(52px + var(--nm-esp-3) + var(--nm-esp-5)
    + 1px + var(--nm-esp-4) + env(safe-area-inset-bottom));
}
/* La ficha de la fila ya no adivina el alto de su barra (2026-09-30): el
   reserva de arriba era un número escrito a mano (105 px) y la barra real
   mide 125 con la unidad colgando bajo el stepper, así que «Ver ficha ·
   Cambiar producto · Mover» quedaba 20 px debajo de su borde. Ahora la barra
   es `sticky` al fondo del cuerpo, dentro del flujo: al llegar al final del
   desplazamiento ocupa su sitio DESPUÉS de lo último, sea cual sea su alto,
   y mientras tanto queda pegada al borde. El cuerpo no lleva relleno abajo
   (la barra trae el suyo, con el área segura). */
.hoja--alta .hoja__cuerpo:has(:is(.ficha-hoja__pie, .escaneo__pie)) { padding-bottom: 0; }
/* La ficha mide LO QUE MIDE su contenido y no se comprime al alto del cuerpo
   (QA G4). Tenía `flex: 1; min-height: 0` para empujar la barra hacia abajo
   con un `margin-top: auto`, y como efecto colateral la caja se encogía al
   alto visible: sus chips desbordaban hacia afuera y ese desborde no cuenta
   para el `scrollHeight` del cuerpo, así que las últimas tres tiendas no se
   podían alcanzar ni desplazándose. La barra ya no necesita que nadie la
   empuje: está fuera del flujo, anclada al panel. */
.hoja--alta .ficha-hoja { display: flex; flex-direction: column; flex: 1 0 auto; }

.ficha-hoja__cabecera { text-align: center; padding: var(--nm-esp-4) 0 var(--nm-esp-2); }
/* Es un chip `.miniatura` (lo arma `miniatura()`), así que el tamaño va acá y
   la foto la coloca la regla de `.miniatura` — con su sustituto cuando el
   archivo no está. */
.ficha-hoja__foto {
  width: 168px; height: 168px; margin: 0 auto var(--nm-esp-4);
  border-radius: var(--nm-radio-lg);
}
.ficha-hoja__foto.miniatura--vacia img { width: 72px; height: 72px; }
[data-theme="oscuro"] .ficha-hoja__foto { background: var(--nm-p-caliza); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="claro"]) .ficha-hoja__foto { background: var(--nm-p-caliza); }
}
/* El piso va en TINTA: en la ficha el oro es del nudo de la tienda ganadora,
   no del precio de portada (DESIGN.md, «el oro se gana, no se hereda»). */
.ficha-hoja__precio {
  margin: 0; font: 700 26px/1.2 var(--nm-cuerpo); letter-spacing: -.01em;
  color: var(--nm-texto);
}
.ficha-hoja__donde { margin: 2px 0 0; }
.ficha-hoja__nombre {
  margin: var(--nm-esp-3) 0 0;
  font: 700 21px/1.2 var(--nm-cuerpo); letter-spacing: -.01em; text-wrap: balance;
}
.ficha-hoja__meta { margin: 2px 0 0; }
.ficha-hoja__pista { margin: var(--nm-esp-2) 0 0; }

.hoja__grupo-cabecera {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--nm-esp-3); margin-bottom: var(--nm-esp-2);
}
.hoja__grupo-cabecera .hoja__rotulo { margin: 0; }
.ficha-hoja__enlace {
  padding: 0; border: 0; background: none;
  color: var(--nm-texto-2); font: inherit; font-size: var(--nm-b3); font-weight: 600;
  text-decoration: none; cursor: pointer; white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .ficha-hoja__enlace:hover { color: var(--nm-texto); }
}
.ficha-hoja__secundarias {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--nm-esp-5);
  margin-top: var(--nm-esp-5); padding: var(--nm-esp-1) 0 var(--nm-esp-4);
  border-top: 1px solid var(--nm-borde);
}
/* Cada acción es un blanco de 44 px de alto, con su propia caja: el enlace y
   los dos botones no se aprietan ni se montan entre sí. */
.ficha-hoja__secundarias > .ficha-hoja__enlace {
  display: inline-flex; align-items: center; min-height: 44px; flex: none;
}

/* La barra fija de la ficha: el control de cantidad y el primario, dentro de
   la hoja y siempre a la vista mientras el cuerpo se desplaza detrás.

   Sale del FLUJO y se ancla al panel, no al final del cuerpo que se desplaza
   (QA G4). Era `position: sticky` dentro del cuerpo, y un `sticky` tapa lo
   que viene antes que él: las chips de «No comprar en» quedaban debajo de la
   barra sin forma de sacarlas de ahí. Fuera del flujo, lo que reserva su
   sitio es el `padding-bottom` del cuerpo (arriba), así que la última chip
   sube por encima de la barra al desplazarse y nada queda detrás
   (PANTALLAS §P8). El ancla es `.hoja--alta .hoja__panel`, que ya es
   `position: relative` por la ✕. */
.ficha-hoja__pie {
  position: sticky; bottom: 0; z-index: 1;
  /* Sale al ras de la hoja: el cuerpo trae su relleno horizontal. Con
     `margin-top: auto` baja al fondo cuando la ficha es más corta que la hoja. */
  margin: auto calc(-1 * var(--nm-esp-5)) 0;
  /* Arriba y no al centro: el stepper y el primario miden lo mismo (52 px) y
     van en el mismo renglón; la unidad cuelga DEBAJO del stepper. Centrados,
     el primario se alineaba con el conjunto stepper + unidad y quedaba a
     media altura del stepper (Gonzalo, 2026-09-28). */
  display: flex; align-items: flex-start; gap: var(--nm-esp-3);
  padding: var(--nm-esp-3) var(--nm-esp-5)
           calc(var(--nm-esp-5) + env(safe-area-inset-bottom));
  background: var(--nm-superficie-1);
  border-top: 1px solid var(--nm-borde);
}
.ficha-hoja__primario { flex: 1; height: 52px; }
.paso-cantidad--grande { flex: none; height: 52px; background: var(--nm-superficie-2); }
.paso-cantidad--grande .paso-cantidad__boton { width: 46px; min-height: 50px; color: var(--nm-texto); }
.paso-cantidad--grande .item-fila__cantidad { font-size: 17px; width: 5ch; color: var(--nm-texto); }
.paso-cantidad--grande .nm-ico { width: 19px; height: 19px; }

/* ───────────── Buscar: la fila de resultado (PANTALLAS §3 P2) ─────────────
   Una fila por producto real, con la misma geometría que la fila de la lista:
   foto, nombre, «desde S/ · N tiendas · marca» y el «+» de BORDE a la derecha
   —el único relleno de acción de la pantalla es el primario de la barra, y
   Buscar no tiene barra—. La zona de la fila abre la ficha; el «+» agrega sin
   abrirla, y por eso está encima. */
/* La lista de resultados deja de ser una grilla de tarjetas con 24 px de aire:
   es una lista de filas con divisores, igual que la lista de compras. Un ítem
   de grilla tiene `min-width: auto`, así que sin el piso en 0 una fila con
   nombre largo empuja el «+» fuera de la pantalla. */
#resultados { display: block; gap: 0; }
#resultados > li { min-width: 0; }
.resultado-fila { position: relative; gap: var(--nm-esp-2); }
.resultado-fila__zona {
  position: absolute; inset: 0; z-index: 0;
  border: 0; background: transparent; cursor: pointer;
  border-radius: var(--nm-radio-md);
}
.resultado-fila__zona:focus-visible,
.resultado-fila__zona[aria-selected="true"] {
  outline: 2px solid var(--nm-foco); outline-offset: -2px;
}
.resultado-fila > *:not(.resultado-fila__zona) { position: relative; z-index: 1; }
.resultado-fila__cuerpo { flex: 1; min-width: 0; }
.resultado-fila .miniatura { flex: 0 0 auto; }
.resultado-fila__mas { flex: none; }
/* El wrapper del «+» de un resultado (encargo 2026-09-27): mismo lugar en el
   flujo de la fila que el botón que reemplaza, así que su CSS de flex no
   cambia entre el estado «+» y el stepper —`.resultado-stepper`, el mismo
   `.paso-cantidad.nm-stepper` de la fila de la Lista, con su unidad al lado
   («kg», «und», nunca la cifra repetida: esa ya la dice el campo). Encima
   de la zona con `z-index` (regla §4: el «+»/stepper nunca abren la ficha),
   como ya hacía el «+». */
/* El chip «und ⇄» va DEBAJO del «+», no al lado: al lado le comía ~60 px al
   texto de la card y el «desde S/ …» quedaba cortado (2026-09-28). */
.resultado-agregar { flex: none; display: flex; flex-direction: column; align-items: center; gap: 4px; }
/* La card genérica de Buscar es una grilla y no una fila de flex
   (PLAN_CANTIDADES.md, regla 6): el «+» vive a la derecha, junto al texto, y
   «Ver marcas» debajo del texto. Al agregar, el «+» se convierte en
   [− N +] [de 390 g ▾], que es más ancho que el texto que lo acompaña, así que
   pasa a SU renglón bajo el precio —foto arriba a la izquierda, el control a
   todo el ancho debajo— y «Ver marcas» baja después de él, como en el
   prototipo aprobado. `data-en-lista` lo prende `marcarEnLaLista`. */
.resultado-fila--generico {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "foto cuerpo mas" ". marcas .";
  align-items: center; column-gap: var(--nm-esp-2); row-gap: 0;
}
.resultado-fila--generico > .miniatura { grid-area: foto; }
.resultado-fila--generico > .resultado-fila__cuerpo { grid-area: cuerpo; }
.resultado-fila--generico > .resultado-agregar { grid-area: mas; }
.resultado-fila--generico > .resultado-fila__elegir-marca { grid-area: marcas; justify-self: start; }
.resultado-fila--generico[data-en-lista] {
  grid-template-areas: "foto cuerpo cuerpo" "ctl ctl ctl" "marcas marcas marcas";
  row-gap: var(--nm-esp-2);
}
.resultado-fila--generico[data-en-lista] > .resultado-agregar {
  grid-area: ctl; flex-direction: row; align-items: center; justify-content: flex-start;
  gap: var(--nm-esp-2);
}
.resultado-fila--generico[data-en-lista] > .resultado-fila__elegir-marca { margin-top: -4px; }
.resultado-stepper__unidad {
  color: var(--nm-texto-3); font-size: var(--nm-b3); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* El sello con tarjeta va debajo del precio de la fila, nunca debajo del
   precio por unidad (regla de Gonzalo). */
.resultado-fila .sello-beneficio, .item-fila--elegida .sello-beneficio { display: block; margin: 2px 0 0; }

/* El nombre y el metadato se leen en DOS renglones, no con puntos suspensivos
   (QA G8). La hoja de componentes recorta a uno, y a 390 px eso dejaba
   «Sixpack Leche Reconstituida …» y «2.34 k…»: el nombre no identificaba el
   producto y la medida —que es la mitad de por qué una fila se elige— se
   perdía. Dos renglones dejan la fila en ~96 px, dentro de los 60–104 de
   PANTALLAS §1. El sello con tarjeta sí conserva su renglón. */
.resultado-fila .nm-fila__nombre,
.resultado-fila .nm-fila__meta {
  white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
/* Con tres renglones —nombre en dos, meta y el sello con tarjeta— la fila se
   iba a 113 px y el techo de PANTALLAS §1.5 es 104. El aire de la hoja de
   componentes (14 px arriba y abajo) es el de una fila de dos renglones; acá
   se ajusta, y las filas cortas siguen en su piso de 72 por `min-height`. */
.resultado-fila.nm-fila { padding: 10px var(--nm-esp-5); }
.resultado-fila .nm-fila__nombre { line-height: 1.2; }

/* El precio de la card genérica, en su PROPIO renglón, sin el clamp de la
   meta (Gonzalo, 2026-09-28: buscando «piña» a 375 px, «Marca de la tienda
   · 1 kg / granel · 8 tiendas · desde S/ 4.49» se recortaba a dos líneas y
   el precio —lo que decide si se agrega— quedaba cortado en «desde S…»).
   Con tinta llena y algo de peso: es el dato más importante de la fila, no
   uno más de la meta terciaria. */
.resultado-fila__precio {
  margin: 1px 0 0; color: var(--nm-texto); font-weight: 600;
}

/* «Ya en tu lista»: lo que el «+» relleno decía solo con su color y el
   `aria-label` solo para el lector de pantalla. Terciario y chico: es un
   estado, no una alarma, y el «+» sigue agregando otro. */
.resultado-fila__puesto {
  margin: 2px 0 0;
  color: var(--nm-texto-2);
  font-weight: 600;
}
/* «Ya tienes <marca>»: mismo trato que «Ya en tu lista» de arriba — un
   estado en texto terciario, sin alarma. */
.resultado-fila__marca-en-lista {
  margin: 2px 0 0;
  color: var(--nm-texto-2);
  font-weight: 600;
}

/* «Elegir marca» (Gonzalo, 2026-09-28: el genérico compara todas las
   marcas y es el default correcto — el carril de marcas queda colapsado y
   este botón lo abre bajo pedido). Secundario de verdad: texto sin relleno
   ni borde, para no competir con el «+» circular que sí agrega — `nm-boton
   --terciario` es justamente ese trato en el sistema, y no lleva oro (R56,
   censo de oro: el dato es la card entera, no este botón). Alineado a la
   izquierda de la card, como la meta que abre. */
.resultado-fila__elegir-marca {
  margin: 4px 0 0 -8px;
}

/* El subtítulo que dice el orden y abre el popover: es texto con un ▾, no una
   pastilla — arriba de los resultados, una pastilla compite con el «+». */
.panel-resultados__barra {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: var(--nm-esp-2) var(--nm-esp-3);
  margin: var(--nm-esp-3) 0 var(--nm-esp-2);
}
.panel-resultados__contador { margin: 0; }
.busqueda-control {
  border: 0; background: none; padding: 0;
  color: var(--nm-texto-2); font-size: var(--nm-b3); font-weight: 600;
}
.busqueda-control::after { content: " ▾"; color: var(--nm-texto-3); }
/* Un filtro puesto se ve como chip, nunca escondido: ahí sí toma la forma
   del chip seleccionado del sistema —tonal con borde de tinta—, porque lo que
   marca es un recorte y no el orden. */
.busqueda-control--filtrado {
  padding: 5px var(--nm-esp-3);
  border: 1px solid var(--nm-texto); border-radius: var(--nm-radio-full);
  background: var(--nm-superficie-2); color: var(--nm-texto);
}
.busqueda-control__icono { display: none; }

/* Los chips de «Lo que más se compra», antes de tipear. */
.sugerencias__intro { margin: 0 0 var(--nm-esp-3); }

/* Al dedo (PANTALLAS §1 regla 5): la cabecera de categoría y el «+» de cada
   resultado llegan a 44 px; el «+» conserva sus 36 a la vista y gana el resto
   por área de toque, como la × del plato. */
@media (pointer: coarse) {
  .categoria { min-height: 44px; }
  .resultado-fila__mas { position: relative; }
  .resultado-fila__mas::before { content: ""; position: absolute; inset: -4px; }
}

/* El toast se apoya SOBRE las capas fijas de abajo —el tab bar y la barra de
   compromiso—, como hace la barra sobre el tab bar, y va por encima de las
   dos: nada queda detrás del pie ni de la barra ni del tab bar (PANTALLAS
   §8, lo mide test_capas_ui). En forma web los dos términos valen 0 px. */
.nm-toast {
  z-index: 41;
  bottom: max(28px, calc(var(--nm-barra-tabs) + var(--nm-barra) + 12px + env(safe-area-inset-bottom)));
}

/* ═══════════════════════════════════════════ P0 · Splash y onboarding ═══
   PANTALLAS §3 P0 y §9.3. Dos pantallas que se ven una sola vez en la vida
   de la app, así que todo lo de acá es layout propio: los componentes
   (`nm-boton`, `nm-chip`, `nm-stepper`, `nm-campo`, `nm-tarjeta`) salen de
   componentes.css sin una regla nueva.

   Movimiento: la animación de marca son 900 ms REPARTIDOS EN RETARDOS —cada
   tramo dura 200–250 ms, que es lo que la regla de «menos de 300 ms» pide
   (§9.1 #6)—, y con `prefers-reduced-motion` queda solo el fundido de 200 ms
   que el §9.3 especifica. */

/* ---------- Splash ---------- */

.splash {
  position: fixed; inset: 0; z-index: 60;
  display: flex; flex-direction: column; align-items: center;
  background: var(--nm-fondo); color: var(--nm-texto);
  padding: 0 var(--nm-esp-5) max(var(--nm-esp-6), env(safe-area-inset-bottom));
}
.splash[hidden] { display: none; }

/* El nombre a media altura y el isotipo debajo (Mealime `spl_7ggh5`): el
   bloque de marca ocupa el 85 % de arriba y se centra en él; la acción vive
   en el 15 % de abajo y no se mezcla. */
.splash__marca {
  flex: 0 0 85%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--nm-esp-5);
}
.splash__quipu { width: 112px; height: 112px; order: 2; overflow: visible; }
.splash__wordmark {
  order: 1; margin: 0;
  font: 700 var(--nm-d2) / 1.1 var(--nm-cuerpo);
  letter-spacing: var(--nm-d2-tracking);
}
/* El único oro del wordmark, como en el lockup de marca. */
.splash__menos { color: var(--nm-acento); }

.splash__madre, .splash__cuerda {
  fill: none; stroke: var(--nm-texto); stroke-width: 4.5; stroke-linecap: round;
}
.splash__nudo { fill: var(--nm-texto); }
/* El nudo focal va en la cuerda MÁS CORTA: en Nada Menos las cuerdas cuentan
   una reducción y la más corta es el precio más bajo (DESIGN.md §Iconography). */
.splash__nudo--maiz { fill: var(--nm-nudo); }

.splash__acciones {
  flex: 0 0 15%;
  display: flex; flex-direction: column; align-items: stretch; justify-content: center;
  gap: var(--nm-esp-2); width: 100%; max-width: 340px;
}
.splash__entrar { color: var(--nm-texto-2); }

/* La animación de marca (§9.3). Se dispara al mostrarse el splash, no al
   cargar la hoja: `data-anima="quipu"` lo pone `onboarding.js`.

   Orden y tiempos: la cuerda madre se dibuja (250 ms), las tres cuerdas caen
   escalonadas 80 ms con la curva de hoja, los nudos neutros aparecen desde
   scale(.6) y el nudo maíz se enciende último con un scale(1.15) de 200 ms;
   el wordmark se funde debajo. 900 ms de punta a punta. */
.splash[data-anima="quipu"] .splash__madre {
  stroke-dasharray: 46; stroke-dashoffset: 46;
  animation: splash-madre 250ms var(--nm-curva-salida) forwards;
}
.splash[data-anima="quipu"] .splash__cuerda {
  transform-origin: center top;
  transform: scaleY(0);
  animation: splash-cuerda 250ms var(--nm-curva-hoja) forwards;
}
.splash[data-anima="quipu"] .splash__cuerda--1 { animation-delay: 220ms; }
.splash[data-anima="quipu"] .splash__cuerda--2 { animation-delay: 300ms; }
.splash[data-anima="quipu"] .splash__cuerda--3 { animation-delay: 380ms; }
.splash[data-anima="quipu"] .splash__nudo {
  transform-box: fill-box; transform-origin: center;
  transform: scale(0.6); opacity: 0;
  animation: splash-nudo 200ms var(--nm-curva-salida) forwards;
}
.splash[data-anima="quipu"] .splash__nudo--1 { animation-delay: 480ms; }
.splash[data-anima="quipu"] .splash__nudo--2 { animation-delay: 560ms; }
.splash[data-anima="quipu"] .splash__nudo--maiz {
  animation-name: splash-nudo-maiz; animation-delay: 640ms;
}
.splash[data-anima="quipu"] .splash__wordmark,
.splash[data-anima="quipu"] .splash__acciones {
  opacity: 0; animation: splash-fundido 200ms var(--nm-curva-salida) forwards;
}
.splash[data-anima="quipu"] .splash__wordmark { animation-delay: 620ms; }
.splash[data-anima="quipu"] .splash__acciones { animation-delay: 700ms; }

@keyframes splash-madre { to { stroke-dashoffset: 0; } }
@keyframes splash-cuerda { to { transform: scaleY(1); } }
@keyframes splash-nudo { to { transform: scale(1); opacity: 1; } }
/* El único que se pasa de 1 y vuelve: es el nudo que gana, y el §9.3 le
   concede ese medio rebote. Bounce 0 en todo lo demás. */
@keyframes splash-nudo-maiz {
  0%   { transform: scale(0.6); opacity: 0; }
  60%  { transform: scale(1.15); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes splash-fundido { to { opacity: 1; } }

/* Reducción de movimiento: solo el fundido de 200 ms, todo junto. Reduce, no
   apaga (§9.1 #7) — y el atributo se pone igual, para no depender de que la
   media query y el JS coincidan. */
.splash[data-anima="fundido"] {
  opacity: 0; animation: splash-fundido 200ms var(--nm-curva-salida) forwards;
}
@media (prefers-reduced-motion: reduce) {
  .splash[data-anima="quipu"] .splash__madre,
  .splash[data-anima="quipu"] .splash__cuerda,
  .splash[data-anima="quipu"] .splash__nudo,
  .splash[data-anima="quipu"] .splash__wordmark,
  .splash[data-anima="quipu"] .splash__acciones {
    animation: splash-fundido 200ms var(--nm-curva-salida) forwards;
    animation-delay: 0ms;
    stroke-dashoffset: 0; transform: none; opacity: 0;
  }
  .onb-carril { transition: none; }
}

/* ---------- Onboarding ---------- */

.onboarding {
  width: 100%; max-width: none; height: 100dvh; max-height: 100dvh;
  margin: 0; padding: 0; border: 0;
  background: var(--nm-fondo); color: var(--nm-texto);
  display: flex; flex-direction: column; overflow: hidden;
}
.onboarding:not([open]) { display: none; }
.onboarding::backdrop { background: var(--nm-fondo); }

/* Contador, «Atrás» y «Saltar» siempre a la vista, en el cromo de 56. */
/* Grilla de tres y no `space-between`: «Atrás» y «Saltar» se esconden donde
   no llevan a nada (M2), y con flex el contador se corría de lado cada vez
   que uno de los dos desaparecía. Las dos columnas de los extremos miden lo
   mismo, así que el contador queda centrado en las cuatro láminas. */
.onboarding__barra {
  flex: 0 0 auto; height: var(--nm-cabecera);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: var(--nm-esp-2); padding: 0 var(--nm-esp-3);
}
.onboarding__atras { grid-column: 1; justify-self: start; }
.onboarding__conteo { grid-column: 2; justify-self: center; margin: 0; }
.onboarding__saltar { grid-column: 3; justify-self: end; }
/* Las cuatro láminas en fila; se corre el carril, no se esconden. (La quinta
   pantalla del §P0 es la app, no una lámina: ver `onboarding.js`.) */
.onb-carril {
  --onb-lamina: 0;
  flex: 1; display: flex; align-items: stretch;
  width: 100%; min-height: 0;
  transform: translateX(calc(var(--onb-lamina) * -100%));
  transition: transform var(--nm-mov-entrada) var(--nm-curva-hoja);
  touch-action: pan-y;
}
/* Mientras el dedo arrastra, el carril lo sigue 1:1 y sin transición. */
.onb-carril--arrastrando { transition: none; }

.onb-lamina {
  flex: 0 0 100%; min-width: 100%; min-height: 0;
  display: flex; flex-direction: column; gap: var(--nm-esp-4);
  padding: var(--nm-esp-5) var(--nm-esp-5) max(var(--nm-esp-5), env(safe-area-inset-bottom));
  overflow-y: auto;
}
/* La lámina que sale NO se esconde con `visibility`: se está deslizando, y
   apagarla la haría desaparecer de golpe en vez de salir por el borde. Lo que
   la saca del recorrido con Tab es el `tabindex="-1"` que pone el módulo, que
   es lo que hace falta y no cambia lo que se ve. El clip lo hace el
   `overflow: hidden` del diálogo. */
.onb-lamina__titulo {
  margin: 0;
  font: 700 var(--nm-d3) / var(--nm-lh-display) var(--nm-cuerpo);
  letter-spacing: var(--nm-d3-tracking);
  max-width: 16ch;
}
.onb-lamina__ayuda { margin: 0; color: var(--nm-texto-2); }
/* La Bolsa de la lámina 1 (DESIGN.md «Mascota: la Bolsa»): centrada, como
   figura sobre el titular. */
.onb-lamina__bolsa { align-self: center; width: 160px; height: 160px; }
/* El contenido, centrado en el aire que sobra; el primario, abajo.
   Dos `margin-top: auto` en la misma columna flex reparten el espacio libre
   en dos mitades iguales: una encima del título y otra encima del pie. Con
   uno solo —el del pie— el contenido se apilaba contra el borde de arriba y
   quedaban entre 700 y 900 px muertos en medio de cada lámina (QA del
   rediseño 2026, M3). */
.onb-lamina > :first-child { margin-top: auto; }
.onb-lamina__pie {
  margin-top: auto; padding-top: var(--nm-esp-4);
  display: flex; flex-direction: column; align-items: center; gap: var(--nm-esp-2);
}
.onb-lamina__pie .nm-boton--terciario { color: var(--nm-texto-2); }

/* 1 · Valor: las dos alturas y el trofeo. */
.onb-valor { padding: var(--nm-esp-4); }
.onb-valor__premisa { margin: 0 0 var(--nm-esp-3); color: var(--nm-texto-2); }
.onb-valor__linea {
  margin: 0; display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--nm-esp-3); padding: var(--nm-esp-2) 0;
}
.onb-valor__linea + .onb-valor__linea { border-top: 1px solid var(--nm-borde-sutil); }
.onb-valor__rotulo { font-size: var(--nm-b2); color: var(--nm-texto-2); }
/* «S/ 617» es una sola cosa: nunca se parte con el «S/» solo al final de la
   línea (hallazgo del 2026-09-24). Mismo criterio que `.nm-fila__precio`
   en `estilo.css`: `white-space: nowrap`, no `&nbsp;` en el marcado, que
   viajaría literal a las pruebas de Node. */
.onb-valor__antes,
.onb-valor__ahora,
.onb-valor__monto { white-space: nowrap; }
/* El trofeo es una FRASE y tiene que poder envolver: con el `nowrap` en la
   frase entera, «Te ahorras nada menos que S/ 62» se salía de la tarjeta
   (reporte de Gonzalo del 2026-09-24). Lo que no se parte es el monto. */
.onb-valor__antes { color: var(--nm-texto-3); font-size: var(--nm-b1); }
.onb-valor__ahora { font: 700 21px / 1.2 var(--nm-cuerpo); letter-spacing: -0.01em; }
/* Mientras el reparto de la compra de ejemplo llega. Las cuatro barras tienen
   el alto de lo que van a reemplazar, así que la tarjeta no cambia de tamaño
   cuando la cifra aterriza. */
.onb-valor__hueco { display: block; height: 21px; margin: var(--nm-esp-3) 0; }
.onb-valor__hueco--linea { width: 60%; height: 15px; }
.onb-valor__hueco--trofeo { height: 34px; margin-top: var(--nm-esp-5); }
/* El trofeo: el único oro de la lámina y el único momento serif de la vista
   (DESIGN.md §Typography: Bitter solo regular, una vez por vista). */
.onb-valor__trofeo {
  margin: var(--nm-esp-4) 0 0;
  font: 400 30px / 1.15 var(--nm-serif);
  color: var(--nm-acento);
}

/* 2 · Cómo: tres pasos con ícono. */
.onb-pasos { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--nm-esp-4); }
.onb-paso { display: flex; align-items: flex-start; gap: var(--nm-esp-3); }
.onb-paso__ico { flex: 0 0 auto; width: 26px; height: 26px; color: var(--nm-texto-2); }
.onb-paso__cuerpo { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.onb-paso__titulo { font: 700 var(--nm-b1) / 1.2 var(--nm-cuerpo); }
.onb-paso__linea { color: var(--nm-texto-2); }

/* 3 · Hogar: dos steppers grandes. */
.onb-hogar { display: grid; gap: var(--nm-esp-2); }
.onb-hogar__fila {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--nm-esp-3); padding: var(--nm-esp-3) 0;
}
.onb-hogar__fila + .onb-hogar__fila { border-top: 1px solid var(--nm-borde-sutil); }
.onb-hogar__cuerpo { display: flex; flex-direction: column; gap: 2px; }
.onb-hogar__nombre { font: 700 var(--nm-b1) / 1.2 var(--nm-cuerpo); }

/* 4 · Tarjetas: los chips del sistema, con el logo en su color real. */
.onb-tarjetas { gap: var(--nm-esp-2); }
.onb-tarjeta { height: 44px; }

/* 5 · Distrito de entrega (2026-09-28): el mismo `.nm-campo` pill del resto
   de la app y un desplegable plano debajo. Hasta el 2026-09-29 el
   desplegable era `position: absolute`, y eso lo sacaba del recorrido de
   `.onb-distrito__caja`: dentro de la hoja «Tu distrito» de Ajustes
   (`.nm-hoja`, que tiene su propio `overflow: auto` y un pie `sticky` con
   los botones Guardar/Cancelar) quedaba recortado por el borde de la hoja o
   tapado por ese pie, y lo mismo en la lámina del onboarding en viewports
   chicos (hallazgo de Gonzalo, 2026-09-29). Ahora vive EN EL FLUJO —empuja
   el contenido que sigue en vez de flotar encima— así que el scroll propio
   de la hoja o de la lámina es el único que hace falta para verlo entero, y
   su propio `max-height` + `overflow-y: auto` evita que una lista larga se
   coma la pantalla. `.onb-distrito__caja` (no un id): la misma lámina del
   onboarding y la hoja «Tu distrito» de Ajustes (`cuenta.js`) comparten
   este bloque. */
.onb-distrito__caja { position: relative; }
.onb-distrito__campo { text-overflow: ellipsis; }
.onb-distrito__elegido { color: var(--nm-texto-2); }
.onb-distrito__opciones {
  position: static; margin: 4px 0 0; z-index: auto;
  list-style: none; padding: var(--nm-esp-1);
  background: var(--nm-superficie-3);
  border: 1px solid var(--nm-borde-fuerte);
  border-radius: var(--nm-radio-lg);
  box-shadow: var(--nm-sombra-2);
  max-height: min(240px, 40vh); overflow-y: auto; overscroll-behavior: contain;
}
.onb-distrito__opcion {
  display: flex; flex-direction: column; gap: 2px; width: 100%;
  padding: var(--nm-esp-2) var(--nm-esp-3);
  background: none; border: 0; border-radius: 6px;
  color: inherit; font-size: var(--nm-b2);
  text-align: left; cursor: pointer;
  transition: background-color var(--nm-mov-micro) var(--nm-curva-salida);
}
@media (hover: hover) and (pointer: fine) {
  .onb-distrito__opcion:hover { background: var(--nm-superficie-2); }
}
.onb-distrito__opcion:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: -2px; }

/* En pantalla ancha las láminas no se estiran: la columna se centra y la
   lectura se queda donde la vista la busca. */
@media (min-width: 700px) {
  .onb-lamina { padding-left: 0; padding-right: 0; }
  .onb-lamina > * { width: min(100%, 460px); margin-left: auto; margin-right: auto; }
  .onb-lamina__pie { width: min(100%, 340px); }
  /* La Bolsa no es una columna de lectura: conserva su caja cuadrada. Sin
     esto la regla de arriba la estiraba a 460 × 160 y el <img> la recortaba
     a una franja (revisión del 2026-09-24). */
  .onb-lamina > .onb-lamina__bolsa { width: 160px; }
}

/* ══════════ La ficha pública, servida fuera del chasis de la app ══════════

   `/producto/<id>` es una PÁGINA (PANTALLAS §2.2: «las páginas servidas
   heredan el chasis web»), no la app con una capa encima: quien llega desde
   Google todavía no entró a nada. El atributo lo escribe `seo._inyectar` y lo
   retira `vistas.js` apenas la persona toca una pestaña, que es donde sí
   entra a la app.

   Queda el cromo web —el logo, que es el camino a la portada, y el campo que
   abre Buscar— y se van las cuatro piezas que el QA encontró encima del
   nombre del producto (QA del rediseño 2026, G24): el tab bar, el título «Tu
   lista» de la pestaña de abajo, el «···» de la lista y el avatar. En las dos
   formas: en la web el tab bar vive en la cabecera y en el teléfono flota
   sobre la barra de la ficha, y sobra en las dos.

   Va al final del archivo A PROPÓSITO: las reglas de forma de más arriba
   esconden el logo y el campo en el teléfono con la misma especificidad, y
   entre iguales gana la última. */
:root[data-pagina="ficha"] .tabs,
:root[data-pagina="ficha"] .cabecera__titulo,
:root[data-pagina="ficha"] .cabecera__acciones { display: none; }
:root[data-pagina="ficha"] .marca { display: flex; }
:root[data-pagina="ficha"] .cabecera__buscar { display: flex; flex: 1; min-width: 0; }
/* Sin tab bar flotante abajo, la ficha no tiene que reservarle su altura. */
:root[data-pagina="ficha"] .contenido { padding-bottom: var(--nm-esp-6); }


/* ═════════ Zona B del QA del rediseño (Carritos, Ofertas, Semana, Ajustes)

   Este bloque se volvió a escribir al final del archivo porque las reglas
   originales de Carritos y del push desaparecieron de `estilo.css` mientras
   esta sesión trabajaba (2 216 líneas menos que en HEAD, sin que las moviera
   nadie a `componentes.css`). Si alguien recupera esa sección, estas reglas
   son las de los hallazgos G13, G14, G16 y G17 y no deben duplicarse. */

/* G14 · la escalera con puntero fino: rejilla que envuelve en vez de carril.
   El asomo del peldaño siguiente quedaba en 56 px sobre una columna de 420 y
   no hay swipe con mouse. */
@media (hover: hover) and (pointer: fine) {
  :root:not([data-forma="app"]) .escalera__tira {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    overflow-x: visible;
  }
  :root:not([data-forma="app"]) .escalera__peldano { width: auto; min-width: 0; }
  /* …y el peldaño se deja achicar. Sin esto la tarjeta conservaba sus 228 px
     de ancho mínimo dentro de una celda de 204 y se salía por la derecha —los
     24 px que el informe leyó como «S/ 45.69 m…» (QA G14)—: lo que sobra no
     se corta, se reparte en otro renglón. */
  :root:not([data-forma="app"]) .escalera__peldano > .nm-escalon {
    min-width: 0; overflow-wrap: anywhere; box-sizing: border-box;
    /* `.nm-escalon` trae `flex: 0 0 230px` de `componentes.css` —la medida
       del carril en un teléfono—, y en la rejilla de escritorio esa base fija
       ganaba sobre la celda de 204: la tarjeta se salía 26 px por la derecha.
       Acá la base la dicta la celda. */
    flex: 1 1 0;
  }
}

/* G16 · la fila del push entra en los 60-104 px de PANTALLAS §1. El recorte a
   dos renglones va en el TEXTO y no en la fila, o el ⇄ cae a un renglón
   propio; la nota de granel se lee apretada; y con «No lo encontré» de 44 px
   (regla de toque) los 14 px de `.nm-fila` sobraban. */
/* R53 (2026-09-17) sacó el inset horizontal de la fila para que no se
   sumara al `padding` de `.carrito-push` y despegara las filas de la
   cabecera y de la barra — pero confundió dos insets que no son el mismo:
   el de `.carrito-push` separa la TARJETA (el `<ul class="carrito__items
   push__filas">` con borde de `.resultados--filas, .alternativas,
   .carrito__items` en la cabecera de este archivo) del borde de la capa, y
   ese es el que alinea cabecera/meta/barra con el borde de la tarjeta; el
   de la fila separa su CONTENIDO del borde de la tarjeta que la envuelve, y
   sin él la miniatura y el precio quedan pegados a ese borde de 1 px —el
   defecto que Gonzalo reportó el 2026-09-23 en el ambiente de pruebas a 1568 px—
   porque `.carrito-push` no tiene ni idea de que uno de sus hijos es una
   tarjeta con borde propio. Los dos insets conviven sin despegar nada: uno
   es el margen ALREDEDOR de la tarjeta, el otro es el padding DENTRO de
   ella, y `.resultado-fila` y `.alternativa` —las otras dos filas que usan
   esa misma tarjeta con borde— ya traían el suyo (`var(--nm-esp-5)`) desde
   antes de R53. */
.carrito__item { align-items: flex-start; padding: 10px var(--nm-esp-4); }
.carrito__item .carrito__item-nombre {
  display: flex; align-items: flex-start; gap: 6px; white-space: normal;
}
.carrito__item .carrito__item-texto {
  min-width: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.carrito__item .carrito__cambiar { flex: 0 0 auto; }
.carrito__item .carrito__item-reemplazo {
  margin-top: 0; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.carrito__item .carrito__no-esta { margin-top: 0; }

/* …y las líneas secundarias del cuerpo se apilan sin aire de más: la de «de
   tu lista» es de un renglón por definición —dice el nombre de la card y la
   cantidad, no una frase—, y el aire entre líneas del mismo párrafo era lo
   que empujaba la fila por encima de los 104 px (QA G16). */
.carrito__item .carrito__item-cuerpo > * + * { margin-top: 2px; }
.carrito__item .carrito__item-cuerpo > .nm-fila__meta {
  -webkit-line-clamp: 1; line-height: 1.3;
}

/* G16 (segunda vuelta) → R29 · el icono solo, sin texto, no se entendía al
   tocarlo (reporte de Gonzalo del 2026-09-17: nada dice que ese botón saca
   el producto del carrito). El texto vuelve en las dos formas; lo que se
   ajusta es el tamaño de fuente y el relleno horizontal para que la pill
   quepa bajo el precio sin volver a los ~130 px que partían el nombre del
   producto en dos renglones (QA G16). Escrito dos veces, como todo lo de
   forma. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .carrito__item .carrito__no-esta {
    min-width: 0; padding: 0 10px; margin-left: auto; font-size: 12px;
  }
}
:root[data-forma="app"] .carrito__item .carrito__no-esta {
  min-width: 0; padding: 0 10px; margin-left: auto; font-size: 12px;
}

/* G17 · la barra del push: la meta se apila bajo la cifra y el primario no se
   parte ni se pisa con ella. */
.push__barra .barra-compromiso__resumen {
  min-width: 0; display: flex; flex-direction: column; margin: 0;
}
.push__barra .barra-compromiso__resumen .nm-meta { line-height: 1.3; }
.push__barra .carrito__enlace { flex: 0 0 auto; white-space: nowrap; }
/* …y cuando la meta parte en dos renglones, parte ENTRE sus dos partes: con
   el corte donde cayera, la barra dejaba «+ envío S/» arriba y la cifra sola
   abajo, separando el «S/» de su número (QA G17, nit de la verificación). */
.push__barra-parte { white-space: nowrap; }

/* G23 (misma raíz) · ni el globo del tour ni la capa del push encienden el
   anillo de foco dorado al abrirse: son contenedores enfocados por programa,
   nadie llegó con el teclado, y el oro de la pantalla ya lo tienen el trofeo
   y el nudo. */
.carrito-push:focus, .carrito-push:focus-visible { outline: none; }
.tour__globo:focus, .tour__globo:focus-visible { outline: none; }

/* G13 · la cobertura de la tarjeta de tienda parte en vez de cortarse, como
   texto y no como caja flex (la regla base vive en `componentes.css`). */
.nm-tienda__cobertura {
  display: block; white-space: normal; overflow: visible; text-overflow: clip;
}
.nm-tienda__cobertura .nm-ico {
  display: inline-block; vertical-align: -2px; margin-right: 4px; flex: none;
}
.nm-tienda > .nm-fila__cuerpo { flex: 1 1 auto; min-width: 0; }

/* G13 (segunda vuelta) · las tres tarjetas medían 69, 88 y 69 px: la de
   Tottus sumaba «· asistido» y la cobertura se le iba a un tercer renglón,
   con la tira perdiendo el ritmo. En una fila flex la cobertura solo puede
   usar la columna de texto (182 px en un teléfono de 390) mientras el total
   se lleva 90 px que en ese renglón están vacíos. Acá la tarjeta pasa a
   REJILLA y la cobertura corre por debajo del total y del chevrón: 296 px
   para la misma línea, que entra entera sin recortar nada de lo que la
   persona necesita para decidir —cuántos productos, cuánto de envío y si
   esa tienda es paso a paso (la disyuntiva conveniencia/ahorro la elige
   ella)—. Va acotado a `#carritos`: la misma clase la pinta la portada
   desde `canasta_precios/seo.py` y ahí la fila es de una sola línea. */
#carritos .nm-tienda {
  display: grid; align-items: center;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
}
#carritos .nm-tienda > .nm-tienda__logo { grid-row: 1 / span 2; align-self: start; }
#carritos .nm-tienda > .nm-fila__cuerpo { display: contents; }
#carritos .nm-tienda .nm-tienda__nombre { grid-column: 2; grid-row: 1; }
#carritos .nm-tienda .nm-tienda__total { grid-column: 3; grid-row: 1; }
#carritos .nm-tienda > .nm-tienda__chevron { grid-column: 4; grid-row: 1; }
#carritos .nm-tienda .nm-tienda__cobertura {
  grid-column: 2 / -1; grid-row: 2; margin-top: 2px;
}

/* G21 · el renglón de estado de Mi semana no reserva 18 px cuando está
   vacío, y sigue en el flujo para que la región viva anuncie. */
.semana__estado:empty { height: 0; overflow: hidden; }


/* ═════════ Zona B1 del fix2 (Carritos y Ofertas), 2026-09-16 ═════════ */

/* R2 · en forma web «Armar en X» quedaba al final del push y había que bajar
   2 594 px de una ventana de 900 para encontrarlo: la barra es el final de su
   columna y el push de Plaza Vea son 26 filas. `PANTALLAS` §P4 lo pide como la
   acción principal de la tarjeta, así que en la forma web la barra sube al
   TERCER lugar —debajo de la cabecera y de la meta, arriba de las filas— y se
   queda pegada al desplazar, que es lo que en el teléfono ya hace la capa
   fija de abajo. El orden del marcado no cambia: lo reordena el CSS, y en
   forma app la barra sigue siendo la capa fija del pie. */
@media (min-width: 768px) and (hover: hover) and (pointer: fine) {
  :root:not([data-forma="app"]) .carrito-push { display: flex; flex-direction: column; }
  :root:not([data-forma="app"]) .carrito-push > .push__cabecera { order: 0; }
  :root:not([data-forma="app"]) .carrito-push > .push__meta { order: 1; }
  :root:not([data-forma="app"]) .carrito-push > .push__barra {
    order: 2; margin: 0 0 var(--nm-esp-4);
    position: sticky; top: var(--nm-cabecera); z-index: 20;
  }
  :root:not([data-forma="app"]) .carrito-push > .push__filas { order: 3; }
  :root:not([data-forma="app"]) .carrito-push > .carrito__llevar { order: 4; }
}
:root[data-forma="web"] .carrito-push { display: flex; flex-direction: column; }
:root[data-forma="web"] .carrito-push > .push__cabecera { order: 0; }
:root[data-forma="web"] .carrito-push > .push__meta { order: 1; }
:root[data-forma="web"] .carrito-push > .push__barra {
  order: 2; margin: 0 0 var(--nm-esp-4);
  position: sticky; top: var(--nm-cabecera); z-index: 20;
}
:root[data-forma="web"] .carrito-push > .push__filas { order: 3; }
:root[data-forma="web"] .carrito-push > .carrito__llevar { order: 4; }

/* G-R4 · el aviso de los precios de hoy vive en `#nota-reparto`, al pie de
   la pestaña (Gonzalo, 2026-09-25: bajó de junto al trofeo para no competir
   con el plan por la primera pantalla). `.reparto__nota` ya pone el margen
   y `.nm-meta` ya lo deja en terciario — no hace falta una clase propia. */

/* G-R5 · la reserva de las capas fijas va al FINAL DE LA PÁGINA. Hasta R28
   el pie vivía visible fuera de `.carritos__cuerpo` y era él quien reservaba
   los 156 px de las capas fijas (medido en 390×844: pie 635-844, capas fijas
   desde 688) — y de paso la escalera quedaba cortada con 170 px de vacío más
   abajo. Con el pie escondido siempre en las dos formas angostas (R28), ya
   no hay quien reserve ese sitio: lo reserva `.carritos__cuerpo`, que ahora
   sí es el final del flujo. Escrito dos veces, como todo lo de forma. */
@media (max-width: 767px), (pointer: coarse) and (hover: none) {
  :root:not([data-forma="web"]) .carritos__cuerpo {
    padding-bottom: calc(var(--nm-esp-5) + var(--nm-barra-tabs, 0px) + 96px + env(safe-area-inset-bottom));
  }
}
:root[data-forma="app"] .carritos__cuerpo {
  padding-bottom: calc(var(--nm-esp-5) + var(--nm-barra-tabs, 0px) + 96px + env(safe-area-inset-bottom));
}

/* G-R7 · el aviso de empate, en la fila del carrito donde el reparto lo
   detecta. Un renglón terciario, con el mismo aire apretado que las demás
   líneas del cuerpo (QA G16: la fila sigue entre 60 y 104 px). */
.carrito__item .carrito__item-empate {
  margin-top: 2px; color: var(--nm-texto-3);
  font-size: var(--nm-b3); line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* G-R9 · la fila de Ofertas pasa a REJILLA y la meta corre por debajo, la
   misma solución que la tarjeta de tienda de Carritos (G13). Con la meta al
   lado, el nombre se quedaba con ~120 px de los 390 del teléfono y se cortaba
   en «Agua Micelar Beauty Care…»: sin los «400 ml» del final no se sabe qué
   bajó de precio. Ahora el nombre tiene la fila entera menos la foto, el
   precio y el «+», y se lee en dos renglones con su presentación adentro. */
.bajada {
  display: grid; align-items: center;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  column-gap: var(--nm-esp-3);
  /* Tres renglones en vez de dos piden el aire de una fila de datos y no el
     de un párrafo: con los 14 px de `.nm-fila` la fila medía 105 y PANTALLAS
     §1 regla 5 la quiere entre 60 y 104. */
  padding: 10px 0;
}
.bajada > .bajada__puerta { grid-column: 1; grid-row: 1 / span 3; align-self: start; }
.bajada > .nm-fila__cuerpo { display: contents; }
/* El nombre se lleva el renglón ENTERO —de la foto al «+»—, y el precio baja
   a compartir el segundo con la tienda y el precio de antes. Con el precio
   arriba a la derecha el nombre se quedaba con 136 px de los 390 del
   teléfono y dos renglones no alcanzaban ni para la marca: «Agua Micelar
   Beauty Care…». Abajo, la cifra de hoy y la bajada en oro van en una sola
   línea a la derecha, que es como se leen de todos modos. */
.bajada .bajada__nombre { grid-column: 2 / 4; grid-row: 1; }
.bajada .bajada__nombre a {
  white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.bajada .bajada__nombre a { line-height: 1.25; }
.bajada > .nm-fila__derecha {
  grid-column: 2 / 4; grid-row: 2; margin-top: 0;
  display: flex; align-items: baseline; justify-content: flex-start;
  gap: var(--nm-esp-2); white-space: nowrap; line-height: 1.3;
}
.bajada > .bajada__mas { grid-column: 4; grid-row: 1 / span 3; }
.bajada .nm-fila__meta {
  grid-column: 2 / 4; grid-row: 3; margin-top: 0;
  white-space: normal; line-height: 1.3;
}
/* «Ya en tu lista» es el mismo rótulo de Buscar y se ve igual acá: el mismo
   aviso no cambia de tono porque cambie la pantalla. */
.bajada__puesto { color: var(--nm-texto-2); font-weight: 600; }


/* ---------- Escanear una lista (ESPEC_LISTAS.md §13, web/js/escaneo.3c3e1697.js) ----------

   La hoja tiene tres estados —elegir, cargando, revisión— y uno de error con
   varias redacciones; todos comparten `.escaneo` como caja de aire vertical.
   Las filas de revisión reusan `.nm-fila`/`.nm-stepper`/`.nm-chip` de
   componentes.css: lo propio de acá es el texto leído (terciario, entre
   comillas), el aviso de lo dudoso y el acomodo de las alternativas. */

/* El título VISIBLE de la hoja: con `alta: true` la ✕ absoluta (12px, 12px,
   40×40) tapaba el título de la cabecera compartida si esta hoja pasaba
   `titulo` además de `alta` (QA 2026-09-25) — la corrección fue dejar de
   pasar `titulo` (así la cabecera compartida con «Listo» queda oculta, como
   en la ficha de producto y el detalle de receta) y pintar este `<h2>`
   propio, centrado y con aire arriba de sobra para no quedar bajo la ✕. */
.escaneo__titulo {
  margin: 0 0 var(--nm-esp-2); padding-top: var(--nm-esp-6);
  text-align: center;
  font: 700 21px/1.2 var(--nm-cuerpo); letter-spacing: -.01em;
}
.escaneo { display: flex; flex-direction: column; gap: var(--nm-esp-4); }
.escaneo__intro { color: var(--nm-texto-2); }
.escaneo__pegar { display: flex; flex-direction: column; gap: var(--nm-esp-3); }
.escaneo__textarea {
  width: 100%; min-height: 128px; padding: var(--nm-esp-3) var(--nm-esp-4);
  border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-lg);
  background: var(--nm-campo); color: var(--nm-texto);
  font: 400 15px / 1.5 var(--nm-cuerpo); resize: vertical;
}
.escaneo__textarea:focus-visible { outline: 2px solid var(--nm-foco); outline-offset: 2px; }

.escaneo--cargando { align-items: center; text-align: center; padding: var(--nm-esp-6) 0; }
/* La Bolsa leyendo (pose `leyendo`), del mismo tamaño que la de la lista
   vacía: acompaña la espera larga, no compite con el texto. */
.escaneo__bolsa { width: 140px; height: 140px; margin-bottom: 4px; }
.escaneo__cargando-texto { font-weight: 600; }
.escaneo__hueso { width: 100%; height: 56px; border-radius: var(--nm-radio-lg); }

/* La barra fija «Agregar N productos» (`.escaneo__pie`, `position:
   sticky`) tapaba el último «Elegir otro» y el renglón de abajo al
   desplazar hasta el fondo (QA con Playwright, 2026-09-25) — el cuerpo
   desplazable necesita tanto padding inferior como la barra mide, para que
   nunca quede contenido detrás de ella. ~84px: el botón (52px, `.nm-
   boton`) más el padding del pie a los dos lados (`--nm-esp-4` × 2). */
.escaneo-lista { display: flex; flex-direction: column; padding-bottom: var(--nm-esp-4); }
.escaneo-fila { align-items: flex-start; flex-wrap: wrap; }
/* El stepper baja a su propia línea, angosta, en vez de competir por ancho
   con el nombre y el aviso en la misma fila (QA con Playwright,
   2026-09-25: con los dos en la misma línea, en móvil el nombre de la
   card y el aviso de duda se cortaban a media palabra — "Papa canchan…",
   "No estamos seguros de este …"). `order` mantiene el ícono de quitar en
   la primera línea, junto al cuerpo, y el margen izquierdo del stepper
   alinea con el texto de arriba (44px de la miniatura + 12px de `gap`). */
.escaneo-fila > .miniatura { order: 0; }
.escaneo-fila > .nm-fila__cuerpo { order: 1; flex: 1 1 0; min-width: 0; }
.escaneo-fila > .nm-fila__icono { order: 2; }
/* El salto de línea y el ancho natural del stepper son dos trabajos
   distintos, y por eso son dos elementos: `.escaneo-fila__cantidad` (el
   `<div>` que envuelve al `.nm-stepper` en el HTML) es el que fuerza la
   línea completa (`flex-basis: 100%` — un ancho fijo por sí solo NO
   fuerza el salto en flexbox, solo evita que el ítem se achique; hace
   falta el 100% para que no quepa nada más a su lado); el `.nm-stepper`
   de adentro se queda con su ancho de contenido (`display: inline-flex`
   ya lo hacía) en vez de estirarse a ocupar esa línea entera — poner
   `flex: 1 1 100%` directo en el propio stepper (primer intento, QA
   2026-09-25) SÍ partía la línea pero además estiraba la píldora entera,
   dejando el «− 1 kg +» apachurrado a la izquierda de una cápsula vacía. */
.escaneo-fila > .escaneo-fila__cantidad {
  order: 3; flex: 1 1 100%; display: flex; margin: var(--nm-esp-2) 0 0 56px;
}
/* Nunca elipsis en el nombre ni en el aviso de duda de la revisión: son
   texto corto (un nombre de producto, una frase de aviso) y el espacio ya
   no es el problema una vez que el stepper no lo compite — pero si aun así
   pasara de dos líneas, que se lea completo y no se corte a media palabra. */
.escaneo-fila .nm-fila__nombre,
.escaneo-fila .escaneo-fila__aviso {
  -webkit-line-clamp: unset; display: block; overflow: visible;
}
.escaneo-fila__original {
  font-style: italic; margin: 0 0 2px;
}
.escaneo-fila--dudosa .escaneo-fila__aviso { color: var(--nm-texto); font-weight: 600; }
.escaneo-fila__aviso { margin: 2px 0 0; }
.escaneo-fila__acciones { margin-top: 4px; }
.escaneo__vacio-revision { color: var(--nm-texto-3); padding: var(--nm-esp-5) 0; text-align: center; }
/* «Agregar otro producto» (ESPEC_LISTAS §14): lo que faltó en la foto, debajo
   del último renglón y a su ancho, no a todo el ancho de la hoja. */
.escaneo__agregar-otro { align-self: flex-start; margin-top: calc(var(--nm-esp-4) * -1); }
/* Sticky al fondo del cuerpo, que no lleva relleno abajo (ver `:has` en
   `.hoja--alta .hoja__cuerpo`): el relleno de reserva de la ficha corría el
   borde de pegado 105 px hacia arriba y la barra quedaba flotando a media
   hoja con contenido debajo. El margen horizontal cancela el relleno del
   cuerpo (`--nm-esp-5`) para que la barra llegue de borde a borde. */
.hoja--alta .hoja__cuerpo > .escaneo--revision { flex: 1 0 auto; }
.escaneo__pie {
  position: sticky; bottom: 0; z-index: 1; background: var(--nm-superficie-1);
  margin: auto calc(var(--nm-esp-5) * -1) 0;
  padding: var(--nm-esp-4) var(--nm-esp-5) calc(var(--nm-esp-4) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--nm-borde-sutil);
}
.escaneo__aviso-error {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--nm-esp-2);
  background: var(--nm-superficie-2); border: 1px solid var(--nm-error); border-radius: var(--nm-radio-md);
  padding: var(--nm-esp-3); margin-bottom: var(--nm-esp-3);
}
.escaneo__aviso-error p { margin: 0; color: var(--nm-error); flex: 1 1 auto; min-width: 140px; }

.escaneo--error.nm-vacio { padding-top: var(--nm-esp-6); }
.escaneo--vacio.nm-vacio { padding-top: var(--nm-esp-6); }
.escaneo__vacio-acciones { display: flex; flex-direction: column; gap: var(--nm-esp-2); width: 100%; margin-top: var(--nm-esp-3); }

/* La bandeja: hasta 4 fotos, cada una con su propio estado — «esperando» y
   «leyendo» laten despacio (nunca giran, §Componentes «esqueletos») y
   «lista»/«error»/«no es lista»/«no se lee bien» llevan ícono Y texto corto,
   nunca solo color (una persona que no distingue el rojo del gris tiene que
   poder leer «No se pudo leer» igual). */
.escaneo-bandeja { display: flex; flex-wrap: wrap; gap: var(--nm-esp-3); }
.escaneo-bandeja__item { width: 84px; display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; }
.escaneo-bandeja__miniatura {
  position: relative; width: 72px; height: 72px; border-radius: var(--nm-radio-md);
  overflow: hidden; background: var(--nm-superficie-2); border: 1px solid var(--nm-borde);
}
.escaneo-bandeja__miniatura img { width: 100%; height: 100%; object-fit: cover; display: block; }
.escaneo-bandeja__estado {
  position: absolute; left: 4px; bottom: 4px; display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: var(--nm-radio-full);
  background: var(--nm-superficie-1); border: 1px solid var(--nm-borde); color: var(--nm-texto-2);
}
.escaneo-bandeja__estado .nm-ico { width: 13px; height: 13px; }
.escaneo-bandeja__estado--lista { color: var(--nm-texto); }
.escaneo-bandeja__estado--error, .escaneo-bandeja__estado--invalida { color: var(--nm-error); border-color: var(--nm-error); }
.escaneo-bandeja__estado--espera, .escaneo-bandeja__estado--leyendo { animation: nm-pulso 1.5s ease-in-out infinite; }
.escaneo-bandeja__quitar {
  position: absolute; top: 2px; right: 2px; width: 22px; height: 22px; padding: 0; border: none;
  display: flex; align-items: center; justify-content: center; border-radius: var(--nm-radio-full);
  background: var(--nm-velo); color: var(--nm-p-hueso);
}
.escaneo-bandeja__quitar .nm-ico { width: 12px; height: 12px; stroke-width: 2.4; }
.escaneo-bandeja__texto { max-width: 84px; }
.escaneo-bandeja__orden { display: flex; gap: 2px; }
.escaneo-bandeja__orden button {
  width: 24px; height: 24px; padding: 0; border: 1px solid var(--nm-borde); border-radius: var(--nm-radio-sm);
  background: var(--nm-superficie-1); color: var(--nm-texto-2); font-size: 12px; line-height: 1;
}
.escaneo-bandeja__orden button:disabled { opacity: 0.35; }
.escaneo-bandeja__ayuda { width: 100%; margin-top: calc(var(--nm-esp-2) * -1); }

/* El escáner de listas (foto o texto pegado) está apagado en producción por
   ahora (Gonzalo, 2026-10-01). El servidor ya quita su HTML cuando
   `NM_ESCANER_LISTAS=0`; esto cubre la app empaquetada, cuyo `index.html`
   no pasa por el servidor: hasta que `/api/sesion` diga "1" no existe
   ninguna entrada. Falla cerrado: sin el atributo, escondido. */
html:not([data-escaner-listas="1"]) #escanear-lista,
html:not([data-escaner-listas="1"]) #pegar-lista,
html:not([data-escaner-listas="1"]) #escanear-lista-vacia,
html:not([data-escaner-listas="1"]) #pegar-lista-vacia { display: none; }
