/* ===========================================================================
   corder.pro — portada
   ===========================================================================

   Se carga sólo en "/", y se carga DESPUÉS de site.css: donde las dos hojas
   declaran el mismo selector con la misma especificidad, gana ésta. Eso es
   deliberado, pero se usa lo menos posible: cada regla que sólo existe aquí es
   una diferencia entre la portada y el resto del sitio que hay que justificar.

   Hereda todos los tokens de site.css: aquí no se define ni un color nuevo.
   Y desde la unificación del ritmo, tampoco el encabezado asimétrico (.lede)
   ni el relleno de .section, que son del sitio entero y viven allá.

   Las tres decisiones de composición que sostienen todo lo demás:

     1. ASIMETRÍA. Ninguna sección usa "título centrado, contenido debajo". El
        encabezado (.lede) pone la etiqueta arriba, el titular pegado a la
        izquierda y el texto de apoyo alineado a la derecha, en la misma banda.
        Seis secciones con el mismo bloque centrado es lo que hace que una
        página se lea como plantilla aunque cada pieza esté bien por separado.

     2. TITULAR A DOS VOCES. Primera línea sólida y pesada, segunda apagada y
        ligera. El contraste lo dan el peso y el color, nunca una itálica:
        Archivo es una variable de 400 a 900 sin corte itálico, y a tamaño de
        rótulo la cursiva sintética del navegador se ve torcida.

     3. SIN HUECOS MUERTOS. Cada bloque termina donde termina su contenido. El
        aire va ENTRE secciones, no colgando debajo de la última línea de cada
        una.

   Diseñado a 360 px primero. Cada media query AGREGA para pantallas grandes;
   ninguna corrige un desastre heredado del escritorio.
   =========================================================================== */

/* ===========================================================================
   0. La intro
   ===========================================================================

   Dos variantes, y cuál toca lo decide el script en línea del <head> de
   home.php escribiendo data-intro en el <html>, antes del primer pintado:

     [data-intro="curtain"]  primera carga de la sesión: telón a pantalla
                             completa con el rótulo "corder".
     [data-intro="hero"]     el resto: sin telón, el titular entra inclinado.
     (sin atributo)          sin JavaScript, con movimiento reducido o con
                             sessionStorage bloqueado: la portada, y ya.

   EL TELÓN ESTÁ OCULTO POR DEFECTO Y SE ENCIENDE CON EL ATRIBUTO, nunca al
   revés. Si se pintara visible y el script tuviera que apagarlo, cualquier
   fallo del script dejaría la portada tapada para siempre — y la portada es el
   cotizador. Un defecto así no se ve en desarrollo y se lleva los leads de un
   día entero.

   El fondo es --bg, el mismo suelo de la página: cuando el telón se abre no hay
   cambio de color, sólo desaparece lo que tapaba. Con un fondo propio se vería
   un salto de tono justo en el cuadro del que se quería presumir.
   =========================================================================== */

.intro { display: none; }

:root[data-intro="curtain"] .intro {
  display: grid;
  place-items: center;
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: var(--bg);

  /* Se cierra como un párpado. clip-path lo resuelve el compositor, así que la
     apertura no cuesta layout ni pintura aunque debajo esté montándose la
     portada entera. */
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.52s var(--in-out), opacity 0.4s var(--out) 0.12s;
}

:root[data-intro="curtain"] .intro[data-open="true"] {
  clip-path: inset(50% 0 50% 0);
  opacity: 0;
  pointer-events: none;
}

/* Mientras el telón está arriba no se desplaza la página por debajo.
   scrollbar-gutter evita que quitar la barra de desplazamiento recoloque el
   contenido: ese salto cuenta como CLS aunque ocurra tapado. */
:root[data-intro="curtain"] {
  overflow: hidden;
  scrollbar-gutter: stable;
}

.intro__stage {
  position: relative;
  display: grid;
  place-items: center;
  width: min(92vw, 46rem);
  aspect-ratio: 16 / 7;
}

.intro__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.3s var(--out);

  /* RESUELTO (2026-09-13). Se deja el historial porque costó dos intentos y el
     segundo sólo salió bien cuando se dejó de razonar y se fotografió.
     ---------------------------------------------------------------------
     Síntoma: el cliente veía "el contorno del recorte del lienzo", y seguía
     viéndolo DESPUÉS de poner la primera máscara.

     Descartado midiendo, por si vuelve a aparecer algo parecido:

       · La máscara sí se aplicaba: dos capas, mask-composite intersect,
         verificado sobre corder.pro.
       · No hay `border`, `outline` ni `box-shadow` en .intro, .intro__stage
         ni .intro__canvas. El contorno no estaba dibujado en CSS.
       · La escena tiene UN solo THREE.Points. Ni planos, ni helpers, ni
         geometría con aristas.
       · La nube de partida ya era esférica, no una caja.
       · El lienzo es transparente de verdad: WebGLRenderer va con
         `alpha: true` y nadie llama a setClearColor, así que no había un
         rectángulo de relleno sólido.

     La causa: la máscara ablandaba el corte pero no cambiaba su forma. Ver la
     explicación completa abajo, en la regla.

     CÓMO SE ENCONTRÓ, que es lo reutilizable: la intro sólo corre en la
     primera carga de la sesión y con la pestaña visible, así que no se puede
     disparar a mano desde una herramienta. En vez de pelearse con eso, se
     forzó `data-intro="curtain"`, se rellenó el lienzo de BLANCO SÓLIDO con el
     contexto 2D —sin Three.js, sin animación, sin requestAnimationFrame— y se
     fotografió. La silueta que salió respondía la pregunta de un vistazo. Para
     comprobar que el rótulo no se recortaba se dibujó encima un rectángulo
     punteado con su extensión real (0.86 del ancho, 0.53 del alto).
     --------------------------------------------------------------------- */

  /* LOS BORDES SE DESVANECEN, y esto no es adorno: quita un artefacto.

     El lienzo es una caja de 16:7 y recorta lo que se sale de ella. Las
     partículas que derivan hacia afuera no se iban apagando: desaparecían de
     golpe contra una línea perfectamente recta, y las cuatro líneas juntas
     dibujaban el contorno de un rectángulo que no existe. Se ve sobre todo en
     el momento en que las partículas se dispersan, que es justo cuando el ojo
     está mirando el borde.

     No había ningún `border` ni `box-shadow` que quitar —se comprobó: cero en
     .intro, .intro__stage y .intro__canvas—. El contorno era el corte.

     EL PRIMER ARREGLO FUE DOS DEGRADADOS RECTOS QUE SE INTERSECABAN, y no
     sirvió: el cliente siguió viendo el contorno. La causa, comprobada
     fotografiando el lienzo relleno de blanco sólido con la máscara puesta, es
     que ablandaba el corte pero no cambiaba su FORMA. Un degradado que entra
     recto por cada lado deja cuatro líneas paralelas a la caja y cuatro
     esquinas: un rectángulo difuminado sigue siendo un rectángulo, y el ojo lo
     lee igual de rectángulo. Subir el desvanecido al 22-25%, que era la
     hipótesis apuntada, sólo habría dado un rectángulo más borroso.

     La máscara es UNA elipse, y los radios son medio ancho y medio alto a
     propósito: así el degradado llega a alfa 0 justo en los puntos medios de
     cada lado, y en las esquinas —que quedan más lejos que cualquier radio— ya
     está pasado, o sea transparente. No queda ni una arista recta ni una
     esquina: la silueta se cierra sobre sí misma y la nube se lee como nube.

     Sigue siendo alfa pura, como las marquesinas, así que funciona igual sobre
     el suelo claro y sobre el oscuro sin una sola regla que consulte el tema.

     EL 86% NO ES UN NÚMERO BONITO: es el ancho que ocupa el rótulo. intro.js
     escala la palabra a 0.86 del ancho visible (`const escala` en montar()).
     Si el núcleo opaco baja de ahí, el desvanecido le muerde las puntas a la
     "c" y a la "r". Si cambia aquel 0.86, este 86% cambia con él. Se comprobó
     dibujando encima la extensión real del rótulo: su tinta vive en la banda
     central —"corder" no tiene astas en los extremos— y queda entera dentro
     del núcleo. */
  --intro-nucleo: 86%;

  -webkit-mask-image:
    radial-gradient(ellipse 50% 50% at 50% 50%, #000 0 var(--intro-nucleo), transparent 100%);
  mask-image:
    radial-gradient(ellipse 50% 50% at 50% 50%, #000 0 var(--intro-nucleo), transparent 100%);
}

/* El canvas sólo se enciende si Three.js llegó a tiempo y montó la escena. */
.intro[data-webgl="true"] .intro__canvas { opacity: 1; }

/* El rótulo en tipografía real. Es lo que se ve en el primer cuadro y también
   lo que se ve en el último: las partículas son el tránsito entre los dos.

   Va en --text y no en el degradado de marca porque es TEXTO, y DESIGN.md
   prohíbe el cian como texto (1.9:1 sobre blanco). El degradado azul→cian vive
   en las partículas, que son remate gráfico y se apagan al armarse la palabra. */
.intro__word {
  position: relative;
  margin: 0;
  font-size: clamp(3.2rem, 1.5rem + 9vw, 8rem);
  font-weight: 800;
  letter-spacing: -0.055em;
  line-height: 1;
  color: var(--text);
  transition: opacity 0.34s var(--out), transform 0.5s var(--out);
}

/* Con WebGL montado, el texto cede el sitio a las partículas y vuelve cuando
   la palabra ya está armada. Sin WebGL nunca se va: entra con su propia
   animación y se queda. */
.intro[data-webgl="true"] .intro__word { opacity: 0; }
.intro[data-webgl="true"][data-settled="true"] .intro__word { opacity: 1; }

/* La versión sin WebGL: el rótulo entra solo, con profundidad de CSS. Es la que
   ve quien tiene la GPU bloqueada, quien todavía está descargando Three.js y
   quien entra con un navegador sin módulos. Tiene que verse bien por sí sola,
   no como el hueco donde iba a haber algo. */
:root[data-intro="curtain"] .intro:not([data-webgl="true"]) .intro__word {
  animation: intro-word 0.72s var(--out) both;
}

@keyframes intro-word {
  from { opacity: 0; transform: perspective(800px) rotateX(24deg) translateY(26px) scale(0.94); }
  to   { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   Variante "hero": las cargas siguientes de la misma sesión
   ---------------------------------------------------------------------------

   Sin telón y sin Three.js. El titular del hero —que es el rótulo de la
   portada— entra inclinado sobre su propio eje y se endereza. Cuesta cero
   bytes, corre en el compositor y no retrasa nada.

   Deliberadamente NO se repite el rótulo "corder" aquí: ya está en el
   encabezado, tres centímetros más arriba, y ponerlo dos veces en la misma
   pantalla convierte una marca en un letrero. */
:root[data-intro="hero"] .opening__title {
  /* Mismo retraso que le da el escalonado de la apertura (.opening__title va
     a 0.08s): esto SUSTITUYE aquella animación para el titular, no se suma a
     ella, así que si se pierde el retraso el titular se adelanta a la etiqueta
     que debería precederlo y el escalonado se lee al revés. */
  animation: hero-tilt 0.85s var(--out) 0.08s both;
  transform-origin: left center;
}

@keyframes hero-tilt {
  from { opacity: 0; transform: perspective(900px) rotateX(18deg) translateY(18px); }
  to   { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   Curvas, tiempos y utilidades
   ---------------------------------------------------------------------------

   --out y --in-out ya NO se declaran aquí: viven en el :root de site.css, que
   siempre se carga antes, junto a --ease y con el porqué de cada una. Estaban
   aquí y el resultado era que la portada tenía tres curvas y las otras veinte
   páginas una sola, sin que nada lo justificara.

   Ninguna transición de interfaz pasa de 250 ms. Las tres excepciones son
   deliberadas y no son interfaz: la entrada del hero (se ve una vez por
   visita), el recorrido de la captura (es explicativo) y las marquesinas
   (movimiento continuo).
   --------------------------------------------------------------------------- */

/* Fuera de pantalla pero legible por lector de pantalla.

   Recorte de un píxel y no display:none: lo que se oculta con display:none
   tampoco lo anuncia el lector de pantalla, que es justamente para quien está
   escrito. Lo usan las dos marquesinas, cuya copia visible va duplicada y no
   se puede leer en voz alta. */
.u-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* El ritmo vertical y el encabezado asimétrico (`.lede`) SUBIERON A site.css.

   Estaban aquí porque nacieron con la portada, y el efecto secundario fue que
   la portada componía en banda mientras las nueve páginas de dentro apilaban
   etiqueta, título y apoyo en tres renglones: dos sitios distintos bajo el
   mismo dominio. Ahora el sistema es del sitio y esta hoja se queda sólo con
   lo que de verdad es exclusivo de "/".

   No los redeclares aquí. home.css carga DESPUÉS de site.css y gana a igual
   especificidad, así que una copia olvidada de `.lede` en este archivo sería
   invisible en el resto del sitio y sólo divergiría en la portada. */

/* ===========================================================================
   1. Apertura
   ===========================================================================

   Asimétrica de verdad: el titular ocupa la columna izquierda a tamaño de
   rótulo y el cotizador la derecha, a toda la altura del bloque.

   La versión anterior ponía el titular cruzando las dos columnas y dejaba la
   izquierda con dos frases y medio metro de vacío debajo. Aquí la columna
   izquierda lleva titular, frase y tres datos duros, así que las dos columnas
   terminan más o menos a la misma altura y no hay hueco que disimular.
   =========================================================================== */

.opening {
  position: relative;
  padding-block: clamp(2.25rem, 6vw, 4.5rem) clamp(2.75rem, 6vw, 4.5rem);
}

.opening__inner {
  display: grid;
  gap: clamp(1rem, 3vw, 1.5rem);
  align-content: start;
}

.opening__eyebrow {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-ink);
}

/* El rótulo. `display: grid` y no dos <span> en línea: cada voz ocupa su
   renglón completo y el salto no depende de dónde caiga el ancho disponible,
   que es lo que hacía que en ciertos anchos el titular se partiera en un sitio
   feo. */
.opening__title {
  display: grid;
  font-size: clamp(2.4rem, 1rem + 5.9vw, 5.6rem);
  line-height: 0.92;
  letter-spacing: clamp(-0.05em, -0.032em - 0.3vw, -0.032em);
  text-wrap: balance;
}

.opening__title-lead {
  font-weight: 800;
  color: var(--text);
}

.opening__title-soft {
  font-weight: 400;
  color: var(--text-faint);
}

/* La palabra que carga el argumento, con un subrayado limpio de un extremo
   azul a un extremo cian.

   Es el único lugar de la portada donde --spark toca el texto, y aparece aquí
   porque es exactamente para lo que sirve: un remate gráfico que se mira, no
   que se lee. Como color de letra no llegaría al mínimo de contraste sobre
   blanco ni de lejos.

   VA EN "medida", en la voz sólida, y no en la segunda línea. Se probó en
   "cómo ya trabajas" y se veía mal por dos razones a la vez: el trazo cruzaba
   los descendentes de la j y la y, y quedaba a media altura del titular en vez
   de rematarlo, así que se leía como una tachadura mal puesta. "medida" no
   tiene descendentes y cierra la primera voz.

   Va detrás del texto y no como text-decoration para controlar el grosor y la
   separación de la línea base con precisión.

   box-decoration-break: clone lo vuelve a pintar en cada renglón si la frase se
   parte en dos. Sin eso, al envolver, la segunda línea se queda sin subrayado. */
.opening__title em {
  font-style: normal;
  padding-inline: 0.04em;
  background-image: linear-gradient(90deg, var(--primary), var(--spark));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.07em;
  border-radius: 1px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

.opening__aside {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  align-content: start;
}

.opening__lead {
  max-width: 42ch;
  font-size: clamp(1.02rem, 0.96rem + 0.4vw, 1.2rem);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--text-mute);
  text-wrap: pretty;
}

/* AQUÍ VIVÍAN LOS SELLOS "Respuesta / Precio / Código", y se quitaron enteros.

   No es que estuvieran mal hechos: eran tres sellos con su pictograma, su
   rótulo diminuto y su valor, y habían sustituido a una tabla de
   etiqueta-valor-línea que sí se leía genérica. El problema fue de CONTENIDO,
   no de forma.

   Ninguno de los tres decía nada que la página no dijera ya:

     · "Respuesta · 24 horas" está literalmente repetido en el pie del propio
       cotizador, a menos de 200 px ("Respuesta en 24 horas · Sin costo ni
       compromiso").
     · "Precio · Cerrado" y "Código · Tuyo" son dos de los tres compromisos de
       la sección "Lo que firmamos", donde además vienen explicados en vez de
       reducidos a una palabra.

   Y costaban lo más caro de la página: medido en un teléfono de 390 × 844
   ocupaban 85 px de los que hacían que el cotizador arrancara en y=504 y sólo
   se viera el 52 % al abrir. Sin ellos arranca en 373 y se ve el 72 %.

   Si alguna vez vuelve a hacer falta un bloque de datos duros en el hero, que
   sea con datos que NO estén ya en la página. Ése es el listón. */

/* --- El campo que el vidrio desenfoca ---------------------------------------
   ===========================================================================

   El cotizador pasó a ser una placa de vidrio esmerilado, y un vidrio sobre
   nada no se lee como vidrio: se lee como un recuadro translúcido. Necesita
   ALGO detrás que desenfocar. Esto es ese algo.

   DOS DECISIONES QUE SOSTIENEN EL RESTO:

   1. El campo está ACOTADO al cotizador, no tendido sobre el hero. Un hero
      entero en azul saturado diluiría la regla de marca más importante del
      sistema: "un solo azul significa acción". Si el fondo es azul, el botón
      azul deja de destacar. Aquí el color vive sólo detrás de la placa, así
      que el acento sigue siendo lo único que pide clic.

   2. El degradado es primary -> secondary, los dos de la familia del azul, y
      NO usa --spark. El cian está reservado a remate gráfico fino —el
      subrayado del titular— y extenderlo a un campo grande lo convertiría en
      color de fondo, que es exactamente lo que DESIGN.md prohíbe.

   Sobresale unos pocos píxeles de la placa por los cuatro lados: eso es lo que
   hace que el vidrio se vea apoyado SOBRE algo en vez de recortado contra el
   suelo de la página. Sin ese asomo, el efecto no se entiende.

   Es un pseudo-elemento y no un div: no es contenido y no debe existir para un
   lector de pantalla. */
.opening__form {
  /* Cuánto asoma el campo de color POR ARRIBA de la placa. Vive aquí y no
     suelto dentro del ::before porque hay que restarlo del hueco de la rejilla:
     si los dos números se escriben por separado, se separan. */
  --campo-asoma: 0.9rem;

  position: relative;
  width: 100%;

  /* APILADO EN MÓVIL: EL CAMPO SE COMÍA EL AIRE ENTERO.
     Medido a 390 px, entre el final de los sellos ("24 horas") y el borde del
     campo quedaban 2 px. La rejilla da 16 px de hueco y el campo sube 14.4:
     16 − 14.4 = 1.6. El cotizador arrancaba pegado al texto de arriba.
     Se devuelve lo que el campo se lleva, más un respiro propio, de modo que el
     hueco que se VE es el que la rejilla pretendía dar. */
  margin-top: calc(var(--campo-asoma) + 0.75rem);
}

.opening__form::before {
  content: "";
  position: absolute;
  /* Negativo: el campo es más grande que la placa y asoma por los bordes. El
     de arriba sale de --campo-asoma, que es el mismo valor que el margen de la
     placa compensa cuando la portada se apila. */
  inset: calc(-1 * var(--campo-asoma)) -0.75rem -1.1rem;
  z-index: 0;
  border-radius: calc(var(--radius-lg) + 0.8rem);

  /* EL CAMPO VA OSCURO, Y NO ES GUSTO: ES LEGIBILIDAD MEDIDA.

     La primera versión iba de --primary a --secondary, los dos tal cual. El
     índigo es claro, y con el 13 % de blanco de la placa encima el fondo
     efectivo quedaba en rgb(102,117,225). Medido: el texto blanco daba 4.04:1,
     por debajo del 4.5 que pide un texto de 15 px. TODO el contenido de la
     placa —la pregunta, las seis opciones, el contador— estaba por debajo del
     mínimo. Un formulario bonito e ilegible no es un formulario.

     Los dos extremos se derivan de --primary mezclado con la tinta del
     sistema, así que siguen siendo el azul de la marca y se mueven solos si
     mañana cambia el token. No hay un hex nuevo en esta hoja.

     Con el campo así, el fondo efectivo baja a ~rgb(63,101,195) y el blanco
     sube a 5.4:1.

     LA ESQUINA FRÍA NO ES ADORNO. En la primera versión los dos extremos eran
     azul e índigo —#2563eb y #6366f1, casi el mismo tono—, y el campo salía
     plano: fotografiado, el vidrio no se leía como vidrio sino como una tarjeta
     azul. Un esmerilado necesita que detrás haya algo que viajar; si el fondo
     es liso, el desenfoque no tiene trabajo que enseñar. El cian entra por la
     esquina inferior muy oscurecido, así que funciona como remate gráfico —que
     es para lo único que DESIGN.md lo autoriza— y no como fondo de un control.

     EL 28 % ESTÁ MEDIDO Y NO ES REDONDEO. Entró primero al 34 %, dando por
     hecho que un cian bien oscurecido sería más oscuro que el azul. Es falso:
     en luminancia relativa el verde pesa 0.72 y el azul 0.07, así que un
     turquesa que a ojo se ve más apagado ilumina más. Medido, ese 34 % era el
     punto MÁS CLARO de todo el campo y tumbaba cuatro textos por debajo del
     4.5 (el pie de la placa en 4.38). El techo real es 30 %, el peso al que el
     cian iguala la luminancia del extremo azul; a 28 % queda por debajo y el
     peor punto del campo vuelve a ser el azul de siempre. */
  background:
    radial-gradient(115% 85% at 10% 2%,
      color-mix(in srgb, var(--secondary) 46%, #0f172a) 0%, transparent 58%),
    radial-gradient(95% 80% at 96% 100%,
      color-mix(in srgb, var(--spark) 28%, #0f172a) 0%, transparent 60%),
    linear-gradient(152deg,
      color-mix(in srgb, var(--primary) 62%, #0f172a) 0%,
      color-mix(in srgb, var(--primary) 30%, #0f172a) 100%);
  /* Sin `filter: blur()`: desenfocar una caja de este tamaño en cada cuadro es
     caro en móvil, y el desenfoque de verdad lo pone el backdrop-filter de la
     placa. Aquí basta con que el degradado ya sea suave. */
  pointer-events: none;
}

/* El respaldo sin JavaScript va POR ENCIMA del campo, no debajo. */
.opening__form > * { position: relative; z-index: 1; }

/* Sin el cotizador montado no hay vidrio, así que tampoco hay nada que
   desenfocar: el campo de color se retira y el enlace de respaldo se queda
   sobre el suelo normal de la página. */
.opening__form[data-quoter-roto="true"]::before { display: none; }

@media (min-width: 1000px) {
  /* Titular y apoyo a la izquierda, cotizador a la derecha ocupando las tres
     filas. Con align-items: start las dos columnas arrancan arriba y la
     derecha crece con su propio contenido. */
  .opening__inner {
    grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
    grid-template-areas:
      "eyebrow forma"
      "titulo  forma"
      "apoyo   forma";
    column-gap: clamp(2.5rem, 5vw, 4.5rem);
    row-gap: clamp(1.1rem, 2vw, 1.6rem);
    align-items: start;
  }

  .opening__eyebrow { grid-area: eyebrow; }
  .opening__title   { grid-area: titulo; max-width: 15ch; }
  .opening__aside   { grid-area: apoyo; padding-top: 0.5rem; }
  /* A dos columnas el cotizador ya no tiene nada encima, así que el margen que
     compensa lo que el campo se come en móvil sobra y desalinearía la placa
     respecto al rótulo. */
  .opening__form    { grid-area: forma; margin-top: 0; }
}

/* ---------------------------------------------------------------------------
   Entrada del hero
   ---------------------------------------------------------------------------

   Lo único que se anima al cargar, y se hace con CSS y no con
   IntersectionObserver por un motivo concreto: está sobre el pliegue, así que
   un observador dispararía igual pero después de que el JavaScript arranque, y
   el primer cuadro ya se habría visto quieto.

   Escalonado corto (60-70 ms entre piezas). Más separación y deja de leerse
   como una entrada y empieza a leerse como que la página carga lenta.

   Sólo opacidad y transform: las dos propiedades que el navegador resuelve sin
   volver a calcular el layout. ------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .opening__eyebrow,
  .opening__title,
  .opening__aside,
  .opening__form {
    animation: enter 0.62s var(--out) both;
  }

  .opening__eyebrow { animation-delay: 0.02s; }
  .opening__title   { animation-delay: 0.08s; }
  .opening__aside   { animation-delay: 0.15s; }
  .opening__form    { animation-delay: 0.21s; }
}

@keyframes enter {
  from { opacity: 0; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}

/* ===========================================================================
   2. Las marquesinas
   ===========================================================================

   Dos cintas continuas: los módulos que construimos y el stack con el que
   están construidos.

   Van en TEXTO y no en píldoras ni en logos. Las píldoras convertían la cinta
   en una fila de botones falsos —algo que parece pulsable y no lo es— y los
   logos de terceros son marcas registradas que además obligan a mantener una
   versión por tema. En texto, la cinta hereda el color del sistema y funciona
   igual en claro y en oscuro.

   Movimiento constante, así que lineal: cualquier curva de aceleración en un
   bucle infinito se nota como un tirón cada vuelta.

   EL TRUCO DEL BUCLE: la pista lleva el listado impreso dos veces y se
   desplaza exactamente la mitad de su ancho. Al terminar, la segunda copia
   está justo donde arrancó la primera, así que el salto de vuelta al origen no
   se ve. Por eso la animación mueve un PORCENTAJE y no píxeles: el porcentaje
   de translate se resuelve contra el propio elemento y sigue siendo exacto
   aunque cambien el número de entradas, la fuente o el ancho de la pantalla.
   =========================================================================== */

/* --- Los módulos, en píldoras quietas ---------------------------------------
   ===========================================================================

   Era una marquesina. Ver el porqué del cambio en home.php; aquí sólo lo que
   hay que saber para tocar el CSS.

   `flex` con `wrap` y no `grid`: las doce etiquetas tienen anchos muy
   distintos ("Facturación" contra "Integración con paqueterías"), y una
   rejilla de columnas iguales dejaría a las cortas nadando en su celda. Con
   flex cada píldora mide lo que mide su texto y las filas se llenan solas, que
   es como se comporta una nube de etiquetas bien hecha.

   `justify-content: center` para que la última fila, que casi nunca queda
   completa, no se vea colgando a la izquierda.

   La banda conserva las dos reglas de un píxel de la marquesina anterior: son
   las que la separan del hero de arriba y de la prueba de abajo sin gastar
   otra sección entera. */
.modules {
  padding-block: clamp(2rem, 4.5vw, 3.25rem);
  border-block: 1px solid var(--border);
  background: var(--bg-alt);
}

.modules__grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.55rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.modules__item {
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--edge);
  font-size: clamp(0.82rem, 0.78rem + 0.2vw, 0.95rem);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.012em;
  color: var(--text-mute);
  white-space: nowrap;
}

/* Separación entre el encabezado nuevo y las píldoras. Menos que el de una
   sección normal: la banda entera es compacta a propósito y no debe empezar a
   pesar como las de arriba. */
.modules .lede { margin-bottom: clamp(1.5rem, 3.5vw, 2.25rem); }

/* A 360 px, "Integración con paqueterías" no cabe de una pieza. Se permite que
   parta: una píldora de dos renglones se lee bien, una que desborda la
   pantalla no. */
@media (max-width: 560px) {
  .modules__item { white-space: normal; }
}

/* --- El recorte de móvil -----------------------------------------------------

   Doce píldoras apiladas son siete renglones a 390 px y DIEZ a 320. Se muestran
   seis y el resto queda tras un botón.

   El recorte lo enciende el JavaScript poniendo data-recorte, y no una media
   query a secas, por un motivo concreto: si lo hiciera el CSS solo, quien
   navegue sin JavaScript vería seis píldoras y ninguna forma de ver las otras
   seis. Así, sin JavaScript no hay recorte y se ven las doce, que es el estado
   correcto cuando no hay nada que pueda desplegarlas.

   `display: none` y no altura cero: una píldora oculta no debe dejar hueco en
   el flex ni recibir el foco al tabular. */
.modules__grid[data-recorte="true"] .modules__item:nth-child(n + 7) {
  display: none;
}

.modules__mas {
  display: block;
  margin: 1rem auto 0;
  padding: 0.5rem 1.1rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: none;
  font: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--accent-ink);
  cursor: pointer;
  transition: border-color 0.15s var(--ease),
              color 0.15s var(--ease),
              transform 0.16s var(--out);
}

.modules__mas[hidden] { display: none; }

/* El mismo tacto que el resto de los controles del sistema. */
.modules__mas:active { transform: scale(0.97); }

@media (hover: hover) and (pointer: fine) {
  .modules__mas:hover { border-color: var(--accent); color: var(--accent); }
}

.modules__mas:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.ticker {
  position: relative;
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  border-block: 1px solid var(--border);
  overflow: hidden;
}

.ticker--stack {
  /* El relleno inferior baja al del RIEL, no al de una sección normal.

     Antes los dos lados valían 7 rem y la banda quedaba descompensada: entre
     el titular y los logos había 44 px y debajo de los logos 108. El ojo lee
     eso como que la fila de logos está colgando del texto en vez de ser un
     elemento propio. Medido a 1440 px.

     Ahora el aire de abajo lo pone el riel con su propio relleno (ver
     `.ticker--stack .ticker__viewport`), y la sección sólo conserva el de
     arriba, que es el que la separa de las capturas. */
  padding-block: clamp(3.5rem, 7.5vw, 7rem) 0;
  border-top: 0;
  background: var(--bg-alt);
}

/* Más aire entre el titular y los logos: los 44 px de antes eran la mitad de
   lo que la sección se daba a sí misma por arriba. */
.ticker--stack .lede { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* EL RIEL DE LOGOS ES SU PROPIA BANDA, con relleno igual arriba y abajo.

   Es lo que resuelve la asimetría de raíz: en vez de que los logos floten
   entre un hueco chico arriba y uno enorme abajo, la fila vive dentro de un
   riel hundido con el mismo aire por los dos lados.

   El suelo (--bg) y no --surface-2: para hundir algo se usa siempre --bg, que
   es el valor más lejano al lector en los DOS temas. Con --surface-2 el riel
   se hundiría en claro y flotaría en oscuro. Es la regla 2 del sistema y ya se
   rompió una vez en el control de navegación. */
.ticker--stack .ticker__viewport {
  padding-block: clamp(1.5rem, 3vw, 2.4rem);
  border-top: 1px solid var(--border);
  background: var(--bg);
}

/* Los extremos se desvanecen para que las entradas salgan y entren en vez de
   cortarse contra el borde de la pantalla. La máscara es alfa pura, así que
   funciona igual sobre el suelo claro y sobre el oscuro sin una sola regla que
   consulte el tema. */
.ticker__viewport {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg,
                      transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(90deg,
              transparent 0, #000 7%, #000 93%, transparent 100%);
}

/* La velocidad se bajó de 58 s a 26 s por vuelta.

   Los 58 s venían de cuando la cinta llevaba DOCE etiquetas de texto largo:
   con esa carga, correr rápido impedía leerlas. Ahora son siete logos, que se
   reconocen de un vistazo y no hay que leer, y a esa velocidad la fila se
   arrastraba: en los primeros segundos de la sección parecía que no se movía.

   26 s es donde el movimiento se lee como movimiento sin que ningún logo pase
   demasiado rápido para reconocerlo. Es un valor por vuelta completa, no por
   logo, así que si mañana entran más marcas habrá que subirlo. */
/* EL HUECO DE LA CINTA: por qué hay seis copias y no dos.

   Iban dos, y el salto de la animación era un -50 % fijo —o sea, exactamente
   una copia—. La cuenta es correcta, pero sólo se ve continua si la pista es
   MÁS ANCHA que la ventana. Medido en producción a 1920: cada copia ocupaba
   885 px y la pista entera 1769, contra un riel de 1905. La cinta era más
   corta que la pantalla, así que el vacío no aparecía "a veces al empezar":
   estaba siempre, y se hacía evidente en cada vuelta.

   Con seis copias la pista pasa de 5300 px y cubre hasta ventanas de ~4400.
   Y el salto deja de ser el -50 % atado a que sean dos: se calcula como una
   copia (`100% / --ticker-copias`), así que cambiar el número de copias aquí
   arriba —o añadir una herramienta a la lista— no vuelve a descuadrar el
   ciclo. Ese es el acoplamiento que causó esto. */
.ticker__track {
  --ticker-copias: 6;

  display: flex;
  width: max-content;
  animation: ticker-run 26s linear infinite;
  will-change: transform;
}

/* La segunda cinta corre al revés, y por eso existe el modificador. Hoy es la
   ÚNICA que queda —los módulos pasaron a píldoras quietas— así que el cruce de
   direcciones ya no se ve; se conserva el sentido inverso porque es el que
   aleja la cinta del sentido de lectura y la hace menos insistente. */
.ticker__track--back {
  animation-name: ticker-run-back;
  animation-duration: 30s;
}

.ticker__group {
  display: flex;
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
  padding-right: clamp(2rem, 5vw, 4rem);
}

/* Texto grande y apagado, no una etiqueta. A este tamaño la cinta se lee de
   reojo mientras se desplaza, que es exactamente el trabajo que tiene. */
.ticker__item {
  flex: none;
  font-size: clamp(1.05rem, 0.9rem + 0.8vw, 1.6rem);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.1;
  color: var(--text-faint);
  white-space: nowrap;
  transition: color 0.18s var(--out);
}

/* --- La cinta del stack, en logos ------------------------------------------
   ===========================================================================

   `.ticker--stack .ticker__item` se quedó sin uso al pasar la cinta de texto a
   marcas: se retiró para no dejar una regla que nadie aplica. Si algún día
   vuelve el texto, era mono a 0.9-1.15 rem con tracking -0.03em.

   Los logos van en UN tono heredado, `--text-faint`, el mismo que ya tenía el
   texto. Eso es lo que hace que la cinta se lea como una textura y no como
   siete anuncios de otras marcas compitiendo con el titular que tienen
   encima — y es también lo que permite que el mismo marcado sirva en claro y
   en oscuro sin una sola regla que consulte el tema.

   Alto fijo y ancho automático: los siete trazos vienen en la misma caja de
   24, pero sus formas ocupan proporciones distintas (el de Google Sheets es
   casi cuadrado, el de MySQL es muy ancho). Fijando el alto, todos pesan
   ópticamente lo mismo; fijando el ancho, unos saldrían enanos y otros
   aplastados. */

/* CADA LOGO EN SU COLOR, SOBRE UNA PLACA BLANCA.

   Iban en un solo tono heredado y se veían apagados. En color se reconocen
   antes de leerse, que es todo el sentido de poner logos.

   La placa no es decoración: es lo que hace que el color FUNCIONE en los dos
   temas. Medido contra el suelo de la página, tres de los siete se caen:

     JavaScript  #F7DF1E   1.2:1 sobre blanco   invisible en claro
     WhatsApp    #25D366   2.1:1 sobre blanco   flojo
     Tailwind    #06B6D4   2.4:1 sobre blanco   flojo
     Apache      #D22128   4.5:1 sobre blanco   pero 2.0:1 sobre el suelo oscuro
     PHP         #777BB4   3.3:1 sobre blanco   pero 2.6:1 sobre el suelo oscuro

   O sea: no hay UN tono de fondo que sirva para los siete en los dos temas.
   Con la placa siempre blanca —`--on-accent`, que es #ffffff en claro y en
   oscuro— cada marca se ve contra el fondo para el que fue diseñada, que es el
   blanco. En tema oscuro la fila se lee como una hilera de iconos de
   aplicación, que es exactamente lo que son.

   Y de paso resuelve el espaciado: la placa trae su propio relleno, así que
   los trazos dejan de tocarse entre sí y con lo que tienen encima.

   DESIGN.md ya contemplaba los logos de terceros como excepción a "un solo
   azul" ("los logos dentro de las capturas"); esto la extiende a la cinta, con
   la misma lógica: el color es identidad ajena, no señal de la interfaz. */

.ticker__logo {
  flex: none;
  display: grid;
  place-items: center;
  width: clamp(2.9rem, 2.4rem + 2.2vw, 3.9rem);
  height: clamp(2.9rem, 2.4rem + 2.2vw, 3.9rem);
  padding: clamp(0.65rem, 0.5rem + 0.6vw, 0.95rem);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--on-accent);
  box-shadow: var(--edge), var(--shadow-sm);
}

.ticker__logo svg {
  width: 100%;
  height: 100%;
  color: var(--marca, var(--text-faint));
  /* Los trazos de Simple Icons son formas cerradas, no líneas: se pintan con
     relleno. Sin esto heredarían el `fill: none` que usan los iconos de trazo
     del resto del sitio y no se vería nada. */
  fill: currentColor;
}

/* Los hex oficiales de Simple Icons, tal como los publica el paquete. No se
   ajustan ni se armonizan con la paleta: una marca ajena se pinta en su color
   o no se pinta. */
.ticker__logo[data-marca="php"]          { --marca: #777BB4; }
.ticker__logo[data-marca="mysql"]        { --marca: #4479A1; }
.ticker__logo[data-marca="javascript"]   { --marca: #F7DF1E; }
.ticker__logo[data-marca="tailwindcss"]  { --marca: #06B6D4; }
.ticker__logo[data-marca="apache"]       { --marca: #D22128; }
.ticker__logo[data-marca="googlesheets"] { --marca: #34A853; }
.ticker__logo[data-marca="whatsapp"]     { --marca: #25D366; }

/* El cuadrado amarillo de JavaScript es un bloque sólido de color muy claro:
   sobre la placa blanca se queda sin contorno y parece un hueco. Un borde
   interior de su propio color oscurecido lo cierra sin repintar la marca. */
.ticker__logo[data-marca="javascript"] svg {
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, #F7DF1E 70%, #000);
}

/* La hoja de símbolos. No se puede ocultar con `display: none` —en algunos
   navegadores eso deja sin resolver los <use> que apuntan a ella—, así que se
   saca del flujo con caja cero y recorte. */
.u-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* El recorrido es UNA copia, no la mitad de la pista. En un translate el
   porcentaje se mide sobre el propio elemento, así que `100% / --ticker-copias`
   es exactamente el ancho de un grupo: al terminar la vuelta, la copia 2 está
   donde estaba la 1 y el corte no existe. Vale para cualquier número de copias
   con tal de que sean dos o más. */
@keyframes ticker-run {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(-100% / var(--ticker-copias)), 0, 0); }
}

@keyframes ticker-run-back {
  from { transform: translate3d(calc(-100% / var(--ticker-copias)), 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* LA CINTA YA NO SE DETIENE AL PASAR EL CURSOR.

   La regla existía por un motivo razonable: quien acerca el ratón a una cinta
   de TEXTO es porque quiere leerla, y perseguir una palabra en movimiento es
   lo que hace que el patrón se sienta barato.

   Pero ese argumento se fue con el texto. Lo que corre ahora son siete logos,
   que no se leen: se reconocen. Aquí el frenazo no ayuda a nadie y sí estorba
   —la cinta cruza media pantalla, así que el cursor la pisa constantemente al
   ir hacia cualquier otra cosa, y se queda congelada sin que nadie lo haya
   pedido. Un elemento que se para solo cada vez que pasas por encima se siente
   roto, no atento.

   Tampoco queda el realce de contraste que la acompañaba: servía para que el
   texto apagado se leyera mejor al detenerse, y los logos ya van a todo color
   con su propio tono de marca. No hay contraste que subir.

   La cinta no responde al cursor en absoluto, y está bien: no es un control,
   es una textura. */

/* ===========================================================================
   3. Prueba
   ===========================================================================

   Va inmediatamente después de la cinta de módulos porque es la objeción que
   sigue: "¿esto de verdad lo han hecho?".

   No es la rejilla del portafolio. Aquí cada proyecto ocupa una franja ancha y
   la captura se ve grande: en el portafolio el trabajo es comparar, aquí el
   trabajo es creer.
   =========================================================================== */

.proof-list {
  display: grid;
  gap: clamp(0.85rem, 2.5vw, 1.35rem);
}

.proof {
  display: grid;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--edge), var(--shadow-sm);
  overflow: hidden;
  transition: border-color 0.22s var(--out),
              box-shadow 0.22s var(--out),
              transform 0.24s var(--out);
}

@media (hover: hover) and (pointer: fine) {
  /* Se levanta dos píxeles. Es el gesto más barato que convierte una franja de
     contenido en una tarjeta de producto: la tarjeta reacciona como un objeto
     antes de que el cursor llegue a ningún enlace. */
  .proof:hover {
    border-color: var(--border-strong);
    box-shadow: var(--edge), var(--shadow-lg);
    transform: translateY(-2px);
  }
}

/* La barra de navegador va encima del marco, no dentro: dentro ocuparía alto
   propio y la captura, que pide el 100 %, se saldría por abajo. */
.proof__media {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border-bottom: 1px solid var(--border);
}

.proof__frame {
  position: relative;
  flex: 1;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--surface-2);
}

/* --- El recorrido de la captura -------------------------------------------

   La tarjeta enseña el encuadre de arriba. Al pasar el cursor —o al entrar en
   pantalla, en táctil— la captura se desplaza dentro del navegador simulado y
   se ve la pantalla entera sin salir de la tarjeta.

   La distancia la mide home.js por captura y la deja en --scroll-distance,
   porque cada imagen tiene su propio alto y un porcentaje fijo dejaría a unas
   cortas y a otras enseñando el fondo. El valor de reserva es 0 %: si el
   JavaScript no llegó a medir, la imagen se queda quieta en vez de irse a un
   lugar equivocado.

   3.2 s es largo a propósito y no contradice el techo de 250 ms de la
   interfaz: esto no responde a un toque, es una animación que explica algo, y
   a menos de un segundo se lee como un tirón en vez de como un recorrido.

   EL <picture> VA ABSOLUTO Y CUBRIENDO EL MARCO, y no es cosmética:
   `min-height: 100%` sobre la imagen sólo resuelve si su contenedor tiene alto
   definido. Con el <picture> en flujo y alto automático, el porcentaje no se
   puede calcular, el navegador lo descarta, y una captura más ancha que 16/10
   queda más baja que el marco y deja una banda gris debajo. Con inset: 0 el
   contenedor hereda el alto del marco y el mínimo sí aplica. */
.proof__frame picture {
  position: absolute;
  inset: 0;
  display: block;
}

.proof__frame img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  min-height: 100%;
  object-fit: cover;
  object-position: top center;
  transform: translate3d(0, 0, 0);
  transition: transform 3.2s var(--out);
  will-change: transform;
}

@media (hover: hover) and (pointer: fine) {
  .proof:hover .proof__frame img,
  .proof:focus-within .proof__frame img {
    transform: translate3d(0, calc(-1 * var(--scroll-distance, 0%)), 0);
  }
}

/* En táctil no hay cursor que pasar por encima, así que el recorrido lo
   dispara home.js al entrar la tarjeta en pantalla. */
.proof[data-revealed="true"] .proof__frame img {
  transform: translate3d(0, calc(-1 * var(--scroll-distance, 0%)), 0);
}

/* --- EL RECORRIDO ATADO AL SCROLL ------------------------------------------

   Lo de arriba se dispara y corre solo durante 3.2 s. Esto es distinto: la
   captura avanza EXACTAMENTE lo que avanza el dedo. Bajas, la pantalla del
   sistema baja contigo; paras, se para.

   Por qué esta sección y no otra: no es adorno. Estas imágenes son la prueba
   del sitio —las pantallas reales de cada cliente— y el marco sólo enseña el
   encuadre de arriba. Atarlo al scroll hace que el visitante VEA el sistema
   entero sin tocar nada, y en celular, donde no hay cursor que pasar por
   encima, es la única forma de que ocurra.

   La línea de tiempo cuelga del MARCO y no de la imagen. La imagen es más
   alta que su marco y va en posición absoluta, así que su caja se sale por
   abajo y el recorrido empezaría y acabaría en momentos raros. El marco sí
   mide lo que se ve, que es lo que queremos cronometrar.

   El rango no arranca en 0 ni acaba en 100: entre el 25 % y el 80 % de lo que
   la tarjeta tarda en cruzar la pantalla. Así el recorrido ocurre mientras la
   tarjeta está en el centro —donde se mira— y no mientras asoma por el canto.

   Con `both`, fuera del rango la animación sostiene sus extremos, así que le
   gana a las reglas de arriba sin tener que anularlas: donde existe esto,
   manda esto; donde no, quedan el hover y el data-revealed de siempre. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .proof__frame { view-timeline-name: --marco-captura; }

    .proof__frame img {
      transition: none;
      animation: recorrido-captura linear both;
      animation-timeline: --marco-captura;
      animation-range: cover 25% cover 80%;
    }

    @keyframes recorrido-captura {
      from { transform: translate3d(0, 0, 0); }
      to   { transform: translate3d(0, calc(-1 * var(--scroll-distance, 0%)), 0); }
    }
  }
}

.proof__body {
  display: grid;
  gap: 0.5rem;
  align-content: start;
  padding: 1.25rem 1.25rem 1.4rem;
}

/* La industria, como etiqueta de acento y con el mismo tratamiento que los
   eyebrow de sección. En gris era un rengloncito de plantilla; en acento es
   parte del sistema. */
.proof__kind {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--accent-ink);
}

.proof__body h3 {
  font-size: clamp(1.3rem, 1.1rem + 0.85vw, 1.75rem);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

/* El enlace cubre la tarjeta entera.

   Un ::after absoluto sobre una tarjeta con position: relative convierte todo
   el bloque en objetivo de clic sin anidar la tarjeta dentro de un <a>, que
   metería la captura, las etiquetas y el título dentro de un solo nombre
   accesible ilegible. El texto del enlace sigue siendo sólo el nombre del
   proyecto. */
.proof { position: relative; }

.proof__body h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.proof__body h3 a {
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.16em;
  text-decoration-thickness: 2px;
  transition: text-decoration-color 0.15s var(--out);
}

@media (hover: hover) and (pointer: fine) {
  .proof:hover h3 a { text-decoration-color: var(--accent-ink); }
}

.proof__say {
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--text-mute);
  text-wrap: pretty;
}

.proof__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.3rem;
}

.proof__tags li {
  padding: 0.26rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mute);
}

/* "Ver el caso" aparece al pasar el cursor. En táctil se queda visible: sin
   hover, un aviso que sólo existe al pasar el cursor no existe. */
.proof__go {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.6rem;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--accent-ink);
  transition: gap 0.18s var(--out), opacity 0.18s var(--out);
}

.proof__go svg { width: 14px; height: 14px; }

@media (hover: hover) and (pointer: fine) {
  .proof__go { opacity: 0; }
  .proof:hover .proof__go { opacity: 1; gap: 0.55rem; }
}

/* En pantallas anchas la captura y el texto se ponen lado a lado, y el lado
   se alterna. Alternar no es un truco visual: evita que cuatro franjas
   idénticas se lean como una lista y se dejen de mirar a la segunda. */
@media (min-width: 900px) {
  .proof { grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr); align-items: stretch; }

  .proof__media {
    border-bottom: 0;
    border-right: 1px solid var(--border);
  }

  .proof__frame { aspect-ratio: 16 / 9; }

  .proof__body { align-content: center; padding: clamp(1.5rem, 2.8vw, 2.25rem); }

  .proof:nth-child(even) .proof__media {
    order: 2;
    border-right: 0;
    border-left: 1px solid var(--border);
  }

  /* Un caso sin captura no deja media columna vacía: ocupa el ancho completo
     y el texto respira. Puede pasar en cuanto se publique un proyecto antes de
     subirle la imagen. */
  .proof--flat { grid-template-columns: minmax(0, 1fr); }
}

/* ===========================================================================
   5. El cambio
   ===========================================================================

   Dos columnas enfrentadas: cómo se opera hoy y cómo se opera después.

   La columna de "hoy" se hunde (sin fondo, borde punteado, texto apagado) y la
   de "después" es placa elevada con el filo de acento. La jerarquía la da el
   material, no un icono de palomita contra uno de tache.
   =========================================================================== */

.shift {
  display: grid;
  gap: 0.85rem;
}

.shift__col {
  border-radius: var(--radius-lg);
  padding: 1.25rem 1.2rem 1.35rem;
}

.shift__col--now {
  border: 1px dashed var(--border-strong);
  background: transparent;
}

.shift__col--next {
  border: 1px solid var(--border-strong);
  background: var(--surface);
  box-shadow: var(--edge), var(--shadow-md);
  border-top: 3px solid var(--accent-ink);
}

/* --- EL ÚNICO MOMENTO CON MOVIMIENTO PROPIO DE LA PORTADA -------------------

   Todo lo demás que entra al hacer scroll usa el mismo gesto: sube 18 px y se
   desvanece. Aquí no, y el motivo no es variar por variar: ésta es la única
   sección donde el movimiento CUENTA algo. Las dos columnas son el antes y el
   después, y la animación las pone en ese orden —primero lo que hay hoy,
   apagado; medio segundo más tarde lo que habrá, levantándose—. El gesto dice
   lo mismo que el texto, así que no es decoración: es la frase otra vez.

   Por eso está sola. Si la portada entera tuviera momentos así, éste no sería
   un momento; sería el ruido de fondo.

   Las dos entran del mismo `data-in`, y la diferencia la ponen los retrasos y
   el punto de partida de cada una: la de hoy arranca más apagada y sin
   desplazarse, la de después arranca hundida 22 px y llega a su sitio. Nada de
   esto necesita JavaScript nuevo. */
.shift__col[data-reveal] {
  transition: opacity 0.6s var(--out),
              transform 0.6s var(--out),
              filter 0.6s var(--out);
}

.shift__col--now[data-reveal] {
  opacity: 0;
  transform: none;
  /* Entra desvaída, no desplazada: lo de hoy no llega a ningún sitio nuevo. */
  filter: saturate(0.6);
}

.shift__col--next[data-reveal] {
  opacity: 0;
  transform: translateY(22px);
}

.shift__col--now[data-reveal][data-in="true"] {
  opacity: 1;
  filter: none;
}

/* 0.42 s de espera, no 0.5 redondo: la columna de hoy tarda 0.6 en asentarse,
   así que a 0.42 la segunda arranca cuando la primera ya se lee pero todavía
   se está quieta del todo. Salir después de que la primera termine del todo
   deja un hueco muerto y las dos dejan de leerse como un par. */
.shift__col--next[data-reveal][data-in="true"] {
  opacity: 1;
  transform: none;
  transition-delay: 0.42s;
}

.shift__tag {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.shift__col--next .shift__tag { color: var(--accent-ink); }

.shift__list {
  display: grid;
  gap: 0.7rem;
  margin-top: 0.95rem;
}

.shift__list li {
  display: flex;
  gap: 0.6rem;
  font-size: 0.95rem;
  line-height: 1.45;
}

.shift__col--now li { color: var(--text-mute); }

.shift__list svg {
  width: 1.05rem;
  height: 1.05rem;
  flex: none;
  margin-top: 0.15rem;
}

.shift__col--now svg { color: var(--text-faint); }
.shift__col--next svg { color: var(--accent-ink); }

@media (min-width: 780px) {
  .shift { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; align-items: start; }

  /* La placa de "después" se levanta medio escalón sobre la de "hoy". El
     desnivel dice cuál de las dos es la salida antes de leer una palabra. */
  .shift__col--next { transform: translateY(-0.85rem); }
}

/* ===========================================================================
   6. Cómo se trabaja
   ===========================================================================

   Una secuencia, no cuatro opciones. Por eso es una línea con nodos y no
   cuatro tarjetas iguales: cuatro tarjetas se leen como un menú del que hay
   que elegir uno.
   =========================================================================== */

.flow {
  display: grid;
  gap: 0;
}

.flow__step {
  position: relative;
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr);
  gap: 0 0.9rem;
  padding-bottom: 1.75rem;
}

.flow__step:last-child { padding-bottom: 0; }

/* La línea que une los nodos. Se dibuja desde el nodo hacia abajo y el último
   paso no la lleva, para que la secuencia termine en vez de desvanecerse. */
.flow__step:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 1.22rem;
  top: 2.6rem;
  bottom: 0.35rem;
  width: 2px;
  background: var(--border-strong);
}

.flow__n {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--edge);
  font-size: 0.9rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.flow__step[data-first="true"] .flow__n {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-accent);
}

.flow__body { padding-top: 0.3rem; }

.flow__body h3 {
  font-size: 1.12rem;
  font-weight: 700;
  letter-spacing: -0.028em;
}

.flow__body p {
  margin-top: 0.35rem;
  font-size: 0.93rem;
  line-height: 1.55;
  color: var(--text-mute);
  max-width: 44ch;
}

@media (min-width: 900px) {
  .flow { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.5rem; }

  .flow__step { grid-template-columns: 1fr; gap: 0.85rem; padding-bottom: 0; }

  /* En horizontal la línea corre entre nodo y nodo, a la altura del centro del
     nodo, y se corta antes del siguiente para no pasarle por encima. */
  .flow__step:not(:last-child)::before {
    left: 3.25rem;
    right: -1.5rem;
    top: 1.25rem;
    bottom: auto;
    width: auto;
    height: 2px;
  }

  .flow__body { padding-top: 0; }
}

/* AQUÍ VIVÍA "Compromisos" (.pact), y la sección entera se eliminó.

   Eran tres declaraciones numeradas —precio cerrado, pagos por hitos, el código
   es tuyo— y estaban bien resueltas: sin tarjetas, separadas por una regla, con
   el ordinal apagado para que no compitiera con la declaración.

   El problema no era la forma: decían las MISMAS tres promesas que los cuatro
   pasos de "Cómo se trabaja", y estaban justo debajo de ellos.

     · Precio cerrado      → paso 2, "alcance por escrito, precio cerrado y fecha"
     · Pagos por hitos     → paso 3, "la pruebas antes de liberar el siguiente pago"
     · El código es tuyo   → paso 4, "con el código, la base de datos y capacitación"

   178 palabras para tres ideas ya dichas dos renglones más arriba. Lo único que
   la sección aportaba y los pasos no —"sin renta mensual y sin licencia por
   usuario"— se rescató dentro del paso 4 antes de borrarla.

   Si alguna vez vuelve a hacer falta un bloque de garantías, el listón es el
   mismo que con los sellos del hero: que diga algo que la página no diga ya. */

/* ===========================================================================
   8. Preguntas
   ===========================================================================

   La lista se alinea con el borde izquierdo del resto de la página y no en una
   columna estrecha centrada. Centrada arrancaba 200 px a la derecha de todos
   los demás títulos y se leía como un descuadre, no como jerarquía.
   =========================================================================== */

.faq--home { max-width: 52rem; }

/* ===========================================================================
   9. Cierre
   ===========================================================================

   Devuelve al cotizador en vez de abrir otro formulario. Dos formularios en
   una página compiten entre ellos y parten la analítica en dos.
   =========================================================================== */

.closing {
  display: grid;
  gap: 1.1rem;
  justify-items: start;
  padding: clamp(1.75rem, 5vw, 3.5rem);
  border-radius: var(--radius-lg);
  background: var(--cta-bg);
  color: var(--cta-fg);
  box-shadow: var(--shadow-lg);
}

/* El titular del cierre también va a dos voces, pero aquí la segunda voz no
   puede usar --text-faint: sobre el azul saturado ese gris no se lee. Se
   apaga con opacidad sobre el mismo blanco, que mantiene el contraste por
   encima del mínimo. */
.closing h2 {
  display: grid;
  font-size: clamp(1.7rem, 1.25rem + 2.1vw, 2.75rem);
  line-height: 1.02;
  letter-spacing: -0.038em;
  max-width: 20ch;
}

.closing__lead { font-weight: 800; }
.closing__soft { font-weight: 400; opacity: 0.78; }

.closing p {
  font-size: clamp(0.98rem, 0.94rem + 0.3vw, 1.1rem);
  line-height: 1.55;
  max-width: 44ch;
  opacity: 0.92;
}

.closing__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem;
  margin-top: 0.25rem;
  width: 100%;
}

/* Invertido: sobre el acento, el botón principal es la placa clara. Repetir el
   acento sobre el acento lo haría desaparecer. */
.closing .btn--on-accent {
  background: var(--on-accent);
  color: var(--accent);
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}

/* El contorno va al 55 % y no al 45 %: el borde ES el botón cuando no hay
   relleno, y más abajo se queda por debajo del mínimo con el que un contorno
   todavía se distingue del fondo. */
.closing .btn--edge {
  background: transparent;
  color: var(--cta-fg);
  border-color: rgb(255 255 255 / 0.55);
}

@media (hover: hover) and (pointer: fine) {
  .closing .btn--on-accent:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
  .closing .btn--edge:hover { border-color: var(--cta-fg); background: rgb(255 255 255 / 0.14); }
}

@media (max-width: 420px) {
  .closing__actions .btn { width: 100%; }
}

/* ===========================================================================
   10. Movimiento reducido
   ===========================================================================

   site.css ya apaga transiciones y animaciones de forma global. Lo que falta
   aquí es que la portada siga siendo usable y siga comunicando lo mismo con
   las animaciones apagadas:

     · El cotizador tiene que seguir midiendo el alto de cada paso, así que la
       transición se apaga pero la mecánica se queda.
     · La opción destacada pierde el giro, pero conserva un filo degradado
       fijo: si se quedara igual que las otras cinco, la información de que ésa
       es la puerta más pedida desaparecería junto con la animación.
     · Las cintas se detienen y se vuelven desplazables con el dedo: paradas y
       recortadas serían listas a las que les falta la mitad de las entradas.
     · La captura se queda en su encuadre de arriba, que es el que importa.

   Movimiento reducido significa menos movimiento, no menos información.
   =========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .quoter__step[data-state="in"],
  .quoter__step[data-state="out"] { animation: none; }

  .quoter__body { transition: none; }

  .pick__tick { transition: none; clip-path: inset(0 0 0 0); }

  /* El aviso se queda, pero sin desplazamiento: es información, no adorno. */
  .quoter__alert:not([hidden]) { animation: none; }

  .pick--star {
    animation: none;
    background: linear-gradient(115deg, var(--primary), var(--spark));
  }

  .lede__dot { animation: none; }

  .shift__col--next { transform: none; }


  .ticker__track {
    animation: none;
    will-change: auto;
  }

  .ticker__viewport {
    overflow-x: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .proof {
    transition: none;
  }

  .proof:hover { transform: none; }

  /* `animation: none` y no sólo la transición: una animación atada al scroll no
     tiene duración propia —la marca el desplazamiento— así que el reset de
     `animation-duration` de site.css no la detiene. Sin esta línea, quien pide
     menos movimiento se quedaba con el único recorrido largo de la portada
     todavía corriendo. */
  .proof__frame img {
    transition: none;
    animation: none;
    will-change: auto;
  }

  .proof:hover .proof__frame img,
  .proof:focus-within .proof__frame img,
  .proof[data-revealed="true"] .proof__frame img { transform: none; }

  .proof__go { opacity: 1; }
}
