/* ============================================================
   VIMPACT — v2

   Blanco y negro puro. Grilla a la vista. Dos regímenes tipográficos.
   Sin gradientes, sin glows, sin blobs, sin iconos, sin emojis.

   El color lo ponen los carteles: cuando lleguen las fotos de los
   trabajos, van a ser lo único a color de la página.
   ============================================================ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--fondo);
  color: var(--texto);
  font-family: var(--sistema);
  font-size: var(--cuerpo-tam);
  line-height: var(--cuerpo-alto);
  font-variation-settings: "wdth" var(--cuerpo-wdth), "wght" var(--cuerpo-wght);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, svg, canvas { display: block; max-width: 100%; }

/* [hidden] LE GANA A TODO.
   El atributo `hidden` trae `display: none` de la hoja del navegador,
   que tiene la especificidad mas baja que existe: cualquier regla de
   clase con `display` se lo come. Los enlaces de WhatsApp y Email del
   pie tenian hidden=true y display:flex al mismo tiempo, asi que el
   pie ofrecia un WhatsApp que no existe.

   Es el patron que vamos a volver a usar el dia que aparezca el numero,
   asi que va una vez y para toda la pagina. Hay un test que falla si un
   elemento con hidden termina con display distinto de none. */
[hidden] { display: none !important; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
::selection { background: var(--texto); color: var(--fondo); }

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.oculto-visual {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ============================================================
   GRANO

   Se pinta una vez sobre toda la página y no depende del scroll.
   Rompe la lisura de la placa generada y unifica el sitio entero.
   Pesa cero: es un feTurbulence en un data URI.
   ============================================================ */

.grano {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.028;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='r'><feTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='160' height='160' filter='url(%23r)'/></svg>");
  background-size: 160px 160px;
}

/* ============================================================
   TIPOGRAFÍA — dos regímenes
   ============================================================ */

/* ANTON SE QUEDA SOLO EN EL CLAIM DEL HERO.
   Anton a 76px repetida seis veces hace que la página grite todo el
   tiempo, y le compite al hero, que es el único lugar donde el grito
   tiene sentido. El hero funciona PORQUE es la excepción.

   Los títulos de sección van en Archivo con peso alto y ancho abierto:
   la misma familia del cuerpo, así que la página tiene una sola voz y
   un solo grito. */
/* ANTON, DESPUES DE HABERLA SACADO Y VUELTO A PONER.

   Se probo Archivo en los titulos de seccion y no dio: con la pagina
   desalineada Anton parecia gritar, pero lo que gritaba era el
   desorden. Con el eje puesto, Anton se lee como caracter y Archivo se
   lee como una web mas.

   O sea que el trabajo de haberla sacado no fue perdido: fue lo que
   permitio juzgarla. No se puede evaluar una tipografia sobre una
   composicion rota. */
.display {
  font-family: var(--display);
  line-height: var(--display-alto);
  letter-spacing: var(--display-track);
  text-transform: uppercase;
  margin: 0;
}

   La única que sigue en Anton es .cartel__claim (y su versión de
   celular), que la declara directo en su propia regla. Si alguna vez
   aparece un segundo lugar con Anton, la pregunta es por qué, no cómo. */

/* ============================================================
   >>> NINGUN TITULO PARTE UNA PALABRA. <<<  2026-08-15
   COMO REGLA, NO CASO POR CASO.

   Es la segunda vez que aparece lo mismo. La primera fue "IM-PACTO" en
   el hero y se arreglo AHI, en la regla del claim de celular; volvio en
   otro titulo —"MARCAS QUE CONFIAN EN VIM-PACT"— porque el arreglo era
   de un caso y el problema era de la pagina.

   La regla, entonces, es de la pagina: si la palabra no entra, BAJA
   ENTERA al renglon siguiente; si aun asi no entra, se achica —para eso
   todos los tamanos de titulo son `clamp()` con un termino en vw—.
   Nunca se corta.

   Va con las dos escrituras: Safari todavia pide `-webkit-hyphens`, y
   ahi es donde se veia el guion en el telefono de Mati.
   ============================================================ */
h1, h2, h3, h4,
.display,
.prod__nombre,
.paso__titulo {
  hyphens: none;
  -webkit-hyphens: none;
}

.d-xxl { font-size: clamp(38px, 8.5vw, 128px); }
.d-xl  { font-size: clamp(30px, 5.5vw, 76px); }
.d-l   { font-size: clamp(24px, 3.4vw, 46px); }
.d-m   { font-size: clamp(19px, 2.1vw, 28px); }

/* LA MONOESPACIADA, RACIONADA (corrección 7)
   Antes rotulaba cada sección con su número, que es un gesto prestado.
   Ahora queda solo para datos: medidas, coordenadas, el precio de un
   metro cuadrado. Cosas que de verdad son datos. Nunca para títulos ni
   para navegación. */
/* LA ETIQUETA TÉCNICA, YA SIN MONOESPACIADA.
   Space Mono cumplía el rol de "esto es un dato, no prosa", pero el rol
   lo hace el tracking abierto y las versalitas, no el ancho fijo de la
   fuente: ninguno de los veintitrés lugares donde estaba era una tabla
   ni una cifra alineada. Con Archivo en peso medio y el mismo tracking
   dice lo mismo, y la página baja de tres familias a dos.

   Anton para display, Archivo para todo lo demás. Nada más. */
.mono {
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--secundario);
}

/* UN BLOQUE ACOTADO SE CENTRA SOLO, Y NO ES OPCIONAL.
   Este parrafo estuvo 405px corrido a la izquierda durante tres tandas.
   Tenia text-align: center heredado, asi que el RENGLON estaba centrado
   adentro de una caja que no lo estaba: exactamente el error que el
   sistema decia evitar.

   La causa fue que la clase que lo centraba nunca llego al HTML —un
   reemplazo que no encontro su texto y devolvio el original sin avisar—
   y nadie se entero porque el elemento igual reportaba `center`.

   La leccion no es "acordate de poner la clase": es que un bloque con
   max-width tiene que centrarse por defecto y que olvidarse sea
   imposible. `margin: 0 auto` en vez de `margin: 0`. */
.cuerpo { max-width: 62ch; color: var(--secundario); margin: 0 auto; }

/* ============================================================
   GRILLA — con los filetes a la vista

   Las líneas visibles son buena parte del carácter de esta estética
   y cuestan cero.
   ============================================================ */

.contenedor {
  max-width: var(--max-w);
  margin: 0 auto;
  padding-inline: var(--pad-x);
}

.seccion {
  border-top: var(--filete) solid var(--borde);
  padding-block: clamp(56px, 8vw, 120px);
}

/* La sección de contacto termina cuando termina el formulario: no hace
   falta el aire de abajo porque el pie ya trae el suyo. */
#contacto { padding-bottom: clamp(24px, 3vw, 44px); }


/* ============================================================
   EL SISTEMA DE ALINEACIÓN

   >>> ESTO ES UNA REGLA, NO UN PARCHE. <<<

   Se corrigió tres veces con arreglos sueltos y volvió tres veces. El
   motivo era simple: NO HABÍA REGLA. Cuando no hay regla, todo cae al
   valor por defecto, y el valor por defecto de `text-align` es la
   izquierda. Por eso los seis títulos de sección estaban a la
   izquierda: no es que se escapó uno, es que nadie decidió.

   Son cuatro casos y no hay un quinto. Si aparece algo que no encaja en
   ninguno, la discusión es a cuál de los cuatro pertenece — no se
   inventa un `text-align` suelto en la regla del componente.

   LA REGLA, y es una sola: EN ESTA PÁGINA TODO EL TEXTO VA CENTRADO.
   Encabezados, bajadas, nombres de tarjeta, descripciones, etiquetas de
   paso, pie. Todo. Por eso el centrado no se aplica componente por
   componente —así fue como volvió tres veces— sino que se declara una
   vez en el contenedor de sección y baja por herencia.

   HAY EXACTAMENTE DOS EXCEPCIONES Y ESTÁN ESCRITAS ACÁ:

   a · LA NAVEGACIÓN (.barra). Es navegación, no contenido.
   b · .lectura, para un párrafo de más de tres líneas: se centra EL
       BLOQUE y el renglón queda a la izquierda adentro, porque centrar
       seis renglones hace que el ojo pierda el punto de retorno.
       Hoy no hay ninguno en la página. La clase existe para el día que
       aparezca, no como decoración.

   Si aparece algo que parece necesitar una tercera excepción, la
   respuesta por defecto es que no. Un `text-align` suelto en la regla
   de un componente es exactamente la recaída que este sistema vigila, y
   hay un test que falla si aparece uno.

   .eje y .centrado se conservan para acotar y centrar BLOQUES —ancho
   máximo y `margin-inline: auto`—, que es otra cosa que alinear texto.
   ============================================================ */

/* De acá baja el centrado a toda la página. Una declaración, no
   setenta y cinco. */
.seccion, .pie { text-align: center; }

/* excepción a · la navegación */
.barra { text-align: start; }

.eje {
  max-width: 800px;
  margin-inline: auto;
  text-align: center;
}
/* La bajada adentro del eje también va centrada: son una o dos líneas,
   y a esa longitud centrar no cuesta nada. */
.eje > * { margin-inline: auto; }

.lectura {
  max-width: 65ch;
  margin-inline: auto;
  text-align: start;
}

/* .tabla ya sólo acota el ancho: la alineación la hereda como todo lo
   demás. Antes forzaba `start` y era la regla que dejaba 75 elementos
   de texto contra el margen izquierdo. */
.tabla { width: 100%; }

.centrado { text-align: center; }

/* El encabezado de sección ES el caso 1 del sistema. Antes era un grid
   de 2fr/3fr que empujaba todo contra el margen izquierdo. */
.seccion__cab {
  max-width: 800px;
  margin-inline: auto;
  margin-bottom: clamp(32px, 4vw, 60px);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.seccion__cab > * { margin-inline: auto; }

/* rejilla de 12 / 4 columnas con filetes entre celdas */
.rejilla {
  display: grid;
  gap: var(--filete);
  background: var(--borde);
  border: var(--filete) solid var(--borde);
  grid-template-columns: repeat(1, 1fr);
}
.rejilla > * { background: var(--fondo); }

@media (min-width: 620px)  { .rejilla { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .rejilla { grid-template-columns: repeat(4, 1fr); } }

/* ============================================================
   BARRA SUPERIOR
   ============================================================ */

.barra {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px var(--pad-x);
  border-bottom: var(--filete) solid var(--borde);
  background: color-mix(in srgb, var(--fondo) 88%, transparent);
  backdrop-filter: saturate(1) blur(6px);
}

.barra__marca { display: flex; align-items: center; gap: 9px; }
/* EL ARCHIVO ESTABA MAL ELEGIDO, NO EL TAMANO.
   Usaba /ojo.png, que es el lienzo de 680x453 donde el dibujo ocupa el
   50% del ancho y el 28% del alto: de 26px de caja, el trazo real media
   13. Y encima ojo.png NO TIENE PUPILA, porque en el hero la pupila es
   un elemento aparte que sigue al mouse. Sin pupila el ojo se lee como
   dos flechas enfrentadas.

   /ojo-marca-blanco.png es el trazo recortado CON la pupila compuesta y
   con tinta clara, asi que la caja del <img> es el dibujo y el ancho que
   se pide es el ancho que se ve.

   LA VARIANTE BLANCA EXISTE PORQUE LA OSCURA ES INVISIBLE ACA: la barra
   es negra al 88% y la tinta del archivo original es #111. Negro sobre
   negro. Se resuelve con un parametro de herramientas/componer-ojo.py y
   NO con filter: invert(), que se rompe con forced-colors y ademas no
   deja una fuente de verdad. Ver la tabla del lienzo contra el trazo en
   ARQUITECTURA.md. */
.barra__ojo { width: 33px; height: auto; }
/* La marca de la barra queda en Archivo a propósito: es el logotipo,
   no una pieza de display. Pasarla a Anton sería redibujar la marca. */
.barra__nombre {
  font-family: var(--sistema);
  font-variation-settings: "wdth" 118, "wght" 800;
  font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase;
}

.barra__nav { display: none; gap: 22px; }
@media (min-width: 860px) { .barra__nav { display: flex; } }

.barra__link {
  display: flex; align-items: baseline;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 600;
  font-size: 13px;
  letter-spacing: 0.01em;
  color: var(--secundario);
  transition: color var(--t-corto) var(--ease);
}
.barra__link:hover, .barra__link[aria-current="true"] { color: var(--texto); }

/* ============================================================
   LOS BOTONES — tres niveles y ninguno más

   Había tres tratamientos sueltos sin criterio: uno con filete
   completo, ocho sin filete ninguno, uno con fondo claro. Y el problema
   de fondo era peor que la falta de sistema: ocho de los diez eran
   texto con una rayita abajo, y UN TEXTO SUBRAYADO NO SE LEE COMO
   BOTÓN — se lee como un enlace o como un campo. En el generador, que
   es la pantalla que se muestra en la reunión, eso pasaba cuatro veces
   seguidas.

   SOBRE EL VIDRIO, que se pidió y que se resolvió así: sobre fondo
   negro plano un botón de vidrio NO EXISTE. El efecto es un desenfoque
   de lo que hay detrás, y detrás no hay nada: queda un rectángulo gris
   translúcido, que se ve peor que un botón sólido. Vidrio sólo donde
   hay imagen atrás, que en esta página son tres lugares: el lienzo del
   generador, el mapa y el hero.

   LOS TRES NIVELES:
     .btn          primario   · la acción principal de su pantalla
     .btn--2       secundario · acciones de apoyo
     .btn--3       terciario  · volver y cancelar, sin caja
     .btn--vidrio  el secundario, pero sobre imagen

   Lo común a los tres: mismo radio (cero en todos), misma transición,
   foco que no depende del color, y 44px de área táctil en celular.
   Documentado en docs/ARQUITECTURA.md.
   ============================================================ */

.btn,
.btn--2,
.btn--3 {
  --btn-alto: 54px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: var(--btn-alto);
  padding: 0 28px;
  border: var(--filete) solid transparent;
  border-radius: 0;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;
  background: none;
  color: var(--texto);
  transition: background-color var(--t-corto) var(--ease),
              color var(--t-corto) var(--ease),
              border-color var(--t-corto) var(--ease),
              opacity var(--t-corto) var(--ease);
}

/* --- primario: relleno claro. Es el que ya tenía ENVIAR y funciona. --- */
.btn { background: var(--texto); color: var(--fondo); }
.btn:hover { opacity: 0.82; }

/* --- secundario: filete claro, fondo transparente --- */
.btn--2 { border-color: var(--secundario); }
.btn--2:hover { border-color: var(--texto); background: #151515; }

/* --- terciario: sin caja. El subrayado NO es permanente: aparece al
   pasar el mouse y al enfocar. Un subrayado fijo es lo que hacía que
   ocho botones se leyeran como enlaces. --- */
.btn--3 {
  min-height: 44px;
  padding: 10px 0;
  color: var(--secundario);
  letter-spacing: 0.14em;
}
.btn--3:hover,
.btn--3:focus-visible {
  color: var(--texto);
  text-decoration: underline;
  text-underline-offset: 5px;
}

/* --- sobre imagen: el secundario con vidrio, con respaldo sólido --- */
.btn--vidrio {
  border-color: rgba(230, 230, 230, 0.5);
  background: rgba(10, 10, 10, 0.72);
}
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
  .btn--vidrio {
    background: rgba(230, 230, 230, 0.1);
    -webkit-backdrop-filter: blur(10px) saturate(0.6);
    backdrop-filter: blur(10px) saturate(0.6);
  }
}
.btn--vidrio:hover { border-color: var(--texto); background: rgba(230, 230, 230, 0.2); }

/* El foco no depende del color: un marco doble se ve sobre cualquier
   fondo, incluido el vidrio sobre una foto clara. */
.btn:focus-visible,
.btn--2:focus-visible,
.btn--3:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--fondo), 0 0 0 4px var(--texto);
}

/* 44px es el mínimo táctil. En celular los de caja van al ancho de su
   columna: en un pulgar, un botón angosto se falla. */
@media (max-width: 720px) {
  .btn, .btn--2 { width: 100%; --btn-alto: 52px; }
}

/* ============================================================
   ENLACES DE TEXTO

   No son botones: llevan a otro lado, y por eso conservan la flecha.
   Un botón que ejecuta una acción acá mismo NO lleva flecha — la
   flecha promete una navegación que no ocurre.
   ============================================================ */

.enlace {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 0;
  border-bottom: var(--filete) solid currentColor;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620; font-size: 11.5px;
  letter-spacing: 0.14em; text-transform: uppercase;
  transition: opacity var(--t-corto) var(--ease);
}
.enlace:hover { opacity: 0.6; }
.enlace::after { content: "→"; }

/* ============================================================
   MOVIMIENTO

   opacity + translateY(40px) en bloques grandes, 10px en chicos,
   250-300ms, escalonado 55ms. Nada más: ni escalas, ni rotaciones, ni
   desenfoques. En una página que es un cartel, el movimiento tiene que
   ser el de una luz encendiéndose, no el de una presentación.
   ============================================================ */

.entra { opacity: 0; transform: translateY(40px); }
.entra--chico { transform: translateY(10px); }
.entra.visible {
  opacity: 1; transform: none;
  transition: opacity var(--t-largo) var(--ease), transform var(--t-largo) var(--ease);
  /* el escalonado sale del índice que pinta el JS; sin --i vale 0 */
  transition-delay: calc(var(--i, 0) * var(--escalonado));
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .entra { opacity: 1; transform: none; }
}

/* ============================================================
   EL HERO — la página es una demostración del producto

   Una escena: gente en silueta mirando un cartel iluminado, y adentro
   de ese cartel está el contenido. El visitante no ve un sitio web de
   una cartelería: ve un cartel de Vimpact, y ve gente mirándolo.

   El texto adentro del cartel es TEXTO DE VERDAD, seleccionable e
   indexable. Escala solo con la imagen.
   ============================================================ */

.hero {
  position: relative;
  height: 100svh;
  overflow: hidden;
  background: var(--fondo);
}

/* EL VIDEO DEL HERO NO EXISTE EN ESCRITORIO. Se apaga por CSS y ademas
   sus <source> llevan `media`, asi que tampoco se descarga. */
.hero__video { display: none; }

/* la escena entera: foto + cartel. Es lo que escala al scrollear. */
.hero__escena {
  position: absolute;
  inset: 0;
  transform-origin: 50% 45%;   /* el centro del cartel, no el de la foto */
  will-change: transform, opacity;
}

/* --- EL LIENZO ---
   Hace a mano lo que haría `object-fit: cover`, y tiene que hacerlo a
   mano por un motivo concreto: el cartel se posiciona en porcentajes
   sobre la foto, y con object-fit el navegador recorta ADENTRO del
   <img> sin que el CSS de afuera pueda saber dónde quedó la imagen.
   Los porcentajes apuntarían a la caja y no a la foto, y el texto se
   despegaría del cartel apenas cambiara la proporción de la ventana.

   Con una caja real que siempre tiene la proporción de la foto y que
   se agranda hasta tapar la pantalla, el cartel sigue a la foto
   siempre. Es la misma regla de cover, escrita donde el CSS la ve. */
/* EL LATIDO VA ACA Y NO EN LA FOTO, y el motivo es el unico que
   importa: el claim, el ojo y la placa estan pintados SOBRE esta
   superficie. Si la foto escala y ellos no, se despegan y el truco se
   rompe. El lienzo los contiene a todos, asi que late el conjunto.

   `scale` va como propiedad suelta y no adentro de `transform`: asi
   convive con el `translate` de centrado sin pisarlo. Y los `cqw` del
   cartel salen del layout, no de la escala visual, o sea que el cartel
   acompana exacto.

   La curva es de latido y no un seno: sube rapido y baja lento. */
.hero__lienzo {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  animation: latir 5.6s cubic-bezier(0.28, 0.9, 0.4, 1) infinite;
  width: max(100%, calc(100svh * 4096 / 2294));
  aspect-ratio: 4096 / 2294;
  container-type: inline-size;   /* de acá salen los cqw de adentro */
}

@keyframes latir {
  0%   { scale: 1; }
  18%  { scale: 1.008; }
  100% { scale: 1; }
}

/* EL PARALAJE, con profundidad.
   La foto se mueve MAS que la placa: esa diferencia es la sensacion de
   profundidad. Si todo se moviera igual no se siente nada.

   El claim y el ojo NO se animan por separado: son hijos de .cartel y
   viajan con el, porque estan pintados sobre esa superficie.

   Las variables las escribe hero.js con interpolacion. Sin JS, sin
   mouse o con prefers-reduced-motion valen 0 y no pasa nada. */
.hero__foto {
  width: 100%; height: 100%; display: block; object-fit: cover;
  /* un pelo de sobreescala: sin esto el desplazamiento descubre el borde */
  scale: 1.02;
  translate: var(--par-x, 0px) var(--par-y, 0px);
}

/* --- LA CAJA DEL CARTEL, EN PERSPECTIVA ---
   El cartel de la foto está visto desde abajo: el borde de abajo es un
   3,22% más ancho que el de arriba, y el lado izquierdo es 7px más alto
   que el derecho. O sea que además de inclinación hay un poco de giro
   horizontal.

   Los valores salen de ajustar una proyección contra las cuatro
   esquinas medidas sobre la placa: el error máximo queda en 3,2px sobre
   1600 de ancho (0,20%). Como el contenido apoya con margen adentro del
   blanco y no toca los bordes, ese error no se ve.

   `perspective` va en cqw y no en px a propósito: es una longitud, así
   que si fuera fija la perspectiva se deformaría al cambiar el tamaño
   de la ventana. En cqw escala con la foto y el cartel queda pegado a
   cualquier ancho. */
.cartel {
  position: absolute;
  left: 50.01%;
  top: 45.33%;
  width: 70.81cqw;
  height: 22.75cqw;
  /* la mitad del recorrido de la foto: el cartel esta mas cerca */
  translate: calc(-50% + var(--par-x, 0px) * 0.45)
             calc(-50% + var(--par-y, 0px) * 0.45);
  transform: perspective(128.22cqw) rotateX(10.49deg) rotateY(0.63deg);

  display: grid;
  grid-template-columns: auto max-content;
  align-content: center;      /* el conjunto centrado en vertical */
  justify-content: center;    /* regla 3: margen igual a los dos lados */
  column-gap: 0.953em;        /* regla 2: una línea de alto */

  font-size: 5.3cqw;          /* de acá salen todos los em de adentro */
  color: var(--cartel-texto);
}

/* el contenido va por debajo de las capas de luz del hero vivo */
.cartel > .ojo,
.cartel > .cartel__claim,
.cartel > .cartel__bajada { position: relative; z-index: 1; }

/* --- EL OJO ---
   Regla 1: el alto NO sale de un porcentaje del cartel, sale del alto
   del bloque de dos líneas del claim. Los dos crecen juntos y se leen
   como una marca al lado de un texto, no como una viñeta decorativa.

   OJO CON EL ARCHIVO. El PNG original es 1536x1024 pero el dibujo
   adentro mide 766x284: la mitad del ancho y el 28% del alto, el resto
   es aire transparente. Midiendo la caja, el ojo se ve chico y los
   márgenes dan iguales por número pero no por ojo. Por eso acá se usa
   /ojo-trazo.png, que es el mismo dibujo recortado a su tinta.

   Y por eso la pupila es 9,437% y no 4,706%: es el MISMO diámetro del
   archivo real (4,706% del lienzo de 1536) expresado contra el trazo.
   Los dos números describen la misma pupila; cambia contra qué se mide.
   Si algún día se vuelve al archivo sin recortar, hay que volver a
   4,706%. */
.ojo {
  position: relative;
  grid-row: 1;
  align-self: center;
  width: 4.919em;
  aspect-ratio: 766 / 284;
}
.ojo img { width: 100%; height: 100%; }
.ojo__pupila {
  position: absolute;
  top: 50%; left: 50%;
  width: 9.437%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--cartel-texto);
  transform: translate(-50%, -50%);
}

/* --- EL CLAIM ---
   El corte de línea va escrito con <br> y no librado al ancho: así el
   bloque mide siempre dos líneas exactas, que es de donde sale el alto
   del ojo. Si lo partiera el navegador, un ancho raro lo pasaría a tres
   y el lockup entero se rompería. */
.cartel__claim {
  grid-row: 1;
  grid-column: 2;
  font-family: var(--display);
  font-size: 1em;
  line-height: 0.95;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  margin: 0;
  color: var(--cartel-texto);
}

/* La bajada sale de la monoespaciada (corrección 6). Estaba ahí por
   inercia: adentro de un cartel, una monoespaciada dice "terminal", no
   dice "cartelería". Pasa a Archivo, que es la de sistema. */
.cartel__bajada {
  grid-row: 2;
  grid-column: 2;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 700;
  font-size: 0.19em;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cartel-tenue);
  /* Centrada contra el ancho del CLAIM y no del cartel: la columna 2 del
     grid mide max-content, o sea exactamente el ancho de la línea más
     larga del claim, así que centrar acá adentro es centrar contra el
     claim.

     Sin compensar el tracking del último caracter, a propósito. Le puse
     un margen negativo para corregirlo y al medir la tinta dio peor: 2,4px
     de desvío contra 0,7px sin nada. Los motores de hoy ya no cuentan el
     tracking sobrante al centrar, así que la corrección corregía un
     problema que no existe. */
  text-align: center;
  margin: 1.79em 0 0;
}

/* --- entrada, con el primer frame. Sin preloader: esta landing se abre
   desde un link de Instagram con datos móviles. Un preloader no
   enmascara latencia, la fabrica. --- */
@keyframes heroEntra { from { opacity: 0; transform: translateY(10px); } }
.cartel__claim   { animation: heroEntra var(--t-largo) var(--ease) 60ms backwards; }
.cartel__bajada  { animation: heroEntra var(--t-largo) var(--ease) 120ms backwards; }

/* --- la cámara entra al cartel, fuera del hilo principal donde exista ---

   >>> Y SOLO EN ESCRITORIO (2026-08-15). <<< En el telefono el hero es
   un video que tapa la pantalla entera, y hacerle zoom al scrollear
   convierte el gesto de bajar en un efecto que nadie pidio: "quiero que
   quede simplemente el video ahi como estaba".

   Se acota con la media query, no se borra: en escritorio el efecto se
   queda igual, que ahi es lo que hace que el hero se lea como una
   camara entrando al cartel. El respaldo en JS —el rAF de hero.js— se
   apaga por separado y NO se registra siquiera abajo del umbral: un
   listener que corre sesenta veces por segundo para escribir `scale(1)`
   cuesta lo mismo que el efecto, sin el efecto. */
@media (min-width: 768px) {
  @supports (animation-timeline: scroll()) {
    .tiene-scroll-timeline .hero__escena {
      animation: entrarAlCartel linear both;
      animation-timeline: scroll();
      animation-range: 0 100vh;
    }
  }
}
@keyframes entrarAlCartel {
  to { transform: scale(1.85); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .cartel__claim, .cartel__bajada { animation: none; }
  .tiene-scroll-timeline .hero__escena { animation: none; }
}

/* ============================================================
   EL HERO ESTÁ VIVO — tres capas, y ninguna es video

   Un video de fondo son megas, necesita autoplay muted playsinline, no
   arranca confiable en modo de bajo consumo y suma decodificación en un
   celular de gama media. Todo esto pesa cero y es solo `opacity`.

   LOS LÍMITES, que no son de gusto:
     · nunca más de tres destellos por segundo, tampoco al encender;
     · amplitud baja y áreas chicas;
     · con prefers-reduced-motion se apaga TODO y la escena queda fija.
   ============================================================ */

/* --- a · LOS FOCOS TITILAN, LA CARCASA NO ---
   La carcasa es fierro y no se mueve: lo que se anima es un halo propio
   apoyado sobre cada foco. Las seis posiciones están medidas sobre la
   foto.

   Las seis duraciones son primos en décimas (41, 53, 61, 71, 83, 89) y
   los seis desfasajes también. Al ser primos entre sí, los focos no
   vuelven a sincronizar nunca: el mínimo común múltiplo de la serie está
   en el orden de los días. */
.focos {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
}
.foco {
  position: absolute;
  top: 17%;
  width: 9cqw;
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(circle closest-side,
              rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0) 70%);
  mix-blend-mode: screen;
  animation: titilar linear infinite alternate;
  will-change: opacity;
}
/* AMPLITUD 0,55 A 1, y el margen de destellos alcanza de sobra.
   Estaba en 0,82-1 por miedo al techo de tres destellos por segundo, y
   era miedo mal puesto: con ciclos de 4 a 9 segundos el conjunto de los
   seis da 0,2 destellos por segundo. Hay quince veces de margen. */
@keyframes titilar {
  from { opacity: 0.55; }
  to   { opacity: 1; }
}

/* CICLOS CORTOS. Estaban entre 4,1 y 8,9 segundos y con esa amplitud
   apenas se notaban: un titileo de nueve segundos se lee como una foto
   fija. Van entre 2,3 y 3,5.

   Los seis en décimas —23, 26, 29, 31, 33, 35— son coprimos de a pares,
   así que el patrón completo no se repite antes de las 20 horas. Si dos
   compartieran factor, cada tanto latirían juntos y el ojo lo leería
   como un pulso único. */
.foco:nth-child(1) { left: 20.4%; animation-duration: 2.3s; animation-delay: -1.3s; }
.foco:nth-child(2) { left: 32.4%; animation-duration: 2.6s; animation-delay: -2.9s; }
.foco:nth-child(3) { left: 44.5%; animation-duration: 2.9s; animation-delay: -0.7s; }
.foco:nth-child(4) { left: 56.5%; animation-duration: 3.1s; animation-delay: -3.7s; }
.foco:nth-child(5) { left: 68.1%; animation-duration: 3.3s; animation-delay: -1.9s; }
.foco:nth-child(6) { left: 79.0%; animation-duration: 3.5s; animation-delay: -1.1s; }

/* EL FALSO CONTACTO, EN TRES FOCOS NO CONTIGUOS.
   Van el 1, el 3 y el 5: repartidos a lo ancho del cartel, la falla se
   lee como una instalación vieja. Amontonados en un rincón se lee como
   un error de render.

   Cada uno con su ciclo, y los tres coprimos entre sí y con los seis de
   arriba (83, 119 y 137 contra 23, 26, 29, 31, 33 y 35): así ninguna
   falla coincide nunca con otra ni con el titileo de su propio foco.

   >>> ESTE BLOQUE FALTABA, Y ESO ROMPIÓ MEDIA PÁGINA. <<<
   Un movimiento anterior se llevó la regla y dejó su llave de cierre
   suelta. A partir de esa llave el navegador descarta el resto de la
   hoja: 63 kB en disco, 75 reglas parseadas. Ninguna de las que se
   cayeron estaba mal escrita — no llegaban a existir. Hay un test nuevo
   (test/css.test.js) que ahora falla si el balance se va a negativo. */
.foco--falla-a { animation: titilar 2.3s linear infinite alternate, fallaA 8.3s linear infinite; }
.foco--falla-b { animation: titilar 2.9s linear infinite alternate, fallaB 11.9s linear infinite; }
.foco--falla-c { animation: titilar 3.3s linear infinite alternate, fallaC 13.7s linear infinite; }

@keyframes fallaA {
  0%, 58%     { opacity: 1; }
  59%         { opacity: 0.1; }
  60.5%       { opacity: 1; }
  62%         { opacity: 0.1; }
  63.5%, 100% { opacity: 1; }
}
@keyframes fallaB {
  0%, 41%     { opacity: 1; }
  42%         { opacity: 0.12; }
  43.8%       { opacity: 1; }
  45.4%, 100% { opacity: 1; }
}
@keyframes fallaC {
  0%, 73%     { opacity: 1; }
  74%         { opacity: 0.1; }
  75.2%       { opacity: 1; }
  76.6%       { opacity: 0.1; }
  78%, 100%   { opacity: 1; }
}



/* --- b · LA CARA DEL CARTEL RESPIRA ---
   El texto NO se anima. Está apoyado sobre una superficie iluminada: si
   varía la superficie, varía con ella, y eso se lee como luz. Animar el
   texto directo se lee peor y se nota barato.

   Una capa negra de opacidad 0 a 0,04 es exactamente `brightness(1)` a
   `brightness(0.96)`: el oscurecimiento es multiplicativo, así que la
   variación es del mismo 2% en toda la cara y no aplasta el degradado
   que ya trae la foto. Y es solo `opacity`, o sea que la resuelve el
   compositor sin repintar nada.

   Va POR ENCIMA del texto a propósito: en un cartel real la luz baja
   sobre toda la cara, no alrededor de las letras. Sobre tinta casi negra
   un 4% no se ve, así que el claim no pierde contraste. */
.cartel__respiro {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: #000;
  opacity: 0;
  pointer-events: none;
  animation: respirar 6s ease-in-out infinite alternate;
  will-change: opacity;
}
@keyframes respirar {
  from { opacity: 0; }
  to   { opacity: 0.04; }
}

/* --- c · EL CARTEL SE PRENDE AL CARGAR ---
   No es un preloader: no bloquea, no retrasa el contenido y el texto ya
   está desde el primer frame. Es la escena la que se enciende.

   ACÁ HAY UN CONFLICTO REAL CON EL LÍMITE DE DESTELLOS, y se resolvió
   así. Tres parpadeos en 600ms son 5 por segundo, o sea que la
   secuencia pedida viola el techo de 3. Pero un destello solo cuenta
   como tal si el cambio de luminancia relativa llega al 10%. Entonces:

     · UN solo cambio grande, el encendido (0,42 -> 0,08). Uno en 580ms
       es 1,72 por segundo, adentro del límite.
     · Los rebotes de después nunca saltan más de 0,08 de opacidad. Sobre
       esta cara eso es un 7% de luminancia relativa, por debajo del 10%
       que define un destello: no cuentan, y se siguen viendo como un
       tubo que engancha.

   O sea que se ven los tres parpadeos y el techo se respeta.

   LA PRIMERA VERSIÓN DE ESTA SECUENCIA ESTABA MAL Y LA CAZÓ LA MEDICIÓN,
   no la lectura: terminaba en 0,11 -> 0, que es un salto de 0,11 y
   contaba como un segundo destello. Daban 3,45 por segundo. Por eso el
   tope de los rebotes es 0,08 y no 0,09: 0,09 deja el margen tan al
   filo que cualquier retoque futuro lo cruza sin que se note.

   Si alguien sube estos números, rompe el techo. La comprobación está en
   el registro del commit y se puede volver a correr sobre el DOM. */
.cartel__luz {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: #000;
  opacity: 0;
  pointer-events: none;
  animation: encender 580ms linear 1 backwards;
}
@keyframes encender {
  0%   { opacity: 0.42; }
  9%   { opacity: 0.08; }   /* el encendido: el único cambio grande */
  17%  { opacity: 0.16; }   /* de acá en adelante, saltos de 0,08 o menos */
  26%  { opacity: 0.08; }
  36%  { opacity: 0.15; }
  48%  { opacity: 0.07; }
  60%  { opacity: 0.12; }
  78%  { opacity: 0.05; }
  100% { opacity: 0; }
}

/* --- d · TODO SE APAGA CON prefers-reduced-motion ---
   Sin titileo, sin respiración, sin encendido. La escena queda fija y se
   ve bien igual: es una foto de un cartel encendido. */
@media (prefers-reduced-motion: reduce) {
  .foco, .foco--falla-a, .foco--falla-b, .foco--falla-c,
  .cartel__respiro, .cartel__luz { animation: none; }
  .foco { opacity: 0.92; }
  .cartel__respiro, .cartel__luz { opacity: 0; }
  /* el latido y el paralaje, tambien */
  .hero__lienzo { animation: none; scale: 1; }
  .hero__foto { translate: 0 0; }
}

/* ============================================================
   EL INDICADOR DE SCROLL

   Reemplaza a los dos botones. Los botones competían con el cartel, que
   tiene que ser lo único de esa pantalla, y con las secciones armadas
   abajo eran redundantes.

   OJO, NOTA CONCEPTUAL: esos dos botones eran el mecanismo de las dos
   puertas —el dueño de local y la marca que quiere vía pública—. Al
   sacarlos, la bifurcación pasa a vivir en la barra, donde "Vía pública"
   ya es un ítem propio, y en la sección de servicios, donde "Cartelería
   en vía pública" es la única de las ocho que lleva ahí. Los dos caminos
   tienen que quedar claros al armar las secciones.

   No es un icono dibujado: es la misma regla de 1px que usa toda la
   grilla, con un segmento que la recorre.
   ============================================================ */
.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: clamp(18px, 4vh, 44px);
  translate: -50% 0;
  width: var(--filete);
  height: 46px;
  background: var(--borde);
  overflow: hidden;
  z-index: 4;
  transition: opacity var(--t) var(--ease);
}
.hero__scroll::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 15px;
  background: var(--texto);
  animation: bajar 2.6s var(--ease) infinite;
}
@keyframes bajar {
  0%        { transform: translateY(-15px); }
  55%, 100% { transform: translateY(46px); }
}
/* se apaga al primer scroll y no vuelve */
.hero--scrolleado .hero__scroll { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .hero__scroll::after { animation: none; transform: translateY(15px); }
}

/* --- CELULAR ---
   Acá el `cover` de la corrección 2 NO se puede aplicar, y conviene
   decir por qué en vez de forzarlo: el cartel tiene relación 2,7:1.
   Para tapar una pantalla de 375x812 con esta foto hay que llevarla a
   1450px de ancho, y ahí el cartel mide 1045px adentro de una ventana
   de 375. Queda un pedazo de cartel sin cartel.

   Lo que sí se cumple es el efecto que la corrección busca: que la foto
   ocupe el fondo y no se vea dónde termina. La foto va al ancho
   completo pegada arriba, y abajo sigue el negro de la página, que es
   exactamente el negro de la multitud. El corte cae en la zona oscura y
   no se ve. Un degradado corto lo garantiza aunque cambie el recorte.

   Adentro del cartel queda solo el ojo y la marca: el claim no entra
   sin apretarse, así que baja a la página. Se sigue viendo un cartel de
   Vimpact con gente mirándolo, y después se lee el claim. --- */

/* ============================================================
   EL HERO EN CELULAR — LA IMAGEN OCUPA TODO, EL TEXTO VA ENCIMA

   ANTES: una franja de foto arriba, el claim abajo sobre negro plano, y
   entre los dos un rectangulo vacio que se comia casi la mitad del
   hero. En escritorio el hero es una foto a sangre con el cartel
   adentro; en el telefono se leia como otra pagina.

   AHORA es lo mismo que en escritorio: foto atras, texto encima, sin un
   solo pixel de negro suelto.

   >>> EL ALTO DEL HERO NO ES UNA ELECCION DE DISENO: ES ARITMETICA. <<<

   El cartel mide 2945 px de los 4096 del original. Para que entre
   ENTERO en un telefono de 390, la escala no puede pasar de 390/2945, y
   a esa escala la imagen no puede mostrar mas de

       390 x 2294 / 2945 = 304 px de alto

   Y NO DEPENDE DEL ENCUADRE: recortar mas angosto sube la proporcion
   pero baja la escala en la misma medida. Un hero mas alto que eso solo
   se consigue cortandole los costados al cartel.

   Asi que lo unico que habia para ganar era usar TODA la altura del
   original —el recorte ahora arranca en el cielo y no arriba del
   cartel— y sacar el vacio. El hero pasa de 572 px a 330, con mas foto
   adentro: 284 contra 228.
   ============================================================ */
@media (max-width: 720px) {
  /* SIN min-height. Era `100svh` y es exactamente de donde salia el
     vacio: la foto medía 228 y el hero se estiraba a 572. Ahora el hero
     mide lo que mide la foto, y adentro no sobra nada. */
  /* ============================================================
     >>> EL HERO ES LA PRIMERA PANTALLA. ENTERA. <<<  2026-08-15

     Deroga lo del v76, que dejaba asomar 40px de la seccion siguiente
     para invitar a bajar. Ahora no asoma NADA: la invitacion la hace la
     flecha, y el video va de arriba abajo — incluido atras de la barra,
     que es negra sobre negro y no se nota el corte.

     >>> `svh` Y NO `dvh`. LO CORREGIMOS EL 2026-08-15, Y EL MOTIVO ES
         UN BUG QUE MATI VEIA Y EL CONTROL NO. <<<

     Estaba en `dvh`, elegido para que el hero no se pasara de largo
     cuando la barra se retrae. Pero `dvh` es el alto ACTUAL del viewport,
     y ese alto CAMBIA mientras scrolleas: al empezar el gesto la barra de
     Safari se esconde, el `dvh` crece, el hero se estira y el video —que
     llena ese alto— se re-encuadra en pleno movimiento. Se ve como un
     zoom sutil que acompana el dedo. Medido variando el alto del viewport
     390x780 -> 390x880: el video pasa de 440x783 a 496x882.

     `svh` es el alto con la barra VISIBLE, el mas chico, y NO cambia
     nunca durante el scroll. El hero queda clavado. Se pierde la franja
     que ocupa la barra —queda negro de pagina abajo del video, del mismo
     color, no se nota— y a cambio la pantalla deja de respirar. Una
     franja fija es mejor que un hero que hace zoom con cada gesto.

     >>> POR QUE EL CONTROL NO LO AGARRA: <<< en el Chrome headless de las
     pruebas NO hay barra que se retraiga, asi que `dvh` esta congelado y
     el scroll no mueve nada. Medir el scroll ahi da verde SIEMPRE. La
     validacion de verdad es el telefono de Mati; aca se prueba que el
     alto ya no depende de `dvh`.
     ============================================================ */
  .hero {
    height: 100svh;
    margin-top: 0;
    overflow: hidden;      /* es el que recorta el lienzo */
  }
  /* >>> LA ESCENA VUELVE A SER ABSOLUTA, Y NO ES UN DETALLE. <<< En
     celular era `position: relative; inset: auto` porque el lienzo iba
     en el flujo. Ahora el lienzo es absoluto y su bloque contenedor es
     esta escena: en `relative` sin alto propio, la escena mide CERO, el
     `top: 50%` del lienzo resuelve contra cero y el video queda centrado
     sobre el borde de arriba del hero — o sea medio video arriba de la
     pantalla, con el cartel metido abajo de la barra. Se ve en la
     captura: el cartel cortado contra la barra.

     `inset: 0` la hace del tamano del hero, que es contra lo que el
     lienzo se tiene que centrar. */
  .hero__escena {
    position: absolute; inset: 0;
    transform: none !important; opacity: 1 !important;
  }
  .hero__lienzo {
    position: relative;
    top: auto; left: auto; translate: none;
    width: 100%;
    /* >>> EL HERO OCUPA LA PRIMERA PANTALLA. <<<  2026-08-15
       El video es 9:16 (720/1280 = 0.5625), o sea la proporcion de un
       telefono, asi que en vez de fijar la proporcion se fija el ALTO:
       lo que queda de pantalla despues de la barra, menos lo que tiene
       que asomar de la seccion siguiente.

       >>> `svh` Y NO `dvh`. <<< Mismo motivo que el `.hero` de arriba:
       `dvh` es el alto actual y cambia cuando la barra de Safari se
       retrae al scrollear, y con el se re-encuadra el video en pleno
       gesto. `svh` es el alto con la barra visible y no cambia nunca.

       El video va con `object-fit: cover`, asi que fijar el alto no lo
       deforma: le recorta cielo y multitud, que es lo que sobra. */
    /* >>> Y ACA HAY UNA TRAMPA QUE ME COMI. <<< El primer intento fue
       `height: var(--alto-hero)` sobre el lienzo, con el video en
       `object-fit: cover`. Se veia bien y estaba MAL: con `cover` el
       navegador recorta el video, asi que la caja blanca deja de estar
       donde dicen los porcentajes del archivo. Medido: a 320 el cartel
       salia 245x105 —2,33:1— cuando el del archivo es 3,19:1.

       La salida es que el LIENZO conserve la proporcion del video y sea
       el HERO el que recorta. El ancho de abajo es la formula de `cover`
       escrita a mano: el mayor entre el ancho disponible y el que hace
       falta para tapar el alto. Con eso los porcentajes valen otra vez,
       porque describen una caja que SI tiene la forma del archivo. */
    /* La formula de `object-fit: cover` escrita a mano: el mayor entre
       el ancho disponible y el que hace falta para tapar el alto. Se
       hace aca y no con `cover` sobre el <video> porque el lienzo tiene
       que conservar la proporcion del archivo — si no, los porcentajes
       de la caja blanca dejan de valer y el cartel se va de lugar.

       SIN TOPE. En el v76 el recorte se topaba en 112% para que no se
       cortara el cartel, al precio de que el hero no llenara la
       pantalla. Ahora la pantalla manda: el cartel del video nuevo mide
       el 85% del ancho, y abajo se comprueba a los cuatro anchos que
       ningun borde se corte. */
    aspect-ratio: 720 / 1280;
    width: max(100%, calc(100svh * 720 / 1280));
    /* Centrado y recortado por el hero, que lleva `overflow: hidden`. */
    position: absolute;
    top: 50%;
    left: 50%;
    translate: -50% -50%;
    margin-top: 0;
    /* 58 y no 46 —el alto de la barra fija—: con 46 la imagen arrancaba
       pegada al borde de abajo de la barra y el logo quedaba tocando el
       cielo de la foto, sin nada de aire propio. Los 12 de mas son negro
       de pagina, el mismo que el de la barra: no se ve un hueco, se ve
       que el logo respira. */
    margin-top: 58px;
  }
  /* el corte contra el negro de la página, en la zona oscura */
  .hero__lienzo::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: -1px;
    /* Baja de 22% a 12%: la placa es casi el doble de alta, y el 22%
       de esta caja son 107 px de degradado comiendose media multitud. */
    height: 12%;
    background: linear-gradient(to bottom, transparent, var(--fondo));
    pointer-events: none;
  }
  /* ============================================================
     >>> EL CARTEL, CONTRA LA CAJA BLANCA DE LA PLACA. <<<  2026-08-15

     Los cuatro numeros de abajo NO estan puestos a ojo: los imprime
     `herramientas/preparar-hero.py` a partir de la caja blanca medida
     sobre el archivo —un barrido de luminancia por la fila del medio
     del panel da x 78-843 clavado, y por columnas fuera del poste,
     y 240-491, sobre un lienzo de 928x1152—. Si manana cambia la placa,
     se corre el script y se copian de la salida.

         x  78 - 843   ->   8.41% a 90.84%   ->  width  82.44%
         y 240 - 491   ->  20.83% a 42.62%   ->  height 21.79%
         centros                             ->  left 49.62%  top 31.73%

     >>> EL CUERPO SIGUE SIENDO LA MISMA PROPORCION QUE EN ESCRITORIO. <<<
     La caja de la placa vertical es 3,048:1 y la de escritorio 3,158:1,
     practicamente la misma, asi que no hace falta inventar un numero
     para el telefono:

         escritorio:  cartel 70.81cqw, cuerpo 5.3cqw  ->  7.485% del cartel
         celular:     cartel 82.44cqw                 ->  6.17cqw

     >>> Y SIN PERSPECTIVA. <<< El cartel de la placa vertical es
     FRONTAL: eso es lo que faltaba. El `rotateX(6deg)` estaba para
     acompanar la inclinacion del cartel del 4k, y sobre un panel recto
     dejaria el lockup torcido contra su propia caja.
     ============================================================ */
  /* >>> EL VIDEO OCUPA EL LIENZO Y LA PLACA FIJA SE APAGA. <<< La
     `<picture>` sigue en el HTML para escritorio; aca la tapa el video. */
  .hero__video {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* ============================================================
       >>> EL VIDEO ES INERTE AL TOQUE. <<<  2026-08-15

       Mati: "necesito que el video permanezca estatico y que no sea
       receptible a un clic o a un movimiento". En iOS un <video> se
       toma el toque —muestra controles, se pausa, abre el menu de
       "guardar video" si lo mantenes apretado—.

       `pointer-events: none` hace que el toque PASE DE LARGO hacia la
       pagina: el video no lo recibe, asi que no se pausa, no muestra
       controles ni menu, y el scroll del dedo scrollea la pagina. La
       flecha y el boton de WhatsApp NO son hijos del video —son otros
       elementos, encima— asi que siguen recibiendo el toque.

       `-webkit-touch-callout: none` es el cinturon: apaga el menu de
       mantener-apretado de iOS aunque algo llegara a tocar el video. */
    pointer-events: none;
    -webkit-touch-callout: none;
    /* EL POSTER, PINTADO ACA Y NO COMO ATRIBUTO. Ver el comentario del
       <video> en index.html: como atributo lo bajaba tambien escritorio,
       48 kB de una imagen que no muestra nunca. Los dos usan `cover`
       sobre la misma caja, asi que encuadran igual por construccion. */
    background: url("/hero-movil-poster.webp") center / cover no-repeat var(--fondo);
  }
  .hero__foto { display: none; }

  /* >>> LOS PORCENTAJES CAMBIARON CON EL VIDEO. <<< No son los de la
     placa fija: el recorte a 4:5 movio la caja blanca. Medidos sobre el
     PRIMER CUADRO del video, con el mismo barrido de luminancia:

         x 119 - 599  ->  16.53% a 83.19%   ->  width  66.66%
         y 465 - 641  ->  36.33% a 50.08%   ->  height 13.75%
         centros                            ->  left 49.86%  top 43.21%

     >>> Y ASI SE CIERRA EL CONFLICTO. <<< El video anterior tenia el
     cartel al 85% del cuadro, y con eso tapar la pantalla entera y no
     cortarlo eran incompatibles:

         el borde en pantalla = x0*L - (L - P)/2 >= 0
         ->  L <= P / (1 - 2*x0)

         video anterior (x0 = 0.075)   ->  L <= 1.176 * P
         hace falta para tapar          ->  L  = 1.27  * P     NO ENTRA
         video nuevo    (x0 = 0.1653)  ->  L <= 1.494 * P      ENTRA

     El limite se aleja tanto que haria falta una pantalla de 375x996
     para volver a rozarlo, y no existe un telefono asi.

     >>> Y ESTE PANEL TIENE DEGRADADO, ASI QUE EL UMBRAL ES RELATIVO. <<<
     Con un umbral fijo alto se agarra solo el centro del panel y sale
     una proporcion imposible. Se busca la MESETA: el promedio de
     luminancia por fila, y las filas que pasan el 85% del promedio
     maximo. La cara blanca es plana y alta; la bruma de abajo sube en
     rampa y nunca llega a esos valores. El ancho se mide en la fila del
     medio de esa meseta, con umbral relativo a esa fila.

     El cartel es 2.73:1 contra 3.158:1 de escritorio. Es mas bajo en
     proporcion y el lockup entra igual, porque tiene aire propio arriba
     y abajo. El cuerpo sale del mismo 7.485% del ancho: 4.99cqw. */
  .cartel {
    left: 49.86%;
    top: 43.21%;
    width: 66.66%;
    height: 13.75%;
    font-size: 4.99cqw;
    transform: none;
  }

  /* >>> EL OJO BAJA UN PUNTO. <<<  2026-08-15
     Mati, mirando el telefono: el ojo estaba muy pegado al borde de
     arriba del cartel blanco. Un empujon chico hacia abajo —0.14em del
     cuerpo del cartel, unos 3px a 390— que le da aire arriba sin sacarlo
     de la caja. El control del lockup lo mide a los cuatro anchos. */
  .cartel > .ojo { transform: translateY(0.14em); }


  /* >>> LA BAJADA ES LO UNICO QUE NO PUEDE ESCALAR SOLO. <<<
     Va en `0.19em` del cuerpo del cartel, y eso en escritorio son 16px
     —el cartel mide 1134 de ancho— pero en el telefono son CINCO: el
     cartel mide 357. Cinco pixeles no son una bajada, son una mancha
     gris. Es el unico lugar del lockup donde la proporcion no alcanza,
     porque el claim es Anton en mayusculas y aguanta cualquier tamano y
     esto no.

     Sale de la caja: `2.5cqw` con piso de 8, y el tracking baja de 0.2
     a 0.12em para que "CARTELERIA & PUBLICIDAD" entre en la columna del
     claim a 320, que es donde menos lugar hay. El margen de arriba pasa
     a cqw: en `em` crecia con la letra y empujaba la bajada afuera del
     blanco. */
  .cartel__bajada {
    /* >>> ACHICADA. <<<  2026-08-15, Mati mirando el telefono.
       El piso baja de 8 a 7px: 7px se lee bien en un panel retina, y 8
       era mas de lo que queria. El cqw casi nunca gana en celular —a 390
       son 6,7px— asi que el piso es el que manda a lo ancho de la franja. */
    font-size: max(7px, 1.55cqw);
    /* ============================================================
       >>> Y NO CUELGA: EL TRACKING BAJA DE 0.2 A 0.1em. <<<  2026-08-15

       El pedido de Mati fue que la bajada quede alineada con el bloque
       del claim y no colgando. La causa de que colgara: con `0.2em`,
       "CARTELERIA & PUBLICIDAD" —23 caracteres— quedaba mas ANCHA que el
       claim. La columna 2 del grid es `max-content`, asi que la bajada
       definia el ancho de la columna y sobresalia de los dos lados del
       claim. A `0.1em` la linea entra dentro del ancho del claim: la
       columna la define el claim, y la bajada centra abajo, alineada con
       el bloque de arriba.

       (Antes esto vivia en tres declaraciones sueltas dentro del
       @media de 320 —fuera de todo selector, o sea muertas—: el
       `letter-spacing`, el `margin-top` y el `white-space` que este
       bloque describia nunca se aplicaban. Quedan puestas donde van.)

       LO QUE NO CAMBIA: el texto, el contenido, el tamano. Solo el aire
       entre letras, que es el parametro que existe para esto. */
    letter-spacing: 0.1em;
    /* Mas pegada al claim: 1.79em sobre la bajada eran ~13px de aire, y
       sobraba. Con la bajada tucada abajo del claim el lockup se lee como
       un bloque, no como dos cosas separadas. */
    margin-top: 0.9em;
    /* `max-content` ya evita que envuelva; `nowrap` es el cinturon. */
    white-space: nowrap;
  }
  /* A 320 el ancho es la mitad y el piso de 7px pesa mas en proporcion:
     se aprieta un punto mas el tracking para que la bajada no toque los
     bordes del blanco. El control del lockup lo mide a los cuatro anchos. */
  @media (max-width: 359.98px) {
    .cartel__bajada { letter-spacing: 0.04em; }
  }

  .hero__scroll { display: none; }   /* en celular el scroll es obvio */

  /* >>> LOS SEIS FOCOS NO VAN EN LA PLACA VERTICAL. <<< Estan colocados
     a `top: 17%` y repartidos entre el 20 y el 79% del ancho porque ahi
     estaba la parrilla de luces de la foto de escritorio. La placa
     vertical no tiene esa parrilla: sus luminarias van ABAJO del panel
     y apuntan para arriba. Dejarlos seria poner seis resplandores
     inventados en el cielo. */
  .focos { display: none; }

  /* >>> ACÁ VIVÍA EL CLAIM DE ABAJO. <<< Era medio bloque de CSS con un
     `margin-top: -24vw` calculado a partir del recorte, un degradado de
     tres paradas para fundir con el negro, y su propio <h1>. Todo eso
     existía para acomodar un texto que ahora está adentro del cartel,
     donde siempre tuvo que estar. Se fue el 2026-08-15. */
}


/* ============================================================
   UTILIDADES DE ACCESIBILIDAD
   ============================================================ */

/* Oculto para la vista, presente para lectores de pantalla y para el
   foco. No se usa `display:none` ni `visibility:hidden` porque los dos
   lo sacan del árbol de accesibilidad, que es justo lo contrario de lo
   que queremos. */
.visualmente-oculto {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* El foco se ve SIEMPRE y con la misma forma en todo el sitio. Un
   `outline: none` sin reemplazo deja la página inutilizable con teclado
   y es de los errores más caros de detectar tarde. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--texto);
  outline-offset: 3px;
}

.aviso {
  border: var(--filete) solid var(--borde);
  padding: 20px;
  color: var(--secundario);
  margin: 0;
}

.enlace--fuerte { color: var(--texto); }

/* ============================================================
   SECCIÓN 2 · LO QUE HACEMOS
   ============================================================ */

.prod {
  display: flex;
  flex-direction: column;
  padding: 0 0 22px;
  margin: 0;                /* es un <figure>: trae margen de fabrica */
  transition: background-color var(--t-corto) var(--ease);
}

/* LA SALIDA DE LA SECCION. Reemplaza a los ocho links de las tarjetas:
   uno solo, visible, abajo de la grilla. */
.prod__salida { margin-top: clamp(24px, 3vw, 40px); text-align: center; }
.prod__salida .btn { display: inline-flex; text-decoration: none; }
/* Hover discreto: el filete de la celda se aclara y la foto gana un
   punto de contraste. Solo opacity y filter — nada que dispare layout,
   que en una grilla de ocho celdas se nota. */
/* El realce al pasar por encima se queda, pero ya no promete un clic:
   es el mismo punto de contraste que tiene cualquier foto del sitio. La
   tarjeta no es un link desde el 2026-08-15. */
.prod:hover { background: #0A0A0A; }
.prod:hover .prod__foto { filter: contrast(1.08) brightness(1.04); }
.prod__foto { transition: filter var(--t-corto) var(--ease); }


/* EL HUECO DE LA FOTO, RESERVADO Y VACÍO A PROPÓSITO.
   Las fotos de producto todavía no llegaron. La proporción está fija
   desde ahora, así que el día que lleguen no se mueve nada: no hay
   salto de layout ni recálculo. Mientras tanto el hueco con su filete
   se lee como una decisión, no como un error de carga.

   La diagonal tenue es la marca de "acá va algo". No es un placeholder
   gris con un ícono: es la misma regla de 1px del resto del sitio. */
/* OJO CON height: auto, QUE NO ES DECORATIVO.
   El <img> lleva width y height como ATRIBUTOS —van para reservar el
   espacio y evitar el salto de layout— y el navegador los mapea a CSS
   como pistas de presentacion. Si la hoja no declara `height`, esos
   800px se aplican, y con ancho Y alto definidos el `aspect-ratio` se
   ignora entero: la foto salia 331x800 en vez de 331x221, o sea una
   tira vertical.

   La opcion del generador nunca tuvo el problema porque su regla si
   trae `height: auto`. Misma foto, dos resultados, por una linea. */
.prod__hueco,
.prod__foto {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  border-bottom: var(--filete) solid var(--borde);
}
.prod__hueco {
  background:
    linear-gradient(to top right,
      transparent calc(50% - 0.5px), var(--borde) 50%,
      transparent calc(50% + 0.5px));
}
.prod__foto { object-fit: cover; }

.prod__txt {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 26px var(--canaleta) 0;
}
.prod__nombre { line-height: 1; }
/* El nombre corto existe solo para el telefono; en escritorio no se ve.
   Los dos estan en el DOM: el largo es el que se indexa. */
.prod__nombre-corto { display: none; }

/* El numero y el texto corto de los pasos son SOLO del telefono. */
.paso__numero, .paso__corto { display: none; }
/* Los puntitos son sólo del teléfono: en escritorio la secuencia se
   cuenta con el escenario que scrollea, que tiene su propio indicador. */
/* La muestra de Visualizá es sólo del teléfono: en escritorio la
   sección ES el generador, embebido y con lugar de sobra. */
.vis__muestra { display: none; }

/* ============================================================
   >>> ANTON VA SIEMPRE EN MAYUSCULA. <<<  2026-08-15
   REGLA DE LA PAGINA, NO CASO POR CASO.

   Anton es una tipografia de cartel: esta dibujada para versalitas, con
   un solo trazo muy pesado y sin contraste. En minuscula las
   ascendentes y descendentes le rompen el bloque y el trazo se ve
   sucio — que es exactamente como salieron los titulos del acordeon.

   Va aca arriba, junto a `.display`, y no en cada lugar donde se usa:
   la unica forma de que no vuelva es que el que elige la tipografia
   elija tambien la caja.
   ============================================================ */
.display,
.cartel__claim,
.prod__nombre,
.paso__titulo { text-transform: uppercase; }
.prod__desc {
  color: var(--secundario);
  font-size: 14px;
  line-height: 1.5;
}

/* ACA VIVIA EL DESTACADO DE "CARTELERIA EN VIA PUBLICA".
   Era un borde al costado (.prod--puertaB) y un rotulo "Ver via
   publica" (.prod__puerta). Se fue el 2026-08-12: dentro de la grilla,
   una de ocho gritando hacia que las otras siete parecieran de
   segunda. La seccion de via publica sigue existiendo y sigue en el
   menu; lo que se saco es el privilegio de la tarjeta. */

/* ============================================================
   SECCIÓN 3 · VÍA PÚBLICA — el mapa en vectores

   El SVG lo genera herramientas/preparar-mapa.py desde datos de
   OpenStreetMap y se incrusta en el DOM: no es un <img>, porque el zoom
   necesita animarle el viewBox y los pines necesitan vivir en su mismo
   sistema de coordenadas.

   El grano y la curva de contraste que tenía la versión en imagen se
   fueron. No se traducen a vectores, y no se extrañan: nítido y limpio
   le gana a texturado y blando.

   La atribución va en la línea legal del pie, no acá: la licencia la
   pide, pero debajo del mapa ensuciaba el bloque.
   ============================================================ */

#viapublica { padding-bottom: clamp(32px, 4vw, 56px); }

/* EL BLOQUE MIDE LO QUE MIDE EL DIBUJO.

   Quedó una franja negra de sesenta píxeles abajo del mapa, adentro del
   filete: el alto del bloque y la relación del viewBox no coincidían.

   Arreglarlo con un alto fijo lo desincroniza de nuevo la próxima vez
   que se toque el encuadre, y esa vez nadie va a acordarse. El alto sale
   del propio SVG: `.vp__lienzo` toma su `aspect-ratio` de una variable
   que pone el JS con lo que leyó del `viewBox` del archivo. Cambia el
   encuadre, cambia el bloque, sin tocar CSS.

   El ancho pasa al del contenedor. Con el alto atado a la relación, el
   que salga va a estar bien. */
/* EL ANCHO NO ES EL DEL CONTENEDOR, Y ES A PROPOSITO.

   Con el ancho del contenedor —1326 px— y la relación del encuadre en
   1,18, el bloque queda de 1124 px de alto: más alto que los 999 que ya
   eran demasiado. Sacar la franja negra no lo achica, porque la franja
   eran 60 px y el ancho de más suma 125.

   Son dos palancas para el mismo alto: angostar el bloque, o ensanchar
   el encuadre. Se angosta el bloque, porque el encuadre ya está mirado y
   aprobado y ensancharlo devuelve el río y la provincia vacía que
   costaron dos tandas sacar.

   Este es el número a mover si el alto vuelve a molestar: sube o baja
   solo, con el alto atado a la relación del dibujo. */
.vp__mapa {
  position: relative;
  width: 100%;
  max-width: 1060px;
  margin: 0 auto clamp(20px, 2.4vw, 32px);
  border: var(--filete) solid var(--borde);
  background: var(--fondo);
}

/* El lienzo recorta, no el bloque: el botón de volver flota por encima
   y se ubica contra este mismo rectángulo. */
/* --relacion la escribe el JS con el viewBox del archivo. El respaldo
   sólo cubre el instante entre que se pinta el bloque y que se lee el
   SVG; si el JS no llegara, el bloque queda con una proporción sensata
   en vez de con alto cero. */
.vp__lienzo {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--relacion, 1.18);
}
/* El dibujo llena el lienzo. `height: 100%` con `object-fit: cover` NO:
   recortaría. El SVG ya tiene la misma relación que el lienzo, así que
   estirarlo a los dos lados es exacto, no aproximado. */
.vp__svg { display: block; width: 100%; height: 100%; }

/* La atribución, donde la licencia la pide y donde no ensucia el pie.
   Chiquita y tenue sobre una esquina del mapa: es la práctica estándar y
   cumple igual. */
.vp__osm {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 4px 8px;
  background: rgba(10, 10, 10, 0.55);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: rgba(230, 230, 230, 0.55);
}
.vp__osm a { color: inherit; text-decoration: none; }
.vp__osm a:hover, .vp__osm a:focus-visible { text-decoration: underline; }

/* ============================================================
   EL PIN, DECORATIVO.  2026-08-18

   >>> ES UN `<span>` Y ASI SE VE. <<< No tiene area tactil de 44px, ni
   `:hover`, ni `:focus-visible`, ni cursor de mano: nada que insinue que
   se puede tocar. Un pin que parece tocable y no hace nada es peor que
   uno que claramente no lo es.

   Se fueron con la interaccion: `.vp__zona` (el boton), `.vp__ficha` (el
   panel con foto y nombre), los cuatro modificadores que lo daban vuelta
   cerca de los bordes, `.vp__punto--fuera` —sin zoom ningun pin se sale
   del encuadre— y `.vp__volver`, la salida del zoom.

   EL PIN ES UN PIN: la gota de siempre, blanca, con halo. Sobre un mapa
   oscuro salta sin necesitar color — y meter un acento aca obligaria a
   definir donde mas va ese acento en toda la pagina, que viene en blanco
   y negro estricto hace quince tandas.
   ============================================================ */
.vp__mapa { --pin: #FFFFFF; }
.vp__punto {
  position: absolute;
  translate: -50% -100%;
  display: block;
  pointer-events: none;
}
/* El dibujo, sin `transition`: no hay estado al que pasar. */
.vp__pin {
  display: block;
  width: 26px; height: 34px;
  fill: var(--pin);
  stroke: rgba(0, 0, 0, 0.55);
  stroke-width: 1.2;
}
.vp__pin circle { fill: #111111; stroke: none; }

/* La unica salida de la seccion, centrada bajo el mapa.

   El ancho se limita: `.btn.boton` es de caja y en un contenedor de 1300
   px salia un boton de 1300 px de largo, que no se lee como boton sino
   como una franja. En celular llega al ancho de la columna, que ahi si
   es lo que corresponde. */
.vp__salida { margin-top: clamp(20px, 2.4vw, 32px); text-align: center; }
.vp__salida .btn { display: inline-flex; width: auto; min-width: 260px; padding-inline: 34px; }

/* La salida por WhatsApp, debajo del formulario y mas liviana. No es un
   segundo boton igual al lado del primero: dos botones iguales le pasan
   la decision al visitante y pierden a los dos. El formulario califica;
   esto convierte mas rapido y llega en crudo. */
/* LA SALIDA POR WHATSAPP: LA MISMA CAJA QUE "ENVIAR", UN NIVEL ABAJO.

   Sin línea separadora —cortaba el formulario en dos justo antes del
   final— y sin subrayado, que era el único de toda la página. Sólo aire
   y un botón secundario del mismo ancho que el primario. */
.otra-via {
  /* El bloque va FUERA del <form>, así que no hereda su ancho: sin
     esto tomaba el de la sección entera y el botón salía de 1313px al
     lado de uno de 560. Mismo ancho que .frase, de la misma fuente. */
  max-width: 560px;
  margin-inline: auto;
  margin-top: clamp(20px, 2.4vw, 28px);
}
.otra-via__link {
  /* El primario es de ancho completo dentro del formulario. Éste lo
     iguala: dos botones de anchos distintos se leen como dos cosas
     distintas, y son la misma cosa en dos niveles. */
  display: flex;
  width: 100%;
  text-decoration: none;
}

/* ============================================================
   EL WHATSAPP, QUE ES UNO SOLO EN TRES LUGARES

   El mismo objeto aparece en la pantalla de construcción, abajo del
   formulario y flotando sobre el scroll. Lo que comparten vive acá: el
   ícono al lado del texto, alineados y con el mismo aire. Lo que cambia
   —terciario abajo del formulario, círculo cuando flota— lo pone la
   clase de cada lugar.

   El ícono NO se pinta de verde. Es la marca de otra empresa metida en
   una página que es negra y blanca a propósito: hereda `currentColor`
   y se comporta como cualquier otro elemento del sitio.
   ============================================================ */
.wa {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.wa__icono { flex: none; }

/* ---- EL FLOTANTE ----

   `position: fixed` en la esquina de abajo a la derecha, que es donde
   el pulgar llega sin estirar la mano en un teléfono.

   ESTÁ SIEMPRE, del primer píxel al último. Estuvo condicionado
   —aparecía después del hero, se apagaba en el formulario— y se sacó:
   el que quiere escribir quiere escribir en cualquier punto, y un
   botón que a veces está obliga a buscarlo. */
/* DOS BOTONES, NO UNO.  2026-08-26

   El de presupuesto lleva a `#contacto` y el de WhatsApp abre el chat.
   Van en una fila y el de presupuesto queda a la izquierda, que es el
   orden en que se leen: es el principal —mas ancho y con texto— porque
   es el que cae en el panel y dispara el aviso por mail. El de WhatsApp
   no cae en ningun lado, asi que pasa a segundo, con el mismo peso
   visual que tenia pero sin ser el unico.

   El `gap` crece con la pantalla igual que los margenes de la esquina:
   pegados se leen como un solo boton partido. */
.flota {
  position: fixed;
  right: clamp(14px, 3vw, 26px);
  bottom: clamp(14px, 3vw, 26px);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: clamp(8px, 2vw, 12px);
  /* >>> LA CAJA NO RECIBE TOQUES, LOS BOTONES SI. <<< Sin esto, el
     `div` se come todo lo que caiga bajo su rectangulo aunque no pinte
     nada: cuando el de presupuesto se esconde, su hueco de 160x46 sigue
     siendo del contenedor y un dedo ahi no llega al campo que hay
     debajo. Medido con `elementFromPoint` sobre el hueco: devolvia
     `div.flota` en los tres puntos. Es el mismo error que evita el
     `visibility` del boton, un nivel mas arriba. */
  pointer-events: none;
}
.flota__link { pointer-events: auto; }

/* Lo que comparten los dos: la caja. Filete, fondo, sombra y alto son
   los mismos —esto no es un widget nuevo, es el mismo objeto con un
   hermano—. Lo unico que cambia entre ellos es el ancho. */
.flota__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 0 18px;
  background: var(--fondo);
  border: var(--filete) solid var(--texto);
  color: var(--texto);
  text-decoration: none;
  box-shadow: 0 6px 22px rgba(0, 0, 0, .55);
}
.flota__link:hover { background: #151515; }

/* La tipografia es la de los botones de la pagina, no una propia:
   misma familia, mismo peso, mismo cuerpo, misma caja alta. */
.flota__link--pedir {
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: opacity .28s var(--curva), visibility 0s;
}

/* >>> SE VA CUANDO EL FORMULARIO ESTA A LA VISTA. <<< El atributo lo
   mueve un IntersectionObserver sobre `#contacto`; el porque de que se
   condicione este y no el de WhatsApp esta explicado en `app.js`, en
   `ocultarPedirEnElFormulario`.

   `visibility` ademas de `opacity`, y no solo `opacity`: un boton
   transparente sigue recibiendo toques y sigue en el tabulador, o sea
   sigue estando para todos menos para el que mira. El `0s .28s` retrasa
   el corte hasta que termino el desvanecido — si se cortara al empezar,
   desapareceria de golpe, que es justo lo que no queremos.

   El hueco NO se colapsa: la caja sigue midiendo lo mismo y el de
   WhatsApp se queda donde estaba. Un boton que salta de lugar cuando
   el otro se va se lee como un error. */
.flota__link--pedir[data-oculto="1"] {
  opacity: 0;
  visibility: hidden;
  transition: opacity .28s var(--curva), visibility 0s .28s;
}

/* ---- EL PULSO Y EL HALO ----

   EL HALO ES BLANCO, NO VERDE, Y ES UNA DECISIÓN.

   Esta página es blanco y negro estricto: no hay un solo acento
   cromático en todo el sitio. Un halo verde de WhatsApp sería el primer
   color de la web y se leería como un pegote de otro sistema —el botón
   dejaría de ser parte de la página para ser un widget pegado encima—.
   El blanco consigue lo mismo, que es despegarlo del fondo negro, sin
   romper nada.

   Si Mati quiere el verde, se cambia --halo acá abajo y listo:
     --halo: 37, 211, 102;   (el verde de WhatsApp)

   EL RITMO ES UN LATIDO CORTO CADA SIETE SEGUNDOS, no una vibración
   continua. Algo que se mueve todo el tiempo en una página que se lee
   durante un minuto deja de llamar la atención a los veinte segundos y
   empieza a molestar. El pulso dura menos de un segundo del ciclo: se
   mueve justo cuando el ojo ya se acostumbró a que está quieto.

   El halo va en un ::after DETRÁS del botón, no en el box-shadow del
   propio botón, para que la sombra de apoyo no dependa de la animación
   y el hover no tenga que pelearse con ella. */
.flota {
  --halo: 255, 255, 255;
  --pulso: 7s;
}

/* >>> LATE UNO SOLO, Y ES EL DE PRESUPUESTO. <<< Dos cosas latiendo
   una al lado de la otra no llaman el doble de atencion: se anulan, y
   ademas el ojo no sabe cual de las dos le estan senialando. El pulso
   va en el que queremos que toquen —el que deja el lead medido— y el de
   WhatsApp se queda quieto, que es lo que hace un boton secundario. */
.flota__link--pedir::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border: var(--filete) solid rgba(var(--halo), .5);
  opacity: 0;
  animation: latir-halo var(--pulso) var(--curva) infinite;
}

/* El botón acompaña el mismo pulso con un empujón mínimo. 1.05 sería
   un rebote de aplicación; 1.02 es un latido. */
.flota__link--pedir { animation: latir-boton var(--pulso) var(--curva) infinite; }

@keyframes latir-halo {
  /* Los cuatro pasos ocupan el 12% del ciclo. El 88% restante está
     quieto, que es lo que hace que el 12% se note. */
  0%   { opacity: 0;   transform: scale(1); }
  4%   { opacity: .55; transform: scale(1); }
  12%  { opacity: 0;   transform: scale(1.5); }
  100% { opacity: 0;   transform: scale(1.5); }
}
@keyframes latir-boton {
  0%, 12%, 100% { transform: scale(1); }
  4%            { transform: scale(1.02); }
}

/* ============================================================
   EL CÍRCULO, EN TODAS LAS PANTALLAS

   ERA SÓLO DE ESCRITORIO, y en celular quedaba un rectángulo con
   texto. Medido a 390px con el formulario a la vista: el rectángulo
   ocupaba x 172-306 y el campo "Tu teléfono" x 32-288, así que le
   tapaba 116 de sus 288 px. Tocar ahí abría WhatsApp en vez de enfocar
   el campo — y el teléfono es el ÚNICO campo obligatorio del
   formulario. Comprobado con `elementFromPoint`, no a ojo.

   Aprobado por Mati el 2026-08-14: en celular pasa a ser el círculo.
   Lo que NO cambia es que esté siempre visible; eso ya se decidió una
   vez y se sostiene. Lo único que cambia es la forma, y con 46px de
   lado tapa un tercio de lo que tapaba.

   El texto se va de la pantalla pero NO del acceso: un botón cuyo
   único contenido es un <svg aria-hidden> no tiene nombre para un
   lector, y se anuncia como "enlace" y nada más.
   ============================================================ */
.flota__link {
  border-color: var(--borde);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .45);
}
/* >>> EL CUADRADO DE 46 ES SOLO DEL DE WHATSAPP. <<< Esta regla valia
   para `.flota__link` a secas, cuando el flotante era uno. Aplicada a
   los dos le pone 46 px de ancho fijo al de presupuesto y le corta el
   texto. */
.flota__link--wa {
  width: 46px;
  padding: 0;
}
.flota__link:hover { border-color: var(--texto); }
.flota .wa__texto {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* CON MOVIMIENTO REDUCIDO NO SE MUEVE NADA: ni el pulso del botón ni
   el halo. El botón queda donde está, quieto y con su borde, que es
   perfectamente visible sin latir. */
@media (prefers-reduced-motion: reduce) {
  .flota__link--pedir,
  .flota__link--pedir::after { animation: none; }
  .flota__link--pedir::after { opacity: 0; }
  /* El desvanecido tambien es movimiento. Se sigue yendo —eso no es
     decoracion, es que el boton sobra— pero sin transicion. */
  .flota__link--pedir { transition: none; }
}

/* ============================================================
   SECCIÓN 6 · PROYECTOS

   Va a ser la única sección con color de toda la página, cuando
   lleguen las fotos. Por eso acá no se le impone ningún filtro.
   ============================================================ */
.folio { margin: 0; }
.folio img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; display: block; }
.folio__pie {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 14px var(--canaleta) 18px;
}
.folio__cat { color: var(--secundario); }
.folio__nombre { font-size: 15px; }

/* ============================================================
   COMO TRABAJAMOS — UN SOLO CARRUSEL, EN LOS DOS ANCHOS.  2026-08-18

   >>> ACA VIVIA EL ESCENARIO DE ESCRITORIO. <<< Un `#proceso` de 240vh
   con el contenido en `position: sticky`, que al scrollear cambiaba de
   paso. Andaba, y se fue igual: era un SEGUNDO componente para lo mismo
   que el carrusel del celular ya hacia. Con `.pasos__marcas`, que era su
   indicador.

   Lo que queda es el carrusel, y ahora empieza aca —en la regla base— en
   vez de adentro de la consulta de celular.

   >>> LA TARJETA SIGUIENTE TIENE QUE ASOMAR. <<< Es lo unico que le dice
   al ojo que hay mas; una tarjeta sola centrada parece el final. En
   celular la tarjeta mide 86% y asoma el 14% de la siguiente; en
   escritorio entran dos de 44% y asoma el resto. El piso es 8% del ancho
   de la tarjeta: abajo de eso no se lee como "hay mas", y hay un control
   que lo mide en los dos anchos.
   ============================================================ */
/* Sin `max-width` propio: median 1328 y 1100, dos anchos distintos en la
   misma pagina. El ancho lo pone el contenedor, como en la grilla de
   productos. */
#pasos {
  max-width: none;
  margin-inline: auto;
  display: grid;
  grid-auto-flow: column;
  /* >>> EL ANCHO VA EN PIXELES ENTEROS, Y NO ES UN CAPRICHO. <<<
     Con `44%` de un riel de 1313 px cada tarjeta media 577,72 px y
     TODAS caian en coordenadas fraccionarias: el titulo quedaba en
     x=88,67 y el navegador lo dibujaba entre dos pixeles, con halo. El
     mismo Anton en el hero se ve nitido porque ahi cae entero.
     Medido: `subpixel: true` antes, y no habia ni `text-shadow` ni
     `filter` ni `transform` en toda la cadena de ancestros — o sea que
     el halo no era un efecto puesto a proposito.
     560 y 20 son enteros y su suma tambien: las cuatro tarjetas caen en
     enteros. */
  grid-auto-columns: 560px;
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 0;
  scrollbar-width: none;
}
#pasos::-webkit-scrollbar { display: none; }

/* >>> EL FOCO SE TIENE QUE VER. <<< El riel entra en el tabulador para
   que las flechas del teclado lo puedan recorrer; sin un anillo visible,
   quien navega con teclado no sabe que ya esta parado ahi. */
#pasos:focus-visible { outline: 2px solid var(--texto); outline-offset: 4px; }

#proceso .paso {
  display: block;
  scroll-snap-align: start;
  /* >>> EL RELLENO TAMBIEN VA ENTERO. <<< Con `clamp(22px, 2.6vw, 42px)`
     a 1440 daba 37,44 px, y ESE era el resto que dejaba el titulo en
     x=94,4375. La tarjeta ya caia en entero; lo que la corria era su
     propio relleno. */
  padding: 40px;
  border: var(--filete) solid var(--borde);
  background: var(--sup-1, #0A0A0A);
}
#proceso .paso + .paso { border-top: var(--filete) solid var(--borde); }
#proceso .paso__numero {
  display: block;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 700;
  font-size: 12px;
  letter-spacing: 0.18em;
  color: var(--borde);
  margin-bottom: 10px;
  text-align: left;
}
#proceso .paso__titulo {
  /* Mas grande: en escritorio sobraba ancho y el bloque se veia chico
     para el espacio que tenia. */
  font-size: clamp(22px, 2.6vw, 36px);
  margin: 0 0 14px;
  text-align: left;
}
/* El texto COMPLETO, como siempre. El corto existia para la version de
   tarjetas quietas, que ya no existe. */
#proceso .paso__corto { display: none; }
#proceso .paso__cuerpo {
  display: block; text-align: left; max-width: none;
  font-size: clamp(14px, 1.25vw, 19px);
  line-height: 1.55;
}

/* ---- EL MANDO: los puntitos y las flechas ---- */
.pasos__mando {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2vw, 26px);
  margin-top: clamp(18px, 2vw, 28px);
}
.pasos__puntos {
  display: flex;
  justify-content: center;
  gap: 7px;
}
.pasos__puntos i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--borde);
  transition: background-color var(--t-corto) var(--ease);
}
.pasos__puntos i[data-actual] { background: var(--secundario); }
.pasos__flechas { display: flex; gap: 10px; }
.pasos__flecha {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  background: none;
  border: var(--filete) solid var(--borde);
  color: var(--texto);
  cursor: pointer;
}
.pasos__flecha svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; }
.pasos__flecha:hover:not(:disabled) { border-color: var(--texto); }
.pasos__flecha:focus-visible { outline: 2px solid var(--texto); outline-offset: 2px; }
/* >>> APAGADA DE VERDAD, NO GRIS. <<< En la primera tarjeta no se puede
   ir para atras y en la ultima no para adelante. Un boton que no hace
   nada es peor que no tenerlo, asi que ademas de verse apagado NO se
   puede clickear ni enfocar: lo hace el atributo `disabled`, esto solo
   lo dibuja. */
.pasos__flecha:disabled { opacity: 0.28; cursor: default; }


/* ============================================================
   SECCIÓN 8 · EL FORMULARIO

   Los campos eran cajas negras sobre fondo negro con un filete apenas
   visible: se leían como agujeros, no como superficies donde escribir.
   Ahora cada campo tiene fondo propio —un negro un punto más claro que
   la página— y un filete que se ve sin buscarlo.
   ============================================================ */

.frase {
  max-width: 560px;
  margin-inline: auto;
  text-align: center;
}

.campo { margin: 0 0 clamp(20px, 2.6vw, 30px); }

.campo__et {
  display: block;
  margin-bottom: 10px;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--secundario);
}
.campo__nota {
  letter-spacing: 0.08em;
  text-transform: none;
  opacity: 0.65;
}

/* EL CAMPO ES UNA SUPERFICIE, NO UN HUECO.
   En celular el texto no baja de 16px: abajo de eso iOS hace zoom al
   enfocar y saca al visitante de la página. */
.campo__ctrl {
  display: block;
  width: 100%;
  height: 56px;
  padding: 0 16px;
  font-family: var(--sistema);
  font-size: 19px;
  color: var(--texto);
  background: var(--campo-fondo);
  border: var(--filete) solid var(--campo-borde);
  border-radius: 0;
  text-align: center;
  transition: border-color var(--t-corto) var(--ease),
              background-color var(--t-corto) var(--ease);
}
.campo__ctrl--area {
  height: auto;
  padding: 14px 16px;
  line-height: 1.5;
  resize: vertical;
  font-size: 17px;
}
.campo__ctrl::placeholder { color: var(--secundario); opacity: 1; }
.campo__ctrl:hover { border-color: var(--secundario); }

select.campo__ctrl {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  padding-right: 40px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--texto) 50%),
    linear-gradient(135deg, var(--texto) 50%, transparent 50%);
  background-position: right 22px center, right 16px center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
select.campo__ctrl option { background: #101010; color: #E6E6E6; }

/* El foco no usa el anillo del sistema: el filete pasa a pleno y la
   superficie se aclara un punto. Se ve sin tapar el borde del campo. */
.campo__ctrl:focus-visible {
  outline: none;
  border-color: var(--texto);
  background: var(--campo-foco);
}
.campo__ctrl.mal { border-color: var(--texto); border-width: 2px; }

.campo__error {
  margin: 8px 0 0;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 11.5px;
  letter-spacing: 0.08em;
  color: var(--texto);
}

/* --- LAS PASTILLAS ---
   Es un <input type="checkbox"> nativo escondido con la utilidad de
   accesibilidad —no con display:none, que lo saca del árbol— y su
   <label> hace de superficie. Tab recorre, barra espaciadora marca: el
   comportamiento nativo entero, sin reimplementar nada. */
.pastillas {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--filete);
  background: var(--campo-borde);
  border: var(--filete) solid var(--campo-borde);
}
@media (min-width: 700px) { .pastillas { grid-template-columns: repeat(3, 1fr); } }

.pastilla {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 10px 12px;
  background: var(--campo-fondo);
  font-family: var(--sistema);
  font-size: 14px;
  line-height: 1.2;
  color: var(--secundario);
  cursor: pointer;
  transition: background-color var(--t-corto) var(--ease),
              color var(--t-corto) var(--ease);
}
.pastilla:hover { color: var(--texto); }
.pastilla__ctrl:checked + .pastilla {
  background: var(--campo-elegido);
  color: var(--texto);
  box-shadow: inset 0 0 0 1px var(--texto);
}
/* el foco viaja al <label>, porque el <input> está fuera de vista */
.pastilla__ctrl:focus-visible + .pastilla {
  outline: 2px solid var(--texto);
  outline-offset: -2px;
  color: var(--texto);
}

/* EL BOTÓN ES EL ELEMENTO MÁS PESADO DEL BLOQUE. Antes era más chico
   que los campos y se perdía entre ellos. */
/* ENVIAR es el primario del formulario y va al ancho de la columna: es
   la unica accion de esa pantalla. Todo lo demas lo pone .btn. */
.boton { width: 100%; }

/* La leyenda sale de la monoespaciada: ahí se lee como un mensaje de
   sistema, y es una promesa, no un log. */
.frase__pie {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin-top: clamp(26px, 3vw, 40px);
}
.frase__nota {
  font-family: var(--sistema);
  font-size: 13px;
  color: var(--secundario);
}

/* --- la salida cuando no hay envío --- */
.salida { max-width: 62ch; margin-inline: auto; }
.salida__tit { margin: 0 0 10px; }
.salida__sub { color: var(--secundario); margin: 0 0 20px; }
.salida__txt {
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 13px;
  line-height: 1.7;
  white-space: pre-wrap;
  word-break: break-word;
  border: var(--filete) solid var(--campo-borde);
  background: var(--campo-fondo);
  padding: 18px;
  margin: 0 0 18px;
  color: var(--texto);
  text-align: left;
}
.salida__acciones { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px 28px; }
.salida__estado { margin: 14px 0 0; color: var(--secundario); min-height: 1.2em; }
.salida__ok { font-size: 18px; margin: 0; }

@media (max-width: 720px) {
  /* 16px es el piso: abajo de eso iOS hace zoom al enfocar. */
  .campo__ctrl { font-size: 17px; height: 54px; }
  .campo__ctrl--area { font-size: 16px; }
}

/* ============================================================
   EL PIE

   Eran dos renglones de texto. Un pie no tiene que ser corporativo,
   pero sí tiene que cerrar la página: el ojo le da presencia, la fila
   de secciones le sirve al visitante que llegó hasta abajo sin decidir
   —y al buscador, que lee esos enlaces—, y el copyright dice que atrás
   hay alguien.

   Lleva su propia clase y no `.seccion` porque hay un test que busca
   `class="pie"` literal, y el criterio del test es correcto: el pie es
   el pie. Por eso el borde y el aire se repiten acá.
   ============================================================ */
.pie {
  border-top: var(--filete) solid var(--borde);
  padding-block: clamp(52px, 7vw, 96px);
  text-align: center;
}
/* El ojo es lo que le da presencia al pie. El clamp anterior tenía el
   piso en 40 pero el término medio nunca subía tanto: quedaba en 35. El
   mínimo de un clamp no se alcanza solo por estar escrito. */
/* El pie cierra con el ojo y nada escrito: la marca ya está en la barra
   y en el hero, y repetirla abajo no agrega. El ojo solo alcanza, y es
   el único lugar de la página donde el símbolo aparece sin el nombre. */
.pie__ojo {
  width: clamp(44px, 5.2vw, 54px);
  height: auto;
  margin: 0 auto clamp(26px, 3vw, 38px);
}
.pie__nav,
.pie__canales {
  display: flex; flex-wrap: wrap;
  gap: 8px 14px;
  justify-content: center;
}
.pie__nav { margin-bottom: 14px; }

/* EL SEPARADOR APARECE SOLO CUANDO HAY MAS DE UNO VISIBLE.
   `~` y no `+`: los canales ocultos siguen en el DOM, asi que el
   hermano inmediato de un visible puede ser uno oculto. */
.pie__nav a:not([hidden]) ~ a:not([hidden])::before,
.pie__canales a:not([hidden]) ~ a:not([hidden])::before {
  content: "·";
  margin-right: 14px;
  color: var(--borde);
}
.pie__legal {
  margin: clamp(26px, 3vw, 38px) 0 0;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 10.5px;
  letter-spacing: 0.12em;
  color: var(--secundario);
  opacity: 0.7;
}

/* ============================================================
   SECCIÓN 4 · VISUALIZÁ TU PROYECTO — el generador de maquetas

   El logo del visitante NUNCA llega al servidor. Todo pasa en el
   navegador. Esa decisión hace desaparecer la superficie de ataque
   entera de una subida de archivos: no hay endpoint, no hay
   almacenamiento, no hay archivo de otro que termine en un disco
   nuestro. No se negocia.

   OJO: este CSS se había perdido en la reescritura de la v2 y las
   imágenes salían a tamaño natural — la sección medía 6.600px.
   ============================================================ */

/* --- el indicador de paso ---
   Cuatro momentos. El visitante tiene que saber en cuál está: sin
   esto, "subí tu logo" y "ajustalo" parecen la misma pantalla. */
.bldr__pasos {
  display: flex; flex-wrap: wrap;
  gap: 0 var(--canaleta);
  margin-bottom: clamp(20px, 3vw, 34px);
  padding: 0; list-style: none;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620; font-size: 10.5px;
  letter-spacing: 0.12em; text-transform: uppercase;
}
.bldr__paso {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 8px 0;
  color: var(--secundario);
  opacity: 0.5;
  border-top: 2px solid transparent;
  flex: 1 1 120px;
}
.bldr__paso[aria-current="step"] {
  color: var(--texto);
  opacity: 1;
  border-top-color: var(--texto);
}
.bldr__paso--hecho { opacity: 1; border-top-color: var(--borde); }
/* El paso recorrido ES un boton, asi que se ve y se comporta como uno:
   cursor, subrayado al pasar y foco visible. */
.bldr__paso-btn {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--secundario);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.bldr__paso-btn:hover { color: var(--texto); text-decoration: underline; text-underline-offset: 4px; }
.bldr__paso-btn:focus-visible { outline: 2px solid var(--texto); outline-offset: 3px; }

/* JERARQUIA DE LA COLUMNA DE CONTROLES.
   Archivo, casilla, dos deslizadores y dos acciones pesaban todos lo
   mismo. Mas aire ENTRE grupos que dentro de cada grupo, que es lo que
   hace que se lean como grupos. */
.bldr__campo { margin-bottom: 26px; }
#bldrControles { border-top: var(--filete) solid var(--borde); padding-top: 24px; }
.bldr__herramientas { margin: 22px 0 0; gap: 10px; }
.bldr__acciones { margin-top: 26px; padding-top: 24px; gap: 12px; }

/* --- la grilla de productos --- */
.bldr__grilla {
  display: grid;
  gap: var(--filete);
  background: var(--borde);
  border: var(--filete) solid var(--borde);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 700px)  { .bldr__grilla { grid-template-columns: repeat(4, 1fr); } }
.bldr__opcion {
  display: flex; flex-direction: column;
  background: var(--fondo);
  padding: 0 0 14px;
  transition: background-color var(--t-corto) var(--ease);
}
.bldr__opcion:hover { background: #0A0A0A; }
.bldr__opcion img {
  width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover;
  border-bottom: var(--filete) solid var(--borde);
}
/* POR QUE ESTA NO ES LA MISMA TIPOGRAFIA QUE LA TARJETA DE ARRIBA.
   El visitante ve los mismos ocho productos, con las mismas ocho fotos,
   dos veces en la misma pagina. Arriba el nombre es CONTENIDO: encabeza
   una tarjeta y lleva descripcion, asi que es un titulo y va en Anton.
   Aca es la ETIQUETA DE UN CONTROL: un <button> que se clickea.

   Estaba en Archivo 14, que es exactamente el estilo de la descripcion
   de la tarjeta, y por eso se leia como una descripcion en vez de como
   un control. Pasa a la monoespaciada en versalitas, que es el rol de
   etiqueta tecnica que ya tiene en el resto del proyecto. */
.bldr__opcion span {
  padding: 12px var(--canaleta) 0;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--secundario);
}

/* --- el editor --- */
.bldr__editor {
  display: grid;
  gap: clamp(20px, 3vw, 36px);
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .bldr__editor { grid-template-columns: 1.6fr 1fr; align-items: start; }
}

.bldr__lienzo {
  position: relative;
  border: var(--filete) solid var(--borde);
  /* `touch-action: none` en el canvas, no acá: si se lo pongo al
     contenedor, la página deja de scrollear con el dedo en celular. */
}
.bldr__lienzo canvas {
  display: block; width: 100%; height: auto;
  touch-action: none;
  cursor: grab;
}
.bldr__lienzo canvas:active { cursor: grabbing; }
.bldr__ayuda {
  position: absolute; inset: auto 0 0;
  margin: 0; padding: 14px;
  /* usa .centrado del sistema, no un text-align propio */
  background: rgba(0, 0, 0, 0.72);
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--secundario);
}
/* EL AVISO DE PIXELADO. Discreto a proposito: informa, no alarma. No
   es un error del visitante ni del generador — es que su archivo no da
   para ese tamano, y saberlo ahora le ahorra la vuelta despues. */
.bldr__pixel {
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--secundario);
  max-width: 44ch;
  margin-inline: auto;
}

/* --- LAS MANIJAS ---
   Cuadrados de 1px de filete en las esquinas del logo y una manija de
   giro arriba. Se dibujan en HTML sobre el canvas y no adentro del
   canvas: así el navegador les da el foco, el cursor y el área táctil
   sin que tengamos que reimplementar nada de eso a mano.

   NO son el único camino: los deslizadores de tamaño y rotación siguen
   estando y son los que funcionan con teclado. Las manijas son la capa
   de mouse y dedo. Si alguien las saca, la accesibilidad no se cae. */
.bldr__manijas { position: absolute; inset: 0; pointer-events: none; }
.bldr__manija {
  position: absolute;
  width: 18px; height: 18px;
  translate: -50% -50%;
  background: var(--fondo);
  border: 2px solid var(--texto);
  pointer-events: auto;
  cursor: nwse-resize;
  touch-action: none;
  padding: 0;
}
/* La manija de giro y el marco punteado se sacaron: ver el comentario
   en builder.js. Quedan cuatro manijas iguales. */

/* --- el panel de la derecha --- */
.bldr__panel { min-width: 0; }   /* sin esto el grid no lo deja achicarse */
/* VOLVER ES LA UNICA SALIDA DE ESA PANTALLA y era el control mas debil
   de la pagina: terciario, sin caja, con una linea abajo. Pasa a
   secundario con su filete completo.

   Y la linea que tenia NO era un subrayado de texto: era un
   `border-bottom`. Por eso el test la dejaba pasar — miraba
   `text-decoration`. El test ahora tambien caza el borde inferior
   suelto, que es el mismo agujero en la direccion contraria. */
.bldr__volver { align-self: flex-start; margin-bottom: 20px; }
.bldr__volver::before { content: "\2190"; font-size: 15px; }

.bldr__producto {
  font-family: var(--display);
  font-size: clamp(20px, 2.4vw, 30px);
  text-transform: uppercase;
  margin: 0 0 20px;
}
.bldr__campo {
  display: flex; flex-direction: column; gap: 7px;
  margin-bottom: 16px;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620; font-size: 10.5px;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--secundario);
}
/* El botón de archivo: el <input> sigue existiendo y recibe el foco; el
   <label> es lo que se ve. Cuando el input tiene el foco, el label se
   marca — así Enter y la barra espaciadora abren el selector igual que
   en el control nativo, sin reimplementar el teclado. */
.bldr__et {
  display: block;
  margin-bottom: 9px;
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620; font-size: 10.5px;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--secundario);
}
/* El <label> hace de boton primario: subir el logo es LA accion de ese
   paso. El foco viaja desde el <input>, que es el que lo recibe. */
.bldr__archivo:focus-visible + .btn {
  box-shadow: 0 0 0 2px var(--fondo), 0 0 0 4px var(--texto);
}
.bldr__nombre-archivo {
  display: block;
  margin-top: 10px;
  font-family: var(--sistema); font-size: 13px;
  letter-spacing: 0; text-transform: none;
  color: var(--secundario);
  word-break: break-all;
}
.bldr__campo input[type="range"] { accent-color: #FFFFFF; width: 100%; }
.bldr__check {
  display: flex; align-items: center; gap: 10px;
  margin: 18px 0 6px;
  font-size: 13px;
}
.bldr__check input { accent-color: #FFFFFF; width: 16px; height: 16px; }
.bldr__nota {
  margin: 0 0 14px;
  font-size: 12px; line-height: 1.5;
  color: var(--secundario);
}
.bldr__error {
  margin: 12px 0;
  padding-left: 12px;
  border-left: 2px solid var(--texto);
  font-family: var(--sistema);
  font-variation-settings: "wdth" 100, "wght" 620; font-size: 12px;
}
.bldr__acciones {
  display: flex; flex-wrap: wrap; gap: 10px 24px;
  margin-top: 22px;
  padding-top: 20px;
  border-top: var(--filete) solid var(--borde);
}
.bldr__herramientas {
  display: flex; flex-wrap: wrap; gap: 10px 20px;
  margin: 16px 0 4px;
}
/* ============================================================
   EL CELULAR

   No es la página de escritorio apretada. Es la misma página con
   menos secciones: dos se van enteras, tres se acortan y el resto
   queda igual. El razonamiento de cada una está escrito arriba de la
   regla que la ejecuta, no en un documento aparte que nadie abre.

   >>> EL UMBRAL ES ESTA LÍNEA. Es el único lugar donde se cambia. <<<

   Se decide POR ANCHO y no por user-agent: hay tablets que se declaran
   móviles, escritorios angostos que no lo son, y navegadores que
   mienten. Y el problema acá es el layout, que es exactamente lo que el
   ancho mide.

   El mismo número está en app.js (ANCHO_MOVIL) porque de ahí sale la
   decisión de NO DESCARGAR lo que no se muestra. Los dos valores tienen
   un test que los compara: si se cambia uno solo, el test corta.
   ============================================================ */
:root { --hasta-movil: 767.98px; }

/* Arriba del umbral el botón no existe: los cinco links entran en la
   barra y un botón para desplegarlos sería un clic de más para llegar
   al mismo lado. Va acá afuera porque el estado por defecto de un
   <button> es visible, y lo de adentro del media sólo lo enciende. */
.barra__boton { display: none; }

@media (max-width: 767.98px) {

  /* ---- LO QUE SE VA ENTERO ----

     VÍA PÚBLICA (el mapa). Un mapa de la ciudad con calles de un pixel
     no se lee en 375px, el panel de cada pantalla se abre con hover
     —que en un dedo no existe— y el SVG solo pesa 167 kB. Lo que la
     sección dice, que hay pantallas repartidas por la ciudad, lo dicen
     el hero y "Lo que hacemos" sin cobrar ese precio.

     VISUALIZÁ (el generador). Es un compositor: elegir cartel, elegir
     foto, esperar el armado y mirar el resultado en detalle. En un
     teléfono son cuatro pasos con espera para una imagen chica. Es la
     sección más linda del sitio y la que peor se traduce.

     Se ocultan acá y además no se descargan: eso lo hace app.js. */
  /* >>> `#viapublica` VUELVE (2026-08-15). <<< Se ocultaba porque "carga
     8,5 MB", y ese numero era de `herramientas/origen/mapa-osm.json`,
     que no se publica. Lo que viaja es el SVG chico: 30,7 kB
     comprimidos, medidos, y diferidos hasta que la seccion se acerca.

     `#visualiza` se queda oculta: el generador es un compositor de
     cuatro pasos con espera, y eso no se traduce a un telefono. Esa es
     otra discusion, no esta. */
  /* ============================================================
     >>> `Visualizá` VUELVE, PERO NO COMO EN ESCRITORIO. <<<  2026-08-15

     La sección muestra el preview horneado y un botón; el generador
     entero se abre en una capa a pantalla completa. Embebido en el
     flujo el resultado queda chico —una imagen de 300 px dentro de una
     columna— y ahí es donde esta sección fracasaba.
     ============================================================ */
  .vis__muestra { display: block; }
  .vis__preview {
    display: block;
    width: 100%;
    height: auto;
    border: var(--filete) solid var(--borde);
  }
  .vis__abrir { margin-top: 18px; width: 100%; }

  /* ============================================================
     LA CAPA

     `position: fixed` con `inset: 0` y `100dvh`: la barra del navegador
     se esconde y con `vh` la capa se pasa de largo justo cuando eso
     pasa. Y `hidden` en el HTML, no una clase: si el CSS no carga, la
     capa queda cerrada, que es el lado correcto para fallar.
     ============================================================ */
  .vis__capa[hidden] { display: none; }
  .vis__capa {
    position: fixed;
    inset: 0;
    /* Arriba de la barra fija Y del flotante de WhatsApp: es una capa
       modal, y una barra de navegacion asomando por encima dice que
       todavia se puede navegar. Se vio en la primera captura. */
    z-index: 200;
    display: flex;
    flex-direction: column;
    height: 100dvh;
    background: var(--fondo);
    padding: calc(12px + env(safe-area-inset-top, 0px)) var(--margen-movil)
             calc(12px + env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .vis__capa-cab {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
  }
  .vis__capa-titulo {
    font-family: var(--sistema);
    font-variation-settings: "wdth" 100, "wght" 700;
    font-size: 12px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--secundario);
    margin: 0;
  }
  /* 44 de lado, que es el mínimo de un dedo, y con su etiqueta
     accesible: es una X, no dice nada por sí sola. */
  .vis__cerrar {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    color: var(--secundario);
    cursor: pointer;
  }

  /* El fondo no scrollea mientras la capa está abierta. `position:
     fixed` sobre el body y no `overflow: hidden`: en iOS el `overflow`
     del body no alcanza y la página de atrás se mueve igual. El `top`
     lo escribe el JS para no perder la posición al volver. */
  body.vis__quieto {
    position: fixed;
    left: 0;
    right: 0;
    width: 100%;
  }

  /* Adentro de la capa el generador usa todo el ancho y todo el alto
     que le queda: es el motivo de que la capa exista. */
  .vis__capa .bldr { display: block; flex: 1; }
  .vis__capa .bldr__lienzo { width: 100%; }

  /* En el flujo de la sección el generador NO se ve: vive en la capa. */
  #visualiza > .contenedor > .bldr { display: none; }

  /* ============================================================
     >>> EL MAPA EN UN TELEFONO. <<<  2026-08-15

     LOS PINES NO SE ACHICAN, Y ESTA MEDIDO. Once puntos pensados para
     900 px metidos en 343 podrian encabalgarse; la distancia minima
     entre centros da 49,4 px a 360 y 54,3 a 390, contra 26 px de pin.
     No se tocan, asi que achicarlos o agruparlos seria resolver un
     problema que no existe.

     LO QUE SI FALTABA ES EL AREA TACTIL: el dibujo mide 26x34 y el
     minimo de un dedo son 44. El boton crece alrededor del pin sin
     mover el dibujo — y 44 sigue entrando en los 49,4 de separacion,
     con 5,4 px de aire entre areas.

     >>> SI ALGUN DIA VIMPACT CARGA UBICACIONES REALES MAS JUNTAS, ESTO
     HAY QUE VOLVER A MEDIRLO. <<< El numero que manda es la distancia
     minima entre centros contra 44.

     EL GESTO NO SE SECUESTRA. No hay un solo `touch-action` sobre el
     mapa: arrastrar el dedo encima scrollea la pagina, como en
     cualquier otro lado. El zoom por zona se dispara con un `<button>`,
     o sea con un toque, y por eso tampoco mueve la pagina.
     ============================================================ */
  /* (Aca vivia el area tactil de 44px del pin. Con el pin decorativo no
     hay nada que tocar, asi que no hay area que agrandar.) */

  /* ---- LA BARRA ----

     Cinco links no entran a 375px. El botón despliega y el panel cae
     debajo de la barra. 44px de lado es el mínimo de un dedo, y vale
     tanto para el botón como para cada link del panel. */
  .barra { padding: 10px 16px; }

  .barra__boton {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    margin-right: -10px;   /* el aire del tap se come el padding, no lo suma */
    background: none;
    border: 0;
    color: inherit;
    cursor: pointer;
  }

  .barra__hamburguesa { display: grid; gap: 5px; width: 20px; }
  .barra__hamburguesa i {
    height: 2px;
    background: currentColor;
    transition: transform .22s var(--curva), opacity .16s linear;
  }
  /* Abierto es una X: el mismo botón cierra, así que tiene que decirlo. */
  [aria-expanded="true"] .barra__hamburguesa i:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  [aria-expanded="true"] .barra__hamburguesa i:nth-child(2) { opacity: 0; }
  [aria-expanded="true"] .barra__hamburguesa i:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

  .barra__nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: grid;
    padding: 6px 16px 14px;
    background: var(--fondo);
    border-bottom: 1px solid var(--linea);
    /* Cerrado no se muestra Y NO SE TABULA: `display:none` saca los
       cinco links del orden de foco. Con opacidad sola seguirían ahí,
       invisibles, robándose el tab. */
    display: none;
  }
  .barra__nav[data-abierto="1"] { display: grid; }

  .barra__link {
    display: flex;
    align-items: center;
    min-height: 44px;
    font-size: 15px;
  }

  /* Va DESPUÉS de .barra__link a propósito: las dos reglas tienen la
     misma especificidad y gana la última. Puesta antes, el display:flex
     de arriba la pisaba y el menú ofrecía dos anclas a secciones que en
     celular no existen. */
  .barra__link--escritorio { display: none; }

  /* ---- LO QUE SE ACORTA ----

     EL HERO. Ya no lleva alto propio: mide lo que mide la foto, que
     ahora lo ocupa entero con el texto encima. El `min-height: 82dvh`
     que vivía acá era la otra mitad del vacío negro —estiraba el hero
     por debajo de la imagen— y se fue con él. Ver el bloque del hero
     en celular, más arriba. */

  /* ============================================================
     >>> DOS FILAS DE CUATRO, LAS OCHO A LA VISTA. <<<  2026-08-15

     La version anterior de esto era un carrusel: cuatro columnas de 132
     que se deslizaban. No convencio — y es razonable, porque de las
     ocho tarjetas se veian dos y media, y la seccion que contesta "que
     venden" no deberia pedir un gesto para mostrar la mitad.

     Ahora entran las ocho sin deslizar. Para que entren, la tarjeta
     pierde peso EN EL TEXTO, no en la imagen: la foto manda y el nombre
     baja a 11px, que a ese tamano ya no es un titular sino una
     etiqueta. El nombre en `.d-m` son 19px, y 19px en una tarjeta de 81
     no entran de ninguna manera.

     >>> LA GRILLA SE SALE DEL CONTENEDOR, A PROPOSITO. <<< En celular la
     seccion lleva 16px de relleno y el contenedor otros 16: 32 de cada
     lado, o sea 64 px de los 375 que no son tarjeta. El margen negativo
     los devuelve y la grilla usa el ancho de la seccion. Son 32 px que
     se reparten entre cuatro columnas: casi una decima parte de cada
     tarjeta.

     La canaleta interna baja de 12 a 4 por lo mismo: a 320, cada pixel
     de relleno sale del nombre.
     ============================================================ */
  #servicios .rejilla {
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    /* Sin margen negativo: la grilla arranca donde arranca todo lo
       demas. El -16px existia para robarle ancho al contenedor cuando
       eran cuatro columnas; con dos y con un solo margen para toda la
       pagina, no hace falta. */
    border: 0;
    background: none;
  }
  #servicios .prod { padding-bottom: 10px; }
  #servicios .prod__foto,
  #servicios .prod__hueco { aspect-ratio: 16 / 9; }
  #servicios .prod__txt { padding: 8px 4px 0; gap: 4px; }
  /* Con la tarjeta en 163 px el nombre puede crecer: quedaba chico
     contra su propia foto, que es lo que se ve primero. */
  #servicios .prod__nombre { font-size: 18px; }
  /* Se invierten, pero SOLO en el que declara nombre corto: el atributo
     lo pone app.js. Sin el, esconder el largo para todos dejaba a siete
     tarjetas sin nombre. */
  #servicios .prod__nombre[data-corto] .prod__nombre-largo { display: none; }
  #servicios .prod__nombre-corto { display: inline; }

  /* La animacion de entrada se apaga: con ocho tarjetas de 81 px el
     escalonado se lee como parpadeo, no como entrada. */
  #servicios .prod.entra { opacity: 1; transform: none; animation: none; }

  /* >>> LA DESCRIPCION ES DE ESCRITORIO. <<< En una tarjeta de 81 px no
     hay lugar para tres renglones de texto, y para elegir alcanza con el
     nombre. El texto sigue en el HTML: lo lee el buscador y lo lee un
     lector de pantalla. No se ve, no se borra. */
  #servicios .prod__desc { display: none; }

  /* CÓMO TRABAJAMOS. Era el 25% de la página en escritorio, la sección
     más larga, y es la que menos urgencia tiene en un teléfono: el que
     llega de Instagram quiere ver y preguntar, no leer el método. Se
     queda —da confianza— pero apretada: menos aire entre pasos y el
     número del paso más chico. */
  /* ============================================================
     EL AIRE ENTRE SECCIONES — 112 px POR SECCIÓN, CINCO VECES

     Cada sección llevaba 56 arriba y 56 abajo. Con cinco secciones eso
     son ~500 px de página que no dicen nada, y en un teléfono el aire
     entre secciones se lee igual de bien con la mitad: el borde de la
     pantalla ya hace de separador.

     No es apretar por apretar: es que un valor pensado para una pantalla
     de 1080 px de alto se copió tal cual a una de 698. */
  .seccion { padding-block: 34px; }
  /* ============================================================
     COMO TRABAJAMOS: LO UNICO QUE CAMBIA EN CELULAR.  2026-08-18

     El carrusel entero vive ahora en la regla base —es el mismo
     componente en los dos anchos— y aca queda SOLO lo que de verdad
     depende del ancho:

       · una tarjeta por pantalla en vez de dos (86% y no 44%)
       · el riel se sale del margen de la pagina, para que la tarjeta
         que asoma llegue hasta el borde
       · los cuerpos, un punto mas chicos
       · las flechas se apagan: aca se pasa con el dedo, y dos botones
         de 44px al lado de los puntitos son ruido en una pantalla
         donde el gesto ya existe

     >>> EL 86% NO SE TOCA. <<< Ese 14% que se ve de la siguiente es lo
     unico que hace descubrir que se desliza. Un carrusel donde la
     primera tarjeta ocupa el ancho exacto se ve como un bloque suelto y
     nadie lo toca.
     ============================================================ */
  #pasos {
    grid-auto-columns: 86%;
    gap: 12px;
    /* El riel se sale del margen de la pagina para que la tarjeta
       asomando llegue hasta el borde; el relleno se lo devuelve. */
    margin-inline: calc(-1 * var(--margen-movil));
    padding-inline: var(--margen-movil);
  }
  #proceso .paso { padding: 22px; }
  #proceso .paso__titulo { font-size: 22px; margin-bottom: 8px; }
  #proceso .paso__cuerpo { font-size: 13.5px; line-height: 1.45; }
  .pasos__flechas { display: none; }

  /* ============================================================
     EL RESPIRO DE LA DERECHA — PARA QUE EL FLOTANTE NO TAPE UN CAMPO

     El flotante de WhatsApp vive pegado abajo a la derecha y no se
     apaga nunca; eso ya se decidió. Con los campos llegando hasta el
     margen de la página, la aritmética no cierra:

         el campo termina en   W - 32   (el margen de la página)
         el botón arranca en   W - 60   (14 de margen + 46 de ancho)
         solape                28 px, en todos los anchos

     La salida no es apagar el botón ni rediseñar el formulario: es
     darle a ESTA sección 30 px más de aire del lado derecho. Los campos
     pasan de 326 a 296 px a 390 — nadie lo nota — y el solape da cero.

     Va sobre `#formulario` y no sobre la sección entera para que el
     encabezado siga centrado: lo que se corre es el bloque de
     controles, que es lo único que el botón puede tapar.

     >>> Y VA DE LOS DOS LADOS, QUE ES LO QUE FALTABA. <<<  2026-08-15
     Estaba SOLO a la derecha, y eso no reservaba lugar: CORRIA EL
     FORMULARIO ENTERO 15 px a la izquierda. Medido a 390: el bloque de
     controles empezaba en 32 y terminaba en 313, o sea 32 de margen de
     un lado y 62 del otro. Se veía comparándolo con el pie, que sí está
     centrado — que es exactamente como lo encontró Mati.

     La aritmética obliga a que los 30 sean 30: el campo tiene que
     terminar antes de `W - 60`, y con relleno simétrico P el borde
     derecho queda en `W - 32 - P`, así que P >= 28. Los campos pasan de
     281 a 251 px a 390. Treinta píxeles menos de campo se notan mucho
     menos que un formulario torcido. */
  /* >>> EL FORMULARIO VUELVE AL ANCHO COMUN. <<< Tenia 30px de relleno
     a cada lado para que el flotante de WhatsApp no tapara un campo, y
     eso lo dejaba 30px mas adentro que el resto de la pagina: era el
     bloque que mas se notaba fuera de eje.

     LO QUE SE PIERDE, DICHO: el flotante vive en el borde inferior
     derecho de la PANTALLA, asi que ahora se superpone unos 40px con el
     borde derecho de un campo mientras se scrollea. Se puede vivir con
     eso y aca esta el por que: el texto de los campos va centrado, asi
     que el boton nunca tapa lo que se escribio, y tapa como mucho un
     campo por vez. Un solo ancho para toda la pagina vale mas que 40px
     de superposicion con un boton flotante. */
  #contacto #formulario { padding-inline: 0; }

  /* ============================================================
     >>> EL FORMULARIO, MAS COMPACTO. <<<  2026-08-15

     Quedaba grande al lado del resto de la pagina: 815 px de
     formulario cuando la seccion de productos entera mide 361. Lo que
     sobra es AIRE, no contenido — no se saca ni un campo.

     >>> LOS 16px DE LOS CAMPOS NO SE TOCAN, Y NO ES ESTETICA. <<< Safari
     hace zoom automatico al enfocar un campo con letra menor a 16px, y
     ese zoom no se deshace solo: el visitante queda con la pagina
     agrandada y corrida. El campo baja de 17 a 16 y ahi se planta.

     Lo que si baja: el alto del campo (54 -> 46), el aire entre campos
     (20 -> 14), la etiqueta (13 -> 12) con su margen (10 -> 6), y el
     aire del bloque de cierre (26 -> 16). Las pastillas se quedan en
     44px de alto, que es el minimo de un dedo.
     ============================================================ */
  #contacto .campo { margin-bottom: 14px; }
  #contacto .campo__ctrl {
    /* 16 EXACTOS: el piso de Safari. Ver arriba. */
    font-size: 16px;
    /* `height` y no `min-height`: el bloque de 720px fija `height: 54px`
       y un minimo no achica lo que ya tiene alto propio. Se midio: con
       `min-height` el campo seguia en 54. */
    height: 46px;
  }
  /* PERO EL AREA DE TEXTO NO. `height: 46px` la alcanzaba igual —es un
     `.campo__ctrl`— y la dejaba de un renglon, con barra de scroll
     adentro. Su alto sale del contenido, como siempre. */
  #contacto .campo__ctrl--area { height: auto; }
  /* El area de texto NO se toca: su alto sale del contenido y ya era el
     mas chico de la pagina. Un `min-height` aca la agrandaba, que es lo
     contrario de lo que se pidio — tambien medido. */
  #contacto .campo__et {
    font-size: 12px;
    margin-bottom: 6px;
  }
  #contacto .frase__pie { margin-top: 16px; }
  #contacto .frase__nota { margin-top: 10px; }

  /* >>> EL BOTON DE WHATSAPP DEL FORMULARIO SE VA. <<< El flotante ya
     esta abajo a la derecha y no se apaga: dos botones para lo mismo en
     la misma pantalla es uno de mas. */
  .otra-via--escritorio { display: none; }

  /* ============================================================
     >>> EL PIE, EN UNA FILA, Y CON SU LINEA. <<<  2026-08-15

     Estaba muy separado del formulario y sus textos pesaban mas que el
     ojo. Ahora los cinco links van en UNA sola fila, separados por el
     punto medio que ya usaba el sistema, y mas chicos.

     >>> Y VUELVE UNA LINEA HORIZONTAL, LA UNICA. <<< En la tanda pasada
     se sacaron todas las lineas separadoras de seccion del telefono
     porque cortaban la pantalla en vez de separar. Esta es distinta y
     por eso vuelve: no separa dos secciones, MARCA EL FINAL DE LA
     PAGINA. Despues de ella no hay contenido, hay creditos.
     ============================================================ */
  .pie {
    border-top: var(--filete) solid var(--borde);
    margin-top: var(--aire-interno);
  }

  /* LOS CINCO LINKS EN UNA FILA DE VERDAD. Con las dos <nav> en
     `inline-flex` seguian siendo dos bloques apilados: un elemento
     inline-flex sigue empezando renglon propio si el que tiene al lado
     no cabe. En `display: inline` los diez elementos fluyen como texto
     corrido y arman una fila sola. */

  /* ============================================================
     >>> EL PIE, MAS CHICO. <<<  2026-08-15

     Tres columnas a 375px son tres columnas de una palabra, asi que ya
     iba en una. Lo que estaba mal era el PESO: los links y el legal se
     leian mas grandes que el ojo que tienen arriba, y un pie que pesa
     mas que la marca que cierra deja de ser un pie.

     Tres cosas, y ninguna es "achicar todo":
       · el ojo manda: sube de 44 a 52 y el texto baja a 13, con lo que
         la relacion se da vuelta;
       · menos aire entre las dos filas de links: 22 -> 12, y el bloque
         se lee como UN cierre y no como dos listas;
       · menos relleno arriba y abajo: 52 era el aire de una seccion, y
         esto no es una seccion.
     ============================================================ */
  .pie__cuerpo { grid-template-columns: 1fr; gap: 12px; }
  .pie__ojo { width: 46px; margin-bottom: 16px; }

  /* LOS CINCO LINKS EN UNA FILA. Eran dos navegaciones apiladas —tres
     secciones arriba, tres canales abajo— y en un pie de telefono eso
     son dos listas donde alcanza con una. Se juntan en una sola linea
     que envuelve si hace falta, con el punto medio de siempre entre
     uno y otro. */
  /* >>> EN CELULAR EL PIE PIERDE EL MENU DE SECCIONES. <<<  2026-08-15
     Mati: "el footer queda como muy cargado". La barra de arriba ya
     lleva a todos lados, y despues de haber scrolleado la pagina entera
     los cuatro links de seccion —Lo que hacemos, Via publica, Visualiza,
     Como trabajamos— son ruido. En el telefono el pie queda con TRES
     cosas y nada mas: el logo, los canales y la linea legal.

         el logo arriba
         WhatsApp · Instagram · Email
         © 2026 Vimpact

     Solo en celular. En escritorio hay ancho de sobra, el pie con
     navegacion es lo esperable y nadie se quejo — se queda como esta.

     (Antes esto acomodaba `.pie__nav` y `.pie__canales` en dos filas.
     Con el nav afuera queda una sola: los canales.) */
  .pie__nav { display: none; }
  .pie__canales {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 12px;
    margin: 0;
  }
  .pie__canales a { font-size: 13px; }
  .pie__legal { font-size: 11.5px; margin-top: 12px; }

  /* ---- LO QUE QUEDA IGUAL ----

     CONTACTO: es a dónde va todo. En un teléfono importa MÁS que en
     escritorio, no menos. Los campos ya son de 56px de alto.

     TRABAJOS: sigue oculto, igual que en escritorio, hasta que haya
     fotos reales de obra. */

  /* (Acá se ocultaba el "Ver vía pública" de la tarjeta, que en celular
     apuntaba a una sección escondida. Ya no hace falta: el rótulo se
     sacó de las ocho tarjetas, en escritorio y en celular.) */

  /* ============================================================
     >>> EL RITMO. <<<  2026-08-15

     "En la computadora se ve premium y en el telefono no." No era una
     seccion: la pagina no tenia ritmo. Todo a la misma distancia de
     todo, todos los titulos pesando igual, y cada bloque arrancando en
     una x distinta. Tres numeros arreglan las tres cosas, y los tres
     son de ESCALA, no de contenido.

     1. UN SOLO ANCHO PARA TODA LA PAGINA. Era lo mas roto: la grilla de
        productos arrancaba en 16, el formulario en 62 y el pie en 0.
        Tres margenes distintos en la misma pantalla se leen como tres
        pedazos pegados. Ahora hay UNA variable y la usan todos.

     2. EL AIRE ENTRE SECCIONES ES EL DOBLE QUE EL DE ADENTRO. Y se
        escribe asi, con el `calc`, para que no se puedan separar: si
        manana alguien toca uno, el otro lo sigue.

     3. LOS TITULOS DE SECCION DEJAN DE SER EL EVENTO. `LO QUE HACEMOS.`
        en 30px ocupa la mitad del ancho de un telefono de 375. A 22 es
        un rotulo, que es lo que un titulo de seccion tiene que ser: lo
        que manda es el contenido.
     ============================================================ */
  :root {
    --margen-movil: 20px;
    /* ============================================================
       >>> EL AIRE, TERCERA SUBIDA. <<<  2026-08-15
       Fue 34/32, despues 22/44, y sigue sin alcanzar. Pasa a 32 adentro
       y 64 entre secciones —el doble, con los dos rellenos que se
       tocan—, mas 8 de margen que se suman abajo de cada seccion para
       llegar a los 72 pedidos.

       La pagina queda mas larga y esta bien. El largo no es el problema:
       lo que se ve barato es todo junto para que entre. Una pagina que
       se siente cara tiene mas aire del que uno cree que necesita. */
    --aire-interno: 32px;
    /* (Aca vivian `--asoma` y `--tope-lienzo`, del v76: el hero dejaba
       40px de la seccion siguiente a la vista y se topaba el recorte
       para no cortar el cartel. Los dos se fueron: el hero ocupa la
       pantalla entera y la invitacion a bajar la hace la flecha.) */
  }

  /* El margen lateral, UNO. La seccion lo pone y el contenedor de
     adentro no agrega nada: sumaban 16 + 16 y por eso el texto arrancaba
     en 32 mientras el pie arrancaba en 0. */
  .seccion,
  .pie { padding-inline: var(--margen-movil); }
  .contenedor { padding-inline: 0; max-width: none; }

  /* El aire entre dos secciones son los dos rellenos que se tocan, o sea
     el doble del de adentro. Escrito con calc para que quede atado. */
  .seccion { padding-block: var(--aire-interno); }
  /* Los dos rellenos que se tocan dan 64; los 8 de margen completan los
     72. Se escribe como margen y no subiendo el relleno a 36 para que la
     relacion con el aire de adentro siga siendo legible: 32 adentro. */
  .seccion + .seccion,
  .pie { margin-top: 8px; }
  .pie { padding-block: var(--aire-interno) calc(var(--aire-interno) + 4px); }
  .seccion__cab { margin-bottom: var(--aire-interno); }

  /* ============================================================
     >>> LA JERARQUIA ESTABA AL REVES. <<<  2026-08-15

     El titulo iba en 22 y su bajada en 15, o sea una relacion de 1,47 —
     y eso EN NUMEROS parece suficiente pero EN PANTALLA no lo era: Anton
     es condensada, asi que 22 de Anton pesan visualmente lo mismo que 15
     de Archivo, que es ancha. La bajada se leia igual de fuerte que el
     titulo. En escritorio la relacion es 5,07 y por eso ahi se ve bien.

     No se vuelve a 30, que era el tamano de antes del v74 y ocupaba
     media pantalla de ancho. Sube a 26 y la bajada baja a 14: relacion
     1,86, el titulo manda, y "LO QUE HACEMOS." sigue entrando en un
     renglon sin comerse la pantalla.
     ============================================================ */
  /* >>> Y OTRA VUELTA MAS: 34. <<<  El criterio dejo de ser una relacion
     y paso a ser uno solo: el titulo de seccion es LO MAS GRANDE de su
     bloque, sin discusion. 26 contra 14 daba 1,86 y seguia sin mandar,
     porque Anton es condensada y pesa menos de lo que mide.

     >>> Y EL TITULO LARGO SE RESUELVE POR PALABRAS, NO ACHICANDO. <<<
     "MARCAS QUE CONFIAN EN VIMPACT." no entra en un renglon a 34, y esta
     bien que no entre: parte entre palabras y quedan dos renglones. Dos
     renglones grandes son mejores que uno chico. `text-wrap: balance`
     reparte el texto entre los renglones en vez de dejar una viuda de
     una palabra. */
  .seccion__cab .display,
  .seccion__titulo {
    font-size: 34px;
    line-height: 1.02;
    text-wrap: balance;
  }
  .seccion__cab .cuerpo,
  .seccion__cab p { font-size: 14px; line-height: 1.5; }

  /* ---- QUE NADA SE DESBORDE ----

     El piso tipográfico y el corte de palabras largas. A 375px una
     palabra como "presupuestación" en un título de 40px se sale de la
     pantalla y aparece la barra horizontal. */
  /* (Aca habia otro `.seccion { padding-inline: 16px }` y otro tamano de
     titulo. Los dos los manda ahora el bloque del RITMO, mas arriba: eran
     justamente las reglas que hacian que cada bloque arrancara en una x
     distinta.) */
  /* >>> ACA VIVIA `hyphens: auto`, Y ERA EL BUG. <<< Se puso para que
     una palabra larga no desbordara a 375, pero `auto` no espera a que
     la palabra no entre en NINGUN lado: corta apenas no entra en lo que
     queda del renglon. "VIMPACT." entraba de sobra en el renglon
     siguiente y el navegador igual escribio "VIM-PACT".

     Queda `overflow-wrap: break-word` como valvula contra el desborde
     horizontal —eso si hace falta y no mete guion—, y el `hyphens: none`
     de la regla global de arriba manda. */
  h1, h2, h3, .display { overflow-wrap: break-word; }

  /* EL PISO SON 12px, Y SE ESCRIBE CAMPO POR CAMPO.

     `body { font-size }` no alcanza: sube lo que hereda y no toca lo
     que tiene tamaño propio, que es justamente lo chico. En escritorio
     el legal y el bajo-marca del pie van en 10.5px y se leen porque la
     pantalla está a 60cm; en un teléfono a 30cm y con brillo de calle,
     no. Nada acá abajo de 12. */
  body { font-size: 15px; }
  .pie__legal { font-size: 12px; }
  .apunte, .campo__pista, .campo__opcional { font-size: 13px; }
  .campo__et { font-size: 13px; }

  /* ============================================================
     >>> LAS LINEAS SEPARADORAS DE SECCION NO VAN EN EL TELEFONO. <<<
     2026-08-15

     En escritorio el filete de 1px arriba de cada seccion es lo que
     hace que la pagina se lea como una tabla: separa bloques anchos que
     se ven todos juntos. En un telefono no hay dos bloques a la vista al
     mismo tiempo —se ve uno por vez— asi que la linea no separa nada:
     TOCA LOS DOS BORDES DE LA PANTALLA Y CORTA. El aire entre secciones
     ya hace el trabajo.

     Se van las cuatro: arriba de "Lo que hacemos", de "Como trabajamos",
     de "Inicia tu proyecto" y del pie.

     (Eran cinco, y habia una excepcion escrita aca: las dos lineas de la
     cinta de logos se quedaban porque enmarcaban la fila en vez de
     cortar la pantalla. La cinta salio el 2026-08-18 y la excepcion se
     fue con ella.)
     ============================================================ */
  /* Las lineas separadoras de SECCION se van (tanda anterior). El pie
     sale de esta regla el 2026-08-15: su linea vuelve, y no es una
     separadora de seccion — marca el final de la pagina. Ver mas abajo. */
  .seccion { border-top: 0; }

  /* ============================================================
     >>> EL ORDEN, QUE ES EL MISMO QUE EN ESCRITORIO. <<<  2026-08-18

       lo que hacemos -> tu marca en la calle -> visualiza -> como
       trabajamos -> contacto

     O sea el del HTML, sin reordenar nada.

     >>> ACA HUBO UN ORDEN DISTINTO PARA EL TELEFONO, Y SE REVIRTIO. <<<
     Entre el 15 y el 18 de agosto el celular subia "como trabajamos"
     arriba de "tu marca en la calle". El motivo escrito era bueno: "lo
     que hacemos" y "como trabajamos" son la misma pregunta —que me dan y
     como— y habia UNA TANDA DE LOGOS EN EL MEDIO partiendolas; ademas
     las marcas rendian mas pegadas al formulario.

     Esa tanda de logos era la seccion de marcas, y se saco entera el
     2026-08-18. El bloque que justificaba el reordenamiento desaparecio y
     el reordenamiento se habia quedado: dos pantallas con las secciones
     en distinto orden, sin ningun motivo vivo.

     Un comentario que justifica algo que ya no pasa es peor que no tener
     comentario, asi que este cuenta lo que hay hoy — y por que hubo otra
     cosa antes, para que nadie lo "arregle" de vuelta.

     >>> SE HACE CON `order`, NO MOVIENDO EL HTML. <<< Mover las secciones
     en el archivo cambiaria tambien escritorio, donde el orden se queda
     como esta. Para que `order` valga, el contenedor tiene que ser flex:
     el contenedor es `body`.

     Y eso es seguro porque los elementos `position: fixed` —la barra de
     arriba y el flotante de WhatsApp— NO son items de flex: estan fuera
     del flujo. Las secciones ocultas (`#viapublica`, `#visualiza`,
     `#trabajos`) tampoco. Quedan el muro, el hero, las cuatro secciones
     y el pie, y a los seis se les escribe el orden: dejar alguno en el 0
     de fabrica lo mandaria antes que todos los demas.
     ============================================================ */
  body {
    display: flex;
    flex-direction: column;
  }
  /* >>> Y `#viapublica` NECESITA EL SUYO, QUE ES POR LO QUE ESTO SE
     ROMPIO. <<< Volvio al celular el 2026-08-15 y quedo SIN `order`, o
     sea con el 0 de fabrica: se fue arriba de todo, antes del muro y
     del hero. Y encima disparo su IntersectionObserver al arrancar,
     asi que los 76 kB del SVG entraron en la primera pantalla — que era
     exactamente la condicion que no se podia romper.

     Lo agarro la medicion del peso, no la vista: la seccion estaba
     arriba pero el hero tapa la pantalla entera, asi que no se veia.

     Dejar a alguno en el 0 de fabrica lo manda antes que todos los
     demas. Por eso TODOS llevan numero escrito, sin huecos. */
  .obra       { order: 1; }
  #top        { order: 2; }
  #servicios  { order: 3; }
  #viapublica { order: 4; }
  #visualiza  { order: 5; }
  /* Su número escrito, igual que el de arriba y por el mismo motivo:
     sin `order` se va al 0 de fabrica, o sea arriba de todo, y no se ve
     porque el hero tapa la pantalla. Ya paso una vez con el mapa. */
  #proceso    { order: 6; }
  /* >>> `#trabajos` TAMBIEN, aunque hoy no se vea. <<< Esta en
     `display: none` porque todavia no hay fotos de obra, asi que con
     `order: 0` no molesta a nadie. Pero el dia que haya trabajos y
     alguien la encienda, aparece ARRIBA DEL HERO — exactamente lo que
     paso con `#viapublica` y por el mismo motivo. El numero se escribe
     ahora, que el arreglo es una linea y no una sorpresa. */
  #trabajos   { order: 7; }
  #contacto   { order: 8; }
  .pie        { order: 9; }

  /* ============================================================
     >>> LOS PRODUCTOS: DOS FILAS DE CUATRO, QUE SE DESLIZAN JUNTAS. <<<
     2026-08-15

     Eran cuatro filas de dos y ocupaban tres pantallas de scroll para
     ocho tarjetas. Ahora son dos filas de cuatro, en horizontal.

     >>> POR QUE SE DESLIZA Y NO ENTRA. <<< Cuatro columnas adentro de
     los 311 px utiles de un telefono de 390 dan tarjetas de 76 px. En
     76 px el nombre mas largo —"Carteleria en via publica", 25
     caracteres— no entra ni en cuatro renglones a 19px, que es el piso
     de `.d-m`. La salida NO es achicar la tipografia: la tarjeta se
     queda en 132 px, las cuatro columnas suman 531 y la fila se
     desliza. La cuarta columna asomando por el borde es lo que avisa
     que hay mas.

     LAS DOS FILAS SE MUEVEN JUNTAS porque son UNA grilla adentro de UN
     solo contenedor con scroll: `grid-auto-flow: column` con dos filas
     reparte los ocho en cuatro columnas de dos. No son dos carruseles
     sincronizados —eso se desincroniza el dia que uno tenga un elemento
     de mas—; es una sola pieza.

     EL SCROLL SE ANUNCIA: `role="group"` y `tabindex="0"` los pone
     app.js, asi el teclado tambien puede recorrerlo.
     ============================================================ */

  /* ---- LO QUE SE TOCA CON EL DEDO ----

     44px es el mínimo, y se mide en el elemento que recibe el toque,
     no en el que se ve. La marca de la barra es un renglón de 20px:
     el texto queda como está y el área crece alrededor.

     Las pastillas del formulario son <label> con el checkbox oculto —
     el label ES el control, y por eso el que tiene que medir 44. */
  .barra__marca { display: flex; align-items: center; min-height: 44px; }
  .pastilla { min-height: 44px; display: inline-flex; align-items: center; }

  /* ============================================================
     EL MURO, MIENTRAS LA VERSIÓN CELULAR ESTÁ EN REVISIÓN

     TODO CUELGA DE `html:not([data-paso])`, y el orden importa: el
     estado por defecto —sin el atributo, o sea antes de que corra una
     sola línea de JS— es EL MURO PUESTO. El JS sólo puede abrir, nunca
     cerrar.

     Así no hay destello: el visitante casual no llega a ver la web
     mientras el script decide. Si el JS falla, tarda o alguien lo
     bloquea, se queda afuera, que es el lado correcto para fallar.

     Y cuando el muro se apague, estas reglas dejan de aplicarse solas,
     porque el JS pone el atributo siempre.

     Se apaga con MURO_CELULAR en app.js. Esto de acá no se toca.
     ============================================================ */
  html:not([data-paso]) .obra { display: flex; }

  /* El grano queda: es el fondo, no una sección. */
  html:not([data-paso]) body > *:not(.obra):not(.grano) { display: none !important; }

  /* ============================================================
     >>> UN CANDADO Y UN CAMPO. NADA MAS. <<<  2026-08-15

     Aca habia una pantalla que explicaba: ojo de la marca, "Sitio en
     construccion", una bajada, tres puntos latiendo, WhatsApp,
     Instagram, y abajo de todo una puerta con su etiqueta y su boton.

     Ahora es un candado y un campo, centrados sobre el negro. Sin una
     sola palabra: el que llega de casualidad no tiene que leer nada, y
     el que tiene la clave tampoco.
     ============================================================ */
  .obra {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 26px;
    min-height: 100dvh;
    padding: 40px 24px;
    background: var(--fondo);
  }

  .obra__candado { color: var(--secundario); opacity: .55; }

  .obra__puerta { width: min(220px, 62vw); }

  .obra__puerta-ctrl {
    width: 100%;
    /* 16 EXACTOS es el piso: abajo de eso Safari hace zoom al enfocar y
       deja la pantalla agrandada y corrida. En un campo que es lo unico
       que hay, ese zoom se nota el doble. */
    font-size: 16px;
    min-height: 46px;
    padding: 0 14px;
    text-align: center;
    /* El punto del password, con aire: apretados se leen como una
       mancha. */
    letter-spacing: 0.3em;
    color: var(--texto);
    background: var(--campo-fondo);
    border: var(--filete) solid var(--campo-borde);
    border-radius: 0;
    transition: border-color var(--t-corto) var(--ease);
  }
  .obra__puerta-ctrl:focus { border-color: var(--secundario); }
  .obra__puerta-ctrl:focus-visible { outline: none; }

  /* >>> EL ERROR NO ESCRIBE, TIEMBLA. <<< Cuatro sacudidas de 6px y el
     filete en rojo, y se acabo. `animation` y no `transition` porque
     tiene que volver sola al lugar. */
  .obra__puerta-ctrl.mal {
    border-color: #E0625C;
    animation: temblar 320ms var(--ease);
  }

  @media (prefers-reduced-motion: reduce) {
    /* Sin temblor: queda el filete rojo, que dice lo mismo sin mover
       nada. La senal no puede depender del movimiento. */
    .obra__puerta-ctrl.mal { animation: none; }
  }
}

/* Fuera del celular la pantalla no existe. Va acá afuera porque el
   estado por defecto de un <main> es visible. */
.obra { display: none; }

@keyframes temblar {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(4px); }
}

@keyframes latir-punto {
  from { opacity: .25; transform: scale(.85); }
  to   { opacity: 1;   transform: scale(1); }
}

/* ============================================================
   >>> SI PIDEN MENOS MOVIMIENTO, EL VIDEO NO ARRANCA. <<<  2026-08-15

   No es un detalle de accesibilidad: hay gente que se marea con una
   imagen que se mueve sola en pantalla completa, y el hero del telefono
   es exactamente eso.

   Se pausa desde el JS —`prefers-reduced-motion` no puede detener un
   video desde CSS— y queda el poster, que es el primer cuadro exacto.
   O sea que la pantalla se ve igual: quieta en vez de andando.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .hero__video { animation: none; }
}

/* ============================================================
   LA FLECHA DE BAJAR — 2026-08-15

   Solo en el telefono, y solo porque el hero paso a tapar la pantalla
   entera: sin ella, una primera pantalla que ocupa exactamente el alto
   disponible se lee como el final de la pagina.

   Se va apenas empieza el scroll —la clase la saca app.js— porque una
   vez que el visitante entendio que hay mas, la flecha es un adorno
   arriba del contenido.
   ============================================================ */
.hero__flecha { display: none; }

@media (max-width: 767.98px) {
  .hero__flecha {
    display: block;
    position: absolute;
    left: 50%;
    bottom: calc(18px + env(safe-area-inset-bottom, 0px));
    translate: -50% 0;
    z-index: 3;
    color: var(--secundario);
    opacity: .75;
    pointer-events: none;
    animation: flechita 1.8s var(--ease) infinite;
    transition: opacity var(--t-corto) var(--ease);
  }
  /* Se fue: no se esconde con `display` para que la transicion se vea. */
  .hero__flecha[data-ida] { opacity: 0; }
}

@keyframes flechita {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(7px); }
}

@media (prefers-reduced-motion: reduce) {
  /* Quieta, no ausente: sigue diciendo lo mismo sin moverse. */
  .hero__flecha { animation: none; }
}
