Saltar al contenido
motionframe

Buscar piezas

Categorías

La pieza

Qué hace y dónde encaja

Al llegar a la galería, la sección se queda fija y el scroll vertical desplaza las tarjetas en horizontal. Cada tarjeta es una casa rural con un paisaje ilustrado por código en siete capas (cielo, sol o luna, sierras, aldea clara, arboleda y robles) que se mueven a distinta velocidad: el fondo se queda atrás y el primer plano adelanta. Los paisajes van del amanecer a la noche y la barra de progreso es un reloj: revela el degradado del día, mueve un sol-marcador, interpola la hora y sus marcas llevan a cada casa. En móvil pasa a carrusel táctil con snap y la misma barra. Encaja en alojamientos, bodegas, catálogos de proyectos, cartas por temporadas o cualquier colección que se cuente en orden. Cambia las paletas para estaciones, platos o fases de obra.

Prompt
≈13.778 tokens · con Lite
Librerías
gsap · lenis
Demo
32,1 KB · ligero
Prueba en frío
9,7/10 · 30-09-2026

Antes de copiar

Tu marca, dentro del prompt

Cambia lo que quieras y copia: el prompt sale ya con el nombre, los colores y los textos de tu cliente. Lo que dejes en blanco se queda como en la demo.

Colores

Sin cambios: se copia con los valores de la demo.

Gratis y entero

El prompt, entero y a la vista

Qué trae el prompt, por apartados

  1. 01Qué es
  2. 02Librerías y esqueleto
  3. 03Tokens de diseño
  4. 04Estructura y contenido
  5. 05Movimiento
  6. 06Código clave
  7. 07Interacción
  8. 08Adaptación a móvil y tablet
  9. 09Accesibilidad y movimiento reducido
  10. 10Rendimiento
  11. 11Personalización
  12. 12Comprobación final
Completo
5.835 palabras · ≈13.778 tokens
Lite
1.532 palabras · ≈3.146 tokens
Librerías
gsap 3.15.0 · lenis 1.3.26
Peso de la demo
32,1 KB (ligero)
Resolución máxima (DPR)
2
Se pausa fuera de pantalla
sí
Respeta «menos movimiento»
sí

Tokens estimados para español (≈3,6 caracteres por token): sirven para orientarte, no son exactos.

prompt.md
# Recrea en un único archivo HTML: Galería horizontal fijada

Una sección que se queda fija mientras el scroll vertical la recorre en horizontal: seis tarjetas con paisajes por capas que se desplazan en paralaje, una barra de progreso que es la hora del día y, en móvil, un carrusel táctil con la misma barra.

Eres un desarrollador front-end creativo senior. Genera un único `index.html` autocontenido que
reproduzca exactamente la pieza descrita: mismo layout, textos, colores, movimiento e interacción.
HTML, CSS y JS en un solo archivo, sin build ni frameworks. Librerías solo por CDN y en las versiones
indicadas. Todos los valores numéricos de este documento son definitivos: úsalos tal cual.
Responde solo con el archivo completo.

## Qué es

Una página corta de un conjunto de casas rurales ficticio, **Casas de Alder**, en el valle de Alder (Altmere). Tiene tres bloques:

1. **Portada** a pantalla completa (fondo papel cálido): navegación mínima, un arco fino que dibuja el recorrido del sol con las seis horas marcadas y un sol terracota que lo recorre despacio, un titular serif enorme en dos líneas («Un día entero / en el *valle de Alder*») y una fila inferior con texto, tres datos y un indicador «Baja para empezar el día».
2. **Galería horizontal fijada** (la pieza): en escritorio (≥ 768 px) la sección se fija (`pin`) a pantalla completa y el scroll vertical desplaza la pista de tarjetas en horizontal, 1:1. Hay seis tarjetas de casas, cada una asociada a una hora del día (06:40 primera luz → 22:10 noche azul). La parte superior de cada tarjeta es un paisaje ilustrado en SVG, **generado por código**, hecho de 7 capas (cielo, sol o luna, sierra lejana, bruma opcional, sierra media, loma con aldea clara, sierra cercana con arboleda, primer plano con robles). Cada capa se desplaza en horizontal a una velocidad distinta según la posición de la tarjeta en pantalla: paralaje dentro de la tarjeta. Al final de la pista hay una tarjeta oscura de cierre «¿Cuál es tu hora?». Debajo, una **barra de progreso** que es un reloj del día: una línea con el degradado de los seis cielos que se va revelando, un sol-marcador que avanza, seis marcas con la hora (botones que llevan a cada casa), la hora interpolada a la izquierda («Hora 16:38») y a la derecha «04 / 06 Molino de Alder».
3. **Reserva** (fondo noche): titular «Elige tu hora. / *La casa la ponemos nosotros.*», texto, teléfono y email grandes y pie.

En móvil (< 768 px) y con movimiento reducido no hay pin: la pista es un **carrusel táctil nativo** con `scroll-snap` y la misma barra de progreso, alimentada por el `scrollLeft` del carrusel. Sin JavaScript también funciona (carrusel, sin paisajes).

La sensación: editorial, cálida, calmada; la página va del amanecer a la noche a medida que bajas y acaba literalmente de noche.

## Librerías y esqueleto

- GSAP 3.15.0 + ScrollTrigger y Lenis 1.3.26, como scripts clásicos al final del `<body>`, en este orden:

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.min.js"></script>
<script>/* código de la pieza (ver «Código clave») */</script>
```

- En el `<head>`:

```html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Casas de Alder · Un día entero en el valle</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.css">
```

- `<html lang="es">`. Tipografías: **Instrument Serif** (display, regular e itálica) y **Hanken Grotesk** 400/500/600 (texto). Ambas tienen ñ, tildes, ¿ y ¡.
- Nada de imágenes: los paisajes son SVG generados en JS; el grano es un SVG `feTurbulence` en un data URI de CSS.

## Tokens de diseño

```css
:root{
  --papel:#efe9df;
  --papel-2:#e4dbcc;
  --tinta:#1f1b17;
  --tinta-2:#5a5249;
  --linea:rgba(31,27,23,.16);
  --acento:#9e3d26;
  --noche:#121824;
  --noche-texto:#e9e2d4;
  --serif:"Instrument Serif","Times New Roman",serif;
  --sans:"Hanken Grotesk",system-ui,sans-serif;
  --gutter:clamp(16px,4vw,56px);
  --dia:linear-gradient(90deg,#f3d2b4 0%,#8fb8d3 20%,#6fa6cc 40%,#d99e63 60%,#ec8d5f 80%,#2c4566 100%);
  --ease:cubic-bezier(.16,1,.3,1);
}
```

Colores fijos que no son tokens (úsalos tal cual): fondo de tarjeta `--papel-2`; textos sobre noche `#bdb5a6` (párrafos), `#c9b89c` (etiquetas), `#a79f91` (pie), `#e8a57f` (itálica del titular de reserva); número de escena `rgba(255,255,255,.92)`.

Degradado del día (`--dia`), de izquierda a derecha: `#f3d2b4` (primera luz) · `#8fb8d3` (mañana) · `#6fa6cc` (mediodía) · `#d99e63` (tarde) · `#ec8d5f` (atardecer) · `#2c4566` (noche). Se usa en la franja de 4 px sobre la portada y en el relleno de la barra de progreso.

## Estructura y contenido

Marcado completo del `<body>` (antes de los scripts). Copia los textos literalmente. Los paisajes no están en el HTML: el JS los inserta al principio de cada `.escena`.

```html
<header class="hero">
  <nav class="nav" aria-label="Principal">
    <a class="marca" href="#">
      <svg viewBox="0 0 28 28" aria-hidden="true"><circle class="marca-sol" cx="14" cy="13" r="6"/><path d="M1 22c5-6 9-8 13-5s8 1 13-3v11H1z" fill="#1f1b17"/></svg>
      Casas de Alder
    </a>
    <ul>
      <li><a href="#casas">Las casas</a></li>
      <li><a href="#reservar">Reservar</a></li>
    </ul>
  </nav>

  <div class="hero-medio">
    <svg class="arco" viewBox="-44 0 528 250" aria-hidden="true">
      <path id="recorrido" d="M20 225 A200 200 0 0 1 420 225" fill="none" stroke="currentColor" stroke-opacity=".3" stroke-dasharray="1 7" stroke-linecap="round"/>
      <line x1="0" y1="225" x2="440" y2="225" stroke="currentColor" stroke-opacity=".25"/>
      <g class="arco-marcas"></g>
      <circle class="arco-sol" r="9" cx="220" cy="25"/>
    </svg>
  </div>

  <div>
    <h1 data-in>Un día entero<br>en el <em>valle de Alder</em></h1>
    <div class="hero-pie">
      <p data-in>Seis casas rurales entre castaños y robles, a veinte minutos de Norvale. Recórrelas en el orden en que les llega la luz.</p>
      <ul class="datos" data-in>
        <li><strong>06</strong><span>casas</span></li>
        <li><strong>2–8</strong><span>huéspedes</span></li>
        <li><strong>20 min</strong><span>de Norvale</span></li>
      </ul>
      <a class="bajar" href="#casas" data-in><i aria-hidden="true"></i>Baja para empezar el día</a>
    </div>
  </div>
</header>

<main>
  <section class="galeria" id="casas" aria-labelledby="gal-titulo">
    <div class="gal-cab">
      <div>
        <p class="etq">Las casas · de 06:40 a 22:10</p>
        <h2 id="gal-titulo">Seis casas, de la primera luz a la última.</h2>
      </div>
      <p class="intro">Cada casa mira a una hora distinta del día. Avanza con calma: el valle se enciende y se apaga a tu paso.<span class="solo-carrusel"> Desliza las tarjetas o toca una hora de la barra.</span></p>
    </div>

    <div class="pista" tabindex="0" role="region" aria-label="Carrusel de casas">
      <article class="card" data-escena="0" data-hora="06:40" data-nombre="Casa del Roble" aria-labelledby="c1">
        <div class="escena" aria-hidden="true"><span class="escena-num">01</span></div>
        <div class="info">
          <p class="info-hora"><b>06:40</b>Primera luz</p>
          <h3 id="c1">Casa del Roble</h3>
          <p class="info-datos">Eskby · 4 huéspedes · 2 dormitorios</p>
          <p class="info-pie"><span>Desde <strong>165 €</strong> / noche · tasas incl.</span><a href="#reservar">Ver la casa<span class="vh"> Casa del Roble</span> <span aria-hidden="true">→</span></a></p>
        </div>
      </article>
      <article class="card card--ancha" data-escena="1" data-hora="10:15" data-nombre="El Castañar" aria-labelledby="c2">
        <div class="escena" aria-hidden="true"><span class="escena-num">02</span></div>
        <div class="info">
          <p class="info-hora"><b>10:15</b>Mañana clara</p>
          <h3 id="c2">El Castañar</h3>
          <p class="info-datos">Hollin · 6 huéspedes · 3 dormitorios · chimenea</p>
          <p class="info-pie"><span>Desde <strong>210 €</strong> / noche · tasas incl.</span><a href="#reservar">Ver la casa<span class="vh"> El Castañar</span> <span aria-hidden="true">→</span></a></p>
        </div>
      </article>
      <article class="card" data-escena="2" data-hora="13:30" data-nombre="La Solana" aria-labelledby="c3">
        <div class="escena" aria-hidden="true"><span class="escena-num">03</span></div>
        <div class="info">
          <p class="info-hora"><b>13:30</b>Mediodía</p>
          <h3 id="c3">La Solana</h3>
          <p class="info-datos">Merrow · 2 huéspedes · 1 dormitorio</p>
          <p class="info-pie"><span>Desde <strong>120 €</strong> / noche · tasas incl.</span><a href="#reservar">Ver la casa<span class="vh"> La Solana</span> <span aria-hidden="true">→</span></a></p>
        </div>
      </article>
      <article class="card" data-escena="3" data-hora="17:50" data-nombre="Molino de Alder" aria-labelledby="c4">
        <div class="escena" aria-hidden="true"><span class="escena-num">04</span></div>
        <div class="info">
          <p class="info-hora"><b>17:50</b>Tarde dorada</p>
          <h3 id="c4">Molino de Alder</h3>
          <p class="info-datos">Alderford · 8 huéspedes · 4 dormitorios</p>
          <p class="info-pie"><span>Desde <strong>290 €</strong> / noche · tasas incl.</span><a href="#reservar">Ver la casa<span class="vh"> Molino de Alder</span> <span aria-hidden="true">→</span></a></p>
        </div>
      </article>
      <article class="card card--ancha" data-escena="4" data-hora="20:45" data-nombre="Granja La Umbría" aria-labelledby="c5">
        <div class="escena" aria-hidden="true"><span class="escena-num">05</span></div>
        <div class="info">
          <p class="info-hora"><b>20:45</b>Atardecer</p>
          <h3 id="c5">Granja La Umbría</h3>
          <p class="info-datos">Wynmere · 5 huéspedes · 2 dormitorios · piscina</p>
          <p class="info-pie"><span>Desde <strong>185 €</strong> / noche · tasas incl.</span><a href="#reservar">Ver la casa<span class="vh"> Granja La Umbría</span> <span aria-hidden="true">→</span></a></p>
        </div>
      </article>
      <article class="card" data-escena="5" data-hora="22:10" data-nombre="La Atalaya" aria-labelledby="c6">
        <div class="escena" aria-hidden="true"><span class="escena-num">06</span></div>
        <div class="info">
          <p class="info-hora"><b>22:10</b>Noche azul</p>
          <h3 id="c6">La Atalaya</h3>
          <p class="info-datos">Fenlow · 2 huéspedes · 1 dormitorio</p>
          <p class="info-pie"><span>Desde <strong>140 €</strong> / noche · tasas incl.</span><a href="#reservar">Ver la casa<span class="vh"> La Atalaya</span> <span aria-hidden="true">→</span></a></p>
        </div>
      </article>
      <article class="card card--fin" aria-labelledby="c7">
        <div>
          <p class="etq">Reserva directa</p>
          <h3 id="c7">¿Cuál es tu hora?</h3>
        </div>
        <p>Te guardamos la casa que mejor encaje con tu viaje. Sin comisiones y con desayuno de productores del valle.</p>
        <a class="boton" href="#reservar">Reservar ahora <span aria-hidden="true">→</span></a>
      </article>
    </div>

    <div class="dia">
      <p class="dia-hora"><span class="etq">Hora</span><strong data-hora-actual>06:40</strong></p>
      <div class="dia-linea">
        <div class="dia-riel" aria-hidden="true"><span class="dia-relleno"></span><span class="dia-sol"></span></div>
        <ol class="dia-marcas"></ol>
      </div>
      <p class="dia-cuenta"><strong data-num>01</strong><span>/ 06</span><em data-nombre-actual>Casa del Roble</em></p>
    </div>
  </section>

  <section class="reservar" id="reservar" aria-labelledby="res-titulo">
    <div class="res-grid">
      <div>
        <p class="etq">Casas de Alder · valle de Alder</p>
        <h2 id="res-titulo">Elige tu hora.<br><em>La casa la ponemos nosotros.</em></h2>
      </div>
      <div class="res-texto">
        <p>Escríbenos con tus fechas y cuántos venís. Te respondemos en el día con disponibilidad y precio cerrado, tasas incl.</p>
        <ul class="contacto">
          <li><a href="tel:+00 000 000 000">+00 000 000 000</a></li>
          <li><a href="mailto:hola@casasdealder.com">hola@casasdealder.com</a></li>
        </ul>
      </div>
    </div>
    <footer class="pie">
      <span>© 2026 Casas de Alder · valle de Alder, Altmere</span>
      <span>Atendemos de 9:00 a 21:00, todos los días</span>
    </footer>
  </section>
</main>
```

Datos de las seis casas (ya están en el marcado; resumen para no equivocarse):

| # | Hora | Momento | Casa | Pueblo · capacidad | Precio | Forma |
|---|---|---|---|---|---|---|
| 01 | 06:40 | Primera luz | Casa del Roble | Eskby · 4 huéspedes · 2 dormitorios | 165 € | alta |
| 02 | 10:15 | Mañana clara | El Castañar | Hollin · 6 huéspedes · 3 dormitorios · chimenea | 210 € | ancha |
| 03 | 13:30 | Mediodía | La Solana | Merrow · 2 huéspedes · 1 dormitorio | 120 € | alta |
| 04 | 17:50 | Tarde dorada | Molino de Alder | Alderford · 8 huéspedes · 4 dormitorios | 290 € | alta |
| 05 | 20:45 | Atardecer | Granja La Umbría | Wynmere · 5 huéspedes · 2 dormitorios · piscina | 185 € | ancha |
| 06 | 22:10 | Noche azul | La Atalaya | Fenlow · 2 huéspedes · 1 dormitorio | 140 € | alta |

Todos los precios: «Desde X € / noche · tasas incl.». En el `href="tel:…"` escribe el teléfono sin espacios.

### CSS completo

Va en un único `<style>` en el `<head>`, justo después del bloque `:root` anterior:

```css
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--papel);color:var(--tinta);font:400 16px/1.55 var(--sans);-webkit-font-smoothing:antialiased;overflow-x:clip}
h1,h2,h3,p{margin:0}
a{color:inherit}
:focus-visible{outline:2px solid var(--acento);outline-offset:3px;border-radius:2px}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}
.etq{font:600 12px/1.2 var(--sans);letter-spacing:.14em;text-transform:uppercase;font-variant-numeric:tabular-nums}

/* ---------- Portada ---------- */
.hero{position:relative;min-height:100svh;display:grid;grid-template-rows:auto 1fr auto;padding:0 var(--gutter) clamp(28px,5svh,56px)}
.hero::before{content:"";position:absolute;inset:0 0 auto;height:4px;background:var(--dia)}
.nav{display:flex;align-items:center;justify-content:space-between;padding:28px 0}
.marca{display:flex;align-items:center;gap:10px;text-decoration:none;font:500 17px/1 var(--sans);letter-spacing:-.01em}
.marca svg{width:28px;height:28px}
.marca-sol,.arco-sol{fill:var(--acento)}
.nav ul{display:flex;gap:clamp(16px,3vw,36px);list-style:none;margin:0;padding:0}
.nav ul a{font-size:15px;text-decoration:none;background:linear-gradient(currentColor,currentColor) 0 100%/0 1px no-repeat;transition:background-size .5s var(--ease)}
.nav ul a:hover{background-size:100% 1px}
.hero-medio{display:flex;justify-content:flex-end;align-items:center}
.arco{width:min(528px,56vw);height:auto;color:var(--tinta);overflow:visible}
.arco text{font:500 11px var(--sans);fill:var(--tinta-2);letter-spacing:.04em}
.hero h1{font:400 clamp(48px,8.4vw,140px)/.92 var(--serif);letter-spacing:-.02em}
.hero h1 em{color:var(--acento)}
.hero-pie{display:grid;grid-template-columns:1fr 1fr auto;gap:24px 48px;align-items:end;margin-top:clamp(24px,4svh,44px);padding-top:20px;border-top:1px solid var(--linea)}
.hero-pie p{max-width:40ch;color:var(--tinta-2)}
.datos{display:flex;gap:clamp(20px,3vw,44px);list-style:none;margin:0;padding:0}
.datos strong{display:block;font:400 34px/1 var(--serif);letter-spacing:-.01em;color:var(--tinta)}
.datos span{font-size:13px;color:var(--tinta-2)}
.bajar{display:flex;align-items:center;gap:12px;font-size:14px;text-decoration:none;white-space:nowrap}
.bajar i{position:relative;display:block;width:1px;height:44px;background:var(--linea);overflow:hidden}
.bajar i::after{content:"";position:absolute;inset:0;background:var(--tinta);transform:translateY(-100%);animation:caer 2.4s var(--ease) infinite}
@keyframes caer{0%{transform:translateY(-100%)}55%,100%{transform:translateY(100%)}}

/* ---------- Galería (base: carrusel táctil) ---------- */
.galeria{position:relative;padding:clamp(56px,9svh,96px) 0 clamp(40px,6svh,64px)}
.gal-cab{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:16px 40px;padding:0 var(--gutter) clamp(20px,3svh,32px)}
.gal-cab .etq{color:var(--acento);margin-bottom:12px}
.gal-cab h2{font:400 clamp(32px,3.6vw,54px)/1 var(--serif);letter-spacing:-.015em;max-width:18ch}
.gal-cab p.intro{max-width:36ch;color:var(--tinta-2)}
.solo-carrusel{display:inline}
.pista{position:relative;display:flex;gap:16px;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;padding:4px var(--gutter) 8px;scroll-padding-inline:var(--gutter);scrollbar-width:none}
.pista::-webkit-scrollbar{display:none}
.card{position:relative;flex:0 0 min(84vw,400px);display:flex;flex-direction:column;background:var(--papel-2);scroll-snap-align:start}
.card--ancha{flex-basis:min(84vw,400px)}
.escena{position:relative;aspect-ratio:4/5;overflow:hidden;background:#cfc6b6;isolation:isolate}
.escena::after{content:"";position:absolute;inset:0;z-index:9;pointer-events:none;opacity:.5;mix-blend-mode:multiply;background:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .5 0'/></filter><rect width='100%' height='100%' filter='url(%23g)'/></svg>")}
.cielo,.capa{position:absolute;inset:0}
.capa{inset:0 -12%;will-change:transform}
.capa svg{position:absolute;inset:0;width:100%;height:100%;display:block}
.bruma{background:linear-gradient(180deg,transparent 50%,var(--bruma) 63%,transparent 78%);opacity:.75}
.escena-num{position:absolute;z-index:10;top:14px;right:16px;font:italic 400 30px/1 var(--serif);color:rgba(255,255,255,.92);text-shadow:0 1px 12px rgba(0,0,0,.18)}
.info{display:grid;gap:6px;padding:18px 20px 20px}
.info-hora{font-size:13px;color:var(--tinta-2)}
.info-hora b{font-weight:600;color:var(--acento);font-variant-numeric:tabular-nums;margin-right:6px}
.info h3{font:400 clamp(28px,2.5vw,38px)/1.02 var(--serif);letter-spacing:-.01em}
.info-datos{font-size:14px;color:var(--tinta-2)}
.info-pie{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:6px 16px;margin-top:8px;padding-top:12px;border-top:1px solid var(--linea);font-size:14px}
.info-pie strong{font-weight:600}
.info-pie a{font-weight:500;text-decoration:none;white-space:nowrap}
.info-pie a span[aria-hidden]{display:inline-block;transition:transform .5s var(--ease)}
.info-pie a:hover span[aria-hidden]{transform:translateX(5px)}
.card--fin{justify-content:space-between;gap:28px;padding:28px 24px;background:var(--noche);color:var(--noche-texto)}
.card--fin .etq{color:#c9b89c}
.card--fin h3{font:400 clamp(40px,4vw,64px)/.95 var(--serif);letter-spacing:-.02em;margin-top:14px}
.card--fin p{color:#bdb5a6;max-width:30ch}
.boton{display:inline-flex;align-items:center;gap:10px;align-self:flex-start;padding:14px 22px;border-radius:999px;background:var(--noche-texto);color:var(--noche);font-weight:600;text-decoration:none;transition:transform .5s var(--ease),background-color .3s}
.boton:hover{transform:translateY(-2px);background:#fff}

/* barra de progreso: la hora del valle */
.dia{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:28px;padding:clamp(18px,3svh,28px) var(--gutter) 0}
.dia-hora{display:flex;align-items:baseline;gap:10px;min-width:150px}
.dia-hora .etq{color:var(--tinta-2)}
.dia-hora strong{font:400 30px/1 var(--serif);font-variant-numeric:tabular-nums;min-width:3.2ch}
.dia-linea{position:relative;height:44px}
.dia-riel{position:absolute;left:0;right:0;top:13px;height:3px;background:var(--linea);border-radius:3px}
.dia-relleno{position:absolute;inset:0;border-radius:3px;background:var(--dia);clip-path:inset(0 100% 0 0)}
.dia-sol{position:absolute;z-index:2;left:-7px;top:-7px;width:17px;height:17px;border-radius:50%;background:var(--papel);border:2px solid var(--tinta);will-change:transform}
.dia-marcas{position:absolute;inset:0;list-style:none;margin:0;padding:0}
.dia-marcas li{position:absolute;top:0;transform:translateX(-50%)}
.dia-marcas button{display:grid;justify-items:center;gap:4px;min-width:40px;padding:12px 4px 0;border:0;background:none;color:var(--tinta-2);font:500 11px/1 var(--sans);letter-spacing:.04em;font-variant-numeric:tabular-nums;cursor:pointer}
.dia-marcas button::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor;margin-bottom:8px}
.dia-marcas button[aria-current="true"]{color:var(--tinta)}
.dia-cuenta{display:flex;align-items:baseline;gap:8px;font-size:14px;color:var(--tinta-2);min-width:230px;justify-content:flex-end;font-variant-numeric:tabular-nums}
.dia-cuenta strong{font:400 30px/1 var(--serif);color:var(--tinta)}
.dia-cuenta em{font-style:normal;color:var(--tinta);margin-left:8px}

/* ---------- Galería fijada (escritorio, con movimiento) ---------- */
.is-pinned .galeria{height:100svh;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto;padding:clamp(28px,6svh,64px) 0 clamp(20px,3.5svh,36px);overflow:clip}
.is-pinned .solo-carrusel{display:none}
.is-pinned .pista{overflow:visible;scroll-snap-type:none;width:max-content;height:100%;gap:clamp(20px,2.4vw,40px);padding:0 var(--gutter)}
.is-pinned .card{flex:0 0 auto;height:100%;aspect-ratio:.74;max-width:70vw}
.is-pinned .card--ancha{aspect-ratio:1.2;max-width:84vw}
.is-pinned .card--fin{aspect-ratio:.8;padding:clamp(24px,3vw,40px)}
.is-pinned .escena{aspect-ratio:auto;flex:1;min-height:0}

/* ---------- Reserva ---------- */
.reservar{background:var(--noche);color:var(--noche-texto);padding:clamp(80px,14svh,150px) var(--gutter) 40px}
.res-grid{display:grid;grid-template-columns:1.3fr 1fr;gap:48px;align-items:end}
.reservar .etq{color:#c9b89c;margin-bottom:20px}
.reservar h2{font:400 clamp(44px,6.4vw,104px)/.92 var(--serif);letter-spacing:-.02em}
.reservar h2 em{color:#e8a57f}
.res-texto p{color:#bdb5a6;max-width:38ch;margin-bottom:28px}
.contacto{display:grid;gap:4px;list-style:none;margin:0;padding:0}
.contacto a{font:400 clamp(24px,2.4vw,34px)/1.3 var(--serif);text-decoration:none;background:linear-gradient(currentColor,currentColor) 0 100%/0 1px no-repeat;transition:background-size .6s var(--ease)}
.contacto a:hover{background-size:100% 1px}
.pie{display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;margin-top:clamp(64px,12svh,120px);padding-top:20px;border-top:1px solid rgba(233,226,212,.18);font-size:13px;color:#a79f91}

/* ---------- Adaptación ---------- */
@media (max-width:900px){
  .hero-pie{grid-template-columns:1fr auto}
  .hero-pie p{grid-column:1/-1;grid-row:1}
}
@media (max-width:767px){
  .nav ul{gap:18px}
  .nav ul li:first-child{display:none}
  .hero-medio{justify-content:center;padding:24px 0}
  .arco{width:100%}
  .hero-pie{grid-template-columns:1fr;gap:22px}
  .datos strong{font-size:28px}
  .bajar{display:none}
  .gal-cab h2{font-size:clamp(32px,9vw,40px)}
  .dia{grid-template-columns:1fr auto;gap:10px 16px}
  .dia-linea{grid-column:1/-1;grid-row:2;height:36px}
  .dia-marcas button{font-size:0;gap:0;min-width:32px;min-height:32px}
  .dia-hora{min-width:0}
  .dia-cuenta{min-width:0}
  .dia-cuenta em{display:none}
  .res-grid{grid-template-columns:1fr}
}
@media (min-width:768px){
  .card--ancha{flex-basis:min(84vw,600px)}
  .card--ancha .escena{aspect-ratio:5/4}
  .is-pinned .card--ancha .escena{aspect-ratio:auto}
}
@media (prefers-reduced-motion:reduce){
  .bajar i::after{animation:none;transform:none}
  .pista{scroll-behavior:auto}
  *{transition-duration:.01ms!important}
}
```

Claves del layout que no debes cambiar:
- **Modo carrusel (base, sin clase)**: `.pista` es `display:flex` con `overflow-x:auto`, `scroll-snap-type:x mandatory`, tarjetas de `min(84vw,400px)` (las anchas `min(84vw,600px)` desde 768 px) con `scroll-snap-align:start`; escenas `aspect-ratio:4/5` (anchas `5/4` desde 768 px).
- **Modo fijado (`html.is-pinned`, lo pone el JS)**: `.galeria` mide `100svh` y es una rejilla de 3 filas `auto / minmax(0,1fr) / auto` con **`grid-template-columns:minmax(0,1fr)`** (si no, la pista de ancho `max-content` ensancha la columna y la cabecera y la barra se salen de la pantalla) y `overflow:clip` (evita que el foco desplace la pista por su cuenta). La pista es `width:max-content; height:100%` y cada tarjeta toma la altura de la fila con `aspect-ratio:.74` (altas, `max-width:70vw`), `1.2` (anchas, `max-width:84vw`) y `.8` (cierre). La escena ocupa el alto sobrante (`flex:1; min-height:0`).
- Cada `.capa` sobresale un 12 % por cada lado (`inset:0 -12%`) para que el paralaje nunca deje huecos; el desplazamiento máximo es el 10 % del ancho de la tarjeta.

## Movimiento

| Elemento | Propiedad | De → a | Duración | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Scroll de la página | suavizado Lenis | — | continuo | `lerp: 0.1` | — | rueda / trackpad (táctil nativo) |
| `.pista` (≥ 768 px) | `x` | `0` → `-(pista.scrollWidth - clientWidth)` | ligada al scroll 1:1 | `none` | — | ScrollTrigger `pin`, `start:'top top'`, `end:'+=' + distancia`, `scrub:true` |
| `.capa` de cada escena | `translate3d(X,0,0)` | `X = -off · f · anchoTarjeta · 0.1`, `off ∈ [-1,1]` | continuo | lineal | — | cada actualización del scroll (pin o carrusel) |
| Factores `f` de capa | — | astro `1` · lejos `0.75` · bruma `0.62` · medio `0.5` · loma `0.32` · cerca `0.14` · primer plano `-0.22` · cielo sin mover | — | — | — | — |
| `.dia-relleno` | `clip-path: inset(0 R% 0 0)` | `R = 100 - p·100` | continuo | lineal | — | scroll |
| `.dia-sol` | `translate3d(p·anchoRiel,0,0)` | `0` → ancho del riel | continuo | lineal | — | scroll |
| Hora («Hora 16:38») | texto | interpolada en minutos entre las horas de las dos casas vecinas | continuo | lineal | — | scroll |
| Número / nombre activo | texto + `aria-current` | casa más cercana (`Math.round(f)`) | instantáneo | — | — | scroll |
| Portada `[data-in]` (h1, párrafo, datos, «Baja…») | `y`, `autoAlpha` | `28px, 0` → `0, 1` | 1.2 s | `expo.out` | 0.1 s, `stagger 0.09` | carga |
| `.arco-sol` | posición sobre el arco | `u 0 → 1` y vuelta (`yoyo`, infinito) | 14 s por trayecto | `sine.inOut` | — | carga; pausa si la portada no se ve |
| Línea de «Baja para empezar el día» | `translateY` | `-100%` → `100%` (55 % del ciclo), luego espera | 2.4 s infinito | `cubic-bezier(.16,1,.3,1)` | — | CSS |
| Salto a una casa (marcas de la barra, foco) | scroll | posición que centra la tarjeta | 1.4 s | easing por defecto de Lenis | — | clic / `focusin` |
| Subrayado de enlaces de nav | `background-size` | `0 1px` → `100% 1px` | 0.5 s | `--ease` | — | hover |
| Flecha de «Ver la casa» | `translateX` | `0` → `5px` | 0.5 s | `--ease` | — | hover |
| Botón «Reservar ahora» | `translateY`, fondo | `0` → `-2px`, `#e9e2d4` → `#fff` | 0.5 s / 0.3 s | `--ease` | — | hover |
| Enlaces de contacto | subrayado | `0` → `100%` | 0.6 s | `--ease` | — | hover |

Definiciones:
- `off = clamp(-1, 1, (centroTarjeta - vista/2) / (vista/2 + anchoTarjeta/2))`, donde `centroTarjeta = offsetLeft + ancho/2 - desplazamiento` y `vista` es el ancho visible (ventana en modo fijado, `pista.clientWidth` en carrusel). Signo negativo: cuando la tarjeta avanza hacia la izquierda, el sol y las sierras lejanas se quedan atrás (parecen lejos) y el primer plano adelanta a la tarjeta (parece cerca).
- `f` (índice fraccionario) = qué casa pasa por el punto de referencia: el centro de la ventana en modo fijado, o el centro inicial de la primera tarjeta en carrusel (así, con el snap, cada tarjeta encajada da un entero exacto). `p = f / 5`.

## Código clave

JavaScript completo (un único `<script>` tras los tres de las librerías). Cópialo literal:

```js
(() => {
  // ---------- Paletas: una por hora del día ----------
  const PAL = [
    { c:['#b9c3d4','#f3d2b4'], s:'#fff1dc', sp:[380,455], l:'#a9a9bf', m:'#8f90a8', h:'#7d8a85', n:'#56685e', fr:'#2f3b35', cal:'#f4ede4', t:'#b06a52', v:'#4a3d38', bruma:'#f6e3d2', torre:1, seed:11 },
    { c:['#8fb8d3','#e3ecec'], s:'#fffbef', sp:[860,230], l:'#9ab4bf', m:'#7b9a95', h:'#6f8d6a', n:'#4c6a4a', fr:'#2b3d2b', cal:'#f7f4ee', t:'#b5654a', v:'#3f3a36', bruma:'#eef3f2', seed:23 },
    { c:['#6fa6cc','#efe8d2'], s:'#fffdf4', sp:[640,150], l:'#a7b39a', m:'#98a071', h:'#b39a5f', n:'#6f7440', fr:'#3b3d22', cal:'#fbf8f1', t:'#bf6c49', v:'#3f362c', torre:1, seed:37 },
    { c:['#d99e63','#f5dfb1'], s:'#fff0c8', sp:[930,360], l:'#caa079', m:'#b0805d', h:'#9a6f45', n:'#6e4e30', fr:'#3a2a1b', cal:'#f6e7d0', t:'#a9533a', v:'#4b3526', seed:41 },
    { c:['#4b3f66','#ec8d5f'], s:'#ffb07a', sp:[300,470], l:'#7a5a7a', m:'#5c4561', h:'#48364f', n:'#33263a', fr:'#1c1522', cal:'#e8c9b8', t:'#7a3b34', v:'#ffcf8a', torre:1, seed:53 },
    { c:['#0d1a2e','#2c4566'], s:'#f1ead7', sp:[900,170], l:'#23344c', m:'#1b2a3f', h:'#172437', n:'#111b2a', fr:'#0a111c', cal:'#9fa9b8', t:'#4f4450', v:'#ffd27f', noche:1, seed:67 }
  ];
  const W = 1240, H = 800;
  const rng = (a) => () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
  const f1 = (n) => n.toFixed(1);

  // Perfil de sierra: suma de senos + un poco de azar, 41 puntos de 0 a W
  function perfil(r, base, amp, freq) {
    const a = r() * 6.28, b = r() * 6.28, pts = [];
    for (let i = 0; i <= 40; i++) {
      const x = i / 40 * W;
      pts.push([x, base - amp * (0.6 * Math.sin(x * freq + a) + 0.4 * Math.sin(x * freq * 2.3 + b) + 0.18 * (r() - 0.5))]);
    }
    return pts;
  }
  function trazado(pts, color) {
    let d = `M0 ${H} L0 ${f1(pts[0][1])}`;
    for (let i = 1; i < pts.length - 1; i++) {
      const [x, y] = pts[i], [nx, ny] = pts[i + 1];
      d += ` Q${f1(x)} ${f1(y)} ${f1((x + nx) / 2)} ${f1((y + ny) / 2)}`;
    }
    return `<path d="${d} L${W} ${f1(pts[pts.length - 1][1])} L${W} ${H}Z" fill="${color}"/>`;
  }
  function alturaEn(pts, x) {
    const i = Math.min(pts.length - 2, Math.max(0, Math.floor(x / W * (pts.length - 1))));
    const [ax, ay] = pts[i], [bx, by] = pts[i + 1];
    return ay + (by - ay) * (x - ax) / (bx - ax);
  }
  const capa = (f, inner) => `<div class="capa" data-f="${f}"><svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMax slice">${inner}</svg></div>`;

  function escena(p, i) {
    const r = rng(p.seed);
    // astro (sol o luna) y estrellas
    let astro = `<defs><radialGradient id="halo${i}"><stop offset="0" stop-color="${p.s}" stop-opacity=".75"/><stop offset="1" stop-color="${p.s}" stop-opacity="0"/></radialGradient></defs>`
      + `<circle cx="${p.sp[0]}" cy="${p.sp[1]}" r="${p.noche ? 130 : 220}" fill="url(#halo${i})"/><circle cx="${p.sp[0]}" cy="${p.sp[1]}" r="${p.noche ? 22 : 34}" fill="${p.s}"/>`;
    if (p.noche) for (let k = 0; k < 70; k++) astro += `<circle cx="${(r() * W) | 0}" cy="${(r() * 430) | 0}" r="${f1(0.6 + r() * 1.3)}" fill="#fff" fill-opacity="${(0.35 + r() * 0.6).toFixed(2)}"/>`;
    // sierras
    const lejos = perfil(r, 470, 60, 0.006);
    const medio = perfil(r, 545, 46, 0.009);
    const loma = perfil(r, 612, 24, 0.005).map(([x, y]) => [x, y - 78 * Math.exp(-(((x - 620) / 280) ** 2))]);
    const cerca = perfil(r, 690, 24, 0.013);
    const suelo = perfil(r, 772, 12, 0.02);
    // aldea clara sobre la loma
    let casas = '';
    if (p.torre) {
      const x = 600 + (r() - 0.5) * 80, y = alturaEn(loma, x) + 6;
      casas += `<rect x="${f1(x - 10)}" y="${f1(y - 92)}" width="20" height="104" fill="${p.cal}"/><path d="M${f1(x - 12)} ${f1(y - 92)}L${f1(x)} ${f1(y - 114)}L${f1(x + 12)} ${f1(y - 92)}Z" fill="${p.t}"/><rect x="${f1(x - 4)}" y="${f1(y - 84)}" width="8" height="13" rx="4" fill="${p.v}"/>`;
    }
    const nC = 8 + Math.floor(r() * 4), lista = [];
    for (let k = 0; k < nC; k++) {
      const x = 620 + (k - (nC - 1) / 2) * 46 + (r() - 0.5) * 18;
      lista.push({ x, y: alturaEn(loma, x) + 8 + (k % 2) * 16, w: 34 + r() * 28, h: 24 + r() * 20 });
    }
    lista.sort((a, b) => a.y - b.y);
    for (const c of lista) {
      const x0 = c.x - c.w / 2, y0 = c.y - c.h;
      casas += `<rect x="${f1(x0)}" y="${f1(y0)}" width="${f1(c.w)}" height="${f1(c.h + 14)}" fill="${p.cal}"/><path d="M${f1(x0 - 3)} ${f1(y0)}L${f1(x0 + 6)} ${f1(y0 - 9)}L${f1(x0 + c.w - 6)} ${f1(y0 - 9)}L${f1(x0 + c.w + 3)} ${f1(y0)}Z" fill="${p.t}"/>`;
      const nv = c.w > 48 ? 2 : 1;
      for (let v = 0; v < nv; v++) casas += `<rect x="${f1(x0 + (v + 1) * c.w / (nv + 1) - 3)}" y="${f1(y0 + 9)}" width="6" height="9" fill="${p.v}"/>`;
    }
    // arboleda en la sierra cercana
    let arboles = '';
    for (let x = 10; x < W; x += 34 + r() * 46) {
      if (r() < 0.45) continue;
      const y = alturaEn(cerca, x) + 6, s = 0.7 + r() * 0.8;
      arboles += `<circle cx="${f1(x)}" cy="${f1(y - 14 * s)}" r="${f1(13 * s)}"/><circle cx="${f1(x + 11 * s)}" cy="${f1(y - 8 * s)}" r="${f1(10 * s)}"/><circle cx="${f1(x - 10 * s)}" cy="${f1(y - 7 * s)}" r="${f1(9 * s)}"/>`;
    }
    // robles de primer plano
    const copa = (cx, cy, s) => `<rect x="${f1(cx - 5 * s)}" y="${cy}" width="${f1(10 * s)}" height="160"/><circle cx="${cx}" cy="${f1(cy - 30 * s)}" r="${f1(60 * s)}"/><circle cx="${f1(cx - 50 * s)}" cy="${cy}" r="${f1(45 * s)}"/><circle cx="${f1(cx + 55 * s)}" cy="${f1(cy - 5 * s)}" r="${f1(50 * s)}"/><circle cx="${f1(cx + 10 * s)}" cy="${f1(cy + 22 * s)}" r="${f1(44 * s)}"/>`;
    return `<div class="cielo" style="background:linear-gradient(180deg,${p.c[0]},${p.c[1]})"></div>`
      + capa(1, astro)
      + capa(0.75, trazado(lejos, p.l))
      + (p.bruma ? `<div class="capa bruma" data-f="0.62" style="--bruma:${p.bruma}"></div>` : '')
      + capa(0.5, trazado(medio, p.m))
      + capa(0.32, trazado(loma, p.h) + casas)
      + capa(0.14, trazado(cerca, p.n) + `<g fill="${p.fr}" fill-opacity=".45">${arboles}</g>`)
      + capa(-0.22, trazado(suelo, p.fr) + `<g fill="${p.fr}">${copa(150, 650, 1.1)}${copa(1100, 670, 0.9)}</g>`);
  }

  // ---------- Montaje ----------
  const root = document.documentElement;
  const galeria = document.querySelector('.galeria');
  const pista = document.querySelector('.pista');
  const cards = [...pista.querySelectorAll('.card[data-escena]')];
  const hijos = [...pista.children];
  cards.forEach((c) => { const i = +c.dataset.escena; c.querySelector('.escena').insertAdjacentHTML('afterbegin', escena(PAL[i], i)); });
  const capas = cards.map((c) => [...c.querySelectorAll('.capa')].map((el) => ({ el, f: +el.dataset.f })));
  const aMin = (h) => { const [a, b] = h.split(':').map(Number); return a * 60 + b; };
  const fmt = (m) => { m = Math.round(m); return String(Math.floor(m / 60)).padStart(2, '0') + ':' + String(m % 60).padStart(2, '0'); };
  const horas = cards.map((c) => c.dataset.hora), mins = horas.map(aMin), nombres = cards.map((c) => c.dataset.nombre);
  const N = cards.length;

  // marcas de la barra
  const marcas = document.querySelector('.dia-marcas');
  marcas.innerHTML = horas.map((h, i) => `<li style="left:${(i / (N - 1)) * 100}%"><button type="button" data-i="${i}" aria-label="Ir a ${nombres[i]}, ${h}">${h}</button></li>`).join('');
  const botones = [...marcas.querySelectorAll('button')];
  const relleno = document.querySelector('.dia-relleno'), sol = document.querySelector('.dia-sol'), riel = document.querySelector('.dia-riel');
  const elHora = document.querySelector('[data-hora-actual]'), elNum = document.querySelector('[data-num]'), elNombre = document.querySelector('[data-nombre-actual]');

  // arco del sol en la portada (06:00 → 23:00)
  const arcoPos = (m, rad = 200) => { const a = Math.PI * (1 - (m - 360) / 1020); return [220 + rad * Math.cos(a), 225 - rad * Math.sin(a)]; };
  document.querySelector('.arco-marcas').innerHTML = mins.map((m, i) => {
    const [x, y] = arcoPos(m), [tx, ty] = arcoPos(m, 222);
    return `<circle cx="${f1(x)}" cy="${f1(y)}" r="3" fill="#1f1b17"/><text x="${f1(tx)}" y="${f1(ty + 4)}" text-anchor="${i < 2 ? 'end' : i > 3 ? 'start' : 'middle'}">${horas[i]}</text>`;
  }).join('');
  const arcoSol = document.querySelector('.arco-sol');

  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  let pinned = false, movimiento = !reduce.matches, geo = [], vista = 0, railW = 0, ref = 0, activo = -1, st = null, lenis = null;

  function medir() {
    geo = cards.map((c) => ({ l: c.offsetLeft, w: c.offsetWidth }));
    vista = pinned ? root.clientWidth : pista.clientWidth;
    ref = pinned ? vista / 2 : geo[0].l + geo[0].w / 2;
    railW = riel.clientWidth;
  }
  const desplazamiento = () => pinned ? -gsap.getProperty(pista, 'x') : pista.scrollLeft;

  function pintar() {
    const sx = desplazamiento(), centros = [];
    geo.forEach((g, i) => {
      const cx = g.l + g.w / 2 - sx;
      centros.push(cx);
      if (!movimiento) return;
      const off = Math.max(-1, Math.min(1, (cx - vista / 2) / (vista / 2 + g.w / 2)));
      for (const k of capas[i]) k.el.style.transform = `translate3d(${(-off * k.f * g.w * 0.1).toFixed(2)}px,0,0)`;
    });
    // índice fraccionario: qué casa pasa por el punto de referencia
    let f = 0;
    if (centros[N - 1] <= ref) f = N - 1;
    else if (centros[0] < ref) for (let i = 0; i < N - 1; i++) if (centros[i + 1] > ref) { f = i + (ref - centros[i]) / (centros[i + 1] - centros[i]); break; }
    const p = f / (N - 1);
    relleno.style.clipPath = `inset(0 ${(100 - p * 100).toFixed(2)}% 0 0)`;
    sol.style.transform = `translate3d(${(p * railW).toFixed(1)}px,0,0)`;
    const i = Math.min(N - 2, Math.floor(f)), hora = fmt(mins[i] + (mins[i + 1] - mins[i]) * (f - i));
    if (elHora.textContent !== hora) elHora.textContent = hora;
    const a = Math.round(f);
    if (a !== activo) {
      activo = a;
      elNum.textContent = String(a + 1).padStart(2, '0');
      elNombre.textContent = nombres[a];
      botones.forEach((b, j) => b.setAttribute('aria-current', j === a ? 'true' : 'false'));
    }
  }

  function irA(el) {
    if (pinned && st) {
      const dist = st.end - st.start;
      const x = Math.max(0, Math.min(dist, el.offsetLeft + el.offsetWidth / 2 - vista / 2));
      lenis ? lenis.scrollTo(st.start + x, { duration: 1.4 }) : window.scrollTo(0, st.start + x);
    } else {
      pista.scrollTo({ left: el.offsetLeft - geo[0].l, behavior: movimiento ? 'smooth' : 'auto' });
    }
  }
  botones.forEach((b) => b.addEventListener('click', () => irA(cards[+b.dataset.i])));
  pista.addEventListener('focusin', (e) => {
    if (!pinned) return;
    const c = e.target.closest('.card');
    if (c) irA(c);
  });

  let pendiente = false;
  const programar = () => { if (pendiente) return; pendiente = true; requestAnimationFrame(() => { pendiente = false; pintar(); }); };
  pista.addEventListener('scroll', () => { if (!pinned) programar(); }, { passive: true });
  window.addEventListener('resize', () => { medir(); programar(); });

  // ---------- GSAP + Lenis ----------
  gsap.registerPlugin(ScrollTrigger);
  if (movimiento) {
    lenis = new Lenis({ lerp: 0.1, anchors: true });
    lenis.on('scroll', ScrollTrigger.update);
    gsap.ticker.add((t) => lenis.raf(t * 1000));
    gsap.ticker.lagSmoothing(0);
  }

  const mm = gsap.matchMedia();
  mm.add({ escritorio: '(min-width: 768px)', mov: '(prefers-reduced-motion: no-preference)' }, (ctx) => {
    const { escritorio, mov } = ctx.conditions;
    movimiento = mov;
    if (!(escritorio && mov)) { pinned = false; medir(); pintar(); return; }

    pinned = true;
    root.classList.add('is-pinned');
    pista.removeAttribute('tabindex');
    pista.scrollLeft = 0;
    const dist = () => Math.max(0, pista.scrollWidth - root.clientWidth);
    const tween = gsap.to(pista, {
      x: () => -dist(),
      ease: 'none',
      onUpdate: pintar,
      scrollTrigger: {
        trigger: galeria,
        start: 'top top',
        end: () => '+=' + dist(),
        pin: true,
        scrub: true,
        invalidateOnRefresh: true,
        onRefresh: () => { medir(); pintar(); }
      }
    });
    st = tween.scrollTrigger;
    return () => {
      pinned = false; st = null;
      root.classList.remove('is-pinned');
      pista.setAttribute('tabindex', '0');
      gsap.set(pista, { clearProps: 'transform' });
      capas.flat().forEach((k) => { k.el.style.transform = ''; });
    };
  });

  // entrada de la portada y sol que recorre el arco
  const camino = document.getElementById('recorrido'), largo = camino.getTotalLength();
  const ponerSol = (u) => { const pt = camino.getPointAtLength(u * largo); arcoSol.setAttribute('cx', f1(pt.x)); arcoSol.setAttribute('cy', f1(pt.y)); };
  if (movimiento) {
    gsap.from('[data-in]', { y: 28, autoAlpha: 0, duration: 1.2, ease: 'expo.out', stagger: 0.09, delay: 0.1 });
    const s = { u: 0 };
    const giro = gsap.to(s, { u: 1, duration: 14, ease: 'sine.inOut', repeat: -1, yoyo: true, onUpdate: () => ponerSol(s.u) });
    new IntersectionObserver(([e]) => { e.isIntersecting ? giro.play() : giro.pause(); }).observe(document.querySelector('.hero'));
  } else {
    ponerSol(0.5);
  }

  document.fonts && document.fonts.ready.then(() => ScrollTrigger.refresh());
  medir(); pintar();
})();
```

Puntos delicados del código:
- **Paisaje procedural**: `perfil()` genera 41 puntos por sierra como suma de dos senos más un 18 % de azar sembrado (`rng` = mulberry32 con la semilla de cada paleta, así cada casa sale igual siempre). `trazado()` los une con curvas cuadráticas por puntos medios y cierra el polígono por abajo. La loma del pueblo lleva una joroba gaussiana centrada en x = 620 (`78 · e^-((x-620)/280)²`). Las casas (8–11) se reparten cada 46 unidades alrededor del centro, en dos filas alternas (+16), se ordenan por `y` para que las de delante tapen a las de detrás y llevan tejado trapezoidal y 1–2 ventanas. Tres paletas tienen campanario. La noche añade 70 estrellas y ventanas encendidas `#ffd27f`.
- Cada capa es un `<div class="capa">` con su propio `<svg viewBox="0 0 1240 800" preserveAspectRatio="xMidYMax slice">`: se mueve con `transform` en compositor, sin repintar el SVG.
- `medir()` cachea `offsetLeft`/`offsetWidth` de las tarjetas, el ancho visible y el del riel; se llama en `resize` y en cada `onRefresh` de ScrollTrigger. `pintar()` solo escribe transformaciones y textos cuando cambian.
- El `gsap.matchMedia()` con las condiciones `(min-width: 768px)` y `(prefers-reduced-motion: no-preference)` decide el modo. La función de limpieza devuelve todo al modo carrusel (quita la clase, limpia `transform`, restaura `tabindex`).
- `focusin` en una tarjeta con la pista fijada hace scroll vertical hasta centrarla: así el tabulador nunca deja el foco fuera de la vista.

## Interacción

- **Escritorio**: rueda, trackpad, barra espaciadora, flechas o Re Pág/Av Pág desplazan la página y, con ella, la pista. Los enlaces «Las casas» y «Reservar» y los «Ver la casa» son anclas; Lenis (`anchors: true`) hace el scroll suave.
- **Barra de progreso**: las seis marcas son `<button>` con `aria-label="Ir a <casa>, <hora>"`. Al pulsar: en modo fijado, `lenis.scrollTo(st.start + x, { duration: 1.4 })` con `x` = la posición que centra la tarjeta (acotada a `[0, distancia]`); en carrusel, `pista.scrollTo({ left: offsetLeft - offsetLeft de la primera, behavior:'smooth' })`.
- **Teclado**: todos los enlaces y botones tienen foco visible (`outline: 2px solid var(--acento)`, `offset 3px`). En carrusel la pista es una región enfocable (`tabindex="0"`, `role="region"`, `aria-label="Carrusel de casas"`) y se desplaza con las flechas; en modo fijado se le quita el `tabindex`.
- **Táctil** (< 768 px): carrusel nativo con inercia y `scroll-snap`; la barra, la hora y el contador siguen al dedo (escucha `scroll` del carrusel con `requestAnimationFrame`). Texto de ayuda visible solo en carrusel: «Desliza las tarjetas o toca una hora de la barra.»

## Adaptación a móvil y tablet

- **≥ 768 px con movimiento**: modo fijado. A 1440×900 caben unas tres tarjetas y media; a 768×1024 las tarjetas están limitadas a 70 vw (altas) y 84 vw (anchas).
- **≤ 900 px**: la fila inferior de la portada pasa a dos columnas (texto arriba a todo el ancho; datos y «Baja…» debajo).
- **≤ 767 px**: carrusel táctil; se oculta el enlace «Las casas» de la nav y el indicador «Baja…»; el arco ocupa el 100 % del ancho y se centra; datos a 28 px; titular de la galería `clamp(32px,9vw,40px)`; la barra pasa a dos filas (hora y contador arriba, riel debajo a todo el ancho) y las marcas pierden el texto (quedan puntos de 32×32 px de área táctil); se oculta el nombre de la casa del contador; la reserva pasa a una columna.
- Sin scroll horizontal de página en ningún ancho de 360 a 1920 px (`body{overflow-x:clip}`); todas las alturas en `svh`.

## Accesibilidad y movimiento reducido

- HTML semántico: `header` con `nav aria-label="Principal"`, `main`, `section` con `aria-labelledby`, cada tarjeta es un `article` con `aria-labelledby` a su `h3`. Paisajes, arco y riel con `aria-hidden="true"`. Los «Ver la casa» llevan el nombre de la casa en un `<span class="vh">` para que no sean enlaces repetidos.
- Contraste AA: `--tinta-2 #5a5249` sobre papel ≈ 6,3:1 (5,6:1 sobre las tarjetas); `--acento #9e3d26` ≈ 5,5:1 sobre papel y 4,9:1 sobre las tarjetas; textos claros sobre `--noche` ≥ 6,7:1.
- `prefers-reduced-motion: reduce`: no se crea Lenis ni el pin; la galería queda como carrusel con snap y sin paralaje (las capas quietas); la portada aparece sin entrada, el sol del arco quieto en la mitad del recorrido y la línea de «Baja…» sin animación; transiciones a 0,01 ms.

## Rendimiento

- Sin WebGL: SVG + transformaciones CSS. Solo se animan `transform` y `clip-path` de una barra de 3 px; nada de propiedades de layout durante el scroll.
- `will-change: transform` en `.capa`, `.pista` (lo gestiona GSAP) y `.dia-sol`. Geometría cacheada en `medir()`.
- Un único bucle: el ticker de GSAP alimenta a Lenis (`gsap.ticker.add(t => lenis.raf(t*1000))`, `lagSmoothing(0)`). El sol del arco se pausa con `IntersectionObserver` cuando la portada sale de pantalla.
- `document.fonts.ready` → `ScrollTrigger.refresh()` para que la distancia del pin se mida con las fuentes definitivas.
- Peso objetivo: menos de 40 KB de HTML.

## Personalización

- **Marca y textos**: `Casas de Alder`, `valle de Alder`, `Norvale`, `Altmere`, `+00 000 000 000`, `hola@casasdealder.com` aparecen en portada, reserva y pie. Las casas se cambian en el marcado (nombre, pueblo, precio y `data-hora`/`data-nombre`); la barra, las marcas del arco y el contador se generan a partir de esos atributos.
- **Colores**: `#9e3d26` (titular en itálica, horas, sol del logo y del arco, foco), `#efe9df` (fondo) y `#121824` (tarjeta de cierre y reserva). Cambia también `--papel-2` para que las tarjetas sigan un tono por debajo del fondo.
- **Paisajes**: cada entrada de `PAL` es una hora: `c` (cielo arriba/abajo), `s` y `sp` (color y posición del astro), `l m h n fr` (sierras de lejos a cerca), `cal` (fachadas), `t` (tejados), `v` (ventanas), `bruma`, `torre`, `noche` y `seed`. Cambia la `seed` para otro perfil de sierra con la misma luz.
- **Otra cantidad de tarjetas**: añade o quita `article.card` con su `data-escena` y su paleta; todo lo demás se adapta.
- **Intensidad del paralaje**: el `0.1` de `pintar()` (máximo 0.12, que es lo que sobresalen las capas).

## Comprobación final

1. A 1440×900, al llegar a la galería la sección se queda fija y el scroll vertical mueve la pista en horizontal hasta mostrar la tarjeta oscura «¿Cuál es tu hora?» pegada al margen derecho; luego la página sigue hacia la reserva.
2. Dentro de cada paisaje, el sol y las sierras lejanas se mueven más despacio que la tarjeta y los robles del primer plano más deprisa; nunca aparece un borde vacío.
3. Los seis paisajes van del amanecer malva a la noche con estrellas y ventanas encendidas; son idénticos en cada recarga (semillas).
4. La barra revela el degradado del día, el sol-marcador avanza, la hora cambia de forma continua (06:40 → 22:10) y el contador marca «01 / 06 Casa del Roble» … «06 / 06 La Atalaya».
5. Pulsar la marca «20:45» lleva en 1,4 s a Granja La Umbría centrada.
6. Tabular por los «Ver la casa» con la galería fijada desplaza la página para que la tarjeta enfocada quede a la vista, con anillo de foco terracota.
7. A 390×844 no hay pin: carrusel con snap, 84 vw por tarjeta, la barra y el contador siguen al dedo; sin scroll horizontal de página.
8. Con `prefers-reduced-motion: reduce` todo se ve completo y quieto, en modo carrusel.
9. La portada muestra el titular en dos líneas («Un día entero» / «en el *valle de Alder*») con la itálica en `#9e3d26` y el sol recorriendo el arco.
10. La página termina en fondo `#121824` con teléfono y email clicables.
11. Cero errores en consola, ninguna petición externa aparte de Google Fonts y jsDelivr, y menos de 40 KB de HTML.

Prueba en frío

Lo que salió al pegarlo en frío

Pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos el resultado con la demo. Prueba hecha el 30-09-2026.

Lo que generó la IA con el prompt Completo de Galería horizontal fijada
Prompt CompletoFiel a la demo9,7/10

Prácticamente idéntica al original: portada con arco solar, galería fijada con paisajes SVG en siete capas y paralaje, barra-reloj del día y reserva nocturna coinciden en maquetación, color, textos y movimiento. Las únicas diferencias visibles se deben al momento de la captura (posición del sol animado).

Lo que generó la IA con el prompt Lite de Galería horizontal fijada
Prompt LiteFiel a la demo8/10

Recrea bien lo esencial: pin con scroll horizontal 1:1, paisajes por capas con los mismos factores de paralaje, paletas del amanecer a la noche y barra que revela el degradado e interpola la hora. Cambia la composición de la portada (titular en tres líneas junto al arco en vez de debajo), el logo pasa a círculo, los datos y precios van en línea y en serif, y la barra y las etiquetas de tarjeta tienen otro estilo.

Solo publicamos lo que hemos probado. Otras herramientas (ChatGPT, Gemini, Lovable…) pueden dar resultados distintos.

Paso a paso

Cómo usarlo en 4 pasos

  1. 01

    Personaliza

    Rellena el formulario de arriba con el nombre, los colores y los textos de tu cliente. Se meten en el prompt al copiar.
  2. 02

    Copia el Completo o el Lite

    El Completo, para Claude, ChatGPT, Gemini o Cursor. El Lite, más corto, para Lovable, Bolt o v0, que aceptan menos texto.
  3. 03

    Pega e itera

    Pégalo en una conversación nueva. Si quieres cambiar algo, pídelo con una frase corta:
    • «Que el scroll horizontal vaya más lento, con 1,5 veces más recorrido vertical»
    • «Cambia las seis horas por las cuatro estaciones del año»
    • «Haz el paralaje más marcado dentro de cada tarjeta»
    • «Pon las tarjetas todas del mismo ancho y en formato horizontal»
  4. 04

    Publica

    Guarda el resultado como HTML (o en tu proyecto) y súbelo a Vercel, Netlify o tu hosting de siempre. Tienes los pasos en la guía.

Guías por herramientaClaudeChatGPTGeminiCursorLovableBoltv0

Licencia en claro

Qué puedes hacer con esta pieza

Todas las piezas incluyen licencia de uso comercial. Usa el resultado tal cual en tus productos, encargos freelance o webs de clientes, sin atribución.

Puedes

  • Usarlo en tus webs y en las de tus clientes, también si cobras por ellas.
  • Cambiarlo todo lo que quieras.
  • Publicar el resultado sin citarnos.

No puedes

  • Revender o regalar el prompt, la demo o el código como plantilla o biblioteca.
  • Compartir tu cuenta Pro.

Es la misma licencia para lo gratis y lo Pro. La tienes entera en licencia de uso.

Preguntas frecuentes

Lo que suele preguntarse

¿Puedo poner fotos reales en lugar de los paisajes?
Sí. Sustituye el contenido de cada .escena por dos o tres capas con tu foto (fondo, figura recortada, primer plano) y dales los mismos factores de paralaje; con una sola foto, usa una capa con factor 0.5 un 12 % más ancha que la tarjeta.
¿Qué pasa en móvil?
Por debajo de 768 px no se fija nada: la galería es un carrusel táctil nativo con scroll-snap y la barra de horas sigue al dedo. Es más cómodo que secuestrar el scroll vertical en un teléfono.
¿Puedo tener más o menos de seis tarjetas?
Sí. Añade o quita artículos con su data-hora, data-nombre y una paleta en PAL; la barra, las marcas, el contador y la distancia del pin se calculan solos.
¿Afecta al SEO o a la accesibilidad?
No: todo el texto está en el HTML como artículos con su encabezado, los paisajes son decorativos y se ocultan a lectores de pantalla, y al tabular la página se desplaza para que la tarjeta con foco quede a la vista.
¿Qué diferencia hay entre el prompt Completo y el Lite?
El Completo (≈13778 tokens) describe la pieza con todo detalle y es el que mejor resultado da en Claude, ChatGPT, Gemini o Cursor. El Lite (≈3146 tokens) resume lo esencial para herramientas que aceptan menos texto, como Lovable, Bolt o v0.

Versión 1.0 · publicada el 30-09-2026

Cópialo desde aquí

Tu navegador no nos deja escribir en el portapapeles. El texto ya está seleccionado: pulsa Ctrl + C (o ⌘ + C) o el botón.