
Acordeón con muelle
GratisBloques de web · JS
La pieza
Un hero editorial de pantalla completa construido alrededor de un titular gigante en Fraunces variable. Cada letra sube desde una máscara al cargar y el punto final rebota en color de acento. Al hacer scroll, el hero queda fijo y el titular pasa de un trazo finísimo y aireado (peso 160) a un negro rotundo (peso 900): el espaciado entre letras se recalcula en cada fotograma para que la línea mantenga exactamente su anchura. Un contador de peso con barra acompaña el recorrido. Debajo, subtítulo, dos CTA y una franja de datos. En vertical el titular se recompone en escalera, palabra a palabra. Incluye interruptor claro/oscuro. Encaja como portada de estudios creativos, arquitectura, bodegas, restaurantes o cualquier marca que quiera presentarse con una sola frase contundente. Funciona con cualquier paleta: papel y tinta, verde oliva, azul añil o negro total.
Encaja bien en webs de
Antes de copiar
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.
Gratis y entero
Qué trae el prompt, por apartados
Tokens estimados para español (≈3,6 caracteres por token): sirven para orientarte, no son exactos.
# Recrea en un único archivo HTML: Hero con titular gigante
Un hero editorial de estudio: un titular a sangre en serif variable que entra letra a letra desde una máscara y, al hacer scroll, pasa de un trazo finísimo y aireado a un negro rotundo y apretado sin cambiar de anchura; con versión clara y oscura.
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 sección hero de pantalla completa para la web de **Taller Meridiano** (Estudio de marca y web, con sede en Valdera). Todo gira alrededor de un titular gigante de dos líneas:
- Línea 1, redonda: **Diseño**
- Línea 2, cursiva: **con acento.** (el punto final se pinta en color de acento y entra con un rebote propio).
Cómo se comporta:
1. **Entrada.** Cuando las fuentes están cargadas, cada letra sube desde debajo de su línea (la línea actúa como máscara: por debajo de ella no se ve nada) con un leve giro que se endereza. La línea 1 empieza primero y la 2 la sigue con 0,22 s de retraso. Después aparecen el subtítulo, los dos botones (CTA) y la franja de datos inferior.
2. **Cada línea ocupa exactamente el ancho útil** (de gutter a gutter). El tamaño de fuente de cada línea se calcula en JS midiendo el texto real, de modo que al peso máximo (900) la línea llena el ancho justo (salvo que el tope de alto del punto 5 obligue a reducirlo).
3. **Scroll = peso.** El hero mide 240svh y su contenido está fijado con `position: sticky`. Al recorrerlo, el eje `wght` de la fuente variable pasa de 160 a 900. Para que la línea **no cambie de anchura** mientras engorda, el `letter-spacing` se recalcula en cada fotograma: con peso 160 las letras quedan muy separadas (aire), con 900 quedan con espaciado 0 (apretadas). La anchura de la línea es constante; lo que cambia es la relación negro/blanco.
4. **Lectura de peso.** Abajo a la derecha, un contador «Peso 160 … 900» con cifras tabulares y una barrita de acento que se llena con el mismo progreso.
5. **Composición adaptativa.** En pantallas verticales (ancho/alto < 0,9) cada palabra pasa a su propia línea (queda «Diseño / con / acento.»). La primera línea es el ancla: ninguna línea posterior puede ser más grande que ella; si una línea se queda corta (p. ej. «con»), se alinea a la derecha formando una escalera editorial. Si el titular no cabe en alto, se reduce todo a la vez y las líneas a partir de la segunda se alinean a la derecha (en monitores anchos y bajos queda una diagonal «Diseño» izquierda / «con acento.» derecha).
6. **Tema claro / oscuro** con un interruptor tipo píldora en la navegación («Claro | Oscuro»). Arranca según `prefers-color-scheme` (o la última elección guardada) y la transición de colores dura 0,7 s.
7. Debajo del hero hay una sección corta «Trabajo reciente» con cuatro proyectos en filas y un pie con contacto, para que el hero se vea en contexto al terminar el scroll.
Sensación: papel cálido, tinta casi negra, un único acento rojo almagra; tipografía con intención, mucho aire, detalles finos (grano sutil, filetes de 1 px, cifras tabulares). Nada de sombras, degradados ni glassmorphism.
## Librerías y esqueleto
- GSAP 3.15.0 con ScrollTrigger y SplitText (todos por jsDelivr, al final del `<body>`, en este orden):
- `https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js`
- `https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js`
- `https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/SplitText.min.js`
- Fuentes de Google (ambas con ñ, tildes, ¿ y ¡):
- **Fraunces** variable, redonda y cursiva, ejes `opsz` 9–144 y `wght` 100–900 → titular y titulares secundarios.
- **Instrument Sans** variable `wght` 400–700 → texto, navegación, botones, datos.
- Sin imágenes: el sello del logo, las flechas y el grano son SVG en línea.
- Un script mínimo en el `<head>` fija el tema antes de pintar (evita el parpadeo) y añade la clase `js` al `<html>` (el CSS oculta el titular hasta que el JS lo prepara).
`<head>` literal:
```html
<!doctype html>
<html lang="es" data-tema="claro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Taller Meridiano — Estudio de marca y web en Valdera</title>
<meta name="description" content="Taller Meridiano: Estudio de marca y web en Valdera. Diseñamos desde Valdera para quien tiene algo que contar y no quiere parecerse a nadie.">
<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=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&family=Instrument+Sans:wght@400..700&display=swap" rel="stylesheet">
<script>
(function () {
var d = document.documentElement, t = null;
try { t = localStorage.getItem("tp-tema"); } catch (e) {}
if (t !== "claro" && t !== "oscuro") t = matchMedia("(prefers-color-scheme: dark)").matches ? "oscuro" : "claro";
d.dataset.tema = t;
d.classList.add("js");
})();
</script>
<style>
/* aquí va TODO el CSS de «Tokens de diseño» y «Código clave» */
</style>
</head>
```
## Tokens de diseño
Bloque literal (inicio del `<style>`). El tema oscuro se activa con `data-tema="oscuro"` en `<html>`.
```css
:root {
--papel: #efe9dd;
--tinta: #17140f;
--tinta-suave: #5b5448;
--linea: rgba(23, 20, 15, .16);
--acento: #b33810;
--sobre-acento: #efe9dd;
--grano: .07;
--gutter: clamp(14px, 1.6vw, 28px);
--f-display: "Fraunces", "Iowan Old Style", Georgia, serif;
--f-texto: "Instrument Sans", "Helvetica Neue", Arial, sans-serif;
--e-salida: cubic-bezier(.16, 1, .3, 1);
--e-tema: cubic-bezier(.65, 0, .35, 1);
color-scheme: light;
}
[data-tema="oscuro"] {
--papel: #121110;
--tinta: #efe7d8;
--tinta-suave: #a39a8a;
--linea: rgba(239, 231, 216, .16);
--acento: #ff6b3d;
--sobre-acento: #121110;
--grano: .09;
color-scheme: dark;
}
```
Resumen de uso:
| Token | Claro | Oscuro | Dónde |
|---|---|---|---|
| `--papel` | #efe9dd | #121110 | fondo |
| `--tinta` | #17140f | #efe7d8 | titular, texto fuerte, botón lleno |
| `--tinta-suave` | #5b5448 | #a39a8a | subtítulo, datos, etiquetas |
| `--linea` | tinta al 16 % | tinta al 16 % | filetes de 1 px |
| `--acento` | #b33810 | #ff6b3d | punto final, barra de peso, hover del botón, cursiva del h2 |
| `--grano` | .07 | .09 | opacidad del ruido SVG |
Tipografía:
- Titular: Fraunces, `font-variation-settings: "wght" <160…900>, "opsz" 144`, `line-height: .8`, tamaño calculado por JS (en 1440×900 sale ≈ 439 px en la línea 1 y ≈ 267 px en la 2, ya con el tope de alto aplicado).
- H2 de la sección siguiente: Fraunces 350, `clamp(30px, 3.6vw, 56px)`, `line-height 1.04`, `letter-spacing -.02em`, `opsz` 96.
- Nombres de proyecto: Fraunces 400, `clamp(22px, 2.2vw, 34px)`, `opsz` 72.
- Texto: Instrument Sans 16 px / 1.55; subtítulo `clamp(16px, 1.15vw, 18px)`; datos 12 px en mayúsculas con `letter-spacing .04em` y `tabular-nums`; botones 600 15 px.
## Estructura y contenido
Árbol:
```
body
├─ a.saltar «Saltar al contenido»
└─ main#contenido
├─ section.hero#inicio (240svh; 200svh en móvil)
│ └─ div.hero__pin (sticky, 100svh, grid de 4 filas: nav · titular · pie · meta)
│ ├─ header.nav: marca (sello SVG + nombre) · nav (Proyectos, Estudio, Contacto) · botón .tema
│ ├─ div.hero__titular: p.eyebrow + h1.titular (span.sr con el texto completo + span.titular__lineas aria-hidden con las líneas)
│ ├─ div.hero__pie: p.sub + div.ctas (a.btn--lleno con flecha, a.btn--linea)
│ └─ div.hero__meta: coordenadas · pista «Desliza: el titular gana peso» · lectura de peso
└─ section.trabajo#trabajo: cabecera (etiqueta + h2) · ul.lista (4 filas) · footer.pie
```
`<body>` literal completo (textos incluidos; usa estos textos exactos):
```html
<body>
<a class="saltar" href="#contenido">Saltar al contenido</a>
<main id="contenido">
<section class="hero" id="inicio" aria-labelledby="titular">
<div class="hero__pin">
<header class="nav">
<a class="marca" href="#inicio" aria-label="Taller Meridiano, inicio">
<svg class="marca__sello" viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="12" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M4 16.5h18" stroke="currentColor" stroke-width="1.4"/><path d="M7.5 16.5a5.5 5.5 0 0 1 11 0" fill="currentColor"/></svg>
<span>Taller Meridiano</span>
</a>
<nav aria-label="Principal">
<ul class="nav__links">
<li><a href="#trabajo">Proyectos</a></li>
<li><a href="#trabajo">Estudio</a></li>
<li><a href="mailto:hola@tallermeridiano.com">Contacto</a></li>
</ul>
</nav>
<button class="tema" type="button" aria-pressed="false" aria-label="Modo oscuro">
<span class="tema__claro" aria-hidden="true">Claro</span><span class="tema__oscuro" aria-hidden="true">Oscuro</span>
</button>
</header>
<div class="hero__titular">
<p class="eyebrow">Estudio de marca y web — Valdera, desde 2014</p>
<h1 class="titular" id="titular">
<span class="sr">Diseño con acento.</span>
<span class="titular__lineas" aria-hidden="true">
<span class="linea" data-linea>Diseño</span>
<span class="linea linea--cursiva" data-linea data-cursiva>con acento.</span>
</span>
</h1>
</div>
<div class="hero__pie">
<p class="sub">Identidades, webs y packaging para <strong>negocios con alma que quieren sonar a ellos mismos</strong>. Diseñamos desde Valdera para quien tiene algo que contar y no quiere parecerse a nadie.</p>
<div class="ctas">
<a class="btn btn--lleno" href="#trabajo">Ver proyectos
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 8h11M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
</a>
<a class="btn btn--linea" href="mailto:hola@tallermeridiano.com">Hablemos de tu marca</a>
</div>
</div>
<div class="hero__meta">
<span>Valdera · 00°00′ N 00°00′ E</span>
<span class="pista" aria-hidden="true"><span class="pista__raya"></span>Desliza: el titular gana peso</span>
<span class="peso" aria-hidden="true">Peso <b class="peso__num">160</b><span class="peso__barra"><i></i></span></span>
</div>
</div>
</section>
<section class="trabajo" id="trabajo" aria-labelledby="trabajo-tit">
<div class="trabajo__cab">
<p class="trabajo__eti">(02) Trabajo reciente</p>
<h2 class="trabajo__tit" id="trabajo-tit">Cuatro marcas con alma que ya <em>suenan a sí mismas.</em></h2>
</div>
<ul class="lista">
<li><span class="n">01</span><h3>Bodega Arrayán</h3><span class="s">Identidad y etiquetas</span><span class="l">Lumera</span><span class="a">2026</span></li>
<li><span class="n">02</span><h3>Hotel Casa Levante</h3><span class="s">Web y señalética</span><span class="l">Sirmio</span><span class="a">2025</span></li>
<li><span class="n">03</span><h3>Aceites Sierra Chica</h3><span class="s">Packaging</span><span class="l">Orlana</span><span class="a">2025</span></li>
<li><span class="n">04</span><h3>Horno La Espiga Dorada</h3><span class="s">Identidad y local</span><span class="l">Valdera</span><span class="a">2024</span></li>
</ul>
<footer class="pie">
<span>© 2026 Taller Meridiano · Calle Álamos 12, Valdera</span>
<span><a href="mailto:hola@tallermeridiano.com">hola@tallermeridiano.com</a> · <a href="tel:+00 000 000 000">+00 000 000 000</a></span>
</footer>
</section>
</main>
<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/gsap@3.15.0/dist/SplitText.min.js"></script>
<script>
/* aquí va el JS de «Código clave» */
</script>
</body>
</html>
```
Notas de contenido:
- En `tel:` escribe el teléfono sin espacios (p. ej. `tel:+000000000000`).
- Los cuatro proyectos de la lista son de ejemplo y se quedan tal cual si no hay otros: Bodega Arrayán (Identidad y etiquetas, Lumera, 2026), Hotel Casa Levante (Web y señalética, Sirmio, 2025), Aceites Sierra Chica (Packaging, Orlana, 2025), Horno La Espiga Dorada (Identidad y local, Valdera, 2024).
- El `span.sr` del h1 contiene el titular completo para lectores de pantalla; las líneas visuales van en `span.titular__lineas` con `aria-hidden="true"`, porque el JS las reescribe y parte en letras.
## Movimiento
Entrada (timeline de GSAP con `delay: 0.15`, `defaults: { ease: "power4.out" }`; posiciones en segundos desde el inicio del timeline). Arranca cuando las fuentes han cargado o, como mucho, a los 2,5 s.
| Elemento | Propiedad | De → a | Duración | Easing | Posición / retardo | Disparador |
|---|---|---|---|---|---|---|
| `.nav` | y, opacity | −16 px, 0 → 0, 1 | 0,9 s | power4.out | 0 | carga |
| `.eyebrow` | opacity | 0 → 1 | 0,8 s | power2.out | 0,2 | carga |
| letras línea *i* (sin el punto) | yPercent, rotate | 180, 7° → 0, 0° (`transformOrigin "0% 100%"`) | 1,2 s | power4.out | 0,1 + i × 0,22; stagger 0,045 | carga |
| punto final `.ch--punto` | scale | 0 → 1 (`transformOrigin "50% 85%"`) | 0,7 s | back.out(3) | 1,15 | carga |
| `.hero__pie > *` (subtítulo y bloque de CTA) | y, opacity | 24 px, 0 → 0, 1 | 1 s | expo.out | 1,0; stagger 0,1 | carga |
| `.hero__meta` | opacity | 0 → 1 | 0,8 s | power2.out | 1,35 | carga |
| titular (todas las líneas) | `wght` + `letter-spacing` | 160 → 900 (espaciado calculado) | ligado al scroll | lineal, `scrub: 0.9` | hero `top top` → `bottom bottom` | scroll |
| `.peso__num` / `.peso__barra i` | texto / scaleX | 160 → 900 / 0 → 1 | ligado al scroll | lineal | igual que arriba | scroll |
| colores de tema | background-color, color, border-color | claro ↔ oscuro | 0,7 s | cubic-bezier(.65, 0, .35, 1) | — | clic en .tema |
| mando de la píldora `.tema::before` | translateX | 0 → 100 % | 0,55 s | cubic-bezier(.16, 1, .3, 1) | — | clic |
| subrayado de enlaces de nav | scaleX | 0 → 1 (origen izq.; sale por la dcha.) | 0,5 s | cubic-bezier(.16, 1, .3, 1) | — | hover |
| flecha del botón lleno | transform | 0 → translateX(4px) rotate(−45°) | 0,5 s | cubic-bezier(.16, 1, .3, 1) | — | hover |
| botones | fondo, color, borde | ver CSS | 0,45 s | cubic-bezier(.16, 1, .3, 1) | — | hover |
| raya de la pista | translateX | −100 % → 100 % (60 % del ciclo), bucle | 2,4 s | cubic-bezier(.16, 1, .3, 1) | infinito, pausado fuera de pantalla | automático |
| nombre de proyecto | translateX, wght | 0 → 10 px, 400 → 640 | 0,6 s | cubic-bezier(.16, 1, .3, 1) | — | hover (solo `hover: hover`) |
## Código clave
### CSS completo (va después de los tokens, dentro del mismo `<style>`)
```css
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; overflow-x: clip; }
html { scroll-behavior: smooth; }
body {
background: var(--papel);
color: var(--tinta);
font-family: var(--f-texto);
font-size: 16px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
transition: background-color .7s var(--e-tema), color .7s var(--e-tema);
}
body::after {
content: "";
position: fixed; inset: 0; z-index: 50; pointer-events: none;
opacity: var(--grano);
background-image: 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='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 0 0 0 .9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
mix-blend-mode: multiply;
}
[data-tema="oscuro"] body::after { mix-blend-mode: screen; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--acento); outline-offset: 4px; border-radius: 2px; }
.sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.saltar { position: absolute; left: var(--gutter); top: -60px; z-index: 60; background: var(--tinta); color: var(--papel); padding: 10px 16px; border-radius: 999px; text-decoration: none; font-weight: 600; }
.saltar:focus { top: 12px; }
/* ——— Hero ——— */
.hero { position: relative; height: 240svh; }
.hero__pin {
position: sticky; top: 0;
height: 100svh; min-height: 560px;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto auto;
padding: 0 var(--gutter);
overflow: clip;
}
.nav {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
height: 72px;
border-bottom: 1px solid var(--linea);
position: relative;
}
.marca { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; font-weight: 600; letter-spacing: -.01em; font-size: 15px; }
.marca__sello { width: 26px; height: 26px; flex: none; }
.nav__links { display: flex; align-items: center; gap: 28px; list-style: none; margin: 0; padding: 0; font-size: 14px; }
.nav__links a { text-decoration: none; position: relative; padding: 6px 0; }
.nav__links a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: right; transition: transform .5s var(--e-salida); }
.nav__links a:hover::after { transform: scaleX(1); transform-origin: left; }
.tema {
position: relative; display: inline-grid; grid-template-columns: 1fr 1fr; align-items: center;
height: 34px; padding: 3px; border: 1px solid var(--linea); border-radius: 999px;
background: transparent; color: var(--tinta); font: 500 13px/1 var(--f-texto); cursor: pointer;
transition: border-color .7s var(--e-tema), color .7s var(--e-tema);
}
.tema span { position: relative; z-index: 1; padding: 0 12px; transition: color .5s var(--e-tema); }
.tema::before {
content: ""; position: absolute; top: 3px; bottom: 3px; left: 3px; width: calc(50% - 3px);
border-radius: 999px; background: var(--tinta);
transition: transform .55s var(--e-salida), background-color .7s var(--e-tema);
}
.tema[aria-pressed="false"] .tema__claro, .tema[aria-pressed="true"] .tema__oscuro { color: var(--papel); }
.tema[aria-pressed="true"]::before { transform: translateX(100%); }
.hero__titular { display: flex; flex-direction: column; justify-content: flex-end; min-height: 0; padding-top: 20px; }
.eyebrow { display: flex; align-items: center; gap: 12px; margin: 0 0 14px; font-size: 13px; letter-spacing: .02em; color: var(--tinta-suave); transition: color .7s var(--e-tema); }
.eyebrow::before { content: ""; width: 28px; height: 1px; background: currentColor; }
.titular { margin: 0; font-family: var(--f-display); font-weight: 400; color: var(--tinta); }
.titular__lineas { display: block; }
.linea {
display: block; white-space: nowrap;
font-size: clamp(3.5rem, 17vw, 20rem);
line-height: .8;
padding: .12em 0 .2em; margin: -.12em 0 -.2em;
clip-path: inset(-.6em -.6em 0 -.6em);
}
.linea--cursiva { font-style: italic; }
.linea--derecha { text-align: right; }
.linea__txt { display: inline-block; font-variation-settings: "wght" 160, "opsz" 144; will-change: letter-spacing; }
.ch { display: inline-block; will-change: transform; }
.ch--punto { color: var(--acento); transition: color .7s var(--e-tema); }
.js .titular__lineas { visibility: hidden; }
.js .hero__pie > *, .js .hero__meta, .js .eyebrow { opacity: 0; }
.hero__pie {
display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 24px 48px;
padding: 28px 0 24px;
}
.sub { margin: 0; max-width: 46ch; font-size: clamp(16px, 1.15vw, 18px); color: var(--tinta-suave); transition: color .7s var(--e-tema); }
.sub strong { color: var(--tinta); font-weight: 600; transition: color .7s var(--e-tema); }
.ctas { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 10px;
height: 52px; padding: 0 24px; border-radius: 999px;
font: 600 15px/1 var(--f-texto); text-decoration: none; white-space: nowrap;
transition: background-color .45s var(--e-salida), color .45s var(--e-salida), border-color .45s var(--e-salida);
}
.btn svg { width: 16px; height: 16px; transition: transform .5s var(--e-salida); }
.btn:hover svg { transform: translateX(4px) rotate(-45deg); }
.btn--lleno { background: var(--tinta); color: var(--papel); border: 1px solid var(--tinta); }
.btn--lleno:hover { background: var(--acento); border-color: var(--acento); color: var(--sobre-acento); }
.btn--linea { border: 1px solid var(--tinta); color: var(--tinta); }
.btn--linea:hover { background: var(--tinta); color: var(--papel); }
.hero__meta {
display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px;
height: 56px; border-top: 1px solid var(--linea);
font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--tinta-suave);
font-variant-numeric: tabular-nums; transition: color .7s var(--e-tema), border-color .7s var(--e-tema);
}
.pista { display: inline-flex; align-items: center; gap: 12px; }
.pista__raya { position: relative; width: 44px; height: 1px; background: var(--linea); overflow: hidden; }
.pista__raya::after { content: ""; position: absolute; inset: 0; background: var(--tinta); transform: translateX(-100%); animation: raya 2.4s var(--e-salida) infinite; }
.fuera .pista__raya::after { animation-play-state: paused; }
@keyframes raya { 0% { transform: translateX(-100%); } 60%, 100% { transform: translateX(100%); } }
.peso { justify-self: end; display: inline-flex; align-items: center; gap: 12px; }
.peso__num { color: var(--tinta); min-width: 3ch; text-align: right; transition: color .7s var(--e-tema); }
.peso__barra { position: relative; width: 72px; height: 3px; background: var(--linea); border-radius: 3px; overflow: hidden; }
.peso__barra i { position: absolute; inset: 0; background: var(--acento); transform-origin: left; transform: scaleX(0); border-radius: 3px; }
/* ——— Trabajo reciente ——— */
.trabajo { position: relative; padding: clamp(72px, 10vw, 140px) var(--gutter) 40px; border-top: 1px solid var(--linea); }
.trabajo__cab { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 24px 48px; margin-bottom: 48px; }
.trabajo__eti { margin: 0; font-size: 13px; letter-spacing: .02em; color: var(--tinta-suave); }
.trabajo__tit { margin: 0; font: 350 clamp(30px, 3.6vw, 56px)/1.04 var(--f-display); font-variation-settings: "opsz" 96; letter-spacing: -.02em; max-width: 18ch; }
.trabajo__tit em { font-style: italic; color: var(--acento); }
.lista { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--linea); }
.lista li {
display: grid; grid-template-columns: 3rem minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, .7fr) 4rem; align-items: baseline; gap: 16px;
padding: 22px 0; border-bottom: 1px solid var(--linea); color: var(--tinta-suave); font-size: 14px;
}
.lista .n { font-variant-numeric: tabular-nums; font-size: 12px; }
.lista h3 { margin: 0; font: 400 clamp(22px, 2.2vw, 34px)/1.1 var(--f-display); font-variation-settings: "opsz" 72; color: var(--tinta); letter-spacing: -.01em; transition: transform .6s var(--e-salida), font-variation-settings .6s var(--e-salida); }
@media (hover: hover) { .lista li:hover h3 { transform: translateX(10px); font-variation-settings: "opsz" 72, "wght" 640; } }
.lista .a { text-align: right; font-variant-numeric: tabular-nums; }
.pie { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px 24px; margin-top: 64px; font-size: 13px; color: var(--tinta-suave); }
.pie a { text-decoration-color: var(--linea); text-underline-offset: 4px; }
.pie a:hover { text-decoration-color: currentColor; }
/* ——— Adaptación ——— */
@media (max-width: 900px) {
.hero__pie { grid-template-columns: 1fr; }
.trabajo__cab { grid-template-columns: 1fr; }
.lista li { grid-template-columns: 2.2rem minmax(0, 1fr) auto; row-gap: 4px; }
.lista .s { grid-column: 2; grid-row: 2; }
.lista .l { display: none; }
.lista .a { grid-column: 3; grid-row: 1; }
}
@media (max-width: 640px) {
.hero { height: 200svh; }
.nav { height: 60px; }
.nav__links { display: none; }
.hero__pie { padding: 22px 0 18px; gap: 20px; }
.ctas { display: grid; grid-template-columns: 1fr; }
.btn { height: 50px; }
.hero__meta { grid-template-columns: 1fr auto; height: 50px; }
.pista { display: none; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.hero { height: auto; }
.hero__pin { position: relative; }
.pista__raya::after { animation: none; transform: none; }
.js .titular__lineas { visibility: visible; }
.js .hero__pie > *, .js .hero__meta, .js .eyebrow { opacity: 1; }
*, *::before, *::after { transition-duration: .01ms !important; }
}
```
Puntos delicados del CSS:
- `.linea` usa `clip-path: inset(-.6em -.6em 0 -.6em)`: recorta **solo por abajo** (así las letras «salen» de debajo de la línea) y deja respirar arriba y a los lados (tildes, la virgulilla de la ñ, los remates de la cursiva). El `padding-bottom: .2em` con `margin-bottom: -.2em` deja ver los descendentes sin abrir el interlineado.
- `line-height: .8` en `.linea` es el mismo valor que la constante `LH = 0.8` del JS: si cambias uno, cambia el otro.
- `html, body { overflow-x: clip }` y `.hero__pin { overflow: clip }` evitan scroll horizontal sin romper el `sticky` (no uses `overflow: hidden` en ancestros del sticky).
- El grano es un `feTurbulence` en un SVG en data URI sobre `body::after`, con `mix-blend-mode: multiply` en claro y `screen` en oscuro.
### JavaScript completo (en un `<script>` normal tras los tres de GSAP)
```js
(() => {
const html = document.documentElement;
const reducido = matchMedia("(prefers-reduced-motion: reduce)").matches;
const hayGsap = !!(window.gsap && window.ScrollTrigger && window.SplitText);
/* ——— Tema claro / oscuro ——— */
const btnTema = document.querySelector(".tema");
const pintarTema = () => btnTema.setAttribute("aria-pressed", String(html.dataset.tema === "oscuro"));
pintarTema();
btnTema.addEventListener("click", () => {
html.dataset.tema = html.dataset.tema === "oscuro" ? "claro" : "oscuro";
try { localStorage.setItem("tp-tema", html.dataset.tema); } catch (e) {}
pintarTema();
});
/* ——— Motor de peso ——— */
const WMIN = 160, WMAX = 900, WQUIETO = 720, LH = 0.8;
const MUESTRAS = [160, 250, 350, 450, 550, 650, 750, 850, 900];
const hero = document.querySelector(".hero");
const zona = document.querySelector(".hero__titular");
const eyebrow = document.querySelector(".eyebrow");
const cont = document.querySelector(".titular__lineas");
const num = document.querySelector(".peso__num");
const barra = document.querySelector(".peso__barra i");
const FUENTES = [...cont.querySelectorAll("[data-linea]")].map((el) => ({
texto: el.textContent.trim(), cursiva: el.hasAttribute("data-cursiva"),
}));
const estado = { w: reducido ? WQUIETO : WMIN };
let lineas = [], split = null, vertical = null;
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const fvs = (w) => `"wght" ${w.toFixed(1)}, "opsz" 144`;
function construir() {
vertical = innerWidth / innerHeight < 0.9;
if (split) split.revert();
const piezas = vertical
? FUENTES.flatMap((f) => f.texto.split(/\s+/).map((t) => ({ texto: t, cursiva: f.cursiva })))
: FUENTES;
cont.innerHTML = piezas.map((p) =>
`<span class="linea${p.cursiva ? " linea--cursiva" : ""}"><span class="linea__txt">${esc(p.texto)}</span></span>`).join("");
const txts = [...cont.querySelectorAll(".linea__txt")];
if (hayGsap) {
split = SplitText.create(txts, { type: "chars", charsClass: "ch", tag: "span", aria: "none" });
const ultimo = split.chars[split.chars.length - 1];
if (ultimo && /[.!?…]/.test(ultimo.textContent)) ultimo.classList.add("ch--punto");
}
lineas = txts.map((el) => ({ el, linea: el.parentElement, n: el.textContent.length, tabla: [], fs: 0, objetivo: 0 }));
medir();
}
function medir() {
const ancho = cont.clientWidth;
lineas.forEach((l) => {
const s = l.el.style;
s.fontSize = "100px"; s.letterSpacing = "0px"; s.marginRight = "0px";
l.tabla = MUESTRAS.map((w) => { s.fontVariationSettings = fvs(w); return l.el.getBoundingClientRect().width; });
l.fs = (ancho / l.tabla[l.tabla.length - 1]) * 100;
});
// La primera línea es el ancla: ninguna otra la supera.
const ancla = lineas[0].fs;
const cortas = lineas.map((l, i) => i > 0 && l.fs > ancla);
lineas.forEach((l, i) => { if (cortas[i]) l.fs = ancla; });
// Tope de alto: todo el titular cabe en su franja.
const alto = zona.clientHeight - parseFloat(getComputedStyle(zona).paddingTop) - eyebrow.offsetHeight - 14 - 24;
const total = lineas.reduce((a, l) => a + l.fs * LH, 0);
const k = total > alto ? Math.max(0.2, alto / total) : 1;
// Las líneas que no llegan al borde (y todas menos la primera si hubo tope) se alinean a la derecha.
lineas.forEach((l, i) => l.linea.classList.toggle("linea--derecha", i > 0 && (cortas[i] || k < 0.985)));
lineas.forEach((l) => {
l.fs *= k;
l.el.style.fontSize = l.fs.toFixed(2) + "px";
l.linea.style.fontSize = l.fs.toFixed(2) + "px";
l.objetivo = (l.tabla[l.tabla.length - 1] * l.fs) / 100;
});
aplicar();
}
function anchoNatural(l, w) {
let i = 0;
while (i < MUESTRAS.length - 2 && w > MUESTRAS[i + 1]) i++;
const t = (w - MUESTRAS[i]) / (MUESTRAS[i + 1] - MUESTRAS[i]);
return ((l.tabla[i] + (l.tabla[i + 1] - l.tabla[i]) * t) * l.fs) / 100;
}
function aplicar() {
const w = estado.w;
lineas.forEach((l) => {
const ls = (l.objetivo - anchoNatural(l, w)) / Math.max(1, l.n - 1);
l.el.style.fontVariationSettings = fvs(w);
l.el.style.letterSpacing = ls.toFixed(2) + "px";
l.el.style.marginRight = (-ls).toFixed(2) + "px";
});
num.textContent = Math.round(w);
barra.style.transform = `scaleX(${((w - WMIN) / (WMAX - WMIN)).toFixed(4)})`;
}
/* ——— Arranque ——— */
const fuentes = Promise.race([
Promise.all([
document.fonts.load('160 100px "Fraunces"'),
document.fonts.load('italic 900 100px "Fraunces"'),
document.fonts.load('500 16px "Instrument Sans"'),
]),
new Promise((r) => setTimeout(r, 2500)),
]).catch(() => {});
fuentes.then(() => {
construir();
if (!hayGsap) {
html.classList.remove("js");
return;
}
gsap.registerPlugin(ScrollTrigger, SplitText);
if (reducido) {
gsap.set(cont, { visibility: "visible" });
return;
}
const chars = split.chars;
const punto = cont.querySelector(".ch--punto");
gsap.set(chars, { yPercent: 180, rotate: 7, transformOrigin: "0% 100%" });
if (punto) gsap.set(punto, { yPercent: 0, rotate: 0, scale: 0, transformOrigin: "50% 85%" });
gsap.set(cont, { visibility: "visible" });
const porLinea = lineas.map((l) => [...l.el.querySelectorAll(".ch")].filter((c) => c !== punto));
const tl = gsap.timeline({ defaults: { ease: "power4.out" }, delay: 0.15 });
tl.from(".nav", { y: -16, opacity: 0, duration: 0.9 }, 0)
.to(".eyebrow", { opacity: 1, duration: 0.8, ease: "power2.out" }, 0.2);
porLinea.forEach((grupo, i) => {
tl.to(grupo, { yPercent: 0, rotate: 0, duration: 1.2, stagger: 0.045 }, 0.1 + i * 0.22);
});
if (punto) tl.to(punto, { scale: 1, duration: 0.7, ease: "back.out(3)" }, 1.15);
tl.fromTo(".hero__pie > *", { y: 24, opacity: 0 }, { y: 0, opacity: 1, duration: 1, stagger: 0.1, ease: "expo.out" }, 1.0)
.to(".hero__meta", { opacity: 1, duration: 0.8, ease: "power2.out" }, 1.35);
gsap.to(estado, {
w: WMAX, ease: "none", onUpdate: aplicar,
scrollTrigger: { trigger: hero, start: "top top", end: "bottom bottom", scrub: 0.9 },
});
new IntersectionObserver(([e]) => hero.classList.toggle("fuera", !e.isIntersecting)).observe(hero);
});
let t;
addEventListener("resize", () => {
clearTimeout(t);
t = setTimeout(() => {
if (!lineas.length) return;
const v = innerWidth / innerHeight < 0.9;
if (v !== vertical) {
construir();
if (split && !reducido) gsap.set(split.chars, { yPercent: 0, rotate: 0, scale: 1 });
} else medir();
if (hayGsap) ScrollTrigger.refresh();
}, 150);
});
})();
```
Cómo funciona el motor de peso (explicado para que no se «simplifique» mal):
1. `medir()` pone cada línea a 100 px y mide su anchura real con `getBoundingClientRect()` en 9 pesos (`MUESTRAS`). Esa tabla dice cuánto ocupa la línea a cada peso.
2. El tamaño de cada línea es `anchoDisponible / anchoA900 × 100`: al peso 900 y `letter-spacing: 0` la línea llena justo el ancho.
3. `anchoNatural(l, w)` interpola linealmente la tabla para cualquier peso intermedio.
4. `aplicar()` calcula `ls = (objetivo − anchoNatural) / (nLetras − 1)` y lo aplica como `letter-spacing`. Se divide entre `n − 1` porque el espacio tras la última letra no se ve; ese sobrante se anula con `margin-right: −ls` (importante en las líneas alineadas a la derecha).
5. SplitText parte en letras (`type: "chars"`, `tag: "span"`, `aria: "none"`); el `letter-spacing` del contenedor lo heredan las letras, así que no hay que tocar cada una.
6. El scroll no anima CSS directamente: GSAP anima el objeto `estado.w` de 160 a 900 con `scrub: 0.9` y, en cada `onUpdate`, `aplicar()` escribe `font-variation-settings` y `letter-spacing` en las líneas.
7. En `resize` (con 150 ms de espera) se vuelve a medir; si cambia la orientación (vertical ↔ horizontal) se reconstruyen las líneas, se vuelven a partir y las letras se colocan ya en su sitio (sin repetir la entrada).
## Interacción
- **Scroll:** es la interacción principal. Funciona igual con rueda, trackpad, teclado (espacio, AvPág, flechas) y gesto táctil, porque solo depende de la posición de scroll.
- **Tema:** botón `.tema` con `aria-pressed` (false = claro, true = oscuro) y `aria-label="Modo oscuro"`. Al pulsarlo alterna `data-tema` en `<html>` y guarda la elección en `localStorage` bajo `tp-tema` (dentro de `try/catch`: si no hay almacenamiento, sigue funcionando).
- **CTA:** «Ver proyectos» lleva a `#trabajo` (scroll suave por CSS); «Hablemos de tu marca» abre `mailto:hola@tallermeridiano.com`.
- **Hover (solo punteros finos):** subrayado que crece en los enlaces de la navegación, flecha que gira −45° en el botón lleno, fondo de acento en el botón lleno, relleno de tinta en el botón de contorno, nombre de proyecto que se desplaza 10 px y engorda a 640.
- **Foco:** `:focus-visible` con contorno de 2 px en color de acento y separación de 4 px en todos los controles. Enlace «Saltar al contenido» visible al tabular.
## Adaptación a móvil y tablet
- **Composición del titular:** se decide por proporción, no por ancho: si `innerWidth / innerHeight < 0.9` cada palabra va en su línea (móvil y tablet en vertical: «Diseño / con / acento.», con «con» alineada a la derecha al mismo tamaño que «Diseño»). En horizontal se mantienen las dos líneas originales.
- **≤ 900 px:** el pie del hero pasa a una columna (subtítulo arriba, botones debajo); la cabecera de «Trabajo reciente» a una columna; las filas de proyecto muestran número, nombre y año en la primera fila, servicio debajo del nombre, y se oculta la localidad.
- **≤ 640 px:** hero de 200svh; nav de 60 px de alto y sin enlaces (quedan marca e interruptor de tema); botones a ancho completo, apilados, de 50 px; la franja de datos pierde la pista central y queda en dos columnas (coordenadas · peso).
- `.hero__pin` tiene `min-height: 560px` para pantallas muy bajas; el gutter es `clamp(14px, 1.6vw, 28px)`.
- Probado de 360 a 1920 px sin scroll horizontal: en 390×844 el titular ocupa unos 280 px de alto en tres líneas (≈ 120 / 120 / 113 px de cuerpo); en 1920×900 queda en diagonal.
## Accesibilidad y movimiento reducido
- HTML semántico: `main`, `section` con `aria-labelledby`, `header`, `nav aria-label="Principal"`, `h1` único, `h2`, `h3` por proyecto, `ul` para la lista, `footer`.
- El h1 se lee completo gracias a `span.sr`; las letras partidas están dentro de un contenedor `aria-hidden="true"`. SVG decorativos con `aria-hidden="true"`; la pista y la lectura de peso también son decorativas.
- Contraste AA: tinta suave #5b5448 sobre #efe9dd ≈ 6,3:1; el botón lleno en hover (#efe9dd sobre #b33810) ≈ 5:1; en oscuro, el acento #ff6b3d sobre #121110 ≈ 6,6:1.
- `prefers-reduced-motion: reduce`: no hay entrada ni scroll ligado; el hero mide `auto` (el pin deja de ser sticky), el titular se muestra al instante con peso fijo **720** y su espaciado calculado (ocupa el mismo ancho), la raya de la pista no se anima y las transiciones duran 0,01 ms.
- Si GSAP no carga, se quita la clase `js`, el titular se ajusta igualmente con JS (sin partir en letras) y todo queda visible con peso 160.
## Rendimiento
- Sin WebGL ni bucles propios: solo se trabaja cuando hay scroll (ScrollTrigger con `scrub`) y durante la entrada. Cada fotograma escribe dos propiedades en 2–3 elementos.
- La tabla de anchuras se mide una vez por carga y por `resize` (9 lecturas por línea), nunca durante el scroll.
- La animación CSS de la pista se pausa (`animation-play-state: paused`) cuando el hero sale de pantalla, vía IntersectionObserver (clase `.fuera`).
- `will-change` solo en `.linea__txt` (letter-spacing) y en las letras (transform).
- HTML final de unos 23 KB.
## Personalización
- **Marca y textos:** Taller Meridiano, Estudio de marca y web, Valdera, 2014, 00°00′ N 00°00′ E, Calle Álamos 12, hola@tallermeridiano.com, +00 000 000 000, los botones (Ver proyectos, Hablemos de tu marca) y el subtítulo en tres partes (Identidades, webs y packaging para + **negocios con alma que quieren sonar a ellos mismos** + Diseñamos desde Valdera para quien tiene algo que contar y no quiere parecerse a nadie.).
- **Titular:** Diseño y con acento.. Funciona con cualquier texto: el JS calcula tamaños y espaciados. Mejor 1–2 palabras por línea; la primera línea es la que manda el tamaño. Si la última letra es `.`, `!`, `?` o `…` se pinta en acento y entra con rebote.
- **Colores:** los seis tokens de color (claro y oscuro). Mantén `--sobre-acento` igual al papel del tema.
- **Rango de peso:** constantes `WMIN = 160`, `WMAX = 900` y `WQUIETO = 720` (peso con movimiento reducido). Con otra fuente variable, ajusta `MUESTRAS` a su rango de `wght`.
- **Recorrido:** altura de `.hero` (240svh; 200svh en móvil). Más alto = el peso crece más despacio.
- **Otra fuente:** cualquier variable con eje `wght` amplio sirve (el motor mide, no supone), pero Fraunces con `opsz 144` es la que da el contraste fino/negro de la demo.
## Comprobación final
1. Al cargar, las letras suben desde debajo de su línea, sin verse antes de tiempo, y el punto final aparece con un rebote de acento.
2. En 1440×900 el titular cabe entero entre la nav y el subtítulo: la línea 1 arranca en el gutter izquierdo (23 px) y la línea 2 termina en el derecho (alineada a la derecha, con el tope de alto activo); el contador marca 160.
3. Al hacer scroll, el titular pasa de finísimo y espaciado a negro y apretado **sin cambiar de anchura**; el contador llega a 900 y la barra se llena.
4. El hero se queda fijo (sticky) mientras dura el recorrido de 240svh y luego aparece «Trabajo reciente».
5. En 390×844 el titular se ve en tres líneas «Diseño / con / acento.», con «con» a la derecha; botones apilados a ancho completo.
6. En 1920×900 el titular queda en diagonal (línea 1 a la izquierda, línea 2 a la derecha) y cabe entero en alto.
7. El interruptor alterna claro/oscuro con transición de 0,7 s, `aria-pressed` cambia y la elección se recuerda.
8. Con `prefers-reduced-motion: reduce` no hay animaciones; el titular aparece a peso 720 y el hero no es sticky.
9. Tildes, ñ y el punto de acento se ven completos (no los recorta la máscara).
10. Cero errores en consola, sin scroll horizontal de 360 a 1920 px.
11. Foco visible en todos los enlaces y botones; «Saltar al contenido» aparece al tabular.
12. Al redimensionar o girar el móvil, el titular se recompone sin repetir la entrada.Prueba 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.

Calco prácticamente exacto: las capturas de escritorio y móvil coinciden píxel a píxel, y la entrada letra a letra, el paso de peso 160 a 900 sin cambiar de anchura, el tema y la adaptación son idénticos. Solo cambia la meta description.

El efecto principal está bien (entrada con máscara, peso 160→900 a anchura constante, contador y barra, escalera en móvil), pero en escritorio el eyebrow se monta sobre el filete de la nav y la segunda línea no queda alineada a la derecha. También cambian detalles de la sección de trabajo: el hover pinta el nombre en acento en vez de engordarlo, el h2 sale más grande y el grano y el logo son distintos.
Solo publicamos lo que hemos probado. Otras herramientas (ChatGPT, Gemini, Lovable…) pueden dar resultados distintos.
Paso a paso
Personaliza
Copia el Completo o el Lite
Pega e itera
Publica
Licencia en claro
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
No puedes
Es la misma licencia para lo gratis y lo Pro. La tienes entera en licencia de uso.
Preguntas frecuentes
Versión 1.0 · publicada el 30-09-2026

Bloques de web · JS

Bloques de web · CSS

Bloques de web · CSS

Bloques de web · JS

Bloques de web · GSAP

Bloques de web · GSAP

Bloques de web · SVG

Bloques de web · GSAP