Saltar al contenido
motionframe

Buscar piezas

Categorías

La pieza

Qué hace y dónde encaja

Cada letra de los titulares es independiente y mueve dos ejes de Roboto Flex: en reposo es finísima y estrecha; cerca del cursor gana peso, se ensancha y vira al color de acento. Engorda rápido y se adelgaza despacio, como una respiración. Un sistema de contención reparte el aire para que ninguna línea desborde a ningún ancho, y sin cursor (táctil o ratón quieto) respira la línea que cruza el centro de la pantalla, así que en móvil el scroll lo anima todo. Incluye una respiración guiada 4·7·8 con botón, cuenta atrás y frase activa, un anillo que marca el radio de influencia y una lectura en vivo del peso y la anchura. Encaja en portadas de estudios de yoga, fisioterapia, bienestar, estudios creativos o marcas editoriales. Funciona igual en papel y tinta, en blanco y negro o con cualquier acento cálido o frío.

Prompt
≈12.917 tokens · con Lite
Librerías
ninguna
Demo
26,3 KB · ligero
Prueba en frío
9,8/10 · 30-09-2026

Antes de copiar

Tu marca, dentro del prompt

Cambia lo que quieras y copia: el prompt sale ya con el nombre, los colores y los textos de tu cliente. Lo que dejes en blanco se queda como en la demo.

Ver 5 campos más
Colores

Sin cambios: se copia con los valores de la demo.

Gratis y entero

El prompt, entero y a la vista

Qué trae el prompt, por apartados

  1. 01Qué es
  2. 02Librerías y esqueleto
  3. 03Tokens de diseño
  4. 04Estructura y contenido
  5. 05Movimiento
  6. 06Código clave
  7. 07Interacción
  8. 08Adaptación a móvil y tablet
  9. 09Accesibilidad y movimiento reducido
  10. 10Rendimiento
  11. 11Personalización
  12. 12Comprobación final
Completo
5.865 palabras · ≈12.917 tokens
Lite
1.249 palabras · ≈2.224 tokens
Librerías
ninguna
Peso de la demo
26,3 KB (ligero)
Resolución máxima (DPR)
2
Se pausa fuera de pantalla
sí
Respeta «menos movimiento»
sí

Tokens estimados para español (≈3,6 caracteres por token): sirven para orientarte, no son exactos.

prompt.md
# Recrea en un único archivo HTML: Texto que respira con el cursor

Un titular en tipografía variable cuyas letras toman aire al notar el cursor —ganan peso, se ensanchan y se calientan de color— y lo sueltan despacio al alejarte, como una respiración tranquila.

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 corta de tres secciones para **Aliento**, una casa de respiración consciente en Portonova. El protagonista es el efecto tipográfico: cada letra de los tres titulares grandes es un `<span>` independiente con la fuente variable **Roboto Flex**, y dos ejes (`wght` peso y `wdth` anchura) se mueven según la cercanía del cursor.

Idea y reglas del efecto (son la esencia; no las cambies):

1. **Reposo**: letra finísima y muy estrecha (`wght` 250, `wdth` 45), color tinta. **Pico**: negra y ancha (`wght` 1000, `wdth` 151), color que vira al acento almagra.
2. Cada letra tiene un valor `v` entre 0 y 1. Los dos ejes se interpolan **en línea recta** con `v`; el color vira con `v^1,4 · 0,9`.
3. **Toma aire rápido y lo suelta despacio**: `v` sigue a su objetivo con un suavizado exponencial asimétrico, constante 7,5 /s al subir y 1,9 /s al bajar (independiente de los fps).
4. **Influencia del cursor**: gaussiana elíptica alrededor del puntero. `q = (dx² + (dy·1,25)²) / R²`, influencia `= e^(−1,4·q)`. `R` = tamaño de fuente del bloque × `data-radio`.
5. **Respiración de fondo**: aunque nadie toque nada, todas las letras oscilan entre `v` 0,04 y 0,10 con una ola que recorre el texto: `0,04 + 0,06·(0,5 + 0,5·sin(t·0,9 − u·5,2))`, donde `u` es la posición de la letra en el bloque (0 → 1).
6. **Sin cursor** (pantalla táctil, o ratón quieto 2,5 s, o fuera de la ventana): respira **la línea que cruza el centro vertical de la pantalla**. Es una banda gaussiana vertical (`e^(−1,4·(dy/R)²)`) modulada por una ola horizontal `0,25 + 0,75·ola²`, con `ola = 0,5 + 0,5·sin(t·1,8 − (x/anchoVentana)·7)`. Así, en móvil, al hacer scroll cada línea se hincha al pasar por el centro.
7. **El aire se reparte, no desborda**: la anchura de cada letra crece casi en línea recta con `v` (medido: `ancho(v) ≈ ancho_reposo + crece·v`). En cada línea se suma `Σ crece·v`; si supera el hueco libre hasta el borde derecho de la caja (×0,985), se escalan todos los `v` de esa línea por el mismo factor. Ninguna línea se sale nunca de su caja, a ningún ancho.
8. **Posiciones vivas sin leer el layout**: con ese mismo modelo se calcula dónde está cada letra ahora (`xv` = posición en reposo + lo que han empujado las letras anteriores de su línea + la mitad de su propio crecimiento). La influencia se calcula con `xv`, así el cursor afecta a la letra que el usuario ve debajo, no a donde estaba en reposo.
9. **Respiración guiada 4·7·8** (sección oscura): un botón arranca cuatro ciclos de Inhala 4 s · Sostén 7 s · Suelta 8 s. Las letras de ese bloque se llenan de izquierda a derecha al inhalar, se sostienen con un temblor mínimo y se vacían al soltar; la frase de la fase activa queda a opacidad 1 y las otras a 0,28.

Detalles de estudio que forman parte de la pieza: grano de papel en toda la página (SVG `feTurbulence` en línea), anillo fino que marca el radio de influencia y respira ±5 %, lectura en vivo de la letra más cercana al cursor («Letra · Peso · Anchura»), marca con un círculo que late, línea «Sigue bajando» que se dibuja y se borra, escalonado de las tres frases del método en escritorio y dos marcas almagra en los bordes a media altura en táctil que indican la línea que respira.

## Librerías y esqueleto

- **Sin librerías JS.** JS vanilla, un único `<script>` clásico al final del `<body>` (una IIFE).
- Fuentes de Google (solo estas, con `display=swap`; ambas tienen ñ, tildes, ¿ y ¡):

```html
<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=Newsreader:ital,opsz,wght@0,6..72,400;1,6..72,400&family=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000&display=swap" rel="stylesheet">
```

- Roboto Flex (display y etiquetas): se controla SIEMPRE con `font-variation-settings`, fijando `"opsz" 144` en los titulares (así la medida no cambia con el tamaño). Newsreader (texto corrido, cursivas y el número de la cuenta atrás).
- Cabecera del documento:

```html
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aliento — Texto que respira</title>
<meta name="description" content="Tipografía variable que toma aire al notar el cursor. Aliento, casa de respiración en Portonova.">
<!-- aquí los tres <link> de fuentes -->
<style> /* CSS completo de la sección «Código clave» */ </style>
</head>
```

## Tokens de diseño

```css
:root{
  --papel:#eee8dc;
  --papel-suave:#b9b1a3;
  --tinta:#191713;
  --tinta-suave:#5d574d;
  --almagra:#a63d22;
  --almagra-clara:#e58a67;
  --linea:rgba(25,23,19,.16);
  --linea-oscura:rgba(238,232,220,.16);
  --gutter:clamp(16px,4vw,64px);
  --display:"Roboto Flex",system-ui,sans-serif;
  --texto:"Newsreader",Georgia,serif;
  --ease-aire:cubic-bezier(.22,.8,.24,1);
}
```

- Los cuatro colores con variable deben ser **hexadecimales de 6 cifras** (`#rrggbb`): el JS los lee con `getComputedStyle` para interpolar el color de cada letra.
- Cada bloque que respira define `--c-reposo` y `--c-pico`: en fondo claro `var(--tinta)` → `var(--almagra)`; en la sección oscura `var(--papel)` → `var(--almagra-clara)`.
- Tipografía: titulares Roboto Flex `line-height:.84`, `letter-spacing:-.01em`; etiquetas Roboto Flex 12 px, mayúsculas, `letter-spacing:.14em`, `"wdth" 88,"wght" 520`; texto Newsreader 18 px / 1,5 (entradilla 18–22 px / 1,4).
- Contrastes comprobados: tinta sobre papel 14,6:1; `--tinta-suave` sobre papel 5,9:1; almagra sobre papel 5,2:1; almagra clara sobre tinta 6,9:1; `--papel-suave` sobre tinta 8,4:1.

## Estructura y contenido

Árbol de la página (textos literales):

1. **Cabecera** absoluta sobre el hero, rejilla de tres columnas `1fr auto 1fr`, borde inferior `--linea`:
   - Marca (enlace a `#inicio`, `aria-label="Aliento, inicio"`): SVG 22 px con un círculo de contorno (r 11, trazo 1,2) y un círculo relleno almagra (r 7, clase `pulmon`) que late; texto «Aliento» en Roboto Flex 20 px `"wdth" 70,"wght" 720`.
   - Centro: «Casa de respiración — Portonova».
   - Derecha (color `--tinta-suave`): «N.º 01 · Tipografía viva».
2. **Hero** (`min-height:100svh`):
   - Fila superior de etiquetas: «Clases de respiración consciente» a la izquierda y «wght 250—1000 · wdth 45—151» a la derecha.
   - `h1.respira` con `data-texto="Respira/hondo,|suelta/despacio."`, `data-ancho="0.6" data-alto="0.5" data-radio="0.9"`. Por defecto el titular es «Respira hondo, / suelta despacio.»: dos líneas en escritorio y cuatro en móvil.
   - Pie del hero (borde superior `--linea`) en tres columnas: entradilla, lectura en vivo y «Sigue bajando».
     - Entradilla con ratón: «Acerca el cursor: cada letra *toma aire* al notarte cerca y lo suelta despacio cuando te alejas.»
     - Entradilla en táctil: «Desliza despacio: la línea que cruza el centro de la pantalla *toma aire* y lo suelta al pasar.» (*toma aire* en cursiva almagra).
     - Lectura (`aria-hidden`): «Letra» R · «Peso» 250 · «Anchura» 45 (los valores se actualizan en vivo).
3. **Guía** (fondo tinta, texto papel):
   - Cabecera en tres columnas: «Método 4 · 7 · 8» (almagra clara) · «Una pauta sencilla para bajar pulsaciones antes de dormir o de una reunión difícil. Pulsa el botón y deja que las letras marquen el ritmo.» · botón píldora «Empezar respiración guiada» (pasa a «Detener»).
   - `h2.respira` con `data-texto="Inhala/en cuatro.|Sostén/en siete.|Suelta/en ocho."`, `data-ancho="0.68" data-alto="0.66" data-radio="0.85"`. En escritorio la 2.ª frase se desplaza 14 % y la 3.ª 28 % a la derecha (escalera).
   - Fila de estado: número grande en cursiva (cuenta atrás, empieza en «4») + texto de estado «Cuatro ciclos bastan para notar el cambio» + barra fina de progreso de la fase.
   - Mensajes de estado: «Inhala · 4 segundos», «Sostén · 7 segundos», «Suelta · 8 segundos», «Pausa. Vuelve cuando quieras», «Hecho. Cuatro ciclos completos».
4. **Cierre** (`min-height:90svh`):
   - Etiqueta «Primera sesión en Portonova».
   - `h2.respira` con `data-texto="Sin prisa."`, `data-ancho="0.52" data-alto="0.42" data-radio="0.9"`.
   - Lista de datos en cuatro columnas: «Dónde» Calle del Lago 21 / 00150 Portonova · «Horario» Lunes a sábado / 7:30 — 21:00 · «Primera sesión» 18 € impuestos incl. / 60 minutos · «Contacto» hola@aliento.com / +00 000 000 000.
   - Pie: «© 2026 Aliento · Casa de respiración» y «Roboto Flex · ejes de peso y anchura».

**Sintaxis de `data-texto`**: `|` es un salto de línea siempre; `/` es un espacio en pantallas de 700 px o más y un salto de línea por debajo de 700 px. Dentro de cada elemento `.respira` escribe como texto de respaldo (sin JS) el mismo texto cambiando `|` y `/` por espacios; el JS lo sustituye al arrancar.

HTML literal del `<body>` (antes del `<script>`):

```html
<header class="cab">
  <a class="marca" href="#inicio" aria-label="Aliento, inicio">
    <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.2"/><circle class="pulmon" cx="12" cy="12" r="7"/></svg>
    <span>Aliento</span>
  </a>
  <p class="etq centro">Casa de respiración — Portonova</p>
  <p class="etq der">N.º 01 · Tipografía viva</p>
</header>

<main id="inicio">
  <section class="hero" aria-labelledby="t-hero">
    <div>
      <div class="arriba">
        <p class="etq">Clases de respiración consciente</p>
        <p class="etq">wght 250—1000 · wdth 45—151</p>
      </div>
      <h1 id="t-hero" class="respira" data-texto="Respira/hondo,|suelta/despacio." data-ancho="0.6" data-alto="0.5" data-radio="0.9">Respira/hondo,|suelta/despacio.</h1>
    </div>
    <div class="pie-hero">
      <p class="entradilla"><span class="solo-puntero">Acerca el cursor: cada letra <em>toma aire</em> al notarte cerca y lo suelta despacio cuando te alejas.</span><span class="solo-tactil">Desliza despacio: la línea que cruza el centro de la pantalla <em>toma aire</em> y lo suelta al pasar.</span></p>
      <dl class="lectura" aria-hidden="true">
        <dt>Letra</dt><dd class="glifo" id="r-glifo">R</dd>
        <dt>Peso</dt><dd id="r-wght">250</dd>
        <dt>Anchura</dt><dd id="r-wdth">45</dd>
      </dl>
      <p class="etq baja" aria-hidden="true"><i></i>Sigue bajando</p>
    </div>
  </section>

  <section class="guia" aria-labelledby="t-guia">
    <div class="guia-cab">
      <p class="etq">Método 4 · 7 · 8</p>
      <p class="txt">Una pauta sencilla para bajar pulsaciones antes de dormir o de una reunión difícil. Pulsa el botón y deja que las letras marquen el ritmo.</p>
      <button class="boton" type="button" id="b-guia" aria-pressed="false" aria-controls="t-guia"><span class="dot" aria-hidden="true"></span><span class="b-txt">Empezar respiración guiada</span></button>
    </div>
    <h2 id="t-guia" class="respira" data-texto="Inhala/en cuatro.|Sostén/en siete.|Suelta/en ocho." data-ancho="0.68" data-alto="0.66" data-radio="0.85">Inhala en cuatro. Sostén en siete. Suelta en ocho.</h2>
    <div class="estado-fila">
      <span class="cuenta" id="cuenta" aria-hidden="true">4</span>
      <div>
        <p class="estado" id="estado" aria-live="polite">Cuatro ciclos bastan para notar el cambio</p>
        <div class="pista-fase" aria-hidden="true"><i id="pista"></i></div>
      </div>
    </div>
  </section>

  <section class="cierre" aria-labelledby="t-cierre">
    <p class="etq">Primera sesión en Portonova</p>
    <h2 id="t-cierre" class="respira" data-texto="Sin prisa." data-ancho="0.52" data-alto="0.42" data-radio="0.9">Sin prisa.</h2>
    <dl class="datos">
      <div><dt>Dónde</dt><dd>Calle del Lago 21<br>00150 Portonova</dd></div>
      <div><dt>Horario</dt><dd>Lunes a sábado<br>7:30 — 21:00</dd></div>
      <div><dt>Primera sesión</dt><dd>18 € impuestos incl.<br>60 minutos</dd></div>
      <div><dt>Contacto</dt><dd><a href="mailto:hola@aliento.com">hola@aliento.com</a><br><a href="tel:+00 000 000 000">+00 000 000 000</a></dd></div>
    </dl>
    <footer class="pie etq"><span>© 2026 Aliento · Casa de respiración</span><span>Roboto Flex · ejes de peso y anchura</span></footer>
  </section>
</main>

<div class="anillo" aria-hidden="true"></div>
<div class="punto" aria-hidden="true"></div>
<div class="guia-centro" aria-hidden="true"></div>
```

## Movimiento

| Elemento | Propiedad | De → a | Duración / constante | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Cada letra | `v` → `wght` 250→1000, `wdth` 45→151 (lineales) | reposo → pico | subida `k` 7,5 /s (τ ≈ 133 ms); bajada `k` 1,9 /s (τ ≈ 525 ms) | exponencial `1 − e^(−dt·k)` | — | objetivo = máx. de todas las influencias |
| Cada letra | color | `--c-reposo` → `--c-pico` | igual que `v` | mezcla `v^1,4 · 0,9` | — | igual |
| Influencia del ratón | peso de la fuente `ampR` | 0 ↔ 1 | `k` 5 /s | exponencial | se apaga tras 2,5 s quieto o al salir | `pointermove` (no táctil) |
| Influencia del dedo | `ampT` | 0 ↔ 1 | `k` 6 /s | exponencial | dura 1,4 s tras el último toque | `touchstart` / `touchmove` |
| Banda central | `ampA = máx(0, 1 − ampR − ampT)` | — | ola horizontal sin(t·1,8 − x/W·7), periodo ≈ 3,5 s | seno | — | sin cursor |
| Respiración de fondo | `v` mínimo | 0,04 ↔ 0,10 | sin(t·0,9 − u·5,2), periodo ≈ 7 s | seno | — | siempre |
| Anillo | posición | → puntero | `k` 14 /s | exponencial | — | puntero |
| Anillo | diámetro | → 2·R del bloque de la letra más cercana | `k` 8 /s, ×(1 + 0,05·sin(t·1,3)) | exponencial | — | puntero |
| Anillo y punto | opacidad | = `ampR` | — | — | — | puntero |
| Guía: inhala | llenado `p` | 0 → 1 | 4 s | `senoIO` (−(cos(πx)−1)/2) | — | botón |
| Guía: sostén | `p` = 1 + temblor | ±0,025·sin(t·3 + u·7) | 7 s | — | — | — |
| Guía: suelta | `p` | 1 → 0 | 8 s | 1 − `senoIO` | — | — |
| Guía: letra | objetivo | `0,08 + 0,84·suave(clamp((p·1,3 − u)/0,3))` | — | smoothstep | — | frente de 0,3 que avanza |
| Guía: frases inactivas | opacidad | 1 → 0,28 | 0,7 s | `--ease-aire` | — | cambio de fase |
| Guía: barra | `scaleX` | 0 → 1 por fase | duración de la fase | lineal | — | — |
| Guía: cuenta atrás | número | segundos restantes (`ceil`, mín. 1) | — | — | — | — |
| Marca (círculo) | `scale` | 0,55 → 1 (40–55 %) → 0,55 | 7 s, infinito | `--ease-aire` | — | carga |
| «Sigue bajando» | `scaleY` de una línea de 44 px | 0 → 1 (origen arriba) → 0 (origen abajo) | 2,8 s, infinito | `--ease-aire` | — | carga |
| Botón | fondo / color | transparente → papel | 0,35 s | `--ease-aire` | — | hover |
| Punto del botón | opacidad | 1 → 0,25 → 1 | 1,2 s, infinito | ease-in-out | — | guía activa |
| Enlaces de contacto | subrayado (`background-size`) | 100 % → 0 (sale por la derecha) | 0,45 s | `--ease-aire` | — | hover |

## Código clave

CSS completo (va dentro de `<style>`; el bloque `:root` es el de «Tokens de diseño»):

```css
/* :root → ver «Tokens de diseño» */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--papel);color:var(--tinta);font-family:var(--texto);font-size:18px;line-height:1.5;overflow-x:hidden;-webkit-font-smoothing:antialiased}
body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:50;opacity:.32;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .1 0 0 0 0 .09 0 0 0 0 .07 0 0 0 .55 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E")}
.sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
a{color:inherit}
:focus-visible{outline:2px solid var(--almagra);outline-offset:4px;border-radius:2px}

/* Etiquetas */
.etq{font-family:var(--display);font-variation-settings:"wdth" 88,"wght" 520;font-size:12px;letter-spacing:.14em;text-transform:uppercase;line-height:1.3;margin:0}

/* Cabecera */
.cab{position:absolute;top:0;left:0;right:0;z-index:5;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;padding:22px var(--gutter);border-bottom:1px solid var(--linea)}
.marca{display:flex;align-items:center;gap:10px;font-family:var(--display);font-variation-settings:"wdth" 70,"wght" 720;font-size:20px;letter-spacing:.01em;text-decoration:none}
.marca svg{width:22px;height:22px;overflow:visible}
.marca .pulmon{fill:var(--almagra);transform-origin:12px 12px;animation:marca 7s var(--ease-aire) infinite}
@keyframes marca{0%,100%{transform:scale(.55)}40%,55%{transform:scale(1)}}
.cab .centro{text-align:center}
.cab .der{text-align:right;color:var(--tinta-suave)}

/* Bloques que respiran */
.respira{margin:0;font-family:var(--display);font-weight:400;font-variation-settings:"opsz" 144,"wght" 250,"wdth" 45;line-height:.84;letter-spacing:-.01em;font-size:clamp(64px,19vw,300px);--c-reposo:var(--tinta);--c-pico:var(--almagra);user-select:none;-webkit-user-select:none}
.respira .linea{display:block;width:max-content;white-space:nowrap;transition:opacity .7s var(--ease-aire)}
.respira .pal{display:inline-block;white-space:nowrap}
.respira .l{display:inline-block}
@media (min-width:700px){.guia .linea[data-f="1"]{margin-left:14%}.guia .linea[data-f="2"]{margin-left:28%}}
.respira[data-fase="0"] .linea:not([data-f="0"]),
.respira[data-fase="1"] .linea:not([data-f="1"]),
.respira[data-fase="2"] .linea:not([data-f="2"]){opacity:.28}

/* Hero */
.hero{position:relative;min-height:100svh;display:grid;grid-template-rows:1fr auto;padding:calc(92px + 5svh) var(--gutter) 30px}
.hero .arriba{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:3svh}
.hero .arriba .etq{color:var(--tinta-suave)}
.hero h1{align-self:center}
.pie-hero{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr) auto;gap:32px;align-items:end;padding-top:28px;border-top:1px solid var(--linea);margin-top:4svh}
.entradilla{margin:0;font-size:clamp(18px,1.5vw,22px);line-height:1.4;max-width:34ch}
.entradilla em{font-style:italic;color:var(--almagra)}
.lectura{font-family:var(--display);font-variation-settings:"wdth" 80,"wght" 450;font-size:13px;letter-spacing:.04em;color:var(--tinta-suave);font-variant-numeric:tabular-nums;display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:0}
.lectura dt{text-transform:uppercase;letter-spacing:.14em;font-size:11px;padding-top:2px}
.lectura dd{margin:0;color:var(--tinta)}
.lectura .glifo{font-variation-settings:"wdth" 120,"wght" 800;color:var(--almagra)}
.baja{display:flex;align-items:center;gap:12px;color:var(--tinta-suave)}
.baja i{display:block;width:1px;height:44px;background:var(--tinta-suave);transform-origin:top;animation:baja 2.8s var(--ease-aire) infinite}
@keyframes baja{0%{transform:scaleY(0)}45%{transform:scaleY(1);transform-origin:top}55%{transform:scaleY(1);transform-origin:bottom}100%{transform:scaleY(0);transform-origin:bottom}}
.solo-tactil{display:none}
@media (hover:none),(pointer:coarse){.solo-tactil{display:inline}.solo-puntero{display:none}}

/* Guía 4-7-8 */
.guia{background:var(--tinta);color:var(--papel);padding:clamp(80px,14svh,160px) var(--gutter);position:relative}
.guia .respira{--c-reposo:var(--papel);--c-pico:var(--almagra-clara)}
.guia-cab{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.2fr) auto;gap:32px;align-items:start;padding-bottom:28px;border-bottom:1px solid var(--linea-oscura);margin-bottom:clamp(40px,8svh,96px)}
.guia-cab .etq{color:var(--almagra-clara)}
.guia-cab p.txt{margin:0;color:var(--papel-suave);max-width:40ch;font-size:clamp(17px,1.35vw,20px)}
.boton{appearance:none;border:1px solid var(--papel);background:transparent;color:var(--papel);font-family:var(--display);font-variation-settings:"wdth" 90,"wght" 560;font-size:14px;letter-spacing:.08em;text-transform:uppercase;padding:15px 22px;border-radius:999px;cursor:pointer;display:inline-flex;align-items:center;gap:12px;transition:background .35s var(--ease-aire),color .35s var(--ease-aire);min-height:48px;white-space:nowrap}
.boton:hover{background:var(--papel);color:var(--tinta)}
.boton:focus-visible{outline-color:var(--almagra-clara)}
.boton .dot{width:8px;height:8px;border-radius:50%;background:var(--almagra-clara)}
.boton[aria-pressed="true"] .dot{animation:dot 1.2s ease-in-out infinite}
@keyframes dot{50%{opacity:.25}}
.estado-fila{display:grid;grid-template-columns:auto minmax(0,1fr);gap:24px;align-items:center;margin-top:clamp(40px,7svh,80px)}
.cuenta{font-family:var(--texto);font-style:italic;font-size:clamp(56px,6vw,88px);line-height:.9;color:var(--almagra-clara);min-width:1.2ch;font-variant-numeric:tabular-nums}
.estado{margin:0 0 10px;font-family:var(--display);font-variation-settings:"wdth" 88,"wght" 480;font-size:14px;letter-spacing:.1em;text-transform:uppercase;color:var(--papel-suave)}
.pista-fase{height:1px;background:var(--linea-oscura);position:relative;overflow:hidden}
.pista-fase i{position:absolute;inset:0;background:var(--almagra-clara);transform:scaleX(0);transform-origin:left}

/* Cierre */
.cierre{padding:clamp(80px,14svh,160px) var(--gutter) 28px;min-height:90svh;display:flex;flex-direction:column}
.cierre .respira{margin:3svh 0 clamp(48px,9svh,110px)}
.cierre .etq{color:var(--tinta-suave)}
.datos{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:28px;margin:0;border-top:1px solid var(--linea);padding-top:26px}
.datos dt{font-family:var(--display);font-variation-settings:"wdth" 88,"wght" 520;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--tinta-suave);margin-bottom:8px}
.datos dd{margin:0;font-size:clamp(18px,1.4vw,21px);line-height:1.35}
.datos a{text-decoration:none;background:linear-gradient(currentColor,currentColor) 0 100%/100% 1px no-repeat;transition:background-size .45s var(--ease-aire)}
.datos a:hover{background-size:0 1px;background-position:100% 100%}
.pie{margin-top:auto;padding-top:64px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;color:var(--tinta-suave)}

/* Anillo del cursor */
.anillo,.punto{position:fixed;left:0;top:0;pointer-events:none;z-index:40;opacity:0;mix-blend-mode:difference}
.anillo{width:100px;height:100px;border:1px solid #fff;border-radius:50%;will-change:transform}
.punto{width:6px;height:6px;margin:-3px 0 0 -3px;border-radius:50%;background:#fff}
.guia-centro{display:none}
@media (hover:none),(pointer:coarse){
  .anillo,.punto{display:none}
  .guia-centro{display:block;position:fixed;top:50%;left:0;right:0;height:0;pointer-events:none;z-index:30}
  .guia-centro::before,.guia-centro::after{content:"";position:absolute;top:0;width:18px;height:1px;background:var(--almagra)}
  .guia-centro::before{left:0}.guia-centro::after{right:0}
}

@media (max-width:900px){
  .cab{grid-template-columns:1fr auto}
  .cab .centro{display:none}
  .pie-hero{grid-template-columns:1fr;gap:22px}
  .baja{display:none}
  .guia-cab{grid-template-columns:1fr;gap:20px}
  .datos{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:520px){
  .hero{padding-top:calc(76px + 4svh)}
  .cab{padding-top:18px;padding-bottom:18px}
  .hero .arriba .etq:last-child{display:none}
  .lectura{display:none}
  .datos{grid-template-columns:1fr;gap:20px}
  .estado-fila{gap:16px}
}
@media (prefers-reduced-motion:reduce){
  .marca .pulmon,.baja i,.boton[aria-pressed="true"] .dot{animation:none}
  .marca .pulmon{transform:scale(.8)}
  .baja i{transform:none}
  .respira .linea{transition:none}
  .anillo,.punto{display:none}
}
```

JS completo (un único `<script>` al final del `<body>`; cópialo tal cual):

```js
(() => {
  const REPOSO = { w: 250, d: 45 };
  const PICO = { w: 1000, d: 151 };
  const SUBIDA = 7.5, BAJADA = 1.9;      // toma aire rápido, lo suelta despacio
  const INACTIVO = 2500;                  // ms sin mover el ratón → respira sola
  const FASES = [
    { n: "Inhala", s: 4 }, { n: "Sostén", s: 7 }, { n: "Suelta", s: 8 }
  ];
  const CICLO = 19, CICLOS = 4;

  const reducido = matchMedia("(prefers-reduced-motion: reduce)");
  const tactil = matchMedia("(hover: none), (pointer: coarse)");
  const bloques = [...document.querySelectorAll(".respira")].map(el => ({ el, letras: [], R: 100, visible: true }));
  const anillo = document.querySelector(".anillo"), punto = document.querySelector(".punto");
  const rG = document.getElementById("r-glifo"), rW = document.getElementById("r-wght"), rD = document.getElementById("r-wdth");
  const bGuia = document.getElementById("b-guia"), estado = document.getElementById("estado");
  const cuenta = document.getElementById("cuenta"), pista = document.getElementById("pista");
  const bloqueGuia = bloques.find(b => b.el.id === "t-guia");

  const hex = h => { h = h.trim().replace("#", ""); return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16)); };
  const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
  const suave = x => x * x * (3 - 2 * x);
  const senoIO = x => -(Math.cos(Math.PI * x) - 1) / 2;

  let estrecho = null;

  function construir(b) {
    const el = b.el, frases = el.dataset.texto.split("|");
    el.textContent = "";
    const sr = document.createElement("span");
    sr.className = "sr";
    sr.textContent = frases.map(f => f.replace(/\//g, " ")).join(" ");
    el.appendChild(sr);
    b.letras = []; b.lineas = [];
    frases.forEach((f, fi) => {
      const partes = estrecho ? f.split("/") : [f.replace(/\//g, " ")];
      partes.forEach(p => {
        const linea = document.createElement("span");
        linea.className = "linea";
        linea.dataset.f = fi;
        linea.setAttribute("aria-hidden", "true");
        const palabras = p.trim().split(" ");
        const enLinea = [];
        palabras.forEach((w, wi) => {
          const pal = document.createElement("span");
          pal.className = "pal";
          for (const ch of w) {
            const s = document.createElement("span");
            s.className = "l";
            s.textContent = ch;
            pal.appendChild(s);
            const L = { el: s, ch, f: fi, v: 0, m: 0, pintado: -1, x: 0, xv: 0, y: 0, u: 0, rel: 0, crece: 0, letra: /\p{L}/u.test(ch) };
            b.letras.push(L); enLinea.push(L);
          }
          linea.appendChild(pal);
          if (wi < palabras.length - 1) linea.appendChild(document.createTextNode(" "));
        });
        enLinea.forEach((L, i) => L.rel = enLinea.length > 1 ? i / (enLinea.length - 1) : .5);
        b.lineas.push({ el: linea, letras: enLinea, reposo: 0, hueco: 0 });
        el.appendChild(linea);
      });
    });
    b.letras.forEach((L, i) => L.u = b.letras.length > 1 ? i / (b.letras.length - 1) : 0);
    const cs = getComputedStyle(el);
    b.cR = hex(cs.getPropertyValue("--c-reposo"));
    b.cP = hex(cs.getPropertyValue("--c-pico"));
  }

  const vfs = (w, d) => `"opsz" 144, "wght" ${w}, "wdth" ${d}`;
  function pintar(b, L, forzar) {
    const v = L.m;
    if (!forzar && Math.abs(v - L.pintado) < .002) return;
    L.pintado = v;
    const w = REPOSO.w + (PICO.w - REPOSO.w) * v;
    const d = REPOSO.d + (PICO.d - REPOSO.d) * v;
    L.el.style.fontVariationSettings = vfs(w.toFixed(1), d.toFixed(1));
    const m = Math.pow(v, 1.4) * .9;
    const c = b.cR.map((r, i) => Math.round(r + (b.cP[i] - r) * m));
    L.el.style.color = `rgb(${c[0]},${c[1]},${c[2]})`;
  }

  function medir() {
    const nuevo = innerWidth < 700;
    if (nuevo !== estrecho) { estrecho = nuevo; bloques.forEach(construir); }
    const H = innerHeight, sx = scrollX, sy = scrollY;
    bloques.forEach(b => {
      const el = b.el;
      const reposo = () => b.letras.forEach(L => { L.el.style.fontVariationSettings = vfs(REPOSO.w, REPOSO.d); });
      reposo();
      el.style.fontSize = "100px";
      const lineas = [...el.querySelectorAll(".linea")];
      const ancho = Math.max(...lineas.map(l => l.getBoundingClientRect().width));
      const caja = el.clientWidth;
      const porAncho = caja * parseFloat(el.dataset.ancho) / ancho * 100;
      const porAlto = H * parseFloat(el.dataset.alto) / (lineas.length * .84);
      const fs = Math.max(40, Math.min(porAncho, porAlto, 420));
      el.style.fontSize = fs.toFixed(1) + "px";
      b.R = fs * parseFloat(el.dataset.radio);
      const borde = el.getBoundingClientRect().right;
      b.letras.forEach(L => {
        const r = L.el.getBoundingClientRect();
        L.x = L.xv = r.left + r.width / 2 + sx;
        L.y = r.top + r.height / 2 + sy;
        L.crece = -r.width;
      });
      b.lineas.forEach(ln => {
        const r = ln.el.getBoundingClientRect();
        ln.reposo = r.width;
        ln.hueco = Math.max(0, (borde - r.left) * .985 - r.width);   // aire que cabe en la línea
      });
      // Cuánto crece cada letra en su pico: sirve para que ninguna línea se salga de su caja
      b.letras.forEach(L => { L.el.style.fontVariationSettings = vfs(PICO.w, PICO.d); });
      b.letras.forEach(L => { L.crece += L.el.getBoundingClientRect().width; });
      reposo();
      b.letras.forEach(L => pintar(b, L, true));
    });
  }

  // Entrada
  const raton = { x: -9999, y: -9999, dentro: false, t: -1e9 };
  const toque = { x: 0, y: 0, t: -1e9 };
  addEventListener("pointermove", e => {
    if (e.pointerType === "touch") return;
    raton.x = e.clientX; raton.y = e.clientY; raton.dentro = true; raton.t = performance.now();
  }, { passive: true });
  document.addEventListener("mouseout", e => { if (!e.relatedTarget) raton.dentro = false; });
  const alTocar = e => { const p = e.touches[0]; if (!p) return; toque.x = p.clientX; toque.y = p.clientY; toque.t = performance.now(); };
  addEventListener("touchstart", alTocar, { passive: true });
  addEventListener("touchmove", alTocar, { passive: true });

  // Respiración guiada
  const guia = { activa: false, t0: 0, fase: -1 };
  function alternarGuia(on) {
    guia.activa = on; guia.t0 = performance.now(); guia.fase = -1;
    bGuia.setAttribute("aria-pressed", String(on));
    bGuia.querySelector(".b-txt").textContent = on ? "Detener" : "Empezar respiración guiada";
    if (!on) { bloqueGuia.el.removeAttribute("data-fase"); cuenta.textContent = "4"; pista.style.transform = "scaleX(0)"; }
    if (reducido.matches) arrancar();
    despertar();
  }
  bGuia.addEventListener("click", () => {
    alternarGuia(!guia.activa);
    if (!guia.activa) estado.textContent = "Pausa. Vuelve cuando quieras";
  });
  addEventListener("keydown", e => { if (e.key === "Escape" && guia.activa) { alternarGuia(false); estado.textContent = "Pausa. Vuelve cuando quieras"; } });

  function estadoGuia(now) {
    if (!guia.activa) return null;
    const s = (now - guia.t0) / 1000;
    if (s >= CICLO * CICLOS) {
      alternarGuia(false);
      estado.textContent = "Hecho. Cuatro ciclos completos";
      return null;
    }
    let tau = s % CICLO, fi = 0;
    while (tau >= FASES[fi].s) { tau -= FASES[fi].s; fi++; }
    const F = FASES[fi], prog = tau / F.s;
    if (fi !== guia.fase) {
      guia.fase = fi;
      bloqueGuia.el.dataset.fase = fi;
      estado.textContent = `${F.n} · ${F.s} segundos`;
    }
    cuenta.textContent = Math.max(1, Math.ceil(F.s - tau));
    pista.style.transform = `scaleX(${prog.toFixed(4)})`;
    const p = fi === 0 ? senoIO(prog) : fi === 1 ? 1 : 1 - senoIO(prog);
    return { p, fi };
  }

  // La anchura de una letra crece casi en línea recta con v: ancho(v) = ancho_reposo + crece · v.
  // 1) Si el aire no cabe, la línea se contiene: se escalan todos sus valores a la vez.
  // 2) Con ese modelo se sabe dónde está cada letra ahora (xv) sin leer el layout.
  function contener(b, ln) {
    let suma = 0;
    for (const L of ln.letras) suma += L.crece * L.v;
    const s = suma > ln.hueco ? ln.hueco / suma : 1;
    let empuje = 0;
    for (const L of ln.letras) {
      L.m = L.v * s;
      const extra = L.crece * L.m;
      L.xv = L.x + empuje + extra / 2;
      empuje += extra;
      pintar(b, L, false);
    }
  }

  // Bucle
  let rafId = 0, ultimo = 0, t = 0, ampR = 0, ampT = 0;
  function cuadro(now) {
    rafId = 0;
    const dt = Math.min(.05, (now - ultimo) / 1000 || .016);
    ultimo = now; t += dt;
    const W = innerWidth, H = innerHeight, sx = scrollX, sy = scrollY;
    const k = x => 1 - Math.exp(-dt * x);
    const esTactil = tactil.matches;
    ampR += ((!esTactil && raton.dentro && now - raton.t < INACTIVO ? 1 : 0) - ampR) * k(5);
    ampT += ((now - toque.t < 1400 ? 1 : 0) - ampT) * k(6);
    const ampA = Math.max(0, 1 - ampR - ampT);
    const fuentes = [
      { x: raton.x, y: raton.y, a: ampR },
      { x: toque.x, y: toque.y, a: ampT }
    ].filter(f => f.a > .01);
    const yBanda = H * .5;   // sin cursor: respira la línea que cruza el centro de la pantalla
    const g = estadoGuia(now);

    let cerca = null, dMin = Infinity;
    for (const b of bloques) {
      if (!b.visible) continue;
      const R2 = b.R * b.R;
      for (const L of b.letras) {
        const px = L.xv - sx, py = L.y - sy;
        let inf = 0;
        for (const f of fuentes) {
          const dx = px - f.x, dy = (py - f.y) * 1.25;
          const q = (dx * dx + dy * dy) / R2;
          const v = f.a * Math.exp(-q * 1.4);
          if (v > inf) inf = v;
        }
        if (ampA > .01) {
          const dy = (py - yBanda) / b.R;
          const ola = .5 + .5 * Math.sin(t * 1.8 - (px / W) * 7);
          const v = ampA * Math.exp(-dy * dy * 1.4) * (.25 + .75 * ola * ola);
          if (v > inf) inf = v;
        }
        let obj = Math.max(inf, .04 + .06 * (.5 + .5 * Math.sin(t * .9 - L.u * 5.2)));
        if (g && b === bloqueGuia) {
          const lleno = suave(clamp((g.p * 1.3 - L.u) / .3, 0, 1));
          let v = .08 + .84 * lleno;
          if (g.fi === 1) v += .025 * Math.sin(t * 3 + L.u * 7);
          obj = Math.max(obj, v);
        }
        L.v += (obj - L.v) * k(obj > L.v ? SUBIDA : BAJADA);
        if (ampR > .05 && L.letra) {
          const dx = px - raton.x, dy = py - raton.y, d = dx * dx + dy * dy;
          if (d < dMin) { dMin = d; cerca = L; }
        }
      }
      for (const ln of b.lineas) contener(b, ln);
    }

    // Anillo y lectura
    if (ampR > .01) {
      let R = 120;
      if (cerca) R = bloques.find(b => b.letras.includes(cerca)).R;
      anillo._s = (anillo._s || R) + (R - (anillo._s || R)) * k(8);
      anillo._x = (anillo._x ?? raton.x) + (raton.x - (anillo._x ?? raton.x)) * k(14);
      anillo._y = (anillo._y ?? raton.y) + (raton.y - (anillo._y ?? raton.y)) * k(14);
      const resp = 1 + .05 * Math.sin(t * 1.3);
      const D = anillo._s * 2 * resp;
      anillo.style.width = anillo.style.height = D.toFixed(1) + "px";
      anillo.style.transform = `translate(${(anillo._x - D / 2).toFixed(1)}px,${(anillo._y - D / 2).toFixed(1)}px)`;
      punto.style.transform = `translate(${raton.x}px,${raton.y}px)`;
    }
    anillo.style.opacity = ampR.toFixed(3);
    punto.style.opacity = ampR.toFixed(3);
    if (cerca) {
      const v = cerca.m;
      rG.textContent = cerca.ch;
      rW.textContent = Math.round(REPOSO.w + (PICO.w - REPOSO.w) * v);
      rD.textContent = Math.round(REPOSO.d + (PICO.d - REPOSO.d) * v);
    }
    arrancar();
  }

  // Movimiento reducido: una respiración congelada, legible y sin bucle
  function estatico() {
    bloques.forEach((b, bi) => b.letras.forEach(L => {
      const c = [.28, .62, .4][(L.f + bi) % 3];
      L.v = .12 + .5 * Math.exp(-Math.pow((L.rel - c) / .24, 2));
    }));
    bloques.forEach(b => b.lineas.forEach(ln => { contener(b, ln); ln.letras.forEach(L => pintar(b, L, true)); }));
    estadoGuia(performance.now());   // solo textos, cuenta y frase activa
  }

  let temporizador = 0;
  function arrancar() {
    if (reducido.matches) {
      estatico();
      clearTimeout(temporizador);
      if (guia.activa) temporizador = setTimeout(arrancar, 250);
      return;
    }
    if (rafId || document.hidden || !bloques.some(b => b.visible)) return;
    rafId = requestAnimationFrame(cuadro);
  }
  function despertar() { ultimo = performance.now(); arrancar(); }

  const io = new IntersectionObserver(es => {
    es.forEach(e => { const b = bloques.find(b => b.el === e.target); if (b) b.visible = e.isIntersecting; });
    despertar();
  }, { rootMargin: "20% 0px" });
  bloques.forEach(b => io.observe(b.el));
  document.addEventListener("visibilitychange", despertar);
  reducido.addEventListener("change", () => { medir(); despertar(); });

  let rt = 0;
  addEventListener("resize", () => { cancelAnimationFrame(rt); rt = requestAnimationFrame(() => { medir(); despertar(); }); });

  const listo = () => { medir(); despertar(); };
  if (document.fonts && document.fonts.ready) {
    Promise.all([document.fonts.load('400 100px "Roboto Flex"'), document.fonts.ready]).then(listo, listo);
  } else listo();
  medir();
})();
```

Puntos delicados del código (no los simplifiques):

- `medir()` se ejecuta al cargar, cuando `document.fonts.load('400 100px "Roboto Flex"')` y `document.fonts.ready` se resuelven, y en cada `resize` (agrupado en un `requestAnimationFrame`). Pone todas las letras en reposo, mide a 100 px, calcula el tamaño de fuente de cada bloque con `min(porAncho, porAlto, 420)` (mínimo 40), guarda el centro en reposo de cada letra en coordenadas de página, el `hueco` de cada línea y cuánto crece cada letra en su pico (la mide en pico y vuelve a reposo). Sin esa medida el efecto desborda o apunta a letras equivocadas.
- `.linea` lleva `width:max-content`: si no, `getBoundingClientRect().width` devuelve el ancho de la caja y el ajuste falla.
- Solo se escribe el estilo de una letra si su valor cambia más de 0,002.
- En cada fotograma no se lee el layout: todas las posiciones salen del modelo (`contener`).

## Interacción

- **Ratón / lápiz** (`pointermove` con `pointerType` distinto de `touch`): las letras en un radio `R` toman aire; el anillo sigue al puntero con retardo elástico y su diámetro es `2·R` del bloque de la letra más cercana; el punto central va pegado al puntero. La lectura «Letra · Peso · Anchura» muestra la letra más cercana (solo letras, no signos) con sus valores reales.
- Ratón quieto 2,5 s o fuera de la ventana (`mouseout` sin `relatedTarget`): el anillo se desvanece y pasa a respirar la banda central.
- **Táctil** (`(hover: none), (pointer: coarse)`): no hay anillo ni lectura de ratón; manda la banda central, así que el scroll hace respirar cada línea al cruzar la mitad de la pantalla. Tocar o arrastrar el dedo añade una influencia puntual durante 1,4 s. Dos marcas almagra de 18 × 1 px en los bordes izquierdo y derecho, a media altura y fijas, señalan esa línea.
- **Teclado**: el botón es un `<button>` con `aria-pressed`; Intro o Espacio lo alternan; `Esc` detiene la guía. Foco visible con contorno de 2 px almagra (almagra clara en la sección oscura) y desplazamiento de 4 px.
- **Guía**: al pulsar, `aria-pressed="true"`, el texto pasa a «Detener», el punto parpadea, el bloque recibe `data-fase` (0, 1, 2) y la frase activa se queda a opacidad 1. Tras cuatro ciclos (76 s) se detiene sola con «Hecho. Cuatro ciclos completos». Al detenerla a mano: «Pausa. Vuelve cuando quieras», la cuenta vuelve a 4 y la barra a 0.
- Los titulares llevan `user-select:none` para que arrastrar no seleccione las letras.

## Adaptación a móvil y tablet

- **< 700 px**: `data-texto` rompe también en `/` (el titular pasa a cuatro líneas: «Respira / hondo, / suelta / despacio.»; el método a seis). Se reconstruye el DOM al cruzar ese umbral en cualquier sentido. Desaparece la escalera de la guía.
- **≤ 900 px**: cabecera en dos columnas (se oculta la etiqueta central); pie del hero en una columna y sin «Sigue bajando»; cabecera de la guía en una columna; datos en dos columnas.
- **≤ 520 px**: hero con `padding-top: calc(76px + 4svh)`; cabecera con 18 px de relleno vertical; se oculta la etiqueta de ejes del hero y la lectura en vivo; datos en una columna.
- El tamaño de los titulares siempre sale de `medir()`: ancho del bloque × `data-ancho`, limitado por `alto de ventana × data-alto / (líneas × 0,84)` y por 420 px. Referencias: a 1440 × 900 el titular mide ≈ 268 px; a 390 × 844, ≈ 125 px.
- Sin scroll horizontal de 360 a 1920 px (`overflow-x:hidden` en `body` como red, pero el sistema de contención ya lo garantiza). Alturas con `svh`, nunca `100vh`.

## Accesibilidad y movimiento reducido

- Cada `.respira` contiene un `<span class="sr">` con el texto completo (visualmente oculto) y todas sus líneas visuales llevan `aria-hidden="true"`: un lector de pantalla lee «Respira hondo, suelta despacio.» una sola vez y no letra a letra.
- Secciones con `aria-labelledby` hacia su titular. Anillo, punto, lectura, cuenta atrás, barra y marcas táctiles con `aria-hidden`.
- El estado de la guía está en `aria-live="polite"` y **solo cambia al cambiar de fase** (no cada segundo), para no saturar el lector. La cuenta atrás visual es `aria-hidden`.
- `prefers-reduced-motion: reduce`: no hay bucle `requestAnimationFrame`. Cada línea muestra una respiración congelada y legible: `v = 0,12 + 0,5·e^(−((rel − c)/0,24)²)`, con `rel` la posición de la letra en su línea (0 → 1) y `c` = 0,28, 0,62 o 0,40 según `(índice de frase + índice de bloque) mod 3`. Se paran el latido de la marca, la línea «Sigue bajando» y el parpadeo del botón; no hay anillo. La guía sigue funcionando sin mover letras: actualiza texto, cuenta atrás, barra y frase activa cada 250 ms.
- Área táctil del botón ≥ 48 px de alto.

## Rendimiento

- Sin WebGL ni canvas: solo DOM y `font-variation-settings`. Unas 70 letras en total.
- El bucle solo corre si algún bloque `.respira` está en pantalla (IntersectionObserver con `rootMargin: "20% 0px"`) y la pestaña está visible (`visibilitychange`); si no, se detiene y se reanuda con `despertar()`.
- `dt` limitado a 0,05 s para que una pestaña que vuelve no dé saltos.
- Ninguna lectura de layout por fotograma; escrituras de estilo solo cuando el valor cambia.
- Peso del archivo final ≈ 27 KB.

## Personalización

- **Marca y datos**: `Aliento`, `Portonova`, `Calle del Lago 21`, `00150`, `Lunes a sábado`, `7:30 — 21:00`, `18 €`, `hola@aliento.com` y `+00 000 000 000` (en el `href` usa `tel:` seguido del teléfono sin espacios y `mailto:` con el email).
- **Titulares**: `Respira/hondo,|suelta/despacio.` y `Sin prisa.` siguen la sintaxis de `data-texto` (`|` salto siempre, `/` salto solo en móvil). Con textos más largos no hace falta tocar nada: el tamaño se recalcula y la contención impide desbordes. Las frases del método 4·7·8 son parte de la función guiada: si cambias el método, cambia a la vez `FASES` en el JS y el texto del `h2`.
- **Color**: `#eee8dc`, `#191713`, `#a63d22` y `#e58a67` (hex de 6 cifras). Si cambias la tinta, ajusta `--linea` a su rgba al 16 %; si cambias el papel, ajusta `--linea-oscura`. Mantén AA: acento sobre papel ≥ 4,5:1 y acento claro sobre tinta ≥ 4,5:1.
- **Carácter del efecto**: más o menos radio con `data-radio` (0,6 = foco muy puntual; 1,2 = nube amplia); respiración más lenta cambiando `BAJADA` (1,2 = suelta muy despacio); rango de ejes en `REPOSO` y `PICO` (Roboto Flex admite `wght` 100–1000 y `wdth` 25–151).
- **Otra fuente variable**: sirve cualquiera con ejes `wght` y `wdth` (cambia el `<link>` y `--display`); si no tiene `opsz`, quita `"opsz" 144` de `vfs`.

## Comprobación final

1. A 1440 × 900 el titular ocupa dos líneas («Respira hondo,» / «suelta despacio.») y en reposo sus letras son finas y estrechas, color tinta.
2. Al pasar el ratón sobre una palabra, 2–4 letras se vuelven negras, anchas y almagra; al quitarlo se adelgazan despacio (claramente más lento que al engordar).
3. El anillo, fino y en modo `difference`, sigue al ratón con retardo, mide el radio de influencia y respira.
4. La lectura en vivo cambia de letra y de números al mover el ratón.
5. Pasando el cursor por el final de cualquier línea, a 360, 390, 768, 1440 y 1920 px de ancho, ninguna línea sobrepasa el borde derecho de su caja ni aparece scroll horizontal.
6. Con el ratón quieto 2,5 s, la línea que cruza el centro de la ventana respira sola con una ola que la recorre.
7. En un móvil (táctil), al hacer scroll cada línea se hincha al cruzar la mitad de la pantalla, y se ven las dos marcas almagra en los bordes.
8. Por debajo de 700 px el titular pasa a cuatro líneas y el método a seis; al volver a ensanchar, se recompone.
9. El botón «Empezar respiración guiada» inicia Inhala (4 s, las letras se llenan de izquierda a derecha), Sostén (7 s) y Suelta (8 s), con cuenta atrás, barra, frase activa a opacidad 1 y las demás a 0,28; `Esc` o «Detener» lo paran.
10. Con `prefers-reduced-motion: reduce` las letras quedan quietas en una respiración congelada legible y no hay bucle de animación.
11. Un lector de pantalla lee cada titular completo una vez, no letra a letra.
12. Cero errores en consola y el bucle se detiene con la pestaña oculta o con los titulares fuera de pantalla.
13. Funciona dentro de un iframe (sin `window.top`, sin `alert`, sin `localStorage`).

Prueba en frío

Lo que salió al pegarlo en frío

Pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos el resultado con la demo. Prueba hecha el 30-09-2026.

Lo que generó la IA con el prompt Completo de Texto que respira con el cursor
Prompt CompletoFiel a la demo9,8/10

Calco exacto de la demo: el código coincide con el original y las capturas solo cambian por la posición del cursor y el momento de la respiración. Maquetación, colores, anillo, lectura en vivo, guía 4·7·8 y adaptación móvil son idénticos.

Lo que generó la IA con el prompt Lite de Texto que respira con el cursor
Prompt LiteAceptable6,8/10

El efecto clave sale bien (letras que engordan, se ensanchan y viran a almagra, suelta lenta, línea central que respira) y las tres secciones tienen sus textos. Cambian detalles de composición: el titular del método sale bastante más pequeño, el botón va debajo del texto, la marca pierde su aro, la lectura en vivo se reparte en horizontal y «Sigue bajando» sigue visible en móvil.

Solo publicamos lo que hemos probado. Otras herramientas (ChatGPT, Gemini, Lovable…) pueden dar resultados distintos.

Paso a paso

Cómo usarlo en 4 pasos

  1. 01

    Personaliza

    Rellena el formulario de arriba con el nombre, los colores y los textos de tu cliente. Se meten en el prompt al copiar.
  2. 02

    Copia el Completo o el Lite

    El Completo, para Claude, ChatGPT, Gemini o Cursor. El Lite, más corto, para Lovable, Bolt o v0, que aceptan menos texto.
  3. 03

    Pega e itera

    Pégalo en una conversación nueva. Si quieres cambiar algo, pídelo con una frase corta:
    • «Que las letras suelten el aire todavía más despacio»
    • «Haz el radio del cursor más amplio, como una nube»
    • «Cambia la paleta a blanco y negro con acento azul cobalto»
    • «Usa solo el titular del hero, sin la guía ni el cierre»
  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 poner un titular más largo?
Sí. El tamaño de fuente se recalcula según el ancho y el alto disponibles, y la contención reparte el crecimiento para que ninguna línea se salga. Usa «|» para forzar saltos y «/» para los que solo quieras en móvil.
¿Qué pasa en el móvil, donde no hay cursor?
Respira la línea que cruza el centro de la pantalla, con una ola que la recorre; al hacer scroll cada línea se hincha al pasar. Tocar la pantalla añade además un foco puntual durante un segundo y medio.
¿Funciona con otra fuente?
Con cualquier fuente variable que tenga ejes de peso (wght) y anchura (wdth). Cambia el enlace de Google Fonts y la variable --display; si la fuente no tiene eje opsz, quita ese valor.
¿Es accesible para lectores de pantalla?
Sí. Cada titular se lee completo una sola vez gracias a un texto oculto, las letras sueltas están ocultas para el lector y, con movimiento reducido, las letras quedan quietas en una composición legible.
¿Qué diferencia hay entre el prompt Completo y el Lite?
El Completo (≈12917 tokens) describe la pieza con todo detalle y es el que mejor resultado da en Claude, ChatGPT, Gemini o Cursor. El Lite (≈2224 tokens) resume lo esencial para herramientas que aceptan menos texto, como Lovable, Bolt o v0.

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