Saltar al contenido
motionframe

Buscar piezas

Categorías

La pieza

Qué hace y dónde encaja

Un cursor propio hecho a mano, sin librerías. En reposo es un punto con un aro que lo sigue con un muelle y se estira al ir rápido. Al acercarse a un botón, enlace o chip, el elemento se desplaza hacia el puntero y el aro cambia de forma hasta envolverlo y lo pone en negativo con mix-blend-mode: difference. Sobre los titulares se convierte en una lupa de 150 px que invierte las letras, y sobre las fotos en un disco con texto («Ver proyecto», «Abrir serie») que se inclina al moverlo. El bucle se detiene cuando todo está quieto. En táctil desaparece: las fotos muestran su etiqueta fija y los botones dejan un eco al tocarlos. Encaja en portfolios, estudios creativos, agencias, arquitectura y marcas de producto cuidado. Funciona igual con fondo oscuro: el negativo se recalcula a partir del color del fondo.

Prompt
≈15.154 tokens · con Lite
Librerías
ninguna
Demo
34,2 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.

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
6.085 palabras · ≈15.154 tokens
Lite
1.269 palabras · ≈2.651 tokens
Librerías
ninguna
Peso de la demo
34,2 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: Cursor magnético

Un cursor propio de punto y aro que se imanta a botones y enlaces, se convierte en lupa invertida sobre los titulares y en una etiqueta «Ver proyecto» sobre las imágenes: la web parece responder a la mano antes de que hagas clic.

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 web de una sola página para **Estudio Salitre**, un estudio ficticio de dirección de arte y fotografía de Valmont, construida para lucir un **cursor magnético** hecho a mano (JS vanilla + CSS, sin librerías). El cursor tiene cuatro estados y todos se interpolan con muelles físicos, nunca con transiciones CSS:

1. **Reposo**: un punto de 6 px que va pegado al puntero y un aro de 38 px con borde de 1,5 px que lo sigue con un muelle (ligero retraso y un pequeño rebote). Cuando el aro va rápido se estira en la dirección del movimiento (hasta un 30 %).
2. **Imán** (botones, enlaces, chips y el sello redondo, marcados con `data-magnet`): al entrar en una zona que rodea al elemento (12–40 px más allá de su borde), el elemento se desplaza hacia el puntero (30 % de la distancia a su centro, o el valor de `data-magnet`), su contenido interior se desplaza un 50 % extra, y el aro abandona el puntero, se pega al elemento y **cambia de forma** hasta envolverlo con 6 px de holgura y su mismo radio (píldora, círculo o píldora de texto). A la vez se rellena por completo. Como el aro usa `mix-blend-mode: difference` con el color del fondo, el botón se ve **en negativo**: un botón tinta pasa a claro con texto oscuro y un halo negro de 6 px alrededor. El punto desaparece (escala 0). Al salir, el elemento vuelve a su sitio con un rebote elástico.
3. **Lupa** (las tres líneas del titular, marcadas con `data-cursor="lens"`): el aro crece a un círculo relleno de 150 px que invierte lo que tiene debajo: el fondo se vuelve negro, la tinta clara y la palabra en color acento, su complementario (turquesa).
4. **Etiqueta** (las cuatro fotos de proyectos, marcadas con `data-cursor="label"` y `data-cursor-text`): el aro se encoge y se oculta; aparece un disco de tinta de 112 px, **sin** mezcla, con el texto del proyecto («Abrir serie», «Ver proyecto», «Ver campaña», «Ver piezas») y una flecha diagonal. Entra escalando desde 0 con muelle y se inclina hasta ±10° según la velocidad horizontal.

Además: al pulsar, el aro se encoge a 0,84; al salir del documento el cursor se desvanece; el bucle `requestAnimationFrame` **se detiene solo** cuando todo está en reposo. En pantallas táctiles el cursor no existe: las fotos muestran su etiqueta como chip fijo y, al tocar un botón imantado, se dibuja un «eco» del aro que se expande y se apaga. Un interruptor «Cursor propio» permite desactivarlo. Paleta cálida de yeso, tinta casi negra y un acento almagra; tipografía editorial Instrument Serif con Geist.

## Librerías y esqueleto

- Sin librerías JS. Todo es HTML, CSS y un único `<script>` clásico al final del `<body>` (una IIFE).
- Fuentes de Google (tienen ñ, tildes, ¿ y ¡): **Instrument Serif** (regular e itálica) para display y **Geist** 400/500/600 para texto.
- Sin imágenes: las «fotos» son composiciones CSS en duotono con grano SVG `feTurbulence` en línea.

Cabecera literal del documento:

```html
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Salitre — Cursor magnético</title>
<meta name="description" content="Estudio Salitre: dirección de arte y fotografía en Valmont. Demo de cursor magnético.">
<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=Geist:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
<style>
  /* aquí va el :root de «Tokens de diseño» y después todo el CSS de «Código clave» */
</style>
</head>
<body>
  <!-- aquí va el HTML de «Estructura y contenido» -->
  <script>
    /* aquí va el JS de «Código clave» */
  </script>
</body>
</html>
```

## Tokens de diseño

```css
:root{
  --fondo:#ece6dc;
  --tinta:#16140f;
  --acento:#a63f1e;
  --tinta-suave:#4a453c;
  --linea:color-mix(in srgb, var(--tinta) 16%, transparent);
  --cursor:var(--fondo);
  --f-display:"Instrument Serif", Georgia, "Times New Roman", serif;
  --f-texto:"Geist", system-ui, -apple-system, "Segoe UI", sans-serif;
  --ease-out:cubic-bezier(.16,1,.3,1);
  --ease-io:cubic-bezier(.65,0,.35,1);
  --gutter:clamp(16px, 3.9vw, 56px);
  --aro:38px;
  --grano:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}
```

Notas:
- `--cursor` es el color del fondo: con `mix-blend-mode: difference`, el aro sobre el fondo da negro puro y sobre la tinta da un tono casi igual al fondo. Así el negativo siempre casa con la paleta.
- El resto de tonos derivados de la tinta se escriben con `color-mix(in srgb, var(--tinta) N%, transparent)` (líneas 16 %, chips 40 %, interruptor apagado 35 %, rótulos de foto 62 %).
- Contraste: tinta sobre fondo ≈ 16:1; `--tinta-suave` sobre fondo ≈ 7,7:1; el acento sobre fondo ≈ 5:1 (se usa solo en titulares grandes y en el punto de la pista).

## Estructura y contenido

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

- `header.portada#inicio` (alto mínimo `100svh`, rejilla de 3 filas: cabecera / cuerpo / pie)
  - `.cabecera`: marca «Salitre» en Instrument Serif itálica 30 px + «ESTUDIO» (12 px, versalitas espaciadas). Nav: «Trabajo», «Estudio», «Diario», «Contacto» y botón píldora tinta «Hablemos →». Todos imantados.
  - `.portada-cuerpo`: antetítulo «DIRECCIÓN DE ARTE Y FOTOGRAFÍA · Valmont» con un guion de 28 px delante; `h1` en tres líneas escalonadas: «Imágenes» (sin sangría) / «que *atraen*» (sangría 15vw, «atraen» en itálica color acento) / «la mirada.» (sangría 30vw). Cada línea es lupa.
  - `.portada-pie`: sello redondo de 168 px (tinta, texto circular «DIRECCIÓN DE ARTE · FOTOGRAFÍA · CAMPAÑAS ·» girando, flecha ↓ en el centro), colocado en absoluto con su centro al 77,7 % del ancho y al 64 % del alto de la portada; bloque de entrada con párrafo y dos botones («Ver trabajos →» tinta, «Pedir presupuesto» con borde); columna derecha con coordenadas «40°00′ N · 10°00′ E», pista con punto que late («Mueve el ratón: los botones te buscan» / en táctil «En táctil, toca un botón y verás su eco») e interruptor «Cursor propio».
- `main`
  - `section.seccion#trabajo`: cabecera con antetítulo «01 — TRABAJO SELECCIONADO», `h2` «Lo último que / hemos mirado» y enlace subrayado «Ver archivo completo ↗» a la derecha, línea inferior. Rejilla de 12 columnas con 4 obras:
    1. «Cal y sombra» — Casa Alhelí · Arquitectura · 2025 — foto vertical 4:5 (5 columnas), rótulo «Foto: patio blanco junto al mar», etiqueta «Abrir serie».
    2. «Mar de fondo» — Conservas La Lonja · Campaña · 2026 — foto 4:3 (7 columnas, bajada 8vw), rótulo «Foto: horizonte en La Malagueta», etiqueta «Ver proyecto».
    3. «Hora dorada» — Bodega Sierra Alta · Etiqueta y campaña · 2024 — foto 4:3 (7 columnas), rótulo «Foto: viñedo en el valle de Alder», etiqueta «Ver campaña».
    4. «Barro lento» — Cerámica Tres Lunas · Producto · 2025 — foto vertical 4:5 (5 columnas, bajada 8vw), rótulo «Foto: botijo sobre mármol», etiqueta «Ver piezas».
  - `section.contacto#contacto`: antetítulo «02 — CONTACTO»; `h2` «¿Hacemos algo / que *atraiga*?» con párrafo a la derecha; «CUÉNTANOS QUÉ NECESITAS» y cinco chips conmutables (`aria-pressed`): «Campaña» (activo), «Fotografía de producto», «Arquitectura e interiores», «Vídeo corto», «Dirección de arte»; correo gigante «hola@salitre.com» imantado con fuerza 0,12; tres columnas: Teléfono («+00 000 000 000»), Estudio («Barrio del Puerto, Valmont» / «Lunes a viernes, de 9:00 a 18:00»), Redes (Instagram, Vimeo, LinkedIn en pestaña nueva); pie «© 2026 Estudio Salitre · Valmont» y «Sesiones desde 1.200 € IVA incl.».
- Fuera de todo, al final del `body`: los cuatro elementos del cursor (`.c-aro` con `.c-relleno` dentro, `.c-punto`, `.c-etiqueta`, `.c-eco`), todos `aria-hidden="true"`. Tienen que ser hijos directos de `body` para que `mix-blend-mode` mezcle con la página.

HTML literal del `body` (sin el `<script>`). En el `href` del teléfono escribe el número sin espacios:

```html
<header class="portada" id="inicio">
  <div class="cabecera">
    <a class="marca" href="#inicio" data-magnet="0.25" aria-label="Salitre, estudio. Inicio"><span class="mag-in"><b>Salitre</b><small>estudio</small></span></a>
    <nav class="nav" aria-label="Principal">
      <ul>
        <li><a href="#trabajo" data-magnet><span class="mag-in">Trabajo</span></a></li>
        <li><a href="#contacto" data-magnet><span class="mag-in">Estudio</span></a></li>
        <li><a href="#trabajo" data-magnet><span class="mag-in">Diario</span></a></li>
        <li><a href="#contacto" data-magnet><span class="mag-in">Contacto</span></a></li>
      </ul>
      <a class="btn btn-lleno btn-peq" href="#contacto" data-magnet><span class="mag-in">Hablemos <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></span></a>
    </nav>
  </div>

  <div class="portada-cuerpo">
    <p class="antetitulo">Dirección de arte y fotografía · Valmont</p>
    <h1 class="titular">
      <span class="l"><span data-cursor="lens">Imágenes</span></span>
      <span class="l l2"><span data-cursor="lens">que <em>atraen</em></span></span>
      <span class="l l3"><span data-cursor="lens">la mirada.</span></span>
    </h1>
  </div>

  <div class="portada-pie">
    <a class="sello" href="#trabajo" data-magnet="0.4" aria-label="Bajar a los trabajos">
      <svg class="sello-texto" viewBox="0 0 200 200" aria-hidden="true"><defs><path id="circulo" d="M100,100 m-76,0 a76,76 0 1,1 152,0 a76,76 0 1,1 -152,0"/></defs><text><textPath href="#circulo" textLength="474" lengthAdjust="spacing">DIRECCIÓN DE ARTE · FOTOGRAFÍA · CAMPAÑAS ·</textPath></text></svg>
      <span class="mag-in sello-flecha"><svg viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M17 7v20M9 19l8 8 8-8"/></svg></span>
    </a>
    <div class="entrada">
      <p>Campañas, producto y arquitectura para marcas con carácter que prefieren mirar despacio. Trabajamos desde Valmont para todo el mundo.</p>
      <div class="acciones">
        <a class="btn btn-lleno" href="#trabajo" data-magnet><span class="mag-in">Ver trabajos <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></span></a>
        <a class="btn btn-linea" href="#contacto" data-magnet><span class="mag-in">Pedir presupuesto</span></a>
      </div>
    </div>
    <div class="meta">
      <span class="coordenadas">40°00′ N · 10°00′ E</span>
      <span class="pista"><i aria-hidden="true"></i><span class="raton">Mueve el ratón: los botones te buscan</span><span class="tactil">En táctil, toca un botón y verás su eco</span></span>
      <button class="interruptor" type="button" aria-pressed="true" id="interruptor">Cursor propio <span class="pildora" aria-hidden="true"></span></button>
    </div>
  </div>
</header>

<main>
  <section class="seccion" id="trabajo" aria-labelledby="t-trabajo">
    <div class="seccion-cab">
      <div>
        <p class="antetitulo">01 — Trabajo seleccionado</p>
        <h2 id="t-trabajo">Lo último que<br>hemos mirado</h2>
      </div>
      <a class="enlace-flecha" href="#" data-magnet><span class="mag-in">Ver archivo completo <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M4 10l6-6M5 4h5v5"/></svg></span></a>
    </div>

    <div class="obras">
      <a class="obra f-cal" href="#" data-cursor="label" data-cursor-text="Abrir serie">
        <div class="marco" aria-hidden="true"><div class="img"><div class="arco"></div></div><span class="rotulo">Foto: patio blanco junto al mar</span><span class="chip-tactil">Abrir serie <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 10l6-6M5 4h5v5"/></svg></span></div>
        <div class="pie-obra"><h3>Cal y sombra</h3><p>Casa Alhelí<br>Arquitectura · <span class="n">2025</span></p></div>
      </a>
      <a class="obra f-mar" href="#" data-cursor="label" data-cursor-text="Ver proyecto">
        <div class="marco" aria-hidden="true"><div class="img"></div><span class="rotulo">Foto: horizonte en La Malagueta</span><span class="chip-tactil">Ver proyecto <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 10l6-6M5 4h5v5"/></svg></span></div>
        <div class="pie-obra"><h3>Mar de fondo</h3><p>Conservas La Lonja<br>Campaña · <span class="n">2026</span></p></div>
      </a>
      <a class="obra f-hora" href="#" data-cursor="label" data-cursor-text="Ver campaña">
        <div class="marco" aria-hidden="true"><div class="img"></div><span class="rotulo">Foto: viñedo en el valle de Alder</span><span class="chip-tactil">Ver campaña <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 10l6-6M5 4h5v5"/></svg></span></div>
        <div class="pie-obra"><h3>Hora dorada</h3><p>Bodega Sierra Alta<br>Etiqueta y campaña · <span class="n">2024</span></p></div>
      </a>
      <a class="obra f-barro" href="#" data-cursor="label" data-cursor-text="Ver piezas">
        <div class="marco" aria-hidden="true"><div class="img"><div class="vasija"></div></div><span class="rotulo">Foto: botijo sobre mármol</span><span class="chip-tactil">Ver piezas <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 10l6-6M5 4h5v5"/></svg></span></div>
        <div class="pie-obra"><h3>Barro lento</h3><p>Cerámica Tres Lunas<br>Producto · <span class="n">2025</span></p></div>
      </a>
    </div>
  </section>

  <section class="contacto" id="contacto" aria-labelledby="t-contacto">
    <p class="antetitulo">02 — Contacto</p>
    <div class="contacto-cab">
      <h2 id="t-contacto">¿Hacemos algo<br>que <em>atraiga</em>?</h2>
      <p>Respondemos en un día laborable. Si ya tienes fecha, dínosla: reservamos las sesiones con tres semanas de margen.</p>
    </div>
    <p class="temas-tit" id="t-temas">Cuéntanos qué necesitas</p>
    <div class="temas" role="group" aria-labelledby="t-temas">
      <button class="tema" type="button" aria-pressed="true" data-magnet><span class="mag-in">Campaña</span></button>
      <button class="tema" type="button" aria-pressed="false" data-magnet><span class="mag-in">Fotografía de producto</span></button>
      <button class="tema" type="button" aria-pressed="false" data-magnet><span class="mag-in">Arquitectura e interiores</span></button>
      <button class="tema" type="button" aria-pressed="false" data-magnet><span class="mag-in">Vídeo corto</span></button>
      <button class="tema" type="button" aria-pressed="false" data-magnet><span class="mag-in">Dirección de arte</span></button>
    </div>
    <a class="correo" href="mailto:hola@salitre.com" data-magnet="0.12"><span class="mag-in">hola@salitre.com</span></a>
    <div class="datos">
      <div><h3>Teléfono</h3><p><a href="tel:+00 000 000 000" data-magnet><span class="mag-in">+00 000 000 000</span></a></p></div>
      <div><h3>Estudio</h3><p>Barrio del Puerto, Valmont<br>Lunes a viernes, de 9:00 a 18:00</p></div>
      <div><h3>Redes</h3><p class="redes">
        <a href="https://www.instagram.com/" target="_blank" rel="noopener" data-magnet><span class="mag-in">Instagram</span></a>
        <a href="https://vimeo.com/" target="_blank" rel="noopener" data-magnet><span class="mag-in">Vimeo</span></a>
        <a href="https://www.linkedin.com/" target="_blank" rel="noopener" data-magnet><span class="mag-in">LinkedIn</span></a>
      </p></div>
    </div>
    <footer class="final"><span>© 2026 Estudio Salitre · Valmont</span><span>Sesiones desde 1.200 € IVA incl.</span></footer>
  </section>
</main>

<div class="c-aro" aria-hidden="true"><div class="c-relleno"></div></div>
<div class="c-punto" aria-hidden="true"></div>
<div class="c-etiqueta" aria-hidden="true"><div><span>Ver proyecto</span><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 10l6-6M5 4h5v5"/></svg></div></div>
<div class="c-eco" aria-hidden="true"></div>
```

## Movimiento

| Elemento | Propiedad | De → a | Duración / física | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Líneas del `h1` (span interior) | `transform` | `translateY(105%)` → `none` (la línea tiene `overflow:hidden`) | 1,15 s | `cubic-bezier(.16,1,.3,1)` | 0 / 0,09 / 0,18 s | carga |
| `.cabecera` | `opacity` | 0 → 1 | 0,9 s | `cubic-bezier(.16,1,.3,1)` | 0,1 s | carga |
| `.antetitulo`, `.portada-pie`, `.sello` | `opacity` | 0 → 1 | 0,9 s | `cubic-bezier(.16,1,.3,1)` | 0,35 s | carga |
| Texto circular del sello | `rotate` | 0 → 360° | 24 s, infinito | `linear` | — | siempre (se pausa fuera de pantalla) |
| Punto de la pista | `scale` + `opacity` | 1 → 0,55 → 1 / 1 → 0,5 → 1 | 2,2 s, infinito | `cubic-bezier(.65,0,.35,1)` | — | siempre |
| Foto de obra (`.img`) | `scale` | 1 → 1,045 | 1,1 s | `cubic-bezier(.16,1,.3,1)` | — | `:hover` |
| Aro: posición x/y | muelle | puntero o centro del imán | k = 340, c = 30 | física | — | puntero |
| Aro: ancho, alto, radio, relleno, escala del punto, presión | muelle | según estado | k = 260, c = 25 | física | — | cambio de estado |
| Elemento imantado | `translate3d` | 0 → (puntero − centro) × fuerza | k = 170, c = 13 (rebota) | física | — | entrar/salir de la zona |
| Contenido `.mag-in` | `translate3d` | desplazamiento del imán × 0,5 (extra) | igual que el imán | — | — | imán |
| Etiqueta | `scale` | 0 → 1 | k = 320, c = 24 | física | — | foto con `data-cursor="label"` |
| Etiqueta | `rotate` | vx × 0,012, limitado a ±10° | continuo | — | — | velocidad del aro |
| Aro en reposo | estirado | `scale(1+s, 1−0,55·s)` orientado, s = min(|v|/2600, 0,3) | continuo | — | — | velocidad |
| Aro | escala de presión | 1 → 0,84 → 1 | muelle k = 260, c = 25 | física | — | `pointerdown` / `pointerup` |
| Aro, punto, etiqueta | `opacity` | 0 ↔ 1 | 0,25 s (etiqueta 0,2 s) | `linear` | — | entrar/salir del documento |
| Eco táctil | `scale` + `opacity` | 1 → 1,35 / 1 → 0 | 0,6 s | `cubic-bezier(.16,1,.3,1)` | — | toque en imán |
| Interruptor (bolita) | `translateX` | 16 px ↔ 0 | 0,35 s | `cubic-bezier(.16,1,.3,1)` | — | clic |
| Chips de temas | fondo y color | contorno ↔ tinta | 0,3 s | `ease` | — | clic |

Valores del estado del aro (objetivos de los muelles):

| Estado | Centro | Ancho × alto | Radio | Relleno | Punto | Etiqueta |
|---|---|---|---|---|---|---|
| Reposo | puntero | 38 × 38 | 19 | 0 | 1 | 0 |
| Imán | centro del elemento + su desplazamiento actual | (w + 12) × (h + 12) | radio del elemento + 6 | 1 | 0 | 0 |
| Lupa | puntero | 150 × 150 (o `data-cursor-size`) | 75 | 1 | 0 | 0 |
| Etiqueta | puntero | 12 × 12 (y aro oculto por opacidad) | 6 | 0 | 0 | 1 |

## Código clave

CSS completo (va justo después del bloque `:root`):

```css
*,*::before,*::after{box-sizing:border-box}
html{background:var(--fondo);-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion: no-preference){html{scroll-behavior:smooth}}
body{margin:0;background:var(--fondo);color:var(--tinta);font:400 16px/1.55 var(--f-texto);-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
em{font-style:italic}
:focus-visible{outline:2px solid var(--acento);outline-offset:4px;border-radius:4px}

/* ——— Cursor propio ——— */
html.cursor-on, html.cursor-on *{cursor:none!important}
.c-aro,.c-punto,.c-etiqueta,.c-eco{position:fixed;left:0;top:0;pointer-events:none;z-index:100;opacity:0;will-change:transform}
.c-aro{width:var(--aro);height:var(--aro);border:1.5px solid var(--cursor);border-radius:50%;mix-blend-mode:difference;transition:opacity .25s linear}
.c-relleno{position:absolute;inset:-1.5px;border-radius:inherit;background:var(--cursor);opacity:0}
.c-punto{width:6px;height:6px;border-radius:50%;background:var(--cursor);mix-blend-mode:difference;transition:opacity .25s linear}
.c-etiqueta{width:112px;height:112px;margin:-56px 0 0 -56px;border-radius:50%;background:var(--tinta);color:var(--fondo);display:grid;place-items:center;text-align:center;font:500 13px/1.15 var(--f-texto);letter-spacing:.01em;transition:opacity .2s linear}
.c-etiqueta span{display:block;max-width:80px}
.c-etiqueta svg{display:block;margin:6px auto 0;width:14px;height:14px}
html.cursor-visible .c-aro, html.cursor-visible .c-punto, html.cursor-visible .c-etiqueta{opacity:1}
html.modo-etiqueta .c-aro{opacity:0!important}
.c-eco{border:1.5px solid var(--cursor);mix-blend-mode:difference}
.c-eco.activo{animation:eco .6s var(--ease-out) forwards}
@keyframes eco{0%{opacity:1;transform:var(--t) scale(1)}100%{opacity:0;transform:var(--t) scale(1.35)}}

/* ——— Cabecera ——— */
.cabecera{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:24px var(--gutter)}
.marca{display:flex;align-items:baseline;gap:10px;padding:6px 2px}
.marca b{font:400 30px/1 var(--f-display);font-style:italic;letter-spacing:-.01em}
.marca small{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--tinta-suave)}
.nav{display:flex;align-items:center;gap:6px}
.nav ul{display:flex;gap:2px;margin:0 18px 0 0;padding:0;list-style:none}
.nav ul a{display:inline-block;padding:10px 14px;font-size:15px;font-weight:500}
.mag-in{display:inline-flex;align-items:center;gap:10px;will-change:transform}

/* ——— Botones ——— */
.btn{display:inline-flex;align-items:center;height:52px;padding:0 26px;border-radius:999px;font-size:15px;font-weight:500;white-space:nowrap;will-change:transform}
.btn svg{width:16px;height:16px;flex:none}
.btn-lleno{background:var(--tinta);color:var(--fondo)}
.btn-linea{box-shadow:inset 0 0 0 1.5px var(--tinta)}
.btn-peq{height:44px;padding:0 20px}

/* ——— Portada ——— */
.portada{position:relative;min-height:100svh;display:grid;grid-template-rows:auto 1fr auto}
.portada-cuerpo{padding:clamp(8px,2svh,28px) var(--gutter) 0;display:flex;flex-direction:column;justify-content:center}
.antetitulo{display:flex;align-items:center;gap:12px;margin:0 0 clamp(14px,2.4svh,28px);font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--tinta-suave)}
.antetitulo::before{content:"";width:28px;height:1.5px;background:currentColor}
.titular{margin:0;font:400 clamp(3.4rem,10.4vw,10.5rem)/.9 var(--f-display);letter-spacing:-.025em}
.titular .l{display:block;overflow:hidden;padding:.02em 0 .1em;margin-bottom:-.1em}
.titular .l > span{display:inline-block}
.titular .l2{padding-left:15vw}
.titular .l3{padding-left:30vw}
.titular em{color:var(--acento);padding-right:.04em}
.sello{position:absolute;left:calc(77.7% - 84px);top:calc(64% - 84px);width:168px;height:168px;border-radius:50%;background:var(--tinta);color:var(--fondo);display:grid;place-items:center;will-change:transform}
.sello-texto{position:absolute;inset:0;width:100%;height:100%;animation:giro 24s linear infinite}
.sello-texto text{font:500 12.5px var(--f-texto);letter-spacing:.2em;fill:currentColor}
.sello-flecha svg{width:34px;height:34px}
@keyframes giro{to{transform:rotate(360deg)}}
.portada-pie{display:flex;align-items:flex-end;justify-content:space-between;gap:32px;padding:clamp(20px,4svh,44px) var(--gutter) clamp(22px,4svh,40px)}
.entrada{max-width:440px}
.entrada p{margin:0 0 22px;font-size:17px;color:var(--tinta-suave)}
.acciones{display:flex;flex-wrap:wrap;gap:10px}
.meta{display:flex;flex-direction:column;align-items:flex-end;gap:12px;text-align:right;font-size:13px;color:var(--tinta-suave)}
.coordenadas{font-variant-numeric:tabular-nums;letter-spacing:.06em}
.pista{display:flex;align-items:center;gap:10px}
.pista i{width:8px;height:8px;border-radius:50%;background:var(--acento);animation:latido 2.2s var(--ease-io) infinite}
@keyframes latido{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(.55);opacity:.5}}
.pista .tactil{display:none}
.interruptor{display:inline-flex;align-items:center;gap:10px;padding:8px 4px 8px 12px;font-size:13px;font-weight:500;color:var(--tinta)}
.interruptor .pildora{position:relative;width:38px;height:22px;border-radius:99px;background:var(--tinta);transition:background .3s var(--ease-io)}
.interruptor .pildora::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--fondo);transform:translateX(16px);transition:transform .35s var(--ease-out)}
.interruptor[aria-pressed="false"] .pildora{background:color-mix(in srgb, var(--tinta) 35%, transparent)}
.interruptor[aria-pressed="false"] .pildora::after{transform:none}

/* ——— Trabajo ——— */
.seccion{padding:clamp(72px,12svh,140px) var(--gutter)}
.seccion-cab{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;padding-bottom:28px;margin-bottom:clamp(28px,5svh,56px);border-bottom:1px solid var(--linea)}
.seccion-cab .antetitulo{margin-bottom:14px}
.seccion h2{margin:0;font:400 clamp(2.6rem,6vw,5.6rem)/.95 var(--f-display);letter-spacing:-.02em}
.enlace-flecha{display:inline-flex;align-items:center;padding:10px 4px;font-size:15px;font-weight:500;border-bottom:1.5px solid currentColor;will-change:transform}
.enlace-flecha svg{width:14px;height:14px}
.obras{display:grid;grid-template-columns:repeat(12,1fr);align-items:start;gap:clamp(28px,4vw,56px) clamp(16px,2.2vw,32px)}
.obra{display:block;grid-column:span 7}
.obra:nth-child(1),.obra:nth-child(4){grid-column:span 5}
.obra:nth-child(2),.obra:nth-child(4){margin-top:clamp(32px,8vw,120px)}
.marco{position:relative;aspect-ratio:4/3;overflow:hidden;border-radius:4px;isolation:isolate}
.obra:nth-child(1) .marco,.obra:nth-child(4) .marco{aspect-ratio:4/5}
.img{position:absolute;inset:0;transition:transform 1.1s var(--ease-out)}
.marco::after{content:"";position:absolute;inset:0;background:var(--grano);opacity:.38;mix-blend-mode:multiply;pointer-events:none}
@media (hover:hover){.obra:hover .img{transform:scale(1.045)}}
.rotulo{position:absolute;left:14px;bottom:14px;z-index:2;padding:5px 10px;border-radius:99px;background:color-mix(in srgb, var(--tinta) 62%, transparent);color:#f3eee6;font-size:11.5px;letter-spacing:.04em}
.chip-tactil{position:absolute;right:14px;top:14px;z-index:2;display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:99px;background:var(--tinta);color:var(--fondo);font-size:12.5px;font-weight:500;opacity:0;transition:opacity .3s}
.chip-tactil svg{width:11px;height:11px}
html:not(.cursor-on) .chip-tactil{opacity:1}
.pie-obra{display:flex;justify-content:space-between;align-items:baseline;gap:16px;padding-top:16px}
.pie-obra h3{margin:0;font:400 clamp(1.6rem,2.2vw,2.1rem)/1.05 var(--f-display)}
.pie-obra p{margin:0;font-size:13.5px;color:var(--tinta-suave);text-align:right}
.pie-obra .n{font-variant-numeric:tabular-nums}

/* Fotos procedurales (duotono + grano) */
.f-mar .img{background:
  radial-gradient(circle at 63% 49%, #f6e3c8 0 7%, rgba(246,227,200,.0) 7.4%),
  radial-gradient(120% 70% at 63% 52%, rgba(247,206,160,.75), rgba(247,206,160,0) 60%),
  linear-gradient(#e8c29a 0%, #d98b5e 52%, #2e4957 52.2%, #1a2932 100%)}
.f-mar .img::before{content:"";position:absolute;left:0;right:0;top:52.2%;bottom:0;background:repeating-linear-gradient(180deg, rgba(246,220,190,.22) 0 1px, transparent 1px 9px);-webkit-mask:radial-gradient(40% 60% at 63% 0, #000, transparent);mask:radial-gradient(40% 60% at 63% 0, #000, transparent)}
.f-cal .img{background:linear-gradient(118deg, #f1ebe1 0 52%, #d9cdbb 52% 100%)}
.f-cal .arco{position:absolute;left:30%;width:40%;top:22%;bottom:0;border-radius:999px 999px 0 0;background:linear-gradient(180deg,#43342a,#2a211b)}
.f-cal .arco::after{content:"";position:absolute;inset:auto 0 0 0;height:22%;background:linear-gradient(118deg, rgba(241,235,225,.18) 0 40%, transparent 40%)}
.f-cal .img::after{content:"";position:absolute;left:0;right:0;bottom:0;height:9%;background:#b7a58d}
.f-barro .img{background:linear-gradient(105deg, rgba(255,226,196,0) 30%, rgba(255,226,196,.16) 30% 52%, rgba(255,226,196,0) 52%) 0 0/100% 66% no-repeat, linear-gradient(#b8664a 0 66%, #e3d8ca 66% 100%)}
.f-barro .vasija{position:absolute;left:27%;width:46%;top:34%;height:36%;border-radius:50% 50% 44% 44% / 64% 64% 36% 36%;background:radial-gradient(60% 55% at 36% 34%, #e6a883, #a4512f 55%, #6f3019)}
.f-barro .vasija::before{content:"";position:absolute;left:39%;width:22%;top:-15%;height:20%;border-radius:6px 6px 2px 2px;background:linear-gradient(90deg,#8b4024,#c0714c 45%,#7a361e)}
.f-barro .vasija::after{content:"";position:absolute;left:8%;right:8%;bottom:-10%;height:12%;border-radius:50%;background:rgba(60,26,14,.35);filter:blur(6px);z-index:-1}
.f-hora .img{background:
  radial-gradient(circle at 28% 40%, #fbe1a6 0 6%, rgba(251,225,166,0) 6.4%),
  radial-gradient(140% 90% at 30% 110%, #4f5536 0 48%, transparent 48.3%),
  radial-gradient(120% 80% at 88% 100%, #6e7447 0 50%, transparent 50.3%),
  linear-gradient(#f0c677, #e0914c 70%, #c77a3e)}
.f-hora .img::before{content:"";position:absolute;inset:55% 0 0 0;background:repeating-linear-gradient(100deg, rgba(20,22,10,.22) 0 2px, transparent 2px 22px);-webkit-mask:linear-gradient(transparent, #000 40%);mask:linear-gradient(transparent, #000 40%)}

/* ——— Contacto ——— */
.contacto{padding:clamp(72px,12svh,140px) var(--gutter) 28px;border-top:1px solid var(--linea)}
.contacto-cab{display:flex;justify-content:space-between;align-items:flex-end;gap:32px;margin-bottom:40px}
.contacto-cab > p{max-width:340px;margin:0 0 10px;font-size:16px;color:var(--tinta-suave)}
.contacto h2{margin:0;font:400 clamp(2.8rem,7.4vw,7rem)/.95 var(--f-display);letter-spacing:-.02em}
.contacto h2 em{color:var(--acento)}
.temas-tit{margin:0 0 12px;font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--tinta-suave)}
.temas{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:clamp(40px,8svh,88px)}
.tema{height:42px;padding:0 18px;border-radius:999px;box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--tinta) 40%, transparent);font-size:14.5px;font-weight:500;transition:background .3s,color .3s;will-change:transform}
.tema[aria-pressed="true"]{background:var(--tinta);color:var(--fondo);box-shadow:none}
.correo{display:inline-block;font:400 clamp(2.4rem,8.6vw,8.4rem)/1 var(--f-display);letter-spacing:-.03em;padding:.06em .02em .12em;word-break:break-word;will-change:transform}
.datos{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;margin-top:clamp(36px,6svh,64px);padding-top:28px;border-top:1px solid var(--linea)}
.datos h3{margin:0 0 8px;font-size:12px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--tinta-suave)}
.datos p{margin:0;font-size:16px}
.datos a{display:inline-block;padding:4px 0;font-weight:500;will-change:transform}
.redes{display:flex;flex-wrap:wrap;gap:4px 18px}
.final{display:flex;justify-content:space-between;gap:16px;margin-top:clamp(48px,9svh,96px);font-size:13px;color:var(--tinta-suave)}

/* ——— Entrada ——— */
@media (prefers-reduced-motion: no-preference){
  .titular .l > span{animation:sube 1.15s var(--ease-out) both}
  .titular .l2 > span{animation-delay:.09s}
  .titular .l3 > span{animation-delay:.18s}
  .antetitulo,.portada-pie,.sello,.cabecera{animation:aparece .9s var(--ease-out) .35s both}
  .cabecera{animation-delay:.1s}
}
@keyframes sube{from{transform:translateY(105%)}to{transform:none}}
@keyframes aparece{from{opacity:0}to{opacity:1}}

/* ——— Táctil ——— */
@media (hover:none), (pointer:coarse){
  .pista .raton{display:none}
  .pista .tactil{display:inline}
  .interruptor{display:none}
}

/* ——— Tablet ——— */
@media (max-width:1024px){
  .nav ul{display:none}
  .sello{width:140px;height:140px;left:auto;right:var(--gutter);top:auto;bottom:calc(100% + 24px)}
  .portada-pie{position:relative}
  .titular .l3{padding-left:22vw}
  .titular{font-size:13vw}
}
/* ——— Móvil ——— */
@media (max-width:720px){
  .cabecera{padding-top:16px;padding-bottom:16px}
  .marca small{display:none}
  .portada-cuerpo{justify-content:flex-start;padding-top:9svh}
  .titular{font-size:clamp(3.2rem,17.5vw,5.4rem)}
  .correo{font-size:12.4vw}
  .contacto-cab{flex-direction:column;align-items:flex-start;gap:20px}
  .titular .l2{padding-left:9vw}
  .titular .l3{padding-left:18vw}
  .portada-pie{display:grid;grid-template-columns:1fr auto;align-items:end;gap:28px 16px}
  .entrada{grid-column:1 / -1}
  .sello{position:relative;inset:auto;grid-column:2;grid-row:2;width:112px;height:112px}
  .sello-texto text{font-size:13px}
  .sello-flecha svg{width:26px;height:26px}
  .meta{grid-column:1;grid-row:2;align-items:flex-start;text-align:left}
  .interruptor{padding-left:0}
  .entrada p{font-size:16px}
  .btn{height:50px;padding:0 22px}
  .obras{grid-template-columns:1fr;gap:40px}
  .obra,.obra:nth-child(n){grid-column:auto;margin-top:0}
  .obra:nth-child(1) .marco,.obra:nth-child(4) .marco{aspect-ratio:4/3}
  .seccion-cab{flex-direction:column;align-items:flex-start}
  .datos{grid-template-columns:1fr}
  .final{flex-direction:column}
}
@media (max-width:380px){.acciones .btn{flex:1 1 auto;justify-content:center}}
@media (prefers-reduced-motion: reduce){
  .sello-texto,.pista i{animation:none}
  .img{transition:none}
}
```

JS completo (un `<script>` clásico al final del `body`):

```js
(() => {
  const ETIQUETA_POR_DEFECTO = "Ver proyecto";
  const AJUSTES = {
    aro: 38,          // diámetro del aro en reposo (px)
    lupa: 150,        // diámetro del aro sobre titulares (px)
    margen: 6,        // holgura del aro alrededor del elemento imantado (px)
    fuerza: 0.3,      // cuánto sigue el elemento al puntero (0–1)
    fuerzaInterior: 0.5, // el contenido se mueve un 50 % extra
    muelleAro: { k: 340, c: 30 },
    muelleForma: { k: 260, c: 25 },
    muelleIman: { k: 170, c: 13 },
    muelleEtiqueta: { k: 320, c: 24 }
  };

  const html = document.documentElement;
  const fino = matchMedia("(hover: hover) and (pointer: fine)");
  const reducido = matchMedia("(prefers-reduced-motion: reduce)");
  const aro = document.querySelector(".c-aro");
  const relleno = document.querySelector(".c-relleno");
  const punto = document.querySelector(".c-punto");
  const etiqueta = document.querySelector(".c-etiqueta");
  const etiquetaTexto = etiqueta.querySelector("span");
  const eco = document.querySelector(".c-eco");
  const interruptor = document.getElementById("interruptor");

  class Muelle {
    constructor(v, p) { this.x = v; this.t = v; this.v = 0; this.k = p.k; this.c = p.c; }
    paso(dt) {
      if (reducido.matches) { this.x = this.t; this.v = 0; return 0; }
      const a = this.k * (this.t - this.x) - this.c * this.v;
      this.v += a * dt;
      this.x += this.v * dt;
      return Math.abs(this.t - this.x) + Math.abs(this.v) * 0.01;
    }
    fijar(v) { this.x = this.t = v; this.v = 0; }
  }

  const A = AJUSTES;
  const ax = new Muelle(0, A.muelleAro), ay = new Muelle(0, A.muelleAro);
  const aw = new Muelle(A.aro, A.muelleForma), ah = new Muelle(A.aro, A.muelleForma);
  const ar = new Muelle(A.aro / 2, A.muelleForma), af = new Muelle(0, A.muelleForma);
  const ap = new Muelle(1, A.muelleForma), ps = new Muelle(1, A.muelleForma);
  const es = new Muelle(0, A.muelleEtiqueta);
  const muelles = [ax, ay, aw, ah, ar, af, ap, ps, es];

  const imanes = [...document.querySelectorAll("[data-magnet]")].map((el) => ({
    el,
    interior: el.querySelector(".mag-in"),
    fuerza: parseFloat(el.dataset.magnet) || A.fuerza,
    tx: new Muelle(0, A.muelleIman),
    ty: new Muelle(0, A.muelleIman),
    r: null
  }));

  let activo = false, visible = false, px = -100, py = -100, primera = true;
  let estado = { modo: "reposo" }, sucio = true, elegir = true, corriendo = false, ultimo = 0;

  // Radio real del elemento en px (acepta "999px" y "50%").
  function radioDe(el, ancho) {
    const r = getComputedStyle(el).borderTopLeftRadius;
    return r.endsWith("%") ? (parseFloat(r) / 100) * ancho : parseFloat(r) || 0;
  }

  function medir() {
    for (const m of imanes) {
      const b = m.el.getBoundingClientRect();
      const x = b.left - m.tx.x, y = b.top - m.ty.x;
      const rad = radioDe(m.el, b.width);
      m.r = {
        x, y, w: b.width, h: b.height, cx: x + b.width / 2, cy: y + b.height / 2,
        radio: rad > 0 ? Math.min(rad, b.height / 2) : b.height / 2,
        zona: Math.max(12, Math.min(40, Math.min(b.width, b.height) * 0.35)),
        vivo: b.width > 0 && b.height > 0
      };
    }
    sucio = false;
  }

  function buscar() {
    let mejor = null, dMejor = Infinity;
    for (const m of imanes) {
      const r = m.r;
      if (!r || !r.vivo) continue;
      const dx = Math.max(r.x - r.zona - px, 0, px - (r.x + r.w + r.zona));
      const dy = Math.max(r.y - r.zona - py, 0, py - (r.y + r.h + r.zona));
      if (dx === 0 && dy === 0) {
        const d = Math.hypot(px - r.cx, py - r.cy);
        if (d < dMejor) { dMejor = d; mejor = m; }
      }
    }
    if (mejor) return { modo: "iman", m: mejor };
    const el = document.elementFromPoint(px, py)?.closest("[data-cursor]");
    if (el?.dataset.cursor === "label") return { modo: "etiqueta", texto: el.dataset.cursorText || ETIQUETA_POR_DEFECTO };
    if (el?.dataset.cursor === "lens") return { modo: "lupa", d: parseFloat(el.dataset.cursorSize) || A.lupa };
    return { modo: "reposo" };
  }

  function despertar() {
    if (!corriendo && activo && !document.hidden) { corriendo = true; ultimo = performance.now(); requestAnimationFrame(cuadro); }
  }

  function cuadro(ahora) {
    const dt = Math.min((ahora - ultimo) / 1000, 1 / 30) || 1 / 60;
    ultimo = ahora;
    if (sucio) medir();
    if (elegir) {
      const nuevo = buscar();
      if (nuevo.modo === "etiqueta" && estado.texto !== nuevo.texto) etiquetaTexto.textContent = nuevo.texto;
      estado = nuevo;
      elegir = false;
      html.classList.toggle("modo-etiqueta", estado.modo === "etiqueta");
    }
    const quieto = reducido.matches;
    let energia = 0;

    for (const m of imanes) {
      const on = estado.modo === "iman" && estado.m === m && !quieto;
      m.tx.t = on ? (px - m.r.cx) * m.fuerza : 0;
      m.ty.t = on ? (py - m.r.cy) * m.fuerza : 0;
      energia += m.tx.paso(dt) + m.ty.paso(dt);
      const x = m.tx.x, y = m.ty.x;
      if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01 && !on) {
        if (m.el.style.transform) { m.el.style.transform = ""; if (m.interior) m.interior.style.transform = ""; }
      } else {
        m.el.style.transform = `translate3d(${x.toFixed(2)}px,${y.toFixed(2)}px,0)`;
        if (m.interior) m.interior.style.transform = `translate3d(${(x * A.fuerzaInterior).toFixed(2)}px,${(y * A.fuerzaInterior).toFixed(2)}px,0)`;
      }
    }

    if (estado.modo === "iman") {
      const m = estado.m, r = m.r, g = A.margen;
      ax.t = r.cx + m.tx.x; ay.t = r.cy + m.ty.x;
      aw.t = r.w + g * 2; ah.t = r.h + g * 2; ar.t = r.radio + g; af.t = 1; ap.t = 0; es.t = 0;
    } else if (estado.modo === "lupa") {
      ax.t = px; ay.t = py; aw.t = ah.t = estado.d; ar.t = estado.d / 2; af.t = 1; ap.t = 0; es.t = 0;
    } else if (estado.modo === "etiqueta") {
      ax.t = px; ay.t = py; aw.t = ah.t = 12; ar.t = 6; af.t = 0; ap.t = 0; es.t = 1;
    } else {
      ax.t = px; ay.t = py; aw.t = ah.t = A.aro; ar.t = A.aro / 2; af.t = 0; ap.t = 1; es.t = 0;
    }
    for (const s of muelles) energia += s.paso(dt);

    // Estirado según la velocidad (solo en reposo)
    let estira = 0, ang = 0;
    if (estado.modo === "reposo" && !quieto) {
      const v = Math.hypot(ax.v, ay.v);
      estira = Math.min(v / 2600, 0.3);
      ang = Math.atan2(ay.v, ax.v) * 57.2958;
    }
    const w = Math.max(aw.x, 0), h = Math.max(ah.x, 0);
    aro.style.width = w.toFixed(2) + "px";
    aro.style.height = h.toFixed(2) + "px";
    aro.style.borderRadius = Math.max(ar.x, 0).toFixed(2) + "px";
    aro.style.transform = `translate3d(${(ax.x - w / 2).toFixed(2)}px,${(ay.x - h / 2).toFixed(2)}px,0) rotate(${ang.toFixed(1)}deg) scale(${((1 + estira) * ps.x).toFixed(3)},${((1 - estira * 0.55) * ps.x).toFixed(3)}) rotate(${(-ang).toFixed(1)}deg)`;
    relleno.style.opacity = Math.min(Math.max(af.x, 0), 1).toFixed(3);
    const sp = Math.min(Math.max(ap.x, 0), 1.2);
    punto.style.transform = `translate3d(${px - 3}px,${py - 3}px,0) scale(${sp.toFixed(3)})`;
    const inclina = quieto ? 0 : Math.max(-10, Math.min(10, ax.v * 0.012));
    etiqueta.style.transform = `translate3d(${ax.x.toFixed(2)}px,${ay.x.toFixed(2)}px,0) rotate(${inclina.toFixed(2)}deg) scale(${Math.max(es.x, 0).toFixed(3)})`;

    if (energia > 0.02 || elegir) requestAnimationFrame(cuadro);
    else corriendo = false;
  }

  function mostrar(v) { visible = v; html.classList.toggle("cursor-visible", v && activo); }

  function alMover(e) {
    if (!activo || e.pointerType === "touch") return;
    px = e.clientX; py = e.clientY;
    if (primera) { ax.fijar(px); ay.fijar(py); primera = false; }
    if (!visible) mostrar(true);
    elegir = true;
    despertar();
  }

  function soltarImanes() {
    for (const m of imanes) { m.tx.fijar(0); m.ty.fijar(0); m.el.style.transform = ""; if (m.interior) m.interior.style.transform = ""; }
  }

  function activar(on) {
    activo = on;
    html.classList.toggle("cursor-on", on);
    interruptor.setAttribute("aria-pressed", String(on));
    if (!on) { mostrar(false); soltarImanes(); estado = { modo: "reposo" }; html.classList.remove("modo-etiqueta"); primera = true; }
    else { sucio = true; }
  }

  addEventListener("pointermove", alMover, { passive: true });
  addEventListener("pointerdown", (e) => {
    if (activo && e.pointerType !== "touch") { ps.t = 0.84; despertar(); }
    else if (e.pointerType === "touch" && !reducido.matches) {
      const el = e.target.closest("[data-magnet]");
      if (!el) return;
      const b = el.getBoundingClientRect(), g = 6;
      const rad = radioDe(el, b.width);
      eco.style.width = b.width + g * 2 + "px";
      eco.style.height = b.height + g * 2 + "px";
      eco.style.borderRadius = (rad > 0 ? Math.min(rad, b.height / 2) : b.height / 2) + g + "px";
      eco.style.setProperty("--t", `translate3d(${b.left - g}px,${b.top - g}px,0)`);
      eco.classList.remove("activo"); void eco.offsetWidth; eco.classList.add("activo");
    }
  }, { passive: true });
  addEventListener("pointerup", () => { ps.t = 1; despertar(); }, { passive: true });
  document.addEventListener("mouseout", (e) => { if (!e.relatedTarget) mostrar(false); });
  document.addEventListener("mouseover", () => { if (activo && !primera) mostrar(true); });
  addEventListener("blur", () => mostrar(false));
  addEventListener("scroll", () => { sucio = true; elegir = true; despertar(); }, { passive: true });
  addEventListener("resize", () => { sucio = true; despertar(); });
  document.addEventListener("visibilitychange", () => { if (!document.hidden) despertar(); });
  if (document.fonts) document.fonts.ready.then(() => { sucio = true; });

  interruptor.addEventListener("click", () => activar(!activo));
  fino.addEventListener("change", () => activar(fino.matches));
  activar(fino.matches);

  // Demo: los enlaces vacíos no navegan; los temas se marcan y desmarcan.
  document.querySelectorAll('a[href="#"]').forEach((a) => a.addEventListener("click", (e) => e.preventDefault()));
  document.querySelectorAll(".tema").forEach((b) => b.addEventListener("click", () => b.setAttribute("aria-pressed", String(b.getAttribute("aria-pressed") !== "true"))));

  // El sello solo gira cuando se ve.
  const sello = document.querySelector(".sello-texto");
  new IntersectionObserver(([en]) => { sello.style.animationPlayState = en.isIntersecting ? "running" : "paused"; }).observe(sello);
})();
```

Detalles del código que no se pueden cambiar:
- El muelle es un Euler semiimplícito: `a = k·(t − x) − c·v; v += a·dt; x += v·dt`, con `dt` limitado a 1/30 s. Con movimiento reducido, `x = t` al instante.
- Los rectángulos de los imanes se miden **restando su desplazamiento actual** (`b.left − m.tx.x`), para que el imán no se persiga a sí mismo. Se vuelven a medir en `scroll`, `resize` y cuando cargan las fuentes.
- La zona de captura de cada imán es `clamp(12, min(w, h) · 0,35, 40)` px alrededor de su caja; si hay varios, gana el de centro más cercano. Los imanes tienen prioridad sobre la lupa y la etiqueta, que se detectan con `document.elementFromPoint(...).closest("[data-cursor]")`.
- El radio de un imán se lee de `getComputedStyle(...).borderTopLeftRadius`, admitiendo `%` (el sello tiene `50%`); si es 0 (enlaces de texto), se usa `alto / 2` para que el aro sea una píldora.
- En `scroll` se vuelve a elegir estado aunque el ratón no se mueva: al hacer scroll con el puntero quieto, el cursor reacciona a lo que pasa por debajo.
- El bucle solo corre mientras la suma de energía (distancias + velocidades) supera 0,02; en reposo no hay ningún `requestAnimationFrame` pendiente.

## Interacción

- **Puntero fino** (`(hover: hover) and (pointer: fine)`): se añade `html.cursor-on`, que oculta el cursor nativo en todo el documento. El cursor aparece con el primer `pointermove` (el aro nace ya en el puntero, sin volar desde 0,0) y se oculta al salir del documento (`mouseout` sin `relatedTarget`) o al perder el foco la ventana.
- **Imán**: botones de la cabecera, «Ver trabajos», «Pedir presupuesto», sello (fuerza 0,4), marca (0,25), «Ver archivo completo», chips, correo (0,12), teléfono y redes.
- **Lupa**: las tres líneas del titular.
- **Etiqueta**: las cuatro obras; el texto sale de `data-cursor-text` y, si falta, de «Ver proyecto».
- **Clic**: el aro se comprime a 0,84 mientras se mantiene pulsado. Los enlaces `href="#"` de la demo no navegan (`preventDefault`); los chips alternan `aria-pressed`.
- **Scroll**: sin librerías; `scroll-behavior: smooth` salvo con movimiento reducido.
- **Teclado**: todos los controles son `a` o `button` nativos con `:focus-visible` (contorno de 2 px color acento, separación 4 px). El cursor no interfiere con el foco.
- **Táctil** (`hover: none` o `pointer: coarse`): no se crea el cursor ni se mueve nada. Las fotos enseñan su etiqueta como chip tinta arriba a la derecha (`html:not(.cursor-on) .chip-tactil { opacity: 1 }`). Al tocar un elemento imantado se coloca `.c-eco` sobre su caja (6 px de holgura, su mismo radio) y se reinicia su animación de 0,6 s. La pista cambia a «En táctil, toca un botón y verás su eco» y el interruptor se oculta.
- **Interruptor «Cursor propio»**: alterna el cursor; al apagarlo se devuelve el cursor nativo, se sueltan los imanes y aparecen los chips de las fotos.
- Si cambia el tipo de puntero (se conecta un ratón a una tableta), `matchMedia(...).addEventListener("change")` activa o desactiva el cursor.

## Adaptación a móvil y tablet

- **> 1024 px**: todo como se describe; el sello flota a la derecha del titular.
- **≤ 1024 px**: se oculta la lista del nav (quedan marca y «Hablemos»); el titular pasa a `13vw` y la tercera línea a sangría 22vw; el sello mide 140 px y se coloca a la derecha, justo encima del pie de portada (`bottom: calc(100% + 24px)`).
- **≤ 720 px**: cabecera con 16 px de relleno vertical y sin «ESTUDIO»; el cuerpo de portada arranca arriba con `padding-top: 9svh`; titular `clamp(3.2rem, 17.5vw, 5.4rem)` con sangrías 9vw y 18vw; el pie de portada es una rejilla de 2 columnas: el texto y los botones ocupan todo el ancho y debajo quedan la meta (izquierda, alineada a la izquierda) y el sello de 112 px (derecha); obras en una columna, todas 4:3 y sin bajadas; cabeceras de sección apiladas; datos de contacto en una columna; correo a `12.4vw`; botones de 50 px de alto.
- **≤ 380 px**: los dos botones de la portada se estiran para ocupar la fila.
- Nada provoca scroll horizontal de 360 a 1920 px (`overflow-x: hidden` en `body`, `word-break` en el correo). Alturas con `svh`, nunca `100vh`.

## Accesibilidad y movimiento reducido

- HTML semántico: `header`, `nav` con `aria-label="Principal"`, `main`, secciones con `aria-labelledby`, `h1` único, `h2`/`h3` en orden, `footer`.
- Todo lo decorativo lleva `aria-hidden="true"`: los cuatro elementos del cursor, los SVG de flechas, el texto circular del sello (el enlace del sello tiene `aria-label="Bajar a los trabajos"`) y los marcos de foto con sus rótulos.
- Chips con `aria-pressed` dentro de un `role="group"` etiquetado; el interruptor es un `button` con `aria-pressed`.
- Contraste AA en todo el texto (ver tokens). Los rótulos de foto van sobre una píldora de tinta al 62 % con texto `#f3eee6`.
- `prefers-reduced-motion: reduce`: sin entrada animada del titular, sin giro del sello, sin latido de la pista, sin zoom de las fotos y sin `scroll-behavior: smooth`. En JS los muelles saltan al objetivo al instante, los elementos imantados **no se desplazan**, el aro no se estira y la etiqueta no se inclina: el cursor sigue cambiando de forma (imán, lupa, etiqueta), pero sin movimiento añadido. El eco táctil tampoco se dibuja.

## Rendimiento

- Sin WebGL (DPR y fallback no aplican). Solo se animan `transform` y `opacity`; el aro cambia `width`/`height`/`border-radius`, que en un elemento fijo sin hijos es barato.
- El bucle `requestAnimationFrame` arranca con `despertar()` en `pointermove`, `pointerdown`, `pointerup`, `scroll` y `resize`, y se para cuando la energía baja de 0,02. No corre con la pestaña oculta ni con el cursor desactivado.
- Medición de cajas solo cuando algo lo invalida (`sucio = true`), al principio del fotograma, antes de escribir estilos (sin *layout thrashing*).
- La animación CSS del sello se pausa con un `IntersectionObserver` cuando sale de pantalla.
- `will-change: transform` solo en el cursor y en los elementos imantados. Un solo archivo de unos 35 KB.
- Funciona dentro de un `iframe`: nada de `window.top`, `alert` ni almacenamiento.

## Personalización

- **Marca y datos**: `Salitre`, `Valmont`, `hola@salitre.com` y `+00 000 000 000` aparecen en título, marca, antetítulo, párrafo de entrada, contacto y pie. Si cambias de ciudad, actualiza también las coordenadas «40°00′ N · 10°00′ E», el barrio «Barrio del Puerto» y, si quieres, los rótulos de las fotos.
- **Colores**: cambia solo `--fondo` (#ece6dc), `--tinta` (#16140f) y `--acento` (#a63f1e); todo lo demás deriva de ellos con `color-mix`. El cursor usa el color del fondo con `difference`, así que el negativo se recalcula solo. Con un fondo oscuro la pieza funciona igual: el aro se verá claro sobre el fondo.
- **Texto de la etiqueta**: `Ver proyecto` es el valor por defecto; cada foto puede llevar el suyo en `data-cursor-text`.
- **Sensación del imán**: `AJUSTES.fuerza` (0,3) para todos, o `data-magnet="0.4"` por elemento; `AJUSTES.margen` (6 px) para la holgura; `muelleIman` más rígido (k 240, c 20) para un imán seco, más blando (k 120, c 9) para uno gomoso.
- **Tamaños**: `AJUSTES.aro` (38) y `AJUSTES.lupa` (150); una lupa distinta por elemento con `data-cursor-size="200"`.
- **Reutilizar en otra web**: copia los cuatro elementos `.c-*`, el bloque CSS «Cursor propio» y el script; marca tus elementos con `data-magnet`, `data-cursor="lens"` o `data-cursor="label"` + `data-cursor-text`.

## Comprobación final

1. Con ratón, el cursor nativo no se ve en ningún sitio y el aro aparece en el puntero al primer movimiento, sin viajar desde la esquina.
2. El aro sigue al punto con retraso y rebote leve, y se estira en la dirección del movimiento al ir rápido.
3. Al acercarte a «Ver trabajos», el botón se desplaza hacia el puntero, su texto un poco más, y el aro lo envuelve como una píldora rellena que lo muestra en negativo con un halo negro de 6 px.
4. Sobre el sello, el aro es un círculo perfecto (no un cuadrado redondeado) y el sello se mueve con fuerza 0,4.
5. Al salir de un imán, el elemento vuelve a su sitio con un rebote visible.
6. Sobre el titular aparece una lupa de 150 px que vuelve negro el fondo y turquesa la palabra «atraen».
7. Sobre cada foto aparece un disco tinta de 112 px con su texto («Abrir serie», «Ver proyecto», «Ver campaña», «Ver piezas»), que entra escalando y se inclina al moverlo; la foto hace un zoom lento a 1,045.
8. Haciendo scroll con la rueda y el ratón quieto, el cursor cambia de estado según lo que pasa por debajo.
9. Con el puntero quieto, no queda ningún `requestAnimationFrame` en marcha.
10. En móvil real no hay cursor, las fotos muestran su chip de etiqueta y tocar un botón dibuja el eco.
11. El interruptor «Cursor propio» apaga y enciende el efecto y devuelve el cursor nativo.
12. Con movimiento reducido no hay entradas, giros ni desplazamientos de botones, y todo es legible desde el primer fotograma.
13. Se navega todo con Tab y el foco se ve (contorno acento de 2 px).
14. Sin scroll horizontal a 360, 390, 768, 1024, 1440 y 1920 px, y cero errores en consola.

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 Cursor magnético
Prompt CompletoFiel a la demo9,8/10

Prácticamente un calco: el archivo coincide con el original salvo dos líneas en blanco, así que maquetación, lupa invertida, etiqueta, imán con muelles y versión móvil salen idénticos.

Lo que generó la IA con el prompt Lite de Cursor magnético
Prompt LiteAceptable6,4/10

El cursor funciona como en el original (muelles, imán, lupa que vuelve turquesa «atraen», etiqueta sobre las fotos) y la portada se parece mucho, pero el sello cae encima del texto de la pista, las fotos son otras composiciones con otros rótulos y en contacto faltan el párrafo, el rótulo «Cuéntanos qué necesitas» y el chip activo.

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:
    • «Haz el imán más fuerte y más gomoso»
    • «Que el aro no rellene los botones, solo los rodee»
    • «Pasa la web a fondo negro con tinta blanca»
    • «Cambia el texto del cursor sobre las fotos por «Ver caso»»
  4. 04

    Publica

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

Guías por herramientaClaudeChatGPTGeminiCursorLovableBoltv0

Licencia en claro

Qué puedes hacer con esta pieza

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

Puedes

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

No puedes

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

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

Preguntas frecuentes

Lo que suele preguntarse

¿Puedo usar el cursor en mi web sin copiar toda la página?
Sí. Copia los cuatro elementos .c-*, el bloque CSS «Cursor propio» y el script, y marca tus elementos con data-magnet, data-cursor="lens" o data-cursor="label" con data-cursor-text.
¿Qué pasa en el móvil?
El cursor no se crea en pantallas táctiles. Las fotos muestran su etiqueta como chip fijo y, al tocar un botón imantado, aparece un eco del aro. Si se conecta un ratón a una tableta, el cursor se activa solo.
¿Por qué el aro se ve turquesa sobre la palabra en color?
Es mix-blend-mode: difference: el aro usa el color del fondo y resta colores, así que el acento almagra se vuelve su complementario. Si no te gusta, quita el data-cursor="lens" del titular.
¿Afecta al rendimiento o a la accesibilidad?
Solo anima transform y opacity, y el bucle se para en reposo. El cursor es decorativo (aria-hidden), no toca el foco de teclado, respeta el movimiento reducido y se puede apagar con el interruptor «Cursor propio».
¿Qué diferencia hay entre el prompt Completo y el Lite?
El Completo (≈15154 tokens) describe la pieza con todo detalle y es el que mejor resultado da en Claude, ChatGPT, Gemini o Cursor. El Lite (≈2651 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.