
Titular gigante
GratisBloques de web · GSAP
La pieza
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.
Encaja bien en webs de
Antes de copiar
Cambia lo que quieras y copia: el prompt sale ya con el nombre, los colores y los textos de tu cliente. Lo que dejes en blanco se queda como en la demo.
Gratis y entero
Qué trae el prompt, por apartados
Tokens estimados para español (≈3,6 caracteres por token): sirven para orientarte, no son exactos.
# Recrea en un único archivo HTML: 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
Aún sin publicar para esta pieza
En la prueba en frío pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos lo que genera con la demo. Cuando esté hecha, aquí verás la captura, el veredicto y la fecha.
Paso a paso
Personaliza
Copia el Completo o el Lite
Pega e itera
Publica
Licencia en claro
Todas las piezas incluyen licencia de uso comercial. Usa el resultado tal cual en tus productos, encargos freelance o webs de clientes, sin atribución.
Puedes
No puedes
Es la misma licencia para lo gratis y lo Pro. La tienes entera en licencia de uso.
Preguntas frecuentes
Versión 1.0 · publicada el 01-10-2026

Bloques de web · GSAP

Bloques de web · JS

Bloques de web · CSS

Bloques de web · CSS

Bloques de web · JS

Bloques de web · GSAP

Bloques de web · GSAP

Bloques de web · SVG