Saltar al contenido
motionframe

Buscar piezas

Categorías

La pieza

Qué hace y dónde encaja

Un indicador de lectura con forma de regla de taller. Cincuenta y una marcas de 1 px, una larga cada cinco, brotan una tras otra con un muelle que rebota un poco; sobre ellas, una aguja de un único color vivo sigue el scroll con otro muelle y se estira cuando vas deprisa. Las marcas que ya has pasado se vuelven negras, las cercanas a la aguja y al puntero crecen en campana y una etiqueta indica el porcentaje y el capítulo. Se puede hacer clic para saltar, arrastrar para recorrer la página y usar el teclado. Va fija en la cabecera, en horizontal, o en un riel vertical al borde de la pantalla, con un selector que traslada la regla de uno a otro. Es vanilla, sin librerías, y su bucle se detiene en reposo. Encaja en artículos largos, informes, documentación, portafolios y landings editoriales. Admite otro color vivo, otro número de marcas y otra frecuencia de marcas largas.

Prompt
≈12.977 tokens · con Lite
Librerías
ninguna
Demo
32 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.

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.367 palabras · ≈12.977 tokens
Lite
1.222 palabras · ≈2.461 tokens
Librerías
ninguna
Peso de la demo
32 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: Regla de progreso con marca

Un indicador de progreso de scroll con forma de regla: 51 marcas finas (cada cierto número, más largas) que brotan con muelle y una marca de color vivo que se desliza sobre ellas según avanzas por la página, en horizontal dentro de la cabecera o en vertical en un riel lateral.

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

## Qué es

Una página editorial larga de un estudio ficticio, **Escuadra**, que sirve de escaparate de la regla de progreso. Estilo: blanco y negro puros con **un único color vivo** (`#ff3d00`), sin pasteles, sin cursivas ni letra manuscrita, sin degradados.

La regla es un componente reutilizable y autosuficiente (`#regla`) formado por:

- **Escala**: 51 marcas de 1 px repartidas a lo largo del eje. Una de cada 5 es larga (16 px; las demás, 9 px). Con 51 marcas y una larga cada 5 hay 11 largas, que llevan debajo (o al lado) un número de 0 a 100 de diez en diez.
- **Marca de color**: una aguja de 3 px del color vivo y 34 px de largo que sigue al scroll con un muelle. Se estira a lo largo del eje cuando va deprisa.
- **Cifra**: el porcentaje leído, tres dígitos en mono («042») y el signo %.
- **Etiqueta** flotante que aparece al pasar el puntero: «40 % · 03 · El margen».

Las marcas que la aguja ya ha pasado son negras; las que quedan por delante, gris tenue. Las marcas cercanas a la aguja crecen siguiendo una campana gaussiana, y lo mismo ocurre en torno al puntero.

Se puede colocar en **Cabecera** (horizontal) o **Lateral** (vertical) con un selector tipo píldora; al cambiar, la regla se traslada de sitio y las marcas vuelven a brotar. También se puede **hacer clic** en la regla para saltar a ese punto de la página, **arrastrar** para recorrerla y usar el **teclado** (es un `role="slider"`).

La página de demostración es un cuaderno corto: portada con titular gigante, cinco capítulos que alternan fondo blanco y negro (el segundo y el cuarto, negros), una cita a pantalla completa y un cierre con botón. Así se comprueba que la regla se lee bien sobre ambos fondos (la cabecera y el riel tienen siempre fondo blanco propio).

## Librerías y esqueleto

Ninguna librería. Solo Google Fonts con `display=swap` (las tres fuentes tienen ñ, tildes, ¿ y ¡):

```html
<!doctype html>
<html lang="es" data-eje="h">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Escuadra — Regla de progreso con marca</title>
<script>document.documentElement.classList.add('js')</script>
<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,500..900&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>/* ver «Código clave» */</style>
</head>
<body>
  <!-- ver «Estructura y contenido» -->
  <script>/* ver «Código clave» */</script>
</body>
</html>
```

Tipografía: **Archivo** variable con eje de anchura (`font-stretch` de 112 % a 125 %, peso 750 a 800) para titulares, numerales y marca; **Geist** (400/500/600) para texto y botones; **Geist Mono** (500) para etiquetas, números de la regla y cifra, en mayúsculas con `letter-spacing:.08em` salvo en la regla.

El `<script>` inline de la cabecera añade la clase `js` a `<html>`: el estado inicial oculto de las entradas solo aplica con JavaScript.

## Tokens de diseño

```css
:root{
  --papel:#ffffff;
  --tinta:#0a0a0a;
  --gris:#5c5c5c;
  --gris-noche:#a3a3a3;
  --linea:rgba(10,10,10,.16);
  --linea-noche:rgba(255,255,255,.2);
  --apagada:rgba(10,10,10,.22);
  --acento:#ff3d00;
  --f-display:"Archivo","Helvetica Neue",Arial,sans-serif;
  --f-texto:"Geist",system-ui,-apple-system,"Segoe UI",sans-serif;
  --f-mono:"Geist Mono",ui-monospace,"SFMono-Regular",Consolas,monospace;
  --ease:cubic-bezier(.16,1,.3,1);
  --muelle:cubic-bezier(.34,1.45,.5,1);
  --gutter:clamp(16px,4vw,56px);
  --alto-cab:72px;
  --alto-cab-max:72px;
}
```

Más: `html[data-eje=v]{--alto-cab:64px}`; en ≤ 820 px `--alto-cab-max:112px` y, con el eje horizontal, `--alto-cab:112px`. `html` y `body` con fondo `var(--papel)`. Texto base 18 px / 1,6. Contenedor `.cont`: `max-width:1280px`, `padding:0 var(--gutter)`. Los únicos grises son `--gris` (sobre blanco) y `--gris-noche` (sobre negro); ambos cumplen contraste AA. El color vivo no se usa para texto sobre blanco.

## Estructura y contenido

Este es el HTML del `<body>` con todos los textos, literales (castellano neutro, sin referencias locales). La regla (`#regla`) está en el DOM antes de `<main>` y el script la mueve a `#hueco-cab` (cabecera) o a `#hueco-lat` (riel); las marcas y los números los crea el script dentro de `#escala`, antes de `#mk`.

```html
<a class="saltar" href="#contenido">Saltar al contenido</a>

<header class="cab" id="cab">
  <a class="marca" href="#inicio" aria-label="Escuadra, Cuaderno 07"><span class="marca-l" aria-hidden="true"></span>Escuadra<small>Cuaderno 07</small></a>
  <div class="hueco-cab" id="hueco-cab"></div>
  <div class="sel" role="group" aria-label="Posición de la regla de progreso">
    <button type="button" data-eje="h" aria-pressed="true">Cabecera</button>
    <button type="button" data-eje="v" aria-pressed="false">Lateral</button>
  </div>
</header>
<aside class="riel" id="hueco-lat" aria-label="Progreso de lectura"></aside>

<div class="regla" id="regla" role="slider" tabindex="0" aria-label="Progreso de lectura de la página" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-valuetext="0 %" aria-orientation="horizontal" data-eje="h">
  <div class="escala" id="escala" aria-hidden="true">
    <b class="mk" id="mk"></b>
    <span class="tip" id="tip"></span>
  </div>
  <span class="cifra" id="cifra" aria-hidden="true"><b id="cifra-n" style="font-weight:500">000</b><i>%</i></span>
</div>

<main id="contenido">
  <section class="hero" id="inicio" data-cap="Portada">
    <div class="cont">
      <p class="mono" data-rev>Cuaderno 07 · Taller de medidas</p>
      <h1 aria-label="Medir antes de cortar.">
        <span class="w" aria-hidden="true"><span style="--i:0">Medir</span></span>
        <span class="w" aria-hidden="true"><span style="--i:1">antes de</span></span>
        <span class="w" aria-hidden="true"><span style="--i:2">cortar.</span></span>
      </h1>
    </div>
    <div class="cont">
      <div class="hero-pie">
        <p class="lede" data-rev style="--d:.5s">Un cuaderno corto sobre la costumbre más barata del diseño: tomar las medidas tres veces y decidir una. Cinco capítulos y una regla que avanza con tu lectura.</p>
        <dl class="ficha mono" data-rev style="--d:.65s">
          <div><dt>Lectura</dt><dd>6 min</dd></div>
          <div><dt>Edición</dt><dd>Octubre 2026</dd></div>
          <div><dt>Autoría</dt><dd>Escuadra Studio</dd></div>
        </dl>
      </div>
      <div class="cota mono" style="margin-top:clamp(36px,5vw,64px)" aria-hidden="true"><i></i><span>1 200 mm</span><i></i></div>
      <p class="pista mono" style="margin-top:22px">Baja: la regla mide lo que llevas leído</p>
    </div>
  </section>

  <section class="cap" id="c1" data-cap="01 · La unidad" aria-labelledby="t1">
    <div class="cont cap-grid">
      <div class="num" aria-hidden="true">01</div>
      <div>
        <p class="ant mono" data-rev>Capítulo 01 / 05</p>
        <h2 id="t1" data-rev style="--d:.08s">Elige una unidad y no la cambies</h2>
        <p data-rev style="--d:.14s">Casi todos los errores de medida nacen de mezclar unidades a mitad de obra. Un plano en milímetros cortado con una cinta en pulgadas no falla por poco: falla entero.</p>
        <p data-rev style="--d:.2s">Antes de dibujar nada, el estudio fija una sola unidad por proyecto, la escribe en la primera línea del documento y la repite en cada cota. Es un gesto de diez segundos que ahorra jornadas enteras.</p>
        <dl class="datos" data-rev>
          <div><dt class="mono gris">Unidad base</dt><dd>1 mm</dd></div>
          <div><dt class="mono gris">Cotas por plano</dt><dd>12 como máximo</dd></div>
          <div><dt class="mono gris">Revisión</dt><dd>2 personas</dd></div>
        </dl>
      </div>
    </div>
  </section>

  <section class="cap noche" id="c2" data-cap="02 · La tolerancia" aria-labelledby="t2">
    <div class="cont cap-grid">
      <div class="num" aria-hidden="true">02</div>
      <div>
        <p class="ant mono" data-rev>Capítulo 02 / 05</p>
        <h2 id="t2" data-rev style="--d:.08s">Decide cuánto error aceptas</h2>
        <p data-rev style="--d:.14s">Una medida sin tolerancia es una intención, no una especificación. Cada pieza lleva al lado cuánto puede desviarse y quién decide si se acepta.</p>
        <p data-rev style="--d:.2s">Tres franjas bastan: estricta para lo que encaja, normal para lo que se ve y holgada para lo que se oculta. Casi nunca hace falta una cuarta.</p>
        <dl class="datos" data-rev>
          <div><dt class="mono gris">Estricta</dt><dd>±0,1 mm</dd></div>
          <div><dt class="mono gris">Normal</dt><dd>±0,5 mm</dd></div>
          <div><dt class="mono gris">Holgada</dt><dd>±2 mm</dd></div>
        </dl>
      </div>
    </div>
  </section>

  <section class="q" id="cita" data-cap="Una idea" aria-label="Una idea">
    <div class="cont">
      <p aria-label="Lo que no se mide, se adivina.">
        <span class="w" aria-hidden="true"><span style="--i:0">Lo</span></span><span class="w" aria-hidden="true"><span style="--i:1">que</span></span><span class="w" aria-hidden="true"><span style="--i:2">no</span></span><span class="w" aria-hidden="true"><span style="--i:3">se</span></span><span class="w" aria-hidden="true"><span style="--i:4">mide,</span></span><span class="w sub" aria-hidden="true"><span style="--i:5">se</span></span><span class="w sub" aria-hidden="true"><span style="--i:6">adivina.</span></span>
      </p>
    </div>
  </section>

  <section class="cap" id="c3" data-cap="03 · El margen" aria-labelledby="t3">
    <div class="cont cap-grid">
      <div class="num" aria-hidden="true">03</div>
      <div>
        <p class="ant mono" data-rev>Capítulo 03 / 05</p>
        <h2 id="t3" data-rev style="--d:.08s">Deja aire alrededor</h2>
        <p data-rev style="--d:.14s">El margen no es lo que sobra: es lo primero que se reserva. Un diseño sin margen de seguridad obliga a que todo salga perfecto a la vez, y eso casi nunca ocurre.</p>
        <p data-rev style="--d:.2s">Reserva un diez por ciento de holgura en cada eje y no lo toques hasta la última revisión. Si al final no hace falta, se reparte; si hace falta, ya estaba.</p>
        <dl class="datos" data-rev>
          <div><dt class="mono gris">Holgura por eje</dt><dd>10 %</dd></div>
          <div><dt class="mono gris">Se libera</dt><dd>En la revisión final</dd></div>
          <div><dt class="mono gris">Margen del lienzo</dt><dd>24 mm</dd></div>
        </dl>
      </div>
    </div>
  </section>

  <section class="cap noche" id="c4" data-cap="04 · El ritmo" aria-labelledby="t4">
    <div class="cont cap-grid">
      <div class="num" aria-hidden="true">04</div>
      <div>
        <p class="ant mono" data-rev>Capítulo 04 / 05</p>
        <h2 id="t4" data-rev style="--d:.08s">Repite hasta que se note</h2>
        <p data-rev style="--d:.14s">Una regla funciona porque sus marcas se repiten: la vista aprende el intervalo y detecta enseguida cuándo algo se sale. Con el diseño pasa igual.</p>
        <p data-rev style="--d:.2s">Elige un compás, por ejemplo ocho unidades, y construye todo en múltiplos. Las excepciones se marcan a propósito, como las marcas largas que aparecen cada cinco.</p>
        <dl class="datos" data-rev>
          <div><dt class="mono gris">Compás base</dt><dd>8 unidades</dd></div>
          <div><dt class="mono gris">Marca larga</dt><dd>Cada 5</dd></div>
          <div><dt class="mono gris">Excepciones</dt><dd>1 por pantalla</dd></div>
        </dl>
      </div>
    </div>
  </section>

  <section class="cap" id="c5" data-cap="05 · La revisión" aria-labelledby="t5">
    <div class="cont cap-grid">
      <div class="num" aria-hidden="true">05</div>
      <div>
        <p class="ant mono" data-rev>Capítulo 05 / 05</p>
        <h2 id="t5" data-rev style="--d:.08s">Mide otra vez, con otros ojos</h2>
        <p data-rev style="--d:.14s">La última medida la toma otra persona, con otra herramienta y sin ver la tuya. Si coinciden, se corta. Si no, se vuelve al capítulo uno.</p>
        <p data-rev style="--d:.2s">Nadie lo vive como una pérdida de tiempo después de la primera vez que evita una plancha de material tirada a la basura.</p>
        <dl class="datos" data-rev>
          <div><dt class="mono gris">Segunda lectura</dt><dd>Otra persona</dd></div>
          <div><dt class="mono gris">Herramienta</dt><dd>Distinta</dd></div>
          <div><dt class="mono gris">Decisión</dt><dd>Se corta o se repite</dd></div>
        </dl>
      </div>
    </div>
  </section>

  <section class="cierre noche" id="cierre" data-cap="Cierre" aria-labelledby="tc">
    <div class="cont">
      <h2 id="tc" data-rev>Cuadra a la primera.</h2>
      <p data-rev style="--d:.1s">El cuaderno completo trae las plantillas de cotas, la tabla de tolerancias y las diez preguntas de revisión. Escríbenos y lo recibes en PDF.</p>
      <a class="btn" href="mailto:hola@escuadra.com" data-rev style="--d:.2s"><span>Pedir el cuaderno</span><span class="fl" aria-hidden="true"><svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square"><path d="M3 9h12M10 4l5 5-5 5"/></svg><svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square"><path d="M3 9h12M10 4l5 5-5 5"/></svg></span></a>
      <div class="pie mono"><span>© 2026 Escuadra Studio</span><span>hola@escuadra.com · +00 000 000 000</span></div>
    </div>
  </section>
</main>
```

Datos de composición:

- **Cabecera** fija, fondo blanco, filete inferior de 1 px, rejilla de tres columnas (`minmax(130px,1fr) minmax(0,740px) minmax(130px,1fr)`, hueco 28 px). Marca a la izquierda con una «L» de escuadra dibujada con bordes de 3 px; la regla horizontal en el centro; el selector a la derecha. En ≤ 820 px pasa a dos filas: fila 1 de 60 px con marca y selector; fila 2 con la regla a todo el ancho.
- **Riel lateral** (`.riel`): panel fijo blanco con filete, 84 px de ancho (46 px en ≤ 640 px), alto `min(68svh,580px)`, pegado al borde derecho y centrado en vertical bajo la cabecera, con las marcas colgando del borde derecho y los números a la izquierda. En modo horizontal está fuera de pantalla, oculto y transparente.
- **Portada**: `min-height:100svh`; etiqueta mono; `h1` de tres líneas («Medir / antes de / cortar.») a `clamp(44px,12.2vw,200px)`, interlineado .92; abajo, párrafo y ficha de tres filas, una cota con «1 200 mm» y la pista de scroll.
- **Capítulos 01–05**: `padding` vertical `clamp(88px,13vw,200px)`; dos columnas 5fr/7fr con el numeral gigante pegajoso (`position:sticky`) a la izquierda. Capítulos 02 y 04 sobre fondo negro (`.noche`).
- **Cita**: palabras del titular con máscara; «se adivina.» subrayado con una barra del color vivo de .1em.
- **Cierre** negro con titular gigante, párrafo, botón píldora blanco con flecha y pie con filete.

## Movimiento

| Elemento | Propiedad | De → a | Duración / muelle | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Marcas de la regla | escala en el eje corto | 0 → 1 (rebote ligero) | muelle k 170, c 13 | físico | 250 ms + 15 ms × índice (120 ms tras cambiar de eje) | carga y cambio de eje |
| Marca de color | traslación | posición anterior → progreso × (N−1) × paso | muelle k 220, c 21 | físico | — | scroll |
| Marca de color | estiramiento en el eje | 1 → hasta 4,2 | 1 + min(velocidad/45, 3,2) | directo | — | velocidad del muelle |
| Marcas bajo la aguja | escala extra | +0 → +0,5 | campana exp(−d²/9) | directo | — | posición de la aguja |
| Marcas bajo el puntero | escala extra | +0 → +0,5 | campana exp(−e²/5) × fuerza; fuerza con muelle k 240, c 24 | físico | — | puntero encima |
| Color de marca | background-color | gris tenue → negro | .3 s | ease | — | la aguja pasa |
| Números 0–100 | opacidad | 0 → 1 | .6 s | ease | .5 s | clase `listo` |
| Etiqueta | opacidad | 0 → 1 | .2 s | ease | — | puntero |
| Titular de la portada | translateY por línea | 108 % → 0 | 1,2 s | `--ease` | .15 s + .11 s × línea | clase `listo` |
| Cota «1 200 mm» | scaleX de cada raya | 0 → 1 desde el texto hacia fuera | 1,5 s | `--ease` | .7 s | clase `listo` |
| Textos `[data-rev]` | opacidad y translateY | 0 y 26 px → 1 y 0 | .9 s y 1,1 s | ease / `--ease` | `--d` | entra en pantalla (18 %) |
| Numeral del capítulo | relleno (background-size) | 0 % → 100 %, de abajo arriba | 1,3 s | `--ease` | — | el capítulo entra en pantalla |
| Filetes de datos | scaleX | 0 → 1 | 1,3 s | `--ease` | 0 / .12 / .24 s | entra en pantalla |
| Palabras de la cita | translateY | 112 % → 0 | 1,15 s | `--ease` | .08 s × palabra | entra en pantalla |
| Subrayado de la cita | background-size | 0 → 100 % | 1,2 s | `--ease` | 1 s | entra en pantalla |
| Pastilla del selector | translateX | 0 → 100 % | .7 s | `--muelle` | — | cambio de eje |
| Riel lateral | translate y opacidad | fuera → dentro | .8 s / .4 s | `--ease` | — | cambio de eje |
| Botón del cierre | relleno del color vivo | translateY(101 %) → 0; flecha 200 % → 0 | .6 s / .55 s | `--ease` | — | hover o foco |
| Pista de scroll | scaleY de la rayita | 0 → 1 y se desvanece | 2,4 s en bucle | `--ease` | — | siempre |

## Código clave

El CSS completo, literal:

```css
:root{
  --papel:#ffffff;
  --tinta:#0a0a0a;
  --gris:#5c5c5c;
  --gris-noche:#a3a3a3;
  --linea:rgba(10,10,10,.16);
  --linea-noche:rgba(255,255,255,.2);
  --apagada:rgba(10,10,10,.22);
  --acento:#ff3d00;
  --f-display:"Archivo","Helvetica Neue",Arial,sans-serif;
  --f-texto:"Geist",system-ui,-apple-system,"Segoe UI",sans-serif;
  --f-mono:"Geist Mono",ui-monospace,"SFMono-Regular",Consolas,monospace;
  --ease:cubic-bezier(.16,1,.3,1);
  --muelle:cubic-bezier(.34,1.45,.5,1);
  --gutter:clamp(16px,4vw,56px);
  --alto-cab:72px;
  --alto-cab-max:72px;
}
html[data-eje=v]{--alto-cab:64px}
@media (max-width:820px){
  :root{--alto-cab-max:112px}
  html[data-eje=h]{--alto-cab:112px}
}
*,*::before,*::after{box-sizing:border-box}
html{background:var(--papel);-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}
body{margin:0;background:var(--papel);color:var(--tinta);font:400 18px/1.6 var(--f-texto);-webkit-font-smoothing:antialiased}
h1,h2,p,dl,dd,figure{margin:0}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
:focus-visible{outline:2px solid var(--acento);outline-offset:3px}
.mono{font:500 12px/1.2 var(--f-mono);letter-spacing:.08em;text-transform:uppercase}
.saltar{position:fixed;left:16px;top:-60px;z-index:100;background:var(--tinta);color:#fff;padding:10px 14px;border-radius:6px;font:500 13px/1 var(--f-mono);transition:top .25s var(--ease)}
.saltar:focus{top:12px}
.cont{max-width:1280px;margin:0 auto;padding:0 var(--gutter)}
main{overflow-x:clip}

/* ——— Regla de progreso: el núcleo ——— */
.regla{--mj:16px;--mn:9px;--marca:34px;display:flex;align-items:center;gap:18px;width:100%;color:var(--tinta);border-radius:6px;-webkit-user-select:none;user-select:none}
.regla:focus-visible{outline-offset:8px}
.escala{position:relative;flex:1;min-width:0;height:46px;cursor:pointer;touch-action:none}
.m{position:absolute;top:0;width:1px;height:var(--mn);margin-left:-.5px;background:var(--apagada);transform-origin:50% 0;transform:scaleY(0);transition:background-color .3s ease;will-change:transform}
.m.M{height:var(--mj)}
.m.on{background:var(--tinta)}
.n{position:absolute;top:37px;transform:translateX(-50%);font:500 9px/1 var(--f-mono);letter-spacing:.04em;color:var(--gris);font-variant-numeric:tabular-nums;opacity:0;transition:opacity .6s ease .5s}
.listo .n{opacity:1}
.mk{position:absolute;left:0;top:-4px;width:3px;height:var(--marca);margin-left:-1.5px;border-radius:2px;background:var(--acento);will-change:transform;pointer-events:none}
.cifra{flex:none;min-width:3.6ch;text-align:right;font:500 13px/1 var(--f-mono);letter-spacing:.02em;font-variant-numeric:tabular-nums}
.cifra i{font-style:normal;color:var(--gris);margin-left:2px}
.tip{position:absolute;left:0;top:100%;margin-top:14px;z-index:3;pointer-events:none;white-space:nowrap;background:var(--papel);color:var(--tinta);border:1px solid var(--tinta);border-radius:6px;padding:8px 10px;font:500 11px/1 var(--f-mono);letter-spacing:.06em;text-transform:uppercase;opacity:0;transition:opacity .2s ease}
.tip.ver{opacity:1}
/* Vertical: las marcas cuelgan del borde derecho y se miden de arriba abajo */
.regla[data-eje=v]{flex-direction:column;align-items:stretch;gap:12px;height:100%}
.regla[data-eje=v] .escala{height:auto;width:100%}
.regla[data-eje=v] .m{right:0;width:var(--mn);height:1px;margin:-.5px 0 0;transform-origin:100% 50%;transform:scaleX(0)}
.regla[data-eje=v] .m.M{width:var(--mj);height:1px}
.regla[data-eje=v] .n{left:0;top:auto;transform:translateY(-50%)}
.regla[data-eje=v] .mk{left:-4px;right:auto;top:0;width:var(--marca);height:3px;margin:-1.5px 0 0}
.regla[data-eje=v] .mk{left:auto;right:-4px}
.regla[data-eje=v] .cifra{text-align:center;min-width:0}
.regla[data-eje=v] .tip{left:auto;top:0;right:100%;margin:0 16px 0 0}

/* Dónde vive la regla: cabecera (horizontal) o riel lateral (vertical) */
.cab{position:fixed;z-index:50;inset:0 0 auto 0;background:var(--papel);border-bottom:1px solid var(--linea);display:grid;grid-template-columns:minmax(130px,1fr) minmax(0,740px) minmax(130px,1fr);align-items:center;column-gap:28px;padding:0 var(--gutter);min-height:var(--alto-cab)}
.hueco-cab{min-width:0;padding-bottom:2px}
html[data-eje=v] .hueco-cab{display:none}
.riel{position:fixed;z-index:49;right:clamp(8px,1.6vw,24px);top:calc(var(--alto-cab) + (100svh - var(--alto-cab)) / 2);width:84px;height:min(68svh,580px);padding:18px 12px 16px;background:var(--papel);border:1px solid var(--linea);border-radius:12px;transform:translate(0,-50%);transition:transform .8s var(--ease),opacity .4s ease,top .6s var(--ease)}
html[data-eje=h] .riel{transform:translate(calc(100% + 40px),-50%);opacity:0;visibility:hidden;transition:transform .6s var(--ease),opacity .3s ease,visibility 0s .6s}
.marca{display:inline-flex;align-items:center;gap:12px;font:800 20px/1 var(--f-display);font-stretch:125%;letter-spacing:-.02em}
.marca-l{flex:none;width:16px;height:16px;border-left:3px solid var(--tinta);border-bottom:3px solid var(--tinta);transition:transform .6s var(--muelle)}
.marca:hover .marca-l{transform:rotate(-12deg) translate(-1px,1px)}
.marca small{white-space:nowrap;font:500 11px/1 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--gris);padding-left:12px;border-left:1px solid var(--linea)}
.sel{position:relative;grid-column:3;justify-self:end;display:inline-grid;grid-template-columns:1fr 1fr;border:1px solid var(--linea);border-radius:999px;padding:3px;isolation:isolate}
.sel::before{content:"";position:absolute;z-index:-1;top:3px;bottom:3px;left:3px;width:calc(50% - 3px);border-radius:999px;background:var(--tinta);transition:transform .7s var(--muelle)}
html[data-eje=v] .sel::before{transform:translateX(100%)}
.sel button{padding:9px 15px;border-radius:999px;font:500 11px/1 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--tinta);transition:color .3s ease}
.sel button[aria-pressed=true]{color:#fff}
.sel button:focus-visible{outline-offset:2px}

/* ——— Portada ——— */
.hero{min-height:100svh;padding-top:calc(var(--alto-cab-max) + clamp(28px,6vw,88px));padding-bottom:clamp(28px,4vw,56px);display:flex;flex-direction:column;justify-content:space-between;gap:clamp(36px,6vw,72px)}
.hero .cont{width:100%}
.hero h1{margin:clamp(20px,3vw,40px) 0 0;font:800 clamp(44px,12.2vw,200px)/.92 var(--f-display);font-stretch:118%;letter-spacing:-.045em}
.hero h1 .w{display:block;overflow:hidden;padding:.02em 0 .1em;margin-bottom:-.1em}
.hero h1 .w>span{display:block}
.js .hero h1 .w>span{transform:translateY(108%)}
.js.listo .hero h1 .w>span{transform:none;transition:transform 1.2s var(--ease) calc(var(--i) * .11s + .15s)}
.hero-pie{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,4fr);gap:clamp(24px,5vw,80px);align-items:end}
.lede{font-size:clamp(18px,1.7vw,22px);line-height:1.5;max-width:40ch}
.ficha{display:grid;gap:0}
.ficha div{display:flex;justify-content:space-between;gap:16px;padding:12px 0;border-top:1px solid var(--linea)}
.ficha dd{color:var(--gris)}
.cota{display:flex;align-items:center;gap:18px}
.cota i{flex:1;height:14px;position:relative}
.cota i::before{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:var(--tinta)}
.cota i::after{content:"";position:absolute;top:0;bottom:0;width:1px;background:var(--tinta)}
.cota i:first-child::after{left:0}
.cota i:last-child::after{right:0}
.js .cota i{transform:scaleX(0)}
.js .cota i:first-child{transform-origin:100% 50%}
.js .cota i:last-child{transform-origin:0 50%}
.js.listo .cota i{transform:none;transition:transform 1.5s var(--ease) .7s}
.pista{display:flex;align-items:center;gap:12px;color:var(--gris)}
.pista::before{content:"";width:1px;height:20px;background:var(--acento);transform-origin:50% 0;animation:baja 2.4s var(--ease) infinite}
@keyframes baja{0%{transform:scaleY(0)}45%{transform:scaleY(1)}100%{transform:scaleY(1);opacity:0}}

/* ——— Capítulos ——— */
.cap{--c:var(--tinta);padding:clamp(88px,13vw,200px) 0;border-top:1px solid var(--linea)}
.noche{--c:#fff;background:var(--tinta);color:#fff;border-top-color:var(--tinta)}
.cap-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(24px,5vw,96px);align-items:start}
.num{position:sticky;top:calc(var(--alto-cab-max) + 40px);font:800 clamp(110px,19vw,300px)/.78 var(--f-display);font-stretch:125%;letter-spacing:-.05em;color:transparent;-webkit-text-stroke:1.5px var(--c);background:linear-gradient(var(--c),var(--c)) no-repeat 0 100%/100% 100%;-webkit-background-clip:text;background-clip:text;transition:background-size 1.3s var(--ease)}
.js .num{background-size:100% 0%}
.js .cap.vis .num{background-size:100% 100%}
.ant{color:var(--gris);margin-bottom:22px}
.noche .ant,.noche .gris{color:var(--gris-noche)}
.cap h2{font:750 clamp(32px,4.6vw,66px)/1.02 var(--f-display);font-stretch:112%;letter-spacing:-.035em;margin-bottom:clamp(24px,3vw,40px);max-width:15ch}
.cap p{max-width:56ch;margin-bottom:1.2em}
.datos{margin-top:clamp(36px,5vw,64px)}
.datos div{position:relative;display:flex;justify-content:space-between;align-items:baseline;gap:20px;padding:18px 0}
.datos div::before{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:var(--c);opacity:.28;transform-origin:0 50%;transition:transform 1.3s var(--ease) var(--d,0s)}
.js .datos div::before{transform:scaleX(0)}
.js .datos.vis div::before{transform:none}
.datos div:last-child{padding-bottom:0}
.datos dd{font:700 clamp(20px,2.2vw,28px)/1.1 var(--f-display);font-stretch:112%;letter-spacing:-.02em;text-align:right}
.datos div:nth-child(2){--d:.12s}
.datos div:nth-child(3){--d:.24s}

.q{padding:clamp(88px,13vw,200px) 0;border-top:1px solid var(--linea)}
.q p{font:800 clamp(38px,8.4vw,136px)/.98 var(--f-display);font-stretch:112%;letter-spacing:-.045em;max-width:13ch}
.q .w{display:inline-block;overflow:hidden;vertical-align:top;padding:0 .04em .16em;margin-right:.14em;margin-bottom:-.16em}
.q .w>span{display:inline-block}
.q .sub{background:linear-gradient(var(--acento),var(--acento)) no-repeat 0 100%/100% .1em}
.js .q .w>span{transform:translateY(112%)}
.js .q.vis .w>span{transform:none;transition:transform 1.15s var(--ease) calc(var(--i) * .08s)}
.js .q .sub{background-size:0% .1em;transition:background-size 1.2s var(--ease) 1s}
.js .q.vis .sub{background-size:100% .1em}

.cierre{padding:clamp(96px,14vw,220px) 0 clamp(40px,5vw,72px)}
.cierre h2{font:800 clamp(48px,11.4vw,188px)/.92 var(--f-display);font-stretch:118%;letter-spacing:-.045em;max-width:9ch}
.cierre p{max-width:46ch;margin:clamp(28px,4vw,48px) 0 clamp(28px,4vw,44px);color:var(--gris-noche)}
.btn{position:relative;isolation:isolate;overflow:hidden;display:inline-flex;align-items:center;gap:14px;height:60px;padding:0 10px 0 28px;border-radius:30px;background:#fff;color:var(--tinta);font:600 16px/1 var(--f-texto);transition:transform .6s var(--muelle)}
.btn::before{content:"";position:absolute;z-index:-1;inset:0;background:var(--acento);transform:translateY(101%);transition:transform .6s var(--ease)}
.btn:hover::before,.btn:focus-visible::before{transform:none}
.btn:active{transform:scale(.97)}
.btn .fl{width:40px;height:40px;border-radius:50%;background:var(--tinta);color:#fff;display:grid;place-items:center;overflow:hidden}
.btn .fl svg{grid-area:1/1;width:18px;height:18px;transition:transform .55s var(--ease)}
.btn .fl svg+svg{transform:translateX(-200%)}
.btn:hover .fl svg:first-child,.btn:focus-visible .fl svg:first-child{transform:translateX(200%)}
.btn:hover .fl svg+svg,.btn:focus-visible .fl svg+svg{transform:none}
.pie{display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px 32px;margin-top:clamp(72px,11vw,160px);padding-top:22px;border-top:1px solid var(--linea-noche);color:var(--gris-noche)}

/* Entradas por scroll */
.js [data-rev]{opacity:0;transform:translateY(26px);transition:opacity .9s ease var(--d,0s),transform 1.1s var(--ease) var(--d,0s)}
.js [data-rev].vis{opacity:1;transform:none}

/* ——— Móvil y tablet ——— */
@media (max-width:1400px){html[data-eje=v] .cont{padding-right:calc(var(--gutter) + 96px)}}
@media (max-width:1380px){.marca small{display:none}}
@media (max-width:820px){
  .cab{grid-template-columns:1fr auto;grid-template-rows:60px auto;column-gap:12px;row-gap:0;align-content:start}
  .marca small{display:none}
  .sel{grid-column:2}
  .hueco-cab{grid-column:1/-1;grid-row:2;padding:0 0 10px}
  html[data-eje=v] .cab{grid-template-rows:60px}
  .hero-pie,.cap-grid{grid-template-columns:minmax(0,1fr)}
  .num{position:static;font-size:clamp(96px,30vw,180px);margin-bottom:8px}
  .cap h2{max-width:none}
  .pie{flex-direction:column}
}
@media (max-width:640px){
  html[data-eje=v] .cont{padding-right:calc(var(--gutter) + 50px)}
  .riel{width:46px;padding:14px 7px 12px;right:6px}
  .regla[data-eje=v]{--mj:13px;--mn:7px;--marca:24px}
  .regla[data-eje=v] .n{display:none}
  .regla[data-eje=v] .cifra{font-size:11px}
  .regla[data-eje=v] .cifra i{display:none}
  .sel button{padding:9px 12px}
  .btn{height:56px}
}
@media (prefers-reduced-motion:reduce){
  .js [data-rev],.js .hero h1 .w>span,.js .q .w>span,.js .cota i{transform:none!important;opacity:1!important;transition:none!important}
  .js .num{background-size:100% 100%!important;transition:none}
  .js .datos div::before{transform:none;transition:none}
  .js .q .sub{background-size:100% .1em;transition:none}
  .pista::before{animation:none}
  .n{opacity:1;transition:none}
  .riel,.sel::before,.btn,.btn::before,.marca-l{transition:none}
}
```

El JavaScript completo, literal. Es vanilla: dos muelles integrados a mano con subpasos de 1/120 s, un solo bucle `requestAnimationFrame` que se detiene cuando todo está en reposo y se reanima con scroll, puntero o redimensionado:

```js
(() => {'use strict';
  const N = 51, CADA = 5;                       // marcas y separación de las largas
  const K_MARCADOR = 220, C_MARCADOR = 21;      // muelle de la marca de color
  const K_BROTE = 170, C_BROTE = 13;            // muelle con el que brota cada marca (algo de rebote)
  const ESCALONADO = 15;                        // ms entre una marca y la siguiente al brotar
  const root = document.documentElement;
  const $ = (s) => document.querySelector(s);
  const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');

  const regla = $('#regla'), escala = $('#escala'), mk = $('#mk'), tip = $('#tip'), cifraN = $('#cifra-n');
  const hueco = { h: $('#hueco-cab'), v: $('#hueco-lat') };
  const botones = [...document.querySelectorAll('.sel button')];

  /* Marcas y números */
  const marcas = [], numeros = [], enc = new Array(N).fill(false);
  const frag = document.createDocumentFragment();
  for (let i = 0; i < N; i++) {
    const m = document.createElement('i');
    m.className = 'm' + (i % CADA === 0 ? ' M' : '');
    frag.appendChild(m); marcas.push(m);
    if (i % CADA === 0) {
      const n = document.createElement('span');
      n.className = 'n'; n.textContent = Math.round(i / (N - 1) * 100);
      frag.appendChild(n); numeros.push([i, n]);
    }
  }
  escala.insertBefore(frag, mk);

  /* Estado */
  let H = true, len = 1, pitch = 1, raf = 0, last = 0;
  let x = 0, v = 0, tx = 0;                      // marcador, en unidades de marca
  let hs = 0, hv = 0, ht = 0, hx = 0, hxT = 0;   // fuerza y posición del puntero
  const b = new Float32Array(N), bv = new Float32Array(N), t0 = new Float64Array(N);
  let arrastre = null;

  const prog = () => { const m = root.scrollHeight - innerHeight; return m > 1 ? clamp(scrollY / m, 0, 1) : 0; };

  /* Capítulos (para el texto de la etiqueta y de accesibilidad) */
  const caps = [...document.querySelectorAll('[data-cap]')];
  let inicios = [];
  const medirCaps = () => {
    const m = Math.max(1, root.scrollHeight - innerHeight);
    inicios = caps.map((c) => clamp((c.getBoundingClientRect().top + scrollY) / m, 0, 1));
  };
  const capEn = (p) => { let k = 0; for (let i = 0; i < inicios.length; i++) if (inicios[i] <= p + 0.004) k = i; return caps[k] ? caps[k].dataset.cap : ''; };

  /* Colocación de las marcas (en px enteros: líneas de 1 px nítidas) */
  function colocar() {
    const r = escala.getBoundingClientRect();
    len = H ? r.width : r.height;
    pitch = len / (N - 1);
    marcas.forEach((m, i) => {
      const p = Math.round(i * pitch) + 'px';
      m.style.left = m.style.top = '';
      m.style[H ? 'left' : 'top'] = p;
    });
    numeros.forEach(([i, n]) => {
      const p = Math.round(i * pitch) + 'px';
      n.style.left = n.style.top = '';
      n.style[H ? 'left' : 'top'] = p;
    });
  }

  /* Brote con muelle, marca a marca */
  function brotar(retardo = 0) {
    const n = performance.now();
    for (let i = 0; i < N; i++) { b[i] = reduce.matches ? 1 : 0; bv[i] = 0; t0[i] = n + retardo + i * ESCALONADO; }
    kick();
  }

  function kick() { if (!raf) { last = performance.now(); raf = requestAnimationFrame(frame); } }

  function frame(now) {
    raf = 0;
    const dt = Math.min(0.05, Math.max(0, (now - last) / 1000)); last = now;
    tx = prog() * (N - 1);
    let activo = false;
    if (reduce.matches) {
      x = tx; v = 0; hs = 0; b.fill(1);
    } else {
      const n = Math.max(1, Math.ceil(dt * 120)), h = dt / n;
      for (let s = 0; s < n; s++) {
        v += (K_MARCADOR * (tx - x) - C_MARCADOR * v) * h; x += v * h;
        hv += (240 * (ht - hs) - 24 * hv) * h; hs += hv * h;
        for (let i = 0; i < N; i++) if (now >= t0[i]) { bv[i] += (K_BROTE * (1 - b[i]) - C_BROTE * bv[i]) * h; b[i] += bv[i] * h; }
      }
      hx += (hxT - hx) * (1 - Math.exp(-dt * 20));
      activo = Math.abs(tx - x) > 0.003 || Math.abs(v) > 0.02 || Math.abs(ht - hs) > 0.003 || Math.abs(hv) > 0.02 || Math.abs(hxT - hx) > 0.01;
      if (!activo) for (let i = 0; i < N; i++) if (now < t0[i] || Math.abs(1 - b[i]) > 0.002 || Math.abs(bv[i]) > 0.02) { activo = true; break; }
    }
    pintar();
    if (activo) raf = requestAnimationFrame(frame);
    else { x = tx; v = 0; hs = ht; hv = 0; b.fill(1); bv.fill(0); pintar(); }
  }

  function pintar() {
    const q = reduce.matches;
    const est = q ? 1 : 1 + Math.min(Math.abs(v) / 45, 3.2);        // la marca se estira con la velocidad
    for (let i = 0; i < N; i++) {
      const d = i - x, e = i - hx;
      const s = Math.max(0, b[i]) * (1 + 0.5 * Math.exp(-d * d / 9) + (q ? 0 : hs * 0.5 * Math.exp(-e * e / 5)));
      marcas[i].style.transform = (H ? 'scaleY(' : 'scaleX(') + s.toFixed(3) + ')';
      const on = i < x + 0.5;
      if (on !== enc[i]) { enc[i] = on; marcas[i].classList.toggle('on', on); }
    }
    const p = (x * pitch).toFixed(2);
    mk.style.transform = H ? `translateX(${p}px) scaleX(${est.toFixed(2)})` : `translateY(${p}px) scaleY(${est.toFixed(2)})`;
  }

  /* Lectura y accesibilidad */
  let ultimo = -1;
  function alScroll() {
    const p = prog(), pct = Math.round(p * 100);
    if (pct !== ultimo) {
      ultimo = pct;
      cifraN.textContent = String(pct).padStart(3, '0');
      regla.setAttribute('aria-valuenow', pct);
      regla.setAttribute('aria-valuetext', pct + ' % · ' + capEn(p));
    }
    kick();
  }

  /* Puntero: etiqueta, relieve y arrastre */
  const indiceEn = (e) => {
    const r = escala.getBoundingClientRect();
    return Math.round(clamp(((H ? e.clientX - r.left : e.clientY - r.top) / len), 0, 1) * (N - 1));
  };
  function mostrarTip(i) {
    const p = i / (N - 1);
    tip.textContent = Math.round(p * 100) + ' % · ' + capEn(p);
    let pos = Math.round(i * pitch);
    if (H) { const w = tip.offsetWidth / 2; pos = clamp(pos, w, len - w); }
    tip.style.transform = H ? `translateX(${pos}px) translateX(-50%)` : `translateY(${pos}px) translateY(-50%)`;
    tip.classList.add('ver');
  }
  const ocultarTip = () => tip.classList.remove('ver');
  const ir = (p, modo) => scrollTo({ top: p * (root.scrollHeight - innerHeight), behavior: modo });

  escala.addEventListener('pointermove', (e) => {
    if (e.pointerType === 'touch' && !arrastre) return;
    const i = indiceEn(e);
    hxT = i; ht = 1; mostrarTip(i);
    if (arrastre) {
      if (!arrastre.mov && Math.hypot(e.clientX - arrastre.x, e.clientY - arrastre.y) > 3) arrastre.mov = true;
      if (arrastre.mov) ir(i / (N - 1), 'instant');
    }
    kick();
  });
  escala.addEventListener('pointerleave', () => { if (!arrastre) { ht = 0; ocultarTip(); kick(); } });
  escala.addEventListener('pointerdown', (e) => {
    if (e.button !== 0) return;
    arrastre = { x: e.clientX, y: e.clientY, mov: false };
    escala.setPointerCapture(e.pointerId);
    const i = indiceEn(e); hxT = hx = i; ht = 1; mostrarTip(i);
    ir(i / (N - 1), reduce.matches ? 'instant' : 'smooth');
    kick();
  });
  const soltar = (e) => {
    if (!arrastre) return;
    arrastre = null;
    if (e.pointerType === 'touch') { ht = 0; ocultarTip(); }
    kick();
  };
  escala.addEventListener('pointerup', soltar);
  escala.addEventListener('pointercancel', soltar);

  regla.addEventListener('keydown', (e) => {
    const k = e.key; let p = prog();
    if (k === 'ArrowRight' || k === 'ArrowDown') p += 1 / (N - 1);
    else if (k === 'ArrowLeft' || k === 'ArrowUp') p -= 1 / (N - 1);
    else if (k === 'PageDown') p += 0.1;
    else if (k === 'PageUp') p -= 0.1;
    else if (k === 'Home') p = 0;
    else if (k === 'End') p = 1;
    else return;
    e.preventDefault(); ir(clamp(p, 0, 1), 'instant');
  });

  /* Cabecera o lateral */
  function ponerEje(eje, primera) {
    H = eje === 'h';
    root.dataset.eje = eje; regla.dataset.eje = eje;
    regla.setAttribute('aria-orientation', H ? 'horizontal' : 'vertical');
    hueco[eje].appendChild(regla);
    botones.forEach((bt) => bt.setAttribute('aria-pressed', String(bt.dataset.eje === eje)));
    ht = 0; hs = 0; hv = 0; ocultarTip();
    colocar(); medirCaps();
    brotar(primera ? 250 : 120);
    alScroll();
  }
  botones.forEach((bt) => bt.addEventListener('click', () => { if (bt.dataset.eje !== regla.dataset.eje) ponerEje(bt.dataset.eje); }));

  new ResizeObserver(() => { colocar(); medirCaps(); kick(); }).observe(escala);
  new ResizeObserver(() => { medirCaps(); alScroll(); }).observe(document.body);
  addEventListener('scroll', alScroll, { passive: true });
  reduce.addEventListener('change', () => { brotar(0); kick(); });

  /* Entradas por scroll */
  const io = new IntersectionObserver((es) => es.forEach((en) => { if (en.isIntersecting) { en.target.classList.add('vis'); io.unobserve(en.target); } }), { threshold: 0.18, rootMargin: '0px 0px -6% 0px' });
  document.querySelectorAll('[data-rev],.cap,.q').forEach((el) => io.observe(el));

  ponerEje('h', true);
  const listo = () => requestAnimationFrame(() => { root.classList.add('listo'); medirCaps(); });
  (document.fonts && document.fonts.ready ? Promise.race([document.fonts.ready, new Promise((r) => setTimeout(r, 1200))]) : Promise.resolve()).then(listo);
})();
```

Claves para entenderlo:

- **Progreso** = `scrollY / (scrollHeight − innerHeight)`, en 0–1. La aguja persigue `progreso × (N − 1)` (unidades de marca) con un muelle (k 220, c 21): muy amortiguado, con un rebote casi imperceptible.
- **Marcas** posicionadas en px enteros (`Math.round(i × paso)`) para que las líneas de 1 px salgan nítidas. Se escalan con `scaleY` (horizontal) o `scaleX` (vertical) desde su borde fijo; una marca está encendida (`on`) si `i < x + 0,5`.
- **Brote**: cada marca tiene su propio muelle (k 170, c 13, algo subamortiguado, de ahí el rebote) que arranca con 15 ms de escalonado respecto a la anterior.
- **Cambio de eje**: `ponerEje()` mueve el mismo nodo `#regla` de un hueco a otro, cambia `data-eje` en `<html>` y en la regla, recoloca las marcas y vuelve a hacerlas brotar.

## Interacción

- **Scroll**: la aguja y la cifra siguen el progreso; la cifra y `aria-valuenow` se actualizan al cambiar el porcentaje entero.
- **Puntero (ratón)**: al pasar por la escala, las marcas cercanas al cursor crecen y aparece la etiqueta «NN % · capítulo» pegada a la marca más cercana (acotada para no salirse de la pantalla en horizontal).
- **Clic**: salta a ese punto con scroll suave (`behavior:'smooth'`; `'instant'` con movimiento reducido).
- **Arrastre** (ratón o dedo): tras mover 3 px recorre la página con `behavior:'instant'`; `touch-action:none` solo en la escala.
- **Teclado**: la regla (`tabindex="0"`, `role="slider"`) responde a ←/→/↑/↓ (una marca = 2 %), RePág/AvPág (10 %), Inicio y Fin.
- **Selector Cabecera / Lateral**: dos botones con `aria-pressed`; la pastilla negra se desliza con un muelle CSS.
- Las etiquetas de capítulo salen de los atributos `data-cap` de las secciones, medidos al cargar y al redimensionar.

## Adaptación a móvil y tablet

- **≤ 1400 px** con eje vertical: `.cont` gana `padding-right: gutter + 96px` para que el riel no tape texto.
- **≤ 1380 px**: se oculta el subtítulo «Cuaderno» junto a la marca.
- **≤ 820 px**: la cabecera pasa a dos filas (marca y selector arriba, regla debajo); `--alto-cab` = 112 px en horizontal; una sola columna; el numeral deja de ser pegajoso (`clamp(96px,30vw,180px)`).
- **≤ 640 px**: riel de 46 px con marcas de 7 y 13 px, sin números y cifra de 11 px sin «%»; `.cont` gana 50 px a la derecha con eje vertical; botones algo más bajos (56 px).
- Sin scroll horizontal de 360 a 1920 px. En táctil no hay hover: quedan el clic, el arrastre y el scroll.

## Accesibilidad y movimiento reducido

- La regla es un único `role="slider"` con `aria-label`, `aria-valuemin/max/now`, `aria-valuetext` («42 % · 03 · El margen») y `aria-orientation`; las marcas, los números y la cifra son `aria-hidden`.
- Foco visible con contorno del color vivo de 2 px; enlace «Saltar al contenido».
- Los titulares animados llevan `aria-label` con el texto completo y las palabras sueltas `aria-hidden`.
- `prefers-reduced-motion: reduce`: las marcas aparecen ya crecidas, la aguja se coloca sin muelle ni estiramiento, sin relieve por puntero, sin scroll suave, y todas las entradas quedan en su estado final (sin transformaciones ni opacidad parcial).
- Contraste: texto sobre blanco en `#0a0a0a` o `#5c5c5c`; sobre negro en `#fff` o `#a3a3a3`.

## Rendimiento

- El bucle `requestAnimationFrame` solo corre mientras algún muelle se mueve; en reposo no hay trabajo. Las pestañas ocultas lo pausan solas.
- Solo se animan `transform` y `opacity` (más el color de las marcas con una transición corta). `will-change:transform` en marcas y aguja.
- Sin imágenes, sin librerías, sin vídeo. HTML por debajo de 40 KB. No hay canvas ni WebGL, así que no hay fallback que resolver.
- Funciona dentro de un `iframe`: no usa `window.top`, almacenamiento ni navegación externa.

## Personalización

- `#ff3d00`: el único color vivo (aguja, subrayado de la cita, relleno del botón, foco).
- `51` y `5`: cuántas marcas hay y cada cuántas es larga; los números de la regla se recalculan solos como porcentaje.
- `Escuadra`, `07` y `hola@escuadra.com`: marca, número de edición y contacto.
- Para usarla en tu web: copia `#regla`, su CSS (bloque «Regla de progreso») y el JS; elige el hueco donde vive y ajusta `--mj`, `--mn` y `--marca` (largo de las marcas y de la aguja).
- Para dejarla solo horizontal o solo lateral, elimina el selector y llama a `ponerEje('h')` o `ponerEje('v')`.

## Comprobación final

1. Al cargar, las marcas brotan de izquierda a derecha con un ligero rebote y después aparecen los números 0–100.
2. La aguja de color vivo arranca en 0 % y la cifra marca «000».
3. Al hacer scroll, la aguja se desliza con muelle, las marcas que deja atrás se vuelven negras y se estira un poco si el scroll es rápido.
4. Las marcas cercanas a la aguja y al puntero crecen en campana; la etiqueta muestra porcentaje y capítulo.
5. Clic en la regla salta a ese punto; arrastrar la recorre; las flechas, RePág, Inicio y Fin funcionan con el foco en la regla.
6. «Lateral» lleva la regla a un riel vertical a la derecha (cifra abajo, números a la izquierda) y «Cabecera» la devuelve; en ambos casos vuelve a brotar y la pastilla del selector se desliza.
7. En 1440 × 900 se lee bien sobre los fondos blancos y negros de los capítulos.
8. En 390 × 844 la regla ocupa una fila propia bajo la marca; en vertical el riel mide 46 px y no tapa el texto.
9. Sin scroll horizontal y sin errores en consola.
10. Con movimiento reducido todo aparece en su estado final y la aguja sigue al scroll sin muelle.

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:
    • «Cambia el color vivo a azul eléctrico»
    • «Pon 101 marcas y una larga cada diez»
    • «Que la aguja siga el scroll con más rebote»
    • «Déjala solo en el lateral y quita el selector»
  4. 04

    Publica

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

Guías por herramientaClaudeChatGPTGeminiCursorLovableBoltv0

Licencia en claro

Qué puedes hacer con esta pieza

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

Puedes

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

No puedes

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

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

Preguntas frecuentes

Lo que suele preguntarse

¿Puedo usar la regla solo en horizontal o solo en vertical?
Sí. Quita el selector Cabecera / Lateral y llama a ponerEje('h') o ponerEje('v') al arrancar. La regla es un único bloque con su CSS y su JS, y los dos ejes comparten todo el código.
¿Cómo cambio el número de marcas o cada cuántas es larga?
En la primera línea del script: N es el total de marcas y CADA la separación de las largas. Los números de la regla se recalculan solos como porcentaje, así que valen 11 o 101 marcas sin tocar nada más.
¿Funciona si la página es corta o si la regla va dentro de un contenedor con scroll propio?
Con una página más corta que la pantalla la aguja se queda en 0 sin errores. Para un contenedor propio, sustituye la función prog() por scrollTop / (scrollHeight − clientHeight) de ese elemento y escucha su evento scroll.
¿Es accesible y qué pasa con el movimiento reducido?
Es un solo role=slider con valor, texto de valor y orientación, y se maneja con flechas, RePág, AvPág, Inicio y Fin. Con prefers-reduced-motion las marcas aparecen ya crecidas y la aguja sigue el scroll sin muelle ni estiramiento.
¿Qué diferencia hay entre el prompt Completo y el Lite?
El Completo (≈12977 tokens) describe la pieza con todo detalle y es el que mejor resultado da en Claude, ChatGPT, Gemini o Cursor. El Lite (≈2461 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.