/* mes-espejo — hoja única.
 *
 * Sale del diseño aprobado en docs/mes-espejo-ui/*.dc.html. Los mockups son dos
 * artboards de tamaño fijo (390×844 y 820×1180); acá son UNA hoja responsive:
 * el layout de tablet es el mismo sistema de componentes con la lista y el
 * detalle lado a lado, así que es un breakpoint, no un segundo diseño.
 *
 * Reglas de la pasada de limpieza que hay que respetar (costaron una ronda):
 *   · Sin glass decorativo: header y nav sólidos con hairline, no backdrop-filter.
 *   · Una sola codificación de cantidad por fila: el número literal + la barra
 *     fina. El % NO va en las filas.
 *   · Sin chips redundantes (no hay chip «EN PAUSA»: el círculo naranja más el
 *     motivo en naranja ya lo dicen).
 *   · Nada de barra lateral de color en las tarjetas.
 *   · Peso 600 SÓLO en los números grandes, con tabular-nums y letter-spacing.
 *
 * Las tres escalas de este archivo, y por qué son tres:
 *   · ESPACIADO --s1..--s5 (4/8/16/24/32). Regla de uso: --s4 separa secciones,
 *     --s3 bloques dentro de una sección y es el padding interno de toda
 *     tarjeta, --s2 items emparentados, --s1 renglones de un mismo bloque de
 *     texto. Los pocos valores fuera de escala que quedan están comentados uno
 *     por uno: son medidas ópticas o medidas en el navegador, no descuidos.
 *   · TIPOGRAFÍA --t-* (7 pasos) y números --n1..--n4. Había 18 tamaños con
 *     saltos de 1 px: eso no es una escala, es un continuo, y hacía que dos
 *     pantallas con la misma jerarquía no se leyeran igual.
 *   · TÁCTIL, que NO se deriva de las otras dos y viene del contexto físico:
 *     círculo 48 · fila ≥72 · botón primario 56 · nav 54 · control 48 · mínimo 44.
 */

/* ── Fuentes propias: sin pedirle nada a Google en tiempo de carga ────────── */
@font-face { font-family: Roboto; font-weight: 400; font-style: normal; font-display: swap;
             src: url("/static/vendor/fonts/roboto-400.woff2") format("woff2"); }
@font-face { font-family: Roboto; font-weight: 500; font-style: normal; font-display: swap;
             src: url("/static/vendor/fonts/roboto-500.woff2") format("woff2"); }
@font-face { font-family: Roboto; font-weight: 600; font-style: normal; font-display: swap;
             src: url("/static/vendor/fonts/roboto-600.woff2") format("woff2"); }
@font-face { font-family: Roboto; font-weight: 700; font-style: normal; font-display: swap;
             src: url("/static/vendor/fonts/roboto-700.woff2") format("woff2"); }

:root {
  --bg: #141417;            /* liso: sin punteado, sin radial ámbar, sin halo */
  --card: #0a0a0c;
  --hair: rgba(255,255,255,0.06);
  --hair-2: rgba(255,255,255,0.10);
  /* El borde que IDENTIFICA un control, separado del que separa superficies.
     Medido: --hair-2 compuesto sobre --bg da #2C2C2E = 1,32:1, o sea el 44 %
     del 3:1 que pide WCAG 1.4.11 para el límite de un componente. Con 0.38:
     sobre --bg da #6D6D6F = 3,56:1; sobre --card (inputs, chips) el borde
     compone en #676768 y contra la página da 3,26:1. El mínimo que pasa es
     0.33; el margen es para el vidrio con huellas y la luz de tubo.
     --hair-2 se queda como está: lo usan el riel del .sw y el borde del .toast,
     donde el borde no es lo que identifica al control. */
  --borde-ctrl: rgba(255,255,255,0.38);
  --txt: #ededed;
  --muted: #a1a1a1;
  --faint: #828282;   /* 4,78:1 sobre --bg. Con #7d7d7d daba 4,467 y no llegaba a AA. */
  --inv: #04140a;
  --curso: #22c55e;         /* produciendo */
  --pausa: #f97316;         /* en pausa */
  --act: #f59e0b;           /* actividad / tarea de apoyo */
  --marca: #F2A900;
  --linea: #38bdf8;         /* color del grupo; lo pisa el inline style */
  /* El semáforo del historial NO puede usar --linea: en Aerosoles quedaba del
     mismo color que el grupo y «va sobrando» era indistinguible de «al día». */
  --sem-sobre: #38bdf8;
  --sem-medio: #eab308;
  /* El ÚNICO rojo de la app, y ahora dicho una sola vez: el mismo #ef4444
     estaba escrito a mano en cinco reglas de tres tandas distintas —el semáforo
     del historial, el exceso de carga del turno, la barra roja de Planificación
     y los dos controles del rechazo de Calidad—, que es exactamente la forma en
     que dos rojos terminan separándose por un dígito. NO es el naranja: --pausa
     significa «parado» y sólo eso. */
  --sem-rojo: #ef4444;
  /* El MISMO rojo, un escalón más claro, para cuando tiene que LEERSE sobre el
     fondo oscuro. #ef4444 como texto del chip de rechazo encendido daba 4,15:1
     contra el fondo del propio chip (color-mix 16% sobre rgb(20,20,23) =
     rgb(55,28,30)) y el mínimo es 4,5; de borde daba 2,23:1 contra 3. Con éste,
     medido con el mismo composite: texto 5,63:1 y borde (70%) 3,83:1. Sigue
     siendo rojo y no naranja —ΔE76 contra --pausa #f97316 sube de 34,6 a 42,4,
     o sea que se distingue MÁS que antes—. No reemplaza a --sem-rojo: los
     rellenos (`.btn--rechazo`, `.carga__fill--rojo`) llevan texto encima y ahí
     el que contrasta es el oscuro. */
  --sem-rojo-claro: #f87171;
  /* «Quedó a medias, hay que retomarlo» aparece en tres lugares (la lista de
     Iniciar, la fila terminada del hub y la hoja de cierre parcial) y no es ni
     un grupo ni un estado de máquina, así que necesita color propio. Es el
     violeta que dejó libre Líquidos al mudarse al rosa: ya estaba probado sobre
     este fondo y ahora no lo reclama nadie. */
  --retomar: #a78bfa;

  --r-badge: 6px;  --r-btn: 10px;  --r-nav: 12px;  --r-card: 16px;  --r-pill: 999px;
  --s1: 4px; --s2: 8px; --s3: 16px; --s4: 24px; --s5: 32px;
  /* Único valor de opacidad para "deshabilitado" en toda la app: antes convivían
     .3/.35/.4/.45 en la misma pantalla (Planificación). */
  --dis: .45;

  /* Texto: 7 pasos. Cada salto es de 2 px o más justamente para que se note. */
  --t-micro:  11px;   /* mayúsculas con tracking, badge, rótulos de la nav */
  --t-meta:   13px;   /* metadatos, subtítulos, rótulos de sección */
  --t-body:   15px;   /* base de la app: párrafos, chips, avisos */
  --t-fila:   17px;   /* contenido primario de una fila y texto de acción */
  --t-tit:    20px;   /* título de hoja modal, cartel de error */
  --t-tit-l:  23px;   /* título del detalle */
  --t-tit-xl: 28px;   /* el mismo título en tablet */
  /* Números grandes (peso 600 + tabular-nums). Ladder aparte: son cifras, no
     texto, y nunca comparten renglón con un título. */
  --n1: 26px; --n2: 32px; --n3: 46px; --n4: 60px;
  /* NO agregues `font-variant-numeric: tabular-nums` a ninguna regla más, y no
     te sorprenda que las 20 que ya lo declaran no muevan un píxel: contra la
     Roboto self-hosted la propiedad es un NO-OP, medido. «111111» y «888888»
     miden lo mismo con y sin ella (39,41px y 42,00px respectivamente), y los
     dígitos sueltos «1» y «8» dan 7px en los dos casos. La razón no es que
     Roboto ya venga tabular —si lo fuera, las dos cadenas medirían igual entre
     sí y no lo hacen—: es que el archivo de fuente que servimos no expone la
     feature OpenType `tnum`. Se deja lo que ya está por si algún día se cambia
     el archivo; agregar más declaraciones es escribir código muerto. */

  /* Movimiento. El baseline es MOVIMIENTO BAJO: el operario está al lado de una
     máquina en marcha y todo lo que se mueva en visión periférica distrae. El
     presupuesto se gasta casi entero en el acuse del toque. */
  --dur-press: 110ms;  --dur-ui: 180ms;  --ease-out-quart: cubic-bezier(.25, 1, .5, 1);

  --safe-b: env(safe-area-inset-bottom, 0px);
  /* Medidos en el navegador, no estimados: el header da 59px y la nav 69px.
     Con los valores justos el contenido arrancaba 1px por DEBAJO del borde
     del header. Estos dejan además el aire que pide el diseño. */
  --hdr-h: 60px;
  --nav-h: 72px;
  /* Alto de la franja «volver al estado real» (píldora de 40 + su separación):
     lo usan el padding de la página y el toast para no quedar tapados. */
  --franja: 58px;
}

* { box-sizing: border-box; }   /* sin esto el panel de la tablet se desborda */

html, body {
  margin: 0; padding: 0;
  height: 100%;
  background: var(--bg); color: var(--txt);
  font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-size: var(--t-body); line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}
[x-cloak] { display: none !important; }
button { font: inherit; color: inherit; }
a { color: var(--marca); }

/* El foco tiene que verse SIEMPRE: los inputs lo apagaban y dejaban sólo un
   borde tenue que en Líquidos daba 2,89:1. `:where()` no suma especificidad. */
:where(button, [role="button"], a, input, select):focus-visible {
  outline: 2px solid var(--linea); outline-offset: 2px;
}

/* `touch-action: manipulation` mata los 300 ms que el navegador se guarda por
   si el toque es un doble-tap-para-zoom. La lista de abajo lo daba sólo a los
   controles con `:active` propio: censadas 52 cajas táctiles de una hoja
   abierta + nav + header, 45 estaban en `auto` y 7 en `manipulation` — las
   filas (`.row`, `.row__b`, `.row__r`), los círculos de acción (`.circ`), las
   filas del detalle (`.drow`) y TODOS los buscadores quedaban afuera, que son
   justo los blancos que el operario toca en cadena.
   `:where()` no suma especificidad, así que no pisa nada. Los contenedores de
   scroll quedan afuera a propósito (`.hoja__c`, `.picker-caja > .grp`): ahí
   `manipulation` no ayuda y el gesto que importa es el arrastre. */
:where(button, [role="button"], a, input, select, .row, .drow) {
  touch-action: manipulation;
}

/* Con `-webkit-tap-highlight-color: transparent` el dedo no recibe NADA si el
   componente no trae su propio :active. Con guantes eso es un toque perdido. */
.chip, .op, .nav__btn, .sel__lnk, .caido__b, .dban__b, .hdr__volver, .hdr__cambiar,
.btn, .pausag, .sel__g {
  transition: transform var(--dur-press) var(--ease-out-quart),
              background-color var(--dur-ui) var(--ease-out-quart),
              border-color var(--dur-ui) var(--ease-out-quart),
              color var(--dur-ui) var(--ease-out-quart);
  touch-action: manipulation;
}
.chip:active, .op:active, .nav__btn:active, .pausag:active,
.sel__lnk:active, .caido__b:active, .dban__b:active { transform: scale(0.96); }
.hdr__volver:active, .hdr__cambiar:active { opacity: .6; }
/* Las filas no escalan (arrastrarían la tarjeta entera): aclaran el fondo. */
.row, .drow { transition: background-color var(--dur-press) var(--ease-out-quart); }
.pausa-linea:active,
.row:has(.row__b:active), .row:has(.row__r:active), a.row:active,
.drow:not(.drow--data):active { background: rgba(255,255,255,0.055); }

.app { position: relative; min-height: 100dvh; }

/* ── Header ──────────────────────────────────────────────────────────────── */
.hdr {
  position: fixed; top: 0; left: 0; right: 0; z-index: 50;
  background: var(--bg); border-bottom: 1px solid var(--hair);
  /* El 6 de abajo es medido, no elegido: 8 + 44 (alto táctil) + 6 + 1 = 59, que
     es el alto real con el que está calculado --hdr-h. Redondearlo a 8 corre
     todo lo que cuelga de --hdr-h. */
  padding: var(--s2) var(--s3) 6px;
  /* Grilla de tres y NO flex con space-between: con flex el logo se centra en el
     hueco que sobra, así que se corre según lo largo que sea el nombre del grupo
     y según haya o no botón a la derecha. Acá cae en el centro GEOMÉTRICO del
     header y se queda quieto entre pantallas. Las columnas laterales son
     `minmax(0, 1fr)` —no `1fr`— para que un nombre largo se recorte con elipsis
     en vez de empujar el logo. Mismo patrón que el MES, por la misma razón. */
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: var(--s2);
  padding-top: calc(var(--s2) + env(safe-area-inset-top, 0px));
}
/* `max-width: 100%` y no sólo `min-width: 0`: con `justify-self: start` un item
   de grilla se dimensiona AL CONTENIDO y puede desbordar su columna, que es lo
   que pasaba desde que el rótulo de Materiales lleva el número de OT — medido a
   390px, `.hdr__left` daba 201px sobre una columna de 120 y el texto se metía
   57px DEBAJO del logo. Con el tope, la elipsis del span por fin actúa.
   El `minmax(0, 1fr)` de la grilla no alcanzaba solo: acota la COLUMNA, no al
   item que eligió no llenarla. */
.hdr__left { min-width: 0; max-width: 100%; display: flex; align-items: center; justify-self: start; }
/* 26px: el mismo alto que el logo tiene en el MES, y entra en los 44 de alto
   táctil del header sin tocar el hairline de abajo.
   `pointer-events: none` porque no es un link ni un botón: sin eso se come
   toques en el centro de una franja de 44 px de alto, que es justo donde el
   pulgar aterriza al agarrar el celular. */
.hdr__logo { grid-column: 2; justify-self: center; height: 26px; width: auto;
             display: block; pointer-events: none; }
/* Bajo 480px el logo se saca de la grilla: la columna central `auto` le
   restaba ancho a `.hdr__left` en las dos laterales `minmax(0, 1fr)` por
   igual —reparten lo que sobra, no lo que cada una necesita— y a 390px «OT
   12626 · Materiales» (scrollWidth 149) se veía 79 de 149, y «Tarea de
   apoyo» (102), 74: el título es la única orientación que tiene el operario
   y el logo ya está en todo lado (login, tableros, PWA). */
@media (max-width: 479px) {
  .hdr__logo { display: none; }
}
/* De 600 para arriba el logo pasa a 40px (158×40): pedido del dueño el
   15-sep-2026, junto con el acomodamiento a 1280×800.
   40 y no 44: 44 es el TECHO —iguala al botón táctil de al lado y deja 8/7 de
   aire—, pero a 48 el header crece de 59 a 63 y se come la reserva de
   `--hdr-h: 64`, de la que cuelgan los ~8 offsets sticky del archivo (la lista,
   el panel, el rail, la hoja). Con 40 quedan 10 arriba y 9 abajo: se lee como
   un logo, no como un logo apretado contra el hairline.
   El escalón arranca en 600 y no en 480 por la MISMA razón que el logo
   desaparece bajo 480 (ver la regla de acá arriba): la columna central es
   `auto` y le resta ancho a las dos laterales `minmax(0, 1fr)` por igual.
   Medido con el título más largo que existe, «OT 12626 · Materiales»
   (scrollWidth 153): a 480 con el logo en 44 la columna izquierda caía a 129 y
   el título se recortaba; de 600 para arriba entra entero. Entre 480 y 599 el
   logo se queda en 26 —nadie lo pierde y ningún título se corta—, y de 600
   para arriba, que es donde viven la tablet y el kiosco de 1280×800, crece. */
@media (min-width: 600px) {
  .hdr__logo { height: 40px; }
}
.hdr__cambiar { grid-column: 3; justify-self: end; }
.hdr__linea {
  font-weight: 700; font-size: var(--t-body); display: inline-flex; align-items: center; gap: var(--s2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hdr__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--linea); flex: none;
            transition: background-color var(--dur-ui) var(--ease-out-quart); }
.hdr__cambiar {
  background: none; border: none; cursor: pointer; color: var(--muted);
  min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center;
}
/* UNA línea. Desde que `tituloPanel` de Materiales lleva el número de OT
   («OT 12776 · Materiales»), el rótulo envolvía a dos renglones y AGRANDABA el
   header, empujando el logo — justo lo contrario de lo que este cambio vino a
   hacer. Con nowrap + ellipsis se recorta, y por eso la OT va PRIMERO: lo que
   sobra al recortar es la palabra que la pantalla ya está mostrando. */
.hdr__volver > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hdr__volver {
  display: inline-flex; align-items: center; gap: var(--s1); background: none; border: none;
  cursor: pointer; font-weight: 700; color: var(--txt); min-height: 44px;
  min-width: 0; padding: var(--s1) var(--s3) var(--s1) 0;
  text-decoration: none;   /* en las zonas es un <a>, no un <button> */
}
.hdr__volver svg { width: 24px; height: 24px; color: var(--linea); }

/* ── Página ──────────────────────────────────────────────────────────────── */
/* El `+ 6px` de acá abajo NO es espaciado suelto fuera de la escala: es el
   MISMO 6 con el que el header cierra por abajo (ver `.hdr`), y lo que hace es
   que el contenido arranque a la misma distancia de la hairline en las tres
   pantallas. Lo repite `.dban`, que se pega justo ahí. Tocarlo en un solo lado
   descuelga el banner de descanso del borde del header. */
.pg {
  position: relative; z-index: 10;
  min-height: 100dvh;
  padding: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + 6px) var(--s3)
           calc(var(--nav-h) + var(--safe-b) + var(--s2));
  display: flex; flex-direction: column; gap: var(--s3);
}
.pg--plana { padding-bottom: calc(var(--s4) + var(--safe-b)); }
/* Con la píldora de «volver al estado real» en pantalla, el último elemento de
   la lista quedaba debajo de ella. La clase la pone el template. */
.con-real .pg { padding-bottom: calc(var(--nav-h) + var(--safe-b) + var(--franja)); }
.con-real .pg--plana { padding-bottom: calc(var(--s4) + var(--safe-b) + var(--franja)); }

/* La columna del hub. Vive FUERA del media query, igual que .det: abajo de
   820 px .col era un div de bloque y «Pausar el grupo» quedaba pegado a
   «En curso». El mockup lo separa porque ahí el botón cuelga directo de .pg,
   que tiene gap; el wrapper .col —que existe para el split de tablet— se comió
   esa separación.
   Sobre los x-if: los dos <template> de adentro son display:none, así que NO
   son ítems flex y no generan gap. Los hijos visibles son SIEMPRE dos (el clon
   de .pausag O el de .dban, más .scroll): este gap separa una sola vez y nunca
   cae en el caso del contenedor con un único hijo visible. */
.col { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }
.scroll { display: flex; flex-direction: column; gap: var(--s4); }
/* El cuerpo de Historial. En celular es exactamente el `style` inline que
   reemplazó (flex column, gap --s4); existe como clase porque en tablet
   apaisada se convierte en grilla de dos columnas y un inline gana siempre.
   Va ACÁ, antes de los media queries: con la misma especificidad gana la última
   regla del archivo, así que declarado más abajo la grilla no se aplicaba. */
.histv { display: flex; flex-direction: column; gap: var(--s4); }
/* Las dos mitades de Historial. En celular apilan con el mismo gap que daba
   .histv; en tablet apaisada son las columnas. Va acá arriba, antes de los
   media queries, por la trampa de orden del archivo. */
.histv__col { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
.sect__h {
  font-size: var(--t-meta); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--faint); margin: 0 0 var(--s2);
}
/* `.sect__crit` —el renglón que nombra el criterio de orden de una lista— vive
   abajo, con los otros modificadores de `.hoja__hint`: acá arriba la regla
   `.hoja__hint { margin: 0 }` le ganaba por orden con la misma especificidad. */
/* Label de campo dentro de una hoja/formulario: mismo rol que .sect__h pero
   sin mayúsculas ni tracking, porque conviven con el valor en la misma línea. */
.campo__l { font-size: var(--t-meta); font-weight: 700; color: var(--muted); letter-spacing: 0; }
/* Los que rotulan UN control pasaron a ser <label for>: un <label> es inline y
   sin esto se pegaba al campo rompiendo el gap de la hoja. `cursor: pointer`
   porque ahora el rótulo TAMBIÉN enfoca el campo — 27 inputs de las hojas no
   tenían ni un <label> y el lector leía el placeholder como nombre («0», «—»).
   Los campo__l que rotulan una botonera siguen siendo <div>: ahí no hay un
   control al que apuntar y se nombran con `role="group" aria-labelledby`. */
label.campo__l { display: block; cursor: pointer; }

/* ── Contenedor agrupado: una tarjeta, filas separadas por hairline ───────── */
.grp {
  background: var(--card); border: 1px solid var(--hair);
  border-radius: var(--r-card); overflow: hidden;
}
.row {
  position: relative; width: 100%; min-height: 72px;
  display: grid; grid-template-columns: 48px 1fr auto; align-items: center; gap: var(--s3);
  padding: var(--s3); background: none; border: none; text-align: left; color: var(--txt);
}
/* Cuando la fila es un <a> (tableros.html) en vez de un <button>. */
a.row { text-decoration: none; color: inherit; }
/* 80 = padding 16 + círculo 48 + gap 16: el hairline arranca donde arranca el
   texto, no debajo del círculo. */
.row + .row::before {
  content: ""; position: absolute; left: 80px; right: 0; top: 0; height: 1px; background: var(--hair);
}

/* Atenuar con opacity dejaba el `.row__meta` en ~2.6:1 contra el fondo: ilegible
   incluso para texto grande. Con colores explícitos el producto llega a 7,4:1. */
.row--hecha .row__prod { color: var(--muted); }
.row--hecha .row__nro, .row--hecha .row__meta { color: var(--faint); }
/* La barra fina vive en los últimos 11px de la fila (bottom 8 + alto 3). */
.row--conbarra { padding-bottom: calc(var(--s3) + 6px); }
.row__b {
  background: none; border: none; padding: 0; text-align: left; color: inherit;
  cursor: pointer; min-width: 0;
}
/* Las dos filas de atajos no tienen `row__nro` ni `row__meta`, así que su
   `.row__b` colapsa al alto de UNA línea y quedaba en 282×28,3 dentro de una
   fila de 80 (244×26,9 en el celular): los 26px de arriba y los 26 de abajo
   caen en el `div.row`, que no tiene @click, y se comen el toque. `min-height`
   y no padding: el texto sigue centrado por el `align-items` de `.row` y no se
   mueve un píxel.
   `.paso-linea` es el mismo caso y se había escapado por no estar bajo
   `.atajos`: las filas de «¿En qué línea?» tampoco tienen `row__nro` ni
   `row__meta`, y medidas a 1024×768 daban 345×28,3 dentro de una fila de 80
   (220×27 a 390×844), con `elementFromPoint` devolviendo el `div.row` sin
   handler a 2px por encima y a 34 por debajo del rótulo. */
.atajos .row__b, .paso-linea .row__b { min-height: 44px; }
/* El chevron de la fila: es un TARGET, no un ícono decorativo.
   Medido antes de esto, en tablet y en celular por igual: la caja declaraba
   24×24 fijos y adentro había un texto («Ver OTs», «Iniciar», «Revisar») más el
   chevron de 24. El texto desbordaba hasta el `overflow:hidden` de `.grp` —a
   1180 llegaba a x=1001 contra un borde en 996, así que se leía «Ver OT»— y el
   chevron colapsaba a **0px de ancho**: no se veía ni uno solo. Además 24×24 es
   la mitad del mínimo táctil.
   Se fue el texto (el círculo de la izquierda ya dice qué hace la fila, el
   cuerpo entero dispara la misma acción, y el caso «Revisar» ya lo anuncia el
   badge «al 84% — ¿sigue?» dentro de la fila) y quedó el chevron, que es lo
   único que aporta algo distinto: avisa que hay un paso más. Lo que era texto
   pasó a `aria-label`, así que el botón no se queda sin nombre accesible. */
.row__r {
  background: none; border: none; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center; color: var(--faint);
  /* 44 de área táctil con el dibujo donde estaba: el margen negativo devuelve
     los 20px que el crecimiento le sacaba a la columna del texto, así que la
     grilla `48px 1fr auto` de la fila no se mueve y el chevron se sigue viendo
     en 24. Sin esto, en el celular los nombres de producto pasaban a partirse
     en dos renglones. */
  width: 44px; height: 44px; margin: -10px; flex: none;
}
.row__r svg { width: 24px; height: 24px; }

/* El círculo ES la acción: siempre es lo que toca hacer ahora.
   Vacío ▶ arrancar · verde ⏸ produciendo, tocalo para pausar ·
   naranja ▶ pausada, tocalo para seguir · lleno ✓ terminada. */
.circ {
  width: 48px; height: 48px; border-radius: var(--r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: default; flex: none; background: transparent; padding: 0; border: 2px solid transparent;
  /* El cambio de estado se funde en 180ms en vez de saltar: es el único
     movimiento que la app se permite fuera del acuse del toque, y es el que
     hace que «arrancó» se lea como un hecho y no como un repintado. */
  transition: transform var(--dur-press) var(--ease-out-quart),
              background-color var(--dur-ui) var(--ease-out-quart),
              border-color var(--dur-ui) var(--ease-out-quart),
              color var(--dur-ui) var(--ease-out-quart);
}
.circ svg { width: 24px; height: 24px; }
/* Varios círculos son sólo un estado dibujado (el ✓ de lo terminado, la cola en
   cuarentena): si se hunden al apretarlos prometen una acción que no existe. */
button.circ, a.circ { cursor: pointer; }
button.circ:active, a.circ:active { transform: scale(0.92); }
.circ--curso { border-color: var(--curso); background: rgba(34,197,94,0.14); color: var(--curso); }
.circ--pausa { border-color: var(--pausa); background: rgba(249,115,22,0.14); color: var(--pausa); }
.circ--plan  { border-color: color-mix(in srgb, var(--linea) 70%, transparent); color: var(--linea); }
.circ--fin   { border-color: var(--curso); background: var(--curso); color: var(--inv); }
.circ--act   { border-color: var(--act); background: rgba(245,158,11,0.14); color: var(--act); }
/* Cola en cuarentena: espera, no actividad — ámbar sin relleno, prestaba
   .circ--act antes de tener nombre propio. */
.circ--espera { border-color: var(--pausa); color: var(--pausa); }
.circ--add   { border: none; color: var(--c, var(--marca)); }
.circ--add svg { width: 26px; height: 26px; }

.row__nro {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2);
  font-size: var(--t-meta); font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums;
}
.row__prod { display: block; font-size: var(--t-fila); font-weight: 500; line-height: 1.35;
             margin-top: var(--s1); letter-spacing: 0.015em; }
.row__meta {
  display: block; font-size: var(--t-meta); color: var(--muted); margin-top: var(--s1);
  font-variant-numeric: tabular-nums;
}
.row__pausa { display: block; font-size: var(--t-meta); font-weight: 700; color: var(--pausa);
              margin-top: var(--s1); }

.row__bar {
  position: absolute; left: 80px; right: var(--s3); bottom: var(--s2); height: 3px;
  border-radius: var(--r-pill); background: rgba(255,255,255,0.08); overflow: hidden;
}
/* La barra se mueve con `transform`, que compone en la GPU, y NO con `width`,
   que dispara layout en toda la lista cada 20 s para siempre. La migración ya
   está hecha en los cuatro consumidores: `.row__fill` y `.cifra__fill` reciben
   `--p: frac(...)` (index.html), `.carga__fill`, `--p: pct/100` (_vistas.html),
   y `.matcard__fill`, `--p: matFracDe(...)` (_panel.html) — ése tiene su propio
   bloque más abajo, pero corre el MISMO timing y la MISMA curva.
   No queda ni un `style="width: …%"` en las plantillas, y el `var(--p, 1)` es
   el default por si alguna vez llega una barra sin fracción. */
.row__fill, .cifra__fill, .carga__fill {
  display: block; height: 100%; width: 100%; border-radius: var(--r-pill);
  transform: scaleX(var(--p, 1)); transform-origin: left center;
  transition: transform 300ms var(--ease-out-quart);
}

/* El badge de línea SÓLO aparece si la OT no es de la línea del grupo. */
.badge {
  /* El 2 vertical es óptico: el badge vive DENTRO de una línea de 13px y con
     --s1 empujaba el interlineado de `.row__nro`. */
  display: inline-block; font-size: var(--t-micro); font-weight: 700; line-height: 1.25;
  padding: 2px var(--s2); letter-spacing: 0.015em;
  border-radius: var(--r-badge);
  background: color-mix(in srgb, var(--linea) 22%, transparent); color: var(--linea);
}
.badge--otra {
  background: color-mix(in srgb, var(--c-otra, #94a3b8) 22%, transparent); color: var(--c-otra, #94a3b8);
}
/* Avisa, no veta: la fila sigue tocable. Usa el mismo naranja que .aviso--warn
   y .chip--warn, así «esto merece una mirada» se dice siempre igual. */
.badge--aviso {
  background: color-mix(in srgb, var(--pausa) 18%, transparent); color: var(--pausa);
}
.badge--parcial {
  background: color-mix(in srgb, var(--retomar) 22%, transparent); color: var(--retomar);
}

/* ── Nav ─────────────────────────────────────────────────────────────────── */
.nav {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 60;
  display: flex; justify-content: center; gap: var(--s1);
  background: var(--bg); border-top: 1px solid var(--hair);
  /* Abajo: `max(pad, safe-area)` y NUNCA `calc(pad + safe-area)`.
     En Android (safe-area 0) las dos formas dan 8 y la barra mide 70. En iOS el
     home-indicator ABSORBE el padding en vez de sumarse, así que con el calc()
     los botones quedaban 8px arriba de donde tienen que estar y la barra se veía
     DESPEGADA del fondo: es «el navegador inferior se eleva unos cuantos px»
     que reportó el dueño el 31-ago. Medido a 390×844 con safe-b=34: el pie de
     los botones caía en 802 contra 810.
     Es la misma regla —y por el mismo motivo— que ya tiene el `.nav` del MES
     viejo; ver su comentario en mes.css. Los `calc(… + var(--safe-b))` del
     RESTO del archivo se quedan como están y no son el mismo caso: el padding
     de la página y el `bottom:` de las píldoras flotantes SÍ tienen que
     despejar el indicator Y su propio aire. */
  padding: var(--s2) var(--s2) max(var(--s2), var(--safe-b));
}
.nav__btn {
  /* El alto se recorta hasta 8px cuando hay home-indicator: sin esto la barra
     entera crece 34px en iOS —el ancho del indicator— y se come el contenido.
     Android (safe-b 0) queda en 54 y el total en 70; iOS en 46 y el total en 88.
     `min(safe-b, 8px)` y no `safe-b` pelado: en un device con un inset grande
     el botón se iría abajo del mínimo de 44 que pide toda esta pantalla. */
  flex: 1 1 0; max-width: 140px; min-height: calc(54px - min(var(--safe-b), 8px));
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s1);
  background: transparent; border: none; border-radius: var(--r-nav);
  color: var(--faint); font-size: var(--t-micro); font-weight: 500; cursor: pointer;
}
.nav__btn svg { width: 24px; height: 24px; }
.nav__btn--act { color: var(--linea); font-weight: 700;
                 background: color-mix(in srgb, var(--linea) 14%, transparent); }
/* Por clase y no por x-show: el display:none inline de Alpine no se puede
   revertir desde un media query sin !important, y en tablet el rail tiene que
   quedarse también en Iniciar y en Tarea de apoyo. */
.nav--oculta { display: none; }

/* ── Pausa del grupo ─────────────────────────────────────────────────────
 * No es la acción primaria (esa es verde y maciza) ni la salida (esa no tiene
 * caja): es el escalón del medio del handoff §5, una acción real que para la
 * producción de TODO el grupo. Con --hair-2 el borde daba 1,32:1 contra el
 * fondo y el botón se leía como un rótulo centrado, no como un control.
 * Se queda GRIS a propósito: DESIGN.md «la regla del semáforo» reserva el
 * naranja para el ESTADO «parado», y este botón sólo existe cuando el grupo
 * NO está parado (x-if="!descanso"). */
.pausag {
  width: 100%; min-height: 48px; border-radius: var(--r-btn); background: transparent;
  border: 1px solid var(--borde-ctrl); color: var(--muted); font-size: var(--t-body); font-weight: 700;
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
}
.pausag svg { width: 20px; height: 20px; }
/* 0.055 es el mismo susurro de .row:active y del descarte de las hojas; el
   0.04 anterior daba 1,10:1 y con guantes no devolvía nada. */
.pausag:active { background: rgba(255,255,255,0.055); }

.dban { padding: var(--s3); border-radius: var(--r-card); background: var(--card);
        border: 1px solid rgba(249,115,22,0.45);
        position: sticky; top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + 6px); z-index: 20; }
.dban__t { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-fila);
           font-weight: 700; color: var(--pausa); }
.dban__c { margin-left: auto; font-size: var(--t-body); color: var(--muted); font-weight: 700;
           font-variant-numeric: tabular-nums; }
.dban__b { width: 100%; min-height: 52px; margin-top: var(--s3); border-radius: var(--r-btn);
           background: var(--curso); border: none; color: var(--inv); font-size: var(--t-fila);
           font-weight: 700; cursor: pointer; }

/* ── Detalle ─────────────────────────────────────────────────────────────── */
/* El ritmo vertical del detalle es del componente, no del breakpoint: vivía
   dentro del media query de tablet y en el celular lo sostenía un style inline. */
.det { display: flex; flex-direction: column; gap: var(--s4); }
.det__h { margin: 0; font-size: var(--t-tit-l); font-weight: 700; line-height: 1.2;
          text-wrap: balance; }
.det__s { margin: var(--s1) 0 0; font-size: var(--t-meta); color: var(--muted); display: flex;
          align-items: center; gap: var(--s2); flex-wrap: wrap; font-variant-numeric: tabular-nums; }

/* ── Encabezado del panel ───────────────────────────────────────────────────
   Sólo lo llevan las tres vistas que ENTRAN al panel (Iniciar, Tarea de apoyo y
   el detalle de una tarea): el resumen del turno y el detalle de la OT se
   nombran solos y se sueltan tocando de nuevo su fila.
   Se declara acá, fuera del media query, aunque hoy sólo se vea de 820 para
   arriba: el template lo esconde en celular con `solo-ancho` —donde el ← del
   header hace el trabajo— y así el componente no depende del breakpoint.
   Los DOS botones miden 44 y son hermanos del título, que se come el resto:
   el ✕ tiene que caer contra el borde derecho del panel en las tres pantallas,
   y con el ← ausente el título no se puede mover ni un píxel. */
.det__hdr {
  display: flex; align-items: center; gap: var(--s2);
  padding-bottom: var(--s2); border-bottom: 1px solid var(--hair);
}
.det__t {
  margin: 0; flex: 1 1 auto; min-width: 0;
  font-size: var(--t-tit); font-weight: 700; line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.det__ic {
  flex: none; background: none; border: none; cursor: pointer; color: var(--muted);
  min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
}
/* El ← es un paso de la MISMA pantalla (volver a elegir la línea) y va en el
   color del grupo, igual que el ← del header; el ✕ sale, y se queda en --muted:
   son dos cosas distintas y no pueden pesar lo mismo. */
.det__ic:first-child svg { color: var(--linea); }
.det__ic:active { opacity: .6; }

/* El «Listo» de Materiales, en el renglón del encabezado y en el lugar del ✕.
   Era un pie anclado de ancho completo; acá no cuesta un solo píxel de alto.
   `flex: none` y el título con ellipsis: el que se recorta cuando el nombre de
   la OT es largo es el TÍTULO, no la salida. */
/* Es una VUELTA, no un «Listo»: contorno y ← en el color de la línea, el mismo
   idioma que el ← del encabezado y el del header de celular. Relleno verde
   tenía dos problemas — prometía un estado en vez de un destino, y en una
   pantalla con trabajo a medias era el bloque más atractivo que había. */
.hdr-listo {
  flex: none; min-height: 44px; padding: 0 var(--s3); border-radius: var(--r-btn);
  display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer;
  background: transparent; border: 1px solid var(--borde-ctrl); color: var(--txt);
  font-size: var(--t-body); font-weight: 700;
}
.hdr-listo svg { width: 20px; height: 20px; color: var(--linea); }
.hdr-listo:active { background: rgba(255,255,255,0.055); }
/* El estado, al lado de la acción y sin disputarle el peso. */
.hdr-listo__n { font-weight: 500; font-size: var(--t-meta);
                font-variant-numeric: tabular-nums; }
.hdr-listo__n--pend { color: var(--pausa); }
.hdr-listo__n--ok   { color: var(--curso); }

.cifra__l { font-size: var(--t-micro); color: var(--faint); letter-spacing: 0.1em; text-transform: uppercase; }
.cifra__v { font-size: var(--n3); font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;
            letter-spacing: 0.015em; margin-top: var(--s2); }
.cifra__d { font-size: var(--t-fila); font-weight: 500; color: var(--muted); }
.cifra__bar { margin-top: var(--s3); height: 8px; border-radius: var(--r-pill);
              background: rgba(255,255,255,0.08); overflow: hidden; }
/* En el detalle no va el %: va «Faltan 360 u», que es lo que el operario usa. */
.cifra__sub { margin-top: var(--s2); font-size: var(--t-meta); color: var(--muted);
              font-variant-numeric: tabular-nums; }

/* ── La tarjeta de estado del detalle ─────────────────────────────────────
   Todo lo que la corrida SABE, en una caja, y ninguna acción sobre la
   producción adentro. Eran tres bloques sueltos —cifra al aire, `.grp` de
   datos y el control de operarios hundido en el pie anclado— y las dos
   últimas caían debajo del sticky: medido el 29-ago a 1180×820,
   `elementFromPoint` sobre el centro de «Arrancó» devolvía `.acts`. */
.estado {
  background: var(--card); border: 1px solid var(--hair);
  border-radius: var(--r-card); padding: var(--s3);
}
/* Arrancó · Terminó · N parciales. Un renglón donde había tres filas de 56px:
   son metadatos, no filas de una lista, y ninguno se toca. */
.estado__meta { margin-top: var(--s3); font-size: var(--t-meta); color: var(--faint);
                font-variant-numeric: tabular-nums; }
/* Las filas TOCABLES de la tarjeta (Operarios, y Materiales con la OT ya
   cerrada). Geometría propia y no `.drow`: adentro de una tarjeta que ya trae
   su padding, el de `.drow` se duplicaba contra el borde. 60 = 16 de aire
   sobre el hairline + 44 de zona táctil. */
.estado__f {
  width: 100%; min-height: 60px; margin-top: var(--s3); padding: var(--s3) 0 0;
  display: flex; align-items: center; gap: var(--s3);
  background: none; border: 0; border-top: 1px solid var(--hair);
  text-align: left; color: var(--txt); font-size: var(--t-fila); cursor: pointer;
}
.estado__f:active { opacity: .6; }
/* El aviso queda pegado al control que lo resuelve. Hasta el 29-ago el botón
   «Operarios» estaba en una caja y su aviso en otra. */
.estado > .aviso--warn { margin-top: var(--s3); }

/* ── La botonera del detalle: 1 primaria + grilla 2×2 ─────────────────────
   El pie anclado tenía dos botones sólidos de ancho completo (verde
   «Registrar parcial» + cian «Materiales») y tres filas con chevrón, que se
   leen como NAVEGACIÓN y no como acción: 433px de un panel de 737.
   Un tile es cuadrado, tiene el ícono grande arriba y el rótulo debajo — la
   forma de un botón de máquina, no la de un renglón de menú. */
.tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
.tile {
  min-height: 92px; padding: var(--s2);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--s1); text-align: center;
  background: var(--card); border: 1px solid var(--borde-ctrl);
  border-radius: var(--r-card); color: var(--txt);
  font-size: var(--t-body); font-weight: 700; cursor: pointer;
  /* La vuelta del hundido, como en `.btn` y en `.chip`: sin transición el
     `scale` del `:active` se aplica y se saca de golpe y el tile es el único
     control de la botonera que se siente distinto. Va sólo el transform: el
     `:active` no toca ni el fondo ni el borde. */
  transition: transform var(--dur-press) var(--ease-out-quart);
}
/* 28 y no los 20 del atributo del <svg>: a 50cm y con guantes, el ícono es lo
   que se acierta primero y el rótulo lo confirma. El CSS le gana al atributo
   de presentación, así que los macros de _iconos no se tocan. */
.tile__i { width: 28px; height: 28px; flex: none; color: var(--muted); }
.tile__i svg { width: 28px; height: 28px; }
.tile__t { line-height: 1.2; }
/* Los cuatro estados de la materia prima, en el tile y sin entrar. */
.tile__n { font-size: var(--t-micro); font-weight: 500; color: var(--faint); line-height: 1.2; }
.tile:active { transform: scale(0.97); }
.tile[disabled] { opacity: var(--dis); cursor: default; }
/* Misma guarda que `.btn[disabled]:active`: un control que se hunde sin hacer
   nada dice «te escuché», y el operario vuelve a tocar en vez de buscar qué
   le falta. */
.tile[disabled]:active { transform: none; }
.tile--pausa .tile__i { color: var(--pausa); }
.tile--mat .tile__i { color: var(--linea); }
.tile--muted .tile__i, .tile--muted .tile__t { color: var(--muted); }
/* Reanudar es el único tile que además pinta el BORDE: es la salida de la
   pausa y tiene que encontrarse sin leer. Sigue siendo un contorno y no un
   relleno, así que no le disputa el rango a la primaria. */
.tile--curso { border-color: var(--curso); }
.tile--curso .tile__i, .tile--curso .tile__t { color: var(--curso); }

/* Es un <button> y no declaraba ni fondo, ni borde, ni appearance: heredaba el
   chrome del navegador —fondo gris #6b6b6b, borde `2px outset white`, esquinas
   rectas— con el texto naranja encima a 1,9:1. Se veía sólo cuando había una OT
   PAUSADA, que es la mitad del turno en una tablet montada y justamente el
   estado que nadie tenía en pantalla cuando se revisó el contraste. Ahora habla
   el mismo idioma que el resto de lo que significa «parado». */
.pausa-linea {
  width: 100%; appearance: none; -webkit-appearance: none; cursor: pointer;
  display: flex; align-items: baseline; gap: var(--s2); text-align: left;
  padding: var(--s2) var(--s3); border-radius: var(--r-card);
  background: color-mix(in srgb, var(--pausa) 8%, var(--card));
  border: 1px solid color-mix(in srgb, var(--pausa) 35%, transparent);
  font-size: var(--t-body); font-weight: 700; color: var(--pausa);
}
.pausa-linea__c { margin-left: auto; font-size: var(--n1); font-weight: 600;
                  font-variant-numeric: tabular-nums; letter-spacing: 0.015em; }

.drow {
  position: relative; width: 100%; display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3); background: none; border: none; text-align: left; color: var(--txt);
  font-size: var(--t-fila); cursor: pointer; min-height: 56px;
}
.drow + .drow::before { content: ""; position: absolute; left: var(--s3); right: 0; top: 0;
                        height: 1px; background: var(--hair); }
.drow--data { cursor: default; }
/* Un botón deshabilitado se veía idéntico a uno activo. El `:not(.drow--data)`
   es a propósito: cuando falta asignar operarios el template YA pinta la fila
   en --faint por estilo inline, y las dos atenuaciones multiplicadas dejaban el
   texto en ~1,6:1. */
.drow[disabled] { cursor: default; }
.drow[disabled]:not(.drow--data) { opacity: var(--dis); }
.drow__k { color: var(--muted); font-size: var(--t-body); }
.drow__t { font-weight: 700; }
.drow__v { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; text-align: right; }
.drow__i { width: 24px; height: 24px; flex: none; }
.drow__chev { margin-left: auto; width: 24px; height: 24px; color: var(--faint); flex: none; }
.drow__chev svg { width: 24px; height: 24px; }
/* Color de estado por CLASE, no por :style inline armado en el template: el
   valor sale del payload sólo cuando no hay un estado que lo explique. */
.drow--pausa .drow__i, .drow--pausa .drow__t { color: var(--pausa); }
.drow--muted .drow__i, .drow--muted .drow__t { color: var(--muted); }

/* En el celular el botón primario quedaba a 6px de las filas tenues: el ritmo
   de las acciones es del componente, no del breakpoint. */
.acts { display: flex; flex-direction: column; gap: var(--s3); }

/* Una sola acción primaria, y cambia con el estado. */
.btn {
  width: 100%; min-height: 56px; border: none; border-radius: var(--r-btn);
  font-size: var(--t-fila); font-weight: 700; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  background: var(--curso); color: var(--inv);
}
.btn svg { width: 24px; height: 24px; }
.btn:active { transform: scale(0.97); }
.btn[disabled] { opacity: var(--dis); cursor: default; }
/* Un botón deshabilitado que se HUNDE al tocarlo dice «te escuché» cuando no
   pasó nada: medido, «Iniciar la tarea» en disabled pasaba de 584×62 a
   566,48×60,14 con el dedo apoyado, idéntico al botón que sí responde. Con
   guantes ese acuse es todo lo que hay, y el operario vuelve a tocar en vez de
   buscar qué le falta. `.chip[disabled]:active` ya tenía la guarda; estos tres
   no la tenían. */
.btn[disabled]:active,
.pfecha__nav[disabled]:active,
.pit__b[disabled]:active { transform: none; }
.btn--tenue { background: transparent; border: 1px solid var(--borde-ctrl); color: var(--txt); }
/* La otra mitad del veredicto en verde: «Confirmar veredicto» salía en el verde
   de `.btn` tanto para aprobar 63 u como para rechazarlas. El fondo acompaña al
   chip encendido y el rótulo lo dice con todas las letras (ver _zona.html).
   `color` no se toca: --inv sobre #ef4444 mide 5,03:1; el blanco daría 3,4:1. */
.btn--rechazo { background: var(--sem-rojo); }
/* Variante angosta: no ocupa el ancho completo (p.ej. «Agregar» al lado de un
   operario escrito a mano, o «Tomarlo de una OT abierta…» al lado del campo de
   producto). Tres usos en `_hojas.html`, todos dentro de una hoja. */
.btn--chico { width: auto; padding: 0 var(--s3); border-radius: var(--r-btn); font-size: var(--t-body); }

/* «Cancelar», «Volver» y «Listo» NO son acciones: son la salida. Tenían la
   misma caja, el mismo peso y el mismo ancho que el botón que compromete, y con
   guantes y apurado ése es el toque que se hace sin mirar. Pierden la caja y el
   peso, y conservan el ancho completo y 52px de alto: bajan de jerarquía sin
   volverse un link imposible de acertar.
   Acá vivía además un PUENTE estructural —`.hoja__c
   .btn--tenue:not(.btn--chico):last-of-type`— que pescaba al último <button>
   de cada hoja cuando los templates todavía no llevaban esta clase. Su propio
   comentario decía que se borraba el día que la llevaran: los 15 botones de
   salida de las hojas la llevan (contados uno por uno en `_hojas.html` y
   `_zona.html`), y el puente ya no alcanzaba a nadie — de los seis
   `.btn--tenue` que viven dentro de una hoja, tres son `.btn--chico`, uno vive
   en `.par-acc` y los dos de `/calidad` tienen el «Volver» detrás. Se va, y con
   él la trampa de que un `.btn--tenue` nuevo al final de una hoja perdiera la
   caja sin que nadie lo pidiera. */
.btn--salir {
  background: none; border: none; color: var(--muted);
  font-size: var(--t-body); font-weight: 500; min-height: 52px;
}
.btn--salir:active { background: rgba(255,255,255,0.055); transform: none; }

/* ── Selector de grupo ───────────────────────────────────────────────────── */
.sel { min-height: 100dvh; display: flex; flex-direction: column; justify-content: center;
       gap: var(--s3); padding: var(--s5) var(--s3) calc(var(--s5) + var(--safe-b)); max-width: 560px;
       margin: 0 auto; width: 100%; }
.sel__t { margin: 0; font-size: var(--t-body); font-weight: 700; letter-spacing: .1em;
          text-transform: uppercase; color: var(--faint); }
.sel__q { margin: 0 0 var(--s2); font-size: var(--t-tit-l); font-weight: 700; }
.sel__g {
  width: 100%; min-height: 76px; display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3); border-radius: var(--r-card); background: var(--card);
  border: 1px solid var(--hair); color: var(--txt); font-size: var(--t-tit); font-weight: 700; cursor: pointer;
}
.sel__g:active { background: #101014; }
/* Envoltorio de los grupos: en el celular no cambia nada (columna con el mismo
   gap que daba .sel); existe para que en tablet apaisada los tres entren como
   fichas lado a lado. */
.sel__gs { display: flex; flex-direction: column; gap: var(--s3); }
.sel__ic {
  width: 44px; height: 44px; border-radius: var(--r-pill); flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--linea) 18%, transparent); color: var(--linea);
}
.sel__ic svg { width: 24px; height: 24px; }
.sel__mas { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }
.sel__lnk {
  flex: 1 1 auto; min-height: 48px; display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s1); padding: 0 var(--s3); border-radius: var(--r-btn); border: 1px solid var(--borde-ctrl);
  background: transparent; color: var(--muted); font-size: var(--t-body); font-weight: 700;
  cursor: pointer; text-decoration: none;
}
.sel__lnk svg { width: 20px; height: 20px; }
/* El form de «Salir» no puede ser una caja más ancha que su botón: sin
   esto el `flex: 1 1 auto` de `.sel__lnk` estira el BOTÓN dentro del form
   pero el form no crece, y el renglón queda con un hueco. Que el form
   herede el mismo flex deja los cuatro links repartidos parejo. */
.sel__salir { flex: 1 1 auto; display: flex; margin: 0; }
.sel__salir .sel__lnk { width: 100%; }

/* ── Acceso ────────────────────────────────────────────────────────────────
 * La pantalla de login. Vive dentro de `.sel` —el mismo contenedor que el
 * selector de grupo— para que el login y la pantalla que le sigue se lean como
 * la misma app y no como dos productos pegados.
 *
 * El código va ARRIBA y Google DEBAJO, al revés que el CRM y el LIMS: acá el
 * camino de todos los días es el operario de piso, que no tiene cuenta de
 * Google. El SSO es la puerta de las pocas personas que además administran.
 * Todo con el mismo alto de control que el resto de la app (48+), porque esto
 * se toca de pie y con guantes. */
.acc__form { display: flex; flex-direction: column; gap: var(--s2); }
.acc__inp {
  width: 100%; min-height: 56px; padding: 0 var(--s3); border-radius: var(--r-btn);
  background: var(--card); border: 1px solid var(--borde-ctrl); color: var(--txt);
  /* Centrado y espaciado: es un secreto que se tipea a ciegas (type=password),
     y con las viñetas pegadas no se puede contar cuántas se llevan puestas. */
  font-size: var(--t-tit); font-weight: 700; text-align: center; letter-spacing: .3em;
}
/* El `letter-spacing` corre el placeholder a la derecha y lo deja descentrado:
   el texto no lleva el tracking de las viñetas. */
.acc__inp::placeholder { letter-spacing: normal; font-weight: 500; color: var(--faint); }
.acc__inp:focus { outline: none; border-color: var(--linea); }
.acc__btn {
  width: 100%; min-height: 56px; border-radius: var(--r-btn); cursor: pointer;
  background: var(--linea); border: 1px solid var(--linea); color: #0b0b0d;
  font-size: var(--t-tit); font-weight: 700;
  transition: transform var(--dur-press) var(--ease-out-quart);
  touch-action: manipulation;
}
.acc__btn:active { transform: scale(0.97); }
/* Separador con una línea a cada lado. `flex: 1` en los pseudo-elementos y no
   un `border` con posicionamiento: así se adapta al ancho sin medir nada. */
.acc__o {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s2) 0 0; color: var(--faint); font-size: var(--t-micro);
  text-transform: uppercase; letter-spacing: .1em;
}
.acc__o::before, .acc__o::after { content: ""; flex: 1; height: 1px; background: var(--hair); }
/* Google: alto de control y la marca del proveedor —la gente busca el logo, no
   un botón más—, pero SIN relleno macizo: al lado del botón primario tiene que
   leerse como la segunda opción, que es lo que es. */
.acc__google {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  width: 100%; min-height: 56px; padding: 0 var(--s3);
  border-radius: var(--r-btn); border: 1px solid var(--borde-ctrl);
  background: var(--card); color: var(--txt);
  font-size: var(--t-body); font-weight: 700; text-decoration: none;
  transition: transform var(--dur-press) var(--ease-out-quart);
  touch-action: manipulation;
}
.acc__google:active { transform: scale(0.97); }
.acc__google svg { flex: none; }
/* El error del login. Naranja y no rojo: escribir mal un código es lo más
   normal del mundo y no es una falla del sistema. */
.acc__err {
  padding: var(--s3); border-radius: var(--r-btn);
  background: color-mix(in srgb, var(--pausa) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--pausa) 40%, transparent);
  color: var(--pausa); font-size: var(--t-body); font-weight: 500;
}
.acc__pie { margin: var(--s2) 0 0; color: var(--faint); font-size: var(--t-micro); }
/* En tablet `.sel` se ensancha a 900px para que los tres grupos entren como
   fichas lado a lado. Un formulario de login NO quiere ese ancho: medido a
   1180×820, el campo de código daba 868px para ocho caracteres centrados y el
   botón «Entrar» era una barra de punta a punta. Los controles se quedan en el
   ancho de lectura y el resto de la pantalla sigue siendo el mismo contenedor.
   Va en el bloque base y no en el media query: acotar por `max-width` no hace
   nada mientras el contenedor mide menos, así que la regla vale en los dos. */
.acc__form, .acc__o, .acc__google, .acc__err { max-width: 420px; width: 100%; }
/* …pero acotar los controles no alcanza: `.sel` NO declara `align-items`, así
   que un hijo con ancho propio cae en `flex-start` y el bloque entero queda
   PEGADO A LA IZQUIERDA. Medido a 1180×820: `.sel` de 900px con el campo, el
   botón, el separador y Google los cuatro entre x=156 y x=576, y 464px de negro
   al costado. Es el mismo defecto que este archivo ya arregló para `.paso-linea`
   («centrado, no sólo acotado»).
   Y se acota `.sel` en vez de centrar los controles con `margin-inline: auto`
   porque el TÍTULO no está acotado: centrar sólo los controles los despega del
   «Probusol · MES» de arriba, que se queda en el borde. Achicando el contenedor
   se mueven juntos y `.sel` los centra solo, con su `margin: 0 auto`.
   `+ 2 * --s3` compensa el padding lateral del propio `.sel` (border-box), para
   que los controles midan los 420px de lectura que defiende el comentario de
   arriba y no 388. Doble clase para ganarle en especificidad al
   `.sel { max-width: 900px }` del media query de 1024, que va más abajo. */
.sel.sel--acc { max-width: calc(420px + 2 * var(--s3)); }

/* ── Estados: carga, vacío, MES caído ────────────────────────────────────── */
.skel { border-radius: var(--r-card); background: var(--card); border: 1px solid var(--hair);
        overflow: hidden; }
.skel__r { position: relative; overflow: hidden; height: 72px; border-bottom: 1px solid var(--hair); }
.skel__r:last-child { border-bottom: none; }
/* El brillo viaja con `transform` en un pseudo-elemento. Animar
   `background-position` repintaba la fila entera en cada frame, en el celular
   viejo del piso y justo mientras la app está esperando datos. */
.skel__r::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.035), transparent);
  transform: translateX(-100%);
  animation: sh 1.3s ease-in-out infinite;
}
@keyframes sh { from { transform: translateX(-100%) } to { transform: translateX(100%) } }
/* El esqueleto del SELECTOR de grupo, que es otra forma: `.skel` dibuja una
   tarjeta con renglones pegados de 72px —la forma de una lista— y lo que va a
   llegar son fichas sueltas de 76px (celular) o la grilla de tres columnas de
   180px de la tablet (medido: `.sel__gs` = 868×180, tres de 278,66). Es el
   único esqueleto de la app que se pinta EN FRÍO: primer toque del turno, con
   la tablet recién despertando. El `border` explícito le gana a
   `.skel__r:last-child { border-bottom: none }` por orden, no por peso. */
.skel--sel { background: none; border: none; border-radius: 0;
             display: flex; flex-direction: column; gap: var(--s3); }
.skel--sel > .skel__r { height: 76px; border: 1px solid var(--hair);
                        border-radius: var(--r-card); background: var(--card); }

.vacio { padding: var(--s4) var(--s3); text-align: center; color: var(--faint); font-size: var(--t-body); }
/* Segunda línea del vacío: el titular dice QUÉ falta y ésta dice QUÉ lo va a
   llenar («Cuando el piso termine una OT, aparece acá»). El par existía en la
   UI vieja (el vacío de «Pendientes» de mes/calidad.html) y se perdió al mudar las zonas: sin él, una
   cola vacía no distingue «todavía no llegó nada» de «está roto».
   La jerarquía la da el TAMAÑO y no el color: `.vacio` ya está en --faint, que
   es el piso de contraste AA de la app (4,78:1) y no tiene un escalón abajo. */
.vacio__sub { display: block; margin-top: var(--s1); font-size: var(--t-meta); }

/* Cartel de MES caído: qué pasó y Reintentar. Nada de datos viejos en pantalla. */
.caido {
  margin: auto; max-width: 420px; padding: var(--s4);
  border-radius: var(--r-card); background: var(--card); border: 1px solid rgba(249,115,22,0.45);
  display: flex; flex-direction: column; gap: var(--s3); align-items: flex-start;
}
.caido__t { display: flex; align-items: center; gap: var(--s2); font-size: var(--t-tit);
            font-weight: 700; color: var(--pausa); }
.caido__t svg { width: 24px; height: 24px; flex: none; }
/* .nota es el alias de rol: párrafo secundario genérico, usado en 18 lugares
   fuera del cartel de MES caído. Se mantienen los dos nombres para que la
   migración de los templates sea gradual. */
.nota, .caido__d { margin: 0; font-size: var(--t-body); color: var(--muted); }
.caido__b {
  min-height: 48px; padding: 0 var(--s4); border-radius: var(--r-btn); border: none;
  background: var(--pausa); color: var(--inv); font-size: var(--t-body); font-weight: 700; cursor: pointer;
}
.caido--pg { min-height: 100dvh; display: flex; padding: var(--s3); }

/* ── Listas de las otras pantallas (iniciar, historial, apoyo, plan, zonas) ─ */
.busca {
  width: 100%; min-height: 48px; padding: 0 var(--s3); border-radius: var(--r-btn);
  background: var(--card); border: 1px solid var(--borde-ctrl); color: var(--txt); font-size: var(--t-fila);
}
.busca::placeholder { color: var(--faint); }
/* El foco SUBE con el reposo: con el reposo en 3,26:1 y el foco en linea 55 %
   (2,7-2,8:1 en Líquidos y Manuales) el campo enfocado quedaba MENOS marcado
   que el campo en reposo. Al 70 %: 3,8-5,2:1 según el grupo. */
.busca:focus { border-color: color-mix(in srgb, var(--linea) 70%, transparent); }

.chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
/* Aprobar y rechazar no pueden estar a 8px: es la separación que la app usa
   entre opciones equivalentes, y acá una aprueba y la otra rechaza. El margen
   extra sale de la misma escala (--s4) y sólo se aplica cuando las tres entran
   en un renglón; si el ancho las parte, el wrap ya las separa. */
.chips--veredicto > .chip--rechazo { margin-left: var(--s3); }
.chip {
  min-height: 48px; padding: var(--s2) var(--s3); border-radius: var(--r-btn); cursor: pointer;
  background: var(--card); border: 1px solid var(--hair); color: var(--muted);
  font-size: var(--t-body); font-weight: 700; display: inline-flex; align-items: center;
  justify-content: center; text-align: center; line-height: 1.25; gap: var(--s1);
  font-variant-numeric: tabular-nums;
}
.chip--on { border-color: color-mix(in srgb, var(--linea) 55%, transparent);
            background: color-mix(in srgb, var(--linea) 16%, transparent); color: var(--linea); }
/* «Aprobar todo» y «Rechazar todo» encendidos daban EL MISMO VERDE al valor
   exacto —medido rgb(34,197,94) en los dos, mismo fondo y mismo borde— porque
   `.chip--on` pinta con `var(--linea)`, que es el color del GRUPO. Un veredicto
   de Calidad no puede depender de que el operario recuerde qué chip apretó.
   El rojo es el semántico que la app ya usa (`.sem--rojo`, `.carga__fill--rojo`)
   y no `--pausa`: el naranja significa «parado» y es el único que lo significa. */
/* El 55% del borde y el rojo pleno del texto NO pasaban: 2,23:1 el borde
   (mínimo 3 para el canto de un control) y 4,15:1 el texto (mínimo 4,5; son
   15-16px/700, muy abajo de «texto grande», así que no aplica el 3:1 relajado).
   Medido componiendo las capas sobre rgb(20,20,23), no a ojo. Con
   --sem-rojo-claro y el borde al 70%: texto 5,63:1 y borde 3,83:1. El fondo
   sigue mezclando el rojo PLENO al 16%: aclararlo también le comería al texto
   el contraste que se acaba de ganar. */
.chip--rechazo.chip--on {
  border-color: color-mix(in srgb, var(--sem-rojo-claro) 70%, transparent);
  background: color-mix(in srgb, var(--sem-rojo) 16%, transparent);
  color: var(--sem-rojo-claro);
}
/* El ↻ de Iniciar se apaga durante el cooldown y no lo decía por ningún lado. */
.chip[disabled] { opacity: var(--dis); cursor: default; }
.chip[disabled]:active { transform: none; }

/* Las pestañas de las zonas son 3 y envolvían a dos filas en 390. En columnas
   iguales entran siempre, en el orden en que están, y el contador queda
   subordinado al rótulo en vez de competir con él. */
/* `minmax(0, 1fr)` y no `1fr`: el mínimo automático de un ítem de grilla es su
   ancho de contenido, así que a 390 las tres pestañas se negaban a encoger y
   empujaban el layout a 409px —el header entero desbordaba y «Resueltas» quedaba
   cortada contra el borde—. Medido en el navegador. */
.pg--plana > .chips { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
/* Pestañas y buscador ANCLADOS, también en celular. Vivían sólo dentro de
   @media(min-width:820px) y el ancho que más lo necesita —una cola de 231
   pendientes recorrida con un dedo— era justo el que no los tenía: medido a
   390×844, dos gestos de scroll (1.500px) dejaban las pestañas en y=-1434 y el
   buscador en y=-1370, o sea fuera de pantalla como una fila más.
   El offset: el header mide 59 y --hdr-h reserva 60, así que -1px los apoya
   exactamente contra su canto (en tablet la misma cuenta da -5, ver el bloque
   de la media query). El hairline de abajo es la señal de «hay más»: sin él las
   filas se cortan a mitad de glifo contra un fondo opaco. */
.pg--plana > .chips, .pg--plana > .busca {
  position: sticky; top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) - 1px);
  z-index: 40; background: var(--bg); box-shadow: 0 1px 0 var(--hair-2);
}
/* Apilados (celular) el buscador se ancla DEBAJO de las pestañas o se le
   montaría encima: 48px es el alto de un `.chip` y la fila de pestañas es
   SIEMPRE una sola (grid-auto-flow: column con tres columnas iguales). El gap
   de --s3 desaparece al pegarse, que es lo que se busca: dos renglones de
   control, no tres. */
.pg--plana > .busca {
  top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) - 1px + 48px);
}
.pg--plana > .chips > .chip > span { font-weight: 500; opacity: .65; }

.kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s2); }
.kpi { padding: var(--s3); border-radius: var(--r-card); background: var(--card); border: 1px solid var(--hair); }
.kpi__l { font-size: var(--t-micro); letter-spacing: .1em; text-transform: uppercase; color: var(--faint); }
.kpi__v { margin-top: var(--s2); font-size: var(--n1); font-weight: 600; line-height: 1;
          font-variant-numeric: tabular-nums; letter-spacing: .015em; }
.kpi__d { font-size: var(--t-meta); font-weight: 500; color: var(--muted); }

.hline { position: relative; display: grid; grid-template-columns: 56px 1fr auto; gap: var(--s3);
         align-items: baseline; padding: var(--s3); }
.hline + .hline::before { content: ""; position: absolute; left: var(--s3); right: 0; top: 0; height: 1px;
                          background: var(--hair); }
.hline__h { font-size: var(--t-meta); font-weight: 700; color: var(--faint); font-variant-numeric: tabular-nums; }
/* Peso 500 como `.row__prod`: es el MISMO dato (nombre de producto) en la
   séptima lista de la app. `.hline__s` baja a 400 explícito o hereda el 500 y
   queda más pesado que su gemelo `.row__meta`. */
.hline__t { font-size: var(--t-fila); font-weight: 500; min-width: 0; letter-spacing: 0.015em; }
/* 17px y no 15: es el MISMO dato («nombre de producto») que el hub, Calidad,
   Depósito y Plan pintan en --t-fila. En el celular las dos listas nunca se
   comparan; en la tablet ocupan el mismo rectángulo del mismo marco fijo, una
   detrás de la otra, y Historial se leía como una pantalla más chica y apagada
   que el resto de la app. */
.hline__s { display: block; font-size: var(--t-meta); font-weight: 400; color: var(--muted); margin-top: var(--s1); }
/* El token que encabeza una meta (el número de OT o de partida) va en 700, como
   en las cinco pantallas donde encabeza la fila. Sin `font-style` ni color
   propio: es la MISMA voz, sólo que reconocible. */
.tok { font-weight: 700; font-style: normal; }
.hline__v { font-size: var(--t-body); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }

.sem--verde { color: var(--curso); }
.sem--amarillo { color: var(--sem-medio); }
.sem--rojo { color: var(--sem-rojo); }
.sem--sobre { color: var(--sem-sobre); }
/* El color solo no alcanza (ni con daltonismo ni con el sol de frente): cada
   semáforo lleva su signo. Con semáforo nulo la clase queda «sem--», sin
   `content`, y un ::before sin content no genera caja. */
/* El glifo (↑ ✓ – ↓) es la redundancia del semáforo contra el color, y sólo
   tiene sentido donde el número ES un semáforo: la fila del Historial y el
   logro del día en la hoja de Terminar. Los `content` estaban SIN scope
   —sólo el margin lo tenía—, así que se colaban a cualquier `.kpi__v` que
   llevara `sem--amarillo`: en pantalla se leía «PARADO –42 min» y «PARADAS –2»,
   o sea menos 42 y menos 2, y encima pegados al número porque el margin ahí no
   aplicaba. Medido también: el guion partía la tarjeta en dos renglones (60px
   de alto contra 30 de sus cinco hermanas). */
.hline__v[class*="sem--"]::before,
.kpi__v--sem[class*="sem--"]::before { margin-right: var(--s1); font-weight: 700; }
.hline__v.sem--sobre::before,   .kpi__v--sem.sem--sobre::before   { content: "\2191"; }
.hline__v.sem--verde::before,   .kpi__v--sem.sem--verde::before   { content: "\2713"; }
.hline__v.sem--amarillo::before,.kpi__v--sem.sem--amarillo::before{ content: "\2013"; }
.hline__v.sem--rojo::before,    .kpi__v--sem.sem--rojo::before    { content: "\2193"; }

/* Hoja modal (elegir operarios, motivo de pausa, cantidad del parcial…) */
.hoja { position: fixed; inset: 0; z-index: 90; display: flex; align-items: flex-end;
        /* `overflow:hidden` + `overscroll-behavior:contain` hacen del velo un
           contenedor de scroll sin sobrante: arrastrar sobre él ya no scrollea
           la lista de atrás y el operario no pierde el lugar donde estaba. Es
           la versión CSS del bloqueo: no toca html/body, así que no puede
           saltar la posición de scroll al cerrar en iOS. */
        overflow: hidden; overscroll-behavior: contain;
        background: rgba(0,0,0,0.55); animation: hoja-fondo 160ms var(--ease-out-quart); }
.hoja__c {
  width: 100%; max-height: 86dvh; overflow-y: auto;
  /* El scroll de la hoja NO se filtra a la página de atrás al llegar al tope o
     al final (el MES lo resuelve igual en .sheet). */
  overscroll-behavior: contain;
  /* Suaviza el autoscroll cuando el foco automático de un input empuja el campo
     fuera de vista con el teclado abierto. Se anula bajo reduced-motion. */
  scroll-behavior: smooth;
  background: var(--bg); border-top: 1px solid var(--hair);
  border-radius: var(--r-card) var(--r-card) 0 0;
  /* Safe-area lateral: en celular apaisado con notch el botón primario caía bajo
     el recorte. El max() no agranda nada donde no hay recorte. */
  padding: var(--s4) max(var(--s3), env(safe-area-inset-right, 0px))
           calc(var(--s4) + var(--safe-b)) max(var(--s3), env(safe-area-inset-left, 0px));
  display: flex; flex-direction: column; gap: var(--s3);
  /* «Hay más abajo»: en 390px la hoja de Terminar desborda y el botón que
     compromete queda bajo el fold sin que nada lo diga. `scrollbar-gutter:
     stable` es OBLIGATORIO: en Blink, estilar ::-webkit-scrollbar convierte la
     barra de overlay en barra clásica que consume ancho, y sin el gutter todo
     el contenido saltaba 6px a la izquierda al tipear el primer dígito (que es
     cuando aparecen los KPI y la hoja pasa a desbordar). */
  scrollbar-gutter: stable;
  /* Sin esto, el campo que se autoenfoca al tocar «Otro…» nace TAPADO por el
     pie anclado: el autoscroll del navegador no sabe que abajo hay ~100px
     pegados. Medido: con 180px el campo queda pegado con 2px de aire; 192 lo
     despega. Vive en la base y ya no en la media query de tablet porque el pie
     ancla en los DOS anchos (ver `.hoja__c .hoja__pie`). */
  scroll-padding-bottom: calc(var(--s4) * 8);
  /* La hoja entra desde donde estaba el dedo. 200ms y sin rebote: es acuse del
     toque, no una animación de presentación. */
  animation: hoja-in 200ms var(--ease-out-quart);
}
.hoja__c::-webkit-scrollbar { width: 6px; }
.hoja__c::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--linea) 40%, var(--hair-2)); border-radius: var(--r-pill);
}
.hoja__c::-webkit-scrollbar-track { background: transparent; }
@keyframes hoja-fondo { from { opacity: 0 } }
@keyframes hoja-in { from { transform: translateY(12px) } }
/* En la tablet la hoja no sube desde el pulgar: aparece a la derecha, lejos de
   donde está el dedo. Sin NADA de movimiento, un rectángulo de 640 px que
   cambia de golpe en visión periférica se pierde justo cuando el operario está
   mirando la máquina. 10px y 180ms: alcanza para que el ojo lo enganche. */
@keyframes hoja-der { from { opacity: 0; transform: translateX(10px) } }
.hoja__t { margin: 0; font-size: var(--t-tit); font-weight: 700; line-height: 1.25;
            text-wrap: balance; }

/* El tirador del gesto: arrastrar la hoja hacia abajo la cierra, como en el MES
   viejo (`_setupSheetDrag`, mes.js). Es la ÚNICA pista de que el gesto existe,
   así que se pinta donde la hoja de verdad se arrastra: el celular, con la hoja
   apoyada contra el canto de abajo. En tablet la hoja es una tarjeta sobre el
   panel derecho y los 20px del tirador se los come una hoja que ya desborda
   (ver el lote de salidas): ahí el gesto engancha desde el TÍTULO, igual que en
   el MES. El margen negativo es el mismo recurso que los `--pegado`: cancela el
   gap de --s3 de `.hoja__c` y deja el tirador a 4px de lo que sigue, así el
   afordance cuesta 8px de alto y no 20. */
.hoja__grip {
  flex: none; width: 40px; height: 4px; border-radius: var(--r-pill);
  background: var(--hair-2); margin: 0 auto calc(var(--s1) - var(--s3));
}

/* Pie de hoja: el par «lo que compromete» + «la salida», como grupo. La regla
   base existe para el CELULAR: sin ella los dos botones quedan pegados (medido,
   16px → 0 a 390×844). */
.hoja__pie { display: flex; flex-direction: column; gap: var(--s3); }

/* Pie ANCLADO. Mismo recurso que `.acts` en el panel y `.atajos` en la columna,
   y por la misma razón: el cuerpo puede crecer y una pantalla táctil no dibuja
   barra de scroll, así que lo que se va abajo no vuelve solo.
   Vivía dentro de `@media (min-width:820px)` y por eso el celular —que es donde
   la hoja desborda MÁS— era justo el que no lo tenía: medido a 390×844, la hoja
   de tarea de apoyo daba 843 de contenido en 725 de caja, «Iniciar la tarea»
   con 30 de sus 56px visibles y «Cancelar» entero fuera de pantalla; la de
   Retomar dejaba «Cerrar» a ~950px del canto. `.hoja__marco > .hoja__pie` lo
   neutraliza en las seis hojas con marco —en cualquier ancho—, porque ahí el
   cuerpo scrollea aparte y el pie ya está fuera del scroll. Eran dos cuando se
   escribió esto (`operarios` y `actividad`); hoy son cinco en `_hojas.html`
   más la de las zonas. */
.hoja__c .hoja__pie {
  position: sticky; bottom: 0; z-index: 5;
  background: var(--bg); padding-top: var(--s2);
  border-top: 1px solid var(--hair-2);
}

/* Hoja partida en dos: cuerpo scrolleable + pie FUERA del scroll.
 * `position: sticky; bottom: 0` deja el pie en el flujo y, siendo el ÚLTIMO
 * hijo, cuando la hoja desborda se despega hacia arriba y TAPA con fondo opaco
 * lo que lo precede: medido en Tarea de apoyo a 1180×820, el pie ocupaba
 * y=605..779 y «Nota (opcional)» (y=623..708) quedaba escondida detrás —
 * `elementFromPoint` en el centro del campo devolvía «Iniciar la tarea».
 * El contrato del marco vale en TODOS los anchos. Estuvo un rato partido —el
 * `min-height: 0` acá y el `flex`/`overflow-y`/`position: static` sólo dentro
 * de @media(min-width:820px)— y esa mitad suelta es la trampa clásica de
 * flexbox que este proyecto ya se comió en `.pg--split > .det > .scroll > *`:
 * el mínimo automático de un flex item es `auto`, y ponerlo en 0 SIN darle
 * overflow deja que el cuerpo se encoja por debajo de su contenido y que el
 * sobrante se pinte abajo del pie opaco, sin scroll que lo alcance.
 * Medido a 390×844 en Tarea de apoyo → «Enfundado», flujo por defecto:
 *   partido  → #ha-nota en top=758.2/bottom=806.2, 154.7px tapados por el pie,
 *              elementFromPoint sobre su centro devolvía «Cancelar», y
 *              `.hoja__c` daba scrollHeight === clientHeight === 725: CERO para
 *              scrollear, con 171px del cuerpo perdidos.
 *   entero   → tras scrollear el cuerpo, #ha-nota en 587.2/635.2, 0 tapados,
 *              elementFromPoint devuelve el input, 171px de scroll REAL.
 * La otra salida (sacar el `min-height: 0` para que scrollee el `.hoja__c`
 * exterior) también deja el campo alcanzable —896 contra 725, mismos 171px—,
 * pero hasta que el operario scrollea el campo sigue pintado DEBAJO del pie
 * opaco, que es exactamente el síntoma que el marco vino a matar; con el
 * contrato entero el cuerpo recorta y nada se pinta nunca abajo del pie. */
.hoja__marco, .hoja__marco > .hoja__cuerpo {
  display: flex; flex-direction: column; gap: var(--s3); min-height: 0;
}
/* El que scrollea es el CUERPO y el pie queda afuera. `min-height: 0` es lo
   que deja encoger al cuerpo dentro del `max-height` de `.hoja__c` en vez de
   desbordarlo. */
.hoja__c > .hoja__marco { flex: 1 1 auto; min-height: 0; }
.hoja__marco > .hoja__cuerpo {
  overflow-y: auto; overscroll-behavior: contain; scroll-behavior: smooth;
  /* Mismo motivo que en `.hoja__c`: estilar ::-webkit-scrollbar convierte la
     barra de overlay en barra clásica que consume ancho, y sin el gutter el
     contenido salta al aparecer. */
  scrollbar-gutter: stable;
}
.hoja__marco > .hoja__cuerpo::-webkit-scrollbar { width: 6px; }
.hoja__marco > .hoja__cuerpo::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--linea) 40%, var(--hair-2)); border-radius: var(--r-pill);
}
.hoja__marco > .hoja__cuerpo::-webkit-scrollbar-track { background: transparent; }
/* Neutralizador del pie anclado de `.hoja__c .hoja__pie` (misma especificidad
   0-2-0: gana por venir después). En una hoja con marco el pie ya está FUERA
   del scroll, así que el sticky no anclaba nada: era lo que tapaba el último
   campo.
   `relative` y no `static`: neutraliza igual al sticky —lo único que importa es
   que deje de ser `sticky`— y encima le da contenedor al desvanecido de acá
   abajo. */
.hoja__marco > .hoja__pie { position: relative; }
/* «Hay más arriba»: el cuerpo del marco recorta con `overflow-y:auto` y en una
   pantalla táctil no se dibuja barra, así que el corte cae a mitad de glifo y
   se lee como un error de renderizado y no como «seguí scrolleando». Lo que
   queda cortado en la hoja de Terminar es la causa OBLIGATORIA, que es lo que
   traba el cierre. Mismo recurso, mismos 24px y mismo degradado tintado al
   fondo que `.acts::before` del panel (nunca a negro puro: sobre --bg #141417
   una sombra negra se lee como suciedad en el vidrio).
   Va FUERA de la caja del pie (`top` negativo) para no comerle el `padding-top`
   ni pisar la hairline que lo separa del cuerpo. */
/* El gemelo del guard de `.pg--split > .det > .scroll > *`, y por la MISMA razón:
   el mínimo automático de un flex item es `auto`, pero `.grp` es `overflow: hidden`
   y eso le baja el mínimo a 0, así que flexbox prefiere APLASTARLO antes que dejar
   desbordar al cuerpo. Medido en la hoja de Retomar con datos reales: mostraba 4 de
   12 OTs en Aerosoles y 4 de 17 en Líquidos, SIN barra de scroll y sin nada que
   avisara que había más — el cuerpo reportaba `scrollHeight === clientHeight`
   porque el desborde nunca llegaba a ocurrir. Con `flex: none` el contenido del
   cuerpo pasa de 595 a 1.966 y las 17 filas quedan alcanzables. CUARTA vez que
   este proyecto se come esta trampa. */
.hoja__marco > .hoja__cuerpo > * { flex: none; }
/* QUINTA vez, y esta vez en el contenedor que se había salvado: las hojas SIN
   marco —«Corregir» de las zonas, que es la misma plantilla para Calidad y
   Depósito— cuelgan el `.grp` DIRECTO del `.hoja__c`, que también es
   flex-column y también tiene el alto acotado (`max-height: 86dvh`), así que el
   `flex-shrink: 1` por defecto lo achica por debajo de su contenido y el
   `overflow: hidden` de `.grp` se lleva las filas sin dejar rastro. Medido a
   390×844 en /calidad → Resueltas → Corregir con un veredicto parcial: el grupo
   daba 207 de alto contra 345 de contenido —138px comidos— y el `.hoja__c`
   reportaba 725 === 725, o sea CERO scroll que los alcanzara: «Retrabajos
   previos» y «Por: <quién>» no existían en pantalla. Se corregía un veredicto
   sin poder ver quién lo había puesto, que es justo lo que el «¿Quién?»
   obligatorio existe para proteger. A 1024×768 eran 90px. Con `flex: none`:
   345/345 en el grupo y 725/863 en la hoja, que recién ahí scrollea. */
.hoja__c > .grp { flex: none; }

/* El buscador de Retomar se queda arriba de su propia lista, en TODOS los anchos.
   Vivía dentro del @media de 820 y en celular se iba con el scroll — justo donde
   una lista de 17 OTs más lo necesita. No se notaba porque hasta el guard de
   arriba la lista no llegaba a scrollear nunca. `.hoja__cuerpo` es el scroller,
   así que el sticky cuelga de él y no del `.hoja__c`. */
.hoja__c .busca--anclada { position: sticky; top: 0; z-index: 5; background: var(--bg); }

.hoja__marco > .hoja__pie::before {
  content: ""; position: absolute; left: 0; right: 0;
  /* --s3 y no --s4: el hueco REAL entre el cuerpo y el pie es 16px, así que con
     24px el degradado se metía 8px DENTRO del cuerpo. Con una tarjeta `.kpi`
     (fondo --card) como último elemento —Terminar al 100% sin causa, Declarar con
     buen logro— eso le cortaba la esquina redondeada con una banda recta. Medido
     por diff de píxeles: x∈[541,1125] y∈[437,461] a 1180×820. */
  top: calc(var(--s3) * -1); height: var(--s3);
  background: linear-gradient(to top, var(--bg), transparent);
  pointer-events: none;
}

/* El atajo «Tomarlo de una OT abierta…» y el campo del producto comparten
   renglón: apilados costaban 56px de una hoja que a 1180×820 desborda 119, y lo
   que se iba abajo del pie era el campo «Nota» del final. `flex-wrap` con base
   220px en el campo los vuelve a apilar solos donde el ancho no alcanza para
   escribir un producto (celular), sin media query. */
.prod-fila { display: flex; flex-wrap: wrap; gap: var(--s2); }
.prod-fila > .btn { flex: none; }
.prod-fila > .busca { flex: 1 1 220px; }

/* La lista del picker de OT dentro de una hoja se acota, pero el tope viejo de
   212px era una MIRILLA: de las 60 filas que el picker pinta entraba UNA
   completa (medido a 1180×820), y elegir la OT es el atajo que además prellena
   la cantidad. La razón que justificaba los 212 —«sin tope empujaba fuera de
   pantalla el campo de operarios y el botón que confirma»— ya no corre: desde
   la Ronda 4 el pie de esta hoja va anclado y no se mueve con el picker
   (verificado). Con 340px entran las 3 OTs de «plan de hoy» completas y el pie
   queda intacto en las tres resoluciones medidas. Va fuera del media query a
   propósito. */
.picker-caja > .grp { max-height: 340px; overflow-y: auto; overscroll-behavior: contain; }

/* ── Voces subordinadas de una hoja ───────────────────────────────────────
 * El problema no era el color: era que el gap de 16px de .hoja__c suelta el
 * subtítulo, el hint y el error del elemento que explican, así que los tres se
 * leen como párrafos sueltos con el mismo peso. El margen negativo cancela ese
 * gap y los deja a 4px de lo suyo.
 * Los modificadores --pegado SÓLO valen como hijo DIRECTO de un contenedor con
 * gap:var(--s3); dentro de los bloques con gap:var(--s2) se usan sin él.
 * .hoja__hint va en --muted y NO en --faint: faint está reservado para
 * encabezados, labels en mayúsculas, chevrons y estados vacíos, y el texto que
 * se estaría degradando es «Pre-cargado con la suma de parciales», que es lo
 * único que evita que el operario sume dos veces la producción del día.
 * .hoja__err NO es una voz nueva: es el renombre de .row__pausa (13/700/naranja),
 * que era una clase de FILA prestada a los errores de las hojas. */
.hoja__s, .hoja__hint, .hoja__err { margin: 0; }
.hoja__s    { font-size: var(--t-body); color: var(--muted); }
.hoja__hint { font-size: var(--t-meta); color: var(--muted); }
.hoja__err  { font-size: var(--t-meta); font-weight: 700; color: var(--pausa); }
.hoja__s--pegado, .hoja__hint--pegado {
  margin-top: calc(var(--s1) - var(--s3));
}
/* El renglón que dice QUÉ FALTA explica al botón de arriba —el que se apaga—,
   no al «Cancelar» de abajo, y a 16px de los dos se lee como un tercer párrafo
   suelto del pie. Es el mismo `--pegado` de acá arriba aplicado por POSICIÓN y
   no por clase: la clase la tendría que poner el template, y este hint es el
   único de la app que va DEBAJO de lo que explica (el de la rama del corte
   parcial va arriba de su botón, y ese `+` no lo alcanza).
   Los 12px no son cosméticos: en la tablet del piso son los que faltaban para
   el «52%». Con `.par-acc` en dos columnas el pie baja de 404 a 334 y el logro
   entra con 24 de sus 30px —cortado a mitad de dígito, que es exactamente lo
   que este repo llama error de renderizado—; con el hint pegado el pie queda en
   322, el cuerpo en 292 y el número entra entero. */
.hoja__pie > .par-acc + .hoja__hint { margin-top: calc(var(--s1) - var(--s3)); }
/* El renglón que nombra el CRITERIO de orden de una lista (hoy: «Primero, lo que
   más se usa en este grupo», bajo el `.sect__h` del catálogo de apoyo). Mismo
   recurso que los `--pegado`, con la escala de un encabezado de sección: el
   margen negativo se colapsa contra los 8px que suelta el `.sect__h` y deja los
   4px de «esto explica lo de arriba», y el de abajo repone esos 8px antes de la
   tarjeta para que el aire de la lista no cambie. Va DESPUÉS del `margin: 0` de
   arriba, que si no le gana por orden. */
.sect__crit { margin-top: calc(var(--s1) - var(--s2)); margin-bottom: var(--s2); }
/* El error de cantidad va ARRIBA del campo y no debajo: en el celular el propio
   campo abre el teclado, y ahí abajo no hay pantalla — medido a 390×844, el
   número quedaba en y=469 y «Poné cuántas unidades se hicieron.» en y=537,
   contra un viewport útil de ~508 con el teclado abierto. El operario ve el
   borde naranja del campo y ninguna palabra que le diga qué falta.
   Y ocupa su renglón SIEMPRE (`visibility`, no `display`): apareciendo de golpe
   empujaba el campo ~25px hacia abajo justo mientras el dedo tipea. El margen
   negativo lo deja a 4px del campo que explica, como los `--pegado`. */
.hoja__err--sitio { visibility: hidden; margin-bottom: calc(var(--s1) - var(--s3)); }
.hoja__err--sitio.hoja__err--hay { visibility: visible; }

/* Resumen de contexto: los números que el operario necesita ANTES de contestar.
   Deliberadamente NO es una lista de .drow (56px cada una). Sólo se usa en la
   hoja de Iniciar, que es corta; la de Terminar ya desborda y no puede pagarlo.
   Los pares etiqueta/valor van con x-show y NUNCA con x-if: el clon de x-if
   entra como hermano del <template> y mezclar los dos mecanismos dentro de un
   grid de dos columnas es exactamente el terreno donde ya se pisó esa trampa. */
.resumen {
  display: grid; grid-template-columns: 1fr auto; gap: var(--s1) var(--s3);
  padding: var(--s2) var(--s3); border-radius: var(--r-btn);
  background: var(--card); border: 1px solid var(--hair);
}
.resumen__k { font-size: var(--t-meta); color: var(--muted); }
/* En --t-body y no en --t-meta: son los números que decide el operario a 60cm. */
.resumen__v { font-size: var(--t-body); font-weight: 700; text-align: right;
              font-variant-numeric: tabular-nums; }

/* Segunda pregunta (submotivo de pausa, razón del corte parcial): un cuarto
   aparte DENTRO de la hoja, no una pantalla nueva. El color-mix va sobre --card
   y NO sobre transparent: hundir en vez de aclarar, y así no se confunde con
   .aviso--warn, que es pausa 12 % sobre el fondo de la página.
   OJO: esta clase REEMPLAZA dos style="display:flex;flex-direction:column;
   gap:var(--s2)" inline que eran load-bearing — declara exactamente lo mismo. */
.subpanel {
  display: flex; flex-direction: column; gap: var(--s2);
  padding: var(--s3); border-radius: var(--r-card);
  background: color-mix(in srgb, var(--pausa) 8%, var(--card));
  border: 1px solid color-mix(in srgb, var(--pausa) 28%, transparent);
}

/* La causa obligatoria por bajo logro no es un campo más de la hoja: es lo que
   falta para poder cerrar, y por eso vive en el PIE. La hairline que la separa
   del cuerpo es LA DEL PIE, teñida: dice qué falta sin agregar una palabra y
   sin sumarle una segunda línea a 9px de la primera, que es lo que pasaba con
   la caja trayendo su propio borde.
   MISMO caso: reemplaza un inline display:flex;flex-direction:column;gap:s2. */
.hoja__req { display: flex; flex-direction: column; gap: var(--s2); }
/* Sin `:first-child`: Alpine inserta el clon del `x-if` como HERMANO del
   `<template>`, así que la causa nunca es el primer hijo del pie. */
.hoja__c .hoja__pie:has(> .hoja__req) {
  border-top-color: color-mix(in srgb, var(--pausa) 35%, var(--hair-2));
}
/* ANCLADA, pero ya no con `sticky` DENTRO del cuerpo: desde el 2-sep la causa
   vive en el `.hoja__pie`, que es hermano real del cuerpo y no puede
   superponerse a nada, así que no necesita regla de anclaje propia. El
   sticky-bottom resolvía la mitad del problema —los cuatro chips quedaban
   abajo del recorte, medido a 1180×820 con la 12841— y creaba la otra: un
   bloque OPACO de 154px flotando sobre un scroller de 358 tapaba al 100% el
   campo de la cantidad (12250 a 1024×768 y a 390×844, `elementFromPoint` sobre
   el centro del campo devolvía `DIV.chips`). A 1440×900 el cuerpo da 560 y no
   pasa: por eso la medición de la ronda, hecha a 1180×820, no lo cazó.
   El «hay más arriba» lo pone `.hoja__marco > .hoja__pie::before`, que ya
   existía: las dos reglas `.hoja__marco > .hoja__cuerpo > .hoja__req` que
   anclaban esto salieron el 2-sep porque, con la causa en el pie, no matchean
   ningún elemento en las dos hojas que la montan. */

/* El par «la otra salida» + «la que compromete», lado a lado. Estaba dentro del
   @media de tablet y con `minmax(220px)`: dos pistas piden 220+16+220 = 456 y
   el `.par-acc` de 1024×768 mide 452 —508 de hoja − 2 de borde − 48 de
   padding-inline − 6 del scrollbar-gutter—, así que por CUATRO píxeles la
   tablet del piso caía a UNA columna. El pie de «Terminar OT» se iba a 404px
   sobre un cuerpo de 209 y el «52%» del logro —el número que justifica la causa
   que ese mismo pie está pidiendo— quedaba con 0px visibles (OT 12763, medido).
   A 1180 y a 1440 el `.par-acc` mide 584 y ahí SIEMPRE fueron dos columnas: por
   eso la ronda que lo midió a 1180×820 no lo cazó.
   Y abajo de 820 la regla no existía: `.par-acc` era un `display:block` y los
   dos botones quedaban PEGADOS, 0px entre el que cierra la OT y el que no
   (390×844). Baja a la BASE y los cubre a los dos.
   minmax(160px): dos pistas piden 336, que entran en los 452 de la tablet
   (218px por botón) y en los 352 del celular (168), y NO entran en los 320 que
   mide el `.par-acc` de una tablet de 820 —ahí serían 152px por botón y el
   rótulo más ancho que puede compartir renglón mide 142,04px medidos sobre
   roboto-700 a 17px («Cerrar sin declarar»): la etiqueta con un borde, no un
   botón—. Con 160 el peor caso conserva 14px de aire y nunca parte en dos.
   auto-fit y NO `1fr 1fr`: cuando lo declarado completa la OT, «↻ Sigue
   después…» no se renderiza y el que queda tiene que ocupar el renglón entero
   (y `:only-child` no sirve: los `<template>` de Alpine son hijos igual).
   El column-gap va en --s3 y no en --s2: son «↻ Sigue después…» (tenue) y
   «✓ Terminada igual» (verde), la decisión más cara de la hoja, y a 8px
   quedaban a la mitad de lo que la app usa entre el botón que compromete y su
   salida (16px, medido en la hoja de actividad). */
.par-acc { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--s2) var(--s3); }
/* El cartel del segundo toque no es una de las dos opciones: es lo que hay que
   leer ANTES de tocarlas. Sin esto cae en una celda al lado de un botón (284px
   a 1440 hoy, 168 en el celular en cuanto la grilla baja a la base) y la
   advertencia que nombra lo que se está aceptando se lee como una tercera
   acción. Mismo recurso que `.ops > .vacio` y `.grp--cols2 > .vacio`. */
.par-acc > .aviso--warn { grid-column: 1 / -1; }

/* Los dos números del cierre no pesan igual: el LOGRO DEL DÍA dispara la causa
   obligatoria, el AVANCE DE LA OT sólo dice si queda saldo. Con dos columnas
   iguales y la misma cifra había que compararlos leyendo las etiquetas.
   La jerarquía se construye con PROPORCIÓN, CAJA y COLOR: los dos valores
   siguen en --n1. Agrandar el primario sería el «KPI hero» que PRODUCT.md
   rechaza por nombre. */
.kpis--cierre { grid-template-columns: 3fr 2fr; }
.kpis--cierre .kpi--sec { background: none; border-color: transparent; padding-left: 0; }
.kpis--cierre .kpi--sec .kpi__v { color: var(--muted); }

/* Panel de detalle vacío: UNA instrucción, en --muted, legible a 60cm.
   Sin ícono ni segunda línea: PRODUCT.md principio 4 es «reconocer, no leer». */
.vacio-p { margin: 0; max-width: 26ch; text-align: center;
           font-size: var(--t-fila); color: var(--muted); }
/* Grilla y NO flex: ver el comentario largo de `.hoja__c .chips` acá abajo. En
   la botonera de operarios el desparejo era 189px por nombre contra 288 de
   «Ver los otros (6)» y «Otro…», que son justo los dos que NO son una persona. */
.ops { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: var(--s2); }
/* El cartel de «no hay operarios sugeridos» es un hijo más de `.ops`: en flex
   se estiraba solo, en la grilla le tocaría UNA celda de 148px. */
.ops > .vacio { grid-column: 1 / -1; }
.op {
  min-height: 48px; padding: var(--s2) var(--s3); border-radius: var(--r-btn); cursor: pointer;
  background: var(--card); border: 1px solid var(--hair); color: var(--txt);
  font-size: var(--t-body); font-weight: 500; line-height: 1.25;
  display: inline-flex; align-items: center; justify-content: center; text-align: center;
}
.op--on { border-color: color-mix(in srgb, var(--linea) 60%, transparent);
          background: color-mix(in srgb, var(--linea) 18%, transparent); color: var(--linea); font-weight: 700; }
/* Ya está en otra tarea AHORA. Marca de disponibilidad, no de error: el botón
   sigue vivo y se puede elegir igual (una persona puede arrancar una máquina y
   pasar a etiquetar mientras corre) — el que dice qué pasa es el aviso naranja
   del pie, que nombra a quién y dónde.
   Punto naranja arriba a la derecha y NO un tinte de fondo: el fondo ya lo usa
   `--on` para «elegido», y los dos estados se combinan —se puede elegir a
   alguien ocupado— así que tienen que poder verse a la vez. Por eso el
   `.op--on.op--ocupado` no necesita ninguna regla extra.
   `::after` y no un `<span>`: el botón pinta su nombre con `x-text`, que
   REEMPLAZA todo el contenido — un hijo real se borraría en el primer render. */
.op--ocupado { position: relative; }
.op--ocupado::after {
  content: ""; position: absolute; top: 6px; right: 6px;
  width: 8px; height: 8px; border-radius: var(--r-pill);
  background: var(--pausa);
}

/* Las botoneras de la hoja son LA decisión del piso —motivo de pausa, quién
   está, motivo de rechazo—, con etiquetas de largo muy desparejo («Limpieza» /
   «Problema técnico / falla»). Envueltas al ancho del texto quedaban en escalera
   y el ojo tenía que buscar cada opción. Repartidas en columnas iguales cada
   renglón queda lleno, el ancho es predecible y la etiqueta larga parte en dos
   líneas dentro de su caja en vez de correr la de al lado.
   GRILLA y no `flex: 1 1 148px`: con flex, el último de un renglón incompleto
   se comía todo el sobrante y el tamaño pasaba a decir algo que no era. Medido
   en «Terminar» con causa obligatoria: los tres motivos en 189px y «Otros…»
   solo en su renglón, en 584 — 3,09× más grande que sus hermanos, y es la
   opción MENOS informativa de la botonera. Con auto-fit todos miden igual y el
   huérfano se queda en su columna.
   `.chips--veredicto` queda AFUERA: sus tres opciones son excluyentes y de
   consecuencia opuesta, y el `margin-left` con el que se separa «Rechazar todo»
   no tiene sentido dentro de una celda de grilla. */
.hoja__c .chips:not(.chips--veredicto):not(.chips--nums) {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: var(--s2);
}
/* La otra excepción, y por el motivo contrario al de `--veredicto`: los atajos
   de cantidad de la hoja de parcial no son una DECISIÓN del piso, son teclas.
   En la grilla de 148px, «+10» medía 168px y pesaba lo mismo que «Lo que falta
   · 770», que es el atajo que resuelve la carga entera de un toque. Acá el
   tamaño sí tiene que decir algo, y lo que dice es cuánto texto hay.
   El mínimo de 84 es para que las tres teclas cortas sigan siendo un blanco de
   guante y no tres cuadraditos. */
.hoja__c .chips--nums { display: flex; flex-wrap: wrap; gap: var(--s2); }
.chips--nums > .chip { min-width: 84px; }

.num {
  width: 100%; min-height: 64px; padding: 0 var(--s3); border-radius: var(--r-btn);
  background: var(--card); border: 1px solid var(--borde-ctrl); color: var(--txt);
  font-size: var(--n2); font-weight: 600; text-align: center;
  font-variant-numeric: tabular-nums; letter-spacing: .015em;
}
/* Sin spinners nativos: las flechitas de Chrome se comen el borde derecho del
   número de 32px centrado y no aportan nada en piso (se carga con el teclado
   numérico, inputmode="numeric"). Es lo mismo que hace el MES. */
.num { -moz-appearance: textfield; appearance: textfield; }
.num::-webkit-outer-spin-button,
.num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.num:focus { border-color: color-mix(in srgb, var(--linea) 70%, transparent); }
/* Va después de :focus para que el aviso de cantidad inválida no se pierda
   justo cuando el operario está tipeando. */
.num--mal, .num--mal:focus { border-color: var(--pausa); }

/* ── Tablet y tablet montada: rail de secciones · lista · panel ─────────────
 * El mockup aprobado (hub-tablet.dc.html) ya ponía la lista y el detalle lado a
 * lado y ya scrolleaba cada columna por su cuenta; la implementación lo cambió
 * por scroll de página + sticky. Esta pasada recupera el scroll interno y,
 * además, para la navegación de costado: la nav de tres ítems ES el rail de
 * 72px, y el panel derecho pasa a ser el lugar ÚNICO donde aparecen el detalle
 * y todas las hojas.
 *
 * El reparto del ancho, con los números a la vista:
 *   820  = 72 rail + 24 + 300 lista + 24 + 376 panel + 24
 *   1024 = 72 rail + 24 + 372 lista + 24 + 508 panel + 24
 *   1180 = 72 rail + 24 + 372 lista + 24 + 640 panel (tope) + 24 de sobra
 * La lista cede 72px hasta 1024 —y NO el panel— porque el panel a 820 ya medía
 * 376 y el pedido es que ese panel se aproveche, no que se achique. Con 300px
 * la fila entra: el círculo de 48 arranca en 74 y quedan 226 para número +
 * producto + meta, más de lo que tiene la fila del celular a 390. */
@media (min-width: 820px) {
  :root {
    /* ── La escala de texto sube un paso ────────────────────────────────
       No es gusto: es distancia. El celular se mira a ~35 cm en la mano; la
       tablet vive MONTADA en un soporte, a 50-60 cm. Con la misma escala, un
       rótulo de 11px a 55cm subtiende lo mismo que un 7px a 35cm, y los que se
       quedaban abajo eran justo los estructurales: el rótulo del KPI, el
       encabezado de sección y la etiqueta del rail, que es la única navegación
       que existe en esta pantalla. Los tres van además en --faint.
       DESIGN.md ya lo pedía y el CSS no lo cumplía: «Acción 17px, 18px en
       tablet» y «Número secundario 26px / 30px». Se redefinen los TOKENS, no
       las reglas: cero literales de font-size nuevos, y las 40 reglas que los
       consumen suben solas. */
    --t-micro:  12px;
    --t-meta:   14px;
    --t-body:   16px;
    --t-fila:   18px;
    --n1:       30px;

    --hdr-h: 64px;
    /* No hay nav inferior de acá para arriba. En 0 se reacomodan solos los
       cuatro lugares que le reservaban alto: el padding de .pg, el alto del
       panel, la franja de «volver al estado real» y el toast. */
    --nav-h: 0px;
    --rail: 72px;
    --lista: 300px;
    /* Borde izquierdo del panel. Lo comparten el grid del hub y la hoja modal:
       es lo que hace que la hoja caiga EXACTAMENTE sobre el panel. */
    --panel-l: calc(var(--rail) + var(--s4) + var(--lista) + var(--s4));
  }

  /* El header sigue cruzando entero arriba y NO se corre: el punto de color y
     el nombre del grupo quedan justo encima del rail. */
  .hdr { padding-left: var(--s4); padding-right: var(--s4); }

  /* /calidad, /deposito y /tableros son otras páginas y NO tienen rail: por eso
     el sangrado del rail va en :not(.pg--plana). Las dos declaraciones son
     necesarias: sin la primera, .pg--plana caería al padding de 16 de la base
     a la izquierda y quedaría con 24 a la derecha. */
  .pg { padding-left: var(--s4); padding-right: var(--s4); }
  .pg:not(.pg--plana) {
    padding-left: calc(var(--rail) + var(--s4));
    padding-bottom: calc(var(--s4) + var(--safe-b));
  }
  /* MISMA especificidad base que `.con-real .pg`: sin re-declararlo la franja
     «Volver al estado real» tapa la última fila. Se EXCLUYE .pg--split: con
     height:100dvh ese padding extra achicaría la grilla 42px y las dos columnas
     saltarían en el instante del toque, en una tablet montada. */
  .con-real .pg:not(.pg--plana):not(.pg--split) {
    padding-bottom: calc(var(--franja) + var(--safe-b) + var(--s2));
  }
  /* En el split el hueco se reserva DENTRO del scroll de la lista, que es lo
     único que la píldora tapa ahora que está pegada al rail: cambia el alto del
     contenido scrolleado, no el layout. Cero salto. */
  .con-real .pg--split > .col > .scroll { padding-bottom: calc(var(--franja) + var(--s2)); }
  /* El tope de la medida para las vistas que quedan en UNA columna de texto.
     Ya no son las cuatro que decía este comentario: Historial, Plan e Iniciar se
     escapan con `.scroll--ancho` porque se parten en dos columnas, y el menú de
     Apoyo también se parte (`.grp--cols2`) aunque su `.scroll` siga topado —lo
     que lo acota de verdad es este tope, y con 449px por columna sigue leyéndose
     bien—. Sin tope las filas corren 1.130px (medido a 1180×820) y el ojo pierde
     el renglón entre el rótulo y el valor. */
  .pg:not(.pg--plana):not(.pg--split) > .scroll { max-width: 900px; }

  /* ── La nav se para de costado ──────────────────────────────────────────
     Mismo componente, mismos tres ítems, mismo orden, mismo ícono, misma
     etiqueta y mismo activo con el color del grupo: lo único que cambia es
     dónde está. Vive fuera de todos los x-show, así que sobrevive a las seis
     vistas. Arranca en top:0 y el hairline corre de arriba a abajo POR DETRÁS
     del header (que es opaco): con top:--hdr-h quedaba una muesca de 5px entre
     los dos hairlines, porque el header mide 59 y --hdr-h reserva 64.
     z-index 45 (y no 60) justamente para pasar por debajo del header. */
  .nav {
    top: 0; bottom: 0; left: 0; right: auto; width: var(--rail); z-index: 45;
    flex-direction: column; justify-content: flex-start;
    border-top: none; border-right: 1px solid var(--hair);
    padding: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + var(--s2))
             var(--s1) calc(var(--s2) + var(--safe-b));
  }
  /* En columna, el `flex: 1 1 0` de la barra de abajo estiraría cada ítem a un
     tercio del alto de la pantalla. 64 de alto × 64 de ancho útil: sobra el
     mínimo de nav (54) y la etiqueta «Historial» entra. */
  .nav__btn { flex: none; width: 100%; max-width: none; min-height: 64px; }
  /* En Iniciar y en Tarea de apoyo el rail se queda: no tapa nada y sacarlo
     hacía desaparecer la única navegación de la pantalla. */
  .nav--oculta { display: flex; }

  /* ── Hub: lista | panel, cada uno con su scroll ─────────────────────────
     Con scroll de página, recorrer la lista movía las cuatro acciones del
     panel: en una tablet montada eso es apuntarle a un botón que se corre.
     .col NO lleva overflow:hidden a propósito: «Pausar el grupo» y «Reanudar
     todo» son hijos directos suyos y su anillo de foco quedaría recortado. El
     contenido se contiene igual porque .scroll es el único que crece. */
  .pg--split {
    display: grid; grid-template-columns: var(--lista) 1fr; gap: var(--s4);
    align-items: stretch; height: 100dvh; min-height: 0; overflow: hidden;
  }
  .pg--split > .col { display: flex; flex-direction: column; gap: var(--s3);
                      min-width: 0; min-height: 0; position: relative; }
  /* El pie de la lista se corta A MITAD DE TARJETA y una pantalla táctil no
     muestra barra de scroll: medido a 1280×800 con el día andando, la columna
     esconde 109px y lo último que se ve es media fila «42 / 42 u» partida por
     el canto del scrollport, que se lee como un error de renderizado y no como
     «hay más». Es EXACTAMENTE el problema que el panel ya resuelve con
     `.acts::before`, y va el mismo recurso: degradado tintado al fondo (nunca a
     negro puro sobre --bg #141417, que a 50 cm se lee como suciedad en el
     vidrio).
     z-index 4 y no 6: `.atajos` es sticky con z-index 5 y fondo opaco, así que
     cuando está anclado abajo el desvanecido tiene que quedar DETRÁS —ahí no
     hay nada cortado— y no atenuando «Iniciar otra OT».
     El sangrado de --s2 a los lados empata el `margin-inline` negativo de
     `.scroll`, que es 8px más ancho que la columna. */
  .pg--split > .col::after {
    content: ""; position: absolute;
    left: calc(var(--s2) * -1); right: calc(var(--s2) * -1); bottom: 0;
    height: var(--s4); z-index: 4; pointer-events: none;
    background: linear-gradient(to top, var(--bg), transparent);
  }
  /* El sangrado negativo le da aire al anillo de foco de la primera y la última
     fila, que si no se corta contra el borde del contenedor. Va en la escala
     (--s2 = 8) y no en el 6 del mockup: el anillo mide 4 —outline 2 + offset 2—
     así que 8 lo cubre igual, y el margen negativo con el padding positivo se
     cancelan, o sea que mover 6→8 no corre un solo píxel del contenido. */
  .pg--split > .col > .scroll {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    margin-inline: calc(var(--s2) * -1); padding-inline: var(--s2); padding-bottom: var(--s2);
  }
  /* OBLIGATORIO, y su ausencia era un bug silencioso: `.scroll` es flex-column y
     acá pasó a tener alto acotado, así que sus hijos —que por defecto son
     `flex-shrink: 1`— se APLASTAN para entrar en vez de dejar scrollear. Medido
     con 4 OTs en curso y 8 planificadas a 1180x820: la tarjeta de los atajos
     quedaba de 40 px en vez de 160, y como `.grp` es `overflow: hidden` sus
     filas se recortaban sin dejar rastro. Le pasaba a TODAS las tarjetas de la
     columna, no sólo a los atajos. En el celular no existía porque ahí `.scroll`
     no tiene alto acotado: es deuda que trajo el scroll por columna. */
  .pg--split > .col > .scroll > * { flex: none; }
  /* ── Los dos atajos, anclados al pie de la columna ──────────────────────
     Medido con un día cargado (4 OTs en curso + 8 planificadas) a 1180x820:
     los dos «+» caían en y=1131 con el fold de la columna en 796 —fuera de
     vista, con 676 px escondidos— y la columna NO muestra barra de scroll en
     una pantalla táctil, así que nada le dice al operario que ahí abajo hay
     algo. Antes de que la lista tuviera scroll propio scrolleaba la página
     entera y aparecían solos: esto es deuda del layout de tablet, no del hub.
     `sticky` y no un hijo aparte de `.col`: con sticky el DOM no se mueve, así
     que el celular —donde la página scrollea entera y el problema no existe—
     queda EXACTAMENTE igual. Cuando la lista es corta el bloque se queda en su
     lugar natural; sólo se despega cuando iba a salirse por abajo.
     El fondo opaco es obligatorio: sin él las filas de «Terminadas hoy» pasan
     por debajo y se leen encima del texto de los atajos. */
  .pg--split > .col > .scroll > .atajos {
    position: sticky; bottom: 0; z-index: 5;
    background: var(--card);
    /* Era la ÚNICA sombra del archivo y era negra sobre `--bg` negro: a 50 cm,
       con reflejos de tubo, no se lee como elevación sino como suciedad en el
       vidrio. Se reemplaza por el hairline con el que el resto del sistema
       separa superficies, más un desvanecido tintado al fondo (no a negro puro)
       que sólo insinúa que abajo sigue habiendo lista. */
    border-top: 1px solid var(--hair-2);
    box-shadow: 0 -8px 12px -10px var(--bg);
  }

  .pg--split > .det {
    min-width: 0; min-height: 0;
    /* Sin tope el panel se estiraba hasta 670px y el botón primario con él: una
       franja verde de esa lonjitud ya no se lee como un botón. */
    max-width: 640px;
    /* Las DOS canaletas iguales, y las mismas que las de la hoja que cae encima:
       con padding-left 24 y padding-right 8 el contenido del panel arrancaba en
       541 y el de la hoja en 533, así que el botón que compromete saltaba 8px al
       abrirse la hoja sobre el mismo rectángulo. El residuo de 7px que queda es
       el hueco del scrollbar de la hoja, no un error. */
    padding-inline: var(--s4); border-left: 1px solid var(--hair);
    overflow-y: auto; overscroll-behavior: contain;
    /* El sangrado negativo se conserva para los anchos intermedios (1024-1180),
       donde el panel todavía no llegó a su tope de 640 y el anillo de foco de la
       última fila se cortaba contra el borde. A 1180 no hace nada: medido. */
    margin-right: calc(var(--s4) * -1);
    /* El recorte del scrollport arranca 11px por debajo de la hairline del
       header y ahí los glifos se cortaban en el aire. El margen negativo sube el
       tope hasta la hairline y el padding repone el espacio: en reposo no se
       mueve un píxel, y el `.acts` anclado al pie queda donde estaba. */
    margin-top: -11px; padding-top: 11px;
  }
  /* El panel vacío deja de necesitar min-height: ahora estira con el grid. */
  .det--vacio { align-items: center; justify-content: center; }
  /* El cuerpo del panel vacío: la frase arriba, el resumen del turno abajo, los
     dos centrados en el rectángulo. 420px y dos columnas para que las cuatro
     cifras se lean como un bloque y no como una lista. */
  .det__vac { display: flex; flex-direction: column; align-items: center;
              gap: var(--s5); width: 100%; max-width: 420px; }
  .det__vac .kpis { width: 100%; grid-template-columns: repeat(2, 1fr); }
  /* `.det__vac` es flex column con align-items:center, así que el esqueleto
     necesita el ancho explícito o queda encogido contra el centro. */
  .det__vac .skel--panel { width: 100%; }

  /* Las pestañas y el buscador van lado a lado: apilados cuestan 64px de alto
     en una pantalla que es una COLA de 195 pendientes, y entran con 176px de
     sobra. Medido: la primera fila sube de y=199 a y=135, las pestañas se
     achican de 520 a 455 (151px cada una, siguen leyéndose como grupo de tres)
     y el buscador queda en 301px a 820. */
  .pg--plana { display: grid; grid-template-columns: auto 1fr; align-content: start; }
  .pg--plana > .chips { grid-column: 1; }
  .pg--plana > .busca { grid-column: 2; justify-self: start; margin-left: var(--s3); }
  /* Anclados: en una cola de 195 los controles se perdían a dos gestos de
     scroll y volver de otra pestaña te dejaba en la fila 1 sin las pestañas a
     la vista. El -5px cierra la muesca por la que se veían pasar las filas: el
     header mide 59 y --hdr-h reserva 64. */
  /* El hairline es la señal de «hay más»: sin él las filas se cortan a mitad de
     glifo contra un fondo opaco y la franja no se lee como borde. Va en los DOS
     y no sólo en las pestañas —como pedía el hallazgo— porque desde la ronda 3
     comparten renglón: una línea que muere a los 520px parte la franja al medio.
     El `position: sticky` en sí ya viene de la base (ahora el celular también
     lo tiene): acá sólo cambia el offset, porque --hdr-h pasa a 64 y el header
     sigue midiendo 59. */
  /* El inset va sumado, como en las otras seis declaraciones que anclan contra
     `--hdr-h`: el header CRECE con el notch (`.hdr` lo suma a su padding-top) y
     el viewport es `viewport-fit=cover` con la barra de estado translúcida, así
     que sin el término los chips —48px de alto— se meten `inset` píxeles por
     debajo de un header que además tiene z-index mayor. Donde el inset es 0
     resuelve al mismo valor de hoy. */
  .pg--plana > .chips, .pg--plana > .busca {
    top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) - 5px);
  }
  .pg--plana > .scroll { grid-column: 1 / -1; }

  .pg--plana > * { max-width: 900px; }
  /* La lista de la bandeja no: es lo único de la página que gana con el ancho.
     A 1180 pasa de 900 —con 256px muertos al costado— a 1132 en dos columnas.
     Medido antes: /calidad con 195 pendientes daba 8.480px de scroll y 6,4
     filas por pantalla, la misma densidad que el celular en una pantalla con
     2,9× de área. Especificidad (0,2,0) para ganarle al `> *` de arriba. */
  .pg--plana > .scroll { max-width: none; }
  /* Los controles siguen siendo controles: un buscador de 900px no es un campo,
     es una franja, y el dedo no sabe dónde apuntar. */
  .busca { max-width: 420px; }
  /* …pero ese tope es del buscador de PÁGINA (`.pg--plana > .busca`), y `.busca`
     no es «buscador»: es el estilo de campo de texto, y lo usan los campos de
     TODAS las hojas. Adentro de una hoja frenaban 164px antes que sus hermanos:
     medido en «¿Cuánto se hizo?», el número iba de x=541 a 1125 y la Nota justo
     debajo terminaba en 961. El buscador anclado de Retomar también entra acá a
     propósito: pintado en var(--bg) y pegado arriba, con 420 de ancho dejaba
     pasar las filas por los 164px que le sobraban al costado. */
  .hoja__c .busca { max-width: none; }
  /* Las pestañas no: estiradas a 900 cada una queda de 300px y deja de leerse
     como un grupo de tres. */
  .pg--plana > .chips { max-width: 520px; }

  /* El cartel de MES caído ocupa la pantalla entera y era la tarjeta de 290px
     del celular, con su única acción en 48px mientras todo `.btn` de tablet mide
     62. El `width: 100%` no es adorno: sin él el max-width no mueve nada.
     La ESCALA deja de ser exclusiva del camino de pantalla completa. Medido a
     1180×820 cortando con 502 el /datos/* de cada pantalla: por `.caido--pg`
     (hub, /calidad, /deposito) el cartel daba 560×210 con titular de 28px y
     botón de 62; el MISMO componente inline (Historial, Plan, Iniciar) quedaba
     en 420×208 con titular de 20 y botón de 48 — la misma pregunta en dos
     tamaños según por dónde se cayó. Se saca el scope y NO se unifica la
     geometría: el camino de pantalla completa tapa todo porque no hay app que
     mostrar (0 links medidos) y el inline conserva header y rail a propósito,
     que es la única salida que queda en pantalla. Alcanza también —a propósito,
     es el mismo componente— a «No hay grupos para elegir» y «No hay líneas para
     elegir». Y el scope baja a `.caido` en vez de irse a `.caido__b` pelado por
     el botón suelto de `.aviso--accion` (la hoja de Retomar): ahí 62px de alto
     se los come una hoja que ya desborda. */
  .caido { width: 100%; max-width: 560px; }
  .caido .caido__t { font-size: var(--t-tit-xl); }
  .caido .caido__b { min-height: 62px; }
  .det__h { font-size: var(--t-tit-xl); }
  .det__s { margin-top: var(--s2); font-size: var(--t-body); gap: var(--s2); }
  .cifra__v { font-size: var(--n4); }
  .cifra__d { font-size: var(--t-tit); }
  .cifra__bar { margin-top: var(--s4); height: 10px; }
  .pausa-linea { font-size: var(--t-fila); }
  .pausa-linea__c { font-size: var(--n2); }
  .btn { min-height: 62px; }
  /* Centra verticalmente el cuerpo del panel cuando NO lo llena: el paso 1 de
     Iniciar son tres líneas y el menú de Apoyo once, y pegados al encabezado
     dejaban medio panel en negro —que a 50 cm se lee como una pantalla que no
     terminó de cargar—. Con el catálogo entero no queda sobrante y el
     `margin-block: auto` no hace nada, así que no hay dos comportamientos que
     mantener. Reemplaza a `.pg--menu > .scroll`, de cuando estas dos pantallas
     eran páginas. */
  .pg--split > .det > .scroll--menu { margin-block: auto; }
  /* EL MISMO guard que `.pg--split > .col > .scroll > *`, y por la misma razón:
     `.det` es flex-column con alto acotado, así que sus hijos —flex-shrink 1 por
     defecto— se APLASTAN para entrar en vez de dejar scrollear. `.grp` es
     `overflow: hidden`, o sea que su mínimo automático es 0 y se recorta sin
     dejar rastro. En la columna esto costó medirlo con 4 OTs y 8 planificadas;
     acá no es teórico ni un poco: el catálogo de Iniciar son 53 filas adentro
     de un panel de 740px de alto. */
  .pg--split > .det > .scroll,
  .pg--split > .det > .scroll > * { flex: none; }

  /* ── El encabezado del panel se queda a la vista ─────────────────────────
     El catálogo de Iniciar son 53 filas y el panel scrollea solo: sin esto, el
     ✕ —la ÚNICA salida en tablet, porque ahí el ← del header no se dibuja— se
     va de pantalla al primer gesto y no hay forma de cerrar sin tocar una fila
     de la lista.
     El par margen/padding de 11px repone el sangrado que `.det` se hace hacia
     arriba para que su scrollport arranque en la hairline del header: sin él,
     el encabezado SALTA esos 11px en el primer scroll y después se queda. El
     fondo opaco es obligatorio —las filas pasan por abajo— y es --bg, el mismo
     del panel, no --card: acá no hay una superficie nueva. */
  /* Los 11px de sangrado del panel se MUDAN al encabezado (`det--hdr` los saca
     de `.det` y esta regla los repone adentro). No es cosmética: con el padding
     en `.det`, el sticky se pega 11px por debajo del tope del scrollport —la
     restricción es sobre el MARGIN box, así que un `margin-top: -11px` acá lo
     empuja para abajo en vez de subirlo— y por esa franja se veían pasar las
     filas por encima del encabezado. Verificado en el navegador: primero con la
     franja, después sin ella. */
  .pg--split > .det--hdr { padding-top: 0; }
  .pg--split > .det--hdr > .det__hdr {
    position: sticky; top: 0; z-index: 6; background: var(--bg);
    padding-top: 11px;
  }

  /* En la columna con scroll propio el cartel de descanso NO necesita pegarse:
     ya está siempre a la vista, arriba del scrollport. Pegado, se superponía
     54px sobre la lista y se COMÍA el toque de la primera OT — el dedo que
     apuntaba a la fila disparaba «Reanudar todo». */
  .pg--split > .col > .dban { position: static; }

  /* Las acciones del detalle se van al pie del panel, como en el mockup. */
  .acts { margin-top: auto; padding-bottom: var(--s1); }
  /* Y quedan ANCLADAS, por la misma razón por la que se anclaron los atajos de
     la columna izquierda: el panel tiene scroll propio y una pantalla táctil no
     dibuja barra. Medido con una OT pausada, producto largo, 3 parciales y 3
     operarios —un estado normal, no el peor caso—: el panel corta en 796 y
     «Terminar OT» empezaba en 775 (21px asomando) y «Fin de turno» en 832, o
     sea entero afuera. Lo que se escondía no era un atajo: era la acción por la
     que existe el MES, la que carga lo producido. `margin-top: auto` se queda,
     porque sigue mandando cuando el contenido no desborda. */
  .pg--split > .det .acts {
    position: sticky; bottom: 0; z-index: 5;
    background: var(--bg); padding-top: var(--s3);
    border-top: 1px solid var(--hair-2);
  }
  /* El desvanecido va ARRIBA del borde y fuera de la caja: sin él, lo que queda
     debajo del ancla se corta a mitad de glifo y se lee como un error de
     renderizado, no como «hay más». Es el mismo recurso que usa .atajos, con el
     degradado tintado al fondo (nunca a negro puro: sobre --bg #141417 una
     sombra negra se lee como suciedad en el vidrio). */
  .pg--split > .det .acts::before {
    content: ""; position: absolute; left: 0; right: 0;
    top: calc(var(--s4) * -1); height: var(--s4);
    background: linear-gradient(to top, var(--bg), transparent);
    pointer-events: none;
  }
  /* Dos columnas para las listas que son un MENÚ o una BANDEJA. El menú de
     tareas de apoyo medía 826px de alto con 11 opciones: no entraba en los 820
     de la tablet y había que scrollear teniendo 600px de ancho sin usar. En dos
     columnas son 6 renglones y entra entero, que es lo que mantiene en 5
     segundos una elección de 5 segundos. */
  .grp--cols2 {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1px;
  }
  /* El hairline lo dibuja el anillo de 1px de CADA fila, que cae exactamente
     dentro del `gap`, y no el fondo del contenedor. La primera versión pintaba
     el contenedor del color del hairline: con un número IMPAR de filas la celda
     que sobra quedaba de ese color —medido en /calidad con 5 pendientes: un
     rectángulo de 565x106 visiblemente más claro que todo lo demás, que se lee
     como un ítem deshabilitado—. Con el anillo, las celdas vacías no existen:
     no hay nada que las pinte. Dos filas vecinas dibujan la MISMA línea (mismo
     color, sin doblar el grosor) y el perímetro lo recorta el `overflow:hidden`
     de `.grp`. Sigue sin contar hijos, que es la restricción dura. */
  .grp--cols2 > .row { background: var(--card); box-shadow: 0 0 0 1px var(--hair); }
  /* Arriba de 820 el separador ES el `gap: 1px` con el anillo de la regla de
     acá arriba, así que el ::before sobra y se dobla con él. Abajo de 820 esto
     sigue siendo una PILA y ahí el ::before es el único separador que hay: por
     eso se apaga ACÁ ADENTRO y no en la regla base — apagarlo afuera dejó al
     celular sin ninguna línea entre filas, verificado en el navegador a 390. */
  .grp--cols2 > .row::before { display: none; }
  /* En el PANEL las dos columnas sólo entran en apaisada. A 820 vertical el
     panel mide 376 y cada columna queda en ~180: las etiquetas se recortan y el
     chevron se les monta encima. Se colapsa a una pista —y no se saca la clase—
     para conservar el hairline por `gap: 1px` y el fondo por fila; con
     `display: block` los anillos de 1px se superponen. Vuelve a dos en 1180. */
  .pg--split > .det .grp--cols2 { grid-template-columns: minmax(0, 1fr); }
  /* El cartel de lista vacía cruza las dos columnas: no es un ítem de la lista. */
  .grp--cols2 > .vacio { grid-column: 1 / -1; background: var(--card); }

  /* auto-fit y no repeat(4): la hoja de la tarea en curso tiene DOS KPIs y en
     una grilla de cuatro quedaban como dos cajas angostas con media fila vacía.
     auto-fit colapsa las pistas sin ítems, así que cada uso llena su fila con
     los que tenga. Historial pide seis en una línea y lo dice aparte. */
  /* El número primero y los badges después: sin esto el número de OT caía en 7
     posiciones distintas según cuántos badges tuviera la fila, y en dos columnas
     hay que barrer dos bordes irregulares en vez de uno. */
  .row__nro .badge { order: 9; }

  /* La misma primera pista que `.row`: el producto del Historial cae en la
     misma x que en las otras seis listas en vez de 8px más a la derecha. */
  .hline { grid-template-columns: 48px 1fr auto; }

  .kpis { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
  /* Va DESPUÉS de .kpis para ganarle al repeat(4,1fr). */
  .kpis--cierre { grid-template-columns: 3fr 2fr; }

  /* ── La hoja cae SOBRE el panel, no en el medio de la pantalla ──────────
     Centrada tapaba las dos columnas: la única cosa que llenaba el panel
     derecho era además la que escondía la lista. Acá ocupa exactamente el
     rectángulo del panel; el fondo oscuro sigue cubriendo el viewport entero y
     sigue cerrando al toque afuera.
     align-items es flex-start y NO stretch: .hoja es flex ROW, así que stretch
     estiraría .hoja__c a todo el alto y, como su único hijo visible (el clon
     del x-if) no tiene flex:1 —y no se le puede dar sin tocar los 11 inline—,
     el botón que compromete quedaría flotando a mitad de pantalla y a distinta
     altura en cada hoja. Además stretch mataría el overflow-y:auto de .hoja__c,
     que es la ÚNICA vía para subir el .num por encima del teclado.
     .hoja--plana (las hojas de /calidad y /deposito, que no tienen ni rail ni
     panel) conserva el comportamiento centrado de siempre. */
  .hoja:not(.hoja--plana) {
    align-items: flex-start; justify-content: flex-start;
    padding: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + var(--s3))
             var(--s4) calc(var(--s4) + var(--safe-b)) var(--panel-l);
  }
  .hoja:not(.hoja--plana) > .hoja__c {
    width: 100%; max-width: 640px;
    max-height: 100%;              /* pisa el 86dvh de la base */
    /* --borde-ctrl y no --hair: flotando sobre el velo, el canto de la hoja daba
       1,24:1 —medido componiendo las capas, no estimado— y era el ÚNICO límite
       entre la hoja y la pantalla de atrás. Con este token da 3,855:1, que es el
       mismo umbral con el que se eligió el token. */
    border: 1px solid var(--borde-ctrl); border-radius: var(--r-card);
    padding-inline: var(--s4);
    animation: hoja-der 180ms var(--ease-out-quart);
  }
  .hoja--plana { align-items: center; justify-content: center; padding: var(--s4); }
  /* El buscador de una hoja con lista larga (Retomar: 18 filas, 1.468px
     escondidos) se queda arriba: sin esto, filtrar cuesta volver a scrollear la
     lista entera hasta el principio. Va por CLASE y no por `.hoja__c .busca`:
     ese selector alcanzaba también al campo «Nota» de la hoja de tarea de apoyo
     —medido— y lo dejaba flotando sobre el contenido en cuanto la hoja
     desbordaba. `.busca` no es «buscador»: es el estilo de campo de texto. */

  /* El tirador se esconde: acá la hoja no está apoyada en el canto de abajo,
     es una tarjeta sobre el panel derecho, y un asa de bottom-sheet arriba de
     una tarjeta no dice nada. El GESTO sigue vivo —engancha desde el título o
     con la hoja arriba de todo—; lo que se va son los 8px que el afordance
     cuesta en la pantalla donde las hojas ya desbordan. */
  .hoja__grip { display: none; }

  /* El contrato del marco (scroll propio del cuerpo + pie fuera del scroll)
     se mudó ENTERO a la base: partido en dos, abajo de 820px quedaba el
     `min-height: 0` sin el overflow que lo hace habitable y el último campo
     terminaba abajo del pie, inalcanzable. Ver el bloque de `.hoja__marco`.
     El desvanecido que traía el neutralizador del pie (`box-shadow: 0 -8px
     12px -10px var(--bg)`) se borró: `.hoja__pie` y `.hoja__c` pintan los DOS
     var(--bg), así que la sombra era del mismo color que el fondo contra el
     que caía y no dibujaba un solo píxel. En `.atajos`, donde la misma línea
     sí se ve, el fondo es var(--card) y la sombra var(--bg). */

  /* Las dos respuestas de «¿Cómo queda?» (el cierre parcial de Terminar) vivían
     acá con minmax(220px) y la tablet de 1024 se les caía a una columna por 4px:
     la regla se mudó ENTERA a la base (ver `.par-acc`), que además es el único
     ancho donde no existía y los dos botones quedaban pegados. */

  /* La hoja gana 8px arriba y 8 abajo: son los que faltaban para que el botón
     que confirma no se corte contra el borde. Pisa a propósito el `padding`
     shorthand de la regla gemela de más arriba (mismo selector, mismo @media),
     que deja --s3 arriba y --s4 abajo; acá bajan a --s2 y --s3. */
  .hoja:not(.hoja--plana) {
    padding-top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + var(--s2));
    padding-bottom: calc(var(--s3) + var(--safe-b));
  }

  .hoja--plana > .hoja__c {
    max-width: 560px; border-radius: var(--r-card);
    /* --borde-ctrl igual que su hermana del piso. Esta es LA hoja que flota por
       los cuatro lados —está centrada, no apoyada sobre el panel—, así que su
       canto es su único límite contra la pantalla de atrás: 1,24:1 → 3,855:1. */
    border: 1px solid var(--borde-ctrl);
    /* El 86dvh de la base le sobraba alto a la tablet: con un rechazo, la hoja
       llegaba a su tope y empujaba «Confirmar veredicto» fuera de pantalla. */
    max-height: 100%;
  }

  /* Con la lista y el detalle a la vez hay que ver QUÉ fila se está mirando. */
  /* 30% y no 10%: con 10 la marca de «esta es la que estoy mirando» daba
     1,142:1 contra la fila de al lado —medido sobre los píxeles compuestos, no
     estimado—, exactamente el mismo peso que la hairline que separa las filas.
     En una tablet montada, con las dos columnas a la vista todo el turno, esa
     marca es lo único que ata el panel derecho con la fila de la izquierda. */
  .row--sel { background: color-mix(in srgb, var(--linea) 30%, transparent); }

  /* La franja deja de estar centrada en el viewport: ahí caía justo encima de
     «Terminar OT» y «Fin de turno» (medido a 1180×820). Pegada al rail no tapa
     ninguna acción. */
  .real-b:not(.real-b--plana) {
    left: calc(var(--rail) + var(--s4)); transform: none;
    bottom: calc(var(--safe-b) + var(--s3));
  }
}

/* De acá para arriba sobra ancho para devolverle a la lista los 372px del
   mockup sin tocar el panel. */
@media (min-width: 1024px) {
  :root { --lista: 372px; }

  /* ── Las dos pantallas de «elegí una» ───────────────────────────────────
     El selector de grupo es el primer toque del turno, de pie y con guantes, y
     usaba 560 de 1180px con tres filas de 76px de alto. Acá los tres grupos son
     fichas lado a lado: el color y la posición hacen el trabajo que en una fila
     hace leer el nombre. Y la elección de línea de Iniciar deja de ser una fila
     de 900px con la etiqueta a la izquierda y el chevron a 800px de distancia. */
  .sel { max-width: 900px; }
  .sel__gs { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
  /* El esqueleto es UN hijo del grid y sin esto caía en una sola columna de
     240px. Ocupa la fila entera y repite adentro la misma grilla y el mismo
     alto que las fichas. */
  .skel--sel { grid-column: 1 / -1; display: grid;
               grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
  .skel--sel > .skel__r { height: 180px; }
  .sel__gs > .sel__g {
    flex-direction: column; justify-content: center; text-align: center;
    min-height: 180px; gap: var(--s3);
  }
  /* Centrado, no sólo acotado: pegado a la izquierda dejaba 524px de negro al
     costado y el paso 2 saltaba a 1.060. El `width: 100%` es obligatorio —sin
     él la tarjeta colapsa a 221px—, la misma trampa del cartel de MES caído. */
  .paso-linea { max-width: 560px; margin-inline: auto; width: 100%; }

  /* ── Historial: dos columnas, y los 6 KPIs en UNA fila ──────────────────
     Medido a 1180×820 antes de esto: el cuerpo entraba en la columna de 900px
     con 184px muertos a la derecha, los 6 KPIs caían 4+2 dejando un hueco del
     tamaño de dos tarjetas, y la vista medía 1.679px de scroll. Con la grilla
     mide 1.089: el turno entero entra casi de un vistazo.
     Va en 1024 y no en 820: a 820 la columna útil son 700px y Órdenes quedaría
     en 296, más angosta que la fila del celular.
     Las dos pistas son EXPLÍCITAS (1fr y 380px) y no auto-fit a propósito: en
     una tablet montada el contenido llega de a poco —el poll trae una parada
     nueva a media mañana— y una pista que aparece y desaparece re-acomoda toda
     la pantalla debajo de la mano del operario. Con pistas fijas, lo que llega
     entra en su columna y nada de lo demás se mueve.
     El reparto: 1fr (656px a 1180) para Órdenes y Tareas —las dos que tienen
     producto y operarios en la misma fila— y 380 para Paradas y Parciales, que
     son hora + etiqueta + duración.
     La especificidad del selector de .scroll--ancho iguala a propósito la del
     tope de 900px (`.pg:not(...):not(...) > .scroll`): con `.scroll--ancho { }`
     a secas el tope ganaba y la grilla quedaba encerrada en 900px. */
  .pg:not(.pg--plana):not(.pg--split) > .scroll--ancho { max-width: none; }
  .histv {
    display: grid; grid-template-columns: 1fr 380px; gap: var(--s4);
    align-content: start; align-items: start;
  }
  /* Los KPIs, el buscador y el cartel de «todavía no hay movimientos» cruzan
     las dos columnas: son del día entero, no de una de las cuatro listas. */
  .histv > .kpis, .histv > .busca, .histv > .vacio { grid-column: 1 / -1; }
  .histv__col--a { grid-column: 1; }
  .histv__col--b { grid-column: 2; }
  .histv > .kpis { grid-template-columns: repeat(6, 1fr); }
}

/* Viewport bajo (1024×600, un celular grande acostado): con 100dvh de alto fijo
   las dos columnas quedan de 470px y no entra ni el detalle ni media lista.
   Ahí se vuelve al scroll de página, que es lo que hay hoy. */
@media (min-width: 820px) and (max-height: 620px) {
  .pg--split { height: auto; min-height: 100dvh; overflow: visible; }
  .pg--split > .col > .scroll, .pg--split > .det { overflow: visible; }
  /* Y con el scroll por columna apagado, los dos anclados se quedan sin ancla:
     su scrollport pasa a ser el documento, así que `bottom: 0` los pega al
     borde INFERIOR DE LA PANTALLA —con su fondo opaco y su borde— encima de la
     lista y del panel, que acá scrollean enteros. Van los MISMOS selectores
     (0-4-0 y 0-3-0) para empatar especificidad y ganar por orden: escritos como
     `.acts`/`.atajos` a secas apagarían también el sticky de 1180×820, que es
     donde «Terminar OT» y «Fin de turno» caen fuera del fold.
     `relative` y no `static`, igual que el neutralizador de
     `.hoja__marco > .hoja__pie`: los desvanecidos `::before` de los dos son
     `position: absolute` y necesitan contenedor. */
  .pg--split > .col > .scroll > .atajos, .pg--split > .det .acts { position: relative; }
  /* Y sin scroll por columna el desvanecido del pie se queda sin nada que
     desvanecer: anclado al borde inferior de una columna que ahora scrollea
     entera, flotaría sobre contenido REAL a mitad de página. */
  .pg--split > .col::after { display: none; }
}

/* A 1180 sobraban 24px exactos entre el borde derecho del panel (que topea en
   640) y el borde de padding de la página: el hub se leía corrido a la
   izquierda y el nombre del producto se partía en dos renglones en una fila de
   372. Los 24 se los lleva la lista —el panel no pierde un píxel, sigue en su
   tope— y el reparto pasa a ser 72 + 24 + 396 + 24 + 640 + 24 = 1180 justo. */
@media (min-width: 1180px) {
  :root { --lista: 396px; }
  /* Recién acá el panel llega a 640 (592 de contenido) y sus dos columnas miden
     296 — más que los ~280 con los que el celular dibuja la misma fila. El menú
     de Apoyo entero mide 972px contra 737 de panel: partido entra completo y
     ninguna de las once tareas queda bajo un fold que la tablet no señaliza. */
  .pg--split > .det .grp--cols2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* En un monitor la lista no puede correr 1.900px de ancho: se acota, y el rail,
   el header y el panel acompañan el mismo borde de página. El 100% se resuelve
   en el punto de USO (elementos fixed → viewport), no en :root: es el mismo
   truco que ya usaba .hdr y por la misma razón (con 100vw la barra de scroll lo
   correría media barra). No uses --panel-l en un elemento NO fixed.

   🔴 El tope era 1120 y eso hacía que 1280 fuera MÁS ANGOSTO que 1180: medido a
   1280×800, el panel caía a 604px contra los 640 que son su tope de diseño, y
   con él caían tres cosas que ese tope sostiene —el menú de Apoyo desbordaba
   21px (la tarea 11 bajo un fold que la tablet no señaliza), los títulos de la
   lista se partían en tres renglones y el subtítulo de cada fila envolvía a
   dos—. La cuenta que lo explica: a 1120 el reparto es 72 + 24 + 396 + 24 + 604
   con 80px de gutter muerto a cada lado; a 1180 —el ancla de diseño— es
   72 + 24 + 396 + 24 + 640 + 24 = 1180 CLAVADO, con el rail pegado al borde
   izquierdo y 24 de aire a la derecha.

   El tope pasa a 1280 y `--lista` a 496, que es la MÁXIMA lista que deja al
   panel en su tope: 1280 − 96 (rail + s4) − 24 (padding derecho) = 1160 de
   contenido, y 496 + 24 + 640 = 1160. Misma gramática que 1180 —full-bleed a la
   izquierda, 24 a la derecha—, cero píxeles muertos, y el ancho de sobra se lo
   lleva la lista, que es donde comprar ancho compra ALTO: una fila que no
   envuelve son 21px menos por tarjeta, en una pantalla de 800 donde lo escaso
   es el alto. El monitor sigue acotado, ahora en 1280 (medido a 1920×1080: pg
   1280 centrada, panel 640, hoja 640 justo encima del panel). */
@media (min-width: 1280px) {
  :root {
    --lista: 496px;
    --panel-l: calc((100% - 1280px) / 2 + var(--rail) + var(--s4) + var(--lista) + var(--s4));
  }
  .pg { max-width: 1280px; margin-inline: auto; }
  .hdr { padding-inline: max(var(--s4), calc((100% - 1280px) / 2)); }
  /* Fijado en left:0 el rail quedaba a 80px del contenido, colgando de la nada. */
  .nav { left: max(0px, calc((100% - 1280px) / 2)); }
  /* Con el tope en 1120 la hoja caía 24px CORTA del borde derecho del panel
     (1176 contra 1200) y encima no llegaba a su tope de 640: se quedaba en 580.
     Con 1280 los dos bordes coinciden —hoja y panel en x=616 r=1256, medido— y
     la hoja recupera sus 640, que es la invariante que `--panel-l` existe para
     sostener. */
  .hoja:not(.hoja--plana) { padding-right: calc((100% - 1280px) / 2 + var(--s4)); }
}

@media (prefers-reduced-motion: reduce) {
  /* `animation-iteration-count` va aparte: las dos animaciones que quedan vivas
     —el brillo del esqueleto y el latido del poll— declaran `infinite` DENTRO
     del shorthand `animation`, y sobreescribir sólo `animation-duration` no lo
     toca. Sin esto el bucle no se detiene: corre a .001ms para siempre. Medido
     con `emulateMedia({reducedMotion:'reduce'})` + `getAnimations().length`:
     2 animaciones corriendo antes, 0 después. Sin `fill-mode` cada una vuelve
     a su estado base, así que el latido queda quieto pero visible y el brillo
     del esqueleto se va a translateX(-100%), o sea afuera. */
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important;
                           animation-iteration-count: 1 !important; }
  /* El autoscroll del foco automático no se anima: en Android el foco
     programático + smooth aterriza a mitad de animación. */
  .hoja__c, .hoja__marco > .hoja__cuerpo { scroll-behavior: auto; }
}

/* ── Una vista por vez en el celular, las dos a la vez en la tablet ───────
 * El markup renderiza SIEMPRE la lista y el detalle; quién se ve lo decide el
 * ancho. Así el layout de tablet del mockup no es una segunda plantilla ni un
 * media query leído desde JS: es la misma pantalla con más lugar. */
@media (max-width: 819px) {
  .pg--split.v-hub > .det,
  /* Iniciar, Tarea de apoyo y el detalle de una tarea entran por acá desde que
     dejaron de ser páginas enteras: en celular el panel ES la pantalla, igual
     que el detalle de la OT, y la lista se va. Es la MISMA regla, no un caso
     nuevo — por eso el celular no cambió una línea con la mudanza. */
  .pg--split.v-detalle > .col,
  .pg--split.v-iniciar > .col,
  .pg--split.v-apoyo > .col,
  .pg--split.v-tarea > .col,
  /* Materiales es el quinto cuerpo del panel y necesita su línea acá: sin
     ella, en celular el header decía «Materiales» con su ← y debajo seguía
     la LISTA del hub. La pantalla existía y no se veía nunca — el `esPanel`
     de JS no alcanza, quién se esconde lo decide esta regla y sólo por
     nombre de vista. Cazado a 390×844 en el QA del 27-ago. */
  .pg--split.v-materiales > .col { display: none; }
  .solo-ancho { display: none !important; }
}
@media (min-width: 820px) {
  .solo-angosto { display: none !important; }
}

/* Latido del poll: una barra de 2px que cruza el header. Sin spinner: el estado
 * de carga no debe competir por atención con lo que el operario está mirando. */
.latido { position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 70; overflow: hidden; }
.latido::after { content: ""; display: block; height: 100%; width: 40%;
                 background: var(--linea); animation: lat 1s ease-in-out infinite; }
@keyframes lat { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }

/* Franja de «volver al estado real»: aparece sólo si el usuario tocó algo.
   Los 44 son el mínimo táctil: estas dos píldoras eran los dos últimos
   controles por debajo (medidas 162,3×40 en tablet y 153,1×40 en celular).
   Sube el ALTO y no el `bottom`: la reserva de `.con-real .acts` son 56px
   contra 44 de píldora + 8 de offset = 52, y mover el offset la rompe.
   Centrada tapaba la fila: en Materiales/apoyo/Historial, a mitad de scroll
   (390×844 y 1024×768, `tocado=true`), sus 153×44 en y=720 le devolvían a
   `elementFromPoint` el BUTTON.real-b antes que la matcard de abajo — el
   `padding-bottom` que reserva `.con-real .pg` sólo protege la ÚLTIMA fila,
   no las de en medio. Achicada a ícono y pegada al canto, deja de cruzarse
   con la columna donde viven las filas. */
.real-b {
  position: fixed; right: var(--s3); left: auto;
  bottom: calc(var(--nav-h) + var(--safe-b) + var(--s2)); z-index: 65;
  width: 44px; height: 44px; padding: 0; border-radius: 50%;
  background: rgba(20,20,23,0.96); border: 1px solid var(--borde-ctrl); color: var(--muted);
  font-size: 0; line-height: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background-color var(--dur-press) var(--ease-out-quart);
}
.real-b::before { content: "↺"; font-size: 20px; line-height: 1; }
.real-b:active { background: rgba(30,30,34,0.96); }

/* Aviso de versión nueva. Misma forma que la píldora de «volver al estado real»
   —es el mismo tipo de mensaje: algo que se ofrece, no algo que interrumpe— pero
   ARRIBA a la derecha, para no pelearse con ella ni con las acciones del pie, y
   en el color del grupo, que es lo único que la distingue. */
.nueva-v {
  position: fixed; right: var(--s4); z-index: 66;
  top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + var(--s2));
  min-height: 44px; padding: 0 var(--s3); border-radius: var(--r-pill);
  background: rgba(20,20,23,0.96); border: 1px solid var(--linea); color: var(--linea);
  font-size: var(--t-meta); font-weight: 700; cursor: pointer; white-space: nowrap;
  transition: background-color var(--dur-press) var(--ease-out-quart);
}
.nueva-v:active { background: rgba(30,30,34,0.96); }

/* El sello de build: lo que se le pide al operario por teléfono cuando dice
   «no me llegó el arreglo». Sale de `_build`, que `_mirarBuild()` levanta del
   header `X-Espejo-Build` de CUALQUIER respuesta, así que no existe hasta el
   primer poll — de ahí el `x-show` del template, y no un `:empty` acá.
   EN FLUJO, a diferencia de sus dos vecinos `.nueva-v` y `.toast`, que son
   `position: fixed`: esos dos interrumpen, esto es un dato de servicio que se
   busca cuando hace falta y el resto del tiempo tiene que poder irse
   scrolleando. Fijarlo costaba además una variante `--plana` para las zonas
   (`--nav-h` sigue en 72px en celular también ahí, ver `.real-b--plana`) y un
   lugar en la pila de z justo donde apoya el pulgar. Es el mismo criterio con
   el que la UI vieja lo pinta en `.selector__pie`. */
.build-tag {
  display: block; margin-top: var(--s2); text-align: right;
  font-size: var(--t-micro); font-variant-numeric: tabular-nums;
  color: var(--faint);
}
/* Cuando la página no tiene nav fija debajo (.pg--plana), la franja se apoya
   en el margen inferior, no en --nav-h. */
.real-b--plana { bottom: calc(var(--safe-b) + var(--s3)); }

/* ── Piezas compartidas: aviso, confirmación en dos toques, aviso efímero ─── */
/* El borde estaba en `--pausa 40%`, que compuesto sobre la hoja da 2,02:1 —
 * por debajo del 3:1 que WCAG 1.4.11 pide para el LÍMITE de un componente. Y el
 * relleno al 12% daba 1,16:1 contra el fondo: el bloque casi no existía como
 * bloque. Daba igual mientras esto era un párrafo suelto; `.aviso--mat` es el
 * primero que CONTIENE UN BOTÓN, y ahí 1.4.11 sí aplica.
 * Es la misma cuenta y el mismo argumento con el que este repo ya subió
 * `--borde-ctrl` a rgba(255,255,255,.38): el vidrio con huellas y la luz de
 * tubo. No cambia el color, sólo la mezcla. */
.aviso--warn {
  border-radius: var(--r-card); padding: var(--s2) var(--s3);
  background: color-mix(in srgb, var(--pausa) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--pausa) 70%, transparent);
  color: var(--muted); font-size: var(--t-body);
}
/* El aviso que además OFRECE la salida del error (hoy: la lista de Retomar que
   no se pudo leer). El botón es el mismo `.caido__b` del cartel de «no se pudo
   leer el MES»: es la misma pregunta y no merece un control nuevo. */
.aviso--accion { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s2); }
/* El aviso que ES el botón (la partida olvidada del hub, 26-sep-2026): tocarlo
   abre lo que avisa. Aviso + botón aparte eran ~150px arriba del hub de la
   tablet y mandaban «En curso» debajo del menú anclado. */
.aviso--toque {
  display: flex; align-items: center; gap: var(--s3); width: 100%;
  min-height: 52px; text-align: left; font-family: inherit; line-height: inherit;
  cursor: pointer;
}
.aviso__ir { flex: none; margin-left: auto; display: inline-flex; color: var(--pausa); }

/* Control (no bloque) con la misma semántica de aviso: para el chip que avisa
   sin vetar, no para el párrafo de un cartel. */
.chip--warn {
  border-color: color-mix(in srgb, var(--pausa) 40%, transparent);
  background: color-mix(in srgb, var(--pausa) 12%, transparent);
  color: var(--pausa);
}

/* Confirmar en dos toques: No a la izquierda, Sí (el que hace la cosa) a la
   derecha y en naranja. Vale marcarlo con .confirm__si o dejarlo último. */
.confirm { display: flex; gap: var(--s2); }
.confirm > button {
  flex: 1 1 0; min-height: 52px; border-radius: var(--r-btn);
  background: transparent; border: 1px solid var(--borde-ctrl); color: var(--txt);
  font-size: var(--t-fila); font-weight: 700; cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out-quart),
              background-color var(--dur-ui) var(--ease-out-quart);
  touch-action: manipulation;
}
.confirm > button:active { transform: scale(0.96); }
.confirm > button:last-child, .confirm__si {
  background: var(--pausa); border-color: var(--pausa); color: var(--inv);
}

/* Aviso efímero: se muestra arriba de la nav y se lo lleva el JS a los 2 s.
   Centrado con margin y no con transform, para no pelearse con x-transition.
   Va por encima de la hoja modal: si no, un aviso disparado desde adentro de
   una hoja no se vería. */
.toast {
  position: fixed; left: 0; right: 0; z-index: 95;
  bottom: calc(var(--nav-h) + var(--safe-b) + var(--franja));
  margin-inline: auto; width: max-content; max-width: calc(100% - var(--s5));
  padding: var(--s2) var(--s3); border-radius: var(--r-btn);
  background: rgba(20,20,23,0.97); border: 1px solid var(--hair-2);
  color: var(--txt); font-size: var(--t-body); font-weight: 700; text-align: center;
  animation: toast-in 200ms var(--ease-out-quart);
  transition: opacity 200ms var(--ease-out-quart), transform 200ms var(--ease-out-quart);
  /* El toast es texto puro y no tiene un solo control adentro, pero atrapaba el
     dedo mientras estaba puesto: medido a 390×844 en Planificación, después de
     «+ Grupo Aerosoles» queda en y=673,5..714 y `elementFromPoint` sobre ese
     rectángulo devuelve el DIV.toast con un chip real de otra candidata debajo
     —el click no llega a ningún lado y la pileta no se mueve—. Mismo recurso que
     ya usa el logo del header. */
  pointer-events: none;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }

/* Los dos contextos donde el ícono no tenía regla propia: sin esto el SVG se
   estira a lo que dé el contenedor. Ya pasó con el pause del detalle. */
.dban__t > svg, .pausa-linea > svg { width: 20px; height: 20px; flex: none; }

/* ── Planificación ────────────────────────────────────────────────────────
 * La pantalla más densa del espejo: fecha, modo, aviso, switch, plan por grupo
 * y candidatas. Todo lo que se toca acá es overlay local (el espejo no
 * escribe), así que la UI tiene que verse igual de real que la del MES: si el
 * botón parece de mentira, la pantalla no sirve para decidir nada.
 */
.planv { display: flex; flex-direction: column; gap: var(--s3); }
/* Las dos mitades de Planificación. En celular apilan igual que antes (el gap
   lo daba .planv y ahora lo dan ellos); en tablet apaisada son las columnas. */
.planv__col { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }
/* Los DOS bloques de la columna derecha —la pileta de candidatas y el sugerido
   del MRP—. Las clases las usan los templates y los scripts de medición desde
   la ronda 2 (ESTADO.md, BACKLOG-r2.md) pero NINGUNA tanda las había definido:
   eran los únicos dos nombres del archivo usados sin regla. Lo que les faltaba
   es el ritmo INTERNO —el gap de `.planv__col` separa un bloque del otro, no a
   sus hijos—, así que el buscador quedaba pegado a la lista y los dos párrafos
   del MRP, pegados entre sí. --s2 es la escala de «items emparentados»; el
   margen del rótulo se cancela para que el aire bajo el encabezado siga siendo
   el mismo 8px que en las otras seis listas y no el doble. */
.ppool, .pmrp { display: flex; flex-direction: column; gap: var(--s2); }
.ppool > .sect__h, .pmrp > .sect__h { margin-bottom: 0; }
/* Los controles de Planificación. En celular es la misma pila de siempre. */
.planv__head { display: flex; flex-direction: column; gap: var(--s3); min-width: 0; }
/* El encabezado del grupo destino toma su color. Es el ÚNICO consumidor de
   `--linea` en la columna del plan: sin esto, el chip «+ Grupo Aerosoles» de la
   pileta era celeste y el grupo al que iba a parar, gris como cualquier lista.
   Los `.sect__h` de la pileta y del MRP se quedan en --faint a propósito: gris =
   lista, color = grupo destino. */
.pgrupo .sect__h { color: var(--linea); }
/* Anatomía general: el conteo de una lista va a la derecha del título de
   sección en cualquier pantalla, no sólo en Planificación (con un solo hijo,
   el flex no cambia nada donde el contador no existe). */
.sect__h { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2); }
.sect__n { font-size: var(--t-meta); font-weight: 700; letter-spacing: 0.02em;
           text-transform: none; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Navegación de fecha: ‹ · date · › . El date picker nativo es el que mejor se
   toca con guantes, así que no se reemplaza por nada propio. */
/* `flex-start` y NO `center`: la columna del medio son DOS cosas apiladas —el
   input de 48px y el renglón «Martes, 1 de septiembre» de 19,5— o sea 71,5px de
   alto. Centrando, los dos botones de 48 se alineaban con el centro del BLOQUE
   y quedaban 11,8px más abajo que la caja que navegan (centro 101,8 contra
   90,0, medido a 390×844): la fila se leía torcida, que es el «la fecha
   desacomodada en mobile» del 31-ago. Con flex-start los tres arrancan en el
   mismo borde y los 48px del botón calzan con los 48 del input. */
.pfecha { display: flex; align-items: flex-start; gap: var(--s2); }
.pfecha__nav {
  flex: none; width: 48px; height: 48px; border-radius: var(--r-btn); cursor: pointer;
  background: var(--card); border: 1px solid var(--hair); color: var(--txt);
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform var(--dur-press) var(--ease-out-quart);
  touch-action: manipulation;
}
.pfecha__nav:active { transform: scale(0.94); }
.pfecha__nav[disabled] { opacity: var(--dis); }
.pfecha__mid { flex: 1 1 auto; min-width: 0; text-align: center; }
.pfecha__inp {
  width: 100%; min-height: 48px; padding: 0 var(--s3); border-radius: var(--r-btn);
  background: var(--card); border: 1px solid var(--borde-ctrl); color: var(--txt);
  font-size: var(--t-fila); font-weight: 700; text-align: center;
  font-variant-numeric: tabular-nums;
}
.pfecha__inp:focus { border-color: color-mix(in srgb, var(--linea) 70%, transparent); }
.pfecha__larga { margin-top: var(--s1); font-size: var(--t-meta); color: var(--muted); }

/* Fila con interruptor (cierre automático de fin de turno). */
.swrow {
  display: flex; align-items: center; gap: var(--s3); padding: var(--s3);
  background: var(--card); border: 1px solid var(--hair); border-radius: var(--r-card);
}
.swrow__t { flex: 1 1 auto; min-width: 0; font-size: var(--t-body); }
.swrow__d { display: block; margin-top: var(--s1); font-size: var(--t-meta); color: var(--muted); }
/* Geometría propia del componente (60 = 3 + 26 + 26 de viaje + 3 + bordes), no
   espaciado: no entra en la escala.
   El riel dibujaba 60×34 y era el ÚNICO target de toda la app por debajo de
   44px (chips 48, .pit__b 48, .pfecha__nav 48, .busca 48, .nav__btn 63×64):
   1 de 128 targets medidos en Plan, 0 en las otras 21 combinaciones. Antes se
   agrandaba el HIT-AREA con un `::before` que sobresalía del `.sw` real —pero
   `.sw` seguía midiendo 34px de alto de verdad, y quien mide con
   `getBoundingClientRect` (el QA final, con `elementFromPoint`) no ve ese
   sobrante: acá no hay fila envolvente que lo reciba, a diferencia del resto
   de los controles. Se invierte: el BOTÓN pasa a medir 60×44 de verdad —cabe
   en los 104px de `.swrow` sin invadir la fila vecina— y la pista visual
   (el mismo dibujo, 60×34) se pinta con `::before`, centrada y por detrás del
   pomo. */
.sw {
  position: relative;
  flex: none; width: 60px; min-height: 44px; padding: 3px; border-radius: var(--r-pill); cursor: pointer;
  background: none; border: none;
  display: flex; align-items: center; justify-content: flex-start;
  touch-action: manipulation;
}
.sw::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 60px; height: 34px; border-radius: var(--r-pill);
  background: rgba(255,255,255,0.10); border: 1px solid var(--hair-2);
  transition: background-color var(--dur-ui) var(--ease-out-quart),
              border-color var(--dur-ui) var(--ease-out-quart);
}
.sw__k { position: relative; z-index: 1; width: 26px; height: 26px; border-radius: 50%; background: var(--muted);
         transition: transform 160ms var(--ease-out-quart), background-color 160ms var(--ease-out-quart); }
.sw--on::before { background: color-mix(in srgb, var(--curso) 34%, transparent); border-color: var(--curso); }
.sw--on .sw__k { transform: translateX(26px); background: var(--curso); }
.sw[disabled] { opacity: var(--dis); cursor: default; }

/* Carga estimada del turno (tiempos ICA). */
.carga { margin: 0 0 var(--s2); display: flex; flex-direction: column; gap: var(--s1); }
.carga__bar { height: 6px; border-radius: var(--r-pill); background: rgba(255,255,255,0.08); overflow: hidden; }
/* `cargaSem()` devuelve verde|amarillo|rojo y el template pinta
   `carga__fill--<eso>`: `--verde` NO tiene regla A PROPÓSITO, porque el verde es
   el estado base de la barra. La clase existe en el DOM y no se define acá; no
   es un cabo suelto. */
.carga__fill { background: var(--curso); }
.carga__fill--amarillo { background: var(--sem-medio); }
.carga__fill--rojo { background: var(--sem-rojo); }
.carga__txt { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2);
              font-size: var(--t-meta); color: var(--muted); }
/* El % sin este renglón miente para abajo: lo que no tiene tiempo ICA ocupa la
   línea igual y no suma un minuto. */
/* `--muted` y no `--pausa`: es un asterisco («hay N OTs sin tiempo cargado»),
   no una alarma, y era lo más fuerte del bloque. Además DESIGN.md reserva ese
   naranja para «parado», que en esta pantalla no existe. */
.carga__warn { font-size: var(--t-meta); color: var(--muted); }
/* El exceso, en el mismo rojo que la barra que lo produce. */
.carga__exceso { font-size: var(--t-meta); font-weight: 700; color: var(--sem-rojo); }

/* Item del plan: posición, producto, cantidad editable y los controles. */
.pit {
  position: relative; display: grid; grid-template-columns: 24px 1fr auto;
  gap: var(--s2); padding: var(--s3); align-items: center;
}
.pit + .pit::before { content: ""; position: absolute; left: var(--s3); right: 0; top: 0;
                      height: 1px; background: var(--hair); }
.pit__ord { grid-column: 1; grid-row: 1 / span 2; align-self: center; text-align: center;
            font-size: var(--t-body); font-weight: 700; color: var(--faint); font-variant-numeric: tabular-nums; }
.pit__main { grid-column: 2; grid-row: 1; min-width: 0; }
.pit__prod { font-size: var(--t-fila); font-weight: 500; letter-spacing: 0.015em; }
.pit__meta { margin-top: var(--s1); font-size: var(--t-meta); color: var(--muted); }
.pit__cant {
  grid-column: 3; grid-row: 1; width: 86px; min-height: 48px; padding: 0 var(--s2);
  border-radius: var(--r-btn); background: var(--bg); border: 1px solid var(--borde-ctrl);
  color: var(--txt); font-size: var(--t-fila); font-weight: 600; text-align: center;
  font-variant-numeric: tabular-nums;
}
.pit__cant:focus { border-color: color-mix(in srgb, var(--linea) 70%, transparent); }
.pit__ctrl { grid-column: 2 / span 2; grid-row: 2; display: flex; justify-content: flex-end; gap: var(--s2); }
.pit__b {
  width: 48px; height: 48px; border-radius: var(--r-btn); cursor: pointer;
  background: var(--bg); border: 1px solid var(--borde-ctrl); color: var(--txt);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--t-tit); line-height: 1;
  transition: transform var(--dur-press) var(--ease-out-quart);
  touch-action: manipulation;
}
.pit__b:active { transform: scale(0.94); }
.pit__b[disabled] { opacity: var(--dis); }
/* Un solo ícono para las dos flechas: el chevron del repo, girado. */
.pit__b svg { transform: rotate(-90deg); }
.pit__b--baja svg { transform: rotate(90deg); }
/* Control de 48px → ícono de 24px, igual que el resto de la escalera táctil. */
.pfecha__nav svg, .pit__b svg { width: 24px; height: 24px; }
/* Rojo semántico y no `--pausa`: el naranja de la app significa «parado» y es
   el único que lo significa. Quitar del plan es destructivo, no una pausa.
   Mismo criterio con el que la Ronda 7 despintó «Rechazar todo» en /calidad.
   Acá va el rojo PLENO y no `--sem-rojo-claro` (que es el de texto sobre
   fondo oscuro) porque el botón se pinta sobre `--bg` #141417 y no sobre la
   mezcla del chip: #ef4444 sobre #141417 da 4,87:1, y son 20px/700, o sea
   texto grande. En el chip de /calidad el mismo rojo daba 4,15:1 sobre su
   fondo compuesto y por eso allá se aclaró. */
.pit__b--x { color: var(--sem-rojo); font-weight: 700; }

/* Candidata: la tarjeta con la que se arma el plan. */
.pcard { position: relative; padding: var(--s3); display: flex; flex-direction: column; gap: var(--s1); }
.pcard + .pcard::before { content: ""; position: absolute; left: var(--s3); right: 0; top: 0;
                          height: 1px; background: var(--hair); }
.pcard__nro { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
              font-size: var(--t-meta); font-weight: 700; color: var(--muted);
              font-variant-numeric: tabular-nums; }
.pcard__prod { font-size: var(--t-fila); font-weight: 500; letter-spacing: 0.015em; }
.pcard__fmt { margin-left: var(--s1); font-size: var(--t-meta); color: var(--faint); }
.pcard__meta { font-size: var(--t-meta); color: var(--muted); }
/* La que el MES da por terminada se puede planificar igual, pero se marca: al
   arrancarla el piso se come el «¿sigue?» del guard de avance. El tinte era
   `--pausa` al 7%: en esta pantalla no hay nada «parado» y ese naranja es el
   único que lo dice en toda la app. `--sem-medio` es el amarillo de «mirá
   esto», el mismo de `.sem--amarillo` y de la barra de carga al tope. */
.pcard--bloq { background: color-mix(in srgb, var(--sem-medio) 7%, transparent); }
/* La acción de la tarjeta: cada botón lleva el color de su grupo, el mismo con
   el que el piso lo ve en el visor. En gris parecían deshabilitados. */
.pcard .chips { margin-top: var(--s1); }
.pcard .chip { color: var(--linea); border-color: color-mix(in srgb, var(--linea) 40%, transparent); }


/* ── Tablet apaisada, segunda tanda: lo que depende del orden del archivo ───
 * Estas reglas viven acá y no en el @media de arriba por una razón mecánica,
 * no de gusto: los selectores que pisan (`.planv`, `.pfecha__mid`, `.swrow__t`)
 * tienen su declaración BASE más abajo en el archivo, en la sección de
 * Planificación. Un @media no suma especificidad, así que con la misma
 * especificidad gana la última regla del archivo y las de arriba no se
 * aplicaban NUNCA: `.planv` seguía siendo flex y la grilla de dos columnas era
 * letra muerta. Verificado en el navegador antes de mover el bloque.
 * Si algún día se reordena el archivo para que todas las bases queden arriba,
 * esto se puede volver a fusionar con el @media de 1024.
 */
@media (min-width: 1024px) {
  /* ── Planificación: el plan a la izquierda, la pileta a la derecha ───────
     La tarea literal de esta pantalla es «poner esta OT en ese grupo», y hasta
     acá las dos mitades no se veían nunca juntas: medido a 1180×820, los
     grupos terminaban en y=534 y las candidatas corrían hasta y=9.150, con el
     documento entero en 9.291px (11,3 pantallas). Se tocaba «+ Grupo
     Aerosoles» a ciegas, sin ver el contador del grupo, que es el único
     acuse de recibo que hay.
     La pileta va sticky y con scroll propio: el plan queda fijo mientras se
     recorren las 53 candidatas, que es lo que convierte dos pantallas en una.
     420px para la pileta porque su tarjeta lleva número + producto + meta en
     tres renglones y los chips «+ Grupo» abajo; menos que eso y los chips
     empiezan a partirse. */
  .planv {
    display: grid; grid-template-columns: 1fr 420px; gap: var(--s4);
    /* `min-content` en la primera fila NO es decorativo: la pileta cruza las dos
       filas y, con las dos en `auto`, el navegador le reparte SU alto a las dos
       — la fila de los controles crecía a 367px para un contenido de 257 y
       empujaba el plan 135px hacia abajo, a un hueco negro en el medio de la
       columna. Medido: con `min-content 1fr` el plan arranca en 351 (24px de
       gap, el que corresponde) en vez de en 462. */
    grid-template-rows: min-content 1fr;
    align-content: start; align-items: start;
  }
  .planv > .caido, .planv > .skel { grid-column: 1 / -1; }
  /* Los controles NO cruzan las dos columnas: si lo hacen, empujan la pileta
     297px hacia abajo y la hacen nacer fuera de la pantalla.
     Esta pista es además lo que acota al switch de cierre automático, que es
     una FILA de control y no un panel: a lo ancho de las dos columnas su
     interruptor quedaba a 700px de la etiqueta que lo nombra. El `.planv >
     .swrow { max-width: 720px }` que lo hacía antes murió cuando el switch se
     mudó adentro de `.planv__head` —dejó de ser hijo directo de `.planv`— y de
     todas formas ya no ataba nada: la columna 1 mide 536px a 1100 y 676 a 1180. */
  .planv__head { grid-column: 1; }
  .planv__col--plan { grid-column: 1; }
  .planv__col--pool {
    /* Las dos filas: la de los controles y la del plan. Así arranca arriba de
       todo (medido: y=70..794, entera en pantalla) en vez de debajo de ellos. */
    grid-column: 2; grid-row: 1 / span 2;
    position: sticky; top: calc(var(--hdr-h) + env(safe-area-inset-top, 0px) + var(--s3));
    max-height: calc(100dvh - var(--hdr-h) - env(safe-area-inset-top, 0px) - var(--s5));
    overflow-y: auto; overscroll-behavior: contain;
    /* El anillo de foco de la primera y la última tarjeta, igual que en la
       lista del hub: el sangrado negativo y el padding se cancelan. */
    margin-inline: calc(var(--s2) * -1); padding-inline: var(--s2);
  }
  /* La pileta esconde 11.000px y su última tarjeta visible queda partida por el
     canto del scrollport —los dos botones «+ Aerosoles / + Líquidos» cortados a
     la mitad, medido a 1280×800—. Mismo recurso que la lista del hub y que
     `.acts::before`: degradado tintado al fondo.
     Acá el desvanecido va ADENTRO del scroller (sticky) y no colgado de
     `.planv`: la pileta es sticky con `max-height`, así que su borde inferior
     es el del viewport, mientras que el de `.planv` sigue al contenido de la
     columna IZQUIERDA. Hoy coinciden en 774 sólo porque el plan del día está
     vacío; con el plan cargado la grilla crece, la página scrollea y un
     desvanecido anclado a `.planv` se iría con ella.
     El margen negativo se come su propio alto MÁS el `gap` de 16 de la pileta:
     sin la segunda mitad el pseudo-elemento sumaba 16px de scroll (11.742
     contra 11.726, medido). Los -8 laterales empatan el sangrado de arriba. */
  .planv__col--pool::after {
    content: ""; position: sticky; bottom: 0; flex: none; align-self: stretch;
    height: var(--s4); margin: calc((var(--s4) + var(--s3)) * -1) calc(var(--s2) * -1) 0;
    background: linear-gradient(to top, var(--bg), transparent);
    pointer-events: none; z-index: 4;
  }

  /* Los controles dejan de estirarse hasta el borde: son controles, y su
     tamaño no significa nada. Medido antes: el campo de fecha ocupaba 788px
     para diez caracteres y el toggle quedaba a 790px de su etiqueta. */
  .pfecha { justify-content: flex-start; }
  .pfecha__mid { flex: 0 1 320px; }
  .swrow__t { flex: 0 1 62ch; }
}

/* ── Tablet ANCHA: el ítem del plan en un solo renglón ──────────────────────
 * Medido a 1180×820 con 12 ítems: cada `.pit` mide 140px y su segundo renglón
 * gasta 550px de ancho para tres botones de 48 pegados a la derecha — 390px de
 * negro por fila, 1.955px de columna para 12 ítems (2,4 pantallas).
 * Con los controles en el primer renglón la fila baja a ~100px: −480px cada 12.
 *
 * Va en 1120 y NO en 1024, que es lo que midió el verificador: recién ahí la
 * columna del plan tiene ancho para que `.pit__main` no baje de ~200px. A 1100
 * ya gana −252px y a 1024 PIERDE +216, porque el producto se parte en tres
 * líneas y la fila crece más de lo que ahorra el renglón.
 *
 * Y va al final del archivo por la trampa de orden que documenta la bitácora:
 * la base de `.pit` está más abajo que el @media de 1024.
 */
@media (min-width: 1120px) {
  .pit { grid-template-columns: 24px 1fr auto auto; }
  .pit__ctrl { grid-column: 4; grid-row: 1; }
}

/* ── Materiales: el segundo botón primario del detalle ──────────────────────
 * Mismo peso y mismo alto que «Registrar parcial», distinto color. No compiten
 * porque no dicen lo mismo: el parcial es lo que el operario hace VARIAS veces
 * por turno; éste es lo que no hay que olvidar UNA vez, y si se olvida, la OT
 * cierra sin dejar rastro de qué lote entró.
 * El verde de `--curso` es de la acción del minuto a minuto y no se comparte:
 * dos bloques del mismo color leídos de reojo son un solo bloque. Éste usa el
 * acento de la línea, que ya es el color de «esto es de esta OT». */
.btn--mat { background: var(--linea); color: var(--inv); }

/* ── Materiales: las tarjetas ──────────────────────────────────────────────
   La lista eran filas de descripción + número y el ÚNICO código visual era el
   color del texto: dos naranjas y una verde entre cuatro descripciones de ERP
   («TAPA Roda Negra 16oz/inse.vrio»). A 50cm eso no se lee sin leer.
   `auto-fit` y no dos columnas fijas: en el panel de tablet (640px) entran dos
   de ~300px, y en el celular una sola de ancho completo, que es lo que necesita
   una descripción de 30 caracteres.
   El mínimo era 240 —el ancho al que «Válv 16oz Sil/Arra.-40VF 0052» deja de
   partirse en tres renglones— y por 5 PÍXELES dejaba a la tablet de 1024, que
   es el dispositivo del piso, en UNA sola columna: dos pistas piden
   2×240+8=488 y el panel ahí mide 483. Medido con la OT 12776 (8 insumos + la
   tarjeta de alta): `grid-template-columns` resolvía a '483px', la grilla medía
   1.252 de alto y el panel quedaba con 655px de scroll. Con 228 pasa a
   '237,5px 237,5px', la grilla baja a 704,5 y el scroll a 108. A 1440 no cambia
   nada: sigue dando dos de 273,5. */
.matgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(228px, 1fr)); gap: var(--s2); }
.matcard {
  min-height: 132px; padding: var(--s3);
  display: grid; grid-template-columns: 24px 1fr; grid-template-rows: auto auto auto;
  gap: var(--s2) var(--s2); align-content: start; text-align: left;
  background: var(--card); border: 1px solid var(--hair);
  border-radius: var(--r-card); color: var(--txt); cursor: pointer;
  /* Igual que el tile y por la misma razón. Sólo el transform: en la transición
     general también viajan `background-color` y `border-color`, y acá los pinta
     el estado del insumo (--ok / --exceso) al volver de la hoja. */
  transition: transform var(--dur-press) var(--ease-out-quart);
}
.matcard:active { transform: scale(0.97); }
/* El ícono manda la columna izquierda entera: ○ pendiente, ✓ cargado, ! de más.
   El estado se lee sin leer una palabra, que es de lo que se trata. */
.matcard__i { grid-row: 1; color: var(--faint); width: 24px; height: 24px; }
.matcard__n { grid-column: 2; font-size: var(--t-fila); font-weight: 700; line-height: 1.25; }
.matcard__bar {
  grid-column: 1 / -1; height: 6px; border-radius: var(--r-pill);
  background: rgba(255,255,255,0.08); overflow: hidden;
}
/* Mismo contrato que las otras tres barras de la app: viaja `--p` y el CSS lo
   aplica con `transform: scaleX()`, compuesto en GPU. Cero propiedades de
   layout animadas en toda la hoja — es una regla, no una casualidad. */
.matcard__fill {
  display: block; height: 100%; width: 100%; border-radius: inherit;
  background: var(--faint); transform: scaleX(var(--p, 0)); transform-origin: left;
  /* Los mismos 300ms y la misma curva que las otras tres: es la única de las
     cuatro que traía timing propio (.25s y el `ease` del navegador), y en la
     tablet ≥1024 se ve al lado de las `.row__fill` de la lista. */
  transition: transform 300ms var(--ease-out-quart);
}
.matcard__v { grid-column: 1 / -1; font-size: var(--t-meta); color: var(--muted);
              font-variant-numeric: tabular-nums; }
.matcard__v--pide { color: var(--faint); }
/* Verde «ya está», naranja «se pasó». Un insumo FUERA de fórmula está cargado y
   no está en orden: queda neutro, con su rótulo — pintarlo de verde le decía lo
   contrario al único que puede revisarlo. */
.matcard--ok { border-color: color-mix(in srgb, var(--curso) 45%, var(--hair)); }
.matcard--ok .matcard__i { color: var(--curso); }
.matcard--ok .matcard__fill { background: var(--curso); }
.matcard--exceso { border-color: color-mix(in srgb, var(--pausa) 55%, var(--hair)); }
.matcard--exceso .matcard__i, .matcard--exceso .matcard__n { color: var(--pausa); }
.matcard--exceso .matcard__fill { background: var(--pausa); }
/* El alta, como una celda más de la grilla. Punteada y centrada: se lee como
   «agregar» y no como un insumo que la fórmula pide. Era una fila `.drow` en
   un `.grp` aparte, un componente y un renglón más abajo — la misma decisión
   («¿qué usé?») repartida en dos lenguajes distintos. */
.matcard--nuevo {
  border-style: dashed; border-color: var(--borde-ctrl);
  align-content: center; color: var(--muted);
}
.matcard--nuevo .matcard__i { color: var(--linea); }
.matcard--nuevo .matcard__n { font-weight: 700; color: var(--txt); }

/* ── Materiales: la TABLA (26-sep-2026, pedido del dueño) ───────────────────
   Las tarjetas decían «pide 286,2 u» en gris de 13px, y lo que el elaborador
   viene a mirar es justamente CUÁNTO pide cada MP. Una fila por MP con tres
   columnas —PIDE, CARGADO, LOTE— y el número de PIDE es el más grande de la
   fila. En tablet la tabla va a lo ancho (abajo, `.v-materiales`); en celular
   la misma fila se apila: nombre, pide, cargado, lote.
   Clases `mt__` propias y no las de `.matcard`: `.matcard` sigue viva en el
   espejo (este archivo es copia byte a byte del de mes-espejo). */
.mt__res { font-size: var(--t-body); color: var(--muted); font-variant-numeric: tabular-nums; }
.mt__res--hdr { display: none; }
.mt {
  display: flex; flex-direction: column;
  background: var(--card); border: 1px solid var(--hair); border-radius: var(--r-card);
  overflow: hidden;
}
.mt__cab { display: none; }
.mt__f {
  position: relative; display: grid; align-items: center; text-align: left;
  grid-template-columns: 28px minmax(0, 1fr) 20px;
  grid-template-areas: "i n chev" "i pide chev" "i carg chev" "i lote chev";
  gap: var(--s1) var(--s3); padding: var(--s3); min-height: 64px; width: 100%;
  background: transparent; border: 0; color: var(--txt); cursor: pointer;
  font: inherit;
}
.mt__f + .mt__f { border-top: 1px solid var(--hair); }
.mt__f:active { background: rgba(255,255,255,0.045); }
.mt__i { grid-area: i; align-self: start; width: 24px; height: 24px; color: var(--faint); }
.mt__i svg { width: 24px; height: 24px; }
.mt__n { grid-area: n; font-size: var(--t-fila); font-weight: 700; line-height: 1.25; min-width: 0; }
.mt__sub { display: block; font-size: var(--t-meta); font-weight: 500; color: var(--muted); margin-top: 2px; }
.mt__sub--mal { color: var(--pausa); font-weight: 700; }
.mt__pide, .mt__carg {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--s2);
  font-variant-numeric: tabular-nums; min-width: 0;
}
.mt__pide { grid-area: pide; }
.mt__carg { grid-area: carg; }
/* El rótulo pegado a cada cifra, sólo en celular: en tablet lo dice el
   encabezado de la columna. Mismo ancho para los dos, así las cifras alinean. */
.mt__l { min-width: 64px; font-size: var(--t-micro); letter-spacing: 0.1em;
         text-transform: uppercase; color: var(--faint); }
.mt__v { font-size: var(--t-fila); font-weight: 700; white-space: nowrap; line-height: 1.15; }
.mt__carg .mt__v { color: var(--muted); }
.mt__lleva { display: none; font-size: var(--t-meta); color: var(--faint); }
.mt__merma { font-size: var(--t-meta); color: var(--muted); white-space: nowrap; }
.mt__bar {
  flex-basis: 100%; height: 6px; margin-top: 3px; border-radius: var(--r-pill);
  background: rgba(255,255,255,0.08); overflow: hidden;
}
/* Mismo contrato que las otras barras de la app: `--p` + `scaleX()`. */
.mt__fill {
  display: block; height: 100%; width: 100%; border-radius: inherit;
  background: var(--faint); transform: scaleX(var(--p, 0)); transform-origin: left;
  transition: transform 300ms var(--ease-out-quart);
}
.mt__lote { grid-area: lote; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); min-width: 0; }
.mt__chip {
  /* Una etiqueta, no un control: el alto sale del padding y no de un
     `min-height` (el guard del espejo reserva ese valor para lo que se toca). */
  display: inline-flex; align-items: center; padding: 3px var(--s2); line-height: 1.4;
  border: 1px solid var(--hair-2); border-radius: var(--r-badge);
  font-size: var(--t-meta); font-weight: 700; font-variant-numeric: tabular-nums;
}
/* El hueco que no se reconstruye mañana: dicho en naranja, con borde. */
.mt__chip--sin { color: var(--pausa); border-color: color-mix(in srgb, var(--pausa) 60%, transparent); }
.mt__nolleva { font-size: var(--t-meta); color: var(--faint); }
.mt__chev { grid-area: chev; width: 20px; height: 20px; color: var(--faint); }
.mt__chev svg { width: 20px; height: 20px; }
.mt__f--ok .mt__i, .mt__f--ok .mt__carg .mt__v { color: var(--curso); }
.mt__f--ok .mt__fill { background: var(--curso); }
.mt__f--exceso .mt__i, .mt__f--exceso .mt__carg .mt__v { color: var(--pausa); }
.mt__f--exceso .mt__fill { background: var(--pausa); }
.mt__f--nuevo { grid-template-areas: "i n chev"; color: var(--muted); }
.mt__f--nuevo .mt__i { color: var(--linea); }
.mt__f--nuevo .mt__n { color: var(--txt); }

/* La pantalla va a lo ancho desde 820 (como el resto del split), pero la fila
   de SEIS columnas recién desde 1024: a 820 las columnas fijas (150 + 190 +
   150) le dejaban 23px al nombre de la MP. Entre 820 y 1023 queda la fila
   apilada del celular, con sus rótulos. */
@media (min-width: 820px) {
  .pg--split.v-materiales { grid-template-columns: minmax(0, 1fr); }
  .pg--split.v-materiales > .col { display: none; }
  .pg--split.v-materiales > .det { max-width: none; }
}
@media (min-width: 1024px) {
  .mt__cab, .mt__f {
    grid-template-columns: 28px minmax(0, 1fr) 150px 190px minmax(150px, 0.7fr) 20px;
    column-gap: var(--s3);
  }
  .mt__l { display: none; }
  .mt__lleva { display: inline; }
  /* El resumen sube al renglón del encabezado del panel (entre el título y
     «Volver»), donde a lo ancho sobra lugar: 40px de alto que vuelven a la
     tabla. Más angosto se recortaba (264 de 433px a 820), así que ahí —y en
     el celular, donde ese encabezado no se dibuja— queda en el cuerpo. */
  .mt__res--cuerpo { display: none; }
  .mt__res--hdr { display: block; flex: 0 1 auto; min-width: 0; text-align: right;
                  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .mt__cab {
    display: grid; align-items: end; padding: var(--s2) var(--s3);
    border-bottom: 1px solid var(--hair);
    font-size: var(--t-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint);
  }
  /* 56 y no 68: una fórmula de ocho MP (el máximo de las partidas abiertas el
     26-sep) entra entera a 1280×800 sin scroll —con el aviso de exceso arriba
     incluido—, que es lo que se viene a ver. Sigue por encima de los 44 del
     mínimo táctil: la fila entera es el blanco. */
  .mt__f { grid-template-areas: "i n pide carg lote chev"; min-height: 56px;
           padding-block: var(--s2); }
  .mt__num { text-align: right; }
  .mt__pide, .mt__carg { justify-content: flex-end; }
  /* El número que se viene a buscar: el más grande de la fila. */
  .mt__pide .mt__v { font-size: var(--n1); font-weight: 700; }
  .mt__i { align-self: center; }
  .mt__f--nuevo { grid-template-areas: "i n n n n chev"; }
}

/* ── La hoja de un insumo: las dos cifras y sus cargas (26-sep-2026) ──────
   PIDE y CARGADO arriba y en grande (lo mismo que la fila de la tabla), y
   debajo la lista de cargas: una fila por carga con hora, cuánto, lote, quién
   y sus acciones. La carga sin lote de un insumo que lo lleva se marca con el
   borde naranja: es la que hay que ir a completar. */
.mcif { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s2); }
.mcif__c {
  display: flex; flex-direction: column; gap: 2px; padding: 6px var(--s3);
  background: var(--card); border: 1px solid var(--hair); border-radius: var(--r-btn);
}
.mcif__l { font-size: var(--t-micro); letter-spacing: 0.1em; text-transform: uppercase; color: var(--faint); }
.mcif__v { font-size: var(--t-tit); font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; }
.mcif__v--ok { color: var(--curso); }
.mcif__v--exceso { color: var(--pausa); }
.mcar { display: flex; flex-direction: column; gap: var(--s2); }
.mcar__f {
  display: grid; grid-template-columns: 48px minmax(0, 1fr); grid-template-areas: "h t" "acc acc";
  gap: var(--s2) var(--s3); align-items: center; padding: var(--s3);
  background: var(--card); border: 1px solid var(--hair); border-radius: var(--r-btn);
}
.mcar__f--sin { border-color: color-mix(in srgb, var(--pausa) 55%, var(--hair)); }
.mcar__h { grid-area: h; font-size: var(--t-meta); font-weight: 700; color: var(--faint);
           font-variant-numeric: tabular-nums; align-self: start; padding-top: 2px; }
.mcar__t { grid-area: t; min-width: 0; }
.mcar__v { display: block; font-size: var(--t-fila); font-weight: 700; font-variant-numeric: tabular-nums; }
.mcar__s { display: flex; flex-wrap: wrap; gap: 0 var(--s2); font-size: var(--t-meta); color: var(--muted); margin-top: 2px; }
.mcar__lote { font-weight: 700; color: var(--txt); }
.mcar__lote--sin { color: var(--pausa); }
.mcar__env { display: block; font-size: var(--t-meta); font-weight: 700; color: var(--pausa); margin-top: 2px; }
.mcar__env--va { color: var(--muted); font-weight: 500; }
.mcar__acc { grid-area: acc; display: flex; justify-content: flex-end; min-width: 0; }
.mcar__bs { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s2); }
.mcar__bs .btn { min-height: 44px; }
.mcar__bor svg { width: 20px; height: 20px; }
@media (min-width: 820px) {
  /* En tablet las acciones van en el mismo renglón que la carga. */
  .mcar__f { grid-template-columns: 48px minmax(0, 1fr) auto; grid-template-areas: "h t acc"; }
}

/* ── El teclado numérico propio ────────────────────────────────────────────
   No hay lectores de código en los puestos (dato del dueño, 30-ago), así que
   esquivar el teclado del sistema es puro control en pantalla. Y el del sistema
   no sólo es chico: en la tablet tapa la mitad de abajo, que es donde vive todo
   lo que la hoja pide después del número — el mismo motivo por el que el error
   del parcial se mudó ARRIBA del campo.
   60px por tecla: por encima de los 44 del mínimo táctil y de los 48 de
   `.chip`, porque acá se toca con guante y sin mirar. */
/* La zona de carga: contenido a la izquierda, teclado a la derecha.
   Apilado, el cuerpo de la hoja daba 237px contra 294 de contenido —el campo de
   lote 57px abajo del fold— y moverlo al pie dejaba un pie de 429, o sea el 63%
   del marco: exactamente el problema que esta misma ronda le sacó al panel de
   la OT. Al lado, la altura es `max(contenido, teclado)` y entra todo.
   Una columna por defecto (celular) y dos recién en la media query de tablet:
   el reparto es del ANCHO disponible, no del componente. */
.tecl__zona { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
.tecl__lote { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }
/* El @media va ACÁ y no en el bloque grande de 820: ese bloque vive 1.100
   líneas más arriba que estas reglas, y con la misma especificidad (0-1-0) el
   que gana es el que viene DESPUÉS en el archivo — o sea la regla base, que
   dejaba el teclado en una columna en tablet. Se ve sólo midiendo: la tecla
   daba 187px de ancho en vez de 80. Pegado a su base no hay orden que valga.
   264px es el ancho al que las teclas quedan de 80×60, el mismo blanco que
   tienen apiladas. */
@media (min-width: 820px) {
  /* Las áreas invierten el orden del DOM: en celular la columna única va
     números → teclado → lote, que es el orden en que se usa y deja el teclado a
     la vista al abrir; en tablet el teclado se va a la derecha y el lote sube
     al hueco que deja, sin tocar el markup. `align-items: start` para que la
     grilla no estire el teclado a la altura de la columna de al lado. */
  .tecl__zona {
    grid-template-columns: minmax(0, 1fr) 264px;
    grid-template-areas: "campos teclado" "lote teclado";
    align-items: start;
  }
  .tecl__campos { grid-area: campos; }
  .tecl        { grid-area: teclado; }
  .tecl__lote  { grid-area: lote; }
}

/* El mismo problema de orden que el teclado de acá arriba, y por eso vive acá:
   la regla base de `.build-tag` está 460 líneas MÁS ABAJO y con la misma
   especificidad (0-1-0) gana la que viene después.

   🔴 `.build-tag` es `display: block; margin-top: 8px` EN FLUJO, y es el hermano
   que va DESPUÉS del `.pg` de 100dvh: 18 de alto + 8 de margen = 26px exactos de
   documento sobre un layout que es `overflow: hidden`. Medido: a 1280×800 el
   documento daba 826 y aparecía barra de scroll en el hub, en el detalle y en
   /plan —las tres pantallas que el diseño fija a la altura de la pantalla—.
   Pasaba en TODOS los anchos de tablet para arriba (1180×820 daba 846).

   Se saca del flujo SÓLO de 820 para arriba, que es donde las tres objeciones
   del comentario de la regla base no aplican: de acá arriba `--nav-h` es 0 (no
   hay nav inferior que obligue a una variante `--plana`), no hay pulgar
   apoyado abajo que se pelee por la pila de z, y sobre todo no hay nada que
   «se vaya scrolleando» —el split no scrollea la página, y esos 26px eran
   justamente lo único que la hacía scrollear—. En celular queda como estaba:
   verificado a 390×844, el documento mide lo mismo con y sin este cambio.
   En las páginas planas (/calidad, /deposito, /tableros), que sí scrollean, el
   dato queda anclado en la esquina: medido a 1280×800 cae en x=1245, fuera del
   contenido, que termina en 1176. */
@media (min-width: 820px) {
  .build-tag { position: fixed; right: var(--s2); bottom: 2px; margin-top: 0; z-index: 1; }
}
.tecl { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
/* `align-self: start`: sin esto la grilla estira el teclado a la altura de la
   columna de al lado y las teclas crecen a 90px de alto cuando el lote agrega
   renglones — un teclado que cambia de tamaño según el insumo. */
.tecl { align-self: start; }
.tecl__k {
  min-height: 60px; border-radius: var(--r-btn); cursor: pointer;
  background: var(--card); border: 1px solid var(--borde-ctrl); color: var(--txt);
  font-size: var(--n1); font-weight: 600; font-variant-numeric: tabular-nums;
  display: inline-flex; align-items: center; justify-content: center;
  /* SÓLO el transform: el `:active` de acá abajo cambia además el `background`
     y meterlo en la transición le colgaría 180ms de cola a cada tecla tipeando
     un número de cinco dígitos. */
  transition: transform var(--dur-press) var(--ease-out-quart);
}
/* 0.075 y no el 0.055 del resto de la app: es el único `:active` sin transición
   de fondo, sobre un control de repetición (hasta 5 toques seguidos por
   número), así que el flash tiene que ser instantáneo — ahí el fondo ES todo
   el acuse, no un refuerzo. */
.tecl__k:active { background: rgba(255,255,255,0.075); transform: scale(0.96); }
/* La coma y el borrar no son dígitos: se ven distinto para que el pulgar no los
   confunda con el 0 que tienen al lado. */
.tecl__k--aux { color: var(--muted); font-size: var(--t-tit); }

/* Los destinos del teclado. Son <button> y no <input> a propósito: tocarlos
   elige dónde escribe el teclado propio, no abre el del sistema. */
.tecl__campos { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: var(--s2); }
.tecl__campo {
  min-height: 72px; padding: var(--s2) var(--s3); border-radius: var(--r-btn);
  display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
  /* 2px y no `var(--s1)`: rótulo y cifra son UN dato, con 4 se leen como dos
     bloques — y el rótulo largo («Merma (si tiraste)») ya envuelve a 130px de
     columna, así que subir el gap sin medir en el navegador es riesgo gratis. */
  gap: 2px; text-align: left; cursor: pointer;
  background: var(--card); border: 1px solid var(--hair); color: var(--txt);
}
/* Los números, pegados abajo. Con `justify-content: center` y un rótulo que
   envuelve —«Merma (si tiraste)» a 130px de columna— los dos valores quedaban a
   distinta altura y el par se leía como si uno fuera más importante que el
   otro. Alineados al pie comparten baseline pase lo que pase con el rótulo. */
.tecl__campo .tecl__v { margin-top: auto; }
/* El elegido se marca con el color de la línea y un borde de 2px: con 1px y
   sólo el color, a contraluz no se distinguía cuál de los dos iba a recibir el
   próximo dígito — y equivocarse ahí carga la merma como consumo. */
.tecl__campo--on {
  border: 2px solid var(--linea);
  background: color-mix(in srgb, var(--linea) 10%, transparent);
}
/* ── La botonera de lotes ──────────────────────────────────────────────────
 * Los lotes en stock del insumo, uno por botón. Grande y no chip: la lista es
 * CORTA de verdad (mediana 2, máximo 7 medidos el 31-ago sobre los 105 insumos
 * que piden lote), así que no hay espacio que ahorrar apretándolos — y esto lo
 * toca alguien de pie, con guantes, al lado de una máquina en marcha.
 *
 * `auto-fit` con piso de 148px y no una columna sola: con dos lotes cortos
 * («F7335», «F6396») una columna deja dos renglones de 62px con el 70% del
 * ancho vacío, y la hoja empuja el pie fuera del fold — el mismo problema que
 * ya costó sacar la tarjeta de estado del panel. Con la grilla entran los dos
 * en un renglón y siguen midiendo 148px cada uno, arriba del mínimo de 44. */
.lotes {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: var(--s2);
}
.lote {
  min-height: 56px; padding: var(--s2) var(--s3); border-radius: var(--r-btn);
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  /* Mismo 2px y misma razón que `.tecl__campo`: número y detalle son UN dato,
     y el detalle («de esta OT · 420 u · ingresó 21/08») también envuelve. */
  gap: 2px; text-align: left; cursor: pointer; min-width: 0;
  background: var(--card); border: 1px solid var(--hair); color: var(--txt);
  transition: transform var(--dur-press) var(--ease-out-quart);
  touch-action: manipulation;
}
.lote:active { transform: scale(0.97); }
/* Borde de 2px como el campo numérico elegido y por el mismo motivo: a
   contraluz, sólo el tinte de fondo no alcanza para saber cuál quedó puesto —
   y el lote equivocado es un error que no se detecta nunca más. */
.lote--on {
  border: 2px solid var(--linea);
  background: color-mix(in srgb, var(--linea) 10%, transparent);
}
/* El número del lote es EL dato: se compara contra lo que dice el tambor. */
.lote__n { font-size: var(--t-fila); font-weight: 700; font-variant-numeric: tabular-nums;
           letter-spacing: .02em; line-height: 1.15;
           overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.lote--on .lote__n { color: var(--linea); }
/* Cuánto hay y desde cuándo. Envuelve en vez de recortarse: «de esta OT · 420 u
   · ingresó 21/08» es la razón por la que se elige un botón y no el otro. */
.lote__d { font-size: var(--t-meta); color: var(--muted); line-height: 1.2; }
/* El escape, deliberadamente más callado que la botonera: es la última entrada
   de la hoja que abre el teclado del sistema. Sin caja llena no compite con los
   lotes reales, pero sigue midiendo 56 de alto — no es una trampa para el dedo. */
.lote--otro { border-style: dashed; color: var(--muted); font-size: var(--t-body);
              align-items: center; justify-content: center; text-align: center; }
.lote__otro { display: flex; flex-direction: column; gap: var(--s2); }

.tecl__v { font-size: var(--n1); font-weight: 600; font-variant-numeric: tabular-nums;
           letter-spacing: .015em; line-height: 1.1; }
/* El cero de «todavía no tipeó nada» no puede pesar lo mismo que un valor real. */
.tecl__v--cero { color: var(--faint); }

/* El aviso de material sin cargar, en la hoja de Terminar. Es el único
 * `.aviso--warn` que trae una acción adentro, así que necesita apilar: título,
 * explicación y botón. Los demás son una línea suelta y no se tocan. */
/* En FILA y no apilado: apilado medía 123px en tablet y 140 en celular (el
 * 32,7% del cuerpo visible de la hoja) y empujaba las cuatro opciones de causa
 * —obligatorias para poder cerrar— entre 184 y 268px fuera de vista. El
 * operario veía el botón de cerrar en gris y el motivo por el que estaba gris
 * quedaba abajo del fold, con 44px de un KPI como única pista de que había más.
 * `wrap` para que en celular angosto el botón baje solo en vez de apretar el
 * texto a dos caracteres por renglón. */
.aviso--mat { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
/* Base 24ch y no 12: con 12 el texto se conformaba con la columna que le
   quedaba al lado del botón —152,8px de ancho a 390×844, siete renglones, 157,5
   de alto: el 41% del cuerpo de la hoja— en vez de empujar el botón a su propio
   renglón. Con 24ch el wrap se dispara por debajo de ~420px, el texto usa los
   346 completos y baja de siete renglones a tres (~80px menos). */
.aviso__t { font-weight: 700; color: var(--txt); flex: 1 1 24ch; }
/* 48px y no los 44 del mínimo: es el único control de la app que aparece dentro
 * de un aviso, o sea el único que el operario no fue a buscar. El que no lo
 * quiere tocar tampoco debería tocarlo sin querer. */
.aviso--mat .btn--chico { min-height: 48px; flex: 0 0 auto; }

/* ── Los campos de la hoja de alta de un insumo ────────────────────────────
   Rótulo arriba, campo abajo. Es lo único que sobrevivió del formulario que se
   desplegaba dentro de la fila: el resto de aquel bloque (`.mat__form`,
   `.mat__ref`, `.mat__acc`, `.grp--mat` y `.btn__nota`) quedó sin un solo uso
   en templates ni en JS cuando la lista pasó a tarjetas y el formulario a hoja,
   y se borró el 2-sep —4.297 bytes que ningún operario llegaba a renderizar. */
.mat__campo { display: flex; flex-direction: column; gap: var(--s1); }
/* El lote es alfanumérico y por eso usa `.busca` y no `.num`, pero eso lo dejaba
 * en 48px de alto y 18px de letra al lado de dos campos de 64 y 32: tres campos
 * en la misma tarjeta con dos vocabularios distintos. Los tres pasan el mínimo
 * táctil, así que no es accesibilidad — es que se lean como el mismo formulario.
 * El cuerpo de letra NO se iguala a los 32px del número: un código de partida
 * es una cadena que se copia de una etiqueta, no una cifra que se lee de lejos.
 * Va en las DOS hojas que piden un lote a mano: la de alta de un insumo fuera
 * de fórmula (`.mat__campo`, dos campos) y el «Otro lote…» de la hoja de carga
 * (`.lote__otro`), que pedía lo mismo con el mismo placeholder y el mismo
 * maxlength en 48px. Sólo CSS: el input del escape ya está rotulado por
 * `aria-labelledby="hm-lote-l"` y envolverlo en un `<label>` pintaría «Lote /
 * partida» dos veces en la misma hoja. */
.mat__campo .busca, .lote__otro .busca { min-height: 64px; width: 100%; }

/* La píldora «Volver al estado real» es `position: fixed` y en celular quedaba
 * encima de «Terminar OT»: con el botón de Materiales el pie creció 72px y la
 * fila de cerrar cayó justo abajo — `elementFromPoint` sobre su centro devolvía
 * la píldora. Es recuperable scrolleando, pero el toque que el operario da
 * primero es el que falla.
 * La píldora es un control DEL ESPEJO y no va a existir en producción; el
 * espacio que se reserva sí tiene que quedar, porque los 394px de pie sobre 844
 * de pantalla no cambian con el cutover. */
@media (max-width: 819px) {
  .con-real .acts { padding-bottom: 56px; }
}
