
Titular gigante
GratisBloques de web · GSAP
La pieza
Cada letra de los titulares es una ficha de fichero con dos hojas. Al pasar el cursor o tocar, la cara se vuelca hacia atrás sobre su borde superior y una copia idéntica sube desde el cajón con un muelle físico resuelto en forma cerrada y horneado en las keyframes de la Web Animations API. El movimiento se escalona desde el inicio, el final o el centro de la palabra y el color vivo existe solo en la copia que entra: aparece al subir y se funde a negro al asentarse. La portada se hojea sola, las palabras entran al hacerse visibles y un banco de pruebas deja escribir tu propia palabra, afinar rigidez, amortiguación y escalonado, y ver la curva real del muelle. Encaja en estudios de diseño, tipografía, editoriales, arquitectura o cualquier web que quiera titulares con carácter. Con movimiento reducido solo hay un fundido de color.
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: Texto de lámina que salta
Una sección editorial en blanco y negro cuyos titulares están hechos de fichas de fichero: al pasar el cursor o tocar, la cara de cada letra salta hacia atrás sobre su borde superior y una copia idéntica, en color vivo, sube desde el cajón con un muelle físico y se asienta en negro.
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 (aquí no hace falta ninguna: solo Google Fonts). Todos los valores numéricos de este documento son definitivos: úsalos tal cual. Responde solo con el archivo completo.
## Qué es
Una página de una sola sección larga para un taller de tipografía y rotulación ficticio, **Taller Gálvez**, en Portovel. Tiene tres bloques: una portada con un titular gigante de dos líneas, un índice de seis trabajos cuyos nombres son filas pulsables, y un banco de pruebas donde el visitante escribe su propia palabra y ajusta el muelle.
La idea central: **cada letra es una ficha de fichero con dos hojas**. Una hoja es la cara visible; la otra espera escondida debajo, recortada por el borde inferior de la letra. Al disparar el efecto:
1. La cara se vuelca hacia atrás girando sobre su **borde superior** (`rotateX` negativo, charnela arriba) y se apaga.
2. Una **copia de la misma letra** sube desde abajo (traslación del 100 % al 0 % de su alto con una inclinación `rotateX` de 62° que se va enderezando) y se asienta en su sitio.
3. Todo ello con un **muelle físico real**, no una curva Bézier: se resuelve la ecuación del oscilador amortiguado en forma cerrada, se muestrea en 44 puntos y se hornea en las keyframes de la Web Animations API (`element.animate`).
4. El movimiento se **escalona** de letra en letra desde el inicio, el final o el centro de la palabra.
5. El titular es **negro sobre blanco**. El color vivo existe solo en la copia que entra: aparece al subir y se funde otra vez a negro mientras el muelle se asienta. Un interruptor del banco de pruebas permite dejarlo fijo.
Al terminar, las dos hojas **intercambian sus papeles** (la que entró pasa a ser la cara; la que se fue queda escondida abajo), así que el efecto se puede repetir indefinidamente sin reiniciar nada.
Extras: las palabras aparecen la primera vez con el mismo volteo al entrar en pantalla; la portada se hojea sola cada 4,2 s si nadie la toca; y una gráfica SVG dibuja la curva del muelle activo con un punto que la recorre en cada volteo.
## Librerías y esqueleto
Sin librerías JS. Solo estas fuentes, dentro de `<head>`:
```html
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cada letra, una ficha · Taller Gálvez, tipografía y rotulación en Portovel</title>
<meta name="description" content="Titulares cuyas letras son fichas de fichero: la cara salta hacia atrás sobre su borde y otra idéntica sube desde abajo, con muelle físico.">
<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=Archivo:wdth,wght@62..125,100..900&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>/* CSS completo más abajo */</style>
</head>
<body>
<!-- cuerpo completo más abajo -->
<script>/* JS completo más abajo */</script>
</body>
</html>
```
- **Archivo** (variable, ejes `wdth` 62–125 y `wght` 100–900): todos los titulares y el texto. Los titulares usan `font-weight: 800` con `font-stretch: 70 %` (condensada y robusta); los subtítulos, 78 %. Tiene ñ, tildes, ¿ y ¡.
- **IBM Plex Mono** 400/500: etiquetas, números y lecturas, en mayúsculas con `letter-spacing: .08em`.
- Sin imágenes, sin iconos externos, sin emojis. Sin cursivas ni tipografía manuscrita en ningún sitio.
## Tokens de diseño
Blanco, negro y **un único color vivo**. Sin pasteles, sin degradados, sin sombras.
```css
:root{
color-scheme:light;
--fondo:#ffffff;
--tinta:#0b0b0b;
--tinta-2:#5a5a5a;
--linea:rgba(11,11,11,.16);
--acento:#ff2e17;
--f-display:"Archivo","Helvetica Neue",Arial,sans-serif;
--f-mono:"IBM Plex Mono",ui-monospace,Menlo,Consolas,monospace;
--e-salida:cubic-bezier(.16,1,.3,1);
--lado:clamp(20px,4.4vw,64px);
}
```
Reglas de uso: el acento **solo** lo usa el JS en las keyframes de la copia que entra (se lee de `--acento`, que debe ser un hexadecimal de 3 o 6 cifras). Nunca se pinta con él un fondo, un borde, un botón ni un foco. Los controles (botones, segmentado, interruptor, deslizadores) son negro sobre blanco y se invierten al activarse. El foco visible es un contorno de 2 px en tinta con 4 px de separación.
## Estructura y contenido
Árbol de la página (todo dentro de `div.pagina`, ancho máximo 1560 px, margen lateral `--lado`):
- `header.barra.mono`: tres textos en una fila, con filete inferior de 1 px en tinta: **Taller Gálvez** (negrita visual en tinta) · «Tipografía y rotulación · Portovel» (centrado, se oculta en móvil) · «Cuaderno de muestras · Otoño 2026» (en móvil queda solo «Otoño 2026»).
- `section.portada`:
- Etiqueta `.cab.mono` «01 — Muestrario» con un filete que se estira hasta el borde derecho.
- `h1.titular` con dos `span.palabra`: «Cada letra,» y «una ficha.», cada una en su línea. Tamaño `clamp(60px,21vw,360px)`, `font-stretch: 70 %`, interlineado 1, `letter-spacing: -.025em`. La segunda línea se sube `-.2em`. El titular entero se sube `-.1em`.
- Pie de portada en dos columnas (5fr / 3fr): párrafo «Un titular que se hojea como un fichero. Cada letra se vuelca sobre su borde superior mientras otra idéntica sube desde el cajón: negro sobre blanco, y el color solo en la ficha que entra.» y, a la derecha, dos líneas mono: «Pasa el cursor por el titular» (en pantallas táctiles, «Toca el titular») y «Muelle real · sin curvas Bézier».
- `section.sec` índice: etiqueta «02 — Índice de trabajos», `h2` «Seis rótulos, seis maneras de entrar.», nota «Cada nombre arranca el escalonado desde un punto distinto: el inicio, el final o el centro de la palabra.» y una lista `ol.indice` de seis filas (`button.fila`):
1. Mercado Alto — Cartelería de puestos · Portovel · 2025 · desde el inicio
2. Cuatro Vientos — Etiquetas y fachada · Altavia · 2025 · desde el final
3. Hotel Albaida — Rotulación de recepción · Valmar · 2024 · desde el centro
4. Cine de Verano — Cartel de temporada · Sanvera · 2024 · desde el inicio
5. Pasacalle — Colección de cubiertas · Portovel · 2023 · desde el final
6. Farmacia Nueva — Identidad y vitrina · Marelva · 2023 · desde el centro
- `section.sec` banco de pruebas: etiqueta «03 — Banco de pruebas». Rejilla 4fr / 7fr. Izquierda, los controles; derecha, la escena con la palabra y la curva.
- `footer.pie.mono`: «Taller Gálvez · Calle Larga, 12 · 00000 Portovel» y, a la derecha, el email (`mailto:`) y el teléfono (`href="tel:+00000000000"`, sin espacios).
### HTML del cuerpo (literal)
```html
<div class="pagina">
<header class="barra mono">
<span><b>Taller Gálvez</b></span>
<span>Tipografía y rotulación · Portovel</span>
<span><span class="ancho">Cuaderno de muestras · </span>Otoño 2026</span>
</header>
<main>
<section class="portada" aria-labelledby="titulo">
<p class="cab mono"><span>01 — Muestrario</span></p>
<h1 class="titular" id="titulo">
<span class="palabra" data-origen="inicio" data-reveal data-hero>Cada letra,</span>
<span class="palabra" data-origen="inicio" data-reveal data-hero data-retardo="240">una ficha.</span>
</h1>
<div class="pie-portada">
<p class="lede">Un titular que se hojea como un fichero. Cada letra se vuelca sobre su borde superior mientras otra idéntica sube desde el cajón: negro sobre blanco, y el color solo en la ficha que entra.</p>
<p class="pistas mono">
<span class="raton">Pasa el cursor por el titular</span>
<span class="tactil">Toca el titular</span>
<span>Muelle real · sin curvas Bézier</span>
</p>
</div>
</section>
<section class="sec" aria-labelledby="t-indice">
<p class="cab mono"><span>02 — Índice de trabajos</span></p>
<h2 id="t-indice">Seis rótulos, seis maneras de entrar.</h2>
<p class="nota">Cada nombre arranca el escalonado desde un punto distinto: el inicio, el final o el centro de la palabra.</p>
<ol class="indice">
<li><button type="button" class="fila"><span class="num mono">01</span><span class="palabra" data-origen="inicio" data-reveal>Mercado Alto</span><span class="det mono"><b>Cartelería de puestos</b><span>Portovel · 2025 · desde el inicio</span></span></button></li>
<li><button type="button" class="fila"><span class="num mono">02</span><span class="palabra" data-origen="final" data-reveal>Cuatro Vientos</span><span class="det mono"><b>Etiquetas y fachada</b><span>Altavia · 2025 · desde el final</span></span></button></li>
<li><button type="button" class="fila"><span class="num mono">03</span><span class="palabra" data-origen="centro" data-reveal>Hotel Albaida</span><span class="det mono"><b>Rotulación de recepción</b><span>Valmar · 2024 · desde el centro</span></span></button></li>
<li><button type="button" class="fila"><span class="num mono">04</span><span class="palabra" data-origen="inicio" data-reveal>Cine de Verano</span><span class="det mono"><b>Cartel de temporada</b><span>Sanvera · 2024 · desde el inicio</span></span></button></li>
<li><button type="button" class="fila"><span class="num mono">05</span><span class="palabra" data-origen="final" data-reveal>Pasacalle</span><span class="det mono"><b>Colección de cubiertas</b><span>Portovel · 2023 · desde el final</span></span></button></li>
<li><button type="button" class="fila"><span class="num mono">06</span><span class="palabra" data-origen="centro" data-reveal>Farmacia Nueva</span><span class="det mono"><b>Identidad y vitrina</b><span>Marelva · 2023 · desde el centro</span></span></button></li>
</ol>
</section>
<section class="sec" aria-labelledby="t-banco">
<p class="cab mono"><span>03 — Banco de pruebas</span></p>
<div class="banco">
<div class="controles">
<div>
<h2 id="t-banco">Afina el muelle.</h2>
<p class="nota">Escribe tu palabra y ajusta rigidez, amortiguación y escalonado. La curva es el muelle real que se hornea en los fotogramas.</p>
</div>
<p class="aviso" id="aviso-reducido" hidden>Tu sistema pide menos movimiento: las fichas no se vuelcan, solo cambian de color un instante.</p>
<div class="campo">
<label class="mono" for="txt">Palabra de prueba</label>
<input type="text" id="txt" value="Fichero" maxlength="14" autocomplete="off" spellcheck="false" placeholder="Escribe aquí">
</div>
<fieldset class="campo">
<legend class="mono">Escalonado desde</legend>
<div class="seg">
<label class="mono"><input type="radio" name="origen" value="inicio" checked><span>Inicio</span></label>
<label class="mono"><input type="radio" name="origen" value="centro"><span>Centro</span></label>
<label class="mono"><input type="radio" name="origen" value="final"><span>Final</span></label>
</div>
</fieldset>
<div class="campo">
<label class="mono" for="rig">Rigidez <output id="o-rig">210</output></label>
<input type="range" id="rig" min="80" max="420" step="5" value="210">
</div>
<div class="campo">
<label class="mono" for="amo">Amortiguación <output id="o-amo">15</output></label>
<input type="range" id="amo" min="4" max="36" step="1" value="15">
</div>
<div class="campo">
<label class="mono" for="esc">Escalonado <output id="o-esc">35 ms</output></label>
<input type="range" id="esc" min="10" max="90" step="5" value="35">
</div>
<div class="campo">
<span class="mono" style="color:var(--tinta-2)">Perfil del muelle</span>
<div class="perfiles" role="group" aria-label="Perfil del muelle">
<button type="button" data-k="330" data-c="27" aria-pressed="false">Seco</button>
<button type="button" data-k="210" data-c="15" aria-pressed="true">Fichero</button>
<button type="button" data-k="140" data-c="6" aria-pressed="false">Gelatina</button>
</div>
</div>
<label class="interruptor"><input type="checkbox" id="queda"><i aria-hidden="true"></i><span>Dejar el color en la ficha nueva</span></label>
<button type="button" class="voltear" id="voltear">Voltear la palabra</button>
</div>
<div class="escena">
<div class="escena-cab mono"><span>Palabra de prueba</span><span>Fichas volteadas · <output id="contador">000</output></span></div>
<div class="escena-pal"><span class="palabra" id="banco-pal" data-origen="inicio" data-reveal>Fichero</span></div>
<div class="curva">
<svg id="plot" viewBox="0 0 480 130" role="img" aria-label="Curva del muelle: la posición de la ficha a lo largo del tiempo, con rebote hasta asentarse en 1">
<line class="eje" x1="8" y1="112" x2="472" y2="112"/>
<line class="guia" id="uno" x1="8" x2="472"/>
<path class="trazo" id="curva" d=""/>
<circle class="punto" id="punto" r="5.5" cx="0" cy="0"/>
</svg>
<p class="lectura mono"><span>Duración <b id="l-dur">—</b></span><span>Sobreimpulso <b id="l-sob">—</b></span><span>Fichas por palabra <b id="l-tot">—</b></span></p>
</div>
</div>
</div>
</section>
</main>
<footer class="pie mono">
<span>Taller Gálvez · Calle Larga, 12 · 00000 Portovel</span>
<span><a href="mailto:hola@tallergalvez.com">hola@tallergalvez.com</a> · <a href="tel:+00000000000">+00 000 000 000</a></span>
</footer>
</div>
```
### CSS completo (literal)
```css
:root{
color-scheme:light;
--fondo:#ffffff;
--tinta:#0b0b0b;
--tinta-2:#5a5a5a;
--linea:rgba(11,11,11,.16);
--acento:#ff2e17;
--f-display:"Archivo","Helvetica Neue",Arial,sans-serif;
--f-mono:"IBM Plex Mono",ui-monospace,Menlo,Consolas,monospace;
--e-salida:cubic-bezier(.16,1,.3,1);
--lado:clamp(20px,4.4vw,64px);
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
body{margin:0;min-height:100svh;background:var(--fondo);color:var(--tinta);font:400 16px/1.55 var(--f-display);font-stretch:100%;-webkit-font-smoothing:antialiased;overflow-x:hidden}
button,input{font:inherit;color:inherit}
a{color:inherit}
[hidden]{display:none!important}
:focus-visible{outline:2px solid var(--tinta);outline-offset:4px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.mono{font:500 11.5px/1.45 var(--f-mono);letter-spacing:.08em;text-transform:uppercase}
/* ---------- barra y estructura ---------- */
.pagina{max-width:1560px;margin:0 auto;padding:0 var(--lado)}
.barra{display:flex;justify-content:space-between;gap:24px;padding:20px 0;border-bottom:1px solid var(--tinta);color:var(--tinta-2)}
.barra b{color:var(--tinta);font-weight:500}
.barra>span{white-space:nowrap}
.barra>span:nth-child(2){flex:1;text-align:center}
.sec{padding:clamp(48px,7vw,104px) 0 0}
.cab{display:flex;align-items:baseline;gap:18px;margin:0 0 clamp(22px,3vw,40px);color:var(--tinta-2)}
.cab::after{content:"";flex:1;height:1px;background:var(--linea);transform:translateY(-3px)}
h2{margin:0;font:800 clamp(30px,3.9vw,60px)/1 var(--f-display);font-stretch:78%;letter-spacing:-.02em}
.nota{margin:14px 0 0;max-width:46ch;color:var(--tinta-2);font-size:clamp(15px,1.2vw,18px)}
/* ---------- la ficha: letra con dos hojas ---------- */
.palabra{display:block;width:fit-content;max-width:100%;position:relative;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}
.vis{display:block}
.g{display:inline-block;white-space:nowrap}
.l{display:inline-block;position:relative;line-height:1.15;perspective:4.2em;clip-path:inset(-2.5em -.4em 0 -.4em)}
.h{display:block;position:relative;white-space:pre;backface-visibility:hidden}
.h+.h{position:absolute;inset:0}
.h[data-e="cara"]{z-index:2;transform-origin:50% 0}
.h[data-e="espera"]{z-index:1;transform-origin:50% 100%;transform:translateY(100%) rotateX(62deg)}
.l[data-vacia] .h[data-e="cara"]{opacity:0}
/* ---------- portada ---------- */
.portada{padding-top:clamp(26px,4.4vw,60px)}
.portada .cab{margin-bottom:clamp(8px,1.5vw,20px)}
.titular{margin:0;font:800 clamp(60px,21vw,360px)/1 var(--f-display);font-stretch:70%;letter-spacing:-.025em}
.titular{margin-top:-.1em}
.titular .palabra+.palabra{margin-top:-.2em}
.pie-portada{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,3fr);gap:clamp(24px,6vw,96px);align-items:end;padding:clamp(12px,2vw,28px) 0 0}
.lede{margin:0;max-width:52ch;font-size:clamp(16px,1.35vw,20px);line-height:1.5}
.pistas{display:grid;gap:6px;justify-items:start;color:var(--tinta-2)}
.pistas .tactil{display:none}
@media (hover:none){.pistas .raton{display:none}.pistas .tactil{display:inline}}
/* ---------- índice de trabajos ---------- */
.indice{list-style:none;margin:clamp(20px,3vw,40px) 0 0;padding:0;border-bottom:1px solid var(--tinta)}
.fila{position:relative;display:grid;grid-template-columns:clamp(40px,5vw,72px) minmax(0,1fr) auto;align-items:center;gap:0 18px;width:100%;margin:0;padding:clamp(10px,1.6vw,22px) 0;background:none;border:0;border-top:1px solid var(--tinta);text-align:left;cursor:pointer}
.fila .num{color:var(--tinta-2);align-self:start;padding-top:clamp(12px,2vw,30px)}
.fila .palabra{font:800 clamp(46px,8.6vw,138px)/1 var(--f-display);font-stretch:70%;letter-spacing:-.02em}
.fila .det{display:grid;gap:4px;text-align:right;color:var(--tinta-2)}
.fila .det b{color:var(--tinta);font-weight:500}
.fila::after{content:"";position:absolute;left:0;right:0;top:-1px;height:4px;background:var(--tinta);transform:scaleX(0);transform-origin:left;transition:transform .7s var(--e-salida)}
.fila:hover::after,.fila:focus-visible::after{transform:scaleX(1)}
.fila:focus-visible{outline-offset:-3px}
/* ---------- banco de pruebas ---------- */
.banco{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,7fr);gap:clamp(28px,5vw,88px);align-items:start;padding-bottom:clamp(48px,7vw,104px)}
.controles{display:grid;gap:22px}
.campo{display:grid;gap:8px;border:0;margin:0;padding:0;min-width:0}
.campo>label,.campo>legend{padding:0;color:var(--tinta-2)}
.campo>label{display:flex;justify-content:space-between;gap:12px}
.campo>label output{color:var(--tinta)}
input[type=text]{width:100%;height:54px;padding:0 16px;background:var(--fondo);border:1px solid var(--tinta);border-radius:0;font:700 22px/1 var(--f-display);font-stretch:80%;letter-spacing:-.01em}
input[type=text]::placeholder{color:#8a8a8a}
.seg{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--tinta)}
.seg label{position:relative;display:block}
.seg input{position:absolute;opacity:0;inset:0;width:100%;height:100%;margin:0;cursor:pointer}
.seg span{display:block;padding:13px 6px;text-align:center;cursor:pointer;transition:background .25s,color .25s}
.seg label+label span{border-left:1px solid var(--tinta)}
.seg input:checked+span{background:var(--tinta);color:var(--fondo)}
.seg input:focus-visible+span{outline:2px solid var(--tinta);outline-offset:3px}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:28px;margin:0;background:transparent;cursor:pointer}
input[type=range]::-webkit-slider-runnable-track{height:2px;background:var(--tinta)}
input[type=range]::-moz-range-track{height:2px;background:var(--tinta)}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;margin-top:-9px;border-radius:50%;background:var(--tinta);border:3px solid var(--fondo);box-shadow:0 0 0 1px var(--tinta);transition:transform .3s var(--e-salida)}
input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--tinta);border:3px solid var(--fondo);box-shadow:0 0 0 1px var(--tinta)}
input[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}
.perfiles{display:flex;flex-wrap:wrap;gap:8px}
.perfiles button,.voltear{height:46px;padding:0 18px;border:1px solid var(--tinta);border-radius:0;background:var(--fondo);cursor:pointer;transition:background .25s,color .25s,transform .4s var(--e-salida)}
.perfiles button[aria-pressed=true]{background:var(--tinta);color:var(--fondo)}
.perfiles button:hover:not([aria-pressed=true]){background:#efefef}
.interruptor{display:flex;align-items:center;gap:14px;cursor:pointer;font-size:15px;line-height:1.3}
.interruptor input{position:absolute;opacity:0;width:1px;height:1px}
.interruptor i{flex:none;position:relative;width:46px;height:26px;border:1px solid var(--tinta);transition:background .25s}
.interruptor i::after{content:"";position:absolute;left:3px;top:3px;width:18px;height:18px;background:var(--tinta);transition:transform .35s var(--e-salida),background .25s}
.interruptor input:checked+i{background:var(--tinta)}
.interruptor input:checked+i::after{transform:translateX(20px);background:var(--fondo)}
.interruptor input:focus-visible+i{outline:2px solid var(--tinta);outline-offset:3px}
.voltear{height:56px;background:var(--tinta);color:var(--fondo);font-weight:700;font-size:17px;letter-spacing:.01em}
.voltear:hover{transform:translateY(-2px)}
.voltear:active{transform:translateY(1px)}
.escena{border:1px solid var(--tinta);container-type:inline-size;min-width:0}
.escena-cab{display:flex;justify-content:space-between;gap:12px;padding:14px 18px;border-bottom:1px solid var(--linea);color:var(--tinta-2)}
.escena-cab output{color:var(--tinta)}
.escena-pal{display:flex;align-items:center;justify-content:center;min-height:clamp(220px,28vw,360px);padding:20px 12px}
#banco-pal{--n:7;font:800 clamp(40px,calc(86cqw / (var(--n) * .5)),210px)/1 var(--f-display);font-stretch:70%;letter-spacing:-.02em;margin:0 auto;text-align:center}
#banco-pal .vis{text-align:center}
.curva{border-top:1px solid var(--linea);padding:14px 18px 16px}
.curva svg{display:block;width:100%;height:auto;overflow:visible}
.curva .eje{stroke:var(--linea);stroke-width:1;vector-effect:non-scaling-stroke}
.curva .guia{stroke:var(--tinta-2);stroke-width:1;stroke-dasharray:3 4;vector-effect:non-scaling-stroke}
.curva .trazo{fill:none;stroke:var(--tinta);stroke-width:2.6;stroke-linejoin:round;stroke-linecap:round;vector-effect:non-scaling-stroke}
.curva .punto{fill:var(--tinta)}
.lectura{display:flex;flex-wrap:wrap;justify-content:space-between;gap:6px 18px;margin-top:8px;color:var(--tinta-2)}
.lectura b{color:var(--tinta);font-weight:500}
.aviso{margin:0;padding:12px 16px;border:1px dashed var(--tinta);font-size:14px}
/* ---------- pie ---------- */
.pie{display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px 28px;padding:22px 0 30px;border-top:1px solid var(--tinta);color:var(--tinta-2)}
.pie a{text-decoration:none;border-bottom:1px solid var(--linea)}
.pie a:hover{border-bottom-color:var(--tinta)}
/* ---------- adaptación ---------- */
@media (max-width:900px){
.banco{grid-template-columns:minmax(0,1fr)}
.pie-portada{grid-template-columns:minmax(0,1fr);gap:16px}
}
@media (max-width:640px){
.barra>span:nth-child(2),.barra .ancho{display:none}
.barra{justify-content:space-between}
.fila{grid-template-columns:32px minmax(0,1fr);row-gap:8px}
.fila .det{grid-column:2;text-align:left}
.fila .num{padding-top:14px}
.escena-pal{min-height:200px}
}
@media (prefers-reduced-motion:reduce){
.fila::after,.seg span,.interruptor i,.interruptor i::after,.voltear,.perfiles button{transition:none}
}
```
Notas del CSS que importan:
- `.l` (la letra) es `inline-block`, tiene `line-height: 1.15`, `perspective: 4.2em` y `clip-path: inset(-2.5em -.4em 0 -.4em)`: deja salir el volteo por arriba y por los lados, pero recorta por abajo, así la copia «sale del cajón» por el borde inferior de la letra.
- Las dos hojas `.h` comparten tamaño: la primera está en flujo y la segunda es absoluta (`inset: 0`). El estado `data-e="cara"` tiene `z-index: 2` y charnela arriba (`transform-origin: 50% 0`). El estado `data-e="espera"` tiene `z-index: 1`, charnela abajo (`50% 100%`) y `transform: translateY(100%) rotateX(62deg)`. `backface-visibility: hidden` en ambas.
- `.l[data-vacia] .h[data-e="cara"]` con `opacity: 0` mantiene invisible la cara de una letra que aún no ha hecho su primera entrada.
- El nombre de cada fila tiene `font: 800 clamp(46px,8.6vw,138px)/1`, `font-stretch: 70 %`. La raya negra de 4 px que crece desde la izquierda al pasar por una fila es su `::after` con `transform: scaleX` y transición de .7 s con `--e-salida`.
- La palabra del banco usa `container-type: inline-size` en la escena y un tamaño `clamp(40px, calc(86cqw / (var(--n) * .5)), 210px)`, donde `--n` es el número de letras que fija el JS, de modo que cualquier palabra de hasta 14 caracteres cabe sin desbordar.
## Movimiento
Sea `p` el valor del muelle en cada muestra (de 0 a 1, con sobreimpulso) y `o` el desplazamiento de la keyframe (0 a 1). Cada letra se anima con `fill: 'both'` y `easing: 'linear'` (la física ya va en las keyframes); la `duration` es la duración del muelle.
| Elemento | Propiedad | De → a | Duración | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Cara que sale | `transform` | `translateY(-5%·p) rotateX(-104°·p)`, charnela arriba | la del muelle (≈ 646 ms con k 210 y c 15) | lineal sobre keyframes del muelle | índice de escalonado × 35 ms | pasar, pulsar, foco de teclado, automático |
| Cara que sale | `opacity` | `1 − suave(0,35; 0,92; p)` | igual | lineal | igual | igual |
| Copia que entra | `transform` | `translateY(100%·(1−p)) rotateX(62°·(1−p))`, charnela abajo | igual | lineal | igual | igual |
| Copia que entra | `color` | acento hasta `o` = 0,5; luego mezcla en RGB hasta tinta en `o` = 0,9 (con «dejar el color», acento fijo) | igual | lineal | igual | igual |
| Entrada inicial (palabra al hacerse visible) | solo la copia | la cara está oculta (`data-vacia`); solo sube la copia | igual | lineal | retardo de la palabra + escalonado | `IntersectionObserver`, umbral 0,55 |
| Fila del índice | `::after` `scaleX` | 0 → 1 | 700 ms | `cubic-bezier(.16,1,.3,1)` | 0 | hover o foco |
| Punto de la gráfica | `transform` | recorre la curva con `translate(x,y)` | la del muelle | lineal | 0 | cada volteo del banco |
| Controles | fondo y color | invertir negro y blanco | 250 ms | ease | 0 | activar |
`suave(a, b, x)` es el smoothstep: `t = clamp((x−a)/(b−a), 0, 1)` y resultado `t²(3−2t)`.
Parámetros por defecto: rigidez `k = 210`, amortiguación `c = 15`, masa 1, escalonado 35 ms. Orden del escalonado: `inicio` = `i`; `final` = `n−1−i`; `centro` = `|i − (n−1)/2|`. El segundo titular de la portada arranca con un retardo de `0,6 × letras del primero × escalonado` cuando se hojea solo, y de 240 ms en su entrada inicial.
Perfiles del muelle: Seco (330 / 27), Fichero (210 / 15, el de por defecto), Gelatina (140 / 6).
## Código clave
El JS completo, literal. Contiene el oscilador amortiguado en forma cerrada (casos subamortiguado, crítico y sobreamortiguado), la construcción de keyframes, el volteo con intercambio de hojas, los disparadores, el bucle automático de la portada y el banco de pruebas.
```js
(() => {
'use strict';
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const el = (cls, txt) => { const e = document.createElement('span'); e.className = cls; if (txt != null) e.textContent = txt; return e; };
const mq = matchMedia('(prefers-reduced-motion: reduce)');
let reducir = mq.matches;
const aviso = $('#aviso-reducido');
const pintarAviso = () => { aviso.hidden = !reducir; };
mq.addEventListener('change', e => { reducir = e.matches; pintarAviso(); });
pintarAviso();
const AJ = { k: 210, c: 15, esc: 35, origen: 'inicio', queda: false };
let ultimaInteraccion = -1e9;
let contador = 0;
/* ---------- muelle: oscilador amortiguado en forma cerrada ---------- */
const cacheMuelle = new Map();
function muelle(k, c) {
const clave = k + '/' + c;
if (cacheMuelle.has(clave)) return cacheMuelle.get(clave);
const w0 = Math.sqrt(k), z = c / (2 * w0);
const x = t => {
if (Math.abs(z - 1) < 1e-3) return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
if (z < 1) {
const wd = w0 * Math.sqrt(1 - z * z);
return 1 - Math.exp(-z * w0 * t) * (Math.cos(wd * t) + (z * w0 / wd) * Math.sin(wd * t));
}
const s = w0 * Math.sqrt(z * z - 1), a = z * w0;
return 1 - Math.exp(-a * t) * (Math.cosh(s * t) + (a / s) * Math.sinh(s * t));
};
let ultimo = 0;
for (let i = 1; i <= 2.4 * 240; i++) if (Math.abs(x(i / 240) - 1) > 0.004) ultimo = i;
const dur = Math.min(2400, Math.max(260, Math.round(((ultimo + 1) / 240) * 1000)));
const N = 44, v = [];
for (let i = 0; i < N; i++) v.push(x((dur / 1000) * (i / (N - 1))));
v[N - 1] = 1;
const m = { v, dur, pico: Math.max(...v) };
cacheMuelle.set(clave, m);
return m;
}
/* ---------- fotogramas: la física va dentro de las keyframes ---------- */
const suave = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
function leerColor(nombre) {
let h = getComputedStyle(document.documentElement).getPropertyValue(nombre).trim().replace('#', '');
if (h.length === 3) h = h.replace(/./g, '$&$&');
const n = parseInt(h, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
const cacheFotos = new Map();
function fotogramas(m, queda) {
const A = leerColor('--acento'), T = leerColor('--tinta');
const clave = [m.dur, m.v[8], queda, A, T].join('|');
if (cacheFotos.has(clave)) return cacheFotos.get(clave);
const n = m.v.length - 1;
const mezcla = f => A.map((a, i) => Math.round(a + (T[i] - a) * f));
const sale = [], entra = [], color = [];
m.v.forEach((p, i) => {
const o = i / n;
sale.push({
offset: o,
transform: `translateY(${(-5 * p).toFixed(2)}%) rotateX(${(-104 * p).toFixed(2)}deg)`,
opacity: (1 - suave(0.35, 0.92, p)).toFixed(3)
});
const f = queda ? 0 : suave(0.5, 0.9, o);
entra.push({
offset: o,
transform: `translateY(${(100 * (1 - p)).toFixed(2)}%) rotateX(${(62 * (1 - p)).toFixed(2)}deg)`,
color: `rgb(${mezcla(f).join(',')})`
});
color.push({ offset: o, color: `rgb(${mezcla(suave(0.1, 1, o)).join(',')})` });
});
const r = { sale, entra, color };
cacheFotos.set(clave, r);
return r;
}
const demora = (i, n, origen) => origen === 'final' ? n - 1 - i : origen === 'centro' ? Math.abs(i - (n - 1) / 2) : i;
/* ---------- construir la palabra ---------- */
const trocear = s => (window.Intl && Intl.Segmenter)
? [...new Intl.Segmenter('es', { granularity: 'grapheme' }).segment(s)].map(x => x.segment)
: Array.from(s);
function armar(pal, texto, vacia) {
pal.textContent = '';
pal.letras = [];
pal.append(el('sr', texto));
const vis = el('vis');
vis.setAttribute('aria-hidden', 'true');
texto.split(/(\s+)/).forEach(tr => {
if (!tr) return;
if (/^\s+$/.test(tr)) { vis.append(document.createTextNode(' ')); return; }
const g = el('g');
trocear(tr).forEach(ch => {
const l = el('l'), a = el('h', ch), b = el('h', ch);
a.dataset.e = 'cara'; b.dataset.e = 'espera';
l._cara = a; l._espera = b;
if (vacia) l.dataset.vacia = '';
l.append(a, b); g.append(l); pal.letras.push(l);
});
vis.append(g);
});
pal.append(vis);
pal.ocupada = false;
pal.style.setProperty('--n', Math.max(1, pal.letras.length));
}
/* ---------- volteo ---------- */
function volteo(pal, o = {}) {
const letras = pal.letras;
if (!letras || !letras.length || pal.ocupada) return false;
const origen = o.origen || pal.dataset.origen || 'inicio';
const m = muelle(AJ.k, AJ.c);
const F = fotogramas(m, !!o.queda);
const total = letras.length;
pal.ocupada = true;
let pendientes = total;
letras.forEach((l, i) => {
const d = (o.retardo || 0) + demora(i, total, origen) * AJ.esc;
const cara = l._cara, espera = l._espera;
const opt = { duration: m.dur, delay: d, fill: 'both', easing: 'linear' };
const anims = [];
if (reducir) {
anims.push(cara.animate(F.color, { duration: 700, delay: d, fill: 'both', easing: 'ease-out' }));
} else {
if (!o.entrada && l.dataset.vacia === undefined) anims.push(cara.animate(F.sale, opt));
anims.push(espera.animate(F.entra, opt));
}
Promise.all(anims.map(a => a.finished)).then(() => {
if (!reducir || !anims.length) { cara.dataset.e = 'espera'; espera.dataset.e = 'cara'; l._cara = espera; l._espera = cara; }
delete l.dataset.vacia;
if (!reducir) { cara.style.color = ''; espera.style.color = o.queda ? 'var(--acento)' : ''; }
anims.forEach(a => a.cancel());
if (pal.id === 'banco-pal' && !o.entrada) { contador++; $('#contador').textContent = String(contador).padStart(3, '0'); }
if (--pendientes === 0) pal.ocupada = false;
}).catch(() => { anims.forEach(a => a.cancel()); if (--pendientes === 0) pal.ocupada = false; });
});
if (pal.id === 'banco-pal') trazarPunto(m);
return true;
}
/* ---------- disparadores ---------- */
function enlazar(pal, disparador) {
disparador.addEventListener('pointerenter', e => {
if (e.pointerType === 'touch') return;
ultimaInteraccion = performance.now();
volteo(pal, { queda: pal.id === 'banco-pal' && AJ.queda, origen: pal.id === 'banco-pal' ? AJ.origen : undefined });
});
disparador.addEventListener('click', () => {
ultimaInteraccion = performance.now();
volteo(pal, { queda: pal.id === 'banco-pal' && AJ.queda, origen: pal.id === 'banco-pal' ? AJ.origen : undefined });
});
if (disparador !== pal) disparador.addEventListener('focus', () => { if (disparador.matches(':focus-visible')) volteo(pal); });
}
const palabras = $$('.palabra');
palabras.forEach(p => {
armar(p, p.textContent.trim(), !reducir && p.hasAttribute('data-reveal'));
enlazar(p, p.closest('.fila') || p);
});
const hero = palabras.filter(p => p.hasAttribute('data-hero'));
const banco = $('#banco-pal');
/* ---------- entrada al hacerse visibles ---------- */
const revelar = p => volteo(p, { entrada: true, retardo: Number(p.dataset.retardo || 0), queda: false });
const listo = (document.fonts && document.fonts.ready) ? document.fonts.ready : Promise.resolve();
listo.then(() => {
if (!('IntersectionObserver' in window)) { palabras.forEach(revelar); return; }
const io = new IntersectionObserver(es => es.forEach(e => {
if (e.isIntersecting) { io.unobserve(e.target); revelar(e.target); }
}), { threshold: 0.55 });
palabras.forEach(p => { if (p.hasAttribute('data-reveal')) io.observe(p); });
});
/* ---------- la portada se hojea sola cuando nadie la toca ---------- */
const ciclo = ['centro', 'final', 'inicio'];
let vuelta = 0, portadaVisible = true, corriendo = false, primera = true, proximo = 0;
function tick(t) {
if (!corriendo) return;
if (t >= proximo && !hero[0].ocupada) {
proximo = t + 4200;
if (!reducir && (primera || t - ultimaInteraccion > 3800)) {
primera = false;
const origen = ciclo[vuelta++ % ciclo.length];
volteo(hero[0], { origen });
volteo(hero[1], { origen, retardo: hero[0].letras.length * AJ.esc * 0.6 });
}
}
requestAnimationFrame(tick);
}
function gestionarBucle() {
const debe = portadaVisible && !document.hidden && !reducir;
if (debe && !corriendo) { corriendo = true; proximo = Math.max(proximo, performance.now() + 1500); requestAnimationFrame(tick); }
if (!debe) corriendo = false;
}
if ('IntersectionObserver' in window) {
new IntersectionObserver(es => { portadaVisible = es[0].isIntersecting; gestionarBucle(); }).observe($('.portada'));
}
document.addEventListener('visibilitychange', gestionarBucle);
mq.addEventListener('change', gestionarBucle);
proximo = Math.max(4200, performance.now() + 1800);
gestionarBucle();
/* ---------- banco de pruebas ---------- */
const NS = 'http://www.w3.org/2000/svg';
const X0 = 8, X1 = 472, Y0 = 112, YT = 12;
let punto = null, ejeY = () => 0;
function dibujar() {
const m = muelle(AJ.k, AJ.c);
const tope = Math.max(1.25, m.pico) * 1.04;
ejeY = v => Y0 - (v / tope) * (Y0 - YT);
const n = m.v.length - 1;
$('#curva').setAttribute('d', m.v.map((v, i) => (i ? 'L' : 'M') + (X0 + (X1 - X0) * i / n).toFixed(1) + ' ' + ejeY(v).toFixed(1)).join(' '));
const y1 = ejeY(1);
const uno = $('#uno'); uno.setAttribute('y1', y1); uno.setAttribute('y2', y1);
$('#l-dur').textContent = m.dur + ' ms';
$('#l-sob').textContent = Math.max(0, Math.round((m.pico - 1) * 100)) + ' %';
punto = $('#punto');
punto.style.transform = `translate(${X1}px,${y1}px)`;
const nl = banco.letras ? banco.letras.length : 0;
$('#l-tot').textContent = nl + ' · ' + Math.round(m.dur + Math.max(0, nl - 1) * AJ.esc / (AJ.origen === 'centro' ? 2 : 1)) + ' ms en total';
}
function trazarPunto(m) {
if (!punto || reducir) return;
const n = m.v.length - 1;
punto.animate(m.v.map((v, i) => ({
offset: i / n,
transform: `translate(${(X0 + (X1 - X0) * i / n).toFixed(1)}px,${ejeY(v).toFixed(1)}px)`
})), { duration: m.dur, easing: 'linear' });
}
const disparoBanco = () => { ultimaInteraccion = performance.now(); volteo(banco, { origen: AJ.origen, queda: AJ.queda }); };
const fijar = (id, fn) => { const i = $(id); i.addEventListener('input', () => { fn(i.value); dibujar(); }); i.addEventListener('change', disparoBanco); };
fijar('#rig', v => { AJ.k = +v; $('#o-rig').textContent = v; sinPerfil(); });
fijar('#amo', v => { AJ.c = +v; $('#o-amo').textContent = v; sinPerfil(); });
fijar('#esc', v => { AJ.esc = +v; $('#o-esc').textContent = v + ' ms'; });
const perfiles = $$('.perfiles button');
function sinPerfil() { perfiles.forEach(b => b.setAttribute('aria-pressed', String(+b.dataset.k === AJ.k && +b.dataset.c === AJ.c))); }
perfiles.forEach(b => b.addEventListener('click', () => {
AJ.k = +b.dataset.k; AJ.c = +b.dataset.c;
$('#rig').value = AJ.k; $('#amo').value = AJ.c; $('#o-rig').textContent = AJ.k; $('#o-amo').textContent = AJ.c;
sinPerfil(); dibujar(); disparoBanco();
}));
$$('input[name=origen]').forEach(r => r.addEventListener('change', () => { AJ.origen = r.value; dibujar(); disparoBanco(); }));
$('#queda').addEventListener('change', e => { AJ.queda = e.target.checked; disparoBanco(); });
$('#voltear').addEventListener('click', disparoBanco);
let tTexto = 0;
$('#txt').addEventListener('input', e => {
const t = e.target.value.replace(/\s+/g, ' ').trim() || 'Ficha';
armar(banco, t, false);
dibujar();
clearTimeout(tTexto);
tTexto = setTimeout(disparoBanco, 280);
});
dibujar();
})();
```
Puntos delicados que no hay que cambiar:
- La duración del muelle se calcula buscando la última muestra a 1/240 s cuyo error supera 0,004, con un máximo de 2,4 s y un mínimo de 260 ms; después se remuestrea en 44 puntos y se fuerza la última a 1.
- Al terminar cada letra se **intercambian** los `data-e` de las dos hojas y se **cancelan** las animaciones en el mismo instante, para que el estado CSS final coincida con la última keyframe y no haya parpadeo.
- Cada palabra se bloquea (`ocupada`) hasta que se asienta su última letra; los disparos intermedios se ignoran.
- El bucle automático usa `requestAnimationFrame` y `performance.now()` (no `setInterval`): se detiene si la portada no está en pantalla, la pestaña está oculta o el usuario pide menos movimiento.
## Interacción
- **Ratón**: `pointerenter` sobre una palabra la voltea (se ignora el puntero táctil para no duplicar). `click` también la voltea, con lo que sirve en táctil y con teclado.
- **Táctil**: un toque voltea la palabra; además, cada palabra se voltea sola al entrar en pantalla y la portada se hojea sola.
- **Teclado**: las filas del índice son `button` y se voltean al recibir foco visible (`:focus-visible`) o con Enter o Espacio. El banco de pruebas se maneja entero con teclado: campo de texto, grupo de radios, tres deslizadores, tres botones de perfil, interruptor y botón «Voltear la palabra».
- **Portada automática**: cada 4,2 s voltea las dos líneas con el origen rotando entre centro, final e inicio, salvo que el usuario haya interactuado en los últimos 3,8 s. El primer ciclo no espera ese margen. Empieza a los 4,2 s de cargar la página (o 1,8 s después de que la portada vuelva a estar visible).
- **Banco de pruebas**: al escribir se reconstruye la palabra (máximo 14 caracteres; si queda vacío, «Ficha») y 280 ms después se voltea; al soltar un deslizador, al cambiar el origen, el perfil o el interruptor, y al pulsar «Voltear la palabra», se voltea. Los deslizadores actualizan la curva y las lecturas mientras se arrastran. Los botones de perfil colocan los deslizadores y marcan `aria-pressed`. El contador «Fichas volteadas» suma las letras de la palabra del banco ya asentadas (no cuenta las entradas iniciales).
- **Gráfica**: `viewBox="0 0 480 130"`; eje inferior en `y = 112`; la curva llega como máximo a `y = 12`, escalada a `max(1,25; pico) × 1,04`; trazo de 2,6 px en tinta con `vector-effect: non-scaling-stroke`; guía discontinua en el valor 1; un punto de 5,5 px recorre la curva en cada volteo. Debajo, lecturas con la duración en ms, el sobreimpulso en % y las fichas por palabra con el tiempo total.
## Adaptación a móvil y tablet
- **≤ 900 px**: el banco de pruebas pasa a una sola columna, y también el pie de la portada.
- **≤ 640 px**: se oculta el texto central de la barra y «Cuaderno de muestras ·»; las filas pasan a dos columnas (32 px de número y el resto), con el detalle debajo del nombre y alineado a la izquierda; la escena del banco baja a 200 px de alto mínimo.
- El titular escala con `21vw` (mínimo 60 px, máximo 360 px): en 390 px «Cada letra,» cabe en una línea sin desbordar. No hay scroll horizontal en ningún ancho de 360 a 1920 px.
- `@media (hover: none)` cambia la pista «Pasa el cursor por el titular» por «Toca el titular».
## Accesibilidad y movimiento reducido
- Cada palabra contiene un `span.sr` con el texto real (oculto visualmente) y el contenedor de letras lleva `aria-hidden="true"`: el lector de pantalla lee «Cada letra, una ficha.» y no letra por letra. Los títulos son `h1` y `h2` reales.
- Contraste: tinta sobre blanco ≈ 19:1; texto secundario `#5a5a5a` sobre blanco ≈ 6,9:1. El acento solo aparece en texto de gran tamaño y de forma transitoria; no transmite información esencial.
- Foco visible en todos los controles; el radio y la casilla están ocultos visualmente pero siguen siendo focables, y su foco se dibuja en el elemento visible contiguo.
- `prefers-reduced-motion: reduce`: las palabras se muestran estáticas desde el principio (no hay `data-vacia`), desaparece el bucle automático y el volteo se sustituye por un **fundido de color** de 700 ms (de acento a tinta, escalonado) sin ningún desplazamiento ni giro. Las transiciones de los controles se desactivan y el banco muestra el aviso «Tu sistema pide menos movimiento: las fichas no se vuelcan, solo cambian de color un instante.» (con `hidden` en el resto de casos).
- Lo esencial funciona sin JS: el texto de los titulares está en el HTML.
## Rendimiento
- Solo se anima `transform`, `opacity` y `color` mediante la Web Animations API; no hay bucles continuos salvo el de la portada, que se detiene fuera de pantalla o con la pestaña oculta.
- Las keyframes (44 por hoja) se cachean por combinación de muelle, color y opción de «dejar el color»; las letras de una misma palabra comparten la misma lista.
- No hay WebGL ni canvas: no aplican DPR ni fallback. El HTML pesa menos de 40 KB, sin imágenes ni scripts externos.
- Funciona dentro de un `iframe`: no usa `window.top`, `alert`, `localStorage` ni navegación fuera de la página.
## Personalización
Variables del personalizador y dónde actúan:
- `Taller Gálvez`: barra superior, pie y `<title>`.
- `Portovel`: barra superior, pie y `<title>`.
- `Cada letra,` y `una ficha.`: las dos líneas del titular. Mantén cada línea por debajo de unos 12 caracteres para que quepa en una línea en móvil; si no cabe, se parte por palabras.
- `Fichero`: palabra inicial del banco de pruebas (máximo 14 caracteres).
- `#ff2e17`: el color vivo de la copia que entra (hexadecimal). Funciona bien con rojo bermellón, azul eléctrico, verde ácido o naranja.
- `#0b0b0b` y `#ffffff`: negro y blanco por defecto; se pueden invertir para una versión oscura.
- `hola@tallergalvez.com` y `+00 000 000 000`: datos del pie.
Ajustes habituales: cambiar `k`, `c` y el escalonado en el objeto `AJ` (por defecto 210, 15 y 35); aumentar la inclinación inicial de la copia (62°) para un cajón más profundo; cambiar `perspective: 4.2em` para un giro más o menos dramático; sustituir los nombres del índice por los trabajos reales.
## Comprobación final
1. La página abre sin errores en consola y sin scroll horizontal a 360, 390, 768, 1440 y 1920 px.
2. Las letras del titular son negras sobre fondo blanco; el único color vivo aparece en la copia que sube y vuelve a negro al asentarse.
3. Al pasar el cursor por «Cada letra,» cada letra se vuelca hacia atrás sobre su borde superior y otra idéntica sube desde abajo, en ola desde la izquierda.
4. La copia entra por el borde inferior de la letra (no se ve fuera de su caja) y sobrepasa un poco su posición antes de asentarse.
5. Las filas «Cuatro Vientos» y «Pasacalle» arrancan desde el final; «Hotel Albaida» y «Farmacia Nueva», desde el centro.
6. Mientras una palabra se anima, volver a pasar el cursor no la reinicia; al asentarse se puede repetir sin saltos visuales.
7. Con el ratón quieto en la portada, a los 4 s las dos líneas se hojean solas y el origen va cambiando.
8. Las palabras de las filas y del banco se vuelcan al entrar por primera vez en pantalla y no son visibles antes.
9. En el banco, «Gelatina» alarga la curva y muestra un sobreimpulso claramente mayor; «Seco» casi no rebota.
10. Escribir una palabra distinta reconstruye las letras y las voltea; el contador de fichas aumenta.
11. Con «Dejar el color en la ficha nueva» activado, la palabra se queda en el color de acento.
12. Con `prefers-reduced-motion: reduce` no hay giros ni desplazamientos, solo un fundido de color, y aparece el aviso del banco.
13. Todos los controles se usan con teclado y se ve el foco; un lector de pantalla lee cada palabra completa una sola vez.
14. En un `iframe` sin permisos especiales todo funciona igual.Prueba en frío
Aún sin publicar para esta pieza
En la prueba en frío pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos lo que genera con la demo. Cuando esté hecha, aquí verás la captura, el veredicto y la fecha.
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 01-10-2026

Bloques de web · GSAP

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