/* ============================================================================
   BAHÍA MAR — Capa "eco / naturaleza"  ·  v7
   Validado en vivo sobre https://landmass.urbatrix.com — 2026-08-05

   ── EL PRINCIPIO ──────────────────────────────────────────────────────────
   Una sombra es información sobre algo que está FUERA del cuadro.
   Un PNG recortado es un objeto DENTRO del cuadro, compitiendo con la villa.

   En un dashboard de venta todo lo que compite con la foto resta. Por eso la
   v7 se queda solo con sombras: la naturaleza entra como luz filtrada, no
   como elementos colocados.

   v7.2 sube el volumen de la sección 12. La v7.1 caliente-pero-blanca se leía
   corporativa: a 300px de tarjeta, un tinte del 2% no es material, es blanco.
   Ahora el verde está en TODAS las capas —suelo de la sección, superficie de
   la tarjeta, hojas, chrome y tinta— en vez de gritar en una sola. Lo que NO
   se toca: los aros de estado del sitio (ver el ⚠️ del filo en 12.4).

   Y la vegetación va en UNA DE CADA TRES tarjetas, no en todas: en todas se
   convertía en papel pintado (reparto en el JS, `hojaDe`). Esa misma tarjeta
   es la que va en salvia; las otras dos van en blanco. Hoja y color son la
   misma decisión — el porqué, en 12.1.

   El contenedor del grid se queda con el arena de la página: hubo un lavado
   verde y se retiró (12.0).

   v7.1 añade la sección 12: el GRID DE UNIDADES entra en la capa. Hasta ahora
   sólo estaban las bandas de tipología, y el catálogo —que es donde el usuario
   pasa el rato— seguía en tarjetas blancas con chrome gris azulado. Mismo
   material, mismas sombras, sin animarlas (ver 12.2).

   v7 cambia respecto a v6:
     · FUERA todos los PNG recortados: las frondas del hero, las enredaderas de
       las villas y el filodendro de la banda verde. Se leían como sticker
       pegado por muy bien alineados que estuvieran
     · el panel de texto de cada villa recibe una sombra vegetal contenida
     · la banda verde cambia el filodendro flotante por follaje de fondo con
       máscara de degradado
     · el HERO se queda sin capa: `.fg-hero::after` es del sitio y la capa lo
       llevaba rompiendo desde la v5 sin que se viera nada a cambio (sección 1)
     · se fija UNA dirección de luz para todo el sitio (--eco-sol-x)
     · grano de papel en los paneles (SVG inline, 0 peticiones, 0 KB)

   Assets huérfanos tras la v7 — se conservan por si se recupera algo, pero
   ya no los usa nadie: eco-fronda, eco-hiedra, eco-corazones, eco-potos.

   ── ASSETS ────────────────────────────────────────────────────────────────
   Van en /images/eco/.

     eco-sombra-fronda.webp    1400×1056  sombra de palma    → villas 1, 3 y 5
     eco-sombra-monstera.webp  1400×1056  sombra de monstera → villas 2 y 4
     eco-sombra-palma.webp     1600×900   palmera densa      → banda verde
     eco-follaje-banda.webp    1536×768   follaje denso      → banda verde

   Las dos sombras nuevas salen de Magnific y se procesan antes de entrar:
   a gris (una sombra tiene que ser neutra) y con el blanco empujado a 255
   puro. Eso último NO es opcional — ver trampa 3.

   ── SELECTORES REALES ─────────────────────────────────────────────────────
     .fg-hero          hero (¡ojo! vive dentro de un slider — ver trampa 6)
     .fg-type-band     cada banda de tipología (Villa A…E)
     .fg-card-inner    contenedor de la banda (grid 2 columnas, overflow:hidden)
     .fg-card-img      mitad foto
     .nav-center       bloque "0 OF 248 UNITS SOLD" del header
   ============================================================================ */

:root{
  --eco-ease:  cubic-bezier(.16, 1, .3, 1);
  --eco-deep:  #1E3D33;
  --eco-green: #2F5D50;
  --eco-sand:  #EFEAE0;

  /* ── EL FONDO DE LA PÁGINA ───────────────────────────────────────────────
     El sitio venía con #f2f5f8, un gris azulado frío. Con el panel de las
     villas en arena, ese azul se lee como otro material: el recuadro claro
     deja de parecer parte de la página y parece un cartón pegado encima.

     Esto NO es un tono nuevo: es el MISMO #f2f5f8 en la familia cálida. La
     luminancia es la de siempre (~96%), lo único que cambia es el matiz, así
     que todo lo que estaba calculado sobre el fondo —contrastes de texto,
     bordes, las sombras de las tarjetas blancas— sigue valiendo igual.

     ⚠️ Tiene que quedar entre los dos extremos del degradado del panel
     (#EFEAE0 → #F7F4EE): un pelo más claro que el arranque y un pelo más
     oscuro que el final. Esa es la relación que la sección 4 dio por buena —
     panel ligeramente más profundo que la página, disolviéndose en ella. Si
     lo igualas al #F7F4EE, el panel desaparece y las bandas pierden el
     recuadro; si lo bajas al #EFEAE0, el degradado se invierte y el panel se
     lee más claro que la página, que es justo al revés de lo diseñado.      */
  --eco-page: #F5F1E9;

  /* ── EL SOL DEL PROYECTO ─────────────────────────────────────────────────
     Alto y por la izquierda: las sombras caen hacia abajo y a la derecha.
     TODO lo que proyecte sombra tiene que caer con esta misma inclinación. Si
     cada sombra sugiere un sol distinto, el conjunto se lee como collage
     aunque nadie sepa decir por qué — y eso es lo que separa "premium" de
     "banco de imágenes".

     Es la fracción de la altura de la sombra que se desplaza a la derecha:
     0.4 ≈ un sol a unos 68° sobre el horizonte.

     ⚠️ Va en el ASSET además de en el CSS: las dos sombras nuevas se generaron
     pidiendo luz desde arriba-izquierda. Si regeneras una, pídelo igual o
     apuntará a otro sol.                                                    */
  --eco-sol-x: .4;

  /* Grano de papel. feTurbulence inline: ni peticiones ni bytes de imagen.
     Convierte el blanco plano en papel de algodón, que es el material que
     asocias con lo natural y lo caro a la vez. */
  --eco-grano: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");

  --eco-sombra:          url("/images/eco/eco-sombra-palma.webp");
  --eco-sombra-fronda:   url("/images/eco/eco-sombra-fronda.webp");
  --eco-sombra-monstera: url("/images/eco/eco-sombra-monstera.webp");
  --eco-follaje:         url("/images/eco/eco-follaje-banda.webp");
}


/* ══ 1. EL HERO NO LLEVA CAPA — Y NO PUEDE LLEVARLA EN `::after` ═══════════
   ⚠️ `.fg-hero::after` ES DEL SITIO. Lo usa para el difuminado blanco que
   funde el hero con el contenido de abajo (style.css: `bottom:-1px;
   height:6%; linear-gradient(to top, #fff ...)`).

   La capa lo estuvo pisando desde la v5 y el resultado fue doble:

     · la sombra NUNCA se vio. El sitio declara `height:6%` y la capa solo
       `inset:-8%`; en la cascada sobrevive el height del sitio, así que el
       pseudo-elemento acababa midiendo 42px colocado en top:-56px, o sea
       fuera de la caja y recortado por el overflow:hidden del hero
     · y el difuminado del sitio desaparecía sin que nadie lo notara

   Medido: 0,0% de los píxeles del hero cambiaban al activar o desactivar la
   capa. Ni pintándolo de rojo sólido a opacidad 1.

   Es la trampa 4 otra vez. Si algún día se quiere efecto en el hero, va en un
   ELEMENTO PROPIO inyectado por el JS, nunca en este pseudo-elemento.

   Dicho lo cual, tampoco tiene mucho sentido: el hero es 60% cielo, y una
   sombra proyectada sobre el cielo no existe. Las sombras caen sobre
   superficies. El resto es la villa, que es el activo de venta.            */

/* Deriva: es lo que convierte "textura de fondo" en "hay un árbol ahí fuera".
   Cada sombra lleva un ritmo distinto y NINGUNO es múltiplo de otro (primos),
   así que no se sincronizan nunca y el ojo no llega a detectar el loop.

   ── POR QUÉ ESTO CAMBIÓ ──────────────────────────────────────────────────
   La versión anterior era `from 0 → to translate(-1.4%, 1%) scale(1.04)` en
   29-43s. Con `alternate` eso son 58-86s de ciclo completo para recorrer 1,4%
   de 371px: **5 píxeles en un minuto y medio**. No es que fuera sutil, es que
   no existía — nadie que abriera la página iba a ver moverse nada.

   Tres cambios, y ninguno es "subirle el volumen":

   1. EL CAMINO NO ES RECTO. Dos keyframes interpolan en línea recta y el ojo
      lee "panorámica", que es cámara moviéndose, no viento. Con cuatro pasos
      intermedios de dirección desigual —una racha, una pausa, un regreso que
      no vuelve del todo por donde vino— se lee como aire.

   2. HAY GIRO, con el pivote en la esquina por la que la sombra ENTRA
      (`transform-origin` en `.eco-shade i`). Una fronda no se desplaza en
      bloque: cuelga de una rama que está fuera del cuadro y se mece desde
      ahí. Sin el giro, cualquier amplitud que se note se lee como que la
      imagen resbala.

   3. ⚠️ TODOS LOS DESPLAZAMIENTOS SON ≤ 0 Y EL GIRO SIEMPRE NEGATIVO.
      No es estética, es la trampa de los offsets otra vez. El asset viene
      cortado por sus bordes superior e izquierdo y ese corte sólo se lee bien
      si coincide con el borde del panel. Cualquier valor positivo mete el
      corte hacia dentro y lo deja a la vista, flotando.

      La versión vieja ya tenía `1%` en la Y —hacia ABAJO— y se lo podía
      permitir porque 1% de 280px son 2,8px. Con la amplitud nueva eso serían
      12px de corte a la vista. Así que la Y se invierte: el viento sólo tira
      hacia arriba y hacia la izquierda, o sea que sólo puede esconder más el
      corte, nunca destaparlo. Por construcción, no por medida.

      El giro, por lo mismo, sólo en sentido antihorario: con el pivote en la
      esquina superior izquierda, el horario baja el borde de arriba y abre
      una cuña. El antihorario lo sube.

   ── LA AMPLITUD SE FIJÓ MIDIENDO, NO A OJO ───────────────────────────────
   El recorrido en píxeles es lo único que decide si esto se ve o no, y no se
   deduce de los porcentajes: dependen del ancho de cada sombra, que cambia
   por variante (45-52% del panel).

   Primer intento a -2,2%: 8,2px de recorrido en 19s. Medido, no estimado — y
   con ese número delante está claro que no llega. Son 0,4px por segundo: si
   te quedas mirando el panel lo notas, y si no, no.

   A -4,6% son ~17px, que sobre una sombra difusa se lee como brisa sin pasar
   a "hay algo animándose ahí". Por encima de eso la sombra empieza a entrar y
   salir del cuadro y el ojo se va detrás, que es justo lo que esta capa lleva
   evitando desde la v3.

   ── EL MOVIMIENTO ES LATERAL ─────────────────────────────────────────────
   De lado a lado, no en diagonal. La Y y la escala se quedan en lo justo para
   que no se lea como una diapositiva deslizándose: son el cabeceo, no el
   gesto. Lo que se tiene que ver es la fronda yendo y viniendo.

   El `alternate` del `animation` es lo que hace el vaivén: 0% es un extremo y
   100% el otro, y la sombra se pasa la vida entre los dos. Sin `alternate`
   saltaría del final al principio en un fotograma y se vería el corte.

   ── TERCERA CALIBRACIÓN ──────────────────────────────────────────────────
   -2,2% → 8px: no se veía.   -6,2% → 23px: se veía, pero había que buscarlo.
   -9%   → 33px, y con los ritmos más cortos salen ~2,5px por segundo, que es
   donde el movimiento se lee sin tener que fijarse.

   Aquí es donde conviene parar. El límite no es el recuadro —hay sitio— sino
   que la sombra recorre un tercio de su propio ancho: pasado eso deja de
   mecerse y empieza a entrar y salir del cuadro, y una sombra que aparece y
   desaparece ya no es una planta al otro lado de la ventana.

   El giro se queda en -0,95°, por debajo de lo que subió la traslación, y a
   propósito: la traslación es segura por construcción (sólo puede esconder
   más el corte) y el giro no —con el pivote arriba a la izquierda, el borde
   superior se descuelga hacia la derecha. Los -1,4% de Y compensan ese
   descuelgue. Si algún día subes el giro, sube la Y negativa con él.        */
@keyframes ecoDeriva{
  0%   { transform:translate3d(0, 0, 0)          rotate(0)       scale(1); }
  22%  { transform:translate3d(-2.6%, -.8%, 0)   rotate(-.36deg) scale(1.02); }
  47%  { transform:translate3d(-6.2%, -.35%, 0)  rotate(-.8deg)  scale(1.045); }
  71%  { transform:translate3d(-4.5%, -1.9%, 0)  rotate(-.32deg) scale(1.032); }
  100% { transform:translate3d(-9%,   -1.4%, 0)  rotate(-.95deg) scale(1.075); }
}


/* ══ 2. SOMBRA VEGETAL EN EL PANEL DE LA VILLA ═════════════════════════════
   Cae sobre el recuadro claro, nunca sobre la foto.

   Va como ITEM DEL GRID, en la columna del texto — el mismo mecanismo que
   usaban las enredaderas de la v6 y por la misma razón: el sitio maqueta la
   banda con `grid-template-columns:1fr 1fr` y mete el texto en la columna 1
   o 2 según la paridad. Pidiendo esa columna, la sombra no puede invadir la
   foto ni hace falta calcular un solo píxel. El lado lo escribe el JS en
   `data-eco-side`.

   ⚠️ `z-index:-1` la deja DETRÁS del texto. Sin eso se pinta encima: el body
   del sitio es `position:static`, así que un hermano posicionado con z-index
   auto o 0 le gana en orden de pintado.                                    */
.fg-type-band{ --eco-sombra-fuerza:.07; }

/* ⚠️ DOS CAPAS, y no es decorativo.

   La caja exterior es el grid item y NO se mueve: marca el límite de la
   celda y recorta. La interior es la que lleva la imagen y la deriva.

   Si se anima el grid item directamente, el `scale(1.04)` del balanceo
   ensancha su bounding box ~28px y la sombra se mete en la columna de la
   foto. Con `z-index:-1` no llega a verse —queda detrás del render— pero
   `invadeFoto` salta, y con razón: basta que alguien suba el z-index para
   que aparezca encima de la villa.

   El `mix-blend-mode` y la opacidad van en la capa EXTERIOR a propósito: así
   se compone el grupo entero de una vez y se mezcla con el fondo del panel.
   Puestos en la interior, el multiply se resolvería contra el contenedor
   transparente y perdería el fondo arena.                                  */
/* ⚠️ `position:relative` NO es opcional. Sin él, `.eco-shade i` se posiciona
   y se dimensiona contra `.fg-card-inner` —que es la BANDA entera, 1425px—
   en vez de contra el panel. Los porcentajes salen del doble de lo que
   parecen: `52%` daba 741px en un panel de 713.

   El `overflow:hidden` lo tapaba, así que la sombra no se salía; solo salía
   al doble de escala. De ahí la mancha que ocupaba todo el cuadro.         */
.eco-shade{
  position:relative;
  grid-row:1 / -1;
  z-index:-1;
  overflow:hidden;
  pointer-events:none;
  mix-blend-mode:multiply;
  opacity:var(--eco-sombra-fuerza);
}

/* ⚠️ LA CAJA INTERIOR **ES** LA SOMBRA, no el panel.

   Con `inset:0` + `background-size:cover` la imagen se estiraba hasta cubrir
   un panel de 843×620: cada hoja medía medio recuadro y el conjunto dejaba de
   leerse como "hay una planta ahí fuera" para leerse como una mancha que
   ocupaba el cuadro entero.

   Y hay una segunda razón, menos obvia: si la caja es el panel, **la máscara
   también se calcula sobre el panel**. Una sombra que ocupa el 38% cae
   entonces dentro de la zona ya desvanecida y desaparece. Pasó con la
   monstera de Villa B: reducida al tamaño correcto, no se veía.

   Dimensionando la caja a la sombra, la máscara opera sobre la sombra y la
   escala se controla con un solo número: --eco-sombra-ancho.               */
.eco-shade i{
  display:block;
  position:absolute;
  top:var(--eco-sombra-y, 0);
  left:var(--eco-sombra-x, 0);
  width:var(--eco-sombra-ancho, 46%);
  aspect-ratio:1.3258;                 /* ratio de los dos assets: 1400×1056 */

  background-repeat:no-repeat;
  background-size:100% 100%;

  /* El pivote del balanceo, en la esquina por la que entra la sombra. Es la
     rama: está fuera del cuadro y lo que se mece es lo que cuelga de ella.
     Con el pivote en el centro (el de por defecto) el giro mueve las cuatro
     esquinas y la sombra se lee como una imagen que rota, no como una hoja.

     Para las variantes con offset negativo la esquina cae ya fuera del panel,
     que es todavía mejor: el pivote queda literalmente fuera del encuadre. */
  transform-origin:0% 0%;

  /* Desvanece la salida de la sombra en diagonal, siguiendo al sol. Va sobre
     la caja de la sombra, así que los porcentajes son de la sombra. */
  mask-image:linear-gradient(145deg, #000 0%, rgba(0,0,0,.82) 48%, transparent 94%);

  animation:ecoDeriva var(--eco-ritmo, 37s) ease-in-out infinite alternate;
  will-change:transform;
}

[data-eco-side="left"]  .eco-shade{ grid-column:1; }
[data-eco-side="right"] .eco-shade{ grid-column:2; }

/* La sombra entra por la esquina superior, que es el borde por el que la corta
   el encuadre: así se lee como que viene de fuera y deja el resto limpio.

   No se espeja según el lado, a propósito: espejarla movería el sol al otro
   lado en media página. Lo único que cambia entre bandas es escala y posición. */
/* ⚠️ La fuerza va por especie, no global. A la misma opacidad la monstera pesa
   bastante más que la fronda: son masas sólidas frente a líneas finas con
   mucho aire entre ellas. Igualadas al 7% la fronda casi desaparecía y la
   monstera se leía bien. Lo que hay que igualar es el peso PERCIBIDO. */
[data-eco-shade="fronda"] .eco-shade{ --eco-sombra-fuerza:.11; }
[data-eco-shade="fronda"] .eco-shade i{
  background-image:var(--eco-sombra-fronda);
  --eco-sombra-ancho:52%;
}

[data-eco-shade="monstera"] .eco-shade{ --eco-sombra-fuerza:.07; }
[data-eco-shade="monstera"] .eco-shade i{
  background-image:var(--eco-sombra-monstera);
  --eco-sombra-ancho:50%;
}

/* Tercera banda: mismo asset que la primera, más pequeña y desplazada. Con dos
   sombras y cinco villas, esto —escala + corte + fuerza— es lo único que
   distingue una banda de otra. Al ser menor necesita algo más de fuerza para
   pesar lo mismo.

   ⚠️ LOS OFFSETS SOLO PUEDEN SER 0 O NEGATIVOS. El asset trae la sombra ya
   cortada por sus bordes superior e izquierdo —se generó así, entrando desde
   la esquina— y ese corte es lo que la hace leerse como que viene de fuera del
   cuadro. Un offset positivo la mete hacia dentro del panel y deja el corte a
   la vista, flotando: con `--eco-sombra-x:8%` quedaban 57px de hueco contra el
   borde izquierdo y la sombra parecía un recorte pegado.

   En negativo entra MÁS cortada, que es justo lo que diferencia esta banda de
   la primera sin necesidad de otro asset.                                    */
[data-eco-shade="fronda-alt"] .eco-shade{ --eco-sombra-fuerza:.13; }
[data-eco-shade="fronda-alt"] .eco-shade i{
  background-image:var(--eco-sombra-fronda);
  --eco-sombra-ancho:48%;
  --eco-sombra-x:-7%;
  --eco-sombra-y:-11%;
}

/* Cuarta banda: la monstera, más pequeña y entrando cortada por la esquina.
   Sube menos que la fronda al hacerse pequeña (.07 → .085 frente a .11 → .13)
   porque es masa sólida: satura antes y a poco que se le suba ensucia el arena.

   ⚠️ Ojo con el eje del recorte: `x` es un % del ANCHO del panel y `y` un % de
   su ALTO, así que el mismo número corta distinto en cada eje. Estos dos (-9/-8)
   quitan ~20% de la sombra por cada lado; parecen asimétricos y no lo son. */
[data-eco-shade="monstera-alt"] .eco-shade{ --eco-sombra-fuerza:.085; }
[data-eco-shade="monstera-alt"] .eco-shade i{
  background-image:var(--eco-sombra-monstera);
  --eco-sombra-ancho:46%;
  --eco-sombra-x:-9%;
  --eco-sombra-y:-8%;
}

/* Quinta banda: la fronda más pequeña de las tres, y la que entra más metida
   por el lateral. El corte va casi todo en `x` —al revés que fronda-alt, que
   lo lleva en `y`— y ese cambio de eje es lo que las separa: a escalas tan
   parecidas, dos frondas cortadas igual se leen como la misma sombra repetida.

   Offsets negativos, como en fronda-alt y por lo mismo. Y no bajar más del 44%
   de ancho: por debajo, la parte densa cae ya dentro de la zona que desvanece
   la máscara y la sombra se apaga entera. */
[data-eco-shade="fronda-corta"] .eco-shade{ --eco-sombra-fuerza:.13; }
[data-eco-shade="fronda-corta"] .eco-shade i{
  background-image:var(--eco-sombra-fronda);
  --eco-sombra-ancho:45%;
  --eco-sombra-x:-11%;
  --eco-sombra-y:-4%;
}


/* ══ 3. GRANO DE PAPEL ═════════════════════════════════════════════════════
   Al 3,5% no se ve; se nota. Quita el plano digital del fondo claro sin
   añadir un solo elemento ni una petición.

   Va en `.fg-card-inner::after` — comprobado que el sitio no lo usa (sí usa
   `.fg-card-img::before` para su degradado, ver trampa 4).                 */
.fg-type-band .fg-card-inner{ overflow:hidden; }

.fg-type-band .fg-card-inner::after{
  content:"";
  position:absolute; inset:0; z-index:-1;
  pointer-events:none;
  background-image:var(--eco-grano);
  background-size:180px 180px;
  mix-blend-mode:multiply;
  opacity:.035;
}


/* ══ 4. NEUTRO ARENA ═══════════════════════════════════════════════════════ */
.fg-type-band .fg-card-inner{
  background:linear-gradient(150deg, var(--eco-sand) 0%, #F7F4EE 48%, rgba(247,244,238,0) 78%);
}


/* ══ 5. BANDA DE COMPROMISO ════════════════════════════════════════════════ */
.eco-band{
  position:relative; overflow:hidden;
  background:linear-gradient(115deg, #16302A 0%, var(--eco-deep) 55%, #27503F 100%);
  color:#F2EFE7;
  padding:64px 7% 68px;
  margin:0;
}
/* Aquí sigue el asset denso: sobre el verde funciona como textura de fondo,
   no como sombra direccional, así que no pelea con el sol del proyecto.

   Va girado 180°: el render original tiene la masa de palmera abajo a la
   derecha y las frondas abriéndose hacia arriba. Girado, nace arriba a la
   izquierda y cae hacia el centro, que es por donde entra la luz en el resto
   de la capa (--eco-sol-x) y por donde entran también las sombras de las
   villas. `scale(-1,-1)` es lo mismo que `rotate(180deg)` en un rectángulo.

   ⚠️ `inset:-9%` NO ES DECORATIVO: ES EL MARGEN PARA MECERSE.
   Con `inset:0` la textura mide exactamente la banda, así que en cuanto se
   mueve un píxel deja una franja sin overlay contra el borde — y como debajo
   hay un degradado verde y encima un `mix-blend-mode:overlay`, esa franja se
   ve como una raya más clara a lo largo del canto. Con sobrante por los
   cuatro lados hay de dónde tirar, y el `overflow:hidden` de `.eco-band`
   recorta lo que sobra.

   Empezó en -6% y se subió midiendo: el recorrido son 80,6px y el sobrante
   eran 86px, o sea 5,8px de margen. Funcionaba, pero es un elemento cuya caja
   cambia de alto en 1600px y otra vez en móvil, y un margen de 5px no
   sobrevive a eso sin que nadie se entere. A -9% son ~130px contra 80,6.

   ⚠️ Y EL VOLTEO VA DENTRO DE LOS KEYFRAMES. `transform` es una sola
   propiedad: si la animación declara `transform` y la regla declara otro, la
   animación gana y el `scale(-1,-1)` desaparece — la palmera se daría la
   vuelta al empezar a moverse. Por eso el flip viaja en cada fotograma.

   Ojo con el orden: con el `scale(-1,-1)` aplicado primero, el eje queda
   espejado, así que un `translateX` POSITIVO mueve la imagen hacia la
   IZQUIERDA en pantalla. Da igual para el resultado —hay margen por los
   cuatro lados— pero si algún día lo tocas, no te fíes del signo.           */
.eco-band::after{
  content:"";
  position:absolute; inset:-9%;
  pointer-events:none;
  background-image:var(--eco-sombra); background-size:cover;
  transform:scale(-1, -1);
  mix-blend-mode:overlay; opacity:.22;
  animation:ecoBandaPalma 29s ease-in-out infinite alternate;
  will-change:transform;
}

/* La palmera de la banda: el mismo vaivén lateral que las villas, más lento
   —es una masa mucho mayor y las masas grandes se mueven despacio— y sin
   giro, porque aquí el asset funciona como textura y no como una fronda
   concreta colgando de un sitio.

   29s, primo y distinto de los de las villas (11/13/17/19/23), del follaje de
   esta misma banda (31) y del hero (47). Lo mismo de siempre: entre primos
   distintos no hay múltiplo, así que nunca se sincronizan y el ojo no llega a
   pillarle el bucle.

   El sobrante del `inset` es el techo del recorrido. No subas este 5% sin
   subir el `inset` primero, o la textura se despega del canto y aparece la
   raya clara. La cuenta se hace midiendo, no de cabeza: el porcentaje del
   `translate` es sobre el ancho de la CAJA (que ya viene inflada por el
   inset) y el del `inset` sobre el ancho de la BANDA — no son la misma base. */
@keyframes ecoBandaPalma{
  0%   { transform:scale(-1,-1) translate3d(0, 0, 0); }
  38%  { transform:scale(-1,-1) translate3d(2.4%, .7%, 0); }
  70%  { transform:scale(-1,-1) translate3d(1.8%, -.9%, 0); }
  100% { transform:scale(-1,-1) translate3d(5%, .4%, 0); }
}
/* Follaje de fondo, denso a la derecha y disuelto hacia la izquierda.

   Antes esto era un filodendro recortado apoyado abajo a la derecha, y tenía
   el mismo problema que las enredaderas de las villas: un PNG flotando —a
   `bottom:4%`, suspendido sin apoyarse en nada— que se leía como objeto
   pegado. Es la última pieza donde quedaba ese patrón.

   El asset ya viene medio resuelto: se generó pidiendo que la masa vegetal se
   disolviera en negro hacia la izquierda, así que la máscara solo remata un
   degradado que ya existe en la imagen. Por eso puede ser tan suave sin que
   se vea el corte.

   ⚠️ LLENA EL ALTO DE LA BANDA (`auto 100%`), y eso condiciona el asset.

   El tamaño al que se ven las hojas y la cobertura vertical son la misma
   variable: bajando el `background-size` las hojas encogen pero dejan hueco
   arriba y abajo, y el borde de la imagen se ve como un corte recto contra el
   verde. Se intentó taparlo con una segunda máscara y `mask-composite:
   intersect` —que además falla en silencio si `-webkit-mask-composite` va
   después, porque en Chrome es la misma propiedad con valores legacy y pisa
   `intersect` con `source-in`— y luego con alpha desvanecido en el propio
   asset. Ninguna de las dos cierra el borde de forma limpia.

   La solución no estaba en el CSS: **el plano lo decide el prompt**. El asset
   se regeneró como plano general, con las hojas ya pequeñas dentro del
   encuadre, y así llena el alto al 100% sin ampliarse. Si regeneras la imagen
   y las hojas salen en primer plano, no lo arregles con `background-size` —
   vuelve a pedir el plano general.

   `background-repeat:no-repeat` es obligatorio: con `auto 100%` la imagen no
   llena la caja a lo ancho y sin eso se tesela.

   ⚠️ La máscara horizontal deja el 58% izquierdo limpio. El titular llega al
   36% y los stats al 62%, así que estos rozan la zona degradada — pero ahí la
   imagen es casi negra y el texto claro, o sea que el contraste SUBE. Medido
   sobre píxeles reales. El mínimo exigible es 4,5.

   ── EL VAIVÉN DEL FOLLAJE SOLO PUEDE IR HACIA LA DERECHA ─────────────────
   Y esta vez el motivo no es el corte del asset, es cómo está anclado.

   El follaje va a `background-position:right center` con `auto 100%`: llena
   el alto justo y se pega al borde derecho, sin sobrar por ningún lado. O sea
   que CADA dirección tiene su forma de romperse:

     · hacia arriba o abajo → asoma el canto horizontal de la imagen contra el
       verde, que es exactamente el corte recto que costó cuatro intentos
       quitar (ver arriba);
     · hacia la izquierda   → el borde derecho se despega y deja una franja de
       verde limpio justo en la zona MÁS densa del follaje: el sitio donde más
       se nota.

   Hacia la derecha, en cambio, no hay nada que destapar: lo que sale por el
   canto derecho lo recorta `.eco-band`, y lo que se descubre por la izquierda
   es zona que la máscara ya tiene desvanecida a casi nada. Así que el
   recorrido va de 0 a +2%, y el `alternate` hace el resto.

   No hace falta margen extra, y por eso no se lo doy: agrandar la caja movería
   los porcentajes de la máscara —que se calculan sobre ELLA— y arrastraría el
   degradado hacia el titular. La máscara se queda donde está.               */
.eco-band-foliage{
  position:absolute; inset:0;
  pointer-events:none;
  background-image:var(--eco-follaje);
  background-size:auto 100%;
  background-position:right center;
  background-repeat:no-repeat;
  animation:ecoBandaFollaje 31s ease-in-out infinite alternate;
  will-change:transform;

  /* Opaca en el borde derecho, transparente al 42%: el follaje ocupa poco más
     de la mitad derecha y el titular se queda en zona limpia. */
  mask-image:linear-gradient(to left, #000 0%, rgba(0,0,0,.9) 14%, transparent 42%);
}

/* 31s: primo, distinto del de la palmera de esta misma banda (29) y de los de
   las villas. Las dos capas de la banda tienen que ir a ritmos distintos o se
   mueven como un bloque, y entonces no es follaje meciéndose: es una foto
   deslizándose.

   El recorrido es el único de toda la capa que NO tiene un techo geométrico
   —hacia la derecha siempre hay sitio—, así que el límite es de lectura: a
   partir de ~4% la masa densa se despega visiblemente del canto derecho y se
   ve que es una imagen moviéndose, no hojas. */
@keyframes ecoBandaFollaje{
  0%   { transform:translate3d(0, 0, 0); }
  44%  { transform:translate3d(2%, 0, 0); }
  100% { transform:translate3d(3.4%, 0, 0); }
}

.eco-band-eyebrow{
  font-size:11px; letter-spacing:.24em; text-transform:uppercase;
  opacity:.72; margin:0 0 14px; font-weight:600;
}
.eco-band-title{
  font-size:clamp(24px, 3.4vw, 42px); line-height:1.15; font-weight:400;
  max-width:15ch; margin:0 0 40px;
  position:relative; z-index:2;
}
.eco-band-stats{
  display:flex; gap:clamp(22px, 3.2vw, 48px); flex-wrap:wrap;
  position:relative; z-index:2;
  /* Los deja en la zona degradada y no sobre la masa densa de follaje. Subió
     de 64 a 72: al mover la banda encima del footer pasó a llevar los márgenes
     laterales de la capa, se quedó más estrecha y «100%» caía a una segunda
     línea. Con el hueco algo menor, los tres vuelven a entrar en una. */
  max-width:72%;
}
.eco-band-stat b{
  display:block;
  font-size:clamp(30px, 4.4vw, 52px); font-weight:300; line-height:1;
  letter-spacing:-.02em; margin-bottom:8px;
}
.eco-band-stat span{
  font-size:11px; letter-spacing:.13em; text-transform:uppercase; opacity:.7;
}


/* ══ 6. CONTADOR ECO EN EL HEADER ══════════════════════════════════════════ */
.eco-counter{
  display:block;
  font-size:9.5px; letter-spacing:.09em; text-transform:uppercase;
  font-weight:600; white-space:nowrap;
  color:var(--eco-green); opacity:.78;
  margin-top:3px;
}
.eco-counter b{ font-weight:800; }


/* ══ 7. SOMBRAS DEL SITIO, ALINEADAS AL MISMO SOL ══════════════════════════
   Solo se tocan las sombras GRANDES. En una sombra de 1-3px de desenfoque el
   ángulo no se percibe y desplazarla solo la ensucia; donde sí se lee es en
   el pill del header, que flota 20px sobre la página.

   Es lo único que esta capa le sobreescribe al sitio. Si molesta, se borra
   este bloque entero y no se rompe nada.                                   */
nav > div{
  box-shadow:
    calc( 5px * var(--eco-sol-x))  5px  5px   rgba(0,0,0,.04),
    calc(18px * var(--eco-sol-x)) 18px 16px   rgba(0,0,0,.05),
    calc(41px * var(--eco-sol-x)) 41px 22.5px rgba(0,0,0,.04),
    calc(73px * var(--eco-sol-x)) 73px 30px   rgba(0,0,0,.02) !important;
}


/* ══ 8. RESPONSIVE ═════════════════════════════════════════════════════════
   La sombra vive en la columna del grid, así que encoge y crece con él sola.
   No hay ni un número que replicar: al desaparecer las plantas desaparece
   también todo el problema de gutters que arrastraba la capa desde la v3.  */
@media (max-width:900px){
  .eco-band-stats{ max-width:100%; }
  .eco-band-foliage{ opacity:.35; }

  /* El grid colapsa a una columna y la fila 1 pasa a ser la FOTO. La sombra
     pide `grid-row:1/-1`, o sea que cubriría también el render de la villa.
     Se limita a la fila del texto y se baja la fuerza. */
  .eco-shade{ grid-row:2 / -1; grid-column:1; --eco-sombra-fuerza:.05; }
}


/* ══ 9. COHERENCIA ═════════════════════════════════════════════════════════
   Un sitio "eco" que quema batería es una contradicción. No es opcional.

   ⚠️ Van las TRES cosas que se mueven, no solo la sombra de las villas. Cada
   vez que añadas una animación a esta capa, súbela también aquí — es el tipo
   de olvido que no se ve en pantalla porque quien lo sufre no eres tú.

   La palmera de la banda se queda en su `scale(-1,-1)`, que es su posición de
   reposo. Sin ese `transform` explícito, quitarle la animación la devolvería
   a la regla de `.eco-band::after`... que también lo declara, así que en
   realidad no hace falta; se escribe igual para que no dependa de leerse dos
   sitios a la vez.                                                          */
@media (prefers-reduced-motion:reduce){
  .eco-shade i,
  .eco-band-foliage{ animation:none; }
  .eco-band::after{ animation:none; transform:scale(-1,-1); }
}


/* ══ 10. HOMOLOGACIÓN DEL FONDO ════════════════════════════════════════════
   La sección 4 puso el panel de las villas en arena y dejó el resto de la
   página en el #f2f5f8 azulado del sitio. Con las bandas a sangre, el salto
   se ve en el borde exacto donde acaba el degradado del panel: la mitad
   cálida contra la mitad fría, y el recuadro claro leyéndose como un cartón
   pegado sobre otra página.

   Aquí se arregla al revés de como se rompió: en vez de enfriar el panel, se
   pasa la PÁGINA a la misma familia. Son cuatro superficies —el fondo del
   listado, la barra de filtros, las bandas y el pie— más el difuminado del
   hero, que es lo que cose el cielo con lo que viene debajo.

   ⚠️ Lo que NO entra aquí, y no es olvido: los #f2f5f8 de los chips, los
   hover, el pill del toggle, los botones de "vendido" y los bordes. Ésos no
   son fondo de página, son gris neutro SOBRE tarjeta blanca, y la tarjeta
   sigue siendo blanca. Calentarlos los volvería beige sucio sobre blanco.
   Regla para saber cuál es cuál: si al elemento le puedes ver el borde
   contra la página, es chrome y se queda; si es la página, entra.          */

/* Fondo del listado. El valor vive en un `style=""` inline del blade —
   `background:var(--eco-page,#f2f5f8)`— porque una hoja de estilos no le gana
   a un inline sin `!important`. Escrito con fallback, así que si esta capa se
   quita, el listado recupera el azul del sitio él solo. */

/* El fondo del `body` es el último que hay debajo de todo: se ve en el rebote
   del scroll (macOS / iOS), antes de que la maqueta termine de montarse y en
   cualquier hueco. Estaba en #84d1fe, un azul cielo saturado — es el más frío
   de todos y el único que además es SATURADO, así que asomando un centímetro
   se lleva por delante la paleta entera.

   Era el cielo de respaldo del hero, pero el hero ya tiene el suyo propio
   (`.fg-hero{background:#cfe7f5}`), o sea que éste no cubría nada. */
body{ background-color:var(--eco-page); }

.fg-filter-bar{ background:var(--eco-page); }

.fg-types-grid,
.fg-type-band{ --band-page:var(--eco-page); }

/* El pie es la última superficie de página. En blanco contra arena se lee
   frío justo donde el ojo ya venía en cálido; el borde superior sigue siendo
   quien lo separa, así que no pierde su función. */
.fg-footer{ background:var(--eco-page); }

/* El hueco de la foto, mientras la foto no está. Era #e6e9ec —gris azulado—
   y se ve más de lo que parece: media banda a sangre, en el momento en que la
   imagen aún no ha cargado o todavía no ha entrado (sección 11). Un rectángulo
   frío de 720px de ancho contra el arena es exactamente el salto que esta
   sección viene a quitar. Mismo tono, un punto más oscuro que la página, para
   que se lea como hueco y no como superficie. */
.fg-type-band .fg-card-img{ background:#E9E3D7; }

/* ⚠️ `.fg-hero::after` ES DEL SITIO — y esto NO contradice la sección 1.
   Lo que allí se prohíbe es SECUESTRARLO: la v5 lo usó para meter una sombra,
   se llevó por delante el difuminado del sitio y encima la sombra no se veía.

   Aquí no se le cambia el trabajo: sigue siendo el mismo difuminado, en el
   mismo sitio y con la misma altura. Sólo se le cambia el COLOR hacia el que
   funde, porque fundir a blanco cuando lo que hay debajo es arena deja una
   banda clara a lo ancho del hero.

   Se sobreescribe únicamente `background`. `height`, `bottom` y `z-index`
   siguen siendo del sitio: la trampa 2 era precisamente que la capa declaraba
   `inset` y el `height:6%` del sitio le ganaba en la cascada, dejando el
   pseudo-elemento fuera de la caja. Toca una propiedad y ninguna geometría.

   La auditoría sigue en verde: `heroIntacto` pide que sea un gradient y que
   no lleve `eco-sombra` dentro. Las dos se cumplen.

   ⚠️ SON DOS, no uno. El hero vive dentro de un slider (`.fg-stage.has-slider`)
   que alterna la celda del hero con celdas de imagen, y cada tipo de celda
   trae SU PROPIO difuminado: `.fg-hero::after` (6% de alto) y
   `.hs-cell-img::after` (8%). Recolorear sólo el primero deja el defecto
   exactamente igual de visible, porque el hero pasa buena parte del tiempo
   fuera de pantalla y lo que se ve es la otra celda. Se detectó midiendo la
   costura: el píxel llegaba a 255,255,255 justo encima del arena.          */
.fg-hero::after,
.hs-cell-img::after{
  background:linear-gradient(to top,
    var(--eco-page)          0%,
    rgba(245,241,233,.92)   35%,
    rgba(245,241,233,.45)   70%,
    rgba(245,241,233,0)    100%);
}

/* El espaciador iba de blanco a #f2f5f8 para coser el hero con el listado.
   Con los dos extremos ya en arena no tiene nada que degradar: plano. */
.fg-hero-spacer{ background:var(--eco-page); }


/* ══ 11. ENTRADAS Y RESPUESTA AL PUNTERO ═══════════════════════════════════
   Las bandas aparecían ya montadas: bajabas y estaban ahí. Con bandas a
   sangre de 544px eso es mucha superficie llegando de golpe.

   ⚠️ NO SE MUEVE LA BANDA, se mueve su CONTENIDO. Las bandas van pegadas
   (`gap:0`), así que desplazar una deja ver el fondo de página por la rendija
   y el borde entre bandas parpadea durante toda la entrada. Moviendo el texto
   y la foto dentro de una caja quieta, el fondo nunca se descose.

   El orden es editorial y es el mismo con el que se lee: primero la foto
   —que es el activo—, luego el texto, luego los botones, y el número de fondo
   el último porque es lo que menos importa. Los retardos son cortos (0 → 90 →
   150 → 220ms): esto tiene que leerse como que la página se asienta, no como
   una secuencia que hay que esperar.

   ⚠️ EL ESTADO OCULTO LO PONE EL JS, no el CSS. Si estuviera escrito aquí y
   el JS fallara —o IntersectionObserver no existiera— la home entera se
   quedaría en blanco. Escrito así, sin JS todo se ve: simplemente no entra. */
/* ⚠️ LAS TRANSICIONES VAN EN `.eco-in`, NO EN EL ESTADO OCULTO.

   Escritas en `.eco-reveal` el efecto salía AL REVÉS de lo que parece: el
   navegador pinta la banda montada, el JS le añade la clase, y como la
   transición ya está declarada, el contenido se DESVANECE en 0,8s antes de
   volver a entrar. Se ve aparecer, irse y volver.

   No se pilló mirando —dura menos de un segundo y engaña— sino midiendo: a
   los 800ms de cargar, con `eco-in` todavía sin poner, la foto daba
   opacity 0.0164 y el texto 0.0043. Un estado supuestamente estático no puede
   dar decimales; eran fotogramas de una transición corriendo hacia 0.

   La regla de CSS que lo arregla: la transición que manda es la del estado al
   que se LLEGA. Declarándola sólo en `.eco-in`, ocultar es instantáneo (no hay
   transición declarada en ese estado) y aparecer se anima. Sin rAF ni reflows
   forzados.                                                                  */
.fg-type-band.eco-reveal .fg-card-img > img,
.fg-type-band.eco-reveal .fg-card-body,
.fg-type-band.eco-reveal .fg-card-actions,
.fg-type-band.eco-reveal .fg-card-price,
.fg-type-band.eco-reveal .fg-band-num{ opacity:0; }

.fg-type-band.eco-reveal .fg-card-body,
.fg-type-band.eco-reveal .fg-card-actions,
.fg-type-band.eco-reveal .fg-card-price{ transform:translateY(18px); }

/* La foto no sube: entra abriéndose. Un desplazamiento sobre una imagen a
   sangre se lee como que la maqueta se está recolocando; una apertura mínima
   (1.06 → 1) se lee como profundidad. */
.fg-type-band.eco-reveal .fg-card-img > img{ transform:scale(1.06); }

.fg-type-band.eco-reveal.eco-in .fg-card-body,
.fg-type-band.eco-reveal.eco-in .fg-card-actions,
.fg-type-band.eco-reveal.eco-in .fg-card-price,
.fg-type-band.eco-reveal.eco-in .fg-band-num{
  opacity:1; transform:none;
  transition:opacity .8s var(--eco-ease), transform .8s var(--eco-ease);
}
/* La foto va más lenta que el texto a propósito: la masa grande tiene que
   pesar más que la pequeña. */
.fg-type-band.eco-reveal.eco-in .fg-card-img > img{
  opacity:1; transform:none;
  transition:opacity 1.15s var(--eco-ease), transform 1.15s var(--eco-ease);
}

.fg-type-band.eco-reveal.eco-in .fg-card-body   { transition-delay:.09s; }
.fg-type-band.eco-reveal.eco-in .fg-card-actions{ transition-delay:.15s; }
.fg-type-band.eco-reveal.eco-in .fg-card-price  { transition-delay:.22s; }
.fg-type-band.eco-reveal.eco-in .fg-band-num    { transition-delay:.3s; transition-duration:1.1s; }

/* La sombra llega con la luz, no con el layout: entra la última y despacio.
   El destino no se escribe: al dejar de cumplirse el `:not(.eco-in)` vuelve a
   mandar `opacity:var(--eco-sombra-fuerza)` de la sección 2, que cambia por
   especie. No se toca el `i` de dentro: la deriva ya está corriendo. */
.fg-type-band.eco-reveal:not(.eco-in) .eco-shade{ opacity:0; }
.fg-type-band.eco-reveal.eco-in .eco-shade{
  transition:opacity 1.6s var(--eco-ease) .35s;
}

/* La banda verde entra igual, en el mismo orden en que se lee. */
.eco-band.eco-reveal .eco-band-eyebrow,
.eco-band.eco-reveal .eco-band-title,
.eco-band.eco-reveal .eco-band-stat{ opacity:0; transform:translateY(20px); }
/* El follaje no entra desplazado: es fondo, y un fondo que se mueve delata
   que hay una capa encima. Sólo aparece, y más lento que todo lo demás. */
.eco-band.eco-reveal .eco-band-foliage{ opacity:0; }

.eco-band.eco-reveal.eco-in .eco-band-eyebrow,
.eco-band.eco-reveal.eco-in .eco-band-title,
.eco-band.eco-reveal.eco-in .eco-band-stat{
  opacity:1; transform:none;
  transition:opacity .85s var(--eco-ease), transform .85s var(--eco-ease);
}
.eco-band.eco-reveal.eco-in .eco-band-foliage{
  opacity:1; transition:opacity 1.5s var(--eco-ease);
}
.eco-band.eco-reveal.eco-in .eco-band-title{ transition-delay:.08s; }
.eco-band.eco-reveal.eco-in .eco-band-stat:nth-child(1){ transition-delay:.22s; }
.eco-band.eco-reveal.eco-in .eco-band-stat:nth-child(2){ transition-delay:.3s; }
.eco-band.eco-reveal.eco-in .eco-band-stat:nth-child(3){ transition-delay:.38s; }

/* ── Puntero ───────────────────────────────────────────────────────────────
   La foto de la banda ABRE una vista (`openVillaType`), así que responder al
   puntero no es adorno: es la única señal de que se puede pulsar. Sólo en
   dispositivos con puntero fino — en táctil el hover se queda pegado tras el
   tap y la foto se queda ampliada.

   1.5s y 1.035 de escala. Más rápido o más grande y deja de ser respuesta
   para ser efecto, que es justo lo que esta capa lleva evitando desde la v3. */
@media (hover:hover) and (pointer:fine){
  .fg-type-band .fg-card-img > img{
    transition:transform 1.5s var(--eco-ease);
  }
  .fg-type-band .fg-card-img:hover > img{ transform:scale(1.035); }

  /* El chip de precio flota sobre la foto: al acercarse, se despega un poco
     más. Es el mismo sol del proyecto, así que la sombra crece hacia la
     derecha (--eco-sol-x), no hacia abajo a plomo. */
  .fg-type-band .fg-card-price{
    transition:transform .5s var(--eco-ease), box-shadow .5s var(--eco-ease);
  }
  .fg-type-band .fg-card-inner:hover .fg-card-price{
    transform:translateY(-2px);
    box-shadow:
      calc(3px * var(--eco-sol-x))  3px  6px rgba(14,18,27,.10),
      calc(9px * var(--eco-sol-x))  9px 18px rgba(14,18,27,.10);
  }

  .fg-type-band .fg-btn-info,
  .fg-type-band .fg-btn-cta{
    transition:transform .35s var(--eco-ease), filter .35s var(--eco-ease),
               background .35s var(--eco-ease), box-shadow .35s var(--eco-ease);
  }
  .fg-type-band .fg-btn-info:hover,
  .fg-type-band .fg-btn-cta:hover{ transform:translateY(-1px); }
}

/* ── LA PÍLDORA DEL HEADER NO ENTRA. NO LO REINTENTES ──────────────────────
   Se probó: esconderla y bajarla al levantarse el telón, enganchada a
   `makai:hero-revealed`. Se cayó dos veces y la segunda no supe arreglarla.

   1) Primera versión, con `requestAnimationFrame` para armar la transición.
      rAF no corre en una pestaña que carga en segundo plano, así que la
      callback se quedaba en cola y la píldora invisible. Medido: a los 12s
      seguía con las dos clases y opacidad 0.

   2) Segunda versión, ya sin rAF y con el mismo mecanismo que las bandas
      —estado oculto sin transición, transición en el destino—. Las clases se
      intercambian bien y aun así la opacidad no se movía del 0.
      `getAnimations()` sobre la píldora: dos transiciones en
      `playState:'running'` con `currentTime: 0`, o sea armadas y sin avanzar
      nunca. Quitándole TODAS las clases seguía en 0, porque la transición
      colgada mantiene el valor. Las mismas transiciones sobre las bandas, en
      la misma página, sí corrían.

   No llegué al motivo. Puede ser cosa del headless con el que se midió, o de
   que la píldora vive dentro de un `position:fixed` que se compone aparte.
   Da igual: la cabecera es la navegación, y no se puede publicar un efecto
   cuyo fallo la deja invisible sin poder demostrar que no falla. Lo que se
   ganaba era una píldora bajando; lo que se arriesgaba, la home sin header.

   Si algún día se retoma: hay que probarlo en un navegador de verdad, en
   pestaña de fondo y con la red lenta, y el estado por defecto tiene que ser
   VISIBLE — que el efecto sea añadir, nunca quitar.                         */

/* ── Coherencia ────────────────────────────────────────────────────────────
   Las entradas se quedan en desvanecido, sin desplazamientos ni escalas, y
   el puntero deja de mover nada. Lo que NO se hace es anularlas del todo: sin
   transición el estado oculto se quedaría fijo y el contenido no aparecería. */
@media (prefers-reduced-motion:reduce){
  .fg-type-band.eco-reveal .fg-card-img > img,
  .fg-type-band.eco-reveal .fg-card-body,
  .fg-type-band.eco-reveal .fg-card-actions,
  .fg-type-band.eco-reveal .fg-card-price,
  .eco-band.eco-reveal .eco-band-eyebrow,
  .eco-band.eco-reveal .eco-band-title,
  .eco-band.eco-reveal .eco-band-stat{ transform:none; }

  .fg-type-band .fg-card-img:hover > img,
  .fg-type-band .fg-card-inner:hover .fg-card-price,
  .fg-type-band .fg-btn-info:hover,
  .fg-type-band .fg-btn-cta:hover{ transform:none; }
}


/* ══ 12. EL CATÁLOGO — LAS TARJETAS ════════════════════════════════════════
   Las bandas de tipología ya estaban en la capa; el grid de unidades no. Y es
   la pantalla donde el usuario pasa el rato: bajaba desde el arena de las
   bandas y se encontraba con cinco rectángulos blancos de chrome gris azulado.
   No es que estuviera feo — es que era OTRA página.

   Aquí se le aplica exactamente el mismo material que a las bandas, en el
   mismo orden de decisiones:

     · la tarjeta deja de ser blanco puro y pasa a papel cálido, con grano
     · una sombra vegetal cae sobre el cuerpo de texto, nunca sobre la foto
     · el chrome frío (#f2f5f8, #ebebeb) se pasa a la familia arena
     · el verde genérico de "hay un asesor" pasa al verde del proyecto

   ⚠️ TODO VA COLGADO DE `.fg-units-grid > .fg-card`. `.fg-card-inner`,
   `.fg-card-body` y `.fg-card-price` son selectores COMPARTIDOS con las
   bandas de tipología: escritos sueltos, cada regla de aquí se llevaría por
   delante las secciones 2, 3 y 4. La sección 10 ya avisaba de lo contrario
   —"la tarjeta sigue siendo blanca"— y esto lo revisa a conciencia: lo que
   se calienta es la SUPERFICIE de la tarjeta, así que los grises que van
   sobre ella se calientan con ella y no quedan beige sucio sobre blanco.  */

/* ── 12.0 EL SUELO DE LA SECCIÓN: NO SE TOCA ───────────────────────────────
   ⚠️ AQUÍ HUBO UN LAVADO VERDE A LO ANCHO DE LA VENTANA Y SE RETIRÓ. No lo
   vuelvas a poner sin que lo pidan.

   La idea era razonable —teñir las calles del grid para que el bloque se
   leyera como una sección propia— y funcionaba, pero se lleva por delante lo
   que la sección 10 dejó cosido: el listado, la barra de filtros, las bandas
   y el pie comparten UN fondo, `--eco-page`, y esa continuidad es justo lo
   que hace que la página no se lea como pantallas pegadas. Un rectángulo
   verde en el medio la parte otra vez.

   El contenedor se queda con el arena de la página. El verde de esta sección
   lo ponen las tarjetas, que es donde hay superficie para sostenerlo.      */


/* ── 12.1 LAS DOS SUPERFICIES ──────────────────────────────────────────────
   La tarjeta tiene dos versiones y la que le toca a cada una la decide una
   sola cosa: si lleva hoja o no.

     · CON hoja  → salvia. La hoja necesita una superficie de color debajo:
       una sombra sobre blanco es gris (fue la v7.1) y el gris apaga el verde
       en vez de sumarle. Sobre salvia, la misma sombra ES verde.
     · SIN hoja  → blanco. Sin nada que sostener, el papel blanco deja
       respirar el listado y, de rebote, hace que las verdes se lean MÁS
       verdes de lo que se leerían entre verdes.

   O sea que las dos decisiones —dónde va la vegetación y de qué color es la
   tarjeta— son la MISMA decisión, y por eso el CSS puede resolverlo con
   `[data-eco-shade]` sin que el JS tenga que marcar nada aparte.

   ⚠️ El chrome NO cambia entre las dos. La tinta, la retícula de specs, el
   filo y la banda de reserva siguen siendo los verdes de 12.3 y 12.4 en
   ambas. Neutralizando el chrome de la blanca, dejaría de pertenecer a la
   familia y volvería a ser la tarjeta corporativa que la v7.2 vino a quitar.

   El límite de cuánto puede bajar la salvia es el texto de 10px de las
   specs: por eso no baja de #E4EEDD en la esquina más oscura, y por eso la
   12.4 sube esos grises a verde oscuro. Lo que hace legible una tarjeta no
   es que el fondo sea claro, es la distancia entre fondo y tinta.          */
.fg-units-grid > .fg-card,
.fg-units-grid > .fg-card .fg-card-inner{
  background:linear-gradient(155deg, #FFFFFF 0%, #FDFEFC 54%, #F6FAF3 100%);
}

.fg-units-grid > .fg-card[data-eco-shade],
.fg-units-grid > .fg-card[data-eco-shade] .fg-card-inner{
  background:linear-gradient(155deg, #F6FAF2 0%, #EDF4E7 52%, #E4EEDD 100%);
}

/* Grano de papel, la mitad de fuerza que en las bandas: en una superficie de
   este tamaño el mismo 3,5% se lee como textura sucia en vez de como papel.

   ⚠️ Va en `.fg-card-body::before`, NO en `.fg-card-inner::after` como en la
   sección 3. En la banda el `inner` es todo panel; en la tarjeta contiene
   también la foto, y `.fg-card-img` está posicionada con z-index auto, así
   que un pseudo-elemento posterior le pintaría el grano ENCIMA al render.
   Colgándolo del cuerpo, el grano cubre exactamente el papel y nada más.   */
.fg-units-grid > .fg-card .fg-card-body::before{
  content:"";
  position:absolute; inset:0; z-index:0;
  pointer-events:none;
  background-image:var(--eco-grano);
  background-size:180px 180px;
  mix-blend-mode:multiply;
  opacity:.018;
}

/* ── 12.2 La sombra vegetal ────────────────────────────────────────────────
   Mismos dos assets que las bandas y misma doctrina: entra cortada por la
   esquina superior izquierda, cae sobre el recuadro de texto y NUNCA sobre la
   foto. Aquí el recorte no lo da una columna de grid sino el propio cuerpo de
   la tarjeta, que es quien la contiene.

   ⚠️ NO LLEVA `z-index:-1` como la de las bandas. `.fg-card-inner` pinta un
   fondo propio y crea contexto (`z-index:2`), así que una capa en negativo se
   iría DETRÁS de ese fondo y no se vería nada. Se resuelve al revés: la
   sombra en `z-index:0` y el contenido del cuerpo subido a 1.

   ⚠️ Y NO SE ANIMA. En las bandas hay cinco sombras en toda la página; aquí
   puede haber cuarenta a la vez. Cuarenta capas compuestas en bucle es
   justo la contradicción que la sección 9 viene a evitar — y encima el
   balanceo, a este tamaño, no se percibe.                                  */
.fg-units-grid > .fg-card .fg-card-body{ position:relative; }
.fg-units-grid > .fg-card .fg-card-body > :not(.eco-shade-card){
  position:relative;
  z-index:1;
}

.eco-shade-card{
  position:absolute; inset:0; z-index:0;
  overflow:hidden;
  pointer-events:none;
  mix-blend-mode:multiply;
  opacity:var(--eco-sombra-fuerza, .09);
}

/* ⚠️ NO LE PONGAS UN `filter` VERDE A LA HOJA. Lo probé y deja una costura.

   La idea era obvia: los dos webp son grises (ver ASSETS), así que
   `sepia() hue-rotate()` los pasaría a verde y la hoja aportaría color en vez
   de sólo oscurecer. Funciona con la hoja… y también con el FONDO BLANCO del
   asset, que deja de ser blanco puro. Y un blanco que ya no es blanco deja de
   ser neutro en `multiply`: la caja entera de la sombra se tiñe y aparece un
   rectángulo de otro tono en la esquina de cada tarjeta.

   Lo que sí tiñe la hoja SIN tocar el blanco es `screen` contra un verde de
   follaje: en `screen` el blanco es el elemento neutro —blanco con lo que sea
   da blanco—, así que el fondo del asset sale intacto y sólo se colorea lo
   que tiene tinta. Como `screen` además aclara, las fuerzas de más abajo
   suben para compensar.                                                     */
.eco-shade-card i{
  background-color:#1B5E36;
  background-blend-mode:screen;
}

.eco-shade-card i{
  display:block;
  position:absolute;
  top:var(--eco-sombra-y, 0);
  left:var(--eco-sombra-x, 0);
  width:var(--eco-sombra-ancho, 58%);
  aspect-ratio:1.3258;                 /* ratio de los dos assets: 1400×1056 */
  background-repeat:no-repeat;
  background-size:100% 100%;

  /* ⚠️ DOS MÁSCARAS, y la segunda no es un adorno.

     La diagonal es la de las bandas: desvanece la salida de la sombra
     siguiendo al sol. Con ella sola, el borde SUPERIOR de la sombra queda a
     corte limpio — y ahí está la diferencia con la banda: allí ese corte cae
     en el borde de la banda y se lee como el encuadre; aquí cae dentro de la
     tarjeta, justo debajo de la foto, y se lee como una costura.

     La segunda máscara la hace entrar desvanecida desde arriba. Si el
     navegador no soporta `mask-composite`, se queda la diagonal sola, que es
     exactamente el estado anterior a esta línea: peor, pero no roto.       */
  mask-image:
    linear-gradient(145deg, #000 0%, rgba(0,0,0,.82) 48%, transparent 94%),
    linear-gradient(to bottom, transparent 0%, #000 24%);
  -webkit-mask-composite:source-in;
  mask-composite:intersect;
}

/* Las cinco variantes de la sección 2, reescaladas al cuerpo de la tarjeta.

   ⚠️ Sólo una de cada tres tarjetas lleva hoja. Quién la lleva y cuál le toca
   lo decide el JS (`hojaDe`), y el porqué del reparto —ciclo primo, huecos
   irregulares, seis hojas contra cinco especies— está escrito allí. Aquí sólo
   importa la consecuencia: como la hoja no está en todas, la que aparece
   tiene que pesar. De ahí las fuerzas de abajo.

   Los anchos suben (58-72% frente al 45-52% de las bandas) porque el cuerpo
   de la tarjeta mide un tercio del panel de una banda: al ancho de allí, la
   hoja se leería como una mancha pequeña en la esquina y no como una sombra
   que entra desde fuera del cuadro. Los offsets siguen siendo ≤ 0 por lo
   mismo que en la sección 2: en positivo se ve el corte del asset flotando. */
/* ⚠️ LAS FUERZAS SUBEN RESPECTO A LAS BANDAS, no bajan.

   En la sección 2 la sombra va sobre arena y al 7-13% ya pesa. Aquí compite
   con seis iconos, una retícula de specs y dos botones en 300px de ancho: a
   la fuerza de las bandas la hoja desaparecía bajo el contenido. Con el
   filtro verde de arriba, además, la mancha ya no ensucia — tiñe.          */
[data-eco-shade="fronda"]        .eco-shade-card{ --eco-sombra-fuerza:.46; }
[data-eco-shade="fronda"]        .eco-shade-card i{
  background-image:var(--eco-sombra-fronda);
  --eco-sombra-ancho:78%;
}

[data-eco-shade="monstera"]      .eco-shade-card{ --eco-sombra-fuerza:.34; }
[data-eco-shade="monstera"]      .eco-shade-card i{
  background-image:var(--eco-sombra-monstera);
  --eco-sombra-ancho:72%;
}

[data-eco-shade="fronda-alt"]    .eco-shade-card{ --eco-sombra-fuerza:.52; }
[data-eco-shade="fronda-alt"]    .eco-shade-card i{
  background-image:var(--eco-sombra-fronda);
  --eco-sombra-ancho:70%;
  --eco-sombra-x:-9%;
  --eco-sombra-y:-13%;
}

[data-eco-shade="monstera-alt"]  .eco-shade-card{ --eco-sombra-fuerza:.40; }
[data-eco-shade="monstera-alt"]  .eco-shade-card i{
  background-image:var(--eco-sombra-monstera);
  --eco-sombra-ancho:66%;
  --eco-sombra-x:-11%;
  --eco-sombra-y:-9%;
}

[data-eco-shade="fronda-corta"]  .eco-shade-card{ --eco-sombra-fuerza:.52; }
[data-eco-shade="fronda-corta"]  .eco-shade-card i{
  background-image:var(--eco-sombra-fronda);
  --eco-sombra-ancho:84%;
  --eco-sombra-x:-14%;
  --eco-sombra-y:-5%;
}

/* La tarjeta vendida se apaga entera: la hoja encima de un cuadro ya
   desaturado sólo lo ensucia. */
.fg-units-grid > .fg-card.is-sold .eco-shade-card{ opacity:.06; }

/* ── 12.3 El chrome, al verde ──────────────────────────────────────────────
   Éstos SÍ entran, al revés que los chips de la sección 10: allí el gris iba
   sobre tarjeta blanca y calentarlo lo ensuciaba; aquí la tarjeta ya es
   salvia, así que el gris frío es lo que canta.

   La retícula de specs, además, deja de ser un recuadro con borde y pasa a
   ser una superficie verde: son seis piezas repetidas en cada tarjeta, o sea
   la mancha de color más grande del cuerpo después de la foto.             */
.fg-units-grid > .fg-card .fg-card-divider{ background:rgba(47,93,80,.20); }
.fg-units-grid > .fg-card .fg-stats{
  background:rgba(47,93,80,.06);
  border-color:rgba(47,93,80,.18);
}
.fg-units-grid > .fg-card .fg-stat-divider{ background:rgba(47,93,80,.16); }
.fg-units-grid > .fg-card .fg-btn-info{
  background:rgba(47,93,80,.10);
  color:var(--eco-deep);
  font-weight:600;
}
.fg-units-grid > .fg-card .fg-btn-info:hover{ background:rgba(47,93,80,.17); }

/* El hueco de la foto mientras carga: verde, para que el primer frame de la
   tarjeta ya sea de la familia y no un rectángulo neutro. */
.fg-units-grid > .fg-card .fg-card-img,
.fg-units-grid > .fg-card .fg-card-img-noimage{ background:#DCE8D4; }

/* La píldora "AÑADIR A LA LISTA" es cristal blanco sobre la foto. Con todo lo
   demás en verde, es el único objeto que queda pidiendo neutralidad. */
.fg-units-grid > .fg-card .fg-add-to-list{
  background:rgba(238,244,234,.72);
  border-color:rgba(47,93,80,.22);
}
.fg-units-grid > .fg-card .fg-add-to-list:hover{ background:rgba(238,244,234,.92); }
.fg-units-grid > .fg-card .fg-add-to-list .heart svg,
.fg-units-grid > .fg-card .fg-add-to-list .text .label{ color:var(--eco-deep); }

/* ── 12.4 La tinta ─────────────────────────────────────────────────────────
   ⚠️ ESTO NO ES DECORACIÓN, ES LO QUE PAGA EL FONDO VERDE.

   Al bajar el fondo de blanco a salvia, los grises del sitio (#5c5c5c en el
   nombre, #a3a3a3 en el subtítulo, #848484 en las specs) pierden separación
   contra él. Pasándolos a la familia verde POR ABAJO —más oscuros, no más
   claros— la tarjeta acaba con más contraste del que tenía en blanco, y de
   paso el color deja de vivir sólo en el fondo: está también en la tinta,
   que es lo que hace que se lea verde de verdad y no "blanco tintado".     */
.fg-units-grid > .fg-card .fg-card-title-row .name{ color:var(--eco-deep); }
.fg-units-grid > .fg-card .fg-card-subtitle       { color:#63775F; }
.fg-units-grid > .fg-card .fg-stat svg            { color:#4A6B4E; }
.fg-units-grid > .fg-card .fg-stat .v,
.fg-units-grid > .fg-card .fg-stat .u             { color:#41613F; }
.fg-units-grid > .fg-card .fg-card-price .sqft    { color:#6B7F67; }

/* "Un asesor está disponible ahora mismo" venía en #1fc16b, el verde de
   semáforo del sitio. Es el único acento verde grande de la tarjeta, así que
   es también el que más lejos deja el conjunto del verde pino del proyecto.
   Se pasa a --eco-green y el punto conserva su halo y su latido: sigue
   leyéndose como señal de disponibilidad, ahora en el color de la casa.    */
.fg-units-grid > .fg-card .fg-card-availability{ color:var(--eco-green); }
.fg-units-grid > .fg-card .fg-card-availability .dot{
  background:var(--eco-green);
  box-shadow:0 0 6px rgba(47,93,80,.55);
}
/* La nota de "vendida" es roja por el sitio y tiene que seguir siéndolo. */
.fg-units-grid > .fg-card.is-sold .fg-card-availability{ color:#e2534e; }
.fg-units-grid > .fg-card.is-sold .fg-card-availability .dot{
  background:#e2534e;
  box-shadow:0 0 6px rgba(226,83,78,.5);
}

/* La banda "Reserva desde $5000" es un cristal BLANCO al 50% con el texto en
   blanco encima: se lee cuando la foto es oscura y se pierde cuando no. Al
   pasarlo al verde profundo del proyecto el cristal se oscurece, así que el
   texto blanco gana contraste en vez de perderlo — es de las pocas veces que
   una decisión de color mejora la legibilidad de paso.

   ⚠️ Si alguna vez se aclara este cristal, hay que cambiar también el color
   del texto: va en `#ffffff` desde el sitio.                               */
.fg-units-grid > .fg-card .fg-reserve-banner{
  background:linear-gradient(180deg, rgba(30,61,51,.42) 0%, rgba(30,61,51,.78) 100%);
}

/* El filo de la tarjeta. Con el suelo de la sección también en verde, una
   tarjeta sin borde se difumina contra él; el filo verde la recorta y de paso
   es la línea que repite el color en las veinte tarjetas a la vez.

   ⚠️ SÓLO EN LAS TARJETAS SIN ESTADO, y esto no es un detalle estético.

   El sitio marca alta demanda / pendiente / 2ª oportunidad / reservada / venda
   con un aro de 3px en `box-shadow` sobre `.fg-card.is-*`. Esa regla tiene la
   MISMA especificidad que ésta (dos clases), así que gana la última en llegar
   — y esta hoja se carga después de style.css. Sin el `:not()`, el filo verde
   apaga los cinco aros de estado del catálogo entero: el usuario deja de ver
   qué unidades están reservadas.                                            */
.fg-units-grid > .fg-card:not(.is-high-demand):not(.is-pending):not(.is-second-chance):not(.is-reserved):not(.is-sold){
  box-shadow:0 0 0 1px rgba(47,93,80,.16);
}

/* ── 12.5 Respuesta al puntero ─────────────────────────────────────────────
   La tarjeta levantaba con una sombra neutra hacia abajo. Se le da la
   inclinación del sol del proyecto —igual que la sección 7 hizo con el pill
   del header— y un filo verde muy tenue, que es el que dice "esto es de la
   misma familia que las bandas". Sin desplazamiento: el grid tiene muchas
   tarjetas y moverlas al pasar por encima convierte el listado en un campo
   de minas.

   Mismo `:not()` que el filo, y por lo mismo: una tarjeta reservada no puede
   perder su aro rojo por pasarle el ratón por encima.                       */
@media (hover:hover){
  .fg-units-grid > .fg-card:not(.is-high-demand):not(.is-pending):not(.is-second-chance):not(.is-reserved):not(.is-sold):hover{
    box-shadow:
      calc( 4px * var(--eco-sol-x))  4px  10px rgba(30,61,51,.10),
      calc(14px * var(--eco-sol-x)) 14px  28px rgba(30,61,51,.12),
      0 0 0 1px rgba(47,93,80,.28);
  }
}

/* ── 12.6 Coherencia y responsive ──────────────────────────────────────────
   La sombra de la tarjeta no se anima, así que no hay nada que apagar en
   `prefers-reduced-motion`. Lo que sí cambia es el móvil: la tarjeta pasa a
   ocupar el ancho de la pantalla y la sombra, a la misma proporción, se
   convierte en una mancha sobre el texto. Se baja fuerza y tamaño.         */
@media (max-width:640px){
  .eco-shade-card{ --eco-sombra-fuerza:.26; }
  .eco-shade-card i{ --eco-sombra-ancho:52%; }
}


/* ══ 13. LA COSTURA ENTRE EL HERO Y EL LISTADO ═════════════════════════════
   La sección 10 puso los dos lados de la junta en el mismo color y dio el
   problema por resuelto. No lo estaba: el color coincidía, pero la junta se
   seguía viendo, y por tres motivos distintos que hay que arreglar por
   separado.                                                                */

/* ── 13.1 El difuminado del hero era demasiado corto ───────────────────────
   El sitio lo resuelve en un 6% del alto del hero (8% en las celdas de
   imagen del carrusel): unos 45px sobre 735. Con vegetación en los bordes
   basta y por eso nadie lo miró; con la piscina en el centro no, porque un
   turquesa saturado necesita mucho más recorrido para llegar al arena. El
   resultado es lo que se ve: las plantas se disuelven y el agua se CORTA.

   Y hay un segundo error, más fino, en la forma de la rampa: el sitio va de
   opaco a .92 en el primer 35% del difuminado. O sea que el 8% de la altura
   que tenía se gastaba casi entero en el tramo donde ya no se ve nada, y la
   parte útil —de .92 a 0— quedaba comprimida en el resto. Eso deja un
   escalón horizontal justo donde debería empezar a desaparecer.

   Aquí se corrige lo uno y lo otro: el triple de recorrido y una rampa que
   sale despacio de la opacidad total.

   ⚠️ Se tocan `height` y `background` y NADA MÁS. `bottom`, `left`, `right`
   y `z-index` siguen siendo del sitio — ver la trampa 2 en la sección 10:
   la capa declaró una vez `inset` y el `height` del sitio le ganó en la
   cascada, dejando el pseudo-elemento fuera de la caja.                    */
.fg-hero::after,
.hs-cell-img::after{
  height:15%;
  background:linear-gradient(to top,
    var(--eco-page)         0%,
    rgba(245,241,233,.985)  9%,
    rgba(245,241,233,.90)  24%,
    rgba(245,241,233,.66)  45%,
    rgba(245,241,233,.34)  68%,
    rgba(245,241,233,.12)  85%,
    rgba(245,241,233,0)   100%);
}


/* ── 13.1b LA JUNTA TIENE DOS LADOS ────────────────────────────────────────
   ⚠️ ALARGAR EL DIFUMINADO DEL HERO NO ARREGLA LA COSTURA, Y ESE ERROR YA SE
   COMETIÓ AQUÍ. Se subió al 22% y la junta seguía viéndose exactamente igual:
   sólo se consiguió que el hero se leyera con niebla.

   El motivo es que el difuminado sólo trabaja el lado de ARRIBA. Por abajo,
   la mitad derecha de la primera banda es una foto a sangre que empieza en un
   corte de un píxel, así que la secuencia era: disolución lenta… y tajo. Un
   borde limpio junto a un degradado se ve MÁS, no menos: el degradado es la
   referencia que delata que el otro lado no lo tiene.

   Así que el difuminado del hero baja (15%: lo justo para que el agua llegue
   al arena sin empañar el hero) y la banda recibe el suyo, en espejo. En el
   punto de encuentro los dos están en arena puro y no hay junta que ver.

   Va en `.fg-types-grid::before` —una sola pieza para toda la portada— y
   afecta sólo a la primera banda porque es la única que toca el borde. Sólo
   en la portada: en el catálogo la junta la resuelve la bandeja de filtros,
   que ya es un objeto suelto sobre el arena.                               */
body:not(.is-catalog) .fg-types-grid{ position:relative; }

body:not(.is-catalog) .fg-types-grid::before{
  content:"";
  position:absolute;
  top:0; left:0; right:0;
  height:110px;
  pointer-events:none;
  /* ⚠️ z-index 3: por encima de la foto de la banda y del número de fondo
     (que va en -1), y por debajo del chip de precio (6) y del panel de texto.
     Si sube de 5, se come el "DESDE $1 100 000" de la primera banda.       */
  z-index:3;
  background:linear-gradient(to bottom,
    var(--eco-page)         0%,
    rgba(245,241,233,.94)  18%,
    rgba(245,241,233,.72)  40%,
    rgba(245,241,233,.42)  63%,
    rgba(245,241,233,.16)  83%,
    rgba(245,241,233,0)   100%);
}


/* ── 13.2 EN LA PORTADA, LAS VILLAS VAN PEGADAS AL HERO ────────────────────
   Entre el hero y la primera banda había 139px de arena vacía. No era
   margen de nadie: era el HUECO EN FLUJO de la píldora Grid/Lista, que en la
   portada está siempre flotando abajo y deja su sitio reservado arriba.

   Con la barra de filtros fuera de la portada, ese hueco se quedó suelto
   entre dos secciones que deberían tocarse, y es lo que hacía que la portada
   se leyera como "hero, un vacío, y luego unas bandas".

   La píldora se fija SIEMPRE en la portada y el hueco se anula. No es un
   cambio de comportamiento: en la portada ya flotaba en cuanto la ventana
   tenía altura normal. Lo que se quita es la reserva de sitio.

   ⚠️ El `!important` del hueco no es pereza: el JS le escribe la altura
   inline (`spacer.style.height`) cuando decide enganchar la píldora, y un
   estilo en línea gana a cualquier regla de esta hoja. Sin él, el hueco
   vuelve en cuanto el JS se despierta.                                     */
body:not(.is-catalog) .fg-toggle-spacer{ height:0 !important; }

body:not(.is-catalog) .fg-toggle-bar{
  position:fixed;
  left:0; right:0;
  bottom:calc(var(--fg-float-gap, 24px) + env(safe-area-inset-bottom, 0px));
  padding:0;
  z-index:90;
}

/* Lo que quedaba del hueco: el espaciador del hero y el margen negativo con
   el que el listado se montaba encima. Los dos existían para coser el hero
   con el gris azulado del sitio; con los dos lados ya en arena y el
   difuminado de 13.1 haciendo el trabajo, sobran.

   Sólo en la portada: en el catálogo, la barra de filtros necesita su aire
   por encima. */
body:not(.is-catalog) .fg-hero-spacer{ display:none; }
body:not(.is-catalog) .fg-types-grid{ padding-top:0; }


/* ── 13.3 LOS FILTROS, EN BANDEJA ──────────────────────────────────────────
   La barra era una franja del color de la página con ocho cajitas blancas
   encima. Con el listado también en tarjetas, eso son diez rectángulos
   blancos apilados en el mismo cuadro y ninguno manda: el ojo no sabe si los
   filtros son parte del listado o de la cabecera.

   La página ya tiene resuelto ese problema una vez —la píldora Grid/Lista es
   UNA pieza blanca con las opciones dentro— así que los filtros se pasan al
   mismo objeto: una bandeja blanca, mismo radio de píldora, misma sombra, y
   los controles convertidos en segmentos suyos en vez de piezas sueltas.

   Quedan tres objetos en la pantalla en lugar de diez: la bandeja, el
   contador y la píldora del toggle. Los tres del mismo material.

   ⚠️ Los controles de dentro pierden fondo y borde, pero NO el estado
   activo: un filtro puesto tiene que seguir viéndose de un golpe de vista
   (`.is-active` conserva su contorno verde de marca).                     */
.fg-filter-bar{ background:transparent; }

.fg-filter-bar > .fg-filters-left{
  flex:0 1 auto;                     /* la bandeja abraza su contenido */
  gap:2px;
  padding:8px;
  background:#ffffff;
  border-radius:100px;
  box-shadow:0 6px 20px rgba(10,13,20,.10);
}

/* El contador se va al otro extremo por su cuenta, como el botón del mapa
   respecto a la píldora del toggle. */
.fg-filter-bar > .fg-pill-matches{
  margin-left:auto;
  border-color:transparent;
  border-radius:100px;
  box-shadow:0 6px 20px rgba(10,13,20,.10);
}

/* Los controles, ya dentro de la bandeja, dejan de ser cajas. */
.fg-filter-bar .fg-search,
.fg-filter-bar .fg-filter-btn,
.fg-filter-bar .fg-filter-icon,
.fg-filter-bar .fg-filters-mobile-trigger{
  background:transparent;
  border-color:transparent;
  box-shadow:none;
  border-radius:100px;
}

/* Dos excepciones, y las dos por lo mismo: sin fondo no se leerían como algo
   sobre lo que se puede actuar. El buscador es un campo —lleva el hueco
   hundido en vez del borde— y en móvil "Filtros" es el único botón de la
   bandeja, así que blanco sobre blanco desaparecería. */
.fg-filter-bar .fg-search{
  background:rgba(47,93,80,.06);
  width:150px;
}
.fg-filter-bar .fg-filters-mobile-trigger{ background:rgba(47,93,80,.09); }

.fg-filter-bar .fg-filter-btn:hover,
.fg-filter-bar .fg-filter-icon:hover{
  background:rgba(47,93,80,.09);
  border-color:transparent;
}

/* El filtro activo conserva su marca: es información, no decoración. */
.fg-filter-bar .fg-filter-btn.is-active,
.fg-filter-bar .fg-filter-btn.is-active:hover{
  background:rgba(7,69,64,.07);
  border-color:transparent;
  box-shadow:none;
  color:var(--eco-deep);
}

/* En móvil la bandeja se queda con dos piezas —buscador y "Filtros"— y el
   contador cae debajo. Que la bandeja no se estire a todo el ancho es lo que
   la mantiene reconocible como el mismo objeto que en escritorio. */
@media (max-width:768px){
  .fg-filter-bar > .fg-filters-left{ flex:1 1 auto; }
  .fg-filter-bar > .fg-pill-matches{ margin-left:0; }
}


/* ============================================================================
   AUDITORÍA — pegar en la consola tras cualquier cambio.

   ⚠️ DESDE LA SECCIÓN 11, PÁSALO CON LA PÁGINA ASENTADA. Las bandas entran al
   aparecer en pantalla y la sombra es lo último en llegar (1,6s de transición
   con 0,35s de retardo). Si lanzas la auditoría mientras esa entrada corre,
   lees `opacidad: "0"` y un `contraste: 15` — que es exactamente el 15 que el
   propio script avisa de que no significa nada, porque mide contra el arena
   limpio en vez de contra el arena oscurecido por la sombra.

   Pasó midiendo: una pasada a 1700 px cantó Villa C con la sombra a 0, y las
   tres siguientes dieron 0.13. No era un fallo de la capa, era la auditoría
   llegando antes que la animación. Baja hasta el final, espera un par de
   segundos y entonces pega el script.

   Pásalo al menos a 1440 px y a 1700 px: el sitio cambia el padding y la
   altura de la banda en 1600. Todo tiene que cumplirse a la vez:

     invadeFoto  == 0     la sombra se queda en el recuadro claro
     recorta     == true  la caja exterior sigue conteniendo el balanceo
     naceDelBorde== true  entra desde el borde, no empieza dentro del panel
     detrasTexto == true  no se pinta por encima del contenido
     contraste   >= 4.5   el título sigue siendo legible sobre la sombra
                          (hoy da 11-12; los 4,8 / 5,2 de antes eran el bug
                          del regex que se arregla más abajo, no una medida)
     zigzag      == ok    el sitio alterna foto/texto y sigue alternando
     ritmos      == ok    ninguna animación es múltiplo de otra
     heroIntacto == true  la capa no ha pisado el ::after del sitio. En el
                          index.html de este repo sale `n/a`: la página se quedó
                          solo con la sección de tipologías y ahí no hay hero.
                          Donde importa —el sitio real y el demo de consola— se
                          sigue midiendo

   (() => {
     // ⚠️ El regex lleva el punto DENTRO. El arena oscurecido se calcula a mano
     // y sale con decimales — y con coma flotante, con muchos: 239*(1-.07) da
     // 222.26999999999998. Con /\d+/ eso se parte en "222" y "26999999999998",
     // el segundo entra como canal G y la luminancia se va a 1e27. Con .085
     // salia un 125 igual de falso. Los contrastes de 4,8 y 5,2 que se venian
     // leyendo eran ESO, no una medida: los reales rondan 11-12.
     const out = {}, lum = c => {
       const [r,g,b] = c.match(/[\d.]+/g).slice(0,3).map(v => {
         v /= 255; return v <= .03928 ? v/12.92 : ((v+.055)/1.055) ** 2.4;
       });
       return .2126*r + .7152*g + .0722*b;
     };

     document.querySelectorAll('.eco-shade').forEach(sh => {
       const band  = sh.closest('.fg-type-band');
       const foto  = band.querySelector('.fg-card-img').getBoundingClientRect();
       const nombre = band.querySelector('.fg-card-title-row .name');
       const hijo = sh.querySelector('i');

       // ⚠️ SE MUESTREA LA ANIMACION REAL, NO UN TRANSFORM ESCRITO A MANO.
       //
       // Aqui habia la cadena 'translate3d(-1.4%,1%,0) scale(1.04)' copiada de
       // los keyframes. En cuanto los keyframes cambiaron, la auditoria siguio
       // midiendo el balanceo viejo — y dando verde, que es lo peor que puede
       // hacer una comprobacion. Una constante duplicada a mano en el script
       // que vigila el original no vigila nada.
       //
       // Con getAnimations() se recorre la linea de tiempo de verdad y no hay
       // nada que mantener sincronizado. Doce pasos, no dos: el camino ya no
       // es recto, asi que los extremos no son necesariamente el peor caso.
       //
       // Con prefers-reduced-motion no hay animacion (anim = null) y el bucle
       // mide doce veces el estado en reposo, que es exactamente lo que se ve.
       const anim = hijo.getAnimations ? hijo.getAnimations()[0] : null;
       const t0 = anim ? anim.currentTime : 0;
       const dur = (anim && anim.effect) ? anim.effect.getComputedTiming().duration : 0;

       let invade = 0, nace = true;
       for (let k = 0; k <= 12; k++) {
         if (anim) { anim.pause(); anim.currentTime = dur * k / 12; }
         const r = sh.getBoundingClientRect();
         if (Math.min(r.right,foto.right) - Math.max(r.left,foto.left) > 1 &&
             Math.min(r.bottom,foto.bottom) - Math.max(r.top,foto.top) > 1) invade = 1;

         // El corte del asset no puede asomar por el borde en NINGUN instante,
         // no solo en reposo. Es la garantia que sostiene el balanceo nuevo:
         // desplazamientos <= 0 y giro antihorario, o sea que el viento solo
         // puede esconder mas el corte, nunca destaparlo.
         //
         // Ojo con una holgura real: al girar, la caja envolvente que devuelve
         // getBoundingClientRect crece ~1,7px, asi que este check afloja en esa
         // misma cantidad. Si algun dia el giro sube de ~0,5deg, hay que medir
         // las esquinas con la matriz en vez de fiarse de la envolvente.
         const ri = hijo.getBoundingClientRect();
         if (Math.round(ri.left - r.left) > 0 || Math.round(ri.top - r.top) > 0) nace = false;
       }
       if (anim) { anim.currentTime = t0; anim.play(); }

       // Contraste del titulo en el PEOR punto: sobre el fondo arena ya
       // oscurecido por la parte mas densa de la sombra. Medir contra el arena
       // limpio daria un 15 que no significa nada — el texto no cae ahi.
       const fuerza = parseFloat(getComputedStyle(sh).opacity);
       const arena = [239, 234, 224].map(c => c * (1 - fuerza));   // multiply con negro
       const L1 = lum(getComputedStyle(nombre).color);
       // concatenacion en vez de template literal: este bloque viaja dentro de
       // un String.raw en demo-consola.js, donde una interpolacion romperia el
       // literal. generar-demo.py lo comprueba y se planta si aparece una.
       const L2 = lum('rgb(' + arena.join(',') + ')');
       const ratio = (Math.max(L1,L2) + .05) / (Math.min(L1,L2) + .05);

       // la sombra tiene que ENTRAR desde el borde, no empezar dentro del panel:
       // el asset viene cortado por arriba y por la izquierda y ese corte solo
       // se lee bien si coincide con el borde. Offsets positivos lo dejan a la
       // vista — paso con --eco-sombra-x:8% en Villa C, 57px de hueco.
       // `nace` sale del bucle de arriba: se comprueba a lo largo de TODO el
       // balanceo, no solo en reposo.

       out[band.dataset.modelName] = {
         sombra: band.dataset.ecoShade,
         lado: band.dataset.ecoSide,
         invadeFoto: invade,
         recorta: getComputedStyle(sh).overflow === 'hidden',
         detrasTexto: getComputedStyle(sh).zIndex === '-1',
         naceDelBorde: nace,
         contraste: +ratio.toFixed(1),
         opacidad: getComputedStyle(sh).opacity
       };
     });

     const lados = [...document.querySelectorAll('.fg-type-band')].map(b =>
       getComputedStyle(b.querySelector('.fg-card-img')).gridColumnStart);
     out.zigzag = lados.every((l, i) => !i || l !== lados[i-1])
       ? 'ok' : 'ROTO — columnas: ' + lados.join(', ');

     // ritmos: si uno es multiplo de otro, acaban sincronizandose y se ve el loop.
     // ⚠️ La animacion vive en `.eco-shade i`. La exterior NO se mueve nunca —es
     // la que recorta, seccion 2— asi que leyendola a ella salian 0s en todas,
     // 0 % 0 = NaN, y el check cantaba SE SINCRONIZAN pasara lo que pasara.
     // Filtra los 0 porque con prefers-reduced-motion no hay animacion y eso no
     // es un fallo.
     //
     // ⚠️ Y entran TAMBIEN las dos capas de la banda verde. Cuando se les puso
     // movimiento, este check seguia mirando solo las villas: dos animaciones
     // nuevas quedaban fuera de la unica comprobacion que existe contra el
     // efecto gif. Un check que no cubre lo ultimo que añadiste da una
     // tranquilidad que no te has ganado.
     const rit = [...document.querySelectorAll('.eco-shade i, .eco-band-foliage')]
       .map(s => parseFloat(getComputedStyle(s).animationDuration))
       .concat(document.querySelector('.eco-band')
         ? [parseFloat(getComputedStyle(document.querySelector('.eco-band'), '::after').animationDuration)]
         : [])
       .filter(d => d > 0);
     out.ritmos = !rit.length ? 'sin animacion (reduced-motion)'
       : rit.every((a, i) => rit.every((b, j) =>
           i === j || (Math.max(a,b) % Math.min(a,b)) > .01)) ? 'ok' : 'SE SINCRONIZAN: ' + rit.join(', ');

     // el ::after del hero es del SITIO (difuminado blanco inferior, height:6%).
     // Si la capa vuelve a pisarlo, se rompe en silencio — ver seccion 1.
     //
     // ⚠️ El guard NO sobra. index.html se quedo solo con la seccion de
     // tipologias, o sea que AQUI no hay hero, y getComputedStyle(null,'::after')
     // lanza TypeError y se lleva por delante el `return out` — la auditoria
     // entera dejaba de dar resultado por una comprobacion que en esta pagina ni
     // aplica. Donde si hay hero (el sitio real, el demo de consola) se mide
     // exactamente igual que antes.
     const hero = document.querySelector('.fg-hero');
     if (hero) {
       const ha = getComputedStyle(hero, '::after');
       out.heroIntacto = ha.backgroundImage.includes('gradient') &&
                         !ha.backgroundImage.includes('eco-sombra');
     } else {
       out.heroIntacto = 'n/a — esta pagina no tiene hero';
     }

     // El follaje de la banda: el titular tiene que caber en la zona que la
     // mascara deja limpia (58% izquierdo). Que los stats rocen el degradado SI
     // vale — ahi la imagen es casi negra y el texto claro, asi que el
     // contraste sube. Medido sobre pixeles reales: 9.3 bajo el titular y 8.4
     // bajo el stat de la derecha, el peor. El minimo exigible es 4.5.
     const fol = document.querySelector('.eco-band-foliage');
     if (fol) {
       const b = document.querySelector('.eco-band').getBoundingClientRect();
       const t = document.querySelector('.eco-band-title').getBoundingClientRect();
       const cs = getComputedStyle(fol);
       out.follajeBanda = {
         tieneMascara: cs.maskImage !== 'none',
         // sin no-repeat se tesela: la imagen ya no llena la caja
         noSeRepite: cs.backgroundRepeat === 'no-repeat',
         titularEnZonaLimpia: (t.right - b.left) / b.width <= 0.58
       };
     }
     return out;
   })()
   ============================================================================ */
