Saltar al contenido
motionframe

Buscar piezas

Categorías

La pieza

Qué hace y dónde encaja

Un cupón de invitación con perforación y un disco central rodeado de un anillo de texto que gira: al pulsar, el relleno verde nace en el punto exacto del clic, el check se dibuja trazo a trazo, el anillo da un tirón y cambia a «Copiado», las letras del código saltan en cascada y estallan chispas. Debajo, tres variantes del mismo gesto: un botón cuadrado donde el icono sube y el color entra desde abajo, una píldora cuya etiqueta rueda y cuyo ancho se adapta con muelle, y un botón redondo con tooltip que cambia. Cada copia lanza un aviso en una pila con muelle, suma al contador del cupón y escribe el valor en un campo para probar el pegado. Si nadie toca nada, un cursor fantasma lo demuestra. Encaja en documentación, paneles de desarrolladores, códigos de invitación y enlaces para compartir; cambia el verde, el negro o los textos para tu marca.

Prompt
≈13.853 tokens · con Lite
Librerías
ninguna
Demo
34,4 KB · ligero
Prueba en frío
pendiente

Antes de copiar

Tu marca, dentro del prompt

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

Ver 2 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.415 palabras · ≈13.853 tokens
Lite
1.649 palabras · ≈2.960 tokens
Librerías
ninguna
Peso de la demo
34,4 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: Copiar con confirmación

Un cupón de invitación negro con un disco central de texto giratorio y tres variantes de botón de copiar (cuadrado, píldora, redondo), todos con check dibujado, etiqueta que rueda, relleno verde ácido y avisos apilados con muelle; la sensación es de precisión y certeza: «ya está copiado».

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

## Qué es

Una microinteracción a pantalla completa (mínimo 100svh) sobre negro puro con una rejilla de 12 columnas apenas visible. Dos columnas en escritorio (5fr / 7fr), una en móvil.

- **Columna izquierda**: marca arriba, titular enorme en grotesca extra-negrita («Un clic.» / «Cero dudas.», la palabra de acento en color vivo), un párrafo corto y, abajo, un campo de texto «Prueba el pegado» donde se escribe el valor copiado.
- **Columna derecha**: un **cupón** (tarjeta con borde fino, esquinas de 30 px y dos muescas semicirculares en una perforación discontinua) con el código de invitación en monoespaciada grande a la izquierda y, a la derecha, el **disco**: un botón circular de 104 px rodeado de un anillo de texto que gira. Debajo, tres filas con la misma acción en otras formas:
  1. **Comando**: botón cuadrado de 56 px; el icono sube y el color entra desde abajo.
  2. **Enlace**: botón píldora; la etiqueta rueda de «Copiar enlace» a «Enlace copiado» y el ancho se adapta con muelle.
  3. **Clave de API**: botón redondo de 56 px con tooltip que cambia de «Copiar» a «Copiado».
- **Avisos** (toasts) apilados abajo (centrados en móvil, a la derecha en escritorio) con un check dibujado, el texto copiado y una barra de tiempo.
- **Cursor fantasma**: si nadie interactúa, un cursor dibujado recorre los cuatro botones y los «pulsa» (solo visual, no toca el portapapeles).

Lo que ocurre al copiar, todo a la vez: el relleno de acento nace en el punto del clic, el icono de las dos hojas se encoge y gira mientras el **check se dibuja** trazo a trazo, la etiqueta rueda, entra un aviso nuevo en la pila, el contador «Copiado N veces» suma con un giro de cifra, una luz verde se expande por la escena y el valor se teclea en el campo de pegado. El disco además da un tirón al anillo (que pasa a decir «COPIADO»), lanza 14 chispas, una onda y hace saltar las letras del código en cascada.

## Librerías y esqueleto

Ninguna librería. Esqueleto del `<head>`, exacto:

```html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Copiar con confirmación</title>
<meta name="description" content="Botones de copiar con check dibujado, etiqueta que rueda, anillo giratorio y avisos apilados con muelle.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,100..900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style> … hoja de estilos (ver «Código clave») … </style>
</head>
<body> … estructura (ver «Código clave») … <script> … script (ver «Código clave») … </script></body>
</html>
```

Tipografías: **Archivo** (variable en ancho `wdth 62–125` y peso; para titular, marca, botones y texto) y **JetBrains Mono** (código, etiquetas, avisos; mayúsculas con tracking .08em en las etiquetas). Las dos tienen ñ, tildes, ¿ y ¡. Sin cursivas serif ni letra manuscrita.

## Tokens de diseño

Blanco y negro con **un** color vivo (verde ácido); sin pasteles. Bloque `:root` literal:

```css
:root{
  --negro:#0a0a0a;
  --papel:#f4f4f0;
  --acento:#c8ff2e;
  --gris:#8f8f8a;
  --linea:rgba(244,244,240,.16);
  --f-display:"Archivo",system-ui,sans-serif;
  --f-mono:"JetBrains Mono",ui-monospace,monospace;
  --ease-out:cubic-bezier(.16,1,.3,1);
  --muelle:linear(0, 0.055, 0.191, 0.366, 0.55, 0.722, 0.866, 0.978, 1.056, 1.103, 1.124, 1.125, 1.113, 1.093, 1.07, 1.047, 1.027, 1.01, 0.998, 0.99, 0.986, 0.984, 0.985, 0.987, 0.989, 0.992, 0.995, 0.998, 0.999, 1.001, 1.002, 1.002, 1.002, 1.002, 1);
  --margen:clamp(20px,3.6vw,56px);
}
```

`--muelle` es un muelle amortiguado (ζ = 0,55; ω₀ = 11 rad/s; sobrepasa un 12 %) muestreado en 35 puntos con `linear()`. Úsalo para todo lo que «rebota»: botones al pulsar, ancho de la píldora, avisos, tooltip.

Escala tipográfica: titular `clamp(46px,7vw,124px)`, peso 800, `font-stretch:88%`, interlínea .9, tracking −.045em. Código del cupón `clamp(24px,3.15vw,48px)` mono 600. Etiquetas 11,5 px mono 500 mayúsculas. Valores de fila `clamp(15px,1.3vw,19px)` mono 500. Texto del párrafo `clamp(16px,1.3vw,19px)`, color `--gris`.

## Estructura y contenido

Textos literales (castellano neutro):

- Marca: «Nodo» con un punto de acento delante. Etiqueta arriba a la derecha: «Micro 06 · Copiar con confirmación» (se oculta bajo 620 px).
- Titular: «Un clic.» / «Cero dudas.».
- Párrafo: «Quien copia necesita saber que **ya está hecho**: el check se dibuja, la etiqueta rueda y un aviso lo confirma.»
- Campo: etiqueta «Prueba el pegado», placeholder «Pega aquí con Ctrl+V», botón «Borrar» (solo visible con texto).
- Cupón: «Código de invitación», el código «NODO-7K2Q-91XD», pie «Válido 7 días · 1 uso» y «Copiado N veces» (singular «vez» cuando N = 1; N en acento).
- Anillo: «COPIAR CÓDIGO · COPIAR CÓDIGO · » y, al copiar, «COPIADO · COPIADO · COPIADO · ».
- Filas: «01 · Comando» → «$ npm i @nodo/ui»; «02 · Enlace» → «nodo.app/i/7k2q» con botón «Copiar enlace» / «Enlace copiado»; «03 · Clave de API» → «nd_test_5f21c9a0b7c1» mostrada enmascarada (primeros 8 caracteres, ocho puntos «••••••••» en gris y los 4 últimos).
- Avisos: «Código copiado», «Comando copiado», «Enlace copiado», «Clave copiada», con el valor debajo en mono (la clave, enmascarada). Si falla la copia: «No se pudo copiar» / «Selecciona el texto y usa Ctrl+C».

El código completo del HTML, el CSS y el JS está en «Código clave»: reprodúcelo tal cual.

## Movimiento

| Elemento | Propiedad | De → a | Duración | Easing | Retardo | Disparador |
|---|---|---|---|---|---|---|
| Barra superior | opacity | 0 → 1 | 800 ms | ease | 100 ms | carga |
| Líneas del titular | translateY (con máscara overflow) | 112 % → 0 | 1100 ms | `--ease-out` | 150 / 300 ms | carga |
| Párrafo | opacity | 0 → 1 | 1000 ms | ease | 700 ms | carga |
| Cupón | opacity, translateY | 0, 22 px → 1, 0 | 1000 ms | `--ease-out` | 350 ms | carga |
| Filas 1·2·3 | opacity, translateY | 0, 22 px → 1, 0 | 900 ms | `--ease-out` | 550 / 650 / 750 ms | carga |
| Campo de pegado | opacity, translateY | 0, 22 px → 1, 0 | 900 ms | `--ease-out` | 900 ms | carga |
| Anillo del disco | rotate (JS) | 14 °/s base · 70 °/s con hover · +900 °/s de impulso al copiar | velocidad suavizada `1−e^(−6·dt)`; el impulso decae `e^(−2,6·dt)` | — | — | continuo |
| Botón disco | scale | 1 → 1,07 (hover) · 0,93 (pulsado) | 500 ms (120 ms al pulsar) | `--muelle` | — | puntero |
| Hojas del icono | translate | atrás −1,6 px, delante +1,6 px | 500 ms | `--muelle` | — | hover |
| Relleno del disco | clip-path circle | `0px` → `90px` desde el punto del clic | 700 ms | `--ease-out` | — | copiar |
| Icono copiar | scale, rotate, opacity | 1 → .3, −30°, 0 | 500 ms | `--muelle` | — | copiar |
| Check | stroke-dashoffset | 1 → 0 (path con `pathLength=1`) | 550 ms | `--ease-out` | 160 ms | copiar |
| Onda | scale, opacity | 1, .9 → 1,9, 0 | 850 ms | `--ease-out` | — | copiar (disco, redondo) |
| Chispas (14) | translateX, scaleX, opacity | radio 56–62 px → 92–114 px | 850 ms | `--ease-out` | — | copiar (disco) |
| Letras del código | translateY, color | 0 → −22 % (al 38 %) → 0; color → acento → heredado | 640 ms | `--ease-out` | 30 ms × índice | copiar (disco) |
| Luz de escena | scale, opacity | .15 → 1; 1 → 0 | 1100 ms | `--ease-out` | — | copiar |
| Cifra del contador | translateY, opacity | 0 → −70 %; al volver 70 % → 0 | 140 ms + 420 ms | ease-in / `--ease-out` | — | copiar |
| Botón cuadrado | relleno translateY, check translateY, icono translateY | 101 % → 0; 160 % → 0; 0 → −160 % | 550 ms | `--ease-out` | — | copiar |
| Píldora | ancho, relleno scaleX (origen izquierda), etiqueta translateY ±120 % | ancho medido en JS | 700 / 600 / 550 ms | `--muelle` / `--ease-out` | — | copiar |
| Botón redondo | fondo, tooltip (opacity, translateY, rueda de texto ±130 %) | negro → acento | 400 / 450 ms | `--muelle` | — | copiar |
| Aviso (entrada) | opacity, translateY, scale | 0, 34 px, .9 → 1, 0, 1 | 700 ms | `--muelle` | — | copiar |
| Check del aviso | stroke-dashoffset | 1 → 0 | 480 ms | `--ease-out` | 180 ms | copiar |
| Barra del aviso | scaleX | 1 → 0 | 2800 ms | linear | — | copiar (pausa con hover) |
| Aviso (salida) | opacity, translateY, scale | 1 → 0, 12 px, .95 | 260 ms | ease-in | — | tras 2800 ms |
| Avisos anteriores | FLIP translateY; scale y opacity en pila | 2.º: .96 / .82 · 3.º: .92 / .55 | 650 ms | `--muelle` | — | nuevo aviso |
| Cursor fantasma | translate | de botón a botón | 800 ms | `cubic-bezier(.65,0,.25,1)` | — | automático |

Todos los botones vuelven solos a reposo 2400 ms después de confirmar (`DURACION_OK`).

## Código clave

### HTML (cuerpo)

```html
<main class="escena" id="escena">
  <header class="barra">
    <span class="marca"><i aria-hidden="true"></i>Nodo</span>
    <span class="mono">Micro 06 · Copiar con confirmación</span>
  </header>

  <section class="izq" aria-label="Presentación">
    <div>
      <h1 class="titular"><span class="linea"><span>Un clic.</span></span><span class="linea"><span>Cero <em>dudas.</em></span></span></h1>
      <p class="intro">Quien copia necesita saber que <b>ya está hecho</b>: el check se dibuja, la etiqueta rueda y un aviso lo confirma.</p>
    </div>
    <div class="pegar" id="pegar">
      <label class="mono" for="prueba">Prueba el pegado</label>
      <div class="pegar__campo" id="campo">
        <input id="prueba" type="text" placeholder="Pega aquí con Ctrl+V" autocomplete="off" spellcheck="false">
        <button class="borrar" type="button" id="borrar">Borrar</button>
      </div>
    </div>
  </section>

  <section class="der" aria-label="Botones de copiar">
    <article class="cupon">
      <div class="cupon__datos">
        <span class="mono">Código de invitación</span>
        <div class="codigo" id="codigo" aria-label="NODO-7K2Q-91XD">NODO-7K2Q-91XD</div>
        <div class="cupon__pie mono">
          <span style="color:var(--gris)">Válido 7 días · 1 uso</span>
          <span class="cuenta">Copiado <span class="cuenta__n" id="cuenta">0</span> <span id="veces">veces</span></span>
        </div>
      </div>
      <div class="cupon__lado">
        <div class="disco" id="disco">
          <svg class="disco__anillo" id="anillo" viewBox="0 0 176 176" aria-hidden="true">
            <defs><path id="circ" d="M 88 88 m -74 0 a 74 74 0 1 1 148 0 a 74 74 0 1 1 -148 0"/></defs>
            <text textLength="464" lengthAdjust="spacing"><textPath href="#circ" id="rotulo">COPIAR CÓDIGO · COPIAR CÓDIGO · </textPath></text>
          </svg>
          <span class="chispas" id="chispas" aria-hidden="true"></span>
          <span class="onda" id="onda" aria-hidden="true"></span>
          <button class="disco__btn" type="button" data-copiar data-tipo="disco" data-valor="NODO-7K2Q-91XD" data-aviso="Código copiado" data-detalle="NODO-7K2Q-91XD" aria-label="Copiar código de invitación">
            <span class="disco__fill" aria-hidden="true"></span>
            <span class="ico" aria-hidden="true">
              <svg class="copia" viewBox="0 0 24 24"><rect class="atras" x="9" y="9" width="11" height="11" rx="2.6"/><rect class="frente" x="4" y="4" width="11" height="11" rx="2.6"/></svg>
              <svg class="check" viewBox="0 0 24 24"><path pathLength="1" d="M5 12.8l4.6 4.6L19 7"/></svg>
            </span>
          </button>
        </div>
      </div>
    </article>

    <ul class="filas">
      <li class="fila">
        <div>
          <span class="fila__tit mono">01 · Comando</span>
          <div class="fila__val"><s>$</s> npm i @nodo/ui</div>
        </div>
        <button class="mini" type="button" data-copiar data-tipo="mini" data-valor="npm i @nodo/ui" data-aviso="Comando copiado" data-detalle="npm i @nodo/ui" aria-label="Copiar comando de instalación">
          <span class="ico" aria-hidden="true">
            <svg class="copia" viewBox="0 0 24 24"><rect class="atras" x="9" y="9" width="11" height="11" rx="2.6"/><rect class="frente" x="4" y="4" width="11" height="11" rx="2.6"/></svg>
            <svg class="check" viewBox="0 0 24 24"><path pathLength="1" d="M5 12.8l4.6 4.6L19 7"/></svg>
          </span>
        </button>
      </li>
      <li class="fila">
        <div>
          <span class="fila__tit mono">02 · Enlace</span>
          <div class="fila__val">nodo.app/i/7k2q</div>
        </div>
        <button class="pildora" type="button" data-copiar data-tipo="pildora" data-valor="https://nodo.app/i/7k2q" data-aviso="Enlace copiado" data-detalle="nodo.app/i/7k2q" aria-label="Copiar enlace de invitación">
          <span class="ico" aria-hidden="true">
            <svg class="copia" viewBox="0 0 24 24"><rect class="atras" x="9" y="9" width="11" height="11" rx="2.6"/><rect class="frente" x="4" y="4" width="11" height="11" rx="2.6"/></svg>
            <svg class="check" viewBox="0 0 24 24"><path pathLength="1" d="M5 12.8l4.6 4.6L19 7"/></svg>
          </span>
          <span class="rot" aria-hidden="true"><span>Copiar enlace</span><span>Enlace copiado</span></span>
        </button>
      </li>
      <li class="fila">
        <div>
          <span class="fila__tit mono">03 · Clave de API</span>
          <div class="fila__val">nd_test_<s>••••••••</s>b7c1</div>
        </div>
        <button class="redondo" type="button" data-copiar data-tipo="redondo" data-valor="nd_test_5f21c9a0b7c1" data-aviso="Clave copiada" data-detalle="nd_test_••••••••b7c1" aria-label="Copiar clave de API">
          <span class="tip" aria-hidden="true"><span>Copiar</span><span>Copiado</span></span>
          <span class="onda" aria-hidden="true"></span>
          <span class="ico" aria-hidden="true">
            <svg class="copia" viewBox="0 0 24 24"><rect class="atras" x="9" y="9" width="11" height="11" rx="2.6"/><rect class="frente" x="4" y="4" width="11" height="11" rx="2.6"/></svg>
            <svg class="check" viewBox="0 0 24 24"><path pathLength="1" d="M5 12.8l4.6 4.6L19 7"/></svg>
          </span>
        </button>
      </li>
    </ul>
    <div class="pegar pegar--mov" id="pegarMov"></div>
  </section>

  <div class="pulso" id="pulso" aria-hidden="true"></div>
</main>

<div class="avisos" id="avisos" role="status" aria-live="polite"></div>
<div class="fantasma" id="fantasma" aria-hidden="true"><svg width="24" height="30" viewBox="0 0 24 30"><path d="M3 2v22l5.6-5.2 3.6 8.2 4-1.7-3.6-8H20z" fill="#fff" stroke="#0a0a0a" stroke-width="1.6" stroke-linejoin="round"/></svg></div>
```

### CSS (hoja de estilos completa)

```css
:root{
  --negro:#0a0a0a;
  --papel:#f4f4f0;
  --acento:#c8ff2e;
  --gris:#8f8f8a;
  --linea:rgba(244,244,240,.16);
  --f-display:"Archivo",system-ui,sans-serif;
  --f-mono:"JetBrains Mono",ui-monospace,monospace;
  --ease-out:cubic-bezier(.16,1,.3,1);
  --muelle:linear(0, 0.055, 0.191, 0.366, 0.55, 0.722, 0.866, 0.978, 1.056, 1.103, 1.124, 1.125, 1.113, 1.093, 1.07, 1.047, 1.027, 1.01, 0.998, 0.99, 0.986, 0.984, 0.985, 0.987, 0.989, 0.992, 0.995, 0.998, 0.999, 1.001, 1.002, 1.002, 1.002, 1.002, 1);
  --margen:clamp(20px,3.6vw,56px);
}
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{background:var(--negro);color:var(--papel);font:400 16px/1.5 var(--f-display);-webkit-font-smoothing:antialiased;overflow-x:hidden}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation}
button:focus-visible,input:focus-visible{outline:2px solid var(--acento);outline-offset:4px}

.escena{position:relative;isolation:isolate;min-height:100svh;overflow:hidden;display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);column-gap:clamp(32px,5.5vw,104px);padding:calc(var(--margen) + 64px) var(--margen) var(--margen);
  background:
    linear-gradient(var(--linea),var(--linea)) 0 0/100% 1px no-repeat,
    repeating-linear-gradient(90deg,rgba(244,244,240,.045) 0 1px,transparent 1px calc(100%/12))}
.mono{font:500 11.5px/1.4 var(--f-mono);letter-spacing:.08em;text-transform:uppercase}

.barra{position:absolute;z-index:3;top:var(--margen);left:var(--margen);right:var(--margen);display:flex;justify-content:space-between;align-items:baseline;gap:16px;animation:aparece .8s .1s both}
.marca{font:800 clamp(20px,1.8vw,26px)/1 var(--f-display);font-stretch:112%;letter-spacing:-.03em;display:flex;align-items:center;gap:.5em}
.marca i{width:.62em;height:.62em;border-radius:50%;background:var(--acento);display:block}
.barra .mono{color:var(--gris)}

/* ---------- Columna izquierda ---------- */
.izq{display:flex;flex-direction:column;justify-content:space-between;gap:48px;min-width:0}
.titular{font:800 clamp(46px,7vw,124px)/.9 var(--f-display);font-stretch:88%;letter-spacing:-.045em}
.linea{display:block;overflow:hidden;padding-bottom:.1em;margin-bottom:-.1em}
.linea>span{display:block;transform:translateY(112%);animation:sube 1.1s var(--ease-out) forwards}
.linea:nth-child(1)>span{animation-delay:.15s}
.linea:nth-child(2)>span{animation-delay:.3s}
.linea em{font-style:normal;color:var(--acento)}
@keyframes sube{to{transform:none}}
@keyframes aparece{from{opacity:0}to{opacity:1}}
@keyframes entra{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}
.intro{margin-top:clamp(20px,3vh,32px);max-width:32ch;font-size:clamp(16px,1.3vw,19px);line-height:1.5;color:var(--gris);animation:aparece 1s .7s both}
.intro b{font-weight:500;color:var(--papel)}

.pegar{animation:entra .9s var(--ease-out) .9s both}
.pegar label{display:block;margin-bottom:10px;color:var(--gris)}
.pegar__campo{position:relative;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--linea);transition:border-color .3s}
.pegar__campo:focus-within,.pegar__campo.lleno{border-color:var(--acento)}
.pegar input{flex:1;min-width:0;height:56px;background:none;border:0;border-radius:0;color:var(--papel);font:500 clamp(15px,1.2vw,18px)/1 var(--f-mono);caret-color:var(--acento)}
.pegar input::placeholder{color:#6f6f6a}
.pegar input:focus-visible{outline:0}
.pegar__campo:has(input:focus-visible){outline:2px solid var(--acento);outline-offset:6px}
.borrar{font:500 11.5px/1 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--gris);padding:10px 4px;opacity:0;pointer-events:none;transition:opacity .25s,color .25s}
.lleno .borrar{opacity:1;pointer-events:auto}
.borrar:hover{color:var(--papel)}

/* ---------- Columna derecha ---------- */
.der{display:flex;flex-direction:column;justify-content:center;gap:clamp(20px,3.4vh,36px);min-width:0}

.cupon{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;border:1px solid var(--linea);border-radius:30px;animation:entra 1s var(--ease-out) .35s both}
.cupon__datos{padding:clamp(24px,2.6vw,40px);display:flex;flex-direction:column;justify-content:space-between;gap:28px;min-width:0}
.cupon__datos .mono{color:var(--gris)}
.codigo{font:600 clamp(24px,3.15vw,48px)/1 var(--f-mono);letter-spacing:-.02em;white-space:nowrap;display:flex}
.codigo span{display:inline-block;will-change:transform}
.cupon__pie{display:flex;justify-content:space-between;gap:16px;align-items:baseline;flex-wrap:wrap}
.cuenta{display:inline-flex;gap:.6em;color:var(--papel)}
.cuenta__n{display:inline-block;min-width:1.2ch;text-align:right;font-variant-numeric:tabular-nums;color:var(--acento)}
.cupon__lado{position:relative;display:grid;place-items:center;padding:clamp(18px,2vw,30px) clamp(22px,2.6vw,40px);border-left:1.5px dashed var(--linea)}
.cupon__lado::before,.cupon__lado::after{content:"";position:absolute;left:-15px;width:29px;height:29px;border-radius:50%;background:var(--negro);border:1px solid var(--linea)}
.cupon__lado::before{top:-15px;clip-path:inset(50% 0 0 0)}
.cupon__lado::after{bottom:-15px;clip-path:inset(0 0 50% 0)}

/* Disco: botón central y anillo de texto que gira */
.disco{position:relative;width:176px;height:176px;display:grid;place-items:center;flex:none}
.disco__anillo{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;will-change:transform;overflow:visible}
.disco__anillo text{font:500 11px var(--f-mono);letter-spacing:.04em;fill:var(--papel);transition:fill .4s}
.disco.ok .disco__anillo text{fill:var(--acento)}
.disco__btn{position:relative;z-index:2;width:104px;height:104px;border-radius:50%;display:grid;place-items:center;overflow:hidden;isolation:isolate;
  box-shadow:inset 0 0 0 1.5px var(--papel);transition:transform .5s var(--muelle),box-shadow .3s}
.disco__btn:hover{transform:scale(1.07)}
.disco__btn:active,.disco__btn.pulsado{transform:scale(.93);transition-duration:.12s}
.disco__fill{position:absolute;inset:0;z-index:-1;background:var(--acento);--fx:52px;--fy:52px;clip-path:circle(0px at var(--fx) var(--fy));transition:clip-path .7s var(--ease-out)}
.disco__btn.is-ok .disco__fill{clip-path:circle(90px at var(--fx) var(--fy))}
.disco__btn.is-ok{color:var(--negro);box-shadow:inset 0 0 0 1.5px var(--acento)}
.onda{position:absolute;z-index:1;left:36px;top:36px;width:104px;height:104px;border-radius:50%;border:1.5px solid var(--acento);pointer-events:none;opacity:0}
.chispas{position:absolute;z-index:1;left:50%;top:50%;width:0;height:0;pointer-events:none}
.chispas i{position:absolute;left:0;top:-1px;width:12px;height:2px;border-radius:2px;background:var(--acento);transform-origin:0 50%;opacity:0}

/* Iconos: copiar (dos hojas) y check dibujado */
.ico{position:relative;display:block;width:1em;height:1em;font-size:44px}
.ico svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.ico .copia{transition:transform .5s var(--muelle),opacity .25s;transform-origin:50% 50%}
.ico .copia rect{fill:none;stroke:currentColor;stroke-width:1.7;stroke-linejoin:round;transition:transform .5s var(--muelle)}
:is(.disco__btn,.mini,.pildora,.redondo):hover .copia .atras{transform:translate(-1.6px,-1.6px)}
:is(.disco__btn,.mini,.pildora,.redondo):hover .copia .frente{transform:translate(1.6px,1.6px)}
.ico .check{opacity:0}
.check path,.aviso__ico path{fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset .12s}
.is-ok .check{opacity:1}
.is-ok .check path{stroke-dashoffset:0;transition:stroke-dashoffset .55s .16s var(--ease-out)}
.is-ok .copia{transform:scale(.3) rotate(-30deg);opacity:0}

/* Filas con otras variantes */
.filas{list-style:none}
.fila{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px 20px;padding:clamp(14px,2.2vh,22px) 0;border-top:1px solid var(--linea)}
.fila:last-child{border-bottom:1px solid var(--linea)}
.fila:nth-child(1){animation:entra .9s var(--ease-out) .55s both}
.fila:nth-child(2){animation:entra .9s var(--ease-out) .65s both}
.fila:nth-child(3){animation:entra .9s var(--ease-out) .75s both}
.fila__tit{color:var(--gris);display:block;margin-bottom:6px}
.fila__val{font:500 clamp(15px,1.3vw,19px)/1.2 var(--f-mono);overflow-wrap:anywhere}
.fila__val s{text-decoration:none;color:var(--gris)}

/* 01 · botón cuadrado: el icono sube y el relleno entra desde abajo */
.mini{position:relative;width:56px;height:56px;border-radius:18px;display:grid;place-items:center;overflow:hidden;isolation:isolate;box-shadow:inset 0 0 0 1.5px var(--linea);transition:box-shadow .3s,transform .5s var(--muelle)}
.mini:hover{box-shadow:inset 0 0 0 1.5px var(--papel)}
.mini:active,.mini.pulsado{transform:scale(.92);transition-duration:.12s}
.mini .ico{font-size:24px}
.mini::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--acento);transform:translateY(101%);transition:transform .55s var(--ease-out)}
.mini .check{transform:translateY(160%);transition:transform .55s var(--ease-out),opacity .2s}
.mini .copia{transition:transform .55s var(--ease-out),opacity .3s}
.mini.is-ok{color:var(--negro);box-shadow:inset 0 0 0 1.5px var(--acento)}
.mini.is-ok::before{transform:none}
.mini.is-ok .check{transform:none}
.mini.is-ok .copia{transform:translateY(-160%);opacity:1}

/* 02 · píldora: la etiqueta rueda y el ancho se adapta con muelle */
.pildora{position:relative;height:56px;padding:0 24px;border-radius:999px;display:inline-flex;align-items:center;gap:10px;overflow:hidden;isolation:isolate;white-space:nowrap;
  font:600 16px/1 var(--f-display);letter-spacing:-.005em;box-shadow:inset 0 0 0 1.5px var(--papel);transition:width .7s var(--muelle),color .3s,box-shadow .3s,transform .5s var(--muelle)}
.pildora:active,.pildora.pulsado{transform:scale(.95);transition-duration:.12s,.3s,.3s,.12s}
.pildora::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--acento);border-radius:inherit;transform:scaleX(0);transform-origin:0 50%;transition:transform .6s var(--ease-out)}
.pildora .ico{font-size:20px;flex:none}
.pildora .copia rect{stroke-width:2}
.pildora.is-ok{color:var(--negro);box-shadow:inset 0 0 0 1.5px var(--acento)}
.pildora.is-ok::before{transform:none}
.rot{display:grid;height:1.2em;overflow:hidden;align-items:center}
.rot>span{grid-area:1/1;justify-self:start;transition:transform .55s var(--ease-out),opacity .35s}
.rot>span:last-child{transform:translateY(120%);opacity:0}
.is-ok .rot>span:first-child{transform:translateY(-120%);opacity:0}
.is-ok .rot>span:last-child{transform:none;opacity:1}

/* 03 · icono redondo con tooltip que cambia */
.redondo{position:relative;width:56px;height:56px;border-radius:50%;display:grid;place-items:center;box-shadow:inset 0 0 0 1.5px var(--linea);transition:box-shadow .3s,background-color .4s,color .3s,transform .5s var(--muelle)}
.redondo:hover{box-shadow:inset 0 0 0 1.5px var(--papel)}
.redondo:active,.redondo.pulsado{transform:scale(.9);transition-duration:.12s}
.redondo .ico{font-size:24px}
.redondo.is-ok{background:var(--acento);color:var(--negro);box-shadow:inset 0 0 0 1.5px var(--acento)}
.redondo .onda{left:0;top:0;width:56px;height:56px}
.tip{position:absolute;right:0;bottom:calc(100% + 12px);padding:7px 11px;border-radius:8px;background:var(--papel);color:var(--negro);font:500 11px/1 var(--f-mono);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;display:grid;overflow:hidden;
  opacity:0;transform:translateY(6px) scale(.94);transform-origin:100% 100%;pointer-events:none;transition:opacity .25s,transform .45s var(--muelle)}
.tip>span{grid-area:1/1;transition:transform .45s var(--ease-out),opacity .3s}
.tip>span:last-child{transform:translateY(130%);opacity:0}
.redondo:hover .tip,.redondo:focus-visible .tip,.redondo.is-ok .tip{opacity:1;transform:none}
.redondo.is-ok .tip>span:first-child{transform:translateY(-130%);opacity:0}
.redondo.is-ok .tip>span:last-child{transform:none;opacity:1}

/* ---------- Avisos ---------- */
.avisos{position:fixed;z-index:30;left:50%;bottom:clamp(16px,3.4vh,34px);translate:-50% 0;width:min(372px,calc(100vw - 32px));display:flex;flex-direction:column;gap:10px;pointer-events:none}
.aviso{position:relative;display:flex;align-items:center;gap:14px;padding:14px 18px 15px 14px;border-radius:18px;background:var(--papel);color:var(--negro);overflow:hidden;box-shadow:0 18px 40px -12px rgba(0,0,0,.7),0 0 0 1px rgba(255,255,255,.08);pointer-events:auto;
  transition:scale .45s var(--muelle),opacity .35s}
.aviso:nth-last-child(2){scale:.96;opacity:.82}
.aviso:nth-last-child(n+3){scale:.92;opacity:.55}
.aviso__ico{flex:none;width:34px;height:34px;border-radius:50%;background:var(--acento);display:grid;place-items:center}
.aviso__ico svg{width:20px;height:20px;overflow:visible}
.aviso__ico path{stroke-dashoffset:0}
.aviso__tx{display:flex;flex-direction:column;gap:3px;min-width:0}
.aviso__tx b{font:700 15px/1.2 var(--f-display);letter-spacing:-.01em}
.aviso__tx span{font:500 12px/1.2 var(--f-mono);color:#5b5b56;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.aviso__barra{position:absolute;left:0;right:0;bottom:0;height:3px;background:var(--negro);transform-origin:0 50%}

@media (min-width:1001px){.avisos{left:auto;right:var(--margen);translate:none}}
/* ---------- Cursor fantasma ---------- */
.fantasma{position:fixed;left:0;top:0;z-index:40;width:24px;height:30px;pointer-events:none;opacity:0;transition:opacity .35s;will-change:transform}
.fantasma.on{opacity:1}
.fantasma svg{display:block;filter:drop-shadow(0 2px 5px rgba(0,0,0,.6))}
.pulso{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden}
.pulso i{position:absolute;left:0;top:0;width:90vmax;height:90vmax;margin:-45vmax 0 0 -45vmax;border-radius:50%;background:radial-gradient(circle,rgba(200,255,46,.16),rgba(200,255,46,0) 62%);opacity:0}

/* ---------- Tablet y móvil ---------- */
@media (max-width:1000px){
  .escena{grid-template-columns:minmax(0,1fr);row-gap:44px;padding-top:calc(var(--margen) + 56px)}
  .izq{gap:0}
  .titular{font-size:clamp(50px,13vw,104px)}
  .codigo{font-size:clamp(26px,7.2vw,52px)}
}
@media (max-width:620px){
  .barra .mono{display:none}
  .cupon{grid-template-columns:minmax(0,1fr)}
  .cupon__lado{border-left:0;border-top:1.5px dashed var(--linea);padding:22px 0 26px}
  .cupon__lado::before,.cupon__lado::after{top:-15px;bottom:auto}
  .cupon__lado::before{left:-15px;clip-path:inset(0 0 0 50%)}
  .cupon__lado::after{left:auto;right:-15px;clip-path:inset(0 50% 0 0)}
  .cupon__datos{padding:24px 22px;gap:22px}
  .codigo{font-size:clamp(24px,7.3vw,34px)}
  .fila{padding:16px 0}
}
@media (max-width:360px){.pildora{padding:0 18px;font-size:15px}}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition:none!important}
  .linea>span{transform:none}
  .fantasma{display:none}
}
```

### JS (script completo)

```js
(() => {
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const RM = matchMedia('(prefers-reduced-motion: reduce)');
  const MUELLE = 'linear(0, 0.055, 0.191, 0.366, 0.55, 0.722, 0.866, 0.978, 1.056, 1.103, 1.124, 1.125, 1.113, 1.093, 1.07, 1.047, 1.027, 1.01, 0.998, 0.99, 0.986, 0.984, 0.985, 0.987, 0.989, 0.992, 0.995, 0.998, 0.999, 1.001, 1.002, 1.002, 1.002, 1.002, 1)';
  const OUT = 'cubic-bezier(.16,1,.3,1)';
  const DURACION_OK = 2400;      // ms que el botón permanece confirmado
  const DURACION_AVISO = 2800;   // ms que vive un aviso
  const MAX_AVISOS = 3;
  // Temporizadores sobre requestAnimationFrame: siguen el reloj de la página y se detienen con la pestaña oculta.
  const alba = (ms, fn) => { let vivo = true; const t0 = performance.now(); (function f() { if (!vivo) return; if (performance.now() - t0 >= ms) fn(); else requestAnimationFrame(f); })(); return () => { vivo = false; }; };
  const wait = (ms) => new Promise((r) => alba(ms, r));

  /* ---------- Código en letras sueltas ---------- */
  const codigoEl = $('#codigo');
  const CODIGO = 'NODO-7K2Q-91XD';
  codigoEl.textContent = '';
  const letras = [...CODIGO].map((c) => { const s = document.createElement('span'); s.textContent = c; s.setAttribute('aria-hidden', 'true'); codigoEl.appendChild(s); return s; });

  /* ---------- Anillo giratorio (velocidad suavizada, un solo bucle) ---------- */
  const anillo = $('#anillo'), disco = $('#disco'), rotulo = $('#rotulo');
  let ang = 0, vel = 14, objetivo = 14, impulso = 0, visible = true, ultimo = 0, raf = 0;
  function bucle(t) {
    raf = 0;
    if (!visible || document.hidden) return;
    const dt = Math.min(0.05, ((t - (ultimo || t)) / 1000) || 0.016); ultimo = t;
    impulso *= Math.exp(-dt * 2.6);
    vel += (objetivo - vel) * (1 - Math.exp(-dt * 6));
    ang = (ang + (vel + impulso) * dt) % 360;
    anillo.style.transform = 'rotate(' + ang.toFixed(2) + 'deg)';
    raf = requestAnimationFrame(bucle);
  }
  function arrancar() { if (!RM.matches && !raf) { ultimo = 0; raf = requestAnimationFrame(bucle); } }
  new IntersectionObserver((e) => { visible = e[0].isIntersecting; if (visible) arrancar(); }, { threshold: 0 }).observe($('#escena'));
  document.addEventListener('visibilitychange', arrancar);
  disco.addEventListener('pointerenter', () => { objetivo = 70; });
  disco.addEventListener('pointerleave', () => { objetivo = 14; });
  arrancar();

  /* ---------- Portapapeles ---------- */
  async function copiarTexto(texto) {
    try { await navigator.clipboard.writeText(texto); return true; } catch (e) { /* sigue */ }
    try {
      const t = document.createElement('textarea');
      t.value = texto; t.setAttribute('readonly', '');
      t.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
      document.body.appendChild(t); t.select();
      const ok = document.execCommand('copy'); t.remove(); return ok;
    } catch (e) { return false; }
  }

  /* ---------- Avisos apilados (FLIP + muelle) ---------- */
  const cont = $('#avisos');
  function mover(fn) {
    const prev = new Map($$('.aviso', cont).map((c) => [c, c.getBoundingClientRect().top]));
    fn();
    if (RM.matches) return;
    $$('.aviso', cont).forEach((c) => {
      if (!prev.has(c)) return;
      const d = prev.get(c) - c.getBoundingClientRect().top;
      if (d) c.animate([{ transform: 'translateY(' + d + 'px)' }, { transform: 'none' }], { duration: 650, easing: MUELLE });
    });
  }
  function quitar(el) {
    if (!el.isConnected || el._saliendo) return; el._saliendo = true;
    const fin = () => mover(() => el.remove());
    if (RM.matches) return fin();
    el.animate([{ opacity: 1, transform: 'none' }, { opacity: 0, transform: 'translateY(12px) scale(.95)' }], { duration: 260, easing: 'ease-in', fill: 'forwards' });
    alba(270, fin);
  }
  function aviso(titulo, detalle) {
    const el = document.createElement('div');
    el.className = 'aviso';
    el.innerHTML = '<span class="aviso__ico"><svg viewBox="0 0 24 24" aria-hidden="true"><path pathLength="1" d="M5 12.8l4.6 4.6L19 7"/></svg></span><span class="aviso__tx"><b></b><span></span></span><span class="aviso__barra"></span>';
    $('b', el).textContent = titulo; $('.aviso__tx span', el).textContent = detalle;
    mover(() => {
      cont.appendChild(el);
      $$('.aviso', cont).filter((a) => !a._saliendo).slice(0, -MAX_AVISOS).forEach(quitar);
    });
    if (RM.matches) { setTimeout(() => quitar(el), DURACION_AVISO); return; }
    el.animate([{ opacity: 0, transform: 'translateY(34px) scale(.9)' }, { opacity: 1, transform: 'none' }], { duration: 700, easing: MUELLE });
    const path = $('.aviso__ico path', el);
    path.animate([{ strokeDashoffset: 1 }, { strokeDashoffset: 0 }], { duration: 480, delay: 180, easing: OUT, fill: 'backwards' });
    const barra = $('.aviso__barra', el).animate([{ transform: 'scaleX(1)' }, { transform: 'scaleX(0)' }], { duration: DURACION_AVISO, easing: 'linear', fill: 'forwards' });
    let cancela = alba(DURACION_AVISO, () => quitar(el));
    el.addEventListener('pointerenter', () => { barra.pause(); cancela(); });
    el.addEventListener('pointerleave', () => { barra.play(); cancela = alba(Math.max(300, DURACION_AVISO - barra.currentTime), () => quitar(el)); });
  }

  /* ---------- Efectos de confirmación ---------- */
  function chispas() {
    if (RM.matches) return;
    const cx = $('#chispas'), n = 14;
    for (let i = 0; i < n; i++) {
      const a = (i / n) * 360 + (Math.random() * 10 - 5);
      const el = document.createElement('i'); cx.appendChild(el);
      const r0 = 56 + Math.random() * 6, r1 = 92 + Math.random() * 22;
      el.animate([
        { opacity: 1, transform: 'rotate(' + a + 'deg) translateX(' + r0 + 'px) scaleX(.2)' },
        { opacity: 1, offset: .35, transform: 'rotate(' + a + 'deg) translateX(' + (r0 + (r1 - r0) * .6) + 'px) scaleX(1)' },
        { opacity: 0, transform: 'rotate(' + a + 'deg) translateX(' + r1 + 'px) scaleX(.3)' }
      ], { duration: 850, easing: OUT });
      alba(860, () => el.remove());
    }
  }
  function onda(el) {
    if (RM.matches || !el) return;
    el.animate([{ opacity: .9, transform: 'scale(1)' }, { opacity: 0, transform: 'scale(1.9)' }], { duration: 850, easing: OUT });
  }
  function pulsoEscena(x, y) {
    if (RM.matches) return;
    const r = $('#escena').getBoundingClientRect(), i = document.createElement('i');
    $('#pulso').appendChild(i);
    i.animate([{ opacity: 1, transform: 'translate(' + (x - r.left) + 'px,' + (y - r.top) + 'px) scale(.15)' }, { opacity: 0, transform: 'translate(' + (x - r.left) + 'px,' + (y - r.top) + 'px) scale(1)' }], { duration: 1100, easing: OUT });
    alba(1110, () => i.remove());
  }
  function rodarLetras() {
    if (RM.matches) return;
    letras.forEach((l, i) => l.animate([
      { transform: 'translateY(0)', color: 'inherit' },
      { transform: 'translateY(-22%)', color: '#c8ff2e', offset: .38 },
      { transform: 'translateY(0)', color: 'inherit' }
    ], { duration: 640, delay: i * 30, easing: OUT }));
  }
  let veces = 0;
  const cuentaEl = $('#cuenta'), vecesEl = $('#veces');
  function contar() {
    veces++;
    const poner = () => { cuentaEl.textContent = veces; vecesEl.textContent = veces === 1 ? 'vez' : 'veces'; };
    if (RM.matches) return poner();
    cuentaEl.animate([{ transform: 'none', opacity: 1 }, { transform: 'translateY(-70%)', opacity: 0 }], { duration: 140, easing: 'ease-in', fill: 'forwards' });
    alba(145, () => {
      poner();
      cuentaEl.getAnimations().forEach((a) => a.cancel());
      cuentaEl.animate([{ transform: 'translateY(70%)', opacity: 0 }, { transform: 'none', opacity: 1 }], { duration: 420, easing: OUT });
    });
  }

  /* ---------- Campo de prueba de pegado ---------- */
  const campo = $('#campo'), entrada = $('#prueba'), borrar = $('#borrar');
  let tecleo = 0;
  function marcar() { campo.classList.toggle('lleno', entrada.value.length > 0); }
  function escribir(texto) {
    cancelAnimationFrame(tecleo); entrada.value = '';
    if (RM.matches) { entrada.value = texto; return marcar(); }
    const t0 = performance.now();
    const paso = () => {
      const n = Math.min(texto.length, Math.floor((performance.now() - t0) / 24) + 1);
      entrada.value = texto.slice(0, n); marcar();
      if (n < texto.length) tecleo = requestAnimationFrame(paso);
    };
    tecleo = requestAnimationFrame(paso);
  }
  entrada.addEventListener('input', marcar);
  borrar.addEventListener('click', () => { cancelAnimationFrame(tecleo); entrada.value = ''; marcar(); entrada.focus(); });
  // En pantallas estrechas el campo se mueve bajo los botones.
  const campoBloque = $('#pegar'), campoMov = $('#pegarMov'), mq = matchMedia('(max-width:1000px)');
  const colocar = () => { (mq.matches ? campoMov : $('.izq')).appendChild(campoBloque); };
  mq.addEventListener('change', colocar); colocar();

  /* ---------- Etiqueta de la píldora: ancho con muelle ---------- */
  const pildora = $('.pildora'), rots = $$('.rot > span', pildora);
  let padPil = 0;
  function medirPildora() {
    pildora.style.width = '';
    const w = rots.map((s) => s.getBoundingClientRect().width);
    padPil = pildora.getBoundingClientRect().width - w[0];
    pildora._w = w;
    pildora.style.width = (padPil + w[pildora.classList.contains('is-ok') ? 1 : 0]) + 'px';
  }
  (document.fonts ? document.fonts.ready : Promise.resolve()).then(medirPildora);
  addEventListener('resize', () => { clearTimeout(medirPildora.t); medirPildora.t = setTimeout(medirPildora, 120); });

  /* ---------- Confirmar ---------- */
  function revertir(btn) {
    btn.classList.remove('is-ok');
    if (btn.dataset.tipo === 'disco') { disco.classList.remove('ok'); rotulo.textContent = 'COPIAR CÓDIGO · COPIAR CÓDIGO · '; }
    if (btn.dataset.tipo === 'pildora' && pildora._w) pildora.style.width = (padPil + pildora._w[0]) + 'px';
  }
  function confirmar(btn, x, y) {
    const tipo = btn.dataset.tipo, r = btn.getBoundingClientRect();
    const ya = btn.classList.contains('is-ok');
    if (tipo === 'disco') {
      btn.querySelector('.disco__fill').style.setProperty('--fx', Math.max(0, Math.min(r.width, x - r.left)) + 'px');
      btn.querySelector('.disco__fill').style.setProperty('--fy', Math.max(0, Math.min(r.height, y - r.top)) + 'px');
    }
    btn.classList.add('is-ok');
    if (!ya) {
      if (tipo === 'disco') { disco.classList.add('ok'); rotulo.textContent = 'COPIADO · COPIADO · COPIADO · '; impulso = 900; chispas(); onda($('#onda')); rodarLetras(); }
      if (tipo === 'pildora' && pildora._w) pildora.style.width = (padPil + pildora._w[1]) + 'px';
      if (tipo === 'redondo') onda($('.onda', btn));
    } else if (tipo === 'disco') { impulso = 500; chispas(); rodarLetras(); }
    pulsoEscena(x, y);
    aviso(btn.dataset.aviso, btn.dataset.detalle);
    contar();
    escribir(btn.dataset.valor);
    if (btn._c) btn._c(); btn._c = alba(DURACION_OK, () => revertir(btn));
  }

  $$('[data-copiar]').forEach((btn) => {
    btn.addEventListener('click', async (e) => {
      const r = btn.getBoundingClientRect();
      const teclado = e.detail === 0 && !e.clientX && !e.clientY;
      const x = teclado ? r.left + r.width / 2 : e.clientX, y = teclado ? r.top + r.height / 2 : e.clientY;
      const ok = await copiarTexto(btn.dataset.valor);
      if (ok) confirmar(btn, x, y);
      else aviso('No se pudo copiar', 'Selecciona el texto y usa Ctrl+C');
    });
  });

  /* ---------- Cursor fantasma: enseña la interacción solo ---------- */
  const fantasma = $('#fantasma');
  let parado = false, corriendo = false, px = -60, py = -60, idle = null;
  const colocarF = (x, y, esc) => { fantasma.style.transform = 'translate(' + x + 'px,' + y + 'px)' + (esc ? ' scale(' + esc + ')' : ''); px = x; py = y; };
  function mueveF(x, y, ms) {
    return new Promise((res) => {
      const a = fantasma.animate([{ transform: 'translate(' + px + 'px,' + py + 'px)' }, { transform: 'translate(' + x + 'px,' + y + 'px)' }], { duration: ms, easing: 'cubic-bezier(.65,0,.25,1)', fill: 'forwards' });
      alba(ms + 5, () => { colocarF(x, y); a.cancel(); res(); });
    });
  }
  async function visibleYQuieto() { while (parado || !visible || document.hidden) { await wait(200); } }
  async function ronda() {
    const orden = $$('[data-copiar]');
    for (const btn of orden) {
      await visibleYQuieto();
      const r = btn.getBoundingClientRect();
      if (r.bottom > innerHeight - 20 || r.top < 0) continue;
      if (px < 0) colocarF(r.left + r.width * .5 - 160, r.top + r.height * .5 + 120);
      fantasma.classList.add('on');
      await mueveF(r.left + r.width * (.45 + Math.random() * .2), r.top + r.height * (.4 + Math.random() * .25), 800);
      if (parado) return;
      btn.classList.add('pulsado'); fantasma.animate([{ scale: 1 }, { scale: .82 }, { scale: 1 }], { duration: 260 });
      await wait(150);
      if (parado) { btn.classList.remove('pulsado'); return; }
      const rr = btn.getBoundingClientRect();
      confirmar(btn, px, py > rr.bottom ? rr.bottom : py);
      await wait(120); btn.classList.remove('pulsado');
      await wait(1700);
    }
    await wait(600);
    if (!parado) { fantasma.classList.remove('on'); await wait(2600); }
  }
  async function guion() {
    if (corriendo || RM.matches) return; corriendo = true;
    await wait(1100);
    while (!RM.matches) { await ronda(); }
    corriendo = false;
  }
  function manoReal() {
    parado = true; fantasma.classList.remove('on');
    if (idle) idle(); idle = alba(9000, () => { parado = false; });
  }
  addEventListener('pointerdown', manoReal, { passive: true });
  addEventListener('keydown', manoReal);
  guion();
})();
```

Notas del código que conviene respetar:

- **Temporizadores con `requestAnimationFrame`** (`alba`): la lógica no usa `setTimeout`, `setInterval` ni `onfinish`. Así el tiempo sigue al reloj de la página, se detiene con la pestaña oculta y la pieza se puede grabar a fotogramas exactos.
- **Anillo de texto**: el `<text>` lleva `textLength="464"` (circunferencia del radio 74) con `lengthAdjust="spacing"`, de modo que «COPIAR» y «COPIADO» ocupan siempre la vuelta entera.
- **Relleno desde el clic**: `--fx` y `--fy` se escriben en el relleno antes de añadir `.is-ok`; al volver a reposo el círculo se encoge hacia el mismo punto.
- **Avisos**: la pila usa FLIP (se mide la posición antes y después de insertar o quitar y se anima la diferencia con el muelle). Máximo 3 avisos; el más antiguo sale cuando entra el cuarto.

## Interacción

- **Ratón**: hover en el disco acelera el anillo y separa las hojas del icono; pulsar escala a .93 con muelle; el relleno nace donde haces clic. Hover sobre un aviso pausa su barra y su cuenta atrás.
- **Teclado**: todos los botones son `<button>`, se activan con Enter o espacio (el relleno nace del centro) y tienen foco visible (contorno de 2 px de acento, separación 4 px).
- **Táctil**: un toque equivale al clic; el cursor fantasma sirve de demostración. El fantasma se detiene con cualquier `pointerdown` o `keydown` y se reanuda tras 9 s sin actividad. No se detiene por mover el ratón.
- **Pegado**: el campo es un `<input>` real; el valor copiado se teclea a 24 ms por carácter y puede borrarse con «Borrar».
- **Portapapeles**: `navigator.clipboard.writeText` y, si falla, un `<textarea>` oculto con `document.execCommand('copy')`. Si ambos fallan, aviso de error y sin check.

## Adaptación a móvil y tablet

- **≤ 1000 px**: una columna; el titular pasa a `clamp(50px,13vw,104px)`; el campo de pegado se mueve debajo de los botones (JS con `matchMedia`); el código del cupón `clamp(26px,7.2vw,52px)`; los avisos se centran abajo.
- **≤ 620 px**: se oculta la etiqueta de la barra; el cupón se apila (datos arriba, disco abajo) y la perforación pasa a horizontal con las muescas en los bordes laterales; código `clamp(24px,7.3vw,34px)`.
- **≤ 360 px**: la píldora reduce su padding a 18 px y su texto a 15 px. Sin scroll horizontal de 360 a 1920 px.
- La página crece en altura en móvil (`min-height:100svh`); el cursor fantasma salta los botones que no estén a la vista.

## Accesibilidad y movimiento reducido

- Botones reales con `aria-label` («Copiar código de invitación», etc.); lo decorativo (anillo, iconos, chispas, ondas, luz, fantasma) con `aria-hidden="true"`. El código se anuncia con un `aria-label` en su contenedor y las letras sueltas van ocultas.
- Los avisos viven en `<div role="status" aria-live="polite">`.
- Contraste AA: texto papel sobre negro, texto negro sobre acento, gris `#8f8f8a` sobre negro (≈ 6:1).
- `prefers-reduced-motion: reduce`: `animation` y `transition` a `none`, el titular aparece ya colocado, el anillo no gira, no hay chispas, ondas, luz ni cursor fantasma, el pegado es instantáneo y los avisos entran y salen sin animar. La confirmación sigue siendo legible (check y color cambian al instante).

## Rendimiento

- Solo `transform`, `opacity`, `clip-path` y `stroke-dashoffset`; el único bucle de `requestAnimationFrame` (el anillo) se detiene cuando la pieza sale de pantalla (`IntersectionObserver`) o la pestaña se oculta, y no arranca con movimiento reducido.
- Sin WebGL ni canvas: no hay DPR ni fallback que gestionar. HTML < 40 KB.
- Funciona dentro de un `iframe` (sin `window.top`, sin `alert`, sin `localStorage`).

## Personalización

- Marca y textos: `Nodo`, `Un clic.`, `Cero`, `dudas.`.
- Valores a copiar: `NODO-7K2Q-91XD` (en el HTML y en la constante `CODIGO` del script), `npm i @nodo/ui`, `nodo.app/i/7k2q` (se copia con `https://` delante) y `nd_test_5f21c9a0b7c1` (se enseña enmascarada; ajusta también el texto visible y el `data-detalle`).
- Colores: `#c8ff2e`, `#0a0a0a`, `#f4f4f0`. Si cambias el acento, actualiza también el `rgba(200,255,46,…)` de la luz de escena; si cambias el blanco, el `rgba(244,244,240,…)` de las líneas.
- Tiempos: `DURACION_OK`, `DURACION_AVISO` y `MAX_AVISOS` al inicio del script.

## Comprobación final

1. Cero errores en consola y sin scroll horizontal a 360, 768, 1024, 1440 y 1920 px.
2. Pulsar el disco: relleno verde desde el punto del clic, check que se dibuja, anillo que cambia a «COPIADO» con un tirón, 14 chispas y letras que saltan en cascada.
3. Pulsar el botón cuadrado: el icono sube y el check entra desde abajo sobre relleno verde.
4. Pulsar la píldora: la etiqueta rueda a «Enlace copiado» y el ancho se adapta con un pequeño rebote.
5. Pulsar el botón redondo: onda, fondo verde y tooltip que pasa a «Copiado».
6. Cada copia crea un aviso con muelle; con tres seguidos se apilan (el segundo a escala .96) y el cuarto expulsa al primero; el hover pausa la barra.
7. El contador suma («1 vez», «2 veces») y el campo de pegado teclea el valor copiado.
8. Tras 2,4 s cada botón vuelve a reposo con la animación inversa.
9. Sin tocar nada, a los ~1,1 s aparece el cursor fantasma y pulsa los botones en orden; un clic real lo detiene.
10. Con Tab se llega a todos los botones con foco visible y Enter o espacio copia.
11. Con movimiento reducido todo cambia al instante y no hay fantasma.
12. Con la pestaña oculta o la pieza fuera de pantalla el anillo se detiene.

Prueba en frío

La prueba en frío

Aún sin publicar para esta pieza

En la prueba en frío pegamos el prompt en Claude Sonnet 5.5, sin ayuda ni contexto, y comparamos lo que genera con la demo. Cuando esté hecha, aquí verás la captura, el veredicto y la fecha.

Paso a paso

Cómo usarlo en 4 pasos

  1. 01

    Personaliza

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

    Copia el Completo o el Lite

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

    Pega e itera

    Pégalo en una conversación nueva. Si quieres cambiar algo, pídelo con una frase corta:
    • «Cambia el verde ácido por azul eléctrico»
    • «Quita el anillo giratorio y deja solo el botón central»
    • «Haz que el aviso aparezca arriba a la derecha»
    • «Pasa toda la pieza a fondo blanco con texto negro»
  4. 04

    Publica

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

Guías por herramientaClaudeChatGPTGeminiCursorLovableBoltv0

Licencia en claro

Qué puedes hacer con esta pieza

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

Puedes

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

No puedes

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

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

Preguntas frecuentes

Lo que suele preguntarse

¿Copia de verdad al portapapeles?
Sí. Usa navigator.clipboard.writeText y, si el navegador o el iframe lo bloquea, un textarea oculto con execCommand('copy'). Si ambos fallan, muestra un aviso honesto en lugar de fingir que ha copiado. El cursor fantasma solo simula el gesto: nunca toca tu portapapeles.
¿Puedo usar solo uno de los cuatro botones?
Sí. Cada botón es independiente: lleva data-copiar, data-tipo, data-valor, data-aviso y data-detalle. Copia su bloque de HTML y de CSS, y deja el script, que ya trata cada tipo por separado y se salta los que no existen.
¿Cómo cambio cuánto tiempo se queda el check?
Con las constantes DURACION_OK (2400 ms que el botón permanece confirmado) y DURACION_AVISO (2800 ms que vive el aviso) al principio del script. El resto del movimiento no depende de ellas.
¿Es accesible para lectores de pantalla y teclado?
Sí: son botones reales con aria-label, se activan con Enter o espacio, tienen foco visible y los avisos viven en una región role=status con aria-live, así que el lector anuncia «Código copiado». Con movimiento reducido todo cambia al instante, sin animaciones.
¿Qué diferencia hay entre el prompt Completo y el Lite?
El Completo (≈13853 tokens) describe la pieza con todo detalle y es el que mejor resultado da en Claude, ChatGPT, Gemini o Cursor. El Lite (≈2960 tokens) resume lo esencial para herramientas que aceptan menos texto, como Lovable, Bolt o v0.

Versión 1.0 · publicada el 01-10-2026

Cópialo desde aquí

Tu navegador no nos deja escribir en el portapapeles. El texto ya está seleccionado: pulsa Ctrl + C (o ⌘ + C) o el botón.