Saltar al contenido
motionframe

Buscar piezas

Categorías

La pieza

Qué hace y dónde encaja

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.

Prompt
≈13.223 tokens · con Lite
Librerías
ninguna
Demo
29 KB · ligero
Prueba en frío
pendiente

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.

Ver 1 campos más
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.597 palabras · ≈13.223 tokens
Lite
1.471 palabras · ≈2.668 tokens
Librerías
ninguna
Peso de la demo
29 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: 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

La 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

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 muelle rebote menos y las letras se asienten antes»
    • «Cambia el color vivo a azul eléctrico»
    • «Haz que el escalonado arranque siempre desde el centro»
    • «Deja el color fijo en la ficha nueva y pon el fondo en negro»
  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

¿Por qué no usa una curva cubic-bezier para el rebote?
Porque una Bézier solo puede curvarse, no rebasar y asentarse con el carácter de un muelle. La pieza resuelve el oscilador amortiguado, muestrea 44 puntos y los mete en las keyframes de la Web Animations API: la física está en los fotogramas.
¿Puedo usarlo en un titular largo o en varias líneas?
Sí. El texto se parte por palabras y cada letra es independiente; con titulares de más de 12 caracteres por línea conviene bajar el tamaño o dejar que se parta. El escalonado cuenta todas las letras de la palabra, así que cuanto más larga, más tarda la ola.
¿Qué pasa en móvil, donde no hay hover?
Un toque voltea la palabra. Además cada palabra se voltea al entrar en pantalla y la portada se hojea sola cada pocos segundos, así que nunca depende solo del puntero.
¿Respeta el movimiento reducido?
Sí: con prefers-reduced-motion no hay giros ni desplazamientos, las palabras se muestran quietas desde el principio y el volteo se sustituye por un breve fundido de color. El texto real está en un span para lectores de pantalla.
¿Qué diferencia hay entre el prompt Completo y el Lite?
El Completo (≈13223 tokens) describe la pieza con todo detalle y es el que mejor resultado da en Claude, ChatGPT, Gemini o Cursor. El Lite (≈2668 tokens) resume lo esencial para herramientas que aceptan menos texto, como Lovable, Bolt o v0.

Versión 1.0 · publicada el 01-10-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.