/* AVISO (2026-07-21): este archivo ha divergido a proposito del
   prototipo (projects/pico-de-gallo/prototype/css/pages.css). Contiene
   ademas reglas .contact-form__response* (formulario de Contacto con
   envio real via AJAX, solo tiene sentido en WordPress) que NO existen
   ahi. Si se vuelve a sincronizar este archivo copiando el del
   prototipo por encima (patron usado varias veces en sesiones previas
   para poner el tema al dia), hay que volver a añadir ese bloque a
   mano despues - buscar "AÑADIDO SOLO EN EL TEMA" mas abajo. */

/* ---------- Home: hero scroll (CR-02, peticion de cliente 2026-06-25) ----------
   Antes: imagen estatica de altura fija (950px, ver historico debajo).
   Peticion del cliente (ver client-feedback/header-scroll-behavior-spec.md):
   la imagen del hero reduce su tamano progresivamente con el scroll y se
   reposiciona hacia la esquina superior izquierda (corregido 2026-06-25,
   instruccion Z: el cliente pidio originalmente "inferior izquierda" pero
   se corrigio a "superior izquierda"), dejando ver el texto principal
   (antes .intro-statement, ahora tambien .hero-scroll__content).
   Referencia citada por el cliente: rubioydelamo.com (no se pudo inspeccionar
   su CSS/JS real en este entorno - sin navegador disponible - la
   implementacion se basa en la descripcion escrita del cliente).

   Tecnica ("scroll pin", sin librerias externas):
   - .hero-scroll: alto = 100vh + distancia extra de scroll que dura la
     transicion (ver nota 2026-06-28 mas abajo).
   - .hero-scroll__pin: position:sticky, queda fijo en el viewport durante
     esa distancia (overflow:hidden para que media/content no se salgan).
   - .hero-scroll__media / .hero-scroll__content: superpuestos (position:
     absolute, inset:0) dentro del pin. JS (main.js) calcula un progreso
     0-1 segun scrollY y lo expone como --hero-media-scale/--hero-content-opacity.
     El calculo (scrollDistance = heroScroll.offsetHeight - innerHeight) es
     independiente del valor exacto de la altura - se adapta solo.
   - scale() con transform-origin:left top logra "reducir tamano" y
     "reposicionar a la esquina superior izquierda" con una sola propiedad:
     el punto de origen queda fijo mientras la caja se encoge hacia el.
     Nota: el header (.site-header, position:fixed, z-index:100) sigue
     siempre por encima - la miniatura resultante queda visualmente debajo
     de la barra del header, no detras ni tapandola.

   2026-06-28 (peticion del usuario): antes 220vh -> en viewports tipicos
   eso son ~120vh (1000-1400px) de scroll extra antes de llegar a
   "Proyectos", demasiado. Cambiado primero a 100vh + 200px, despues
   reducido otra vez a 100vh + 100px (mismo dia, el usuario pidio aun
   menos) - la distancia extra de scroll que dura la transicion pasa a
   ser un valor fijo en vez de escalar con la altura de pantalla. No se
   toca main.js - el calculo de progreso ya es generico (offsetHeight -
   innerHeight), se adapta solo a la nueva altura. Solo prefers-reduced-
   motion desactiva este mecanismo por completo (ver @media al final);
   movil real (<=860px) SI lo usa desde el 2026-06-29 (ver nota mas abajo),
   solo con una interpretacion visual distinta para la imagen.

   Ajuste 2026-06-28 (mismo dia, otra vez): a 100px el encogido de imagen
   se notaba demasiado rapido/brusco (el usuario senalo que antes, con mas
   distancia de scroll, era mas gradual). Subido a 400px - sigue siendo
   muchisimo menos que los 1000-1400px originales (no se reintroduce el
   problema de "mucho espacio"), pero da mas recorrido de scroll para que
   la transicion de escala/opacidad se note mas suave en vez de abrupta.

   Ajuste 2026-06-28 (mismo dia, otra vez): el usuario pidio reducir un
   30% el espacio entre el titular y "Proyectos". 400px * 0.7 = 280px.

   Ajuste 2026-06-29 (vista movil): el mismo .hero-scroll (altura, pin,
   sticky) ahora se reutiliza tambien en movil real (<=860px) - antes solo
   aplicaba en escritorio/tablet. Ver @media (max-width:860px) mas abajo
   para la interpretacion visual movil (fade de opacidad en vez de
   escala/traslacion). */
/* height en dos pasos (vh primero, dvh despues): dvh (altura de viewport
   "dinamica", se ajusta cuando la barra de direccion del navegador
   movil aparece/desaparece) tiene mejor soporte que cuando se escribio
   esto por primera vez, pero algunos navegadores viejos no lo reconocen -
   si no soportan dvh ignoran esa linea y se quedan con el vh de arriba,
   nunca se quedan sin valor. Sin esto, en movil el 100vh inicial incluye
   el alto de la barra de direccion (que tapa parte real de la pantalla),
   asi que la imagen "a pantalla completa" en realidad no llenaba toda la
   pantalla VISIBLE en el primer pintado en algunos navegadores. */
.hero-scroll {
  position: relative;
  height: calc(100vh + 280px);
  height: calc(100dvh + 280px);
}

.hero-scroll__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

/* Corregido 2026-06-25: con transform-origin:left top puro, el borde
   superior de la miniatura se queda siempre en y=0 - exactamente donde
   esta el header (position:fixed, z-index:100, opaco tras el primer
   scroll) - asi que el header tapaba el trozo de arriba de la imagen
   minimizada. Se anade translateY(var(--header-height) * progreso) para
   que, en el estado final (progreso=1), la miniatura baje exactamente la
   altura del header y quede completa, debajo de el, sin recortarse.

   Peticion cliente 2026-07-20 (cuarta vuelta, misma sesion): no basta con
   que la miniatura quede pegada justo debajo del header (0px de hueco) -
   tiene que haber espacio respecto al header. Se suma ese hueco fijo
   (100px, bajado desde 150px en la quinta vuelta, mismo dia) al offset
   vertical final (var(--header-height) + 100px), seguimos multiplicando
   por --hero-progress para que la bajada siga siendo progresiva durante
   el scroll, no un salto. Este mismo offset final (header-height + 100px)
   es el que usa tambien .hero-scroll__content (mas abajo) como
   padding-top fijo, para que imagen y texto queden exactamente a la
   misma altura cuando la imagen esta pequena. */
.hero-scroll__media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  transform-origin: left top;
  transform: translateY(calc((var(--header-height) + 100px) * var(--hero-progress, 0))) scale(var(--hero-media-scale, 1));
  will-change: transform;
}

.hero-scroll__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Peticion cliente 2026-07-20 (dos vueltas, misma sesion): con la
   miniatura de la imagen encogida en la esquina superior izquierda
   (CR-02 mas arriba), el texto quedaba superpuesto encima de ella en vez
   de al lado. Primer intento: padding-left dinamico atado a
   --hero-media-scale (se movia en sincronia con el encogido de la
   imagen). El cliente lo corrigio: el texto NUNCA se debe mover/adaptar
   durante el scroll (complica la lectura) - tiene que quedar FIJO,
   exactamente en la posicion que ya tenia cuando la imagen esta pequena.
   Por eso ahora es un valor fijo, no un calc() con var(): 22% = el mismo
   HERO_MIN_SCALE (0.22) que main.js usa como escala final de la imagen
   (var HERO_MIN_SCALE en la funcion updateHeroProgress) - si ese valor
   cambia alguna vez, este 22% tiene que actualizarse a mano en paralelo,
   no hay una unica fuente compartida entre JS y CSS para esto.
   box-sizing:border-box (regla global) asegura que el padding no ensancha
   la caja (inset:0 sigue mandando en la posicion), solo desplaza el
   contenido flex hacia dentro. Se resetea a 0 en movil (@media
   max-width:860px, mas abajo) porque ahi la imagen no se encoge a una
   esquina - hace crossfade a pantalla completa - y en el fallback de
   prefers-reduced-motion (imagen y texto apilados en flujo normal, sin
   solape posible).
   Selector compuesto `.hero-scroll .hero-scroll__content` (no solo
   `.hero-scroll__content`) a proposito: el elemento tambien lleva la
   clase `.container`, y la regla sitewide `main .container` (mas abajo,
   full-bleed 100px) tiene mas especificidad (0,1,1) que una sola clase
   (0,1,0) y ganaba pese a estar declarada antes en el archivo, dejando
   el padding-left fijo en 100px en vez del valor pretendido. Con el
   selector compuesto (0,2,0) gana de forma normal, sin !important.

   Peticion cliente 2026-07-20 (tercera vuelta, misma sesion): la imagen
   encogida tiene que quedar a la misma altura que el texto, no mas
   arriba. align-items pasa de center (centrado en los 100dvh completos
   del pin, muy por debajo de donde acaba quedando la miniatura) a
   flex-start + padding-top - exactamente el mismo offset vertical final
   que usa .hero-scroll__media en su translateY (ver comentario ahi).
   Con eso el texto arranca a la misma altura en la que empieza la
   miniatura ya encogida, en vez de mas abajo.

   Peticion cliente 2026-07-20 (cuarta vuelta, misma sesion; bajado de
   150px a 100px en la quinta): el offset pasa de var(--header-height) a
   var(--header-height) + 100px, en paralelo con el mismo cambio en
   .hero-scroll__media - ver ese comentario para el porque. Los dos
   valores tienen que moverse juntos: si alguno cambia sin el otro,
   imagen y texto dejan de quedar a la misma altura. */
.hero-scroll .hero-scroll__content {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  padding-top: calc(var(--header-height) + 100px);
  padding-left: 22%;
  opacity: var(--hero-content-opacity, 0);
  transform: translateY(calc(16px * (1 - var(--hero-content-opacity, 0))));
  pointer-events: none;
}

.hero-scroll.is-content-visible .hero-scroll__content {
  pointer-events: auto;
}

/* Vista movil (2026-06-29, sin spec de Figma para esto - peticion directa
   del usuario): al cargar en vertical solo debe verse la imagen del hero
   (a pantalla completa, no 70vh), y al hacer scroll debe desaparecer POR
   COMPLETO de forma gradual (fade de opacidad), no encogerse/reposicionarse
   como en escritorio. Se reutiliza el MISMO mecanismo de pin+progreso que
   CR-02 (.hero-scroll/.hero-scroll__pin sin overrides aqui - heredan la
   regla de escritorio de mas arriba: altura con scroll extra, sticky,
   overflow:hidden) - main.js ya no excluye movil de este calculo (ver
   nota en main.js). Solo cambia la INTERPRETACION visual del progreso
   para la imagen: opacity en vez de scale+translate. El texto
   (.hero-scroll__content) hereda opacity/transform de la regla base de
   escritorio mas arriba (opacity:var(--hero-content-opacity,0), 0 al
   cargar) que es exactamente "solo se ve la imagen al principio, el texto
   aparece con el scroll" - el mismo comportamiento que ya tenia
   escritorio, ahora tambien en movil. El padding-left:22% fijo y el
   align-items:flex-start/padding-top:var(--header-height) anadidos el
   2026-07-20 SI se resetean aqui (mas abajo): en movil la imagen no se
   encoge a una esquina, hace crossfade a pantalla completa, asi que el
   texto debe seguir ocupando todo el ancho y centrado verticalmente
   como antes, no alineado a la posicion de una miniatura que aqui no
   existe. */
@media (max-width: 860px) {
  .hero-scroll__media {
    transform: none;
    opacity: calc(1 - var(--hero-progress, 0));
  }

  /* Bug encontrado 2026-07-21 (peticion cliente: el statement queda sin
     margen izquierdo en movil, desalineado del titulo "Proyectos" de
     debajo): esta regla reseteaba padding-left a 0, pisando (mayor
     especificidad, .hero-scroll .hero-scroll__content vs .container) el
     padding-inline:50px que .container ya aporta - el mismo padding que
     usa "Proyectos" (tambien .container, sin override) para su margen.
     El fix de 2026-07-13 de mas abajo en este archivo (que resetea los
     margenes PROPIOS de .intro-statement/.intro-statement h1 a 0)
     asumia que ese padding de .container seguiria vivo para dar el
     margen en movil - esta regla, mas antigua, se lo comia primero. Se
     iguala explicitamente a --content-padding (el mismo valor y la
     misma variable que usa .container) en vez de 0, para que ambos
     titulos queden alineados sin depender de cual de las dos reglas
     "gana" la cascada. */
  .hero-scroll .hero-scroll__content {
    align-items: center;
    padding-top: 0;
    padding-left: var(--content-padding);
  }

  /* 2026-06-29 (peticion del usuario: la imagen no se ve completa en los
     moviles principales). Foto real: 2160x1350 (ratio 1.6, paisaje). En
     un viewport movil tipico (~390x844, ratio ~0.46) object-fit:cover
     escala por ALTURA (cubre el alto exacto, sin recorte vertical) pero
     solo deja visible ~29% del ANCHO de la foto (390/ (844*1.6) = 0.29) -
     el resto se recorta a los lados. Con object-position por defecto
     (50% 50%, centro), esa ventana de 29% cae en el 35.5%-64.5% del ancho
     de la imagen - la persona que camina (sujeto principal, ver alt del
     <img>) esta aproximadamente en el 20%-40% del ancho real de la foto,
     asi que el centrado por defecto le recorta la cabeza y media figura,
     dejando solo un fragmento de su brazo/torso visible (de ahi "no se ve
     completa"). 30% horizontal centra la ventana de recorte en el
     20%-44.5% del ancho, lo que cubre a la persona completa en vez del
     centro geometrico de la foto (que cae entre dos de los tres carteles
     de fondo, sin sujeto claro). No se toca en escritorio/tablet (cajas
     mucho mas anchas, bastante mas cerca del ratio real de la foto, sin
     este problema de recorte severo). */
  .hero-scroll__media img {
    object-position: 30% center;
  }
}

/* Fallback de accesibilidad (prefers-reduced-motion, cualquier ancho):
   gana en cascada sobre las reglas de arriba (escritorio y movil, misma
   especificidad, pero esta va despues) - sin pin, sin scroll-jacking, sin
   fade, imagen y texto siempre visibles en flujo normal. main.js respeta
   el mismo guard y no ejecuta el calculo de progreso en absoluto cuando
   prefers-reduced-motion esta activo (ver comentario ahi), asi que esta
   media query es la unica responsable del estado final en ese caso. */
@media (prefers-reduced-motion: reduce) {
  .hero-scroll { height: auto; }
  .hero-scroll__pin { position: relative; height: auto; }
  .hero-scroll__media {
    position: relative;
    inset: auto;
    height: 70vh;
    transform: none;
    opacity: 1;
  }
  .hero-scroll .hero-scroll__content {
    position: relative;
    inset: auto;
    height: auto;
    opacity: 1;
    transform: none;
    padding-top: 0;
    padding-left: 0;
    pointer-events: auto;
  }
}

/* Historico (pre-CR-02): Figma (node 1:744) definia el Hero como un frame
   fijo de 1280x800, sin spec responsive. Ya no aplica tal cual porque el
   comportamiento de scroll es una peticion de cliente posterior al diseno
   estatico - el tamano inicial pasa a ser 100vh (pantalla completa) y se
   reduce con el scroll en vez de mantenerse fijo en 950px. Conservado como
   nota porque documenta una decision previa (950px en vez de los 800px
   literales de Figma) que sigue siendo relevante si se revierte CR-02. */

/* En Figma el bloque del statement (1:743, bottom y=1256) termina exactamente
   donde empieza el titulo "Proyectos" (1:792, top y=1256) - 0px de margen
   propio entre ambos. Se reduce solo el padding-bottom (el padding-top se
   mantiene, separa el statement de la foto del hero, sin relacion con esto). */
/* margin-left/right:50px pedido por el cliente 2026-07-10 - adicional al
   padding-inline:100px que ya trae .container (regla sitewide), no lo
   sustituye. Total efectivo: 150px a cada lado solo en este bloque. */
.intro-statement {
  padding-top: var(--space-2xl);
  padding-bottom: var(--space-sm);
  margin-left: 50px;
  margin-right: 50px;
}

/* Arreglo minimo 2026-07-24 (portado desde prototype/css/pages.css, bug
   reportado por el cliente: al reducir la ventana en escritorio y en
   iPad se recortan las palabras del titular) - causa raiz: 98px era un
   valor literal fijo (peticion cliente 2026-07-10) sin ningun breakpoint
   intermedio entre este y el fallback movil de mas abajo (<=860px, 40px)
   - entre 861px y ~1280px el texto envuelve a mas lineas de las que caben
   en el alto del pin (overflow:hidden, ver .hero-scroll__pin), cortando
   el final del titular. Se sustituye el valor fijo por clamp(), con techo
   en 98px (no se toca el tamano de escritorio ya aprobado). Se probo
   primero una version solo-vw (misma pendiente que --text-display-xl) -
   insuficiente: a 1366x768 y 1280x720 (portatiles muy comunes, no solo
   iPad) seguia recortando porque el alto disponible (vh) no entraba en
   la cuenta. Se anade min(vw,vh) para que el texto tambien encoja en
   viewports anchos pero bajos - eso sí exige ceder algo en el otro
   extremo: a 1920x1080 (monitor comun, sin problema de recorte) el
   titular pasa de 98px a 88.8px. Se acepta el cambio (~9%, sigue siendo
   un titular enorme) porque el requisito duro es que no se corte texto.
   Detalle completo (viewports probados) en
   prototype/css/pages.css y client-feedback/implementation-tracker.md,
   CR-13. */
.intro-statement h1 {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(2.5rem, min(5.625vw, 6vh) + 1.5rem, 98px);
  line-height: 1.25;
  /* Peticion cliente 2026-07-10: margen izquierdo/derecho de "unos 30px"
     calculado de forma dinamica (no un valor fijo, escala con el ancho
     real de pantalla via vw) - historial de ajustes solo sobre el
     margen DERECHO: 2vw -> 2.2vw (+10%, tambien aplicado al izquierdo) ->
     2.42vw (+10%) -> 2.662vw (+10%) -> 3.1944vw (+20%) -> 6.3888vw
     (x2, este ultimo). El izquierdo se queda en 2.2vw, ya no son
     simetricos. 6.3888vw da ~95.8px a 1500px de viewport (referencia
     habitual de escritorio), ~81.8px a 1280px, ~122.7px a 1920px.
     Adicional al margin-left/right:50px de .intro-statement (contenedor)
     y al padding-inline:100px de .container (sitewide) - no los
     sustituye. */
  margin-left: 2.2vw;
  margin-right: 6.3888vw;
}

.section { padding-block: var(--space-2xl); }

/* font-weight:normal es necesario porque <h2> tiene bold por defecto del
   navegador (base.css solo resetea margin, no font-weight). El tamano
   base (78px) viene de Figma Dev Mode (nodo 1:734, "Servicios" en Home,
   texto 1:735: Lora Regular 78px) - ya no se usa tal cual en index.html:
   peticion del cliente 2026-07-11, "Servicios" debe tener el mismo
   estilo/tamano que "Proyectos", asi que ahora lleva tambien la clase
   .section__heading--lg (48px, ver mas abajo), igual que el h2 de
   Proyectos - una decision explicita del cliente que se aparta a
   proposito del tamano literal de Figma para esta seccion en concreto.
   La regla base (78px) queda sin uso real por ahora, pero se conserva
   por si se necesita en el futuro sin el modificador --lg. Solo se usa
   en index.html, seguro hardcodear el tamano sin tocar el token. */
.section__heading {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: 78px;
  margin-bottom: var(--space-lg);
}

/* En el Figma de Home, el titulo "Proyectos" (node 1:792) usa 48px exacto
   (cambio de marca 2026-06-26 - antes 72px). Desde 2026-07-11 tambien lo
   usa "Servicios" (peticion del cliente, ver nota de arriba) - antes
   este modificador era exclusivo de Proyectos, distinto del tamano base
   de Servicios (78px); ahora ambos h2 de Home comparten
   .section__heading--lg. */
.section__heading--lg {
  font-size: 48px;
}

/* ---------- Nosotros (columna fija añadida 2026-06-26) ----------
   Figma (frame Nosotros 1:230, re-auditado 2026-06-26): titulo "Nosotros"
   en una columna estrecha a la izquierda y el statement grande a la
   derecha, EN LA MISMA FILA - no apilados como hace hoy .page-hero
   (titulo arriba, statement debajo, mismo ancho). Mismo patron de
   "titulo + frase grande en 2 columnas" ya identificado tambien en
   Contacto (ST-01 en figma-vs-prototype-audit.md, pendiente ahi).
   260px = distancia real en Figma entre el inicio del titulo (x=72,
   nodo 1:305, left:calc(50%-568px) de un frame de 1280) y el inicio del
   statement (x=332, nodo 1:307, left:calc(50%-308px)).
   2026-07-13 (re-verificado via Figma MCP get_design_context): el
   cliente movio ambos elementos en una revision posterior del archivo
   (title x=36->72, statement x=271->332, ver auditoria de 2026-06-26 mas
   arriba) - la distancia entre ellos tambien cambio, de 235 a 260. Se
   actualiza el valor para reflejar la version actual de Figma. */
.about-page {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  padding-top: calc(var(--header-height) + var(--space-2xl));
  padding-bottom: var(--space-xl);
}

/* Figma (node 1:305): "Nosotros", Lora Regular, 48px, blanco - mismo
   escalon que Servicios/Proyectos (.page-hero__title sin modificador usa
   96px, --text-display-xl, equivocado para esta pagina - hallazgo ya
   documentado en typography-map.md). */
.page-hero__title--nosotros {
  align-self: start;
  position: sticky;
  top: calc(var(--header-height) + var(--space-2xl));
  z-index: 2;
  background: var(--color-bg);
  font-size: 48px;
  font-weight: normal;
  line-height: normal;
}

/* Linea decorativa (node 1:277/1:278, "Proyecto_2"): re-verificado
   2026-07-13 via Figma MCP get_design_context - NO es un ::after pegado
   debajo de "Nosotros" (asi estaba antes, error heredado de una
   auditoria previa que solo confirmo el ANCHO, no la posicion). La linea
   real vive en la columna de CONTENIDO (x=332, la misma columna que el
   statement, no x=72 donde esta el titulo) y a media altura del titulo
   (y=331, con el titulo empezando en y=292 - 39px por debajo del inicio
   del titulo, no debajo de todo su bloque de texto). Por eso ahora es un
   elemento propio (.about-page__divider, primer hijo de
   .about-page__content) en vez de un pseudo-elemento del <h1>: necesita
   vivir en la otra columna del grid, algo que un ::after del titulo no
   puede hacer (queda siempre dentro de la caja de su propio elemento).
   884px de ancho: confirmado sin cambios en esta revision (x=332 a
   x=1216). max-width:100% por seguridad en viewports que no lleguen a
   884px (aunque el fallback movil de mas abajo ya neutraliza todo esto).

   position:sticky + mismo `top` que el titulo (+39px, su propio
   margin-top): sin esto, la linea queda escondida SIEMPRE detras de la
   franja opaca de .about-page::before (z-index:1) - esa franja cubre una
   banda fija bajo el header pensada para ocultar el CONTENIDO que sube
   por scroll (statement/parrafo intro), pero la linea, al vivir ahora en
   la columna de contenido (no ya dentro de la caja del <h1>), cae DENTRO
   de esa banda incluso en scroll=0, antes de que el usuario haga scroll.
   Se trata igual que el titulo (mismo `top`, mismo z-index): queda fija
   junto a "Nosotros" durante todo el scroll de la pagina, tal y como se
   ve siempre unida a el en el diseño estatico de Figma. */
.about-page__divider {
  position: sticky;
  top: calc(var(--header-height) + var(--space-2xl) + 39px);
  z-index: 2;
  margin-top: 39px;
  width: 884px;
  max-width: 100%;
  height: 1px;
  background: var(--color-text-muted);
}

/* Franja opaca (mismo mecanismo que en Servicios/Contacto): el contenido
   de la columna derecha desaparece al llegar a la altura del titulo
   "Nosotros", no al llegar al header. Alto = space-2xl (hasta donde
   empieza el titulo) + ~57px (una linea de texto a 48px/line-height
   normal) + 10px de aire, igual que antes - ya no suma el margen/alto de
   la linea decorativa (2026-07-13: la linea se movio fuera de la columna
   del titulo, ver nota en .about-page__divider, asi que ya no forma
   parte del bloque que este fondo opaco necesita cubrir). */
.about-page::before {
  content: "";
  position: fixed;
  top: var(--header-height);
  left: 0;
  right: 0;
  height: calc(var(--space-2xl) + 57px + 10px);
  background: var(--color-bg);
  z-index: 1;
  pointer-events: none;
}

/* El statement empieza justo debajo de .about-page__divider (su hermano
   anterior en el flujo normal, ver nota de mas arriba) - 45px replica el
   espacio real medido en Figma entre la linea (y=331) y el inicio del
   statement (y=377): 377 - 331 - 1(alto de la propia linea) = 45. */
.about-page__content .page-hero__statement {
  margin-top: 45px;
}

/* Re-verificado 2026-06-28 via Figma MCP (get_design_context, nodo
   1:307, archivo WEB_PDG_V2 Dev Mode): el Figma actual especifica este
   texto a 32px Lora, line-height "normal" (estilo nombrado "Subtitulo
   Blanco" en el propio archivo) - NO 48px/92.055% como se documento el
   2026-06-26. El cliente parece haber reducido el tamaño en una revision
   posterior del archivo (no es un error de la auditoria anterior, es un
   cambio de Figma despues de esa fecha). Corregido a 32px/normal. */
.page-hero__statement--nosotros {
  font-size: 32px;
  line-height: normal;
}

/* Parrafo nuevo bajo el statement (Figma, nodo 96:435): "Descubrimos la
   idea que esta en el corazon de cada negocio...". No existia antes -
   confirmado que es contenido nuevo del cliente, no un olvido previo.
   16px, Early Sans Variable, color #989898 (--color-text-soft),
   line-height normal. Gap real aproximado statement->parrafo (no se
   puede medir el alto real del statement sin navegador, estimado a
   partir del numero de lineas visibles en la captura de Figma): 28px. */
.about-page__intro {
  margin-top: 28px;
  color: var(--color-text-soft);
  font-size: 16px;
  line-height: normal;
}

/* Re-auditado 2026-07-20 (verificacion de pixel fidelity con Playwright):
   el nodo 96:435 en Figma tiene las 2 frases como parrafos consecutivos
   con mb-0 (sin margen) entre ellas - fluyen como un unico bloque de
   texto, sin hueco de parrafo. margin-top:1em (sin justificar contra
   Figma, valor por defecto asumido en la auditoria original) quedaba
   claramente visible comparando la captura de Figma con el prototipo
   renderizado: no debe haber hueco aqui. */
.about-page__intro p { margin: 0; }
.about-page__intro p + p { margin-top: 0; }

.manifesto {
  padding-block: var(--space-2xl);
}

/* La imagen del manifiesto ocupa casi todo el ancho del FRAME completo
   (1203.73 de 1280 - margen real ~36-40px a cada lado, node 1:308-1:312),
   NO solo el ancho de la columna de contenido de .about-page (mas
   estrecha, al reservar 260px para "Nosotros") - por eso este wrapper
   (solo la imagen, no el resto de .manifesto) "escapa" de su columna con
   width/margin-left negativos. Mismo mecanismo que .contact-visual en
   Contacto - aqui aislado en su propio wrapper porque .manifesto ahora
   tambien contiene texto (.manifesto__statement/__text) que SI debe
   quedarse dentro del ancho normal de la columna, no a ancho completo.

   2026-07-13 (re-verificado via Figma MCP): el margen real de la imagen
   en Figma (36-40px de un frame de 1280, ~2.9%) es justo la MITAD del
   margen del titulo/texto (72px, x=72) - no coincide con el margen actual
   del sitio (100px, ver "Margenes horizontales ampliados a 100px" en este
   mismo archivo). Se replica la misma proporcion (mitad del margen
   estandar) en vez del valor en pixeles literal de Figma (36px), que ya
   no corresponde al margen real del sitio: bleed objetivo = 50px (mitad
   de 100px) a cada lado, en vez de los 100px de main .container.
   Formula: el wrapper vive dentro de .about-page__content, que empieza en
   (100px margen + 260px columna titulo) = 360px desde el borde de
   viewport y termina en (100% - 100px margen). Para que la imagen llegue
   a 50px de cada borde: margin-left = 50 - 360 = -310px; width = 100% +
   (360-50) + (100-50) = 100% + 360px. */
.manifesto__media-wrap {
  width: calc(100% + 360px);
  margin-left: -310px;
}

/* Imagen real (1203.73 x 651 en Figma, node 1:308-1:312). El "Clip path
   group" es, tras inspeccionar el SVG de mascara real, un rectangulo
   simple del tamano del frame contenedor (no una silueta decorativa) -
   basta aspect-ratio + object-fit: cover, sin mask-image. */
.manifesto__media {
  aspect-ratio: 1203.73 / 651;
  border-radius: 4px;
  overflow: hidden;
}

.manifesto__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Texto bajo la imagen (Figma, nodo 96:436): "Somos el ingrediente que
   le da caracter a cada idea." 32px Lora blanco, line-height "normal".
   2026-07-13 (re-verificado via Figma MCP get_design_context, con
   Playwright ya disponible para medir en navegador real): el cliente
   volvio a mover este texto en una revision posterior de Figma (y=1358 ->
   y=1400, imagen sin cambios en 682+651=1333) - el gap real ahora es 67px
   (1400-1333), no 25px. Confirmado visualmente comparando capturas de
   Figma vs. el prototipo renderizado. */
.manifesto__statement {
  margin: 0;
  margin-top: 67px;
  font-family: var(--font-display);
  font-weight: normal;
  font-size: 32px;
  line-height: normal;
  color: var(--color-text);
}

/* Peticion cliente 2026-07-20 (revision de Figma, nodo 1:230 re-auditado):
   el cliente volvio a mover el cuerpo del manifiesto - x=290 (misma
   columna que el resto del contenido, auditado 2026-06-26/2026-07-13) ->
   x=616 (~48%, mitad derecha otra vez), y añadio dos elementos nuevos que
   no existian en auditorias previas:
   - Titulo "Manifiesto" (nodo 487:1051, Lora 32px blanco, mismo estilo
     que .manifesto__statement) justo encima del cuerpo, con 0px de hueco
     entre ambos (heading bottom y=1337 = texto top y=1337 exacto).
   - Un icono de ojo decorativo (nodo 487:1045, "ojo"+"pupila", ver
     .manifesto__eye mas abajo) a la izquierda del titulo/cuerpo, en el
     hueco que el texto dejo libre al desplazarse a la derecha.
   Se introduce el wrapper .manifesto__body (heading + ojo + texto) para
   poder medir y posicionar los 3 juntos desde un unico origen: su propio
   margin-left (283px) replica la distancia real Figma entre el borde de
   la columna de contenido (x=332, ver .manifesto__media-wrap mas arriba)
   y el nuevo inicio del titulo/texto (x=615, promedio de 614/616) ->
   615-332=283.
   2026-07-26: el ojo (ver .manifesto__eye mas abajo) ya no se posiciona
   relativo a este wrapper - ahora vive dentro de .manifesto__pin como
   columna del grid (peticion cliente "todo dentro del carrusel"), asi
   que position:relative aqui ya no hace falta.
   2026-07-26 (6a, movil y escritorio/tablet): peticion cliente explicita
   de dejar 100px de interlineado despues de .manifesto__statement (antes
   64px) - en movil (flujo normal, sin sticky) este margin-top es
   directamente ese hueco. En escritorio/tablet .manifesto__body ya no
   es lo que crea el hueco visible (.manifesto__pin es sticky, apilado
   contra el statement via un valor en px calculado por JS, no por este
   margin - ver manifestoPinTopValue en main.js, tambien actualizado a
   +100 en vez de +0) - se actualiza igual aqui por coherencia y porque
   determina el hueco inicial antes de que el sticky se active. */
.manifesto__body {
  margin-top: 100px;
  margin-left: 283px;
}

/* Icono decorativo (nodo 487:1045: "ojo" outline + "pupila" solida,
   exportado como un unico SVG plano en
   assets/icons/nosotros-ojo-manifiesto.svg - se quito del export un
   rectangulo de fondo #1E1E1E y otro blanco que Figma incluyo por error
   de contexto de pagina, dejando solo los 2 trazos reales). 256x100,
   tamano literal de Figma (el resto de esta pagina tampoco usa unidades
   fluidas).
   2026-07-26 (4a): el ojo vive ahora DENTRO de .manifesto__pin (ver HTML
   - primer hijo, antes de .manifesto__pin-content), no como hermano
   suelto de .manifesto__showcase - peticion cliente "todo tiene que
   estar incluido dentro del carrusel que quiero crear para el
   Manifiesto". Su posicion final (columna izquierda del grid, ver regla
   >=861px mas abajo) sustituye el antiguo position:absolute/sticky +
   left:-366px por grid-column + margin-top - sin position propia aqui,
   la hereda del flujo normal (bloque apilado en movil, grid en
   escritorio/tablet). */
/* Peticion cliente 2026-07-30 (portado desde el prototipo): se sustituyo
   el SVG (nueva ilustracion, Figma nodo 601:114) - el anterior era un
   icono ancho y bajo (256x100, ratio 2.56:1); el nuevo es casi cuadrado
   (251.13x270.61 en Figma, ratio ~0.93:1, mas alto que ancho). El propio
   SVG lleva preserveAspectRatio="none" (para poder estirarlo a cualquier
   caja sin que el navegador le imponga su propio letterboxing) - eso
   significa que SI no se actualiza esta caja para que respete el ratio
   real del nuevo archivo, el navegador lo deforma (aplasta) para llenar
   los viejos 256x100. Ancho sin cambios (256px, sigue cabiendo de sobra
   en la columna de 366px reservada para el ojo, ver mas abajo) - alto
   recalculado a partir del ratio real del SVG (256 * 270.61/251.13 =
   275.86, redondeado). */
/* Peticion cliente 2026-07-30 (tarea 7, portado desde el prototipo): el
   fundido continuo (opacity, ver historial arriba) se sentia como una
   imagen que simplemente se desvanece en el sitio - el cliente pidio que
   en vez de eso la imagen "desaparezca hacia arriba por debajo de
   'Manifiesto'", es decir, el mismo lenguaje visual que ya usa
   .manifesto__text al pasar por detras del pin (contenido real que se
   desplaza y queda tapado), pero sin mover el ojo de sitio (sigue
   viviendo DENTRO de .manifesto__pin, peticion cliente previa "todo
   dentro del carrusel" - ver mas arriba). Se logra con un wrapper opaco
   al overflow (.manifesto__eye-frame, el marco/mascara) mas un
   translateY continuo sobre la propia imagen - el marco (overflow:
   hidden) recorta lo que ya "salio" en vez de dejarlo escapar por encima
   del resto del diseño.
   Tarea 8 (mismo dia, aclaracion sobre esta misma tarea): --manifesto-
   eye-progress (0-1, normalizado sobre una ventana de scroll fija) se
   sustituye por --manifesto-eye-offset (main.js), un desplazamiento en
   PX ya calculado 1:1 con el recorrido real de la ultima linea de
   .manifesto__text - "alineada con la ultima frase" (peticion cliente).
   translateY consume ese valor directamente (sin invertir/interpolar
   aqui, main.js ya entrega el valor final en px) - 0px = imagen quieta
   en su sitio, manifestoEyeTravelPx (alto del marco) = imagen entera
   fuera de el. La ultima porcion visible antes de desaparecer del todo
   sigue siendo el borde INFERIOR de la imagen (la "ultima parte", ver
   peticion cliente). */
.manifesto__eye-frame {
  width: 256px;
  height: 276px;
  overflow: hidden;
}

.manifesto__eye {
  display: block;
  width: 100%;
  height: 100%;
  transform: translateY(calc(-1 * var(--manifesto-eye-offset, 0px)));
}

/* Mismo estilo que .manifesto__statement (Lora 32px blanco, line-height
   normal) - Figma usa el mismo estilo con nombre "Subtitulo Blanco" para
   ambos textos. */
.manifesto__heading {
  margin: 0;
  font-family: var(--font-display);
  font-weight: normal;
  font-size: 32px;
  line-height: normal;
  color: var(--color-text);
}

/* Figma (node 1:306): el cuerpo NO es texto corrido en parrafos largos -
   son lineas cortas con saltos de linea manuales, agrupadas en 3
   bloques reales separados por una linea en blanco (cadencia poetica
   deliberada). 16px exacto, Early Sans Variable, color #989898
   (--color-text-soft), line-height normal.
   Re-auditado 2026-07-20 (verificacion de pixel fidelity con Playwright,
   captura del prototipo vs captura de Figma lado a lado): la auditoria
   anterior decia "8 grupos" y HTML tenia una etiqueta <p> por cada
   sub-frase (con <br> solo dentro de algunas) - comparando contra el
   nodo 1:306 real solo hay 2 saltos de linea en blanco (<br/><br/> en el
   export), es decir 3 grupos, no 8. Las <p> de mas creaban huecos de
   1.5em donde Figma no tiene ninguno, y a la vez faltaban <br> DENTRO de
   los grupos (ej. "ya conoces," y "pero de pronto sabe distinto." iban
   corridos en una sola frase en vez de en 2 lineas; el parrafo "En Pico
   de Gallo creemos..." no tenia ningun <br>, corria como un parrafo
   largo normal en vez de 6 lineas cortas). Corregido en nosotros.html:
   ahora son exactamente 3 <p>, con <br> entre cada linea dentro de cada
   uno, calcando el nodo 1:306 linea por linea.
   margin-top pasa de 28px (medido desde .manifesto__statement, cuando el
   texto no tenia titulo propio encima) a 0 (2026-07-20: ahora el hueco
   real es entre .manifesto__statement y .manifesto__heading - ver
   margin-top:64px en .manifesto__body - el titulo y el cuerpo ya no
   tienen hueco propio entre ellos en Figma, bottom del titulo y top del
   texto coinciden en y=1337). */
/* Peticion cliente 2026-07-21: aunque en Figma el bottom del titulo
   "Manifiesto" coincide exactamente con el top del cuerpo (0px, ver nota
   de mas arriba en .manifesto__body), el cliente pidio ~10px de aire
   igualmente - queda demasiado justo en la practica. Excepcion deliberada
   sobre el nodo 1:306, igual que otras ya documentadas en este archivo
   (no es un error de fidelidad, es una peticion explicita posterior). */
.manifesto__text {
  margin-top: 10px;
  color: var(--color-text-soft);
  font-size: 16px;
  line-height: normal;
}

.manifesto__text p { margin: 0; }
.manifesto__text p + p { margin-top: 1.5em; }

/* Peticion cliente 2026-07-30 (portado desde el prototipo): sustituye el
   carrusel de scroll-jacking de arriba (pin+persiana+barra de progreso+
   cortina+spacer de despegue) por un efecto de parallax vertical, mismo
   patron ya aplicado el mismo dia al carrusel de Servicios de Home (ver
   "Reveal-on-scroll de Servicios en Home" en components.css/main.js).
   Los 3 parrafos pasan a flujo normal (apilados uno debajo de otro,
   respetando el orden ya existente en el HTML - .manifesto__text ya no
   vive dentro de .manifesto__pin, ahora es su hermano) y se revelan con
   un fade+deslizamiento vertical segun entran en el viewport (clase
   .is-revealed que anade main.js via IntersectionObserver), en vez de
   superponerse en el mismo hueco con un efecto de persiana (translateY
   discreto is-active/is-past sobre parrafos position:absolute). Sin la
   barra de progreso vertical (.manifesto__progress/-bar, eliminada del
   HTML) - mismo motivo que en Servicios, el cliente no la queria.
   "Manifiesto" (titulo+ojo, .manifesto__pin) SIGUE fijo (sticky) mientras
   los parrafos pasan por detras y se ocultan - eso ya pasaba antes, pero
   como un crossfade contenido DENTRO de la misma caja fija; ahora es un
   scroll-past real (el texto, hermano del pin, se desplaza por detras
   suyo) que necesita algo opaco detras del pin para taparlo - ver
   .manifesto__curtain mas abajo (bug del rectangulo negro corregido el
   mismo dia: NO es background en el propio pin, mismo error que se probo
   primero en #home-servicios .service-showcase__title de components.css
   pero que ahi si funciona, porque ese titulo es tan ancho como el texto
   que tapa - aqui "Manifiesto" es mucho mas corto que .manifesto__text).
   "Somos el ingrediente..." (.manifesto__statement) no cambia, sigue fijo
   igual que antes.
   Al ya no necesitar una altura artificial (antes calc(3*100vh), un
   viewport completo por parrafo, para darle "pista" de scroll al pin) ni
   forzar su despegue con JS (antes manifestoPinTopValue+is-releasing),
   .manifesto__release-spacer (hueco extra para que el despegue nativo del
   sticky terminara antes del footer) ya no hace falta y se quita del HTML
   (page-nosotros.php) y de main.js - el alto real de .manifesto__showcase
   lo dan ahora sus propios hijos en flujo normal (pin + texto), mismo
   patron ya usado sin spacer en Servicios/Proyectos.

   .manifesto__curtain SI se mantiene (bug reportado por el cliente
   2026-07-30, "el texto ya no se debe ver cuando se oculta detras de
   Manifiesto" - ver la regla mas abajo): a diferencia de Servicios/
   Proyectos (donde el titulo sticky se pega justo en top:var(--header-
   height), sin hueco por encima), "Manifiesto" se pega mas abajo
   (--manifesto-pin-top, deliberadamente por debajo de "Somos el
   ingrediente..." y de un hueco visual de 100px) - todo el texto que
   sigue subiendo DESPUES de ocultarse detras del pin pasa fisicamente por
   esa franja superior (header/franja opaca de "Nosotros"/statement/hueco
   de 100px) antes de desaparecer del todo por arriba del viewport, y ahi
   no habia ningun fondo opaco continuo que lo tapase (a diferencia del
   viejo diseño, donde el texto nunca salia de la ventana interna del
   propio pin). La cortina (fixed, opaca, cubre 0 a donde termina el pin)
   soluciona exactamente eso. Unica diferencia con el viejo curtain: antes
   solo se hacia visible un instante (cerca del despegue del pin, con
   is-releasing); ahora se hace visible durante TODO el tiempo que
   .manifesto__showcase esta en pantalla (IntersectionObserver sobre el
   propio showcase, ver main.js) - el texto puede estar pasando por detras
   del pin en cualquier punto de ese rango, no solo al final.

   Bug encontrado 2026-07-30 (reportado por el cliente: "aparece un
   rectangulo negro arriba a la derecha" justo al empezar a verse
   "Manifiesto") sobre el primer intento de esta misma tarea: ese primer
   intento le daba background:var(--color-bg) al propio .manifesto__pin
   en vez de a la cortina, para tapar el texto que pasa exactamente por SU
   franja (pinTop a pinTop+altoPin). Pero .manifesto__pin no tiene ancho
   propio (ocupa toda la columna de contenido, ~600-900px) mientras que
   "Manifiesto" solo ocupa sus primeros ~200px - ese fondo se veia como un
   rectangulo negro solido a la derecha del titulo, tapando lo que hubiera
   detras (la foto del estudio) incluso sin texto que ocultar todavia.
   Corregido: el pin ya no lleva background propio (ver esa regla mas
   abajo); la cortina crece para cubrir tambien esa franja (0 a
   pinTop+altoPin, no solo 0 a pinTop) con el ancho correcto (el de
   .manifesto__text, no el del pin, ver .manifesto__curtain mas abajo).

   Peticion cliente 2026-07-30 (tarea 4, mismo dia): el titulo de
   Manifiesto y la imagen tienen que desaparecer A LA VEZ que desaparece
   el texto del manifiesto (los 3 parrafos), sin quedar nunca por encima
   del footer. Dos intentos previos de este mismo dia (ver git history si
   hace falta el detalle): 1) esconder el pin al alcanzar el borde de
   .manifesto__statement (reservaba hueco con .manifesto__eye-spacer); 2)
   esconderlo en cuanto el sticky nativo se despega (reservaba hueco con
   .manifesto__release-spacer, formula basada en la posicion del PIN). El
   2º fallaba en viewports altos (~980px+ con las medidas actuales, un
   monitor externo o ventana maximizada normal): con esa altura de
   ventana, .manifesto__showcase (alto fijo, en flujo normal) ya no tiene
   suficiente scroll total para que el sticky nativo del pin llegue a
   despegarse NUNCA antes del final real de la pagina - el pin se queda
   pegado (visible) indefinidamente y el ojo, mucho mas alto que el pin,
   queda montado sobre el footer sin remedio.
   Version actual: el disparador de .is-releasing (ver mas abajo) ya no
   depende de si el pin se ha despegado (una condicion que puede no
   llegar a cumplirse) sino de si .manifesto__text ha terminado de pasar
   por detras del pin (compara la posicion real de ambos en cada scroll,
   ver updateManifestoCurtain en main.js) - cierto tanto si el pin sigue
   tecnicamente pegado como si no. .manifesto__release-spacer sigue
   haciendo falta (formula recalculada, ya no basada en el pin sino en
   garantizar que ese cruce de posiciones LLEGUE a ocurrir antes del final
   de la pagina, ver main.js) - sin el, en viewports altos el ultimo
   parrafo ni siquiera llega a completar su propio recorrido de scroll
   antes de que la pagina se quede sin mas alto. */

/* .manifesto__release-spacer: display:none base - DEBE ir antes del
   @media (min-width:861px) que le pone display:block mas abajo (mismo
   error de orden ya corregido antes para .manifesto__eye-spacer/este
   mismo elemento, ver git history - misma especificidad, un display:none
   posterior en el archivo le ganaria siempre al display:block del media
   query). Solo hace falta en escritorio/tablet, donde el pin es sticky
   (ver mas abajo) - en movil no hay nada que despegar/ocultar. */
.manifesto__release-spacer {
  display: none;
}

@media (min-width: 861px) {
  /* Bug reportado por el cliente 2026-07-30 ("mucho hueco entre
     'Manifiesto' y donde empieza el texto"): hasta ahora el ojo era la
     columna 1 de un grid en .manifesto__pin (ver git history) - esa
     columna (105px de margen + 100px de alto = 205px) era mas alta que
     la columna del titulo (~38px, una sola linea), y el alto de fila de
     un grid SIEMPRE lo marca la columna mas alta, sin importar
     align-items - asi que .manifesto__pin media esos ~205px aunque el
     titulo solo ocupara los primeros ~38px, y .manifesto__text (su
     hermano en el HTML) empezaba justo despues de esa caja entera,
     dejando ~165px de hueco de mas antes de su propio margin-top:10px
     (regla base, sin cambios). Solucion: el ojo pasa a position:absolute
     (ya no es un item de grid, no aporta altura a .manifesto__pin) -
     left:-366px reproduce el mismo offset horizontal que antes daba el
     grid (columna de 366px + margin-left:-366px en el pin), calculado
     ahora desde el propio .manifesto__pin (position:sticky, que ya sirve
     de containing block para descendientes absolute sin necesitar
     position:relative aparte). Sin el grid, el pin ya no necesita
     margin-left:-366px (ver mas abajo) - vuelve a su posicion natural,
     la misma que .manifesto__text y .manifesto__statement.
     opacity (2026-07-30, tarea 6): antes este fundido continuo vivia en
     .manifesto__pin entero (titulo+ojo se desvanecian juntos) - el
     cliente aclaro que solo la imagen tiene que desaparecer "por debajo
     de 'Manifiesto'", que se queda fijo y visible (igual que "Servicios"/
     "Proyectos", sin fundido propio). --manifesto-eye-offset (main.js,
     updateManifestoCurtain, renombrada en tarea 8 - ver historial en
     main.js) es un desplazamiento en px atado 1:1 al recorrido real de
     la ultima linea de .manifesto__text contra el borde inferior del
     pin, consumido como translateY en .manifesto__eye (regla base, ver
     mas arriba). Sin transition: un valor que ya se recalcula en cada
     frame de scroll no la necesita (añadiria retraso, alejando el
     efecto del progreso real). Fallback 1 (visible, translateY(0)):
     mientras main.js no haya corrido todavia (primer pintado).
     left/top se mueven aqui a .manifesto__eye-frame (el marco que
     recorta la imagen al "desaparecer hacia arriba", ver regla base) -
     la imagen en si ya no lleva su propia posicion, solo rellena el
     marco (width/height:100%) y se desplaza dentro de el.
     Peticion cliente 2026-07-30 (siguiente turno, tarea 13): medido en
     vivo (Playwright) que aunque el ritmo ya era 1:1 (ver main.js), el
     punto de pantalla en el que cada uno desaparece no coincidia - el
     texto se tapa detras de la cortina a la altura del borde inferior del
     pin (pinBottom), pero este marco (top:105px, heredado de un ajuste
     anterior para dejar hueco bajo el antiguo texto "Manifiesto") empezaba
     105px mas abajo en pantalla, asi que aunque avanzaran a la vez, sus
     bordes de desaparicion quedaban desalineados 105px en vertical.
     "Manifiesto" ya esta vacio (h2 sin texto, ver mas abajo) - ese hueco ya
     no tiene texto del que separarse, asi que top:0 no colisiona con nada.
     Con top:0 el marco pasa a ocupar pinBottom a pinBottom+276 (antes
     pinBottom+105 a pinBottom+381) - su borde inferior en reposo coincide
     ahora con pinBottom, el mismo borde que usa la cortina para tapar el
     texto, asi que el punto exacto de desaparicion (no solo el ritmo) es
     el mismo para ambos. */
  .manifesto__eye-frame {
    position: absolute;
    left: -366px;
    top: 0;
  }

  /* Peticion cliente 2026-07-26 (2a, sin cambios sobre este punto):
     "Somos el ingrediente..." se queda fijo junto con "Manifiesto"
     mientras avanzan los parrafos - su propio position:sticky, anclado
     ~50px antes de .about-page__divider (valor resuelto por JS, ver
     --manifesto-statement-top en main.js). background solido: sin esto
     el texto se veria pasar por debajo al hacer scroll.
     z-index:3 (tarea 9, subido desde 2 - antes empataba con
     .manifesto__pin, ver mas abajo): peticion cliente "cuando se oculta
     'Manifiesto', el titulo y la imagen se tienen que ocultar por debajo
     de 'Somos el ingrediente...'" - .manifesto__pin es sticky con su
     PROPIO rango de scroll (mas corto que el de .manifesto__showcase
     completo), asi que en algun momento se despega de su enganche nativo
     y sigue subiendo con el scroll normal, pasando fisicamente por la
     misma franja de pantalla donde este statement esta clavado. Con
     ambos al mismo z-index (2), el orden de pintado ganaba el ultimo en
     el DOM (.manifesto__pin, dentro de .manifesto__body, que va DESPUES
     de este statement como hermano en .manifesto) - el titulo/ojo se
     pintaban POR ENCIMA del statement en vez de tapados por su fondo
     solido. Subiendo el z-index de este statement por encima del pin
     (pero dejando intacta la relacion pin(2) > cortina(1), ver mas
     abajo) el statement, con su propio background opaco, vuelve a tapar
     correctamente lo que pase por detras - mismo mecanismo que ya usa
     para el resto de .manifesto__text, aplicado ahora tambien al pin. */
  .manifesto__statement {
    position: sticky;
    top: var(--manifesto-statement-top, var(--header-height));
    z-index: 3;
    background: var(--color-bg);
  }

  /* El pin se ancla justo debajo del statement (--manifesto-pin-top,
     resuelto por JS - ver main.js). Ya no es un grid (ver .manifesto__eye
     de arriba, 2026-07-30) - bloque simple, mismo ancho/posicion que
     .manifesto__statement/.manifesto__text.
     Bug reportado por el cliente 2026-07-30 ("aparece un rectangulo negro
     arriba a la derecha" al empezar a verse "Manifiesto"): esta regla
     tenia su propio background:var(--color-bg) para tapar el texto que
     pasa detras (intento anterior, mismo dia) - pero .manifesto__pin, sin
     ancho propio, ocupa TODO el ancho de la columna de contenido (~600-
     900px) aunque "Manifiesto" solo ocupe los primeros ~200px, asi que
     ese fondo opaco se veia como un rectangulo negro a la derecha del
     titulo, tapando lo que hubiera detras (la foto del estudio) incluso
     cuando no habia texto que ocultar todavia. Se quita el background de
     aqui (el pin vuelve a ser una caja invisible, solo se ve el propio
     texto "Manifiesto") - la funcion de tapar el texto que pasa detras
     pasa a .manifesto__curtain (ver mas abajo), que ya usaba un ancho
     correcto (el de .manifesto__text, no el del pin) y ahora ademas
     cubre hasta el borde inferior del pin, no solo hasta su borde
     superior. z-index:2 se mantiene: sigue haciendo falta para que el
     titulo se pinte por ENCIMA de la cortina (ambas piezas del mismo
     mecanismo, ver .manifesto__curtain). */
  /* Peticion cliente 2026-07-30 (tarea 6): "Manifiesto" ya NO lleva
     fundido propio - antes (tarea 5) heredaba un --manifesto-pin-fade
     continuo que tambien arrastraba al ojo (descendiente suyo), pero el
     cliente aclaro que la imagen tiene que desaparecer "por debajo de
     'Manifiesto'": el titulo se queda fijo y visible, solo el ojo se
     oculta (ver --manifesto-eye-fade en .manifesto__eye, mas arriba) - el
     titulo pasa a comportarse exactamente igual que "Servicios"/
     "Proyectos" (sticky normal, sin logica de ocultamiento propia,
     simplemente se desplaza fuera de vista de forma nativa al terminar su
     rango de scroll - nunca fue la causa del bug de footer tapado, eso lo
     causaba solo el ojo, mucho mas alto que el). */
  .manifesto__pin {
    position: sticky;
    top: var(--manifesto-pin-top, var(--header-height));
    z-index: 2;
  }

  /* Peticion cliente 2026-07-30 (tarea 10): en cuanto el pin (titulo+ojo)
     ha desaparecido detras de .manifesto__statement (z-index:3, ver mas
     arriba - tarea 9) se oculta del todo (visibility:hidden, no solo el
     z-index) - evita que el ojo (fuera de la columna del statement, ver
     left:-366px mas arriba, por lo que el fondo opaco del statement no lo
     tapa a el directamente) se quede asomando cuando el titulo ya deberia
     estar tapado. visibility (no display:none): el pin sigue sticky/
     ocupando su sitio en el flujo - solo deja de pintarse - evita
     cualquier salto de layout en el instante en que se oculta.
     .is-hidden-behind-statement (main.js, updateManifestoCurtain) se
     recalcula en cada frame de scroll a partir de la posicion real actual
     - NO es un estado permanente (ver tarea 11 en main.js: un primer
     intento con un "trinquete" de una sola direccion rompia el
     comportamiento reversible normal de position:sticky - al scrollear
     hacia atras y hacia adelante otra vez, "Manifiesto" ya no volvia a
     aparecer nunca, ni en la zona donde debia verse). Con la condicion
     recalculada en vivo, esta clase se quita y pone sola segun haga falta
     en cualquier direccion de scroll, igual que .manifesto__curtain
     .is-visible un poco mas abajo. */
  .manifesto__pin.is-hidden-behind-statement {
    visibility: hidden;
  }

  /* .manifesto__release-spacer: garantiza que, para CUALQUIER alto de
     viewport, haya suficiente scroll despues de .manifesto__showcase para
     que .manifesto__text llegue a terminar de pasar por detras del pin,
     CON margen de sobra para que el fundido continuo del ojo
     (--manifesto-eye-fade) tenga tambien esos px de recorrido para
     completarse - sin esto, en viewports altos (ver comentario grande de
     mas arriba) el ultimo parrafo se queda a medio camino indefinidamente
     y el fundido nunca llega a 0. Altura calculada por JS (ver
     syncManifestoStickyOffsets en main.js), recalculada tambien en cada
     resize porque depende de window.innerHeight. */
  .manifesto__release-spacer {
    display: block;
    height: var(--manifesto-release-spacer-h, 0px);
  }

  /* Peticion cliente 2026-07-26 (8a, sin cambios sobre este punto): sube
     3px el texto en escritorio/tablet, 16px (base) -> 19px. */
  .manifesto__text {
    font-size: 19px;
  }

  /* Reveal-on-scroll (2026-07-30): cada parrafo arranca invisible y
     desplazado 56px hacia abajo, y pasa a su posicion natural con un
     fade suave cuando main.js le anade .is-revealed (IntersectionObserver,
     ver el bloque de Manifiesto en main.js) - mismos valores (56px, 0.9s,
     misma curva) que #home-servicios .service-showcase__list .service-entry
     en components.css, para que ambos efectos de parallax del sitio se
     sientan iguales. */
  .manifesto__text p {
    opacity: 0;
    transform: translateY(56px);
    transition: opacity 0.9s cubic-bezier(0.16, 1, 0.3, 1), transform 0.9s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .manifesto__text p.is-revealed {
    opacity: 1;
    transform: translateY(0);
  }
}

/* .manifesto__curtain: ver el comentario grande en el @media
   (min-width:861px) de mas arriba ("SI se mantiene..."). display:none
   base (movil real, cualquier ancho): sin sticky/scroll-jacking en movil
   no hay nada que la cortina necesite tapar. */
.manifesto__curtain {
  display: none;
}

@media (min-width: 861px) {
  /* left/width (2026-07-30, corregido junto con el bug del rectangulo
     negro - ver nota en .manifesto__pin de mas arriba): rect real de
     .manifesto__text (JS, ver main.js), no de .manifesto__pin - la
     cortina tiene que ser tan ancha como el TEXTO que oculta, no como el
     titulo (mucho mas corto). height: var(--manifesto-curtain-height)
     (JS, pinTop + alto real del pin) cubre desde el techo del viewport
     hasta el borde INFERIOR del pin (0 a pinTop+altoPin) - ya no se
     detiene en el borde superior del pin (pinTop a secas): el pin dejo de
     tener su propio background (ver nota de arriba), asi que ahora es la
     cortina, no el pin, quien tapa tambien la franja donde vive el propio
     titulo "Manifiesto". z-index:2 en el pin lo sigue dejando por encima
     de la cortina (z-index:1), asi que el titulo se ve con normalidad.
     is-visible: ver IntersectionObserver sobre .manifesto__showcase en
     main.js. */
  .manifesto__curtain {
    display: block;
    position: fixed;
    top: 0;
    left: var(--manifesto-curtain-left, 0px);
    width: var(--manifesto-curtain-width, 0px);
    height: var(--manifesto-curtain-height, 0px);
    background: var(--color-bg);
    z-index: 1;
    opacity: 0;
    pointer-events: none;
  }

  .manifesto__curtain.is-visible {
    opacity: 1;
  }
}

/* Fallback (prefers-reduced-motion: reduce, cualquier ancho) - consolidado
   2026-07-30 (antes repartido en 2 bloques @media distintos, uno de ellos
   compartido con .manifesto__progress/__release-spacer, los 2 eliminados
   junto con el resto del carrusel de scroll-jacking, ver comentario
   grande en el @media (min-width:861px) de mas arriba): con reduced-motion
   activado, "Manifiesto" deja de fijarse (mismo criterio que
   .manifesto__statement, no una preferencia nueva) y los 3 parrafos se
   muestran directamente en su posicion final, sin el fade+deslizamiento
   de entrada (el guard !prefersReducedMotion en main.js tampoco añade la
   clase .is-revealed en este caso - sin este reset se quedarian
   invisibles, atascados en el estado inicial opacity:0). Sin pin sticky
   no hay nada que .manifesto__curtain necesite tapar tampoco (mismo
   guard !prefersReducedMotion evita que main.js la haga visible, pero se
   oculta aqui tambien por si acaso). .manifesto__eye: mismo guard evita
   que main.js le anade --manifesto-eye-offset, asi que ya cae en su
   propio fallback (translateY(0), ver la regla base de mas arriba) sin
   necesitar reset explicito aqui. */
@media (prefers-reduced-motion: reduce) {
  .manifesto__statement {
    position: static;
  }
  .manifesto__pin {
    position: relative;
    top: auto;
  }
  .manifesto__text p {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .manifesto__release-spacer {
    display: none;
  }
  .manifesto__curtain {
    display: none;
  }
}

/* ---------- Contacto (re-auditada 2026-06-26, columna fija añadida) ----------
   Figma (frame Contacto 1:55): titulo "Contacto" (x=98,y=229) y statement
   Lorem ipsum (x=495,y=416) NO empiezan a la misma altura - a diferencia
   de Nosotros (donde si estan alineados en la misma fila), aqui el
   statement esta deliberadamente 187px mas abajo que el titulo, ademas
   de a la derecha (confirmado visualmente en la captura de Figma: el
   statement queda claramente por debajo del titulo+linea, no a su lado
   a la misma altura). 397px = x=495 (inicio del statement) - x=98
   (inicio del titulo) = ancho de la columna 1. El titulo de esta pagina
   SI usa 96px (--text-display-xl, ya correcto - es la unica pagina de
   las 4 cuyo titulo coincide con el token grande, confirmado en
   typography-map.md), por eso no hace falta modificador de tamano aqui.

   Peticion del cliente (2026-06-26): mismo efecto que en Servicios -
   "Contacto" se queda fijo SIEMPRE (toda la pagina, no solo el hero) y
   el contenido que sube por scroll desaparece a la altura de la linea,
   no en el header. Por eso .contact-page envuelve TODO (statement +
   imagen + cuerpo), no solo el hero como antes (.contact-hero). */
.contact-page {
  position: relative;
  display: grid;
  grid-template-columns: 397px minmax(0, 1fr);
  padding-top: calc(var(--header-height) + var(--space-2xl));
  padding-bottom: var(--space-2xl);
}

/* Re-auditado 2026-07-20 (nodo 1:131, nuevo desde la ultima revision):
   un email de utilidad aparece por encima del titulo "Contacto" - no
   dentro del flujo normal de .contact-page__content (esta 27px MAS
   ARRIBA que el titulo, y el titulo ya es el primer elemento del flujo).
   position:absolute anclado a .contact-page (position:relative, arriba)
   en vez de meterlo en el grid: top = mismo calc() que usa el titulo
   para su propio reposo (header-height + space-2xl) menos esos 27px;
   left = 397px, el mismo ancho de columna 1 que ya usa el grid, para que
   arranque exactamente donde arranca la columna de contenido. Sin spec
   movil en Figma (frame fijo de escritorio) - resuelto en el
   @media(max-width:860px) de mas abajo con el mismo criterio de
   accepted-deviation que el resto del sitio. */
.contact-page__email {
  position: absolute;
  top: calc(var(--header-height) + var(--space-2xl) - 27px);
  left: 397px;
  color: var(--color-text-soft);
  font-size: 16px;
}

/* Excepcion deliberada pedida por el cliente (2026-06-26): el titulo de
   Contacto pasa a 48px, igual que "Servicios" en servicios.html. Figma
   define 96px para el nodo de Contacto (1:130) - se confirmo esto
   explicitamente con el cliente antes de aplicarlo, es una decision de
   diseño consciente que se aparta de Figma, no un error de fidelidad.

   Ajuste 2026-06-26 (pedido por el usuario): mismo margin-top de pagina
   y de titulo, y misma tipografia/tamaño, que "Nosotros". La posicion
   (padding-top de .contact-page, top del sticky) ya coincidia exacto con
   .about-page/.page-hero__title--nosotros (misma formula
   header-height+space-2xl en ambas) - sin cambios ahi. font-weight y
   line-height se declaran aqui explicitamente (antes dependian de
   heredarlos de la clase base .page-hero__title) para que coincidan con
   .page-hero__title--nosotros de forma explicita, no solo por
   coincidencia de cascada. */
.page-hero__title--contacto {
  align-self: start;
  position: sticky;
  top: calc(var(--header-height) + var(--space-2xl));
  z-index: 2;
  background: var(--color-bg);
  font-size: 48px;
  font-weight: normal;
  line-height: normal;
}

/* Franja opaca (mismo mecanismo que .services-layout::before en
   Servicios): el contenido de la columna derecha desaparece al llegar a
   la altura de la linea de "Contacto", no al llegar al header. Alto =
   space-2xl (hasta donde empieza el titulo) + ~69px (texto del titulo a
   48px tras la excepcion deliberada de tamaño + margin-top de la linea,
   ajustado a 13px como en Servicios/Nosotros, + la linea).
   +10px (2026-06-28, peticion del usuario, mismo cambio aplicado antes
   en Nosotros): el texto debe ocultarse 10px por debajo de la linea, no
   exactamente al llegar a ella. */
.contact-page::before {
  content: "";
  position: fixed;
  top: var(--header-height);
  left: 0;
  right: 0;
  height: calc(var(--space-2xl) + 69px + 10px);
  background: var(--color-bg);
  z-index: 1;
  pointer-events: none;
}

/* Figma original mide 64px (var(--text-display-md), heredado de la
   clase base) y 187px de distancia titulo->statement (y=416 vs y=229) -
   pero el usuario pidio igualar tanto el espacio (99px) como la
   tipografia/tamaño a los de Nosotros, senalando que la diferencia entre
   ambas paginas era demasiado grande tras unificar titulo/linea entre
   las dos (mismo criterio que las excepciones deliberadas ya aplicadas:
   tamaño de titulo, ancho de linea, espacio titulo->linea). Mismos
   valores exactos que .page-hero__statement--nosotros - actualizado
   2026-06-28 a 32px/normal junto con esa regla (ver su nota: el Figma de
   Nosotros cambio de 48px/92.055% a 32px/normal despues del 2026-06-26).
   Esta regla de Contacto NO se ha re-auditado de forma independiente
   contra su propio nodo Figma (1:132) - solo se mantiene igualada a
   Nosotros porque asi lo pidio el usuario explicitamente.

   Re-auditado 2026-07-20 (peticion "analiza los cambios de Figma"): el
   texto Lorem ipsum se sustituyo por el copy real ("Conversemos sobre
   nuevas ideas..."). El nodo 1:132 en Figma ahora mide 48px (ni los 64px
   de la auditoria original ni los 32px actuales) y el hueco titulo-
   >statement tambien cambio (y=310 vs y=229 del titulo = 81px, no ya los
   187px originales). NO se tocan font-size/margin-top aqui: la decision
   del usuario de igualar Contacto a Nosotros fue explicita y no
   referenciaba un valor de Figma concreto que debiera seguir
   actualizandose con cada revision - se preserva 32px/99px hasta que el
   usuario diga lo contrario. Señalar esta discrepancia (48px/81px reales
   en Figma vs 32px/99px aplicados) si se revisa esta pagina de nuevo. */
.contact-page__content .page-hero__statement {
  margin-top: 99px;
  font-size: 32px;
  line-height: normal;
}

/* Figma (node 1:102/1:103): linea decorativa de 339px bajo "Contacto" -
   coincide numericamente con la de Servicios (tambien 339px) por
   coincidencia, no por compartir el mismo valor a proposito - cada
   pagina tiene su propio nodo Figma. margin-top ajustado a 13px (era
   25px) a peticion del usuario (2026-06-26): el espacio titulo->linea
   debe coincidir con el de Servicios en las 3 paginas que usan este
   patron (Servicios/Nosotros/Contacto), no solo el ancho de la linea. */
.page-hero__title--contacto::after {
  content: "";
  display: block;
  margin-top: 13px;
  width: 339px;
  height: 1px;
  background: var(--color-text-muted);
}

/* Imagen + cuerpo secundario de Contacto (Figma: node 1:133-1:137 imagen,
   1132 x 479.97; node 1:131 cuerpo, Lorem ipsum). La imagen ocupa casi
   todo el ancho del FRAME completo (1132 de 1280), no solo el ancho de
   la columna de contenido de .contact-page (que es mas estrecha, al
   reservar 397px para "Contacto") - por eso "escapa" de su columna con
   width/margin-left negativos, igual de ancha que si no hubiera columna
   de titulo. Sin esto, la imagen se veria un 35% mas pequeña de lo que
   marca Figma. */
.contact-visual {
  width: calc(100% + 397px);
  margin-left: -397px;
  padding-block: var(--space-xl) 0;
}

.contact-visual__media {
  aspect-ratio: 1132 / 479.972;
  border-radius: 4px;
  overflow: hidden;
}

.contact-visual__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Re-auditado 2026-07-20: el cuerpo secundario Lorem ipsum (node 1:131,
   antigua .contact-visual__body) ya no existe en Figma - la revision
   actual lo sustituye por un formulario de contacto real (nodos
   487:1052-487:1061) y un bloque de datos de contacto (node 424:572,
   email/telefono/ciudad), uno junto al otro, debajo de la imagen.
   .contact-form-section usa la MISMA tecnica de bleed que .contact-visual
   (width:100%+397px / margin-left:-397px) porque tambien vive dentro de
   .contact-page__content (columna 2) y tiene que ocupar el ancho
   completo del frame igual que la imagen - no se restructura el HTML
   para usar grid-column:1/-1 en su lugar por consistencia con el patron
   ya establecido en este archivo (mismo criterio que
   .manifesto__media-wrap en Nosotros).
   padding-left:96px = posicion real medida del formulario (node
   487:1052, x=96) dentro de ese ancho bled (que replica el frame
   Figma completo, no la columna de contenido). margin-top:105px = hueco
   real imagen->formulario (form top y=1197 - imagen bottom
   581+511=1092). gap:64px entre formulario e info = hueco real medido
   entre el borde derecho del formulario (96+496=592) y el inicio del
   bloque de datos (656). */
.contact-form-section {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 64px;
  width: calc(100% + 397px);
  margin-left: -397px;
  margin-top: 105px;
  padding-left: 96px;
}

/* 496px = ancho real del formulario en Figma (los 5 campos comparten
   ese mismo ancho). 15px de gap entre campos = hueco real constante
   medido entre los 5 elementos (48px de alto cada campo, salvo el
   mensaje que mide 324px) - los 5 huecos reales medidos (63, 63, 63, 63px
   de diferencia entre tops menos 48px de alto) dieron el mismo valor,
   15px, en los 4 casos. */
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 15px;
  width: 496px;
  max-width: 100%;
}

.contact-form__field { display: block; }

/* Campos sin borde visible en Figma (fondo solido #d9d9d9, sin trazo) y
   sin esquinas redondeadas (el export de Figma no incluye ninguna clase
   "rounded-*" pese a que get_metadata los llama "rounded-rectangle" -
   nombre generico del TIPO de nodo en Figma, no indica un radio > 0
   real). padding-inline:19px = x real del texto (115) menos x real de
   la caja (96). El campo de texto (input) usa altura fija (48px, la
   misma que la caja) y centra el texto verticalmente por defecto del
   navegador; el textarea, al no centrar verticalmente su placeholder,
   usa padding-top:14px, la distancia real medida entre el top de su caja
   y el top de su texto en Figma. */
.contact-form input,
.contact-form textarea {
  display: block;
  width: 100%;
  border: none;
  border-radius: 0;
  background: #d9d9d9;
  padding-inline: 19px;
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--color-text-soft);
}

.contact-form input {
  height: 48px;
}

.contact-form textarea {
  height: 324px;
  padding-top: 14px;
  resize: vertical;
  font-family: var(--font-body);
}

.contact-form input::placeholder,
.contact-form textarea::placeholder {
  color: var(--color-text-soft);
  opacity: 1;
}

/* No hay CTA de envio en el frame de Figma auditado (ni boton ni texto
   entre el textarea y el pie de pagina) - probable hueco del diseño, no
   una decision deliberada de omitirlo. Se añade un boton minimo y una
   nota de "demo, sin backend" porque un formulario sin forma de enviarlo
   no es utilizable ni evaluable como prototipo - decision del asistente,
   NO viene de Figma, señalar al usuario para confirmar o sustituir por
   el CTA real cuando el cliente lo defina. data-form-state="static" en
   el <form> (contacto.html) documenta que no esta conectado a backend,
   siguiendo el mismo criterio que el resto del prototipo para
   formularios placeholder (ver tambien el preventDefault en main.js). */
.contact-form__note {
  margin: 0;
  color: var(--color-text-muted);
  font-size: 13px;
  line-height: 1.4;
}

.contact-form__submit {
  align-self: flex-start;
  border: 1px solid var(--color-text);
  background: transparent;
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 16px;
  padding: 0.75rem 2rem;
  cursor: pointer;
}

.contact-form__submit:hover,
.contact-form__submit:focus-visible {
  background: var(--color-text);
  color: var(--color-bg);
}

.contact-form__submit:disabled {
  opacity: 0.6;
  cursor: default;
}

/* AÑADIDO SOLO EN EL TEMA (2026-07-21), no existe en el prototipo: el
   formulario del prototipo es una demo sin backend y no necesita mensaje
   de exito/error real. En WordPress si envia de verdad (ver
   includes/contact-form.php del plugin y el fetch() en main.js) y
   necesita feedback inline. Si se vuelve a copiar pages.css del
   prototipo por encima de este archivo (patron ya usado varias veces en
   este proyecto para sincronizar), este bloque se perderia - hay que
   volver a añadirlo a mano. Sin token de color dedicado (no forma parte
   del sistema de diseño de Figma), valores literales suficientemente
   distintos de --color-text/--color-text-muted para ser legibles como
   estado positivo/negativo sobre fondo oscuro. */
.contact-form__response {
  margin: 0;
  font-size: 13px;
  line-height: 1.4;
  min-height: 1.4em;
}

.contact-form__response--success {
  color: #7ed6a5;
}

.contact-form__response--error {
  color: #e8837a;
}

/* Bloque de datos de contacto (node 424:572): email/telefono/ciudad,
   16px, Early Sans Variable, color #989898 (--color-text-soft, el mismo
   tono que el resto del sitio - a diferencia del antiguo cuerpo Lorem
   ipsum que usaba --color-text-muted). max-width = ancho real medido en
   Figma (557px) para que no se estire de mas en pantallas anchas dentro
   del flex de .contact-form-section (que no fija un ancho de columna
   propio para este bloque, solo lo posiciona con gap tras el
   formulario). */
.contact-form__info {
  max-width: 557px;
  color: var(--color-text-soft);
  font-size: 16px;
  line-height: 1.8;
}

.contact-form__info p { margin: 0; }

.contact-form__info a {
  color: inherit;
  text-decoration: none;
}

.contact-form__info a:hover,
.contact-form__info a:focus-visible {
  text-decoration: underline;
}

/* ---------- Proyectos (index) — mosaico real (2026-06-26) ----------
   Figma (frame Proyectos 1:582, "Group 1" 1:609): la pagina de Proyectos
   NO es una lista de fichas texto+imagen como Home - es un mosaico
   asimetrico de 11 piezas en 2 columnas (5 a la izquierda, 6 a la
   derecha), cada una con su propio alto real (sin aspect-ratio comun).
   Reemplaza la version anterior, que reutilizaba por error el layout de
   4 fichas de Home (ver client-feedback/projects-page-rebuild-spec.md).

   El titulo "Proyectos" y el mosaico empiezan a la MISMA altura (no el
   titulo arriba y el mosaico despues, como en el resto de paginas) -
   por eso .projects-layout es un grid de 2 columnas en vez de un
   .page-hero de ancho completo seguido de una seccion aparte. */
.projects-layout {
  display: grid;
  grid-template-columns: minmax(160px, 22%) minmax(0, 1fr);
  padding-top: calc(var(--header-height) + var(--space-2xl));
  padding-bottom: var(--space-2xl);
}

/* Petición del cliente (2026-06-26): al hacer scroll, la columna
   "Proyectos" se queda fija y es la columna de la derecha (el mosaico)
   la que se mueve. `align-self:start` evita que el grid estire la caja
   del titulo a la altura del mosaico (si no, "sticky" seguiria
   funcionando pero la caja ocuparia visualmente toda esa altura).
   `top` usa el mismo `calc(header-height + space-2xl)` que ya marca el
   padding-top de `.projects-layout` - así el titulo se "fija" exactamente
   en la posicion en la que ya estaba en reposo, conservando el espacio
   inicial bajo el header en vez de saltar a quedar pegado al header en
   cuanto empieza el scroll (corrección pedida por el cliente: "quiero
   que se mantenga fijo también el espacio inicial que hay debajo del
   header"). */
.projects-layout__title {
  align-self: start;
  position: sticky;
  top: calc(var(--header-height) + var(--space-2xl));
}

/* Figma (node 1:638): "Proyectos", Georgia Regular, 48px, blanco - igual
   escalon que Servicios/Nosotros/Contacto (ya documentado en
   typography-map.md como bug: esta pagina usaba 96px). Linea decorativa
   (node 1:639/1:640): ancho fijo 230px (no 339px como en Servicios - cada
   pagina tiene su propio ancho de linea, no es el mismo valor reutilizado). */
.page-hero__title--proyectos {
  display: inline-block;
  font-size: 48px;
  font-weight: normal;
  line-height: normal;
}

.page-hero__title--proyectos::after {
  content: "";
  display: block;
  margin-top: 13px;
  width: 230px;
  height: 1px;
  background: var(--color-text-muted);
}

/* Figma: gap real entre columnas y entre piezas de cada columna es ~4px
   en los 11 casos medidos (no var(--space-xl) ni otro token de espaciado
   - es un valor propio de esta pagina, deliberadamente minimo, casi sin
   separacion, como un muro de carteles). Margen derecho del mosaico
   (~2.7% del ancho de referencia) tambien es real - el mosaico casi toca
   el borde derecho del frame en Figma, asimetria ya documentada antes
   para el teaser de Proyectos en Home (HOME-PROJECTS-LAYOUT-01). */
.project-mosaic {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  padding-right: 2.7%;
}

.project-mosaic__col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.project-mosaic__item {
  margin: 0;
  /* Peticion cliente 2026-07-13: el texto del hover (.project-mosaic__caption,
     ver mas abajo) debe quedar justo debajo de su propia imagen, sin
     solaparse con la pieza siguiente (antes: con solo 4px de gap entre
     piezas, el texto quedaba pintado por encima de la imagen de abajo).
     Al pasar el hover, esta pieza gana un margen inferior que empuja el
     resto de la columna hacia abajo, abriendo hueco real para el texto;
     transition sincronizada con la del propio .project-mosaic__caption. */
  margin-bottom: 0;
  transition: margin-bottom 0.4s ease;
}

/* Peticion cliente 2026-07-22: 28px solo alcanzaba para un titulo de una
   linea. main.js mide la altura real del caption de cada pieza (que varia
   segun cuantas lineas ocupe su titulo) y la guarda en
   --mosaic-hover-margin por pieza; 28px queda solo como fallback si el
   script no llega a ejecutarse (mismo patron que el prototipo). */
.project-mosaic__item:has(.project-mosaic__frame:hover),
.project-mosaic__item:has(.project-mosaic__frame:focus-visible),
.project-mosaic__item:has(.project-mosaic__frame.is-active) {
  margin-bottom: var(--mosaic-hover-margin, 28px);
}

.project-mosaic__item img {
  display: block;
  width: 100%;
  height: auto;
}

/* Figma (node 1:622, "Proyecto_4" / DCAVI Bogotá): unica de las 11 piezas
   compuesta con una mascara CSS interna (mask-image) en vez de ser una
   imagen plana. El export estatico de Figma (download_assets y
   get_screenshot, probados ambos) no respeta bien ese compositing y deja
   un hueco en blanco en el resultado - confirmado descargando la capa
   fuente sin componer (limpia, sin hueco) e inspeccionando la mascara en
   si (un SVG con un solo <path> rectangular, sin silueta real, mismo
   patron ya visto en .manifesto__media). Por eso esta pieza no usa
   tamano natural como las otras 10 - se usa la capa fuente directamente
   con aspect-ratio + object-fit:cover (recorte fiable, sin depender del
   export ya compuesto). 482/478 = proporcion real del frame "Proyecto_4". */
.project-mosaic__item--crop {
  aspect-ratio: 482 / 478;
}

.project-mosaic__item--crop img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Rediseño 2026-07-13 (Figma node 1:582, 13 piezas en vez de las 11
   anteriores): las nuevas imagenes "portada" vienen explicitamente
   recortadas en Figma dentro de cajas de tamano fijo (object-cover +
   size-full sobre el nodo), no como imagenes con proporcion natural -
   por eso ya no basta con width:100%/height:auto (regla generica de
   arriba). Se generalizan aqui los dos formatos reales medidos en el
   frame: cuadrado (mismo ancho que alto de columna) y vertical 3:4
   (475/633, igual proporcion en las 5 piezas verticales del frame). */
.project-mosaic__item--square {
  aspect-ratio: 1 / 1;
}

.project-mosaic__item--portrait {
  aspect-ratio: 475 / 633;
}

.project-mosaic__item--square img,
.project-mosaic__item--portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---------- Mosaico de Proyectos: hover (referencia koto.com/work,
   2026-06-26; rediseñado 2026-07-13 a petición del cliente) ----------
   Petición original del cliente: al pasar el cursor sobre cada pieza,
   zoom-out sutil de la imagen + aparece la info del proyecto. No hay
   ningún hover definido en Figma (herramienta estática) - esto es una
   interacción nueva, no una traducción de un diseño existente.

   Cambio 2026-07-13: el cliente ya no quiere la descripción superpuesta
   sobre la imagen (antes .project-mosaic__overlay, con fondo degradado +
   titulo/subtitulo/descripcion encima de la foto). Ahora solo título
   (izquierda) y subtítulo (derecha) aparecen DEBAJO de la imagen -  no
   hay descripción en este nuevo formato. Se posiciona con
   position:absolute/top:100% (no en flujo normal) para no empujar la
   siguiente fila del mosaico, que solo tiene 4px de gap entre piezas -
   se solapa transitoriamente con la pieza de abajo mientras dura el
   hover, mismo criterio ya usado por el overlay anterior (pointer-events:
   none salvo en hover/focus).

   Texto mostrado por pieza (decisión explícita del usuario, sin inventar
   nada nuevo): las 3 piezas que SÍ corresponden a una página de detalle
   ya construida (Slagharen, Superman→Parque Warner Madrid, maratón de
   Barcelona→Wonderlust 108) muestran el título/país/categoría reales de
   esa página y enlazan a ella. "Sweet Candy Studios" muestra su país real
   (único dato confirmado, ya existía como pie de foto fijo - ahora pasa a
   verse solo en el hover). Las 7 piezas restantes (Tropical Islands, Rise
   Melbourne, DCAVI Bogotá, Winds of Change, Arrecife, Prólogo, Terratile)
   no tienen subtítulo propio en Figma - solo título, sin dato a la
   derecha. */
/* height:100% es necesario para que la imagen (width/height:100% +
   object-fit:cover, ver .project-mosaic__item--square/--portrait img mas
   abajo) pueda cubrir realmente la caja de su .project-mosaic__item: sin
   esto el <a> queda con altura auto y el height:100% de la imagen no
   tiene contra qué resolverse, así que la imagen cae a su relación de
   aspecto natural en vez de recortarse - invisible cuando esa relación
   coincide por casualidad con la de la caja (p.ej. Warner, 1600x2133 ≈
   3:4), pero deja hueco negro real cuando no coincide (p.ej. Welt Vogel
   Park, imagen casi cuadrada dentro de una caja --portrait). */
.project-mosaic__frame {
  position: relative;
  display: block;
  height: 100%;
}

.project-mosaic__frame img {
  transition: transform 0.5s ease;
}

/* .is-active ya no la añade ningun JS (el tap-to-hover de movil se quito
   a peticion del cliente 2026-07-13, ver mobile override mas abajo que
   fuerza el caption siempre visible) - el selector se deja por si se
   reutiliza el patron en otra pieza, pero hoy solo aplica :hover puro. */
.project-mosaic__frame:hover img,
.project-mosaic__frame:focus-visible img,
.project-mosaic__frame.is-active img {
  transform: scale(0.96);
}

.project-mosaic__caption {
  position: absolute;
  /* 98%, no 100%: la imagen se escala a 0.96 en hover (scale hacia el
     centro), asi que su borde inferior VISIBLE queda un 2% de la altura
     del frame por encima del borde inferior real de la caja (que no se
     mueve, el transform no afecta el layout). Anclar aqui al 100% dejaba
     ese 2% como hueco extra encima del texto (espacio superior mas
     grande que el inferior) - con 98% el texto queda pegado al borde
     visible de la imagen ya encogida, e invierte la asimetria pedida por
     el cliente 2026-07-13 sin tocar padding-top/margin-bottom. */
  top: 98%;
  /* left/right: 2%, no 0 - mismo motivo que el top:98% de mas arriba: el
     scale(0.96) del hover encoge la imagen tambien en horizontal, dejando
     un margen visible del 2% del ancho del frame a cada lado. Anclar a
     0/0 alineaba el padding-inline (10px, ver mas abajo) contra el borde
     SIN escalar del frame, no contra el borde real de la imagen ya
     encogida - el resultado eran ~0.6px de margen real en vez de 10px.
     Con 2%/2% el 10px de padding-inline se mide desde el borde visible de
     la imagen, igual en cualquier pieza sin importar su ancho. */
  left: 2%;
  right: 2%;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-sm);
  padding-top: 4px;
  padding-inline: 10px;
  opacity: 0;
  transform: translateY(-6px);
  /* Salida (hover-out): rapida y sin retraso - ocultar antes nunca crea
     solape, solo hace que el texto desaparezca un poco antes. */
  transition: opacity 0.2s ease, transform 0.2s ease;
  pointer-events: none;
}

/* Bug encontrado 2026-07-26 (reportado por el usuario: en Proyectos, al
   pasar el hover algunos titulos quedaban tapados por la imagen de la
   pieza de abajo, o con muy poco aire). Causa: .project-mosaic__item gana
   el margen que abre hueco (pages.css, ".project-mosaic__item:has(...)")
   con una transition de 0.4s, pero ESTE bloque (el texto en si) tambien
   animaba en 0.4s con la misma curva "ease" - medido con Playwright: a
   los ~140ms el texto ya tiene opacity ~0.6 (perfectamente legible)
   mientras el hueco todavia es NEGATIVO (imagen de abajo superpuesta);
   el hueco no se vuelve positivo hasta ~190ms. Cuanto mayor el margen
   necesario (titulos de 2 lineas, 58px en vez de 33px) mayor la
   superposicion visible durante esos ~190ms - por eso el usuario lo
   notaba en "algunos titulos" y no en todos.
   Solucion: se retrasa la entrada del texto (transition-delay 0.25s,
   solo en el hover-in, no en la salida) hasta un punto en el que el
   hueco ya mide 10px o mas en TODAS las piezas medidas (incluidas las de
   margen mayor) - el texto ya no se hace legible hasta que el hueco
   minimo pedido por el cliente (5-10px) esta garantizado. */
.project-mosaic__frame:hover .project-mosaic__caption,
.project-mosaic__frame:focus-visible .project-mosaic__caption,
.project-mosaic__frame.is-active .project-mosaic__caption {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.15s ease 0.25s, transform 0.15s ease 0.25s;
}

.project-mosaic__caption-title {
  font-family: var(--font-display);
  font-size: 17px;
  line-height: normal;
  color: var(--color-text);
}

.project-mosaic__caption-subtitle {
  font-family: var(--font-body);
  font-size: 13px;
  line-height: normal;
  color: var(--color-text-soft);
  text-align: right;
}

@media (prefers-reduced-motion: reduce) {
  .project-mosaic__frame img { transition: none; }
  .project-mosaic__caption { transition: none; }
  .project-mosaic__item { transition: none; }
}

/* ---------- Paginas de detalle de proyecto (2026-06-26) ----------
   Figma: 4 frames con la misma plantilla (Proyecto_1..4 = Slagharen,
   Bobbejaanland, Warner, Wanderlust) - columna de texto a la izquierda
   (titulo + pais + categoria + cuerpo) y galeria de imagenes a la
   derecha. 3 de las 4 (Wanderlust/Bobbejaanland/Slagharen) comparten
   exactamente la misma cuadricula de 10 piezas (1 banner + 2 + 3 + 2 + 2);
   Warner tiene su propia cuadricula de 8 (1 banner + 3 + 1 banner + 3).
   Mismo criterio de columnas/margenes ya usado en .projects-layout
   (Proyectos): columna de texto ~26% (texto real empieza en x=42 de 1280,
   galeria en x=333 de 1280 ≈ 26%), galeria con ~2.7% de margen derecho
   (la galeria casi toca el borde derecho del frame, mismo patron que el
   mosaico de Proyectos). */
.project-detail {
  display: grid;
  grid-template-columns: minmax(180px, 26%) minmax(0, 1fr);
  padding-top: calc(var(--header-height) + var(--space-2xl));
  padding-bottom: var(--space-2xl);
}

/* Mismo efecto pedido por el cliente para la pagina indice de Proyectos
   (.projects-layout__title) aplicado aqui: la columna de texto se queda
   fija al hacer scroll y es la galeria la que se mueve. `top` usa el
   mismo calc(header-height + space-2xl) que el padding-top de arriba,
   para que se "fije" justo en la posicion en la que ya estaba en
   reposo (conserva el espacio inicial bajo el header, no salta hacia
   arriba). */
/* Peticion cliente 2026-07-13: el boton "Ver todos los proyectos" (antes
   solo en Home) se traslada a cada pagina de proyecto individual,
   alineado al margen izquierdo del texto (a diferencia de .btn-bar en
   Home, que se alinea a la derecha - ver .project-detail__cta mas abajo
   para el override) y anclado a la zona INFERIOR de esta columna, no
   pegado al final del cuerpo de texto. display:flex column + min-height
   dinamico (basado en 100vh, no en un valor fijo - "de forma dinamica
   en funcion de la pantalla") consiguen esto: min-height reserva el
   mismo alto que tendria la columna hasta el borde inferior de la
   pantalla (100vh menos el offset superior de arriba, menos el mismo
   padding-bottom simetrico que ya usa la pagina, --space-2xl) y el boton
   (ultimo hijo del flex) usa margin-top:auto para consumir el hueco
   sobrante y quedar pegado abajo. Si el cuerpo de texto es mas largo que
   ese alto (proyectos con descripciones extensas), min-height no lo
   recorta - la columna crece con el contenido y el boton simplemente
   queda justo debajo del texto, sin superponerse a la galeria vecina
   (mismo comportamiento de sticky ya existente, sin cambios). */
.project-detail__intro {
  align-self: start;
  position: sticky;
  top: calc(var(--header-height) + var(--space-2xl));
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - var(--header-height) - var(--space-2xl) - var(--space-2xl));
}

/* Peticion cliente 2026-07-16: en todas las fichas de proyecto el boton de
   texto "Ver todos los proyectos" se sustituye por flecha izquierda
   (proyecto anterior) / icono de cuadricula (volver a todos los proyectos,
   Figma node 346:597 "darhboard") / flecha derecha (proyecto siguiente) -
   mismos 3 nodos que en Figma (346:599/346:597/346:598). Aplicado primero
   solo en Parque Warner Madrid como prueba, extendido despues a las 18
   fichas existentes (peticion cliente 2026-07-16, "recuerda que esta
   navegacion la tenemos que incluir en todos los proyectos nuevos que se
   incorporen" - CUALQUIER ficha de proyecto nueva debe llevar este mismo
   .project-detail__nav, ya no el boton de texto). .btn-bar SIGUE en uso
   (boton "Ver todos los proyectos" de Home, index.html) - solo se retira
   .project-detail__cta, que ya no lo usa ninguna ficha.
   .project-detail__nav reutiliza el mismo margin-top:auto dentro del
   flex-column sticky de .project-detail__intro (ver arriba) para quedar
   anclado abajo mientras se hace scroll por la galeria, igual que hacia
   antes el boton de texto.
   Flechas construidas con 2 bordes + rotate(45deg) (sin SVG ni asset
   externo): mismo patron ya usado en .menu-toggle para el icono de
   hamburguesa (CSS puro, sin depender de las URLs temporales de 7 dias
   que expone el MCP de Figma). Icono "ver todos" con el mismo color
   --color-text-soft (#989898) que usa el vector real en Figma. */
.project-detail__nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Mismo margen que .project-detail__body (ver arriba, 25px/1280*100 =
     1.953vw) - sin esto, al no tener .project-detail__nav ese margen
     derecho, quedaba mas ancho que el texto de encima en vez de compartir
     el mismo limite derecho. width:auto (no 100%) para que ese margen
     realmente reduzca el ancho ocupado, igual que en .project-detail__body
     (un <div> en flujo normal se encoge solo con margin-right; con
     width:100% explicito no lo haria, desbordaria el ancho del padre). */
  margin-right: 1.953vw;
  margin-top: auto;
}

/* Peticion cliente 2026-07-31: la misma navegacion de 3 botones (anterior/
   ver todos/siguiente) que ya hay debajo del texto se repite tambien
   debajo de las imagenes de cada proyecto - se coloca como ultimo item de
   .project-detail__gallery, con grid-column:1/-1 para ocupar toda la fila
   tanto en el grid de 6 columnas de escritorio como en el de 2 columnas
   de movil. Anula margin-top:auto/margin-right:1.953vw heredados de
   .project-detail__nav (pensados para anclar el otro nav al fondo de la
   columna sticky de texto, no aplica aqui).
   Aclaracion cliente el mismo dia: solo aplica en movil - oculto por
   defecto (desktop/tablet, el unico breakpoint real del sitio son estos
   860px, ver @media mas abajo) y reactivado dentro de ese media query. */
.project-detail__nav--gallery {
  display: none;
  grid-column: 1 / -1;
  margin-top: var(--space-md);
  margin-right: 0;
}

.project-detail__nav-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
}

.project-detail__nav-arrow::before {
  content: "";
  display: block;
  width: 10px;
  height: 10px;
  border: 0 solid var(--color-text);
  transform: rotate(45deg);
  transition: border-color var(--duration-fast) var(--ease-standard);
}

.project-detail__nav-arrow--prev::before {
  border-left-width: 1.5px;
  border-bottom-width: 1.5px;
}

.project-detail__nav-arrow--next::before {
  border-top-width: 1.5px;
  border-right-width: 1.5px;
}

.project-detail__nav-arrow:hover::before,
.project-detail__nav-arrow:focus-visible::before {
  border-color: var(--color-text-soft);
}

.project-detail__nav-all {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 45px;
  height: 45px;
  flex-shrink: 0;
}

.project-detail__nav-all-icon {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 4px;
  width: 20px;
  height: 20px;
}

.project-detail__nav-all-icon span {
  border: 1px solid var(--color-text-soft);
  border-radius: 1px;
  transition: border-color var(--duration-fast) var(--ease-standard);
}

.project-detail__nav-all:hover .project-detail__nav-all-icon span,
.project-detail__nav-all:focus-visible .project-detail__nav-all-icon span {
  border-color: var(--color-text);
}

@media (max-width: 860px) {
  .project-detail__nav {
    margin-top: 20px;
  }

  .project-detail__nav--gallery {
    display: flex;
  }
}

/* Cambio de marca 2026-06-26: re-verificado en las 6 paginas de detalle
   (Wanderlust 3:105, Bobbejaanland 1:400, Warner 1:313, Slagharen 1:493,
   Abu Dhabi 11:208, DEVCON VI 44:314) - Figma ahora usa **32px en las 6,
   sin excepcion** (antes 40px en 3 de ellas y 48px en Slagharen via el
   modificador --lg, que ya no hace falta - Slagharen tambien es 32px). */
.project-detail__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: normal;
  line-height: normal;
  color: var(--color-text);
}

.project-detail__country {
  margin: 0;
  margin-top: 45px;
  margin-bottom: 8px;
  font-family: var(--font-body);
  font-size: 20px;
  line-height: normal;
  color: var(--color-text);
}

.project-detail__tag {
  margin: 0;
  font-family: var(--font-body);
  font-size: 20px;
  line-height: normal;
  color: var(--color-text-muted);
}

/* Figma (nodo "Proyectos" > "Proyecto_2", presente en las 4 paginas,
   x=42 y w=245 en las 4 - antes pasada por alto porque su frame
   contenedor tiene height:0 en la metadata): linea horizontal de 1px,
   245px de ancho, color #5E5E5E (var(--color-text-muted)) debajo del
   pais/categoria y antes del cuerpo. Gap real medido en las 4 paginas:
   14px desde la categoria hasta la linea, 31px desde la linea hasta el
   cuerpo (14+31=45, el mismo total que se uso antes de encontrar la
   linea, pero repartido en 2 huecos en vez de uno). */
.project-detail__divider {
  margin: 0;
  margin-top: 14px;
  margin-bottom: 31px;
  width: 245px;
  max-width: 100%;
  height: 1px;
  border: none;
  background: var(--color-text-muted);
}

/* Cambio de marca 2026-06-26: re-verificado en las 6 paginas - el cuerpo
   es **16px/26px de interlineado en las 6, sin excepcion** (antes
   Wanderlust usaba 14px/interlineado normal via el modificador --sm, que
   ya no hace falta - Wanderlust tambien es 16px/26px ahora). */
.project-detail__body {
  margin: 0;
  /* Peticion cliente 2026-07-13: separacion de "unos 25px" entre el
     cuerpo de texto y la galeria, medida en devtools sobre el viewport
     de referencia (1280px, el mismo frame de Figma del que salen el
     resto de medidas de esta pagina - ver comentario de .project-detail
     mas arriba). Como margen derecho, no bottom/top: la columna de
     texto y la galeria son vecinas horizontales en la misma fila de
     grid, asi que el hueco entre ambas es un margen a la derecha del
     texto, no vertical. En vw (no %) para que escale con el ancho real
     de pantalla en vez de con el 26% de la columna de texto (un % aqui
     se calcularia sobre ese 26%, no sobre los 1280px de referencia, y
     saldria un valor mucho mas pequeno del pedido). 25/1280*100 = 1.953vw. */
  margin-right: 1.953vw;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 26px;
  color: var(--color-text-soft);
}

.project-detail__body p + p { margin-top: 1em; }

.project-detail__gallery {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 4px;
  padding-right: 2.7%;
}

.project-detail__media {
  margin: 0;
}

.project-detail__media img {
  display: block;
  width: 100%;
  height: auto;
}

.project-detail__media--span-6 { grid-column: span 6; }
.project-detail__media--span-3 { grid-column: span 3; }
.project-detail__media--span-2 { grid-column: span 2; }

@media (max-width: 860px) {
  /* A 111px (peticion cliente 2026-07-10, ver regla base de
     .intro-statement h1 mas arriba), palabras largas del statement
     ("ingrediente", "sensación", "conocido") no caben en el ancho de
     columna movil (~390px de viewport tipico del sitio, columna util
     ~327px tras el margen de --space-md) y desbordan la pagina - cada
     palabra es una caja sin espacios internos, no puede encogerse por
     debajo de su ancho renderizado. Se reduce a 56px en el fallback
     movil, unico tamano que deja margen suficiente para la palabra mas
     larga en el viewport de referencia del sitio (~390px). Igual que el
     resto de fuentes de este archivo, no se usa clamp() (ver historial de
     tokens.css: --content-padding paso de clamp a valor fijo el
     2026-07-10). */
  /* Bug encontrado 2026-07-13 (peticion cliente: el statement se corta en
     movil): .intro-statement traia margin-left/right:50px PROPIO (pensado
     para sumarse al padding-inline:50px de .container en pantallas anchas,
     "150px a cada lado" segun el comentario de la regla base) y su h1
     tenia ADEMAS su propio margin-left:2.2vw/margin-right:6.3888vw - los
     3 margenes se acumulan (nunca se reseteaban para movil: un intento
     anterior de resetearlos en el otro @media(max-width:860px) de mas
     arriba en este archivo no ganaba la cascada, por quedar ANTES que
     estas reglas base con la misma especificidad - movido aqui, que ya
     esta despues de ambas). Con los 3 margenes acumulados quedaba una
     columna de solo ~156px utiles en un viewport de 390px - con eso,
     casi cada palabra del statement ("ingrediente", "sensación",
     "conocido") ocupaba su propia linea a 56px, 12 lineas * 70px de
     interlineado = 840px de texto, mas alto que el propio pin (100dvh)
     con overflow:hidden, asi que las ultimas lineas quedaban recortadas.
     Se resetean aqui los 3 margenes (el ancho disponible pasa a depender
     solo del padding-inline:50px de .container, igual que el resto de
     paginas en movil) y se reduce el padding-top (heredaba
     --space-2xl=96px, pensado para dejar aire bajo un header mucho mas
     ancho) - con mas ancho util, el texto envuelve en menos lineas y
     cabe entero sin recortarse. */
  .intro-statement {
    margin-left: 0;
    margin-right: 0;
    padding-top: var(--space-lg);
  }

  /* 40px (bajado de 56px): con el ancho util ya liberado de mas arriba,
     56px seguia dejando muchas palabras largas en su propia linea. 40px
     es el mayor tamano que, combinado con ese ancho, mantiene el bloque
     de texto completo dentro del alto del pin (100dvh) en el viewport de
     referencia del sitio (~390x844) sin recortarse por el overflow:hidden
     del pin. */
  .intro-statement h1 {
    font-size: 40px;
    margin-left: 0;
    margin-right: 0;
  }

  /* minmax(0, 1fr), no "1fr" a secas: un 1fr puro no evita que la pista
     crezca para acomodar el max-content de un hijo de ancho fijo - aqui
     la linea decorativa de Nosotros (.about-page__divider, width:884px)
     empujaba TODA la columna (y por tanto cada hijo de .about-page) a
     884px de ancho, con scroll horizontal y texto cortado en cualquier
     viewport movil real - su propio max-width:100% (ver esa clase) no
     evita esto porque resuelve el porcentaje contra el ancho de la pista
     del grid, que es la que está creciendo - hay que cortar el problema
     ahi, no en el hijo. */
  .about-page,
  .contact-page { grid-template-columns: minmax(0, 1fr); }

  /* En movil titulo de Nosotros/Contacto va apilado en 1 columna - el
     sticky y la franja de mascara no tienen sentido en ese layout, se
     desactivan (mismo criterio que .services-layout__title en Servicios). */
  .page-hero__title--nosotros,
  .page-hero__title--contacto { position: static; }
  .about-page::before,
  .contact-page::before { display: none; }

  /* Bug encontrado 2026-07-25 (al mejorar la composicion del manifiesto en
     tablet): a diferencia del titulo "Nosotros" (linea de arriba,
     position:static en movil), esta linea decorativa vive en un <div>
     propio, hermano del titulo, no en su ::before - se quedaba sin
     resetear, asi que seguia con el position:sticky de escritorio (queda
     "flotando" segun se hace scroll y se monta encima del manifiesto,
     mucho mas abajo en la pagina). */
  .about-page__divider {
    position: static;
  }

  /* Sin spec movil en Figma (accepted-deviation, igual criterio que el
     resto del sitio): la linea de Nosotros mide 884px en Figma (frame
     fijo de escritorio, ver nota en .about-page__divider - ya tiene su
     propio max-width:100% definido siempre, no solo en movil), mas ancha
     que cualquier viewport movil.
     Las de Contacto (339px), Servicios (.page-hero__title--md, 339px) y
     Proyectos (230px) cabian en moviles >=360-375px mientras el margen
     lateral fue 20-50px, pero con el margen ampliado a 100px (2026-07-10,
     ver client-feedback/home-full-bleed-spec.md) la columna de contenido
     baja de esos anchos en viewports pequenos y las lineas empezaban a
     provocar scroll horizontal de pagina - mismo tope aplicado a las 3
     por consistencia. */
  .page-hero__title--contacto::after,
  .page-hero__title--md::after,
  .page-hero__title--proyectos::after {
    max-width: 100%;
  }

  /* Sin spec movil en Figma - accepted-deviation: en movil el cuerpo de
     Contacto vuelve a ocupar el ancho completo en vez de solo una
     columna a la derecha (que no tendria sentido con el titulo/statement
     ya apilados arriba en 1 columna); las imagenes tampoco necesitan
     "escapar" de ninguna columna de titulo. */
  .contact-visual__body {
    margin-left: 0;
    max-width: none;
  }

  .manifesto__media-wrap,
  .contact-visual {
    width: auto;
    margin-left: 0;
  }

  /* Sin spec movil en Figma (frame fijo de escritorio) - accepted-deviation,
     mismo criterio que el resto del sitio: el bleed de 283px de
     .manifesto__body y el -366px de .manifesto__eye (pensados para un
     viewport ancho) no caben en movil - se resetea a flujo normal
     apilado (ojo primero, tamano reducido, despues titulo y texto a
     ancho completo). */
  .manifesto__body {
    margin-left: 0;
  }

  /* Alto actualizado 2026-07-30 (portado desde el prototipo) junto con la
     regla base de mas arriba (mismo motivo: nuevo SVG casi cuadrado, no
     ancho/bajo) - mismo ratio real del archivo, aplicado al ancho
     reducido de movil (128 * 270.61/251.13 = 137.93, redondeado). Tarea 7
     (mismo dia): tamano y posicion viven ahora en .manifesto__eye-frame
     (el marco/mascara, ver regla base mas arriba) en vez de en la propia
     imagen - en movil no hay pin sticky ni efecto de desaparición (el
     listener de scroll de main.js solo corre en escritorio/tablet, ver
     !isMobileViewport), asi que el marco aqui no necesita overflow:hidden
     activo, solo el tamano/posicion en flujo normal que ya tenia la
     imagen antes. */
  .manifesto__eye-frame {
    position: static;
    width: 128px;
    height: 138px;
    margin-bottom: var(--space-md);
  }

  /* Peticion del usuario 2026-07-25: mejorar la composicion del texto
     poetico del manifiesto en tablet - sin este limite, las lineas
     cortas (cada una con su propio <br> manual, ver nosotros.html)
     quedan sueltas dentro de una columna de ancho completo (~786px
     util en un iPad de 834px), sin la sensacion de bloque compacto e
     intencional que tienen en escritorio (donde la columna de texto
     mide de forma organica ~610px, acotada por su propio contenido
     dentro del layout de 2 columnas). Se fija un ancho similar aqui
     para que el efecto se sienta igual de compuesto en tablet. min()
     evita que estorbe en telefonos reales (390px), donde el contenedor
     ya es mas estrecho que este limite. */
  .manifesto__text {
    max-width: min(480px, 100%);
  }

  /* Mismo criterio: el bleed de .contact-form-section (96px de
     padding-left dentro de un ancho 100%+397px) y el anclaje absoluto de
     .contact-page__email (left:397px, pensado para la columna de
     contenido de escritorio) no tienen sentido con .contact-page ya
     apilado en 1 columna (ver .about-page/.contact-page mas arriba en
     este mismo bloque) - se resetean a flujo normal, ancho completo. */
  .contact-page__email {
    position: static;
    display: block;
    margin-bottom: var(--space-md);
  }

  .contact-form-section {
    flex-direction: column;
    width: auto;
    margin-left: 0;
    padding-left: 0;
  }

  .contact-form,
  .contact-form__info {
    width: 100%;
    max-width: none;
  }

  /* Columna de texto apilada arriba, galeria en cuadricula de 2 columnas
     en vez de 6 - mismo criterio que .projects-layout. El mapeo de
     columnas de la galeria (span-6/span-3/span-2) SI tiene spec real en
     Figma para movil, ver el comentario de .project-detail__gallery mas
     abajo. */
  .project-detail {
    grid-template-columns: 1fr;
    padding-top: calc(var(--header-height) + var(--space-xl));
  }

  /* En movil texto y galeria van apilados en 1 columna (ver arriba) -
     el "sticky" no tiene sentido en ese layout, se desactiva. Se resetea
     tambien el min-height dinamico de la regla base (pensado para anclar
     el boton "Ver todos los proyectos" al fondo de la pantalla en el
     layout de 2 columnas con sticky) - en movil, sin sticky, ese hueco
     dejaria un espacio vacio enorme antes de la galeria si el texto es
     corto. Con min-height:0 el boton simplemente queda justo debajo del
     texto (margin-top:auto de .project-detail__cta no tiene hueco que
     consumir), igual que el resto del contenido apilado. */
  .project-detail__intro {
    position: static;
    margin-bottom: var(--space-lg);
    min-height: 0;
  }

  /* Peticion cliente 2026-07-13: 20px de espacio entre la descripcion y
     el boton "Ver todos los proyectos" en movil (ajustado de 10px a 20px
     el mismo dia) - sin min-height dinamico (regla de arriba) el
     margin-top:auto de la regla base no tiene hueco que consumir y el
     boton quedaba pegado directamente al texto. */
  .project-detail__cta {
    margin-top: 20px;
  }

  /* Peticion cliente 2026-07-13: "no pueden quedar huecos vacios entre
     imagenes" en la galeria movil. Encontrado el mobile mockup real en
     Figma (con node-id equivocado en la URL que dio el cliente, pero
     localizable: frame "Proyecto_Slagharen" en movil, mal etiquetado
     "Home Mobile" #2 en el panel de capas, node 165:167) - SI hay spec
     movil, al contrario de lo que decia el comentario anterior de esta
     regla (ya obsoleto, retirado).
     El mapeo naive anterior (span-6->2 columnas, span-3 Y span-2 ->1
     columna cada uno) podia dejar un hueco: con grid-auto-flow por
     defecto (sparse), un span-6->2-columnas que cae justo despues de un
     numero IMPAR de piezas de 1 columna empieza en una fila nueva sin
     rellenar la celda suelta de la fila anterior - eso deja el hueco
     que describe el cliente.
     El Figma real de Slagharen (10 piezas: span-6,span-3,span-3,span-2,
     span-2,span-2,span-3,span-3,span-3,span-3) NO usa un mapeo tan
     simple - revela una regla mas precisa por contenido:
       - span-6 (ancho completo en escritorio) -> ancho completo en movil.
       - span-3 (mitad en escritorio, ya pensada en pareja) -> mitad en
         movil TAMBIEN, en pareja (verificado: bolsa+patron, y las 2
         parejas finales de Slagharen quedan exactamente asi en Figma).
       - span-2 (un tercio en escritorio, 3 por fila - demasiado estrecho
         en un telefono) -> ancho completo en movil, cada una en su
         propia fila (verificado: los 3 carteles Apollo/Gold Rush/vaquero
         de Slagharen, span-2 en escritorio, aparecen a ancho completo
         y apilados en el Figma movil, no en trio ni en pareja).
     grid-auto-flow:dense como red de seguridad adicional (no el
     mecanismo principal): si en algun otro proyecto una secuencia
     concreta de span-3 no vinera en pareja (numero impar), dense
     recoloca la siguiente pieza que si encaje en la celda suelta en vez
     de dejarla vacia - nunca deja un hueco intermedio sea cual sea el
     orden real de cada proyecto. */
  .project-detail__gallery {
    grid-template-columns: repeat(2, 1fr);
    grid-auto-flow: dense;
    padding-right: 0;
  }

  .project-detail__media--span-6,
  .project-detail__media--span-2 { grid-column: span 2; }
  .project-detail__media--span-3 { grid-column: span 1; }

  /* Titulo apilado arriba, mosaico en 1 sola columna (las 2 columnas de
     escritorio una detras de otra, no intercaladas), mismo criterio que
     el resto del sitio para este tipo de fallback.
     minmax(0, 1fr), no "1fr" a secas: la linea decorativa de "Proyectos"
     (.page-hero__title--proyectos::after, 230px) empujaba la pista a
     230px+padding tras ampliar el margen lateral a 100px (2026-07-10,
     ver client-feedback/home-full-bleed-spec.md), provocando scroll
     horizontal en viewports estrechos. */
  .projects-layout {
    grid-template-columns: minmax(0, 1fr);
    padding-top: calc(var(--header-height) + var(--space-xl));
  }

  /* En movil el titulo y el mosaico van apilados en 1 columna (ver
     arriba) - el "sticky" del titulo no tiene sentido en ese layout
     (no hay 2 columnas independientes que recorrer), se desactiva y
     vuelve al flujo normal. */
  .projects-layout__title {
    position: static;
    margin-bottom: var(--space-lg);
  }

  .project-mosaic {
    flex-direction: column;
    padding-right: 0;
  }

  /* Peticion cliente 2026-07-13: en movil se quita el efecto hover/tap
     del mosaico de Proyectos (ver mas arriba, .project-mosaic__frame:hover
     etc.) - el texto (titulo/subtitulo) se ve SIEMPRE debajo de su imagen,
     sin necesitar tap previo, y el primer tap ya navega a la pagina del
     proyecto (enlace normal, sin preventDefault - ver main.js).

     El caption ya no puede ir position:absolute/top:98% (asi funciona en
     escritorio, pensado para el hueco que deja el scale(0.96) del hover):
     en movil no hay scale, asi que ese 98% cae DENTRO de la imagen todavia
     a tamano completo (texto pintado sobre sus ultimos px) y un margen
     fijo (28px) no basta para titulos que ocupan 2 lineas ("Abu Dhabi
     Internacional Boat Show", "Parque de Atracciones Madrid"), que
     entonces se solapan con la pieza siguiente. Solucion: sacar el caption
     del position:absolute y dejarlo en flujo normal debajo de la imagen -
     su alto pasa a depender del texto real (1 o 2 lineas) en vez de un
     numero fijo adivinado, y por tanto SIEMPRE tiene su propio hueco. La
     relacion de aspecto (cuadrado/vertical) se mueve del contenedor a la
     imagen, ya que el contenedor ahora crece con el contenido (imagen +
     texto), no al reves. */
  .project-mosaic__item {
    aspect-ratio: auto;
    margin-bottom: 0;
    transition: none;
  }

  .project-mosaic__frame {
    height: auto;
    display: flex;
    flex-direction: column;
  }

  .project-mosaic__item--square img {
    aspect-ratio: 1 / 1;
    height: auto;
  }

  .project-mosaic__item--portrait img {
    aspect-ratio: 475 / 633;
    height: auto;
  }

  .project-mosaic__frame:hover img,
  .project-mosaic__frame:focus-visible img,
  .project-mosaic__frame.is-active img {
    transform: none;
  }

  .project-mosaic__caption {
    position: static;
    opacity: 1;
    transform: none;
    padding-top: 8px;
    padding-bottom: 16px;
  }
}

/* ---------- Margenes horizontales ampliados a 100px, todas las paginas
   (peticion cliente 2026-07-10, ver client-feedback/home-full-bleed-spec.md)
   ----------
   Validado primero solo en Home, extendido despues a el resto de paginas
   (misma estructura header/main/footer en todas: un unico `.container` por
   pagina dentro de `main`, mas el de `.site-footer`). max-width:none quita
   el tope de 1280px para que el contenido ocupe pantalla completa en
   viewports anchos, con un margen fijo de 100px a cada lado.

   Peticion cliente 2026-07-16: el header (.site-header__bar, que tambien
   lleva la clase .container) se habia dejado fuera a proposito (ver
   historial de esta regla) para no tocar el token --content-padding
   compartido - eso hacia que el logo/EN-ES quedaran a 1280px+50px
   (centrado) mientras "Proyectos" y el resto de titulos de seccion ya
   usaban este mismo full-bleed de 100px, dejando el logo mas metido hacia
   dentro que "Proyectos" en viewports anchos. Se anade `.site-header
   .container` aqui para que el header comparta EXACTAMENTE el mismo
   margen que el resto de la web, en vez de re-crear la regla aparte. */
main .container,
.site-header .container,
.site-footer > .container {
  max-width: none;
  margin-inline: 0;
  padding-inline: 100px;
}

/* A 100px fijo, viewports movil se quedan con muy poco ancho util (p.ej.
   190px en 390px de pantalla): titulos de 48px (Servicios/Nosotros/
   Contacto/Proyectos) dejan de caber en una linea y las lineas
   decorativas de ancho fijo (230-339px) fuerzan scroll horizontal (ver
   fix de grid-template-columns: minmax(0, 1fr) mas arriba). Se reduce a
   --space-md (24px) por debajo de 860px, mismo breakpoint que el resto
   del sitio. */
@media (max-width: 860px) {
  main .container,
  .site-header .container,
  .site-footer > .container {
    padding-inline: var(--space-md);
  }
}
