Exploding like buttonOne-line
GLM 5.3
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 11 min 6 s
- Cost
- $0.326
- Output tokens
- 74,150
- Spent thinking
- 67,500 tokens
- Speed
- 111.4 tok/s
- Console errors
- 0
- First token after
- 763 ms
- First visible text after
- 10 min 46 s
- Prompt tokens
- 356
- Code size
- 18.5 KB
- Finish reason
- stop
- Served by
- Parasail
- Model id
z-ai/glm-5.3- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
“a "like" button in pure CSS, no JavaScript, that does a crazy animation when you click it”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 451 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>You’re gonna like this — a pure-CSS like button</title>
<style>
/* =====================================================================
THE LIKE BUTTON — pure CSS, zero JavaScript.
All state lives in one hidden <input type="checkbox">;
every effect below is keyed off #like:checked.
===================================================================== */
:root{
--font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--serif: Georgia, "Times New Roman", serif;
--red: #ff2e63;
--red-hi: #ff5c7a;
--pink: #ff8fae;
--gold: #ffd166;
--cyan: #4cc9f0;
--violet: #b388ff;
--ink: #f2f5fc;
--mut: #97a0b6;
--mut2: #707a94;
--hx: 45px; /* horizontal centre of the heart, inside the button */
}
*,*::before,*::after{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0;
overflow:hidden; /* the burst may leave the stage — never the page */
font-family:var(--font);
color:var(--ink);
background:#0a0c13;
-webkit-font-smoothing:antialiased;
}
::selection{ background:rgba(255,46,99,.4); }
/* ---------------- ambient background ---------------- */
.bg{
position:fixed; inset:0; z-index:0; pointer-events:none;
background:
radial-gradient(1000px 560px at 50% 116%, rgba(255,46,99,.11), transparent 62%),
radial-gradient(760px 420px at 50% -8%, rgba(96,130,255,.09), transparent 60%);
}
.bg::after{
content:""; position:absolute; inset:0;
background-image:radial-gradient(rgba(255,255,255,.055) 1px, transparent 1.4px);
background-size:26px 26px;
-webkit-mask-image:radial-gradient(85% 68% at 50% 42%, #000 25%, transparent 78%);
mask-image:radial-gradient(85% 68% at 50% 42%, #000 25%, transparent 78%);
}
/* full-page blush when the like lands */
.flash{
position:fixed; inset:0; z-index:40; pointer-events:none; opacity:0;
background:radial-gradient(600px 420px at 50% 55%, rgba(255,46,99,.22), rgba(255,46,99,.05) 45%, transparent 72%);
}
body:has(#like:checked) .flash{ animation:flash .8s ease-out; }
@keyframes flash{ 0%{opacity:0} 16%{opacity:1} 100%{opacity:0} }
/* ---------------- page frame ---------------- */
.scene{
position:relative; z-index:1;
min-height:100vh; min-height:100svh;
display:flex; flex-direction:column; align-items:center;
padding:clamp(22px,4vh,48px) 20px 24px;
text-align:center;
}
body:has(#like:checked) .scene{ animation:shake .55s cubic-bezier(.3,.6,.4,1); }
@keyframes shake{
0%,100%{ transform:translate(0,0) rotate(0deg); }
15%{ transform:translate(-7px,4px) rotate(-.5deg); }
30%{ transform:translate(6px,-5px) rotate(.45deg); }
45%{ transform:translate(-5px,3px) rotate(-.35deg); }
60%{ transform:translate(4px,-3px) rotate(.3deg); }
78%{ transform:translate(-3px,2px) rotate(-.18deg); }
92%{ transform:translate(2px,-1px) rotate(0deg); }
}
@keyframes enter{ from{opacity:0; transform:translateY(14px);} to{opacity:1; transform:none;} }
header, .hero, .foot{ animation:enter .7s cubic-bezier(.2,.7,.3,1) both; }
.hero{ animation-delay:.12s; }
.foot{ animation-delay:.24s; }
header{ display:flex; flex-direction:column; align-items:center; }
.badge{
display:inline-flex; align-items:center; gap:9px; margin:0;
padding:8px 16px; border:1px solid rgba(255,255,255,.13); border-radius:999px;
background:rgba(255,255,255,.04);
font-size:11px; font-weight:700; letter-spacing:.16em; text-transform:uppercase;
color:#aeb6c9;
}
.bdot{
width:7px; height:7px; border-radius:50%; background:var(--red);
box-shadow:0 0 12px rgba(255,46,99,.9);
animation:breathe 2.2s ease-in-out infinite;
}
@keyframes breathe{ 0%,100%{opacity:1; transform:scale(1)} 50%{opacity:.5; transform:scale(.75)} }
h1{
margin:22px 0 0; text-wrap:balance;
font-size:clamp(30px,5.6vw,54px); font-weight:800;
letter-spacing:-.028em; line-height:1.06;
}
h1 em{
display:inline-block; font-family:var(--serif); font-style:italic; font-weight:600;
padding:0 .05em;
background:linear-gradient(100deg,var(--red) 15%,var(--pink) 85%);
-webkit-background-clip:text; background-clip:text; color:transparent;
}
body:has(#like:checked) h1 em{ animation:emPop .85s cubic-bezier(.3,1.7,.4,1); }
@keyframes emPop{ 0%{transform:scale(1) rotate(0)} 45%{transform:scale(1.13) rotate(-2.5deg)} 100%{transform:scale(1) rotate(0)} }
.sub{
margin:16px auto 0; max-width:46ch;
font-size:clamp(14px,1.9vw,16.5px); line-height:1.65; color:var(--mut);
}
.hero{ margin:auto 0; display:flex; flex-direction:column; align-items:center; padding:26px 0 8px; }
.hint{ margin:18px 0 0; font-size:13px; color:var(--mut2); }
.foot{ margin:0; font-size:12px; letter-spacing:.03em; color:var(--mut2); }
/* ---------------- the one and only input ---------------- */
#like{ position:absolute; width:1px; height:1px; margin:-1px; opacity:0; pointer-events:none; }
#like:focus-visible + .stage .btn{ outline:2px solid var(--red-hi); outline-offset:5px; }
/* ---------------- the button ---------------- */
.stage{
position:relative; display:inline-flex; cursor:pointer;
user-select:none; -webkit-user-select:none;
-webkit-tap-highlight-color:transparent; touch-action:manipulation;
}
.fx-back,.fx-front{ position:absolute; inset:0; pointer-events:none; }
.fx-back{ z-index:1; }
.fx-front{ z-index:3; }
.boom{ position:absolute; left:var(--hx); top:50%; width:0; height:0; } /* the heart’s exact centre */
.btn{
position:relative; z-index:2; overflow:hidden;
display:inline-flex; align-items:center; gap:13px;
height:64px; padding:0 32px 0 26px; border-radius:999px;
background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.16);
box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 14px 34px rgba(0,0,0,.45);
font-size:17px; font-weight:700; letter-spacing:.01em; color:var(--ink);
transition:transform .2s cubic-bezier(.34,1.56,.64,1),
background .35s ease, border-color .35s ease, box-shadow .35s ease;
}
.stage:hover .btn{
transform:translateY(-2px);
border-color:rgba(255,255,255,.3);
box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 18px 40px rgba(0,0,0,.5), 0 6px 26px rgba(255,46,99,.14);
}
.stage:active .btn{ transform:scale(.94); } /* squish while pressed */
/* soft ripple on every press */
.btn::after{
content:""; position:absolute; inset:0; border-radius:inherit;
background:radial-gradient(closest-side circle at 50% 50%, rgba(255,255,255,.3), transparent 72%);
opacity:0; transform:scale(.55);
}
.stage:active .btn::after{ animation:ripple .5s ease-out; }
@keyframes ripple{ 0%{opacity:.85; transform:scale(.55)} 100%{opacity:0; transform:scale(1.12)} }
/* liked skin */
#like:checked + .stage .btn{
background:linear-gradient(180deg, rgba(255,46,99,.18), rgba(255,46,99,.06));
border-color:rgba(255,84,120,.55);
box-shadow:inset 0 1px 0 rgba(255,255,255,.12),
0 10px 34px rgba(255,46,99,.26),
0 0 0 1px rgba(255,46,99,.18);
}
/* ---------------- the heart, drawn entirely in CSS ---------------- */
.heart{
--s:20px;
position:absolute; left:50%; top:50%;
width:var(--s); height:var(--s);
transform:translate(-50%,-50%) rotate(45deg);
background:currentColor; color:#97a1b8;
transition:color .38s ease, filter .45s ease;
}
.heart::before,.heart::after{
content:""; position:absolute; width:var(--s); height:var(--s);
border-radius:50%; background:currentColor;
}
.heart::before{ left:0; top:calc(var(--s) / -2); }
.heart::after { left:calc(var(--s) / -2); top:0; }
.heart-wrap{
position:relative; width:36px; height:36px; flex:none;
transition:transform .4s cubic-bezier(.3,1.5,.45,1);
}
#like:checked + .stage .heart{ color:var(--red); filter:drop-shadow(0 2px 10px rgba(255,46,99,.55)); }
#like:checked + .stage .heart-wrap{
animation:pop .85s cubic-bezier(.25,1,.4,1) both, heartbeat 1.7s linear .85s infinite;
}
@keyframes pop{
0% { transform:scale(1) rotate(0deg); }
28% { transform:scale(1.45) rotate(-8deg); }
44% { transform:scale(.82) rotate(5deg); }
58% { transform:scale(1.28) rotate(-3deg); }
72% { transform:scale(.96) rotate(1deg); }
86% { transform:scale(1.15) rotate(0deg); }
100%{ transform:scale(1.08) rotate(0deg); }
}
@keyframes heartbeat{
0%,100%{ transform:scale(1.08); }
14% { transform:scale(1.22); }
28% { transform:scale(1.08); }
42% { transform:scale(1.18); }
60% { transform:scale(1.08); }
}
/* ---------------- Like → Liked ---------------- */
.txt{ display:grid; overflow:hidden; }
.t{
grid-area:1/1; white-space:nowrap;
transition:transform .38s cubic-bezier(.55,0,.3,1), opacity .32s ease;
}
.t-liked{ color:var(--red-hi); transform:translateY(125%); opacity:0; }
#like:checked + .stage .t-like{
transform:translateY(-125%); opacity:0;
transition:transform .26s cubic-bezier(.5,0,.7,.4), opacity .2s ease;
}
#like:checked + .stage .t-liked{
transform:translateY(0); opacity:1;
transition:transform .52s cubic-bezier(.25,1.5,.4,1) .1s, opacity .3s ease .1s;
}
/* ---------------- idle pulse & ghost heart (behind the button) ---------------- */
.pulse{
position:absolute; inset:-3px; border-radius:999px;
border:1.5px solid rgba(255,84,120,.38); opacity:0;
animation:pulse 2.8s ease-out infinite;
}
#like:checked + .stage .pulse{ animation:none; opacity:0; }
@keyframes pulse{ 0%{transform:scale(1); opacity:.7} 70%,100%{transform:scale(1.3); opacity:0} }
.ghost{ position:absolute; left:var(--hx); top:50%; width:0; height:0; opacity:0; }
.ghost .heart{
--s:62px; color:var(--red);
filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8));
}
#like:checked + .stage .ghost .heart{
filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8));
}
#like:checked + .stage .ghost{ animation:ghostUp 1.3s cubic-bezier(.22,.61,.31,1) .04s both; }
@keyframes ghostUp{
0% { transform:translate(0,0) scale(.2) rotate(0deg); opacity:0; }
16% { opacity:.55; }
38% { transform:translate(4px,-42px) scale(1.1) rotate(-9deg); opacity:.55; }
100%{ transform:translate(30px,-320px) scale(2) rotate(12deg); opacity:0; }
}
/* ---------------- shockwave rings ---------------- */
.ring{
position:absolute; left:-46px; top:-46px; width:92px; height:92px;
border-radius:50%; border:2.5px solid rgba(255,255,255,.85); opacity:0;
}
.r2{ border-color:var(--red); --rd:.09s; }
.r3{ border-color:var(--gold); --rd:.18s; }
#like:checked + .stage .ring{ animation:ring .78s cubic-bezier(.18,.65,.35,1) var(--rd,0s) both; }
@keyframes ring{
0% { transform:scale(.4); opacity:0; }
15% { opacity:.95; }
100%{ transform:scale(2.65); opacity:0; }
}
/* ---------------- flying particles ---------------- */
.pt{ position:absolute; left:0; top:0; opacity:0; will-change:transform,opacity; }
.pt.dot { width:var(--s,10px); height:var(--s,10px); border-radius:50%; background:var(--c,#fff); }
.pt.star{
width:var(--s,14px); height:var(--s,14px); background:var(--c,var(--gold));
clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%);
}
.pt.mh{ width:var(--s,12px); height:var(--s,12px); background:var(--c,var(--red)); }
.pt.mh::before,.pt.mh::after{
content:""; position:absolute; width:var(--s,12px); height:var(--s,12px);
border-radius:50%; background:var(--c,var(--red));
}
.pt.mh::before{ left:0; top:calc(var(--s,12px) / -2); }
.pt.mh::after { left:calc(var(--s,12px) / -2); top:0; }
#like:checked + .stage .pt{ animation:fly var(--t,.9s) cubic-bezier(.16,.68,.32,1) var(--d,0s) both; }
@keyframes fly{
0%{
transform:translate(-50%,-50%) translate(0,0) scale(.3) rotate(0deg);
opacity:0;
}
12%{ opacity:1; }
38%{
transform:translate(-50%,-50%) translate(calc(var(--dx,100px) * .38), calc(var(--dy,-100px) * .38)) scale(1.22) rotate(calc(var(--rot,180deg) * .38));
opacity:1;
}
76%{ opacity:1; }
100%{
transform:translate(-50%,-50%) translate(var(--dx,100px), var(--dy,-100px)) scale(.55) rotate(var(--rot,180deg));
opacity:0;
}
}
/* ---------------- confetti with a real gravity arc ---------------- */
.conf{
position:absolute; left:0; top:0;
width:var(--w,8px); height:var(--h,12px); border-radius:2px;
background:var(--c,var(--gold)); opacity:0; will-change:transform;
}
#like:checked + .stage .conf{ animation:drop var(--t,1.3s) linear var(--d,.15s) both; }
@keyframes drop{
0%{
transform:translate(-50%,-50%) translate(0,0) rotate(0deg);
opacity:0;
animation-timing-function:cubic-bezier(.15,.7,.35,1); /* the toss */
}
10%{ opacity:1; }
32%{
transform:translate(-50%,-50%) translate(calc(var(--sw,30px) * .45), -95px) rotate(230deg);
opacity:1;
animation-timing-function:cubic-bezier(.5,0,.8,.5); /* gravity */
}
82%{ opacity:1; }
100%{
transform:translate(-50%,-50%) translate(var(--sw,30px), 240px) rotate(640deg);
opacity:0;
}
}
/* ---------------- counter with a rolling digit ---------------- */
.meta{ --row:1.5em; position:relative; margin:38px 0 0; font-size:15px; color:var(--mut); }
.counter{
display:inline-flex; align-items:center;
font-weight:700; color:#c6cede; font-variant-numeric:tabular-nums;
transition:color .45s ease;
}
.cs,.cl{ line-height:var(--row); }
.roll{ height:var(--row); overflow:hidden; }
.strip{ display:block; transition:transform .55s cubic-bezier(.3,.9,.3,1.12); }
.strip i{ display:block; height:var(--row); line-height:var(--row); font-style:normal; }
#like:checked ~ .meta .strip{ transform:translateY(calc(-1 * var(--row))); }
#like:checked ~ .meta .counter{ color:var(--red-hi); }
.plus{
position:absolute; left:calc(50% + 58px); top:-14px;
font-size:15px; font-weight:800; color:var(--red-hi);
text-shadow:0 0 14px rgba(255,46,99,.65);
opacity:0; pointer-events:none;
}
#like:checked ~ .meta .plus{ animation:rise .85s cubic-bezier(.2,.7,.4,1) .16s both; }
@keyframes rise{
0% { transform:translateY(6px); opacity:0; }
30% { opacity:1; }
100%{ transform:translateY(-30px); opacity:0; }
}
</style>
</head>
<body>
<!-- no <script> was written in the making of this page -->
<div class="bg"></div>
<div class="flash"></div>
<main class="scene">
<header>
<p class="badge"><span class="bdot"></span>Pure CSS · Zero JavaScript</p>
<h1>You’re gonna <em>like</em> this.</h1>
<p class="sub">One hidden checkbox, a mountain of keyframes, and a frankly excessive celebration. Press the button — then press it again.</p>
</header>
<div class="hero">
<!-- the entire app is this one checkbox -->
<input type="checkbox" id="like" aria-label="Like this page">
<label class="stage" for="like">
<!-- layer 0 · behind the button -->
<span class="fx-back">
<span class="pulse"></span>
<span class="ghost"><i class="heart"></i></span>
</span>
<!-- layer 1 · the button -->
<span class="btn">
<span class="heart-wrap"><i class="heart"></i></span>
<span class="txt">
<span class="t t-like">Like</span>
<span class="t t-liked">Liked</span>
</span>
</span>
<!-- layer 2 · everything that explodes -->
<span class="fx-front">
<span class="boom">
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
<!-- dots -->
<i class="pt dot" style="--dx:122px; --dy:-8px; --c:var(--gold); --s:10px; --t:.8s; --d:0s; --rot:200deg"></i>
<i class="pt dot" style="--dx:98px; --dy:-64px; --c:var(--cyan); --s:12px; --t:.9s; --d:.04s; --rot:-160deg"></i>
<i class="pt dot" style="--dx:46px; --dy:-106px; --c:var(--pink); --s:9px; --t:.85s; --d:0s; --rot:120deg"></i>
<i class="pt dot" style="--dx:-34px; --dy:-110px; --c:var(--violet); --s:11px; --t:.95s; --d:.05s; --rot:-220deg"></i>
<i class="pt dot" style="--dx:-96px; --dy:-64px; --c:var(--red); --s:10px; --t:.8s; --d:.02s; --rot:180deg"></i>
<i class="pt dot" style="--dx:-124px; --dy:-6px; --c:var(--gold); --s:13px; --t:.9s; --d:0s; --rot:-140deg"></i>
<i class="pt dot" style="--dx:-92px; --dy:58px; --c:var(--cyan); --s:9px; --t:.9s; --d:.06s; --rot:240deg"></i>
<i class="pt dot" style="--dx:96px; --dy:60px; --c:var(--pink); --s:11px; --t:.95s; --d:.03s; --rot:-200deg"></i>
<i class="pt dot" style="--dx:12px; --dy:106px; --c:var(--gold); --s:10px; --t:.85s; --d:.02s; --rot:160deg"></i>
<i class="pt dot" style="--dx:-14px; --dy:102px; --c:var(--violet); --s:9px; --t:.9s; --d:.05s; --rot:-120deg"></i>
<!-- sparkles -->
<i class="pt star" style="--dx:150px; --dy:-58px; --c:var(--gold); --s:16px; --t:1s; --d:.02s; --rot:320deg"></i>
<i class="pt star" style="--dx:-148px; --dy:-62px; --c:var(--violet); --s:14px; --t:1.05s; --d:.06s; --rot:-280deg"></i>
<i class="pt star" style="--dx:80px; --dy:-140px; --c:var(--cyan); --s:13px; --t:.95s; --d:0s; --rot:240deg"></i>
<i class="pt star" style="--dx:-76px; --dy:-146px; --c:var(--pink); --s:15px; --t:1s; --d:.05s; --rot:-210deg"></i>
<!-- tiny hearts -->
<i class="pt mh" style="--dx:44px; --dy:-148px; --c:var(--red); --s:14px; --t:1s; --d:.06s; --rot:260deg"></i>
<i class="pt mh" style="--dx:-48px; --dy:-152px; --c:var(--pink); --s:12px; --t:1.05s; --d:.1s; --rot:-230deg"></i>
<!-- confetti -->
<i class="conf" style="--c:var(--gold); --sw:34px; --t:1.25s; --d:.12s; --w:8px; --h:13px"></i>
<i class="conf" style="--c:var(--cyan); --sw:-30px; --t:1.4s; --d:.2s; --w:7px; --h:11px"></i>
<i class="conf" style="--c:var(--pink); --sw:46px; --t:1.3s; --d:.16s; --w:9px; --h:9px"></i>
<i class="conf" style="--c:var(--violet); --sw:-40px; --t:1.45s; --d:.26s; --w:7px; --h:12px"></i>
<i class="conf" style="--c:#ffffff; --sw:22px; --t:1.2s; --d:.1s; --w:6px; --h:10px"></i>
<i class="conf" style="--c:var(--gold); --sw:-24px; --t:1.35s; --d:.22s; --w:8px; --h:8px"></i>
<i class="conf" style="--c:var(--cyan); --sw:38px; --t:1.5s; --d:.3s; --w:7px; --h:13px"></i>
<i class="conf" style="--c:var(--pink); --sw:-34px; --t:1.28s; --d:.18s; --w:9px; --h:11px"></i>
</span>
</span>
</label>
<p class="meta">
<span class="counter"><span class="cs">12,04</span><span class="roll"><span class="strip"><i>7</i><i>8</i></span></span><span class="cl"> likes</span></span>
<span class="plus">+1</span>
</p>
<p class="hint">Click to like · click again to undo · repeat until satisfied</p>
</div>
<footer class="foot">1 checkbox · 14 @keyframes · 0 lines of JavaScript</footer>
</main>
</body>
</html>Reasoning trace (67,500 tokens)
The user wants a "like" button in pure CSS, no JavaScript, that does a crazy animation when clicked. This is a classic use case for the `:active` pseudo-class or the checkbox hack. Since no JavaScript is allowed, I'll use either:
1. A hidden checkbox + label technique (persistent state)
2. `:active` pseudo-class (only while mouse held down)
For a "crazy animation" that plays out after a click, the checkbox hack is better because the animation can trigger on the `:checked` state. But there's a subtlety: with a checkbox, the animation triggers when checked and we can make it play. If unchecked and rechecked, animation replays (CSS animations restart when the state changes).
However, one limitation: with a checkbox, toggling off and on replays the animation. That's fine.
Actually, an even nicer trick: use `:active` on a label to trigger a "burst" animation that plays while held... but the animation won't continue after release. The checkbox approach gives persistent "liked" state with heart turning red, plus particles exploding.
Let me design a really impressive pure-CSS like button:
**Concept**: A heart "Like" button. When clicked:
- The heart pops/scales with a bouncy spring
- Particles burst out in all directions (many small hearts/circles/dots)
- Ripple rings expand
- Confetti-like bits fly
- Text changes "Like" → "Liked!" maybe via CSS content swap
- Screen slightly shakes
- Counter increments? Hard without JS but we can swap text "12.4K" → "12.5K" using two stacked spans and opacity.
- Maybe rainbow hue rotation
- A "shockwave" ring
Implementation details:
- Hidden `<input type="checkbox" id="like">` with `<label for="like">` containing the button UI.
- Use `#like:checked + label ...` selectors to trigger animations.
- Particles: multiple `<i>` or `<span>` elements positioned at center, each with its own `--x`, `--y` custom properties for translation, and an animation that plays when checked. Use `animation-play-state` or apply animation only in checked state.
Actually to make animation play once on check: define `.particle { opacity: 0 }` and `#like:checked + label .particle { animation: burst 0.9s ease-out forwards; }`. When checked, animation runs. When unchecked, animation removed. When re-checked, runs again.
But particles animating "forwards" will stay at end state (opacity 0) which is fine.
One issue: when unchecking, there's a transition back. We can add an uncheck animation too (heart shrinks back to gray, maybe a small "suck back" effect).
Let me plan the layers of the crazy animation:
1. **Heart pop**: scale from 1 → 1.4 (squeeze) → 0.9 → 1.15 → 1 with cubic-bezier bounce. Heart color transitions from gray/dark to red gradient.
2. **Ripple rings**: 2-3 concentric rings expanding from the button with fading opacity and scale.
3. **Particle explosion**: ~12-16 particles (small hearts, circles, stars) flying outward in a radial pattern using custom properties `--dx` and `--dy`, each with rotation, scaling, and fade.
4. **Confetti**: little rectangles falling with rotation.
5. **Text change**: "Like" slides up and out, "Liked" slides in — using two spans and transform.
6. **Button background**: flashes/glow, maybe a subtle pulse animation looping while liked.
7. **Page shake**: a tiny shake on the whole card when clicked.
8. **Sound-like visual**: hue-rotate flash.
Also should add hover effects for polish.
Demo mode consideration: The instructions say games must start in self-playing demo mode. This isn't a game, so no demo mode needed. But something meaningful must be visible on first paint — a beautifully designed like button page is fine.
However — one thought: could I make it auto-demo? The rules only require games to self-play. For a like button, the static design plus maybe an animated hint (pulsing cursor or "click me" wiggle) is good. I could add a subtle idle animation suggesting clickability. Maybe an animated hand/cursor tapping hint via pure CSS animation, or just a gentle pulse on the heart with a tooltip "Click me!".
Let me design the page:
- Dark, premium background (deep navy/near-black with subtle radial gradient or grid).
- Centered card or just a centered composition.
- Big title like "Pure CSS Like Button" with a note "0 lines of JavaScript".
- The like button in the middle: pill shaped with heart icon + "Like" text.
- Maybe a second smaller like button variant? Keep focus on one hero button but maybe show a counter below.
- A footer hint: "Click it. Then click again." and "View source — it's just CSS."
Typography: use a system font stack or Google Fonts? No network requests except CDN allowed... Actually the rules say "No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task." The task doesn't mention any CDN, so I should avoid Google Fonts. Use system font stack. That's fine — I can use a nice stack like `"SF Pro Display", -apple-system, "Segoe UI", Inter, Roboto, sans-serif`.
Heart icon: draw with CSS (two circles + rotated square) or use an inline SVG inside the label. Inline SVG is fine (it's not an external file). Actually a pure-CSS heart is more in spirit: `.heart { }` built with ::before/::after pseudo-elements. Let me do the classic CSS heart:
```css
.heart {
position: relative;
width: 20px; height: 20px;
transform: rotate(-45deg) scale(var(--s, 1));
}
.heart::before, .heart::after {
content: "";
position: absolute;
width: 20px; height: 20px;
border-radius: 50%;
background: currentColor; /* or inherit */
}
.heart::before { top: -10px; left: 0; }
.heart::after { left: 10px; top: 0; }
```
Hmm, the classic: a rotated square with two circles. The square itself has the background too.
```css
.heart {
width: 16px; height: 16px;
background: currentColor;
transform: rotate(45deg);
position: relative;
}
.heart::before, .heart::after {
content: "";
width: 16px; height: 16px;
background: currentColor;
border-radius: 50%;
position: absolute;
}
.heart::before { top: -8px; left: 0; }
.heart::after { left: -8px; top: 0; }
```
Wait let me get this right. Classic CSS heart:
```
.heart {
position: relative;
width: 20px;
height: 20px;
}
.heart::before, .heart::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: #000;
}
.heart::before { left: 10px; top: 0; } /* hmm */
```
Standard recipe: square rotated 45°, two circles on the top edge and left edge.
```css
.heart {
width: 20px;
height: 20px;
background: red;
transform: rotate(-45deg);
position: relative;
}
.heart::before,
.heart::after {
content: "";
width: 20px;
height: 20px;
background: red;
border-radius: 50%;
position: absolute;
}
.heart::before { top: -10px; left: 0; }
.heart::after { left: -10px; top: 0; }
```
Hmm, with rotate(-45deg), the square's top-left corner points left... Let me think. A square rotated 45° looks like a diamond. Adding a circle centered on the top edge midpoint and one on the left edge midpoint. With rotate(-45deg) the diamond points up-left... Actually the standard one uses rotate(-45deg) and circles at top and right, or rotate(45deg)... Let me just reason carefully.
Take a square, 20×20, positioned at (0,0)-(20,20). Place circle 1 centered at the middle of the top edge: (10, 0), so circle spans (-0, -10) to (20, 10) → `top: -10px; left: 0`. Place circle 2 centered at middle of the left edge: (0, 10), spans (-10, 0) to (10, 20) → `top: 0; left: -10px`. Now rotate the whole thing 45° clockwise (rotate(45deg)): the top-left region becomes the top point of the heart. Yes — this forms a heart pointing... the two lobes (circles) end up at top, tip at bottom. With rotate(45deg), point (0,0) (corner where circles meet) goes to top. Hmm, rotate(45deg) rotates clockwise in CSS. Point (0,0) relative to center (10,10) is (-10,-10). Rotating 45° clockwise: (x cos θ - y sin θ, x sin θ + y cos θ) with θ=45°... In CSS, positive angle = clockwise, with y axis pointing down. The math: new_x = x cos θ − y sin θ, new_y = x sin θ + y cos θ. For (-10,-10), θ=45°: new_x = -10(0.707) − (−10)(0.707) = 0; new_y = −10(0.707) + (−10)(0.707) = −14.14. So the corner (0,0) moves to (10, 10−14.14) = (10, −4.14), i.e., top center. And the bottom-right corner (20,20) relative to center is (10,10) → new = (0, 14.14) → bottom center. So with rotate(45deg), corner at top = intersection of the two circles = the cleft between lobes... wait no. Circles are at top edge middle and left edge middle. Their intersection region is near corner (0,0). So corner (0,0) at top means the cleft between lobes is at top and the tip (opposite corner (20,20)) at bottom. That's a correct heart! Great: `transform: rotate(45deg)` with circles at `top: -10px; left: 0` (wait, circle 1 centered at (10,0): left = 10−10 = 0, top = 0−10 = −10 → `left: 0; top: -10px`) and circle 2 centered at (0,10): `left: -10px; top: 0`.
So:
```css
.heart { width: 20px; height: 20px; background: c; transform: rotate(45deg); }
.heart::before { content:""; position:absolute; width:20px; height:20px; background:c; border-radius:50%; left:0; top:-10px; }
.heart::after { same but left:-10px; top:0; }
```
Hmm wait, that gives lobes... circle 1 at top edge, circle 2 at left edge — both adjacent to corner (0,0). Yes correct.
But scaling the heart: if I scale via the container, the rotate(45deg) is on the heart itself. I can wrap: `.heart-wrap { transform: scale(1); animation: pop ... }` and inside, `.heart { transform: rotate(45deg) }`. Or put both on heart: `transform: rotate(45deg) scale(var)`. For the pop animation on the heart, I'd animate the wrapper to avoid conflicts.
Actually, simpler and cleaner: use an inline SVG heart path inside the label. SVG in HTML is inline, not external. That's much easier to style (fill via currentColor, scale via transform). But "pure CSS" spirit — inline SVG is still no-JS; the animation is pure CSS. I think inline SVG is acceptable and gives a crisp heart shape. But some might argue "pure CSS" means CSS-drawn. Either is fine; the key constraint is no JavaScript. I'll use inline SVG for the heart glyph for crispness, but draw particles with CSS.
Hmm, actually, let me reconsider — a CSS-drawn heart is a nice flourish to brag about ("even the heart is drawn with CSS"). Let me do the CSS heart. It's a fun detail. But the pop animation with scale + the heart shape rotate... Manageable with a wrapper.
Let me structure:
```html
<input type="checkbox" id="like" class="sr-toggle">
<label for="like" class="stage">
<span class="btn">
<span class="heart-wrap"><i class="heart"></i></span>
<span class="label-text">
<span class="t like-t">Like</span>
<span class="t liked-t">Liked</span>
</span>
<span class="rings"><i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i></span>
<span class="burst">
<i class="p p1"></i> ... many particles
</span>
</span>
</label>
```
Wait — particles bursting out of the button would be clipped if overflow hidden. I need them absolutely positioned relative to button but overflowing; ensure no overflow:hidden on parents. Also the "crazy" aspect: maybe particles fly across the whole page? Keep them within ~300px radius, overflow visible, page overflow-x hidden maybe not needed if container is centered with enough room. Viewport 1280×800 — button centered, particles flying 200px out is fine. On mobile 375px — particles 200px could overflow horizontally causing scroll. Set `overflow-x: hidden` on body? The rule says no horizontal scrollbar. Particles absolutely positioned within a centered container — if a particle goes beyond viewport width, with `position: absolute` inside a relatively positioned parent, it can extend the scrollable area. To be safe: `body { overflow-x: hidden; }` or clip. But overflow hidden on body could clip vertically... particles also fly up. `overflow: hidden` on a full-viewport wrapper... Actually `overflow-x: hidden` on html/body is standard and fine. Vertical scrolling: the page should fit in 800px height anyway. I'll make the page 100vh flex-centered with `overflow: hidden` on body to contain everything. That's cleanest: fixed full-screen stage, everything centered. But if content is taller than viewport on small screens, overflow hidden would cut it. I'll design compactly so it fits 100svh. Use `min-height: 100svh` with flex center, and `overflow: hidden` on a fixed-position backdrop... Let me just use body overflow hidden and ensure content fits: title, button, hint, footer — fits in 800px and even 600px height. Fine.
Now the crazy animation choreography on `:checked`:
Timeline (t=0 at click):
- 0–0.15s: button scales down slightly (press feel) — actually press feel comes from `:active` on label. Combine: `:active` gives squish, `:checked` gives the explosion.
- 0–0.6s: heart pop — scale 1 → 1.6 with a springy keyframes: 0%: 1, 30%: 1.5, 45%: 0.85, 60%: 1.25, 75%: 0.95, 100%: 1.15. Color flips to red gradient.
- 0.1s–0.7s: three rings expand from button center: each ring scales 0.3 → 2.2 and fades out, staggered delays.
- 0.05s–0.9s: 14 particles fly outward with individual --dx/--dy, spinning, scaling down, fading. Some are circles, some mini-hearts, some stars/sparkles (4-point via clip-path or rotated squares).
- 0.3s–1.4s: confetti — 10 small rectangles pop up above the button then fall with rotation and sway, fading at bottom. Hmm, gravity curves in pure CSS: use two nested elements — outer animates X (linear), inner animates Y with ease-in (gravity-like). Classic technique. That's fancy; let me do it for a few confetti pieces.
- Text swap: "Like" slides up out, "Liked" bounces in. Implement with two absolutely stacked spans in a fixed-width/hight box; checked state triggers animations.
- Counter: "1,204" → "1,205" — flip. Use a vertical strip of numbers translating? A digit roll: container overflow hidden height 1em, inner column with two numbers, translateY. Do for last digit; earlier digits static. Nice detail. Or simpler: two stacked spans cross-fading with slide.
- Button style: outline/border becomes red-tinted, background gets soft red glow, subtle looping pulse while liked.
- Screen shake: keyframes on the whole card, translate few px, rotate tiny, 0.4s.
- Maybe a big ghost heart flying up from behind the button to the top of the screen fading — like Twitter's like burst. Yes! A large heart that flies upward and fades.
Also on **uncheck**: heart deflates (scale down wobble), color back to gray, "Liked" swaps back to "Like", counter decrements. Add a "whoosh back" — small implosion. Maybe reverse: rings implode? Keep it simpler: a quick deflate animation and color transition.
Number of DOM elements: particles ~16 + confetti ~8 + rings 3 + hearts... fine.
Star sparkle shape: use clip-path polygon for a 4-point star: `clip-path: polygon(50% 0, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0 50%, 39% 39%)`. Nice.
Mini hearts as particles: reuse the CSS heart at small scale. Each particle `<i>` with class p and modifier for shape and custom props:
```css
.p { position: absolute; left: 50%; top: 50%; ... }
.p.dot { width: 10px; height: 10px; border-radius: 50%; }
```
For a mini heart particle, I'd need pseudo-elements — but `<i>` has ::before/::after available. OK: `.p.heart-p` builds the heart with pseudos. But then transform on the particle includes translate(-50%,-50%) + translate(var(--dx), var(--dy)) + rotate + scale — all in keyframes. Fine.
Actually, to reduce complexity, particles can all be dots/stars/sparkles, and the mini-heart shape only for 2-3 hero pieces. Let me include: 8 dots (varied colors), 4 stars, 2 mini hearts, plus the big ghost heart. Plus confetti rectangles (8). Plus rings.
Let me also add a hue-flash: the button background briefly cycles hue? Maybe a quick white flash overlay on the button.
Counter with digit roll: Let's do "12,047" → "12,048"? Compute: last digit strip has 7 and 8. Structure:
```html
<span class="count">
<span class="digits">12,04</span>
<span class="digit-roll"><span class="digit-strip"><i>7</i><i>8</i></span></span>
</span>
```
Wait, but should the counter be near the button or below? Put it below the button: "12,047 likes". On like: rolls to 12,048 and text becomes "likes" ... maybe also color accent.
Hmm — actually let me reconsider: maybe simpler numbers: "999" → "1,000"? That changes all digits. Keep last-digit roll: 12,047 → 12,048. Also add a tiny "+1" that floats up on like. Yes! A "+1" bubble floating up — cute and clearly CSS.
Idle state attract: a subtle floating "Click me ↓"? Better: a small pulsing halo behind the button and a cursor hint. Also a looping gentle wiggle on the heart every few seconds to invite a click. And text under: "Go on, click it. It's pure CSS."
Page design: I want it to look striking, not generic. Dark background with subtle radial vignette and faint dotted grid. Big display typography for the heading: "The Like Button" with an outlined/gradient style. A small badge "0 JavaScript · 100% CSS". Center the button. Under it the counter. Bottom hint line: "Click to like · click again to unlike". Maybe also show "double-click" easter egg? Pure CSS double-click... not reliably.
Let me also consider adding a second smaller demo row: a minimal icon-only heart variant? Might dilute focus. Keep one hero, but make it magnificent. Actually a small extra could show versatility... I'll keep single hero to ensure polish, but maybe add a subtle "tap" hint animation using a CSS-drawn cursor? A circle with a ripple, like touch indicator, near the button, looping: appears, taps (scales), ripple. That could nicely invite clicks and shows something moving on first paint. Let me add a small animated "click hint": a pulsing dot with expanding ring next to the button or below: "← try it" with arrow? Simpler: under the counter, text "click the button" with an animated bouncing arrow pointing up at it. Hmm, arrows are easy with a chevron made of borders or an inline SVG. I'll do a small animated mouse/cursor... Let me do a simple elegant approach: a caption "Click it — no, really" and a looping attention pulse on the button itself (soft ring pulsing behind it every 2.5s until first interaction... can't detect "first interaction" without JS, but pulse can continue always subtly — or stop when checked: `#like:checked ~ ... .pulse { animation: none }`. Nice: pulse stops once liked. But if they unlike, pulse resumes — fine, even helpful.
Also first-paint requirement: something meaningful visible — yes, whole page. Good.
Now, let me write the keyframes.
**Pop keyframes (heart wrap):**
```css
@keyframes pop {
0% { transform: scale(1); }
25% { transform: scale(1.55); }
40% { transform: scale(0.8); }
55% { transform: scale(1.3); }
70% { transform: scale(0.95); }
85% { transform: scale(1.12); }
100% { transform: scale(1.05); }
}
```
Applied with `.9s cubic-bezier(.22,1.4,.36,1)` — actually keyframes themselves carry the bounce; use ease. End at 1.05 (slightly bigger when liked). On uncheck, animation removed, transform returns to base — add transition so it eases back. But when unchecking, I want a deflate: define uncheck animation? CSS can't easily animate on uncheck (state-based). When unchecked, the checked animation is removed → element jumps to base. To smooth, give `.heart-wrap { transition: transform .3s }`. But transition doesn't apply when animation removed... transitions apply when property values change due to class removal? The animation was overriding transform; when removed, computed value changes from animated value to specified value — transitions do fire on that change in modern browsers? Actually, when a CSS animation is removed, the property snaps back; transitions generally do NOT animate from an animation's final state... The transition would be from the current computed value? In practice, when an animation is removed, the transition may trigger from the base-to-base (no change) — animations don't contribute to starting values for transitions reliably. Simplest: accept a snap-back, or give unchecked state its own "unlike" animation on a different property timing. Alternative approach: apply a deflate animation in unchecked state? But unchecked is the initial state — it would run on page load. Hmm, that's actually fine if subtle? No — running deflate on load looks odd.
Better approach: instead of animating `transform` scale end-state, keep heart's resting checked transform as scale(1.08) via a CSS variable and use keyframes that end matching it. For uncheck smoothness, I can put the like-state transform change on a *different* element than the pop animation element. E.g.:
- `.heart-wrap` handles pop animation (checked).
- `.heart-wrap` also has base transform scale(1) / checked scale(1.1) with transition — conflicts with animation on same property. When animation runs it overrides; after it ends (forwards? no fill needed since final keyframe = 1.1 = checked value) — if I set `animation-fill-mode: forwards` with last keyframe scale(1.1), and checked base also scale(1.1), then when animation ends, computed stays 1.1 either way. On uncheck: animation removed, base becomes scale(1) — property changes 1.1 → 1. Does transition animate? The element's specified value changes because the selector no longer matches; transition should animate from current computed (1.1, from animation-fill... once animation is removed, before/after get recomputed). Hmm, transitions from animation-filled states: when an animation is removed, the change is usually not transitioned (the animation's effect vanishes, and the property changes to new specified value — transitions compare old and new computed values; old computed just before removal was animation value 1.1? If the animation already finished without forwards fill, computed was 1.1 from the checked rule). Let me simplify: checked rule sets `transform: scale(1.1)` with `transition: transform .35s cubic-bezier(...)`. The pop animation also runs on check with keyframes ending at 1.1 and fill-mode none. During animation, animation wins. After it ends, transform = 1.1 (specified). On uncheck: specified goes 1.1 → 1, transition triggers (both are specified values, transitions work).
But wait — while animation is running (0.9s), and then user unchecks at 0.5s mid-animation: animation removed, transform snaps from mid-animation value to... transition from current? Snap issues are edge-case; acceptable.
Actually a cleaner classic trick for the uncheck: animate on BOTH states with the "unlike" animation triggered by removal using `transition` on all visual props (color, scale). I'll rely on transitions for the un-liking: heart color transitions back to gray over .3s, scale eases back. And to make unlike feel deliberate, add a small counter wobble on uncheck? Can't easily. Fine — smooth transitions on uncheck are acceptable and feel intentional.
Hmm, actually I CAN animate on uncheck: use a sibling technique — when unchecked, apply an animation that is paused at 0% normally?? No. Another trick: `:not(:checked)` animations run on load... Skip. Transitions it is. Actually wait — there IS a neat trick: the label is `:active` during any click (both check and uncheck). I can use `label:active` to trigger a "press squish" animation that plays on every press regardless of direction. Combined with checked-state transitions, unliking will squish + smoothly return.
**Rings:**
```css
.ring { position:absolute; inset: 0; border-radius: 999px; border: 2px solid var(--red); opacity: 0; }
#like:checked ~ .stage .ring { animation: ring 0.8s ease-out; }
@keyframes ring {
0% { transform: scale(.6); opacity: .9; }
100% { transform: scale(2.4); opacity: 0; }
}
```
Stagger via delays: r1 0s, r2 .12s, r3 .24s. Different border colors (white, red, pink). Maybe use box-shadow glow ring instead. Rings sized relative to button (inset 0 with same radius). Scale from center.
**Particles:** each `.p` absolutely at button center; keyframes:
```css
@keyframes fly {
0% { transform: translate(-50%,-50%) translate(0,0) scale(.4) rotate(0deg); opacity: 1; }
70% { opacity: 1; }
100% { transform: translate(-50%,-50%) translate(var(--dx), var(--dy)) scale(1) rotate(var(--rot)); opacity: 0; }
}
```
Hmm, scale up then down: add mid keyframe 60% scale 1.15. Use per-particle `--dx/--dy/--rot/--c/--d` (delay/duration). Vary distances 90–170px. Colors: reds, pinks, gold, white, cyan? "Crazy" — a colorful burst reads crazy. Palette: #ff2d55 (heart red), #ff6b81 pink, #ffd60a gold, #5ac8fa blue, white, #af52de purple. Festive.
**Confetti with gravity:** nested:
```html
<span class="confetti c1"><i class="c-outer"><i class="c-inner"></i></i></span>
```
Outer animates translateX linear over 1.2s (slight horizontal drift), inner animates translateY: 0 → -60px (up) then +240px down with ease-in for gravity... A single keyframe set can do up-then-down:
```css
@keyframes fall {
0% { transform: translateY(0) rotate(0); }
30% { transform: translateY(-80px) rotate(180deg); }
100% { transform: translateY(260px) rotate(720deg); opacity: 0 }
}
```
With easing per keyframe segment — CSS applies one timing function across whole animation... you can specify `animation-timing-function` inside keyframe blocks to apply to the segment starting at that keyframe. So: 0% { animation-timing-function: ease-out } for the rise, 30% { animation-timing-function: ease-in } for the fall (gravity!). And rotation throughout. Opacity fade at 90-100%. That's a solid single-element confetti.
**Ghost heart flying up:** a big heart (scale ~2.5, blurred slightly, low opacity red) positioned behind button, on check: animate translateY(-45vh) scale up + fade, 1.2s ease-out. Wait, the Twitter-style burst has the big heart appear from behind. Let me do: starts scale(0) behind, pops to scale(1.6) then floats up fading. Two-phase keyframes.
**+1 float:** small text "+1" rising from counter, fade. Position near count.
**Shake:** on the whole `.scene` card:
```css
@keyframes shake {
0%,100% { transform: translate(0,0) rotate(0); }
20% { transform: translate(-6px,3px) rotate(-.6deg); }
40% { transform: translate(5px,-4px) rotate(.5deg); }
60% { transform: translate(-4px,2px) rotate(-.4deg); }
80% { transform: translate(3px,-2px) rotate(.3deg); }
}
```
0.5s. Careful: shaking the card that contains the button while particles fly — particles inside will shake too (fine, adds chaos). But shaking might cause overflow — we have overflow hidden on body. But careful: shake on the container that also is flex-centered — transform on it is fine.
Also flash: a full-viewport radial flash? Maybe brief white flash overlay at 8% opacity on check — "crazy". Could be too much; a subtle vignette pulse in brand red. I'll add a quick radial glow behind the button expanding — the rings do that already. Maybe skip full-screen flash, keep it classy-crazy.
**Text swap:**
```html
<span class="txt">
<span class="t t-like">Like</span>
<span class="t t-liked">Liked</span>
</span>
```
`.txt { position: relative; width: 4.5ch?? }` Since "Like" and "Liked" differ in width, animate the txt width too: checked: width from 3.6ch to 4.6ch with transition — animating width between ch values, transition works (both lengths). Set `.txt { display:inline-grid; }` with both spans in same grid cell (`grid-area: 1/1`), so width = max of the two, avoiding width animation entirely! Both overlap; hide one via translateY + opacity/clip.
- t-like: default visible; on check: animation slides it up & out (translateY(-120%), opacity 0).
- t-liked: default translateY(120%) opacity 0; on check: animation slides in with overshoot (keyframes: from translateY(140%) to -15% to 0). Colors: liked text in red.
On uncheck: reverse via transitions (translate/opacity transition .3s). Good.
**Counter roll:**
```html
<span class="counter">
<span class="num">12,04</span><span class="roll"><span class="strip">78</span></span>
</span>
```
Hmm, strip needs digits stacked vertically: `<span class="roll"><i>7</i><i>8</i></span>` with roll overflow hidden, height 1em... Actually simpler: two numbers cross-fade with slide (same technique as text). But a rolling digit is delightful. Implementation: `.roll { display:inline-block; height: 1.2em; overflow: hidden; } .roll .strip { display:flex; flex-direction: column; transition: transform .5s cubic-bezier(.2,.9,.3,1.4); } .roll i { display:block; line-height: 1.2em? }` With checked: `transform: translateY(-1.2em)` — wait, using translateY(-100%) of first item height... Let me set each digit `height: 1.2em` wait — I'll use exact em: `.roll { height: 1.3em; } .roll i { display:block; height: 1.3em; line-height: 1.3em?? }` Hmm, actually, `line-height` and vertical alignment with surrounding text — put the counter on its own line (a separate line below button), so alignment only matters among its own parts. Use fixed px? Font-size of counter, say 15px → each digit row 20px. Let me use em consistently: `.roll { display: inline-block; vertical-align: bottom; height: 1em; overflow: hidden; }`... The digits around it have line-height different from 1. Risk of misalignment between static digits and rolled digit. To sidestep: make the whole counter a flex row with `align-items: baseline`? The roll is inline-block... Simplest robust: give counter `display:inline-flex; align-items: stretch` and fixed `line-height: 1` for all its parts; digits use `line-height: 1`... baseline alignment across inline-block with overflow hidden is tricky (overflow hidden changes baseline to bottom edge!). Classic gotcha: inline-block with overflow:hidden has baseline = bottom margin edge. So aligning rolled digit with neighbors: set all digit containers to same structure or vertical-align: top with matched heights.
Alternative: crossfade whole counter: "12,047 likes" → "12,048 likes" as stacked crossfading spans (same technique as Like/Liked text). Much simpler, still nice. But the rolling digit is a lovely touch... Let me try making it robust: put the entire counter as inline-flex with `align-items: center`, each part `line-height: 1`, static digits as flex items, roll as flex item with `height: 1em; overflow: hidden` — hmm 1em box around a glyph: digits render fine if font-size equal and each strip item is `height: 1em; line-height: 1` — glyph might overflow 1em box vertically for some fonts but overflow hidden would clip... digits typically fit within ~0.72em cap height above baseline; with line-height 1 and centered... Risky clipping. Use height: 1.4em? Then strip items height 1.4em, translateY(-1.4em) on check. With align-items:center, static digits vertically centered relative to roll box... static digit text has its own line box. If I make ALL items `display:flex; align-items:center; height:1.4em`, and counter `display:inline-flex`, everything aligns by construction, not baseline.
Actually — you know what, let me simplify: the counter is on its own line below the button, centered. I'll build the whole counter as inline-flex with items of equal height 1.5em, each digit centered. Static digits: `<b>1</b><b>2</b><b>,</b><b>0</b><b>4</b>` etc. Or just static text "12,04" + roll for last digit + " likes" label. Static text inside a flex item with height 1.5em and display flex center — fine.
Hmm, but do I even want a comma number? Let's use a cleaner small number for punch: "9,999"? Rolling last digit 9→0... The comma: "9,999" → "10,000" changes everything. Use "12,047" → "12,048": static "12,04" + roll(7/8) + " likes"? Reads oddly split but visually it's "12,047". Wait — static part "12,04" then roll "7"→"8": displays "12,047" → "12,048".
Also the "+1" float appears above counter.
Also make the counter bold with tabular numbers: `font-variant-numeric: tabular-nums`.
**Liked pulse loop:** while checked, heart has a slow heartbeat: `animation: heartbeat 1.6s infinite` — scale 1 → 1.12 → 1 → 1.06 → 1 with double-beat timing (lub-dub). Cute! Since pop animation ends and heartbeat begins — can I chain? `animation: pop .8s, heartbeat 1.6s 0.8s infinite`. Both animate transform — the later one (heartbeat) overrides once it starts (animations later in list win). pop runs 0-0.8s, heartbeat delayed 0.8s. During 0-0.8s pop wins? Both are active; the last in the list takes precedence — heartbeat is not active until delay expires, so pop applies, then heartbeat. Heartbeat's keyframes should start at scale matching pop end (1.08). This works.
Hmm wait, careful: while checked, if pop has `fill: forwards`... I'll avoid forwards; checked base transform = scale(1.08) matches pop end. Heartbeat keyframes go 1.08 → 1.2 → 1.05 → 1.14 → 1.08.
Actually — even better idea for heartbeat: make it a REAL heartbeat pattern: quick double pulse. keyframes:
```
0% { scale 1.08 }
12% { 1.22 }
24% { 1.08 }
36% { 1.18 }
50% { 1.08 }
100% { 1.08 }
```
With duration 1.5s infinite.
**Button glow when liked:** box-shadow red glow that pulses subtly with heartbeat on wrapper... The glow can be on the button itself with transition + a separate soft pulse animation on box-shadow. Combine: checked button has `box-shadow: 0 0 0 1px rgba(red,.3), 0 8px 30px rgba(red,.25)` static + background tint transitions from dark glass to red-tinted. Fine.
**Press squish on label:active:** `label:active .btn { transform: scale(.93) }` with fast transition — but the btn also might have animation... The shake is on `.scene`, pop on heart, so `.btn` transform is free for press. Give `.btn { transition: transform .15s }` and `:active { scale .92 }`. But `:active` on label fires when pressing the label — which covers the whole label area. Make label only wrap the button. Also `#like:checked:active`? Not needed.
Wait, actually there's a subtlety: click on label toggles checkbox. The `:active` state applies while mouse down. Good — squish on press, explosion on release. Nice feel.
**Hover:** button lifts, border brightens, heart color slightly warms, maybe preview glow.
**Focus-visible:** accessibility — outline style. Since input is visually hidden, focus goes to input... clicking label moves focus? Clicking a label focuses/activates the input in most browsers (label forwarding). Keyboard: label not focusable directly, but input is — tab to checkbox (visually hidden but focusable — use `.sr` pattern with opacity 0 not display none). `#like:focus-visible + label .btn { outline }`. Nice touch.
Now — the "crazy" factor. Let me push it:
1. Big ghost heart flying up with rotation.
2. Confetti falling from above the button.
3. Rainbow hue-rotate sweep on the burst particles? Particles colored already.
4. Star sparkles (4-point) with rotation.
5. A "shockwave" — the entire page background flashes a subtle radial pulse: a fixed overlay `::before` on body with radial-gradient that animates opacity on check? CSS on body can't know about the checkbox unless checkbox is a descendant and we use `:has()`. Modern Chrome supports `:has()`! `body:has(#like:checked) .flash { animation: flash .6s }`. Since target is latest Chrome, `:has()` is safe. That simplifies EVERYTHING — I could style any ancestor. But keep the sibling selector `#like:checked + label` as primary; use `:has()` for page-level effects like background flash and vignette. I'll use both (sibling for most, :has for page glow). Both fine in latest Chrome.
Actually, since `:has()` is available, I could put the input anywhere. Keep it as first child of scene, label right after. Page-level: `body:has(#like:checked)` — but body's own background animation: use a `.bg-flash` fixed div.
Let me also consider **prefers-reduced-motion**: good practice, but for a demo whose entire point is animation, I'll skip or keep minimal. Skip to save space — no wait, it's good craft, but the task explicitly wants crazy animation. I'll skip (or add a tiny note). Skip.
Let me also think: multiple rapid re-clicks — toggling replays animations (removal/re-add). Great — repeated clicking = repeated explosions. That's the fun. I should note in UI: "click again... and again".
Now page layout & aesthetics:
- Background: deep space navy `#0b0d16` with a subtle radial gradient glow center-bottom in red hint? Plus fine dot grid (radial-gradient dots, 24px grid, very low opacity). Plus a vignette.
- Top: small badge pill: "PURE CSS · NO JAVASCRIPT" with a tiny lightning?? no emojis/icons — use a small CSS dot or SVG icon. I can inline a tiny SVG icon. Or a text badge with gradient border.
- Headline: "The Over-Engineered Like Button" — nice copy. Big, maybe gradient text (white → soft pink). Font: system stack, weight 800, tight letter-spacing. Maybe a two-line headline: "You're gonna like / this button." Hmm. Let me craft: "Click. Explode. Repeat." as a subheading? Headline: "The Like Button" big with a red accent? I like:
- Eyebrow badge: `● PURE CSS — ZERO JAVASCRIPT`
- H1: "You're going to *like* this." with "like" in red/gradient italic serif? Mixing a serif italic (Georgia) for the accent word is a classy touch with system fonts!
Actually: H1 "You're gonna like this." with "like" styled in a warm gradient + maybe the CSS heart inline after? Cute.
- Sub: "One checkbox, a pile of keyframes, and absolutely no JavaScript. Go ahead — press it."
- Center: THE button.
- Below: counter "12,047 likes" + "+1" float.
- Bottom hint: "Tip: click it again. And again. It never gets old." Or show interaction stats? Keep: small footer "Built with one `<input type=checkbox>` + 400 lines of CSS."
Layout: full viewport column: header top-left or centered? Centered column feels right for a hero demo. Let me do centered: badge, h1, sub, button (hero, with room around for explosion ~ needs vertical space ~ 300px above for confetti/ghost heart), counter, hint at bottom. 800px height: badge 30 + h1 60 + sub 24 + gaps + button 64 + counter 20 + hint. Fits with generous spacing.
Hmm wait — confetti falls DOWN 260px from around button top. If button is vertically centered, below it there's the counter and hint. Confetti falling over them is fine (they're just below, confetti fades). The counter at ~100px below button; confetti passes over — z-index confetti above. Fine, looks crazy-good.
Ghost heart flies UP 45vh from button — plenty of headroom since button is mid-page. It'll fly over the headline, fading — awesome.
Particles fly ±160px radially — will fly over text. All decorative, pointer-events none.
On mobile 375px: h1 ~28px, everything centered, particles ±160px horizontal → extends 160px beyond button edges: button ~150px wide centered in 375 → edges at ~112px from center; particles reach 160px from center → 48px beyond viewport → body overflow hidden handles it. Good.
Z-index layering:
- bg grid (fixed, -1)
- flash overlay (fixed, 999? pointer-events none, above content but it's just a translucent radial — above everything is fine, brief)
- scene content z 1
- button burst layers within button stack.
**Button design:** Pill, height 64px, padding 0 28px, dark glass: `background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02)); border: 1px solid rgba(255,255,255,.14); border-radius: 999px; backdrop-filter: blur(10px);` with inner highlight `box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 10px 30px rgba(0,0,0,.4)`. Heart 26px, gap 12px, text 17px semibold, letter-spacing .01em. Hover: border rgba(255,255,255,.3), transform translateY(-2px), glow warm. Checked: border red-ish `rgba(255,45,85,.5)`, background tint `rgba(255,45,85,.12)`, text "Liked" red, heart red gradient.
Heart colors: idle: `#8b93a7` (slate gray) → liked: gradient from #ff5c7a to #ff2d55? The CSS heart uses background + pseudos; can't gradient each circle seamlessly... A gradient on the square + circles separately would show seams. Use solid colors: idle `#94a0b8`... Transition color via `background-color` on all three (heart, ::before, ::after) — set via `currentColor`? Trick: set `color` on heart, and pseudos/background use `background: currentColor`. Then transitioning `color` on `.heart` transitions all! And for gradient — skip; solid #ff3b5f with a tiny white specular dot? I can add a glossy highlight: a small white blurred dot via an extra pseudo? ::before/::after already used for lobes. Put the gloss on the heart-wrap's ::after or a separate span. Optional — skip gloss, or use `filter: drop-shadow` for glow: `filter: drop-shadow(0 0 12px rgba(255,59,95,.6))` when liked. drop-shadow on the composite shape works since it applies to rendered silhouette. Nice.
Heart size: make it a bit bigger, 24px square → 24 tall, 36ish wide overall (lobes at -12 offsets). The heart visual box: the rotated square spans diagonal... overall bounding: circles at left -12 and top -12 with size 24: total bounds x: -12..24+? square 0..24, circle2 spans x -12..12, circle1 spans y -12..12. Overall 36×36 box before rotation; after rotate(45) around center (12,12)... corner (0,0) → distance from center sqrt(288)=16.97 → rotated stays within radius 16.97 of center: bounds roughly 34×34. In a 24px-tall button row it'll look ~30px tall visually. Let me use square 20px, circles 20px at -10 offsets → bounds 40 pre-rotation, ~28 after. Scale wrap to taste. I'll fine-tune with wrap scale 0.8 if needed. Actually let me define heart with size var: base 22px.
The heart-wrap: `width/height: 30px; display:grid; place-items:center;` and heart centered inside via absolute? The heart's box including negative offsets — center it manually: place heart-wrap 30×30, heart at left: (30-22-?)/... The heart visual center: square center (11,11) of 22px square + pseudos extending left/top. Visual bounding box: x from -22? circle2 at left:-22? No wait: size 22 → circle offsets -11? Let me recompute with size s: square s×s at (0,0). Circle1: top: -s/2? Circle centered at (s/2, 0): box from (0, -s/2) size s → `left: 0; top: -s/2`. Circle2 centered at (0, s/2): `left: -s/2; top: 0`. Visual bounds pre-rotation: x: -s/2 .. s, y: -s/2 .. s. Center of visual = (( -s/2 + s )/2, same) = (s/4, s/4)? Bounds x: -s/2 to s → width 1.5s, center x = s/4. Hmm so visual center at (s/4, s/4) whereas rotation origin default center of the square element (s/2, s/2). Rotation about (s/2, s/2) — visual center offset from rotation center by (s/4, s/4)... For a heart, the shape isn't symmetric anyway (cleft top, tip bottom) — after 45° rotation the tip is at bottom center. The rotated shape's bounding box: all points within distance from origin (s/2,s/2): farthest = corners at distance s/√2 ≈ 0.707s. So rotated shape spans at most 1.414s × 1.414s around center. With s=22: ~31px box centered at (11,11) of the square element — but element box is only 22×22, so it overflows the element box symmetrically? The shape after rotation: cleft-top, lobes... rotated content may overflow the 22×22 element box. For layout inside wrap: position heart absolutely with `left: 50%; top: 50%; margin: -s/2` → the square's center aligns with wrap center; rotated visual roughly centered.
To be safe, wrap 34×34, heart s=22 absolutely centered in it. The visual heart appears ~30px tall — good for a 64px button.
Alternatively... honestly, inline SVG heart would be 10x easier to position: `<svg viewBox="0 0 24 24"><path d="M12 21s-6.7-4.3-9.3-8.6C.9 9.6 2.3 5.6 6 5.6c2.2 0 3.6 1.2 4.4 2.5... "fill="currentColor"/></svg>`. Hmm. But CSS-drawn is a flex and bragging point. I'll go CSS-drawn for the main heart, and it doubles as mini-heart particles. Actually for particles, mini hearts via same technique at s=8.
Let me now also add **sparkle stars** — clip-path 4-point star, gold.
DOM plan:
```html
<body>
<input type="checkbox" id="like">
<div class="bg"></div>
<div class="flash"></div>
<main class="scene">
<header>
<span class="badge"><i class="badge-dot"></i> Pure CSS · zero JavaScript</span>
<h1>You’re gonna <em>like</em> this.</h1>
<p class="sub">One checkbox. A mountain of keyframes. No scripts were harmed.</p>
</header>
<label class="stage" for="like" aria-label="Like this demo">
<span class="pulse"></span>
<span class="btn">
<span class="heart-wrap">
<i class="heart"></i>
</span>
<span class="txt">
<span class="t t-like">Like</span>
<span class="t t-liked">Liked</span>
</span>
</span>
<!-- effects anchored to stage center -->
<span class="fx">
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
<span class="ghost"><i class="heart g"></i></span>
<span class="p-dot">... 10 dots each <i style="--dx:.."></i></span>
...
</span>
</label>
<div class="meta">
<span class="counter">...</span>
</div>
<p class="hint">Click it. Then click it again. It scales.</p> <!-- pun -->
</main>
</body>
```
Positioning of fx: `.stage { position: relative; display: inline-block; }` — the fx span absolute inset 0 covering the label (= button area), particles centered at 50%/50%. Rings: centered on button: position absolute, inset: -10px?, matching button radius. Since stage size = label size = button size (label wraps just the button), fx inset 0 = button box. Rings absolute inset 0 border-radius 999px scale from 1 → 2.6. Good. Particles at left/top 50% + translate(-50%,-50%) in keyframes.
Confetti: positioned above button center, falling down past it. Anchor at 50% 50% too, with the fall keyframes handling both up-then-down.
Ghost heart: anchored center, scale big behind: z-index below button (behind: z -1 within stage stacking... stage creates stacking context? label with position relative — z-index auto, children stack in order. Put fx BEFORE btn? But fx needs to be after in markup for sibling selectors? No — selectors don't care about order: `#like:checked + label .fx i` etc. Order in DOM controls paint order. Put `.ghost` with z-index... Since fx and btn are siblings inside label, set `.fx { z-index: 2 }` and ghost inside fx with z-index 0?? Ghost must render BEHIND button: give `.ghost { z-index: -1 }` inside fx? fx with z-index 2 creates stacking context, so ghost's -1 stays within fx → still above btn (fx is above btn). To get ghost behind button: separate ghost outside fx, placed before btn in DOM with position absolute. Let me structure label children in paint order: ghost (first, absolute, z 0), pulse (behind), btn (relative z 2), fx (last, z 3, contains rings/particles/confetti/+1). Wait pulse behind button too: pulse absolute z 0 behind btn z 2.
Actually simpler: one `.fx-back` span (ghost, pulse) before `.btn`, one `.fx-front` after (rings, particles, confetti, sparkles, +1). All absolute inset 0. pointer-events none on label children except btn? Label click anywhere toggles — that's fine/desirable (bigger hit area). Hmm, but label containing absolutely positioned fx spanning exactly the button area — same size. OK.
Rings should be centered on the button; fx inset 0 matches button box.
Now the checkbox input: visually hidden but accessible:
```css
#like { position: absolute; opacity: 0; width: 1px; height: 1px; ... }
```
Use `.sr-only` style. Must not use display:none (kills focus). Put it as first child of body, then `.scene` — sibling selector `#like:checked ~ .scene label ...`. That requires more selector writing; alternatively input inside scene before label: `#like:checked + label ...` wait if input is inside .scene and label follows input: `#like:checked + .stage ...`. Let me place input immediately before the label inside main. And for the counter/hint inside `.meta` (after label), selectors `#like:checked ~ .meta .counter` — works since both descendants of main? Sibling combinator needs same parent: input, label, .meta all children of `.scene` (main). `#like:checked ~ .meta` yes.
For body-level flash with :has(): `body:has(#like:checked) .flash { animation: ... }`.
Now colors: `--red: #ff2e63; --pink: #ff7a9c; --gold: #ffd166; --cyan: #4cc9f0; --violet: #b388ff;` bg: `#0a0c14`.
Let me write particle configs (dots): 10 dots around circle: angles every 36°, distance varying 100–170. Compute dx, dy roughly (I'll just eyeball):
- 0° right: (140, 0) → but with slight randomness: (135, -18)
- Let me list 12 particles alternating shapes:
dots (6):
1. --dx: 120px; --dy: -10px; color gold
2. --dx: 95px; --dy: -70px; color cyan
3. --dx: 30px; --dy: -110px; color pink
4. --dx: -60px; --dy: -95px; color violet
5. --dx: -120px; --dy: -25px; color red
6. --dx: -95px; --dy: 60px; color gold
7. --dx: -20px; --dy: 105px; color cyan — wait below button particles will overlap counter... they fade fast; ok maybe keep mostly upward-biased since confetti falls below. Let me bias up: most particles upward/sideways, few slightly down.
8. --dx: 100px; --dy: 65px; color pink
stars (4): bigger distance:
9. --dx: 150px; --dy: -60px; --rot: 300deg, gold star
10. --dx: -150px; --dy: -55px; violet star
11. --dx: 80px; --dy: -140px; cyan star
12. --dx: -75px; --dy: -145px; pink star
mini hearts (2):
13. --dx: 45px; --dy: -150px; red mini heart, rot 40deg
14. --dx: -50px; --dy: -155px; pink mini heart, rot -30deg
Each with staggered delays 0–0.12s and durations .7–1s, different sizes 8–14px.
Confetti (8): thin rects 10×5 px, colors mixed, anchored near button top (top: 8px?), keyframes: rise then fall with rotation. Horizontal drift via a slight translateX in keyframes per piece (--drift). Position start: left: 50%, top: 30% of button. Actually anchor at center, fine.
Confetti keyframes with per-segment timing:
```css
@keyframes drop {
0% { transform: translate(-50%,-50%) translateX(0) translateY(0) rotate(0); opacity: 1; animation-timing-function: cubic-bezier(.1,.6,.3,1); }
35% { transform: translate(-50%,-50%) translateX(var(--sway,10px)) translateY(-90px) rotate(200deg); opacity: 1; animation-timing-function: cubic-bezier(.5,0,.9,.6); }
100% { transform: translate(-50%,-50%) translateX(calc(var(--sway,10px)*2)) translateY(240px) rotate(680deg); opacity: 0; }
}
```
Wait — animating translateY from 0 to -90 to +240 with segment easing gives pop-up then gravity fall. Also opacity stays 1 until 85%: add 80% keyframe opacity 1, 100% 0. Keyframe at 35% needs same opacity... I'll add opacity interpolation: 0%:1, 75%:1, 100%:0 — but transform keyframes at 35% must also exist; separate concerns: multiple properties across keyframes — unspecified props interpolate between defined keyframes for that property. opacity defined at 0,75,100 interpolates smoothly; transform defined at 0,35,100.
But careful: `animation-timing-function` inside a keyframe applies to the segment starting at that keyframe for ALL properties. Setting it at 0% and 35% as above works for transform; opacity also gets those easings — negligible.
Also each confetti needs rotation around own center while rect is offset — transform order: translate(-50%,-50%) centers it; then rotate. Fine.
Confetti pieces colors and sway values, delays .1–.25s, duration 1.1–1.5s. Only run when checked: `#like:checked + .stage .conf { animation: drop var(--cd,1.2s) ease ...` wait, timing functions are inside keyframes; the animation's own timing function default ease would apply to segments... When keyframes specify timing functions, they override per segment. But my 0% and 35% define them; the 75%/100% opacity segment uses... 75% has no timing → uses the animation's timing function (ease). fine.
Hmm wait, actually there's an issue with confetti running `forwards` — ends at opacity 0, ok. On uncheck, removed. Good.
Ghost heart keyframes:
```css
@keyframes ghost {
0% { transform: translate(-50%,-50%) scale(.2) rotate(0); opacity: 0; }
25% { opacity: .5; } /* hmm */
...
}
```
Let me design: ghost is a big heart (s=64 → visual ~90px) with soft blur + low opacity red, starting behind button invisible. On check: scale .3→1.4 quickly (0-30%), then rise translateY(-46vh)? combined:
```
0% { transform: t(-50%,-50%) scale(.3) translateY(0) rotate(-8deg); opacity: 0 }
15% { opacity: .55 }
30% { transform: ... scale(1.15) translateY(-8%) rotate(6deg); opacity: .55 }
100% { transform: ... scale(1.6) translateY(-320px) rotate(-14deg); opacity: 0 }
```
Hmm — scale + translateY both in transform; combined in single transform strings per keyframe. Since heart inner has rotate(45deg) built in, the ghost wrapper's rotate adds — fine, looks like wobble. duration 1.3s ease-out... use cubic-bezier(.2,.7,.3,1). It should appear to launch from behind the button and float up-right? Straight up with slight rotation drift is elegant. Maybe two ghosts? One big one is cleaner. Actually two, offset slightly, delays 0 and .1s, one left one right? Nah, one big + it's the "spirit of the like" — but "crazy" says more. I'll do one large + a couple small ones already in particles.
Ghost heart color: `#ff2e63` with `filter: blur(1px) drop-shadow(0 0 25px rgba(255,46,99,.8))` and opacity .5. Behind the button initially — as it scales to 1.15 (~104px) it peeks around the 64px-tall button edges.
Rings: 3, delays 0/.1/.2, colors: rgba(255,255,255,.9)? Use mix: r1 border 2px solid #ff2e63; r2 2px #ffd166?; r3 2px white .5. Scale to 2.2/2.7/3.2? Since fx box = button box (~170×64), scaling non-uniformly? scale() is uniform on both axes — an ellipse... border-radius 999px on 170×64 → pill; scaled uniformly stays pill-shaped scaled — expanding pill rings. Actually that looks GOOD around a pill button (like echo of the button). Yes! Pill-shaped shockwaves.
But particles are centered in fx box = center of button. Good.
Also add a quick **button border flash**: on check, button gets animation border-flash? The bg tint transition suffices.
**The +1 float:** absolute near counter, on check: translateY(0→-34px), opacity 1→0, 0.9s, color red, delay .15s, font-weight 800. Position: relative to .meta. Since it's in .meta (sibling after label): `#like:checked ~ .meta .plus { animation: rise ... }`.
**Counter roll:** described above.
**Heartbeat loop** while liked.
**Idle attention pulse:** `.pulse` behind button: a pill ring softly pulsing (scale 1→1.25 opacity fade) every 2.8s, infinite. Hidden when checked (`animation: none; opacity: 0`) — or keep? Stop when liked; resume when unliked. But when unliked it resumes... fine.
Also — hint arrow? The pulse + copy should suffice.
**Background flash:** `.flash` fixed inset 0, radial-gradient at button position (center of screen roughly 50% 55%?) — the button sits around center; use `radial-gradient(circle at 50% 52%, rgba(255,46,99,.22), transparent 45%)`, opacity 0; on check: quick flash animation: 0% opacity 0 → 15% .9 → 100% 0, .7s. Using body:has().
Also maybe page-level subtle zoom punch: `body:has(:checked) .scene { animation: shake }` — shake the scene instead of card only. Wait I planned shake on `.scene` (main content wrapper) via `#like:checked ~ ...`? .scene is the parent of input — can't select ancestor with sibling combinators, but `:has()` handles: `body:has(#like:checked) .scene { animation: shake .5s }`. Hmm — careful: shake transform on .scene (main) also shakes the h1 etc. That's the point (impact). But transform on .scene containing the label + fx... particles shake too — chaotic good.
But wait — will `:has()` matching persist? `body:has(#like:checked) .scene { animation: shake .5s; }` — animation applies while checked... when uncheck, animation removed (snap). During check it runs once. But if animation is still "applied" (state) it won't restart on re-check unless removed first — toggling does remove/re-apply. However if user unchecks mid-shake, snap — negligible. BUT: one gotcha — animation on .scene with transform conflicts with layout? .scene is flex column centered; adding transform shake fine.
Also the flash overlay: `body:has(#like:checked) .flash { animation: flash .7s ease-out; }`.
Hmm, one more idea — **cursor**: make the whole page cursor default, button pointer. Trivial.
Let me also add **press ripple** inside button on :active: a circle scaling from click point — can't get click position in CSS; center ripple ok: `.btn::after` white radial scaling 0→1.4 opacity .25→0 on `label:active`. Actually `:active` styles only persist while held; animation restarts each press. `.stage:active .btn::after { animation: ripple .5s ease-out; }`.
Now — text swap details: `.txt { display: grid; }` with `.t { grid-area: 1/1; }` wait that requires both in same cell: `.t { grid-area: 1 / 1; }` yes. `.txt` width = widest ("Liked"). Transitions/animations:
- `.t-like`: default `transform: none; opacity: 1`. Checked: animation slide-up-out .35s forwards: to translateY(-130%) opacity 0. But for uncheck reverse use transition: set base `.t-like { transition: transform .3s, opacity .3s; }` and checked state (no animation) transform: translateY(-130%) opacity 0? If both animation and transition target... Simpler: use transition-only for like→ out? Then it slides smoothly but boring (no overshoot) and no delay control... I want: on check, "Like" exits up fast; "Liked" enters from below with spring. On uncheck, reverse smoothly.
Approach: both spans have base transitions; checked state sets transforms. Add springy feel via transition cubic-bezier with overshoot on enter (cubic-bezier(.34,1.56,.64,1)). Exit uses standard ease. Different easings for in/out transitions — a single transition property per element can define its own; each span has its own transition so I can give t-like exit ease and t-liked enter overshoot ease. But transition timing applies for both directions (check→uncheck reverses using the same transition defined on base? No — transitions defined on the element apply whenever property changes; the timing function used is from the NEW state's transition? Actually the transition used is the one in effect at the time of the change — the value of transition-* on the element AFTER the change (new state). So t-liked entering on check uses checked-state transition (overshoot), and leaving on uncheck uses base-state transition (ease). I can control both! Great:
```css
.t { grid-area: 1/1; transition: transform .35s cubic-bezier(.4,0,.2,1), opacity .3s; }
.t-liked { transform: translateY(120%); opacity: 0; color: var(--red); }
#like:checked ~ ... .t-like { transform: translateY(-130%); opacity: 0; }
#like:checked ~ ... .t-liked { transform: translateY(0); opacity: 1; transition: transform .5s cubic-bezier(.3,1.5,.5,1), opacity .3s; }
```
Wait, but the enter should be delayed until exit mostly done: add `transition-delay: .08s` on the checked t-liked. On uncheck, t-liked returns with base transition (no delay). And overflow hidden on .txt with fixed height (1em + padding? line-height). `.txt { overflow: hidden; }` clip sliding text. Also add slight blur on exit? Keep simple.
Width: "Liked" wider than "Like" — grid auto sizes to max — good, no layout shift.
**Counter structure:**
```html
<p class="meta">
<span class="counter">
<span class="c-static">12,04</span>
<span class="c-roll"><i>7</i><i>8</i></span>
<span class="c-label"> likes</span>
</span>
<span class="plus">+1</span>
</p>
```
`.counter { display: inline-flex; align-items: center; }` Hmm alignment: c-static is text with line-height; c-roll is a box... Let me set `.counter { display: inline-flex; align-items: stretch; line-height: 1.4; }` and `.c-roll { height: 1.4em; overflow: hidden; display: inline-flex; flex-direction: column; } .c-roll i { height: 1.4em? }` Wait — heights in em of which font-size? .c-roll inherits counter font-size; each i height: 1.4em?? If line-height 1.4, an i block of height 1.4em... but text inside i has line-height 1.4 → line box 1.4em → centers. Let me set: `.counter { font: 600 15px/1.4 stack }`. `.c-roll { display:block; height: calc(1.4 * 15px)?? ` Use em: font-size 15px → 1.4em = 21px. `.c-roll { height: 1.4em; overflow: hidden; } .c-roll i { display: block; height: 1.4em; }` — i content "7" line box 21px tall, glyph centered-ish per font baseline... The static text "12,04" baseline vs rolled "7": both share font metrics; the i block's text sits with baseline at same offset as siblings if the i's line box matches surrounding line boxes. Since all have line-height 1.4 and same font — the digits align on the same baseline as long as flex align-items: baseline. Use `align-items: baseline` on .counter and make c-roll participate: c-roll is inline-block with overflow hidden → its baseline = bottom margin edge (gotcha!). To fix: inside flex with align-items baseline, an inline-block with overflow hidden aligns its BOTTOM edge to baseline → the digit would sit wrong (bottom of 21px box on the baseline = text appears raised?). Ugh, classic. Workaround: don't use baseline; use `align-items: center` won't guarantee baseline equality... but since all items share the same font-size/line-height and the roll box height = 1.4em = line box of siblings, center alignment aligns text centers — with equal font sizes, centers align ⇒ baselines align. Static "12,04" is a flex item: its content box height = line-height 21px; roll box height 21px with digit i's line box 21px. align-items: center centers both 21px boxes → equal → baselines match. But "12,04" flex item: text renders with line-height 1.4 → box height 21px. Yes, matches. And `.c-label` " likes" same. So `align-items: center` works given uniform heights.
Checked: `.c-roll .strip?` I need inner wrapper to translate: `.c-roll > .strip { display:block; transition: transform .45s cubic-bezier(.3,.9,.3,1.2); } #like:checked ~ .meta .strip { transform: translateY(-1.4em)?? }` — translateY(-1.4em): em relative to strip's font-size (15px) → -21px = one row. But strip children are i display:block height 1.4em. translateY(-21px) shows second digit. But 1.4em = 21px exactly? 15×1.4=21. Em in transform refers to element font-size — strip font-size 15px → 1.4em = 21px. i height 1.4em = 21px. Match. But if I change font-size, both scale. To be safer use a CSS var: `--row: 1.4em` used in both heights and translate. Em-based consistency makes it exact anyway. Add a tiny overshoot cubic-bezier on the roll — digit bounces up.
Actually simpler markup: `.c-roll` contains `.strip` contains two `<i>7</i><i>8</i>`. Or make c-roll itself strip: `.c-roll { display:flex; flex-direction: column; height: 1.4em; overflow: hidden; }` with i children, and transform on .c-roll itself: checked → translateY(-1.4em)?? transform on the overflow-clipping element moves the box itself including clipping window — wrong; must transform inner content. So keep inner strip. Fine.
Also liked counter color: whole counter gets red accent on the number? Nice: checked → `.c-static?` keep neutral; maybe number turns red gradient text. Subtle: number color transitions to #ff5c7a. OK.
Also, the number displayed after check = "12,048", plus label "likes".
**+1**: absolute in .meta near right of counter... simpler: inline-block after counter, absolutely positioned overlapping: `.meta { position: relative; } .plus { position: absolute; left: calc(50% + 90px); top: -6px; opacity: 0; }` — appears to float up from counter's right end. Or simpler: place plus absolutely centered above counter: floats up from behind counter. I'll anchor: `left: 50%; margin-left: 70px;` might collide on mobile with narrow... it floats up and fades; fine. Or place it above the "+" roll? Simplest robust: absolutely centered (translateX(-50%)) starting at counter line, floating up 30px. It'd overlap counter text briefly then rise — looks like it emerges. Do that: `.plus { position:absolute; left:50%; top:0; transform: translateX(-50%); }` with animation translateX(-50%) translateY(0→-30px). Since transform in keyframes must include the -50% X. Fine.
**Hint copy**: "Tap to like — tap again to undo. Warning: extremely satisfying." Also maybe a keyboard hint: "or press Space once the button is focused". Since iframe needs click first for keys... The instruction: if demo uses keyboard, show "Click or press a key to start" — this isn't a keyboard game; but I can mention keyboard accessibility subtly. Skip complexity: mouse/touch focus. Actually keyboard DOES work: tab focuses checkbox, space toggles. Nice to mention in footer: "Fully keyboard accessible, too."
**Footer**: small line: "1 checkbox · 27 keyframe animations · 0 lines of JavaScript" — count my keyframes at the end and put real number, fun detail. Let me count when done.
Also let me add subtle **floating ambient hearts** in background? Might be noise. Skip — keep focus.
**Title design**:
```
[ ● PURE CSS — ZERO JAVASCRIPT ]
You're gonna like this.
One checkbox, forty keyframes, and absolutely no scripts.
```
With "like" in the H1 styled: gradient text red→pink, maybe italic serif (Georgia). Nice contrast. H1 white, 44-56px, weight 800, letter-spacing -0.03em.
Hmm — "like" italic serif inside sans headline is a nice editorial touch. `em { font-family: Georgia, 'Times New Roman', serif; font-style: italic; background: linear-gradient(120deg, #ff2e63, #ff8fa3); -webkit-background-clip: text; color: transparent; padding-right: .05em }` — gradient text with background-clip. Watch that italic serif descender... fine.
Layout order top→bottom (centered column, justify center, gap ~18px):
1. badge
2. h1
3. sub
4. (gap 44) stage/button
5. meta (counter + plus)
6. hint
7. footer
Total ~ 480px → fits 800. For 375 mobile: h1 34px, sub 15px, gaps smaller — fits 600+ height. Use clamp() everywhere.
Let me make sure the **fx overflow on mobile**: body overflow hidden. But wait — will `overflow: hidden` on body clip the ghost heart flying up 320px? Ghost starts at button center (~ page middle), flies up ~320px — still within viewport. On short screens (mobile landscape 375×667... it's fine). Confetti falls 240px below button center: page center + 240 ≈ 800/2+240 = 640 < 800 ok. On mobile 375×667: center ~333 + 240 = 573 < 667 ok.
**Now, the checkbox + label semantics**: label `for="like"`. Input placed inside `.scene` before `.stage`? For sibling selectors `#like:checked + .stage` and `#like:checked ~ .meta`. DOM:
```html
<main class="scene">
<header>...</header>
<input id="like" type="checkbox" class="sr">
<label class="stage" for="like">...</label>
<p class="meta">...</p>
<footer>...</footer>
</main>
```
CSS selectors:
- `#like:checked + .stage .heart { ... }`
- `#like:checked + .stage .ring { animation: ... }`
- `#like:checked ~ .meta .strip { transform: ... }`
- `body:has(#like:checked) .flash { animation }` and `.scene` shake — wait shake on .scene: `body:has(#like:checked) .scene { animation: shake .55s }`. But .scene is ancestor of input — :has on body works. However — will shake animation restart on each check? It's applied while `:has` matches (i.e., while checked). Uncheck removes → re-check re-adds → restarts. But if user checks → animation plays (0.55s) → stays checked → fine. If unchecks DURING animation, snap — ok.
Edge: `:has()` with animation and re-check while still shaking? Not possible—must uncheck first (removes). Good.
Hmm wait, one more consideration — the "shake" transform on `.scene` combined with `.scene` being the flex container with `transform` — will transform on .scene create containing block issues for fixed .flash? .flash is outside .scene (body child). Fine.
Also: the LABEL hit area: `.stage { display: inline-flex? }` It wraps `.btn`. Effects absolute inside. Make `.stage { position: relative; display: inline-block; }` sized by .btn. Cursor pointer.
**Sizes:**
- .btn: height 64px, padding 0 30px 0 26px? Symmetric 0 28px. font 17px/1. Gap 14px between heart and text. border-radius 999px.
- heart-wrap: 36×36 grid center. heart s=22 base? In 36px wrap, heart visual ~31px — slightly large for 64px button; set s=20 → visual ~28px. OK.
Hmm wait, actually — let me reconsider the heart orientation. With `rotate(45deg)`, computed earlier: cleft at top, tip at bottom. Circles: one at top edge (before), one at left edge (after). After 45° CW rotation these become top-left?? Let me re-verify with the corner math: corner (0,0) (adjacent to both circles — the cleft corner) maps to top-center after rotate(45deg). Corner (20,20) (the tip, opposite) maps to bottom-center. The two circle centers: c1 at (10,0) relative center (10,10) = (0,-10) → rotated 45° CW: new = (x cosθ − y sinθ, x sinθ + y cosθ) = (0−(−10)(.707), 0+(−10)(.707)) = (7.07, −7.07) → top-right of center. c2 at (0,10) → rel (−10, 0) → (−7.07, −7.07) → top-left. Both lobes at top, tip at bottom. ✓ Correct heart.
**Heart pseudo background using currentColor:**
```css
.heart { position: absolute; width: 20px; height: 20px; left: 50%; top: 50%; margin: -10px 0 0 -10px; background: currentColor; transform: rotate(45deg); color: #97a1b8; transition: color .35s; }
.heart::before, .heart::after { content: ""; position: absolute; width: 20px; height: 20px; border-radius: 50%; background: currentColor; }
.heart::before { left: 0; top: -10px; }
.heart::after { left: -10px; top: 0; }
```
transition on color transitions pseudos too (they use currentColor).
But heart-wrap handles pop scale. `.heart-wrap { position: relative? }` — I said grid; simpler: heart-wrap is 36×36 relative block; heart absolutely positioned as above. Pop animation on `.heart-wrap` transform. Heartbeat also on heart-wrap (after pop). So:
```css
.heart-wrap { width: 36px; height: 36px; position: relative; flex: none; }
#like:checked + .stage .heart-wrap {
animation: pop .8s cubic-bezier(.26,1.4,.48,1)??
```
Hmm — pop keyframes already encode bounce; use `linear`? If keyframes handle positions, easing between them still matters. I'll craft keyframes with the bounce in percentages and use `ease-out`-ish overall... Actually simplest: define pop keyframes densely (they define the bounce) with default `ease` — segment easing between keyframes adds smoothness. With dense keyframes, easing barely matters. OK.
Also combine heartbeat: `animation: pop .8s both?, heartbeat 1.5s .8s infinite;` — "both/forwards" fill for pop? If fill forwards, pop's 100% (scale 1.1) persists — but heartbeat overrides after .8s anyway (later animation wins). During heartbeat's delay period, is heartbeat "active"? With no fill mode, during delay it doesn't apply → pop's forwards fill applies. OK use `pop .8s both`. Then base transform for heart-wrap checked: scale(1.1) — needed for consistency when...? heartbeat runs infinite while checked, so it always overrides after start. Before delay ends, pop fills. So base transform irrelevant. But on UNCHECK: animations removed → transform reverts to base scale(1) instantly — want smooth: add `transition: transform .3s` on heart-wrap... As discussed, removal of animation → computed change; transition from animation-filled value isn't guaranteed... In Chrome, when an animation is removed, transitions do NOT run (the animated value just stops applying). Actually the spec: style change event computes new values; if the difference is between old computed (with animation) and new (without)... In practice, removing an animation causes an immediate jump; transitions treat the change... I recall that transitions DO fire when a class change both removes an animation and changes the underlying value? No — transition trigger compares before/after style change events, where before included the animation. When animation removed AND specified transform changes from scale(1.1)→scale(1)... hmm the before-value = animation-filled 1.1, after = 1 → transition SHOULD trigger per spec (transitions are triggered by computed-value changes between style change events, animations included). I believe modern Chrome does animate this. Let me not rely on edge behavior; instead, make un-check ALSO satisfying without needing transition: on uncheck, animation removed instantly → heart snaps from 1.1 to 1 — a small 10% snap is barely noticeable, plus the `:active` squish covers it (user is pressing during the state flip — the press squish at .93 masks the snap nicely!). Actually that's elegant: every press has squish, state change happens under the squish.
Also add color transition (color on .heart has .35s transition — computed color change 1.1→? no—color: red→gray transitions smoothly on uncheck since no animation involved. ✓.
**Pop keyframes:**
```css
@keyframes pop {
0% { transform: scale(1); }
30% { transform: scale(1.5) rotate(-6deg); }
45% { transform: scale(.85) rotate(4deg); }
60% { transform: scale(1.28); }
75% { transform: scale(.95); }
88% { transform: scale(1.16); }
100% { transform: scale(1.1); }
}
```
Wobble rotation adds craziness. Note pop includes rotate — heart tip swings.
**Heartbeat:**
```css
@keyframes heartbeat {
0%, 100% { transform: scale(1.1); }
12% { transform: scale(1.24); }
24% { transform: scale(1.1); }
36% { transform: scale(1.2); }
54% { transform: scale(1.1); }
}
```
1.6s infinite.
**Rings keyframes:**
```css
@keyframes ring {
0% { transform: scale(.7); opacity: 0; }
10% { opacity: .9; }
100% { transform: scale(2.6); opacity: 0; }
}
```
Wait ring box = fx box = button box (say 178×64) — scale 2.6 → 463×166 pill — spans wide horizontally, moderate vertically. Particles fly further. Maybe rings should be circles instead of pills — scale X more than Y? Non-uniform would be weird at rest... Ellipse rings via scaleX(2.6) scaleY(1.4)?? At 0% scale(.7,.7)... hmm, an expanding pill echo looks intentional. Alternatively make rings circles centered on the heart (not whole button): `.ring { width: 64px; height: 64px; border-radius: 50%; left: calc(50% - 32px)...}` centered on heart position (heart is at left portion of button!). The heart isn't at button center — it's left of text. Hmm! Particles should burst from the HEART, not button center. The heart center in fx coordinates: button padding-left 26 + heart-wrap center 18 → x ≈ 44px from button left edge. If fx = stage = button box, heart center at ~44px, y = 32px. I could position the burst origin there: define a `.origin { position: absolute; left: 44px; top: 50%; }`... fragile but deterministic since I control all sizes. OR — put fx INSIDE the button positioned at the heart: `.heart-wrap` contains an fx layer? But then particles overflow the button (overflow visible — buttons don't clip by default). But heart-wrap is 36×36 with pop animation scaling — children would scale with it! Bad.
Alternative: `.fx { position:absolute; left: 0; top: 0; width: 100%; height: 100%; }` with an inner `.boom` anchor: absolutely at left:44px top:50% — all rings/particles positioned relative to `.boom` (a 0×0 point). Rings: width 70px height 70px left:-35px top:-35px border-radius 50% → circles centered on heart. Particles at left:0 top:0 translate(-50%,-50%) → wait particles are positioned with their own size; anchor at 0,0 point: `left:0; top:0;` with transform translate(-50%,-50%) included in keyframes. Since particle sizes vary but center-based, fine.
Let me compute heart-center x precisely when done with button metrics. Button: display:inline-flex; align-items:center; gap:14px; padding: 0 30px 0 24px? Let me fix: padding-left 26px, heart-wrap 36px wide → heart visual center at 26+18 = 44px. y center = 32px (button 64 tall). So `.boom { position: absolute; left: 44px; top: 32px; }`. Hmm wait, but boom anchored in fx which is inset 0 of stage — stage = label wrapping btn only? The label `.stage` display:inline-block, its content .btn. So stage box == btn box (btn is inline-flex child; stage inline-block shrink-to-fit — with absolute children ignored). Yes stage == btn box.
Rings as circles at heart: nice — shockwave from the heart itself. Ring sizes 80px scaled to 2.4 → 192px circle. Hmm circle rings expanding from heart center will encircle the button too — looks great.
Particles from heart center — but text "Liked" sits right of heart... particles flying from behind heart across text — fine, they're above (z) — playful chaos. Good.
Confetti origin: also boom center. Falling down over the button/meta. Good.
Ghost heart: boom center too.
So `.fx-front { position:absolute; inset:0; pointer-events:none; }` contains `.boom { position:absolute; left:44px; top:50%; width:0; height:0; }`. Wait top:50% of 64 = 32 ✓.
Hmm — but 44px hardcoded: if I change paddings must update. I'll use a CSS var `--heart-x: 44px` documented.
Actually — wait. Simpler idea: make the ORIGIN the heart itself by making fx a sibling positioned via matching offsets... no, current plan is fine with a var.
Hmm, actually alternative: center the burst on the whole button but make the button SYMMETRIC? The heart+text can't be centered-origin... The current approach is good.
**Now, particles HTML** — inside .boom:
```html
<span class="boom">
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
<i class="pt d1"></i> ...
<i class="pt s1 star"></i> ...
<i class="pt h1"></i> (mini heart uses .mini-heart class w/ pseudos)
<span class="conf cf1"></span> ...
</span>
```
Mini-heart particle: `<i class="pt mh m1">` — needs its own pseudo heart drawing at small size. Since `.pt` generic + `.mh` shape:
```css
.pt { position: absolute; left: 0; top: 0; opacity: 0; }
.pt.dot { width: var(--s,10px); height: var(--s,10px); border-radius: 50%; background: var(--c); }
```
Wait dots: use width/height per particle. Let me define each particle inline-style custom props in HTML: `<i class="pt dot" style="--dx:118px; --dy:-14px; --c:var(--gold); --s:11px; --d:.06s; --t:.85s"></i>`. Inline styles keep CSS clean and make per-particle config obvious.
For flying keyframes generic:
```css
@keyframes fly {
0% { transform: translate(-50%,-50%) scale(.2); opacity: 0; }
15% { opacity: 1; }
60% { opacity: 1; }
100% { transform: translate(-50%,-50%) translate(var(--dx),var(--dy)) scale(1.15) rotate(var(--rot,180deg)); opacity: 0; }
}
```
Hmm — scale mid: make 0% scale .2, then explode: 20% scale 1.2? The distance interpolation is linear-ish (with easing). Add rotate throughout: interpolate rotate from 0 to var(--rot). Start rotate 0. Fine. Also easing: cubic-bezier(.15,.65,.35,1) ease-out-ish so particles decelerate as they fly — physical.
But scale: I want a pop: 0% scale(.3), 35% scale(1.2), 100% scale(.9)? Add 35% keyframe: `transform: translate(-50%,-50%) translate(calc(var(--dx)*.35), calc(var(--dy)*.35)) scale(1.25) rotate(calc(var(--rot)*.35));` — must repeat full transform at each keyframe (transforms don't interpolate per-function if lists differ — actually they DO interpolate if lists have same structure; different lists → matrix fallback... to be safe, keep identical function lists in all keyframes). calc(var(--dx)*.35) works. OK.
Opacity fade: 0%:0 → 12%: 1 → 70%: 1 → 100%: 0.
Star shape: `.pt.star { width: var(--s); height: var(--s); background: var(--c); clip-path: polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%); }` ✓ 4-point sparkle.
Mini heart:
```css
.pt.mh { width: var(--s); height: var(--s); background: var(--c); transform-origin: ...; }
.pt.mh::before, .pt.mh::after { content:""; position:absolute; width: var(--s); height: var(--s); border-radius:50%; background: var(--c); }
.pt.mh::before { left: 0; top: calc(var(--s) / -2); }
.pt.mh::after { left: calc(var(--s) / -2); top: 0; }
```
And rotation 45° — but the fly keyframes set transform including rotate(var(--rot))... The base rotate(45deg) needed for heart shape conflicts with fly's rotate — fly's rotate(var(--rot)) covers it if I set --rot like 220deg etc. — final orientation arbitrary anyway. But between keyframes it interpolates from 0 — the heart tumbles as it flies. Fun! But at 0% rotate(0) the mini heart is a diamond — barely visible at scale .3. Fine.
But wait — `.pt` positioned left:0 top:0 of .boom (a 0×0 point) — for dot/star: they need translate(-50%,-50%) to center on point ✓ (in keyframes). But their static (non-animating) state opacity 0 ✓. For .mh, static transform none — but it's invisible anyway.
**Confetti:** `.conf` — width 10px height 5px? Vertical rect 6×12? Mixed: some rects. `border-radius: 2px`. Positioned at boom: left:0; top:0; keyframes `drop` with translate(-50%,-50%) start... confetti near button center; rises then falls 240px. Also horizontal sway var. z above.
Confetti colors alternate gold/cyan/pink/violet/white.
**Ghost:** in `.fx-back` (before btn in DOM):
```html
<span class="fx-back"><span class="ghost"><i class="heart gh"></i></span></span>
```
`.fx-back { position:absolute; inset:0; pointer-events:none; }` wait — if fx-back before .btn and both static/positioned... .btn is position:relative (z 2), fx-back absolute z auto → paints before btn in DOM order since btn later ✓ even with z auto? Positioned elements paint in DOM order among same z — .btn positioned with z-index 2 → above. fx-back absolute z-index 0... Actually to be safe: .fx-back { z-index: 1 } .btn { position: relative; z-index: 2 } .fx-front { z-index: 3 }. ✓
Ghost heart: `.ghost { position:absolute; left: var(--heart-x); top: 50%; } .ghost .heart { width: 64px... }` reuse heart class with custom size via CSS var: let me parametrize heart: `.heart { --s: 20px; width: var(--s); ... }` and `.ghost .heart { --s: 64px; color: #ff2e63; }` with margins computed from --s: `margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2)`. And pseudo offsets calc(var(--s)/-2).
Ghost animation on `.ghost` wrapper (transform), heart inside static (just rotate 45 via .heart base). Ghost keyframes:
```css
@keyframes ghost {
0% { transform: scale(.2) rotate(0deg); opacity: 0; }
20% { opacity: .5; }
35% { transform: scale(1.25) rotate(-8deg); opacity: .5; }
100% { transform: scale(1.9) translateY(-300px) rotate(10deg); opacity: 0; }
}
```
Hmm mixing scale + translateY in later keyframes vs scale+rotate earlier — different function lists across keyframes → discrete interpolation! Transform lists must match structurally (same functions in same order) or interpolation falls back to matrix (actually modern browsers interpolate via matrix decomposition — still animates but rotation/scale blending can look odd... it works okay usually). To be safe use consistent list: `translate(0,0) scale(1) rotate(0)` pattern in ALL keyframes:
```
0% { transform: translate(0,0) scale(.2) rotate(0); opacity:0 }
35% { transform: translate(0,-20px) scale(1.25) rotate(-8deg); opacity:.55 }
100% { transform: translate(30px,-320px) scale(1.8) rotate(12deg); opacity: 0 }
```
Also ghost needs to be anchored: position absolute at heart point, with heart centered by its own negative margins — but ghost scale scales around its own origin — set transform-origin at the anchor: ghost is a 0-size wrapper containing heart positioned with negative margins; transform-origin default 50% 50% of 0-size box = the anchor point ✓ since heart is margin-centered on that point.
Wait, `.ghost` wrapper 0×0 at (44px, 32px): contains `.heart` absolutely? If .ghost has no size, put heart with position:absolute; left:0; top:0; margin: -32px 0 0 -32px (for --s 64). Then ghost transform scales about its origin (0,0 point) — which is heart's center ✓.
Ghost blur: `filter: blur(.5px) drop-shadow(0 0 24px rgba(255,46,99,.7))` — nice glow. Hmm blur on the whole heart incl pseudos ✓.
Also add a SECOND smaller ghost flying up-left with delay? Keep one — restraint.
Actually... let me add small "shockwave star" — skip, we have enough.
**Ring CSS:**
```css
.ring { position: absolute; left: calc(var(--heart-x) - 40px)??
```
Hmm — boom approach: put rings inside .boom too. But .boom is in fx-front — rings should render above button? Shockwaves above is fine (they're outlines, see-through). ✓ Put rings, particles, confetti all in .boom inside .fx-front. Ring: `position:absolute; left:-40px; top:-40px; width:80px; height:80px; border-radius:50%; border: 2.5px solid ...`. Wait border-width scaled with scale() → ring gets thicker visually as it expands — actually border thickness scales too (transform scales rendering). A 2.5px border scaled 2.6× → 6.5px thick ring expanding — looks bold. Alternatively use box-shadow? Keep border, looks punchy. Or thinner: 2px.
Rings colors: r1: rgba(255,255,255,.9); r2: #ff2e63; r3: #ffd166. Delays 0/.09/.18, durations .7/.75/.8.
Also **button ripple** on active.
**Sparkle behind button on hover?** skip.
**Flash overlay:**
```css
.flash { position: fixed; inset: 0; pointer-events: none; opacity: 0; background: radial-gradient(circle at 50% 54%, rgba(255,46,99,.28), rgba(255,46,99,.08) 30%, transparent 60%); }
body:has(#like:checked) .flash { animation: flash .75s ease-out; }
@keyframes flash { 0% {opacity: 0} 18% {opacity: 1} 100% {opacity: 0} }
```
The gradient center 50% 54% ≈ button position (need to match actual button y — button is mid-page; I'll tune: page column centered; button sits below header ~ 58% of viewport. I'll estimate 50% 56%.)
Hmm — but `body:has(...)` — the .flash and body — flash is body's child ✓.
**Shake on .scene** — wait, careful: shake animates transform on .scene — .scene also has... it's `min-height:100svh; display:flex; flex-direction:column; align-items:center; justify-content:center;` no transform otherwise ✓.
But hmm — `:has()` for shake while `#like:checked + .stage ...` handles rest — consistent. But note: `.scene` contains the input; `body:has(#like:checked) .scene` ✓ matches.
Also **h1 reaction**? Maybe the headline's "like" word pulses when liked: `body:has(#like:checked) h1 em { animation: em-pop ... }` — the word "like" pops. Cute detail! `@keyframes empop { 50% { transform: scale(1.12) rotate(-2deg) } }` — em is inline; add display:inline-block. Also maybe the em heart... skip extra.
**prefers-reduced-motion**: I'll include a reduced-motion media query pausing the most aggressive ones — actually you know, it's an animation demo; but good craft includes it. It's ~4 lines. I'll skip it to keep code focused... Hmm. The judging is on visuals; adding it doesn't hurt visuals in normal mode. I'll skip — keep file tighter. Actually no, let me skip it. Fine.
**Focus styles:** `#like:focus-visible + .stage .btn { outline: 2px solid #ff5c7a; outline-offset: 4px; }`.
**Now the count of keyframes** for the footer line: pop, heartbeat, ring, fly, drop, ghost, plus... slideIn? (using transitions there), rise (+1), flash, shake, pulse (idle attention), empop, ripple. That's ~13. Footer: "1 checkbox · 13 @keyframes · 0 lines of JavaScript". I'll count at the end.
**Idle pulse ring:** `.pulse` behind button: pill same size as button, border 1px solid rgba(255,90,120,.35), animation pulse 2.6s infinite: scale 1→1.28, opacity .7→0. Position absolute inset 0, border-radius 999px. Two staggered? One is fine — subtle. Actually make it match button pill shape ✓ inset 0 of stage.
Hmm wait — pulse positioned inset 0 in `.stage` — before .btn in DOM, absolute → under .btn ✓. Actually I planned .fx-back for ghost; put pulse inside fx-back too, or standalone. DOM in stage: `<span class="fx-back"><span class="pulse"></span><span class="ghost">...</span></span><span class="btn">...</span><span class="fx-front"><span class="boom">...</span></span>`.
**Text "Liked" color**: `--red`.
**Button checked background:** `linear-gradient(180deg, rgba(255,46,99,.16), rgba(255,46,99,.07))` + border-color rgba(255,92,122,.55) + box-shadow red glow + inset highlight. Transition .35s.
**Heart glow when liked:** `.heart` gets `filter: drop-shadow(0 0 10px rgba(255,46,99,.55))` — filter transitions? filter is animatable ✓ transition filter .4s.
But careful: heart color gray → red: `#like:checked + .stage .heart { color: #ff3b5f; }`.
**Press squish:** `.stage:active .btn { transform: scale(.93); }` with `.btn { transition: transform .18s cubic-bezier(.34,1.56,.64,1), background .35s, border-color .35s, box-shadow .35s; }` — hover lift: `.stage:hover .btn { transform: translateY(-2px); }` — conflicts with active squish (both transform; active later wins if after in cascade — order: define :hover then :active). ✓
**Ripple:** `.btn::after { content:""; position:absolute; inset:0; border-radius:inherit; background: radial-gradient(circle at 50% 50%, rgba(255,255,255,.25), transparent 60%); opacity:0; }` `.stage:active .btn::after { animation: ripple .5s ease-out; }` keyframes: 0% opacity .6? transform scale(.6) → 100% scale(1.05) opacity 0. Hmm — .btn overflow hidden? Button contains heart+text — must NOT clip (particles are outside btn, in stage-level fx — btn CAN be overflow hidden!). Wait — do particles overflow from .boom which is inside .fx-front inside .stage — not inside .btn. So .btn overflow:hidden is safe and enables ripple clipping + text swap clipping (txt has own overflow). Set .btn overflow hidden? But focus outline-offset 4 outside — fine (outline isn't clipped by overflow... outline draws outside border-box, not affected by own overflow). OK.
Hmm wait, actually one issue: `.btn::after` ripple covering whole button — pointer-events none implicitly (pseudo can't receive? they can affect hit area of parent — fine).
**The txt overflow hidden + fixed height**: `.txt { display: grid; overflow: hidden; }` .t line-height... button content centered by flex; txt height = line height of .t (17px font, line-height 1 → but clipping at exact glyph height might cut descenders — "Liked" has no descenders; "Like" no descenders ✓. line-height: 1.1 for safety? Clipping window = grid height = max child height ✓ fine.
Set `.t { white-space: nowrap; }`.
**Meta line:** `.meta { margin-top: 40px?? }` careful with confetti overlap — confetti falls past; it's above (z? .meta is after .stage in normal flow — fx-front z 3 only within stage stacking context... stage doesn't create stacking context (no transform/opacity) — position:relative alone doesn't. Hmm: .stage position relative → its positioned descendants with z-index participate in... .stage without z-index doesn't create stacking context, so fx z-index 3 competes globally up to nearest stacking context (probably root since .scene has animation? .scene animation → animating transform creates stacking context? Yes — will-change/animation on transform... during animation, .scene becomes a stacking context (animation of transform forces it). Confetti inside would be clipped?? No — stacking context doesn't clip, just contains z-index. .scene has no overflow hidden → fine.
Confetti over .meta: .meta is unpositioned text — positioned fx with z 3 paints above ✓.
**meta content:** "12,047 likes" — wait I want the counter line to feel like part of the demo: maybe "You and 12,047 others liked this"? Too long with roll. Keep: `12,048 likes`. Plus a small caption below? The hint: `p.hint` "Click to like · click again to unlike · it's all CSS". Footer bottom: fixed? Place footer at bottom via scene justify-content: space-between? Use flex column: header top, center block, footer bottom with margin-top:auto. Let me structure: .scene { display:flex; flex-direction: column; align-items:center; justify-content:center; gap: 0; } with explicit margins. Footer absolute bottom? Use margin-top: auto pattern: scene min-height 100svh, padding 32px 20px; header; then `.mid` (button area) margin: auto 0? Simplest: justify-content: center with fixed gaps and footer as last normal element with margin-top 48px — if viewport short, content may exceed → overflow hidden body clips footer. To be safe make footer position: fixed bottom 18px? On mobile with centered content could overlap counter hint... The content total ~520px < 600 ✓ safe either way. I'll use margin-top: auto with scene justify-content: flex-start?? Hmm: .scene { display:flex; flex-direction:column; align-items:center; min-height:100svh; padding: 40px 20px 28px; } .hero { margin: auto 0; } wait margin auto top+bottom on .hero centers it between header and footer: header, .hero(margin:auto 0 → centers in remaining space), footer at bottom via margin-top:auto?? If .hero has margin auto both, it takes free space — header at top, footer pushed to bottom, hero centered between ✓. Structure: header (natural), .hero { margin: auto 0 } wait margin:auto 0 = top&bottom auto, sides 0 ✓, footer natural after. Flex: auto margins absorb free space: header gets none... free space distributed to .hero's auto margins → hero centered between header and footer, footer at very bottom ✓.
Hero = .stage + .meta + .hint grouped with gaps.
**Footer content:** "1 hidden checkbox · N @keyframes · 0 lines of JavaScript — view source, it's all styles" plus maybe heart glyph. Keep single line small gray.
**Header copy refinement:**
- Badge: "PURE CSS · ZERO JAVASCRIPT" with a pulsing dot (green? red). A live green dot suggests "system online" vibe — but brand red? Use amber? Let me do a small gradient dot.
- H1: `You’re gonna <em>like</em> this.` — 48px.
- Sub: `A like button with an unreasonable amount of keyframes. One click sets it all off.`
Nice copy.
**Colors final:**
- bg base #0a0c13; radial top glow subtle #151a2e?
- grid dots rgba(255,255,255,.05) 22px.
- text: #f4f6fb; secondary #9aa3b8 (slate).
- red #ff2e63, pink #ff7a9e, gold #ffd166, cyan #4cc9f0, violet #b388ff, white.
H1 em gradient: linear 100deg #ff2e63 → #ff9ab0.
**Badge style:** border 1px rgba(255,255,255,.14), bg rgba(255,255,255,.04), radius 999, padding 7px 14px, font 11px letterspacing .14em uppercase, color #b9c1d4. Dot: 7px circle #ff2e63 with glow + pulse animation (subtle scale/opacity).
**Hint:** small: "Click to like — click again to undo. Warning: extremely satisfying." with color #6f7890, maybe keyboard mention: " (spacebar works too, once focused)" — the iframe keyboard note: instructions say IF the demo uses keyboard, show "Click or press a key to start" — that's for games needing interaction. This demo's core is the click; keyboard is bonus accessibility. I'll mention spacebar subtly. Fine.
Now — **one more craziness upgrade**: on like, spawn a shower of MANY tiny hearts from top of screen?? Too much? A "heart rain" while liked: an overlay with 8 small hearts falling repeatedly while checked (infinite while liked). Hmm — persistent rain could be annoying... but it IS "crazy when you click". Let me add a tasteful version: while liked, occasional tiny hearts drift up from the button every few seconds? The heartbeat already gives life. I think the one-shot mega-burst + heartbeat + counter + text + ghost is a rich, choreographed response. I'll skip rain to keep it classy-crazy.
Actually — hmm, let me reconsider: "crazy animation" is the explicit ask. I want the WOW. Current list on click:
1. press squish (active)
2. heart pop + color flip + wobble
3. heartbeat loop forever (liked state)
4. 3 expanding shockwave rings from heart
5. 14 particles (dots/stars/mini-hearts) exploding
6. 8 confetti with gravity arcs
7. big ghost heart launching upward
8. page shake
9. page red flash (radial)
10. Like→Liked text spring swap
11. digit roll 7→8 with bounce
12. +1 float
13. button style glow shift
14. headline em pop
15. idle pulse stops
That's a LOT — definitely crazy yet choreographed.
Let me also double check the **uncheck** experience: transitions reverse (text swaps back, color grays, glow fades, digit rolls down, pulse resumes, heartbeat stops). Good enough.
Edge: **rapid toggling** — animations restart ✓ fun.
**Writing the selectors list** (input is `#like`, siblings inside .scene):
```css
#like:checked + .stage .btn {...}
#like:checked + .stage .heart { color: #ff2e63; filter: drop-shadow(...); }
#like:checked + .stage .heart-wrap { animation: pop .85s both, heartbeat 1.6s .85s infinite; }
#like:checked + .stage .t-like { transform: translateY(-140%); opacity: 0; }
#like:checked + .stage .t-liked { transform: none; opacity: 1; transition: transform .55s cubic-bezier(.3,1.6,.4,1) .12s, opacity .3s .12s; }
```
Wait — I decided to use transitions (not animations) for text swap. The checked state changes transforms; transitions run. For t-liked entering with overshoot: its transition defined in CHECKED state applies when entering (state change to checked uses new state's transition) ✓; when leaving (to unchecked), base transition applies ✓. But careful: base `.t` transition must exist for the reverse: `.t { transition: transform .4s cubic-bezier(.5,0,.4,1), opacity .35s; }`.
t-like exit: base transition handles it (ease). But I'd like exit slightly faster... base transition applies to both directions for t-like (checked t-like state change transform → uses checked-state transition if defined there). I can define checked t-like transition faster: `transition: transform .25s ease, opacity .2s;` and base slower. Both directions asymmetric ✓.
Similarly `.heart-wrap` — no transition needed (animation handles check; uncheck snaps under squish). Actually let me add base `transition: transform .35s cubic-bezier(.3,1.3,.4,1)` — IF Chrome transitions on animation removal (it does trigger transitions when computed value changes... let me think about whether the animation-filled value is the "before" — per CSS Transitions spec, transitions start based on before-change style which includes animations. When :checked removed, the pop animation (fill both) is removed AND heartbeat removed → before value scale ~1.1 (heartbeat current), after value scale(1) → transition runs ✓ in compliant browsers. Chrome implements this correctly I believe. And if it doesn't, snap — acceptable.)
**Digit roll:** `#like:checked ~ .meta .strip { transform: translateY(calc(-1 * var(--row))); }` with `.strip { transition: transform .5s cubic-bezier(.25,1.2,.35,1); }` wait — overshoot cubic-bezier y >1: (.25,1.2,.35,1)?? Control points must have y in [0,1]?? NO — x must be in [0,1]; y can exceed (overshoot allowed) ✓. So cubic-bezier(.2,.9,.3,1.3) valid. Bounce the digit ✓. Note overshoot shows a bit of the neighboring digit (above 7 is nothing/clip — overshoot beyond 8 shows below-8 area blank... translating strip up beyond one row → shows part of... row above 7 = empty → blank gap flash. Slight overshoot ok (.15em). Actually strip has rows [7,8]; translateY(-1.4em) exactly shows 8. Overshoot to -1.55em shows top 0.15em of blank under 8? Rows: 7 occupies [0,1.4em], 8 occupies [1.4em,2.8em]. translateY(-1.55em) → 8's top at -0.15em (clipped), and 8's bottom at 1.25em — visible region [0,1.4em] shows 8 fully minus bottom 0.15em?? Wait window height 1.4em; strip shifted -1.55 → 8 spans window [-0.15em, 1.25em] → bottom 0.15em of window empty. Brief — fine, unnoticeable at speed.
Better: add a THIRD row duplicating first? [7,8] with overshoot below 8 empty — add rows [7,8] only; fine.
Actually — simpler idea to also handle the comma: make it "12,04" + roll. Wait, actually let me reconsider the number: what about making it more dramatic: "999" → "1K"?? complex. Or just "12,047" → "12,048" ✓ going with it. Hmm, wait — but if user unlikes: 12,048 → 12,047 ✓ rolls back.
**+1 rise:** `#like:checked ~ .meta .plus { animation: rise .9s ease-out .25s both; }` keyframes rise: 0% { transform: translate(-50%,0); opacity: 0 } 25% {opacity: 1} 100% { transform: translate(-50%,-34px); opacity: 0 }. Position .plus absolute within .meta (position relative) at right of counter: `left: calc(50% + 92px)`?? Counter width varies with font — measure roughly: "12,047 likes" 15px semibold ≈ 100px wide → right edge ≈ 50% + 50px. Put plus at `left: calc(50% + 58px); top: -4px;`. On mobile same. It's decorative; fine. Actually simpler & robust: put the +1 INSIDE the flow? No—absolute is right. Or attach to button: +1 floating from the button's counter... keep meta-anchored.
Hmm — one more idea: also show avatars row? ("Liked by A, B, and 12,045 others") with tiny gradient circles — social proof styling! Adds realism: a row of 3 overlapping avatar circles (CSS gradient heads) + counter text. Cute but adds clutter + the roll digit inside sentence complicates. Skip avatars.
**Now assemble the full HTML.** Also `<title>`, meta viewport, meta charset. Lang en.
Let me write out all particle configs concretely.
dots (8):
1. `--dx: 122px; --dy: -6px; --c: var(--gold); --s: 10px; --d: 0s; --t: .8s; --rot: 200deg`
2. `--dx: 96px; --dy: -66px; --c: var(--cyan); --s: 12px; --d: .04s; --t: .9s; --rot: -160deg`
3. `--dx: 44px; --dy: -108px; --c: var(--pink); --s: 9px; --d: 0s; --t: .85s; --rot: 120deg`
4. `--dx: -34px; --dy: -112px; --c: var(--violet); --s: 11px; --d: .05s; --t: .95s; --rot: -220deg`
5. `--dx: -98px; --dy: -62px; --c: var(--red2)? use #ff2e63; --s: 10px; --d: .02s; --t: .8s`
6. `--dx: -124px; --dy: -4px; --c: var(--gold); --s: 13px; --d: 0s; --t: .9s`
7. `--dx: -92px; --dy: 58px; --c: var(--cyan); --s: 9px; --d: .06s; --t: .9s`
8. `--dx: 98px; --dy: 62px; --c: var(--pink); --s: 11px; --d: .03s; --t: .95s`
(Also maybe two more downward: 9. --dx: 10px; --dy: 108px gold; 10. --dx: -12px; --dy: 104px violet) — below the button they hit meta — they fade by then. Include for full circle feel. 10 dots.
stars (4):
11. `--dx: 148px; --dy: -58px; --c: var(--gold); --s: 16px; --rot: 320deg; --t: 1s`
12. `--dx: -146px; --dy: -62px; --c: var(--violet); --s: 14px; --rot: -280deg; --t: 1.05s; --d: .04s`
13. `--dx: 78px; --dy: -142px; --c: var(--cyan); --s: 13px; --rot: 240deg; --t: .95s`
14. `--dx: -74px; --dy: -148px; --c: var(--pink); --s: 15px; --rot: -200deg; --t: 1s; --d: .05s`
mini hearts (2):
15. `--dx: 40px; --dy: -150px; --c: #ff2e63; --s: 14px; --rot: 260deg; --t: 1s; --d: .06s`
16. `--dx: -46px; --dy: -154px; --c: var(--pink); --s: 12px; --rot: -230deg; --t: 1.05s; --d: .1s`
confetti (8): colors cycle [gold, cyan, pink, violet, white, gold, cyan, pink]; sway ±(18-40px); durations 1.15–1.5s; delays .1–.28s; sizes 8×14? Rect 7×12px radius 2. Or mixed squares 8×8. I'll vary via --w/--h.
Now — **`.pt` base**: `position:absolute; left:0; top:0; opacity:0; will-change: transform;` and `#like:checked + .stage .pt { animation: fly var(--t,.9s) cubic-bezier(.16,.66,.3,1) var(--d,0s) both; }` — `both` so pre-delay state = 0% (opacity 0, scale .3) — same as base, fine. And post → opacity 0 ✓.
Wait — with fill `both` and delay, during delay the 0% frame applies (invisible ✓).
**fly keyframes** (uniform transform list):
```css
@keyframes fly {
0% { transform: translate(-50%,-50%) translate(0,0) scale(.3) rotate(0deg); opacity: 0; }
12% { opacity: 1; }
38% { transform: translate(-50%,-50%) translate(calc(var(--dx)*.38), calc(var(--dy)*.38)) scale(1.25) rotate(calc(var(--rot,.5turn)*.38)); opacity: 1; }
78% { opacity: 1; }
100% { transform: translate(-50%,-50%) translate(var(--dx), var(--dy)) scale(.55) rotate(var(--rot,.5turn)); opacity: 0; }
}
```
Note: `calc(var(--dx)*.38)` — var is a length like 122px → 122px*0.38 ✓ valid calc. `--rot` as deg value ✓ calc multiply ✓.
Hmm — scale shrinking at end (0.55) as they fly out — sparkles dissipate ✓.
**mini-heart pseudos** need same --s. Also since .pt.mh has background var(--c) & pseudos. ✓
**Rings:**
```css
.ring { position:absolute; left:-45px; top:-45px; width:90px; height:90px; border-radius:50%; opacity:0; }
#like:checked + .stage .ring { animation: ring .75s cubic-bezier(.2,.7,.3,1) var(--rd,0s) both; }
.r1 { border: 2.5px solid rgba(255,255,255,.85); --rd: 0s; }
.r2 { border: 2.5px solid var(--red); --rd: .1s; }
.r3 { border: 2.5px solid var(--gold); --rd: .2s; }
@keyframes ring {
0% { transform: scale(.4); opacity: 0; }
15% { opacity: .95; }
100% { transform: scale(2.7); opacity: 0; }
}
```
Ring at boom center (heart) — 90px circle scaled 2.7 → 243px ✓ nice.
**Confetti keyframes:**
```css
@keyframes drop {
0% { transform: translate(-50%,-50%) translate(0,0) rotate(0); opacity: 1; animation-timing-function: cubic-bezier(.12,.75,.35,1); }
32% { transform: translate(-50%,-50%) translate(calc(var(--sw)*.4), -92px) rotate(220deg); opacity: 1; animation-timing-function: cubic-bezier(.55,0,.85,.45); }
85% { opacity: 1; }
100% { transform: translate(-50%,-50%) translate(var(--sw), 250px) rotate(660deg); opacity: 0; }
}
```
Hmm — the timing function at 32% applies to segment 32→85 (for opacity) and 32→100 (transform)? Timing functions apply per keyframe pair per property between the keyframes where EACH property is specified. Transform specified at 0,32,100 → segment 32-100 uses ease at 32% ✓ gravity-ish (ease-in). Opacity specified 0,32,85,100. ✓.
Wait, translateY -92px: is that relative to boom center at button middle. Rise to -92 → above button top ✓ then falls to +250 (below into meta area) ✓.
Confetti base: `position:absolute; left:0; top:0; width:var(--w,8px); height:var(--h,12px); border-radius:2px; background:var(--c); opacity:0;` checked: `animation: drop var(--t,1.3s) linear var(--d,.15s) both;` — note outer animation timing overridden per keyframe ✓ (set `linear` so segments without keyframe-tf are linear).
Confetti configs:
- cf1: gold, --sw: 34px, --t: 1.25s, --d: .12s, w8 h13
- cf2: cyan, --sw: -30px, --t: 1.4s, --d: .2s, w7 h11
- cf3: pink, --sw: 46px, --t: 1.3s, --d: .16s, w9 h9 (square-ish)
- cf4: violet, --sw: -40px, --t: 1.45s, --d: .26s, w7 h12
- cf5: white, --sw: 22px, --t: 1.2s, --d: .1s, w6 h10
- cf6: gold, --sw: -24px, --t: 1.35s, --d: .22s, w8 h8
- cf7: cyan, --sw: 38px, --t: 1.5s, --d: .3s, w7 h13
- cf8: pink, --sw: -34px, --t: 1.28s, --d: .18s, w9 h11
**Ghost:**
```css
.ghost { position:absolute; left:0; top:0; width:0; height:0; opacity:0; }
#like:checked + .stage .ghost { animation: ghostUp 1.25s cubic-bezier(.2,.6,.3,1) .05s both; }
@keyframes ghostUp {
0% { transform: translate(0,0) scale(.25) rotate(0deg); opacity: 0; }
18% { opacity: .5; }
40% { transform: translate(6px,-46px) scale(1.15) rotate(-10deg); opacity: .5; }
100% { transform: translate(34px,-330px) scale(2.1) rotate(14deg); opacity: 0; }
}
```
Ghost heart inside: `.ghost .heart { --s: 62px; color: #ff3b5f; filter: blur(.4px) drop-shadow(0 0 22px rgba(255,46,99,.75)); }` and heart positioned absolute left:0 top:0 margin -31px. Wait `.heart` base has left:50%/top:50% margins relative to a wrap — for ghost, heart is direct child of 0-size ghost: set `.ghost .heart { left:0; top:0; margin: calc(var(--s)/-2) 0 0 calc(var(--s)/-2); }`. Hmm — my base .heart uses `left:50%; top:50%; margin: -s/2...` designed for heart-wrap. Let me define .heart positioning generically: `position:absolute; left:0; top:0;` with margin negative half — then in .heart-wrap, heart needs centering in 36px box: heart-wrap sets heart `left:50%; top:50%`? Conflict... Cleaner: .heart is always `position:absolute; left:0; top:0; margin: calc(var(--s)/-2) 0 0 calc(var(--s)/-2);` — anchored so its CENTER (of the s×s square) is at parent's origin point. Then:
- .heart-wrap: 36×36; give it an inner anchor? The heart center should be at wrap center (18,18). If heart anchors at parent's (0,0)... Set heart-wrap `position:relative` and heart `left:50%`?? — Let me simply give .heart `left:0; top:0` anchor and position PARENTS via their own positioning: For heart-wrap: heart centered → wrap's ::before?? Ugh.
Cleanest: make heart anchor at center of its own square, using `translate(-50%,-50%)`... but base transform must stay rotate(45deg) — combine: `transform: translate(-50%,-50%) rotate(45deg)` with `left:50%; top:50%` relative to any positioned parent of any size ✓✓. For ghost (0×0 parent): left:50% of 0 = 0 ✓ margin-free. For .mh particles — they use fly keyframes transform (overrides base rotate!) — the mini heart's base rotate(45) gets replaced by fly's rotate(var(--rot)) — that's FINE as decided (tumbling). But wait — .pt.mh needs its translate(-50%,-50%) in fly keyframes ✓ included, and pseudos provide lobes ✓. But the mh's static state (pre-anim, opacity 0) — no transform → diamond heart — invisible anyway ✓.
But hold on: `.heart` base has `transition: color .35s, filter .4s` — applies to mh too (harmless).
Ghost: `.ghost .heart { --s: 62px; left:50%; top:50% }` wait parent 0×0 → 50% = 0 ✓ and transform translate(-50%,-50%) rotate(45) centers ✓. But ghost's own animation transforms the WRAPPER (ghost), heart inside static ✓ no conflict!!
And .heart-wrap pop transforms the WRAP, .heart inside static ✓. All consistent: transforms only on wrappers.
Base `.heart { position:absolute; left:50%; top:50%; width:var(--s); height:var(--s); margin:0; transform: translate(-50%,-50%) rotate(45deg); background: currentColor; color: #98a2b8; }` — hmm translate(-50%,-50%) of the SQUARE — the square center = rotation center = visual center-ish ✓. Pseudos: `content:""; position:absolute; width:var(--s); height:var(--s); border-radius:50%; background: currentColor;` `::before { left:0; top: calc(var(--s)/-2); }` `::after { left: calc(var(--s)/-2); top:0; }` ✓.
Wait — pseudo `top: calc(var(--s) / -2)` — var on pseudo inherits from .heart ✓.
heart-wrap: 36×36, position:relative, flex none. Heart centered ✓.
For the button's heart: `--s: 20px` → visual ~28px ✓.
**Now the button:**
```css
.btn {
position: relative; z-index: 2;
display: inline-flex; align-items: center; gap: 13px;
height: 64px; padding: 0 32px 0 26px;
border-radius: 999px;
background: linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.02));
border: 1px solid rgba(255,255,255,.16);
box-shadow: inset 0 1px 0 rgba(255,255,255,.09), 0 14px 34px rgba(0,0,0,.45);
color: #eef1f8; font: 700 17px/1 ...; letter-spacing: .01em;
cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent;
overflow: hidden;
transition: transform .2s cubic-bezier(.34,1.56,.64,1), background .35s ease, border-color .35s ease, box-shadow .35s ease;
}
```
Hover: translateY(-2px), border-color .32, shadow warmer. Active: scale(.93).
Checked:
```css
#like:checked + .stage .btn {
background: linear-gradient(180deg, rgba(255,46,99,.18), rgba(255,46,99,.06));
border-color: rgba(255,84,120,.55);
box-shadow: inset 0 1px 0 rgba(255,255,255,.12), 0 10px 34px rgba(255,46,99,.28), 0 0 0 1px rgba(255,46,99,.2);
}
```
**Text:** `.t { grid-area: 1/1; white-space: nowrap; }` `.t-liked { color: #ff5c7a; transform: translateY(120%); opacity: 0; }`.
Wait — with grid, both .t in cell 1/1; .txt needs `display:grid; overflow:hidden;`.
**Scene paddings/gaps:**
- header: text-align center; margin-bottom ~ 8px? .hero margin auto centers between header & footer.
- gap inside hero: stage, meta (margin-top 34px), hint (margin-top 14px).
Let me compute rough vertical: header ~ 130px (badge 30 + 16 + h1 52 + 12 + sub 22), hero: 64 button + 34 + 22 meta + 14 + 18 hint ≈ 152, footer 20. Total ≈ 130+152+20 + gaps ≈ 340 → lots of breathing room in 800 ✓. Add margin between header and hero automatically (hero margin auto splits remaining ~460 → 230 top/bottom — hmm that pushes hero quite center, header near top. Fine and dramatic. Maybe cap: add `padding-bottom: 90px` to scene so hero sits slightly above true center? The boom effects downward need space below (confetti 250px) — 230px below button + then meta... confetti from button center falls 250 → reaches ~ button_y+250; button_y ≈ 430 (in 800) → 680 < 800 ✓. Ghost up 330 from ~430 → 100 — above is header at top (~40-170) — ghost flies OVER the headline — looks awesome, fades by then (opacity 0 at end) ✓.
**Mobile 375×667:** header h1 clamp(30px, 8vw, 52px) → 30px; sub 14px; total header ~110; hero same 152; footer 20 → 282 + auto margins ✓. Particles ±150px horizontal from heart-x 44px of a ~160px wide button centered at 187 → heart boom x ≈ 187-80+44 = 151; particles to 151+150=301 < 375 ✓; left: 151-150 = 1 ✓ barely inside. Rings 243px wide circle → 151±122 → 29..273 ✓ inside. Confetti sway ±40 at x 151 ✓. Button width: "Liked" + heart + padding ≈ 170px ✓.
Wait — heart boom anchored at left 44px — on the BUTTON, heart center from button left edge: padding-left 26 + heart-wrap half 18 = 44 ✓. Set `--hx: 44px`. But careful — box-sizing border-box: padding 26 inside border; heart-wrap width 36 → center at 26+18=44 from border-box left edge (border 1px negligible, it's from padding edge... border-box left includes border 1px; content starts at 1+26=27?? Hmm: with border-box, width includes border; content-box origin at border+padding... Let me not over-engineer: fx is inset 0 of stage; stage box == btn border-box (label shrink-wrap around block? label inline-block containing inline-flex btn — label box = btn margin box ✓). boom left: calc(1px + 26px + 18px) = 45px. I'll use 45px. Actually let me just measure: border-box left edge → border 1 → padding 26 → content x starts at 27. heart-wrap 36 wide centered at 27+18 = 45 ✓. `--hx: 45px`.
Hmm wait — is stage exactly btn-sized? label.stage display:inline-block; content: btn (inline-flex, inline-level) + absolute spans (no contribution) ✓ → stage width = btn width, height = btn height (line-height of label could add! inline-block's height determined by line box — btn inline-flex baseline alignment might add extra line height below/above due to label's line-height). To kill: `.stage { display: block; width: fit-content?? }` or make stage `display: inline-flex` — then stage height = btn height exactly ✓. Use `.stage { position: relative; display: inline-flex; }` — but then hero centers it (hero is flex column align center ✓). label display inline-flex containing btn inline-flex → fine. Stage height = btn height ✓.
Wait, actually — hero contains stage + meta + hint, hero align-items center — stage inline-flex ok.
Hmm, another subtlety: `.stage { cursor: pointer }` ✓ and `touch-action: manipulation` to avoid double-tap zoom delays on mobile ✓.
**.meta positioning**: `.meta { position: relative; margin-top: 34px; }` counter line-height issue discussed. Let me write it:
```html
<p class="meta">
<span class="counter"><span class="cs">12,04</span><span class="roll"><span class="strip"><i>7</i><i>8</i></span></span><span class="cl"> likes</span></span>
<span class="plus">+1</span>
</p>
```
```css
.meta { margin: 36px 0 0; position: relative; color: #8f99b0; font-size: 15px; }
.counter { display: inline-flex; align-items: center; font-weight: 700; color: #c6cede; font-variant-numeric: tabular-nums; }
.cs, .cl { line-height: 1.5; } /* matches roll rows */
.roll { display: block; height: 1.5em; overflow: hidden; } /* em = 15px → 22.5px */
.strip { display: block; }
.strip i { display: block; height: 1.5em; line-height: 1.5; font-style: normal; }
#like:checked ~ .meta .strip { transform: translateY(-1.5em); }
.strip { transition: transform .55s cubic-bezier(.3,1.1,.35,1.05)??
```
Hmm wait — .cs/.cl line-height 1.5 → their inline text line box = 22.5px; roll height 22.5 ✓ align-items center centers equal boxes → baselines align ✓ (same font). The comma inside "12,04" ✓ tabular-nums keeps digits equal width ✓.
transition on .strip with slight overshoot: cubic-bezier(.25,.9,.3,1.15).
Checked color: `#like:checked ~ .meta .counter { color: #ff7d97; }` transition color .4s.
**plus:**
```css
.plus { position:absolute; left: calc(50% + 64px); top: -10px; font: 800 15px/1 ...; color: #ff5c7a; opacity: 0; }
```
Hmm — counter width ≈ "12,047 likes" ≈ 15px font: digits ~8.5px each... "12,04" ≈ 5 chars ≈ 40px + roll 9 + " likes" ≈ 38 → total ≈ 90px → half 45 → right edge ≈ 50%+45. Put plus at 50%+56px. top -12. Add tiny heart inside plus? "+1" plain ✓.
`#like:checked ~ .meta .plus { animation: rise .85s cubic-bezier(.2,.7,.4,1) .18s both; }`
```css
@keyframes rise {
0% { transform: translateY(0); opacity: 0; }
30% { opacity: 1; }
100% { transform: translateY(-34px); opacity: 0; }
}
```
**hint:** `.hint { margin-top: 16px; font-size: 13px; color: #6d7590; }` content: "Click to like · click again to undo · the spacebar works too" — hmm keyboard: to use spacebar, first click page to focus iframe... The system note is about games. This is fine as a bonus mention. Actually, I'll phrase: "Go on — click it. Click it again, too." Let me finalize copy later.
**footer:** `.foot { margin-top: auto?? ` wait — hero margin auto already handles; footer is after hero: with hero margin:auto 0, free space all goes to hero's margins → footer sits at bottom ✓ (flex column, footer last, no auto needed since hero absorbs). Actually careful: auto margins on hero absorb ALL free space → header top, hero centered in remaining, footer bottom ✓.
footer: `font-size: 12px; color: #59617a; letter-spacing: .02em;` content: "1 checkbox · 13 @keyframes · 0 lines of JavaScript". Count keyframes: pop, heartbeat, ring, fly, drop, ghostUp, rise, flash, shake, pulse, emPop, ripple = 12. I'll finalize count after writing. Add a tiny red heart inline in footer? A mini CSS heart 8px inline-block... cute: "made with <3" — I'll add `<i class="heart mini"></i>`? The .heart is absolute positioned — needs wrapper. Skip, keep text.
**badge:**
```html
<p class="badge"><span class="bdot"></span>Pure CSS · zero JavaScript</p>
```
uppercase via CSS.
**Header h1:** `You’re gonna <em>like</em> this.` em gradient + serif italic. Add also small underline flourish? skip.
**shake:**
```css
@keyframes shake {
0%,100% { transform: translate(0,0); }
15% { transform: translate(-7px, 4px) rotate(-.5deg); }
30% { transform: translate(6px, -5px) rotate(.45deg); }
45% { transform: translate(-5px, 3px) rotate(-.35deg); }
60% { transform: translate(4px, -3px) rotate(.3deg); }
75% { transform: translate(-3px, 2px) rotate(-.2deg); }
90% { transform: translate(2px, -1px); }
}
body:has(#like:checked) .scene { animation: shake .55s ease-out; }
```
Careful — does animating .scene transform break anything? .scene is the flex container min-height 100svh — transform ok. But — the .scene has align-items center etc, transform creates containing block — .flash is OUTSIDE .scene (body child) ✓ unaffected.
Wait — also :has matching inside body: `body:has(#like:checked) .scene` — .scene is child of body ✓.
One concern — when unchecked again, shake animation removed → snap (transform from mid-animation to none). If user unchecks after .55s, no snap ✓. Rapid uncheck during shake → tiny snap, acceptable (masked by squish).
**flash:** `.flash` as body child, fixed, z-index 50, pointer-events none. `body:has(#like:checked) .flash { animation: flash .8s ease-out; }`.
**emPop:** `body:has(#like:checked) h1 em { animation: emPop .8s cubic-bezier(.3,1.6,.4,1); }` em inline → needs display inline-block to transform ✓ (always inline-block, gradient text needs it anyway? background-clip text works on inline too, but transform needs inline-block).
```css
@keyframes emPop { 0% { transform: scale(1) } 40% { transform: scale(1.14) rotate(-2deg) } 100% { transform: scale(1) } }
```
**pulse (idle):**
```css
.pulse { position:absolute; inset:-2px; border-radius:999px; border:1.5px solid rgba(255,84,120,.4); opacity:0; animation: pulse 2.8s ease-out infinite; }
@keyframes pulse { 0% { transform: scale(1); opacity: .75; } 70%,100% { transform: scale(1.32); opacity: 0; } }
#like:checked + .stage .pulse { animation: none; opacity: 0; }
```
inset -2 to sit just outside border. Hmm inset -2 makes it bigger than button ✓.
**ripple:**
```css
.btn::after { content:""; position:absolute; inset:0; border-radius:inherit; background: radial-gradient(closest-side circle at 50% 50%, rgba(255,255,255,.28), transparent 70%); opacity: 0; transform: scale(.5); }
.stage:active .btn::after { animation: ripple .5s ease-out; }
@keyframes ripple { 0% { opacity: .8; transform: scale(.55); } 100% { opacity: 0; transform: scale(1.15); } }
```
Hmm ::after default has transform scale(.5) opacity 0 — when not animating invisible ✓.
**Background:**
```css
body { background: #0a0c13; }
.bg { position: fixed; inset: 0; z-index: 0; pointer-events: none;
background:
radial-gradient(900px 500px at 50% 118%, rgba(255,46,99,.10), transparent 60%),
radial-gradient(700px 420px at 50% -10%, rgba(76,110,245,.08), transparent 60%);
}
.bg::after { content:""; position:absolute; inset:0; background-image: radial-gradient(rgba(255,255,255,.05) 1px, transparent 1.4px); background-size: 26px 26px; mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, black, transparent 75%); }
```
Dot grid with radial mask — subtle depth ✓. z-index: .scene content above bg: .scene position relative? default static — bg is fixed z 0, .scene static → paints above? Static elements without z-index paint above positioned z-0?? Painting order: positioned elements with z-index 0 paint in DOM/tree order interleaved... .bg is positioned (fixed) with z-index 0; .scene is non-positioned block — non-positioned elements paint BELOW positioned ones in the same stacking context (step 4 vs 6/8 of paint algorithm? Actually: background/borders of non-positioned blocks paint early; positioned elements paint later). So .bg (fixed, z 0) would paint ABOVE .scene text!? Positioned descendants paint in tree order at their stacking level; non-positioned content paints in an earlier layer. Yes — .bg fixed z-index:0 paints above non-positioned .scene. Fix: give .scene `position: relative; z-index: 1`. ✓ And .flash z 50 above all ✓.
**Fonts:** headline `font-family: "Avenir Next", "SF Pro Display", Segoe UI, ...`? I'll define: `--font: ui-sans-serif, -apple-system, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;` and em serif: Georgia, "Times New Roman", serif.
**h1 style:** `font-size: clamp(30px, 5.4vw, 54px); font-weight: 800; letter-spacing: -.025em; color: #f2f5fc;` `em { font-style: italic; font-family: Georgia..., font-weight: 600?; background: linear-gradient(100deg, #ff2e63 10%, #ff8fae 90%); -webkit-background-clip: text; background-clip: text; color: transparent; padding: 0 .04em; display: inline-block; }` — note: with background-clip text, drop-shadow on text? skip.
Hmm — em inline-block + gradient text: works ✓.
**sub:** `font-size: clamp(14px, 1.8vw, 16.5px); color: #97a0b6; max-width: 46ch; margin: 14px auto 0; line-height 1.6`.
Also badge margin-bottom: `header { padding-top: 4vh?? }` scene has padding; let scene: `padding: clamp(20px, 4vh, 44px) 20px;`.
**Autoplay demo consideration**: "Something meaningful visible on first paint" ✓ (whole page). Not a game — no AI demo needed. But could I make it self-demo? A CSS-only auto-play: animation on the checkbox? Can't check a checkbox via CSS... I could preload it checked? Then first paint shows LIKED state (less inviting to click — user clicks to UNLIKE, which is less fun). Better unchecked with pulsing hint. The screenshot at 3s shows the beautiful idle page ✓.
Hmm — wait, actually should I reconsider: could add a tiny CSS animation that simulates a pointer tapping the button? E.g., a fake cursor that periodically presses... pressing requires :active — can't simulate programmatically without JS. Skip. The pulse + copy invites.
**Selection color**, **scrollbar**: body overflow hidden — wait, DO I want overflow hidden on body? If viewport very short (like 500px height), content (340px) still fits ✓. Landscape phones 375 height? Content ~340 + header compress... might clip footer — acceptable edge. Use `overflow: hidden` on html,body? Hmm — but clipping fx needs it. Use `overflow-x: clip`? Hmm: overflow-x hidden on body creates scroll container?? Simpler: `body { overflow: hidden; }` — fine. But careful — `body { margin: 0 }` ✓.
Wait — actually there's a subtle issue with `overflow:hidden` on body and fixed .bg — fine.
Also — the ghost heart flying up 330px: on 800 height from y~430 → 100 ✓ inside. On 600 height: button y ~ 350 → ghost to 20 ✓ ok.
**Reduced motion** — skipping, decided.
**Now — will `#like:checked + .stage` work given input is sr-only positioned? Input must be earlier sibling ✓ in DOM before label ✓.
sr-only:
```css
#like { position: absolute; opacity: 0; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; pointer-events: none; }
```
Wait pointer-events none on input — label clicks still toggle (label for=) ✓, keyboard space on focused input works ✓ (pointer-events irrelevant for keyboard).
But note: clicking label focuses + toggles input in Chrome ✓.
**focus-visible:**
```css
#like:focus-visible + .stage .btn { outline: 2px solid #ff6b8c; outline-offset: 5px; }
```
Hmm — does clicking via label show focus-visible? Click-focus isn't focus-visible typically ✓ only keyboard. But on uncheck via keyboard, focus-visible shows while checked ✓ fine.
Also add `#like:focus-visible + .stage .btn { ... }` needs outline on .btn ✓.
**Let me also handle `user-select: none` on stage** ✓ and `-webkit-user-select`.
**Putting HTML together:**
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Over-Engineered Like Button — Pure CSS</title>
<style>
:root { vars }
reset...
...
</style>
</head>
<body>
<div class="bg"></div>
<div class="flash"></div>
<main class="scene">
<header>
<p class="badge"><span class="bdot"></span>Pure CSS · Zero JavaScript</p>
<h1>You’re gonna <em>like</em> this.</h1>
<p class="sub">One hidden checkbox, a pile of keyframes, and a truly unreasonable explosion of joy. Go ahead — press the button.</p>
</header>
<div class="hero">
<input type="checkbox" id="like">
<label class="stage" for="like">
<span class="fx-back">
<span class="pulse"></span>
<span class="ghost"><i class="heart gh"></i></span>
</span>
<span class="btn">
<span class="heart-wrap"><i class="heart"></i></span>
<span class="txt">
<span class="t t-like">Like</span>
<span class="t t-liked">Liked</span>
</span>
</span>
<span class="fx-front">
<span class="boom">
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
<i class="pt dot" style="--dx:...;"></i> × 10
<i class="pt star" ...> × 4
<i class="pt mh" ...> × 2
<i class="conf" ...> × 8
</span>
</span>
</label>
<p class="meta">
<span class="counter"><span class="cs">12,04</span><span class="roll"><span class="strip"><i>7</i><i>8</i></span></span><span class="cl"> likes</span></span>
<span class="plus">+1</span>
</p>
<p class="hint">Click to like · click again to undo · repeat forever, it never gets old</p>
</div>
<footer>1 checkbox · 12 @keyframes · 0 lines of JavaScript</footer>
</main>
</body>
</html>
```
Wait — `.hero` uses margin auto — but hero is a div, flex column align center. And `.hero` children: input, label(stage), meta, hint — all centered via align-items: center ✓. input absolute (sr) ✓.
Hmm — the `.plus` inside .meta: `.meta { position: relative }` ✓.
Also — footer content: let me count keyframes precisely when CSS done.
Also — hmm, one thing: `#like:checked ~ .meta` — meta is sibling AFTER label ✓ works (input, label, meta all children of .hero ✓).
And `body:has(#like:checked) .flash` etc ✓.
Wait — `.scene` selector for shake: `.scene` contains hero contains input — `body:has(#like:checked) .scene` ✓.
Also em pop: `body:has(#like:checked) header em` ✓ or just `h1 em`.
Let me now also double-check **t-like/t-liked transitions with delay interplay**:
```css
.t { grid-area: 1/1; white-space: nowrap; transition: transform .4s cubic-bezier(.55,0,.3,1), opacity .35s ease; }
.t-like { }
.t-liked { color: #ff6b8c; transform: translateY(125%); opacity: 0; }
#like:checked + .stage .t-like { transform: translateY(-125%); opacity: 0; transition: transform .28s cubic-bezier(.5,0,.7,.4), opacity .22s ease; }
#like:checked + .stage .t-liked { transform: translateY(0); opacity: 1; transition: transform .5s cubic-bezier(.25,1.5,.4,1) .1s, opacity .3s ease .1s; }
```
Enter with overshoot bezier (y 1.5) ✓ delayed .1s ✓. Exit fast ✓. Reverse (uncheck): t-liked uses base transition (no delay, standard) ✓; t-like returns with base .4s ✓.
**Heart transitions:**
```css
.heart { transition: color .4s ease, filter .4s ease; }
#like:checked + .stage .btn .heart { color: #ff3b5f; filter: drop-shadow(0 2px 10px rgba(255,46,99,.55)); }
```
**heart-wrap:**
```css
.heart-wrap { position: relative; width: 36px; height: 36px; flex: none; transition: transform .35s cubic-bezier(.3,1.4,.4,1); }
#like:checked + .stage .heart-wrap { animation: pop .85s cubic-bezier(.25,1,.4,1) both, heartbeat 1.7s linear .85s infinite; }
```
Hmm two animations — with fill both on pop: after pop ends, heartbeat (delayed .85 = pop duration) takes over. During heartbeat, transform from heartbeat keyframes ✓. Note heartbeat should also have same base... it's infinite ✓.
pop keyframes with rotate wobble:
```css
@keyframes pop {
0% { transform: scale(1) rotate(0deg); }
28% { transform: scale(1.45) rotate(-8deg); }
44% { transform: scale(.82) rotate(5deg); }
58% { transform: scale(1.28) rotate(-3deg); }
72% { transform: scale(.96) rotate(1deg); }
86% { transform: scale(1.15) rotate(0deg); }
100% { transform: scale(1.08) rotate(0deg); }
}
@keyframes heartbeat {
0%, 100% { transform: scale(1.08); }
14% { transform: scale(1.22); }
28% { transform: scale(1.08); }
42% { transform: scale(1.19); }
60% { transform: scale(1.08); }
}
```
**Check the `.boom` and fx z:**
```css
.fx-back, .fx-front { position: absolute; inset: 0; pointer-events: none; }
.fx-back { z-index: 1; }
.fx-front { z-index: 3; }
.btn { position: relative; z-index: 2; }
.boom { position: absolute; left: var(--hx, 45px); top: 50%; width: 0; height: 0; }
```
Hmm wait — fx-back/front absolute inset 0 relative to .stage ✓; but z-index on them requires... they're positioned ✓.
Hold on — .fx-back contains .pulse (inset -2px of stage) ✓ and ghost anchored at left:45px top:50% of fx-back — set `.ghost { position:absolute; left: var(--hx); top: 50%; }` ✓.
**Particles CSS:**
```css
.pt { position: absolute; left: 0; top: 0; opacity: 0; will-change: transform, opacity; }
.pt.dot { width: var(--s, 10px); height: var(--s, 10px); border-radius: 50%; background: var(--c, #fff); }
.pt.star { width: var(--s, 14px); height: var(--s, 14px); background: var(--c, #ffd166); clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%); }
.pt.mh { width: var(--s, 12px); height: var(--s, 12px); background: var(--c, #ff2e63); }
.pt.mh::before, .pt.mh::after { content: ""; position: absolute; width: var(--s, 12px); height: var(--s, 12px); border-radius: 50%; background: var(--c, #ff2e63); }
.pt.mh::before { left: 0; top: calc(var(--s, 12px) / -2); }
.pt.mh::after { left: calc(var(--s, 12px) / -2); top: 0; }
#like:checked + .stage .pt { animation: fly var(--t, .9s) cubic-bezier(.16,.68,.32,1) var(--d, 0s) both; }
```
Hmm — `.pt.mh` with fly's rotate — noted, tumbling hearts ✓. BUT WAIT — .pt.mh uses ::before/::after — and fly transform applies to the .pt element including pseudos ✓ rotates whole ✓.
Star clip-path + rotation ✓.
**Confetti CSS:**
```css
.conf { position: absolute; left: 0; top: 0; width: var(--w, 8px); height: var(--h, 12px); border-radius: 2px; background: var(--c, #ffd166); opacity: 0; will-change: transform; }
#like:checked + .stage .conf { animation: drop var(--t, 1.3s) linear var(--d, .15s) both; }
```
**Ghost CSS:**
```css
.ghost { position: absolute; left: var(--hx, 45px); top: 50%; width: 0; height: 0; opacity: 0; }
.ghost .heart { --s: 62px; color: #ff3b5f; filter: blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8)); }
#like:checked + .stage .ghost { animation: ghostUp 1.3s cubic-bezier(.22,.61,.31,1) .04s both; }
```
Note: ghost opacity animated by keyframes ✓ (base 0, keyframes control).
Ghost heart positioning: `.heart` base `left:50%; top:50%; transform: translate(-50%,-50%) rotate(45deg)` — parent .ghost 0×0 → 50% of 0 = 0 ✓ heart centered at ghost origin ✓.
Ghost keyframes with transform-list consistency: translate(x,y) scale() rotate() in all frames:
```css
@keyframes ghostUp {
0% { transform: translate(0,0) scale(.2) rotate(0deg); opacity: 0; }
16% { opacity: .55; }
38% { transform: translate(4px,-40px) scale(1.1) rotate(-9deg); opacity: .55; }
100% { transform: translate(30px,-320px) scale(2) rotate(12deg); opacity: 0; }
}
```
Wait — 0% has opacity 0, 16% .55, 38% .55 (transform only), 100% 0 — opacity interpolates 0→.55 over 0-16 ✓ stays .55 til 38 ✓ fades to 0 ✓. But the 38% frame doesn't list opacity... opacity specified at 16 (.55), 38? NOT listed → opacity interpolates between 16% and 100% linearly?? Keyframes: opacity defined at 0,16,100 → between 16 and 100 fades gradually. I want hold: add opacity at 38: `38% { transform: ...; opacity: .55; }` ✓ I wrote ".55" — yes included above ✓.
**flash + bg** — done above.
Let me now think about **`--hx` accuracy** once more. .btn: height 64, border 1, padding-left 26. Content: heart-wrap 36. Heart square center: left border-box edge (0) + border 1 + padding 26 + 18 = 45. y: 32 (half of 64) ✓. boom top: 50% of 64 = 32 ✓.
BUT — with `box-sizing: border-box` globally, padding-left 26 means content starts 1+26=27 from left edge ✓ so 27+18=45 ✓.
**Also `.stage:active .btn` squish + transform conflict with hover lift** — define after hover ✓.
**One more check — the label wraps the whole stage including fx spans sized == button.** Clicking anywhere in button area toggles ✓.
**Accessibility labels:** input aria-label="Like this page" ✓.
**Counter initial value**: "12,047" — plausible.
Let me also double check `.txt` width: grid with two items same cell — width = max content ✓ "Liked" wider ✓ no shift ✓.
Also: **the `.t` fonts**: `.btn` font 700 17px — inherit ✓.
**Wait — btn overflow hidden + heart drop-shadow filter**: drop-shadow on .heart inside overflow-hidden .btn — the glow may clip at button edge — heart is well inside ✓.
**Ghost behind button** — .fx-back z1 under btn z2 ✓ ghost visible around button edges as it grows past 64px height... ghost heart 62px square → visual ~87px tall/wide — pokes out around the button nicely ✓.
**Flash gradient center**: button position ~ (50%, 54%) of viewport — since hero is centered between header (~170px) and footer. In 800: header ends ~190, footer ~760, hero center ~ (190+760)/2 ≈ 475 → 59%?? Hmm hero includes meta+hint below button: hero height ≈ 64+36+22+16+18 ≈ 156 → hero top ≈ 475-78 = 397 → button center ≈ 397+32 = 429 → 53.6% of 800. Use `circle at 50% 54%` ✓ close enough.
**Shake + flash via :has on rapid toggling** — fine.
**Now, potential issue: `body:has(...)` performance** — trivial.
**Prefers color scheme** — fixed dark, fine.
Let me also add **subtle noise**? skip.
Also **`::selection`** styling — small delight: `::selection { background: rgba(255,46,99,.35); }`.
**Title of page** ✓.
Footer keyframe count — let me enumerate @keyframes in final CSS:
1. pop
2. heartbeat
3. ring
4. fly
5. drop
6. ghostUp
7. rise
8. flash
9. shake
10. pulse
11. emPop
12. ripple
= 12 ✓ "1 checkbox · 12 @keyframes · 0 lines of JavaScript".
Hmm, also bdot pulse? If I animate badge dot glow, count changes. I'll animate badge dot with `pulse` reuse? Different keyframes... just reuse a simple opacity pulse — add keyframe #13 "breathe"? Let me add: badge dot gentle glow pulse → 13 keyframes. OK footer: "1 checkbox · 13 @keyframes · 0 lines of JS". I'll count final.
Let me write **complete CSS** now carefully, section by section, keeping it organized with comments.
```css
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
overflow: hidden;
font-family: var(--font);
background: #0a0c13;
color: #f2f5fc;
-webkit-font-smoothing: antialiased;
}
:root {
--font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--serif: Georgia, "Times New Roman", serif;
--red: #ff2e63;
--red-hi: #ff5c7a;
--pink: #ff8fae;
--gold: #ffd166;
--cyan: #4cc9f0;
--violet: #b388ff;
--ink: #f2f5fc;
--mut: #97a0b6;
--mut-2: #6d7590;
--hx: 45px;
}
```
Background:
```css
.bg { position: fixed; inset: 0; z-index: 0; background: radial-gradient(1000px 560px at 50% 115%, rgba(255,46,99,.11), transparent 62%), radial-gradient(760px 420px at 50% -8%, rgba(90,126,255,.09), transparent 60%); }
.bg::after { content: ""; position: absolute; inset: 0; background-image: radial-gradient(rgba(255,255,255,.05) 1px, transparent 1.4px); background-size: 26px 26px; -webkit-mask-image: radial-gradient(85% 70% at 50% 42%, #000 30%, transparent 78%); mask-image: ...; }
```
Flash:
```css
.flash { position: fixed; inset: 0; z-index: 40; pointer-events: none; opacity: 0; background: radial-gradient(560px 380px at 50% 54%, rgba(255,46,99,.22), rgba(255,46,99,.05) 45%, transparent 70%); }
body:has(#like:checked) .flash { animation: flash .8s ease-out; }
@keyframes flash { 0% { opacity: 0 } 16% { opacity: 1 } 100% { opacity: 0 } }
```
Scene:
```css
.scene { position: relative; z-index: 1; min-height: 100svh; display: flex; flex-direction: column; align-items: center; padding: clamp(22px, 4vh, 48px) 20px 26px; text-align: center; }
body:has(#like:checked) .scene { animation: shake .55s cubic-bezier(.3,.6,.4,1); }
```
Header:
```css
header { display: flex; flex-direction: column; align-items: center; }
.badge { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 8px 16px; border: 1px solid rgba(255,255,255,.13); border-radius: 999px; background: rgba(255,255,255,.04); font-size: 11px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: #aeb6c9; }
.bdot { width: 7px; height: 7px; border-radius: 50%; background: var(--red); box-shadow: 0 0 10px rgba(255,46,99,.9); animation: breathe 2.2s ease-in-out infinite; }
@keyframes breathe { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .55; transform: scale(.78); } }
h1 { margin: 20px 0 0; font-size: clamp(30px, 5.6vw, 54px); font-weight: 800; letter-spacing: -.028em; line-height: 1.06; }
h1 em { display: inline-block; font-family: var(--serif); font-style: italic; font-weight: 600; padding: 0 .05em; background: linear-gradient(100deg, var(--red) 15%, var(--pink) 85%); -webkit-background-clip: text; background-clip: text; color: transparent; }
body:has(#like:checked) h1 em { animation: emPop .85s cubic-bezier(.3,1.7,.4,1); }
@keyframes emPop { 0% { transform: scale(1) rotate(0); } 45% { transform: scale(1.13) rotate(-2.5deg); } 100% { transform: scale(1) rotate(0); } }
.sub { margin: 16px auto 0; max-width: 44ch; font-size: clamp(14px, 1.9vw, 16.5px); line-height: 1.65; color: var(--mut); }
```
Hero:
```css
.hero { margin: auto 0; display: flex; flex-direction: column; align-items: center; padding: 30px 0 10px; }
```
Hmm — hero margin auto top AND bottom; plus its own padding for effect clearance visual.
Wait — with hero margin: auto 0 — vertical auto in column flex ✓.
Input:
```css
#like { position: absolute; width: 1px; height: 1px; margin: -1px; opacity: 0; pointer-events: none; }
```
(clipping not strictly needed with opacity 0 + 1px; keep simple. But it's position absolute — where? inside hero — absolute relative to nearest positioned ancestor (.scene relative) — 1px invisible fine.)
Hmm — `overflow: hidden` clip? not needed with opacity 0.
Stage + btn (as designed above).
Heart:
```css
.heart { --s: 20px; position: absolute; left: 50%; top: 50%; width: var(--s); height: var(--s); transform: translate(-50%,-50%) rotate(45deg); background: currentColor; color: #97a1b8; transition: color .38s ease, filter .45s ease; }
.heart::before, .heart::after { content: ""; position: absolute; width: var(--s); height: var(--s); border-radius: 50%; background: currentColor; }
.heart::before { left: 0; top: calc(var(--s) / -2); }
.heart::after { left: calc(var(--s) / -2); top: 0; }
```
Hmm wait — heart-wrap is 36×36 but the heart visual ~28px: fine centered.
heart-wrap:
```css
.heart-wrap { position: relative; width: 36px; height: 36px; flex: none; transition: transform .4s cubic-bezier(.3,1.5,.45,1); }
#like:checked + .stage .heart-wrap { animation: pop .85s cubic-bezier(.25,1,.4,1) both, heartbeat 1.7s linear .85s infinite; }
```
btn text etc. Also `.btn .txt`:
```css
.txt { display: grid; overflow: hidden; }
```
meta/counter/plus/hint/footer — as designed.
footer:
```css
footer { margin-top: auto?? }
```
wait — hero already margin auto. Footer: just normal (it's last; hero's auto margins push it to bottom). Give footer `padding-top: 12px` — but hero margin:auto 0 absorbs space above AND below hero?? margin: auto 0 = top auto, bottom auto, left/right 0?? `margin: auto 0` → vertical auto, horizontal zero. In flex column, auto top+bottom on hero → free space split between hero's top and bottom margins → footer at bottom, header top, hero centered ✓.
footer text: `.foot { font-size: 12px; letter-spacing: .03em; color: #565e78; }` content: "1 checkbox · 13 @keyframes · 0 lines of JavaScript". Also maybe a mini heart divider — skip.
hint:
```css
.hint { margin: 18px 0 0; font-size: 13px; color: var(--mut-2); }
```
copy: "Click it to like · click again to undo · and again… you know you want to"
meta margin-top: 38px.
Wait — one important thing: **.stage and .hero alignment** — hero align-items center ✓.
**Rings position left/top**: rings inside .boom at (0,0): `left: -45px; top: -45px; width: 90px; height: 90px` — hmm, ring center = boom origin ✓.
Let me also double check **fx-front doesn't block clicks** — pointer-events none ✓. Label receives clicks anywhere in its box (== button) ✓.
**Very important sanity check — animation `both` fill on .pt with delays and toggling** ✓ fine.
**Potential issue: `.stage` inline-flex + label semantics** — label containing everything ✓ accessible name from content ("Like Liked") — set aria-label on label? Label implicit for input; its accessible name = text content "LikeLiked" — screen readers announce both spans... could add `aria-hidden` on fx and... The t-liked hidden visually but not from AT. Minor; could set `.t-liked { visibility: hidden }`? Visibility hidden + transition? visibility transitions discretely... With animation approach it complicates. For a demo, acceptable. Actually easy improvement: give input `aria-label="Like this page"` and label... the label text still announces. Whatever — fine for demo.
**Now let me also verify the fly keyframes calc usage:** `calc(var(--dx) * .38)` — --dx defined inline as e.g. `122px` ✓ multiplication by number ✓.
--rot: e.g. `200deg` → calc(200deg * .38) ✓.
**One risk — keyframe with `var()` inside `transform`**: works ✓ (custom props resolve at use-time).
**Confetti `drop` uses `--sw`** e.g. `34px` — calc(var(--sw)*.4) ✓.
**Now, `will-change` on many elements** — fine.
Let me reconsider **`.pt` initial `transform`** — none; but with animation-fill both + delay, pre-delay shows 0% frame ✓ invisible. After animation, 100% frame (opacity 0) ✓.
**Ring keyframes** — base opacity 0, checked animation both ✓.
**Order of CSS rules**: checked-state rules after base.
Now, **button checked text color**: t-liked color set; also maybe t-like color dims. fine.
**Double-check `pop` conflict with `heartbeat` transform-origin**: heart-wrap 36×36, origin center ✓ scale/rotate around center ✓.
**Ghost heart size 62** — with --s var override: `.ghost .heart { --s: 62px; ... }` — but base .heart `left:50%; top:50%; transform: translate(-50%,-50%) rotate(45deg)` — parent 0×0 → left/top = 0 ✓ centered ✓.
Hmm wait — translate(-50%,-50%) percentages relative to the heart's own size (62px) ✓ centers the square on the point ✓. But visual heart center vs square center: square center = rotation center; the visual heart (with lobes) extends more up/left... after rotation it's roughly centered — the tip extends down-right-ish? Visual bounds pre-rotation: x ∈ [-s/2, s], y ∈ [-s/2, s] — center at (s/4, s/4) not (s/2,s/2). After rotate 45° about (s/2, s/2): the visual center offset (s/4,s/4)... rotated 45°, the shape's centroid stays displaced from rotation origin by |(s/4,s/4)| rotated → ~s·0.354 at 45° direction → down-right?? Points rotate CW: vector (s/4, s/4)/|..| at 45° → rotates to 90° → pointing down: so visual center sits ~0.35s BELOW rotation origin?? Hmm let me verify: pre-rotation, visual centroid ≈ (s/4, s/4)+... hmm actually rough centroid: heart shape with tip at (s,s) corner, lobes toward (0,0)-side. Centroid biased toward tip (the square's bulk) — centroid ~ (0.55s, 0.55s)? Mass distribution: square full [0..s]², plus circle at (s/2,0) and (0,s/2). Centroid x = (s·(s/2) + (πs²/4)·(s/2)) / (s²+πs²/2)... = (s³/2 + πs³/8) / (s²(1+π/2)) = s(0.5+0.3927)/(1+1.5708) = s·0.8927/2.5708 = 0.347s. Same y. So centroid (0.347s, 0.347s) pre-rotation, which relative to rotation origin (0.5s,0.5s) is (-0.153s, -0.153s) — magnitude 0.216s at 225°(up-left diag). After rotating +45°: 225°+45° = 270°?? Rotation direction: CSS positive = clockwise (screen coords). Vector at 225° (up-left in screen coords where y down: angle 225° means x=cos225=-.707, y=sin225=-.707 → up-left ✓). Rotating CW by 45°: 225°+45° = 270° → (cos270, sin270) = (0,-1) → straight UP. So visual centroid ends 0.216s ABOVE rotation center. For s=20: 4.3px above center — the button heart will sit ~4px high in its 36px wrap; for ghost s=62: 13px above origin. Slight upward bias — actually hearts read better slightly... hmm, it means ghost rises from a point 13px above button-center — negligible aesthetically. But the button heart in wrap: centered square but visual centroid 4px up — the wrap is 36 tall, heart visual ~28px → occupies from y≈ 18-14+4.3?? Roughly: visual extents around centroid... Fine — it'll look centered enough; can nudge `top: calc(50% + 3px)` on .btn .heart if needed. I'll set `.heart-wrap .heart { top: calc(50% + 3px) }`?? Hmm — wait, but then --hx anchor for boom: heart visual center x: rotation center x = 45; centroid x offset: 0.216s in 270° direction → x offset 0 → boom x=45 still aligns with visual center vertically shifted. Eh — honestly ±4px nobody notices. Keep top:50%. Actually for the MAIN button heart let me just keep it; symmetric-ish.
**Now — write final copy:**
- Badge: "PURE CSS · ZERO JAVASCRIPT"
- H1: "You’re gonna *like* this."
- Sub: "One hidden checkbox, a mountain of keyframes, and a truly excessive celebration. Press the button — then press it again."
- Hint: "Click to like · click again to undo · repeat as needed"
- Footer: "1 checkbox · 13 @keyframes · 0 lines of JavaScript"
Also — title tag: "You're gonna like this — pure CSS like button".
Let me now think about whether the **first-paint screenshot** (3s, no interaction) looks good: dark page, glowing badge, big headline with gradient "like", sub, pulsing pill button with gray heart "Like", counter below, hint, footer. Yes ✓.
**Edge — `100svh` support**: latest Chrome ✓.
**Edge — `:has` on body while .flash/.scene are body descendants** ✓.
Wait — .flash and .bg are body children, .scene too. body:has(#like:checked) — input inside .scene inside body ✓.
**Let me now also double-check the confetti drop keyframes timing-function trick**:
```css
@keyframes drop {
0% { transform: translate(-50%,-50%) translate(0,0) rotate(0deg); opacity: 0; animation-timing-function: cubic-bezier(.15,.7,.35,1); }
10% { opacity: 1; }
32% { transform: translate(-50%,-50%) translate(calc(var(--sw) * .45), -95px) rotate(230deg); opacity: 1; animation-timing-function: cubic-bezier(.5,0,.8,.5); }
82% { opacity: 1; }
100% { transform: translate(-50%,-50%) translate(var(--sw), 240px) rotate(640deg); opacity: 0; }
}
```
Hmm — opacity keyframes 0,10,32,82,100 — between 32 and 82 constant 1 ✓. transform at 0,32,100 ✓. But NOTE: the timing function specified at 32% applies to segments starting at 32% for ALL properties → opacity 32→82 gets ease-in (fine, it's constant 1). ✓
But WAIT — there's a subtlety: transform specified at 0% and 32% and 100%; between 0-32 the tf is from 0% (cubic-bezier(.15,.7,.35,1) ease-out — rise decelerating ✓). 32-100 uses ease-in (gravity accel ✓). But the rise from 0 to -95px happens over 0-32% — then fall 32-100 from -95 to +240 — total drop 335px in 68% of ~1.3s ≈ .88s → fast-ish fall ✓ fine.
Rotation: 0→230deg during rise (tumbling up), then 230→640 during fall ✓.
The confetti initial: at boom (button center) — hmm, confetti STARTS visible at button center (appears at heart) — slight pop-in at 10% opacity ramp ✓ they materialize — ok. Better: start slightly above: add initial translateY -20px? Meh — opacity 0→1 by 10% masks.
**Particle z vs confetti** — same layer ✓.
**Now — will 24 elements inside .boom animate performantly?** transforms+opacity ✓ GPU-friendly ✓.
**Contrast check**: sub #97a0b6 on #0a0c13 ✓ ~7:1 ok. hint #6d7590 ~4.5 ✓. footer #565e78 ~3.2 — small text low-ish but decorative footer; bump to #6b7490? use var(--mut-2) #6d7590 for footer too. Fine.
**Now write the final file.** Also remember: NO text outside the code block, single html file.
Also — subtle: `label` default cursor pointer ✓ set anyway.
Let me also handle **iOS Safari double-tap** — `-webkit-tap-highlight-color: transparent; touch-action: manipulation` on stage ✓ (demo judged in Chrome anyway).
One more design consideration — **the "Like" button width**: "Liked" text + heart + gaps: ~ 36+13+ ~48 + 58 padding ≈ 155px. Good pill.
**Font for counter digits** — tabular-nums ✓ set on .counter.
Let me also reconsider the **`.roll` em math**: .meta font-size 15px; --row: 1.5em?? Let me define `.meta { font-size: 15px; }` `.roll { height: 1.5em; }` .strip i { height: 1.5em; line-height: 1.5;?? } — line-height: 1.5 on i (font 15px → 22.5px line box ✓ matches height ✓ glyph centered in row ✓). `.cs, .cl { line-height: 1.5; }` their line boxes 22.5 ✓ align-items center on .counter (inline-flex) — hmm .cs is inline span (flex item → blockified) with line-height 1.5 → height 22.5 ✓ roll 22.5 ✓ centered ✓ baselines align ✓.
translateY on .strip: `calc(-1 * 1.5em)` → -22.5px ✓ shows second digit ✓.
Use var: `--row: 1.5em;` on .meta; `.roll { height: var(--row); }` `.strip i { height: var(--row); }` checked: `transform: translateY(calc(-1 * var(--row)));` ✓ tidy.
**+1 placement** `left: calc(50% + 60px)` — counter half-width ~48px → just right of it ✓. top: -14px.
Also give `.plus` `text-shadow: 0 0 12px rgba(255,46,99,.6)` glow ✓.
Hmm — also think: **.meta p margin** reset ✓.
**Now — is there anything about "no horizontal scrollbar"?** body overflow hidden ✓ and effects contained ✓.
**Potential glitch: `.stage:active .btn` + hover transform interplay** — order rules: `.stage:hover .btn { transform: translateY(-2px); }` then `.stage:active .btn { transform: scale(.94) translateY(0); }` ✓ active wins (later, same specificity).
**Focus ring** — add.
**Let me write out all particle elements with final values:**
Dots (10):
```html
<i class="pt dot" style="--dx:122px; --dy:-8px; --c:var(--gold); --s:10px; --t:.8s; --d:0s; --rot:200deg"></i>
<i class="pt dot" style="--dx:98px; --dy:-64px; --c:var(--cyan); --s:12px; --t:.9s; --d:.04s; --rot:-160deg"></i>
<i class="pt dot" style="--dx:46px; --dy:-106px;--c:var(--pink); --s:9px; --t:.85s; --d:0s; --rot:120deg"></i>
<i class="pt dot" style="--dx:-34px; --dy:-110px;--c:var(--violet); --s:11px; --t:.95s; --d:.05s; --rot:-220deg"></i>
<i class="pt dot" style="--dx:-96px; --dy:-64px; --c:var(--red); --s:10px; --t:.8s; --d:.02s; --rot:180deg"></i>
<i class="pt dot" style="--dx:-124px;--dy:-6px; --c:var(--gold); --s:13px; --t:.9s; --d:0s; --rot:-140deg"></i>
<i class="pt dot" style="--dx:-92px; --dy:58px; --c:var(--cyan); --s:9px; --t:.9s; --d:.06s; --rot:240deg"></i>
<i class="pt dot" style="--dx:96px; --dy:60px; --c:var(--pink); --s:11px; --t:.95s; --d:.03s; --rot:-200deg"></i>
<i class="pt dot" style="--dx:12px; --dy:106px; --c:var(--gold); --s:10px; --t:.85s; --d:.02s; --rot:160deg"></i>
<i class="pt dot" style="--dx:-14px; --dy:102px; --c:var(--violet); --s:9px; --t:.9s; --d:.05s; --rot:-120deg"></i>
```
Stars (4):
```html
<i class="pt star" style="--dx:150px; --dy:-58px; --c:var(--gold); --s:16px; --t:1s; --d:.02s; --rot:320deg"></i>
<i class="pt star" style="--dx:-148px;--dy:-62px; --c:var(--violet); --s:14px; --t:1.05s;--d:.06s; --rot:-280deg"></i>
<i class="pt star" style="--dx:80px; --dy:-140px;--c:var(--cyan); --s:13px; --t:.95s; --d:0s; --rot:240deg"></i>
<i class="pt star" style="--dx:-76px; --dy:-146px;--c:var(--pink); --s:15px; --t:1s; --d:.05s; --rot:-210deg"></i>
```
Mini hearts (2):
```html
<i class="pt mh" style="--dx:44px; --dy:-148px; --c:var(--red); --s:14px; --t:1s; --d:.06s; --rot:260deg"></i>
<i class="pt mh" style="--dx:-48px;--dy:-152px; --c:var(--pink); --s:12px; --t:1.05s; --d:.1s; --rot:-230deg"></i>
```
Confetti (8):
```html
<i class="conf" style="--c:var(--gold); --sw:34px; --t:1.25s; --d:.12s; --w:8px; --h:13px"></i>
<i class="conf" style="--c:var(--cyan); --sw:-30px; --t:1.4s; --d:.2s; --w:7px; --h:11px"></i>
<i class="conf" style="--c:var(--pink); --sw:46px; --t:1.3s; --d:.16s; --w:9px; --h:9px"></i>
<i class="conf" style="--c:var(--violet); --sw:-40px; --t:1.45s; --d:.26s; --w:7px; --h:12px"></i>
<i class="conf" style="--c:#ffffff; --sw:22px; --t:1.2s; --d:.1s; --w:6px; --h:10px"></i>
<i class="conf" style="--c:var(--gold); --sw:-24px; --t:1.35s; --d:.22s; --w:8px; --h:8px"></i>
<i class="conf" style="--c:var(--cyan); --sw:38px; --t:1.5s; --d:.3s; --w:7px; --h:13px"></i>
<i class="conf" style="--c:var(--pink); --sw:-34px; --t:1.28s; --d:.18s; --w:9px; --h:11px"></i>
```
Rings:
```html
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
```
OK. Also `.conf` uses `--w/--h` with defaults.
**CSS for .ring with per-ring delays via inline? Use classes:**
```css
.ring { position: absolute; left: -46px; top: -46px; width: 92px; height: 92px; border-radius: 50%; opacity: 0; }
.ring.r1 { border: 2.5px solid rgba(255,255,255,.85); }
.ring.r2 { border: 2.5px solid var(--red); animation-delay??
```
Delays: put in the checked rule per class:
```css
#like:checked + .stage .r1 { animation: ring .7s cubic-bezier(.2,.65,.35,1) 0s both; }
#like:checked + .stage .r2 { animation: ring .78s cubic-bezier(.2,.65,.35,1) .09s both; }
#like:checked + .stage .r3 { animation: ring .85s cubic-bezier(.2,.65,.35,1) .18s both; }
```
Or one rule + `--rd` inline. One rule cleaner:
```css
#like:checked + .stage .ring { animation: ring .78s cubic-bezier(.18,.65,.35,1) var(--rd,0s) both; }
.r2 { --rd: .09s; border-color: var(--red); }
.r3 { --rd: .18s; border-color: var(--gold); }
```
with base border 2.5px solid rgba(255,255,255,.85) then overrides ✓.
**Double-check `ring` scale max**: 92px × 2.6 = 240px — from heart center at button left-ish — circle extends 120px left of boom: boom at x=45 in button → button width ~155 → boom is 45 from left → circle left edge at 45-120 = -75 relative to button left → 75px left of button ✓ inside viewport ✓. Vertical: ±120 from center y=32 → button is 64 tall → extends ~88px above/below button ✓ fine, over header/sub text — it's a ring outline, momentary ✓.
Hmm — **rings might be TOO big/overlapping headline**… 240px diameter circle centered at heart: top edge at 32-120 = -88 (88px above button) — sub text is ~60-90px above button? hero margin auto → gap between header and button large (~200px in 800 viewport) ✓ fine.
**Let me reconsider `fly` distances** — max 150px — good, matches rings.
**Ghost flight** — starts behind button (62px heart vs 64px button height — barely peeks! Hmm — ghost scale .2 → 12px, grows to 1.1 → ~68px visual ~97px? --s 62 → visual diagonal ~ 62*1.414 ≈ 88px?? visual box ~ (0.35s..) roughly 87px wide — WIDER than button (155 wide? no — button 155 wide, heart 87 wide) — ghost narrower than button width but taller-ish (87 vs 64) → peeks top/bottom ✓ then rises ✓.
**Ghost behind .pulse?** pulse in fx-back before ghost → ghost paints after pulse ✓ both behind btn ✓.
**One more consideration — `.heart` inside `.ghost` inherits transition color/filter — no state change for ghost — fine.**
**`h1 em` gradient text + `emPop` transform** ✓.
**Badge dot breathe** ✓.
Counting @keyframes final: breathe, emPop, pop, heartbeat, ring, fly, drop, ghostUp, rise, flash, shake, pulse, ripple = 13 ✓ footer "13 @keyframes".
Wait — `pulse` (idle attention) AND `breathe` (badge dot) and `ripple`. Yes 13 total. Let me list in code order: breathe, emPop, flash, shake, pulse, ripple, pop, heartbeat, ring, fly, drop, ghostUp, rise = 13 ✓.
Footer: "1 checkbox · 13 @keyframes · 0 lines of JavaScript".
**Let me write the html skeleton fully now, then CSS, checking every selector.**
Also — I realize I should double check: **label + input inside .hero, but the input is `position:absolute`** — it's positioned; absolute relative to nearest positioned ancestor = .hero? .hero isn't positioned; .scene is (position: relative) → input anchors to .scene at its static position... doesn't matter (1px, invisible, pointer-events none). But careful: it shouldn't create scroll — it's within scene flow area ✓. Hmm — margin: -1px keeps in place ✓.
**Focus-visible works on opacity-0 input?** Chrome focuses it via Tab ✓ (not display:none) ✓.
**Also — label click toggles input even though input pointer-events none** ✓ label→input association is DOM-based ✓.
Alright — also **`.t-liked` initial `transform: translateY(125%)`** — with grid overflow hidden, it's clipped below ✓ invisible + opacity 0 ✓.
Hmm — one thing about `.txt` height: grid rows sized by tallest item — .t line box ~17px... translateY(125%) of what? Percentage in translate refers to the element's own border-box height (17-ish) → 21px below ✓ txt box 17px tall, clip hides ✓.
**Btn hover glow:**
```css
.stage:hover .btn { transform: translateY(-2px); border-color: rgba(255,255,255,.3); box-shadow: inset 0 1px 0 rgba(255,255,255,.09), 0 18px 40px rgba(0,0,0,.5), 0 6px 24px rgba(255,46,99,.12); }
```
Checked hover fine (checked styles later in cascade? checked uses `#like:checked + .stage .btn` — specificity: (1 id, 1 class?? `#like:checked + .stage .btn` = id + pseudo-class(:checked) + class + class → (1,2,1)?? vs `.stage:hover .btn` = (0,3,0)?? Actually compute: `#like:checked + .stage .btn`: ids=1, classes/pseudo=[checked, stage, btn]=3 → (1,3,0)?? :checked counts as class-level, .stage class, .btn class → specificity (1,3,0). `.stage:hover .btn`: (0,3,0). ID wins ✓ checked styles override hover ✓ good (liked button keeps red style even on hover, just lift transform... transform from hover translateY(-2px) vs checked has no transform rule → hover transform still applies when liked ✓ fine).
**Active squish** `#like:checked + .stage:active .btn`?? just `.stage:active .btn { transform: scale(.94); }` specificity (0,3,0) same as hover, later in file → wins over hover when both ✓. But checked bg rules (1,3,0) don't set transform ✓ squish applies when liked too ✓.
**transition on .btn transform .2s** with cubic overshoot — squish release springs back ✓.
Now **write everything**. I'll be careful and thorough. Also add comments in CSS (nice for judges reading source).
Additional small flourish — **`h1 em` underline squiggle?** skip.
Let me also think about whether to include **`prefers-reduced-motion`**… final decision: skip (demo is about animation).
One more: **`.sub` copy** mentions pressing twice ✓ good guidance.
**Potential issue — `.stage` inline-flex shrink**: label inline-flex; width = content ✓.
**Also ensure `.hero` doesn't stretch stage** (align-items center → width fit ✓).
**meta text initial**: "12,047 likes" — when liked: strip rolls to 8 → "12,048 likes" ✓.
Hmm — wait, one more sanity check on `.roll` inline-flex child: `.roll { display: block?? }` inside .counter (inline-flex) — flex items blockified anyway; set `.roll { height: var(--row); overflow: hidden; }` as flex item with align-items: center — since .roll height == siblings' line-box height ✓ centers align ✓. But careful: .cs flex item height: content line-height 1.5 → 22.5px ✓ .cl same ✓ roll 22.5 ✓ → all equal → align center = perfect ✓.
Font — digits in `i` (font-style normal set!) ✓ otherwise italic — set `.strip i { font-style: normal; }`.
**Alright — also `.plus` `left: calc(50% + 60px)`** — .meta is p (block, full hero width? .meta in flex column align center → width shrink?? .meta is a flex item with align-self center → width fit-content ✓ p width = counter width (~90px) → 50% of 90 + 60 = 105 → plus sits 60px right of counter's right edge?? Hmm: left: calc(50% + 60px) where 50% = 45px (half of meta width) → 105px from meta left → counter ends at 90 → plus at 15px past right edge ✓ good.
Hmm wait — .meta width fit-content in flex column with align-items center: yes shrink-to-fit ✓.
**.hint** width — fine.
**Final check on overflow: confetti falls 240px below boom** → boom y in page ≈ hero_y+32; meta at +36 below button... confetti passes over meta/hint/footer area, fading ✓.
**Now — writing the code.** I'll write it cleanly with section comments. Let me also make sure NOT to use any JS. No console errors — pure CSS ✓.
Also — **`background-clip: text` needs -webkit- prefix in Chrome?** Chrome supports unprefixed `background-clip: text`? Chrome supports `-webkit-background-clip: text`; unprefixed `background-clip: text` is supported since Chrome 120 ✓ latest Chrome fine — but include both for safety ✓.
**`mask-image` unprefixed in Chrome 120+ ✓** (also add -webkit-).
**100svh** ✓.
**Let me draft the complete file:**
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>You’re gonna like this — a pure CSS like button</title>
<style>
/* ============ tokens & reset ============ */
:root{
--font: ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--serif: Georgia,"Times New Roman",serif;
--red:#ff2e63; --red-hi:#ff5c7a; --pink:#ff8fae;
--gold:#ffd166; --cyan:#4cc9f0; --violet:#b388ff;
--ink:#f2f5fc; --mut:#97a0b6; --mut2:#707a94;
--hx:45px; /* horizontal center of the heart inside the button */
}
*,*::before,*::after{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0; overflow:hidden;
font-family:var(--font); color:var(--ink);
background:#0a0c13;
-webkit-font-smoothing:antialiased;
}
::selection{ background:rgba(255,46,99,.4); }
/* ============ ambient background ============ */
.bg{ position:fixed; inset:0; z-index:0; pointer-events:none;
background:
radial-gradient(1000px 560px at 50% 116%, rgba(255,46,99,.11), transparent 62%),
radial-gradient(760px 420px at 50% -8%, rgba(96,130,255,.09), transparent 60%);
}
.bg::after{
content:""; position:absolute; inset:0;
background-image:radial-gradient(rgba(255,255,255,.055) 1px, transparent 1.4px);
background-size:26px 26px;
-webkit-mask-image:radial-gradient(85% 68% at 50% 42%, #000 25%, transparent 78%);
mask-image:radial-gradient(85% 68% at 50% 42%, #000 25%, transparent 78%);
}
/* full-page flash on like */
.flash{ position:fixed; inset:0; z-index:40; pointer-events:none; opacity:0;
background:radial-gradient(560px 400px at 50% 54%, rgba(255,46,99,.22), rgba(255,46,99,.05) 45%, transparent 70%);
}
body:has(#like:checked) .flash{ animation:flash .8s ease-out; }
@keyframes flash{ 0%{opacity:0} 16%{opacity:1} 100%{opacity:0} }
/* ============ layout ============ */
.scene{
position:relative; z-index:1;
min-height:100svh; display:flex; flex-direction:column; align-items:center;
padding:clamp(22px,4vh,48px) 20px 24px;
text-align:center;
}
body:has(#like:checked) .scene{ animation:shake .55s cubic-bezier(.3,.6,.4,1); }
@keyframes shake{
0%,100%{ transform:translate(0,0) rotate(0deg); }
15%{ transform:translate(-7px,4px) rotate(-.5deg); }
30%{ transform:translate(6px,-5px) rotate(.45deg); }
45%{ transform:translate(-5px,3px) rotate(-.35deg); }
60%{ transform:translate(4px,-3px) rotate(.3deg); }
78%{ transform:translate(-3px,2px) rotate(-.18deg); }
92%{ transform:translate(2px,-1px) rotate(0deg); }
}
header{ display:flex; flex-direction:column; align-items:center; }
.badge{
display:inline-flex; align-items:center; gap:9px; margin:0;
padding:8px 16px; border:1px solid rgba(255,255,255,.13); border-radius:999px;
background:rgba(255,255,255,.04);
font-size:11px; font-weight:700; letter-spacing:.16em; text-transform:uppercase;
color:#aeb6c9;
}
.bdot{ width:7px; height:7px; border-radius:50%; background:var(--red);
box-shadow:0 0 12px rgba(255,46,99,.9); animation:breathe 2.2s ease-in-out infinite; }
@keyframes breathe{ 0%,100%{opacity:1; transform:scale(1)} 50%{opacity:.5; transform:scale(.75)} }
h1{ margin:22px 0 0; font-size:clamp(30px,5.6vw,54px); font-weight:800;
letter-spacing:-.028em; line-height:1.06; }
h1 em{
display:inline-block; font-family:var(--serif); font-style:italic; font-weight:600;
padding:0 .05em;
background:linear-gradient(100deg,var(--red) 15%,var(--pink) 85%);
-webkit-background-clip:text; background-clip:text; color:transparent;
}
body:has(#like:checked) h1 em{ animation:emPop .85s cubic-bezier(.3,1.7,.4,1); }
@keyframes emPop{ 0%{transform:scale(1) rotate(0)} 45%{transform:scale(1.13) rotate(-2.5deg)} 100%{transform:scale(1) rotate(0)} }
.sub{ margin:16px auto 0; max-width:46ch; font-size:clamp(14px,1.9vw,16.5px);
line-height:1.65; color:var(--mut); }
.hero{ margin:auto 0; display:flex; flex-direction:column; align-items:center; padding:26px 0 8px; }
.foot{ margin:0; font-size:12px; letter-spacing:.03em; color:var(--mut2); }
.hint{ margin:18px 0 0; font-size:13px; color:var(--mut2); }
/* ============ the hidden checkbox ============ */
#like{ position:absolute; width:1px; height:1px; margin:-1px; opacity:0; pointer-events:none; }
#like:focus-visible + .stage .btn{ outline:2px solid var(--red-hi); outline-offset:5px; }
```
Then stage/button:
```css
/* ============ the button ============ */
.stage{ position:relative; display:inline-flex; cursor:pointer; user-select:none;
-webkit-user-select:none; -webkit-tap-highlight-color:transparent; touch-action:manipulation; }
.fx-back,.fx-front{ position:absolute; inset:0; pointer-events:none; }
.fx-back{ z-index:1; }
.fx-front{ z-index:3; }
.boom{ position:absolute; left:var(--hx); top:50%; width:0; height:0; }
.btn{
position:relative; z-index:2; overflow:hidden;
display:inline-flex; align-items:center; gap:13px;
height:64px; padding:0 32px 0 26px; border-radius:999px;
background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.16);
box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 14px 34px rgba(0,0,0,.45);
font-size:17px; font-weight:700; letter-spacing:.01em; color:var(--ink);
transition:transform .2s cubic-bezier(.34,1.56,.64,1),
background .35s ease, border-color .35s ease, box-shadow .35s ease;
}
.stage:hover .btn{
transform:translateY(-2px); border-color:rgba(255,255,255,.3);
box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 18px 40px rgba(0,0,0,.5), 0 6px 26px rgba(255,46,99,.14);
}
.stage:active .btn{ transform:scale(.94); }
/* ripple on press */
.btn::after{
content:""; position:absolute; inset:0; border-radius:inherit;
background:radial-gradient(closest-side circle at 50% 50%, rgba(255,255,255,.3), transparent 72%);
opacity:0; transform:scale(.55);
}
.stage:active .btn::after{ animation:ripple .5s ease-out; }
@keyframes ripple{ 0%{opacity:.85; transform:scale(.55)} 100%{opacity:0; transform:scale(1.12)} }
/* liked button skin */
#like:checked + .stage .btn{
background:linear-gradient(180deg, rgba(255,46,99,.18), rgba(255,46,99,.06));
border-color:rgba(255,84,120,.55);
box-shadow:inset 0 1px 0 rgba(255,255,255,.12),
0 10px 34px rgba(255,46,99,.26),
0 0 0 1px rgba(255,46,99,.18);
}
```
Heart:
```css
/* ============ CSS-drawn heart ============ */
.heart{
--s:20px;
position:absolute; left:50%; top:50%;
width:var(--s); height:var(--s);
transform:translate(-50%,-50%) rotate(45deg);
background:currentColor; color:#97a1b8;
transition:color .38s ease, filter .45s ease;
}
.heart::before,.heart::after{
content:""; position:absolute; width:var(--s); height:var(--s);
border-radius:50%; background:currentColor;
}
.heart::before{ left:0; top:calc(var(--s) / -2); }
.heart::after{ left:calc(var(--s) / -2); top:0; }
.heart-wrap{ position:relative; width:36px; height:36px; flex:none;
transition:transform .4s cubic-bezier(.3,1.5,.45,1); }
#like:checked + .stage .heart{ color:var(--red); filter:drop-shadow(0 2px 10px rgba(255,46,99,.55)); }
#like:checked + .stage .heart-wrap{
animation:pop .85s cubic-bezier(.25,1,.4,1) both, heartbeat 1.7s linear .85s infinite;
}
@keyframes pop{
0%{ transform:scale(1) rotate(0deg); }
28%{ transform:scale(1.45) rotate(-8deg); }
44%{ transform:scale(.82) rotate(5deg); }
58%{ transform:scale(1.28) rotate(-3deg); }
72%{ transform:scale(.96) rotate(1deg); }
86%{ transform:scale(1.15) rotate(0deg); }
100%{ transform:scale(1.08) rotate(0deg); }
}
@keyframes heartbeat{
0%,100%{ transform:scale(1.08); }
14%{ transform:scale(1.22); }
28%{ transform:scale(1.08); }
42%{ transform:scale(1.18); }
60%{ transform:scale(1.08); }
}
```
Text swap:
```css
/* ============ Like → Liked ============ */
.txt{ display:grid; overflow:hidden; }
.t{ grid-area:1/1; white-space:nowrap;
transition:transform .38s cubic-bezier(.55,0,.3,1), opacity .32s ease; }
.t-liked{ color:var(--red-hi); transform:translateY(125%); opacity:0; }
#like:checked + .stage .t-like{ transform:translateY(-125%); opacity:0;
transition:transform .26s cubic-bezier(.5,0,.7,.4), opacity .2s ease; }
#like:checked + .stage .t-liked{ transform:translateY(0); opacity:1;
transition:transform .52s cubic-bezier(.25,1.5,.4,1) .1s, opacity .3s ease .1s; }
```
Idle pulse + ghost:
```css
/* idle attention pulse (stops once liked) */
.pulse{ position:absolute; inset:-3px; border-radius:999px;
border:1.5px solid rgba(255,84,120,.38); opacity:0;
animation:pulse 2.8s ease-out infinite; }
#like:checked + .stage .pulse{ animation:none; opacity:0; }
@keyframes pulse{ 0%{transform:scale(1); opacity:.7} 70%,100%{transform:scale(1.3); opacity:0} }
/* big ghost heart launching upward */
.ghost{ position:absolute; left:var(--hx); top:50%; width:0; height:0; opacity:0; }
.ghost .heart{ --s:62px; color:var(--red);
filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8)); }
#like:checked + .stage .ghost{ animation:ghostUp 1.3s cubic-bezier(.22,.61,.31,1) .04s both; }
@keyframes ghostUp{
0%{ transform:translate(0,0) scale(.2) rotate(0deg); opacity:0; }
16%{ opacity:.55; }
38%{ transform:translate(4px,-42px) scale(1.1) rotate(-9deg); opacity:.55; }
100%{ transform:translate(30px,-320px) scale(2) rotate(12deg); opacity:0; }
}
```
Wait — `.ghost .heart { color: var(--red) }` — but base .heart transition color — ghost never changes color — fine. But ALSO — `#like:checked + .stage .heart` sets color red — that includes ghost heart ✓ same.
Hmm — specificity: `#like:checked + .stage .heart` (1,2,1)?? vs `.ghost .heart` (0,2,0) — checked rule wins — same color anyway ✓.
Rings:
```css
/* ============ shockwave rings ============ */
.ring{ position:absolute; left:-46px; top:-46px; width:92px; height:92px;
border-radius:50%; border:2.5px solid rgba(255,255,255,.85); opacity:0; }
.r2{ border-color:var(--red); --rd:.09s; }
.r3{ border-color:var(--gold); --rd:.18s; }
#like:checked + .stage .ring{ animation:ring .78s cubic-bezier(.18,.65,.35,1) var(--rd,0s) both; }
@keyframes ring{
0%{ transform:scale(.4); opacity:0; }
15%{ opacity:.95; }
100%{ transform:scale(2.65); opacity:0; }
}
```
Particles:
```css
/* ============ particles ============ */
.pt{ position:absolute; left:0; top:0; opacity:0; will-change:transform,opacity; }
.pt.dot{ width:var(--s,10px); height:var(--s,10px); border-radius:50%; background:var(--c,#fff); }
.pt.star{ width:var(--s,14px); height:var(--s,14px); background:var(--c,var(--gold));
clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%); }
.pt.mh{ width:var(--s,12px); height:var(--s,12px); background:var(--c,var(--red)); }
.pt.mh::before,.pt.mh::after{ content:""; position:absolute;
width:var(--s,12px); height:var(--s,12px); border-radius:50%; background:var(--c,var(--red)); }
.pt.mh::before{ left:0; top:calc(var(--s,12px) / -2); }
.pt.mh::after{ left:calc(var(--s,12px) / -2); top:0; }
#like:checked + .stage .pt{ animation:fly var(--t,.9s) cubic-bezier(.16,.68,.32,1) var(--d,0s) both; }
@keyframes fly{
0%{ transform:translate(-50%,-50%) translate(0,0) scale(.3) rotate(0deg); opacity:0; }
12%{ opacity:1; }
38%{ transform:translate(-50%,-50%) translate(calc(var(--dx) * .38), calc(var(--dy) * .38)) scale(1.22) rotate(calc(var(--rot,180deg) * .38)); opacity:1; }
76%{ opacity:1; }
100%{ transform:translate(-50%,-50%) translate(var(--dx), var(--dy)) scale(.55) rotate(var(--rot,180deg)); opacity:0; }
}
```
Hmm wait — the 38% keyframe: translate(calc(var(--dx)*.38)...) — but --dx has value "122px" → calc(122px * .38) ✓.
One issue: at 0% the transform translate(-50%,-50%) — for `.pt.mh` the shape before rotation is a diamond + lobes… whatever invisible→scale .3 fading in ✓.
Confetti:
```css
.conf{ position:absolute; left:0; top:0; width:var(--w,8px); height:var(--h,12px);
border-radius:2px; background:var(--c,var(--gold)); opacity:0; will-change:transform; }
#like:checked + .stage .conf{ animation:drop var(--t,1.3s) linear var(--d,.15s) both; }
@keyframes drop{
0%{ transform:translate(-50%,-50%) translate(0,0) rotate(0deg); opacity:0;
animation-timing-function:cubic-bezier(.15,.7,.35,1); }
10%{ opacity:1; }
32%{ transform:translate(-50%,-50%) translate(calc(var(--sw,30px) * .45), -95px) rotate(230deg); opacity:1;
animation-timing-function:cubic-bezier(.5,0,.8,.5); }
82%{ opacity:1; }
100%{ transform:translate(-50%,-50%) translate(var(--sw,30px), 240px) rotate(640deg); opacity:0; }
}
```
Hmm — issue: the per-keyframe `animation-timing-function` at 0% — but the animation shorthand sets `linear` as base; keyframe tf overrides for segments starting at that keyframe ✓. BUT the 10% and 82% frames don't set tf — segments 10→32 for opacity use... the tf for a property segment comes from the keyframe where the property's value is defined as the segment start. Opacity segments: 0→10 (tf from 0% frame), 10→32 (tf from 10% frame — none set → uses animation's tf = linear ✓ fine), 32→82 (tf from 32% frame = the ease-in ✓ but constant 1 anyway), 82→100 (from 82% → linear ✓). Transform segments: 0→32 (tf from 0% ✓ ease-out), 32→100 (tf from 32% ✓ gravity ease-in) ✓✓.
Counter & meta:
```css
/* ============ counter ============ */
.meta{ --row:1.5em; position:relative; margin:38px 0 0; font-size:15px; color:var(--mut); }
.counter{ display:inline-flex; align-items:center; font-weight:700; color:#c6cede;
font-variant-numeric:tabular-nums; transition:color .45s ease; }
.cs,.cl{ line-height:var(--row); }
.roll{ height:var(--row); overflow:hidden; }
.strip{ display:block; transition:transform .55s cubic-bezier(.3,.9,.3,1.12); }
.strip i{ display:block; height:var(--row); line-height:var(--row); font-style:normal; }
#like:checked ~ .hero??
```
WAIT — meta is sibling of input? Structure: .hero > input, label.stage, p.meta, p.hint. So `#like:checked ~ .meta` ✓ (same parent .hero).
```css
#like:checked ~ .meta .strip{ transform:translateY(calc(-1 * var(--row))); }
#like:checked ~ .meta .counter{ color:var(--red-hi); }
```
Hmm — `.meta` defines --row; `.strip` translateY calc uses var(--row) — inherited ✓.
Wait — `.strip i { line-height: var(--row) }` — var in line-height ✓ (1.5em relative to i font-size 15px → 22.5px) and height var(--row) = 1.5em of... em on height resolves against element's own font-size ✓ 22.5 ✓ consistent.
plus:
```css
.plus{ position:absolute; left:calc(50% + 58px); top:-14px;
font-size:15px; font-weight:800; color:var(--red-hi);
text-shadow:0 0 14px rgba(255,46,99,.65); opacity:0; pointer-events:none; }
#like:checked ~ .meta .plus{ animation:rise .85s cubic-bezier(.2,.7,.4,1) .16s both; }
@keyframes rise{
0%{ transform:translateY(6px); opacity:0; }
30%{ opacity:1; }
100%{ transform:translateY(-30px); opacity:0; }
}
```
Hmm plus starts at -14px top and rises -30 more → up toward button — nice, floats from counter upward ✓ (it's below button by 38px margin; rises toward it ✓).
HTML body:
```html
<div class="bg"></div>
<div class="flash"></div>
<main class="scene">
<header>
<p class="badge"><span class="bdot"></span>Pure CSS · Zero JavaScript</p>
<h1>You’re gonna <em>like</em> this.</h1>
<p class="sub">One hidden checkbox, a mountain of keyframes, and a frankly excessive celebration. Press the button — then press it again.</p>
</header>
<div class="hero">
<input type="checkbox" id="like" aria-label="Like this page">
<label class="stage" for="like">
<span class="fx-back">
<span class="pulse"></span>
<span class="ghost"><i class="heart"></i></span>
</span>
<span class="btn">
<span class="heart-wrap"><i class="heart"></i></span>
<span class="txt">
<span class="t t-like">Like</span>
<span class="t t-liked">Liked</span>
</span>
</span>
<span class="fx-front">
<span class="boom">
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
[16 pt elements]
[8 conf elements]
</span>
</span>
</label>
<p class="meta">
<span class="counter"><span class="cs">12,04</span><span class="roll"><span class="strip"><i>7</i><i>8</i></span></span><span class="cl"> likes</span></span>
<span class="plus">+1</span>
</p>
<p class="hint">Click to like · click again to undo · repeat until satisfied</p>
</div>
<footer class="foot">1 checkbox · 13 @keyframes · 0 lines of JavaScript</footer>
</main>
```
Wait — footer element with class foot; CSS `.foot` ✓ (footer tag default styles fine, margin reset? footer{margin:0} via .foot margin:0 ✓).
Hmm — ghost heart: `.ghost .heart { --s: 62px; }` — but I wrote in HTML `<span class="ghost"><i class="heart"></i></span>` and CSS `.ghost .heart{ --s:62px; color:var(--red); filter:... }` ✓.
Now — ISSUE CHECK: `.heart` base uses `position:absolute; left:50%; top:50%` — inside `.ghost` (0×0) ✓; inside `.heart-wrap` (36×36) ✓ centered.
But — the `.btn` heart: heart-wrap 36 in 64 button ✓.
**Check `.ghost` inside fx-back inset 0** — ghost left: var(--hx)=45px; top:50% of fx-back (= stage height 64) = 32 ✓.
**Check `.boom` inside fx-front**: left 45px, top 50% ✓.
**Check heart visual in ghost**: --s 62 → visual ~87px — from button center-ish; behind btn (z: fx-back 1 < btn 2) ✓.
Now — **CRITICAL correctness check: `#like:checked + .stage ...`** — the input's next sibling IS the label.stage ✓ (input, then label) ✓.
`#like:checked ~ .meta` — meta after label ✓ works (~ general sibling).
`body:has(#like:checked)` ✓.
**Now — the `fly` for `.pt.mh`**: transform in keyframes includes translate(-50%,-50%) ✓ centers on boom origin ✓ rotate tumbling ✓ BUT the mini heart shape needs its base rotate(45deg)?? The fly keyframes REPLACE transform entirely (keyframes override the base `.heart`-like transform)... wait — `.pt.mh` is NOT `.heart` class — it's a standalone `<i class="pt mh">` with its own pseudo hearts — its base transform is none (not the .heart class) ✓ the fly keyframes' rotate(var(--rot)) handles all rotation ✓. At 0% rotate(0) → mini heart appears as diamond+lobes → looks like... at scale .3 & opacity 0→1 quickly — fine, tumbling anyway.
Hmm — wait, actually: does `.pt.mh::before/::after` produce heart correctly? The element is `--s` square with background; ::before circle at top edge; ::after at left edge; rotate comes from fly's rotate ✓ at intermediate rotations it's a proper heart ✓ tumbling heart particles!
**Now — think about the `.txt` grid + overflow hidden + t spans**: grid-area 1/1 both — `.txt { display:grid; }` children `.t` — auto placement: both explicitly same cell ✓ stacked ✓. `.txt` width = max("Like","Liked") ✓. Overflow hidden clips vertical slides ✓. Height: row height = max item height — `.t` line-height inherit from .btn (font 17px, default line-height normal ~1.2 → ~20px)... overflow hidden clips translateY(125%) → 25px below → hidden ✓.
Hmm — but the `.t-liked` at translateY(125%) is clipped by .txt overflow hidden ✓ invisible even without opacity ✓ good.
**Check button content alignment**: .btn inline-flex align center — heart-wrap 36 + txt ~20 → centered vertically ✓.
**One more possible issue**: `.stage` is `display:inline-flex` — its child `.btn` is inline-flex too → flex item ✓ stage size = btn ✓. fx spans absolute ✓.
**Focus outline on .btn when input focused** ✓.
**A11y — `cursor:pointer`** ✓.
**Now — hmm, about `overflow:hidden` on `.btn` and the heart drop-shadow glow**: glow extends ~10px around heart — heart is ~28px wide at x 27-64 within button (155 wide) ✓ inside ✓ no clip issues. But heart-wrap pop scale 1.45 → heart 40px — still inside 64-tall button ✓ (40<64) ✓ rotate fine ✓.
**Ghost blur** — blur on element with transform scale — fine.
**Now — the .bg mask + ::after on .bg — .bg has z-index 0; .scene z-index 1** ✓.
**.flash z 40 above scene ✓ pointer-events none ✓.**
**Footer visible?** .foot color #707a94 (mut2) — I set --mut2:#707a94 ✓.
Hmm — also add `footer` spacing: scene padding bottom 24 ✓.
**One more consideration — clicking label when focus... fine.**
**Now, let me double-check the `drop` confetti translate structure**: keyframes transform lists across keyframes must match for smooth interpolation: 0%: `translate(-50%,-50%) translate(0,0) rotate(0deg)` — 32%: `translate(-50%,-50%) translate(Xpx, -95px) rotate(230deg)` — 100%: `translate(-50%,-50%) translate(var(--sw), 240px) rotate(640deg)` — SAME structure (3 functions: translate, translate, rotate) ✓ interpolates per-function ✓.
`fly` structure: translate(-50%,-50%) translate() scale() rotate() in 0/38/100 ✓ consistent ✓ (0% has translate(0,0) ✓).
`ghostUp`: translate() scale() rotate() ✓ all frames ✓.
`pop`: scale() rotate() ✓.
OK — also **`.strip` transition cubic-bezier(.3,.9,.3,1.12)** — y2 1.12 overshoot ✓ valid (y outside [0,1] allowed ✓).
**Testing the roll overflow**: `.roll { overflow: hidden; height: 22.5px }` — during transition with overshoot, strip moves slightly past -22.5 → shows sliver of empty below 8 — ok.
Hmm wait — overshoot direction: translateY goes from 0 to -22.5; overshoot → -23.5 briefly → window shows bottom .99 of "8" plus 1px empty at bottom?? window height 22.5, strip at -23.5 → "8" row occupies window y [-1, 21.5] → empty [21.5,22.5] — 1px gap at bottom for a split second — invisible at speed ✓.
**Colors of `--c:#ffffff` for confetti cf5** — inline style `--c:#fff` ✓.
**Now, one more thought about the "crazy" factor on MOBILE** — everything scales ok since sizes are px-fixed... fine.
**Potential Chrome quirk**: `calc(var(--dx) * .38)` where --dx = "122px" — calc multiplication: length × number ✓ valid.
**`--rot:200deg` then `calc(var(--rot) * .38)`** — angle × number ✓ valid.
But CAREFUL — I set defaults in var() fallbacks: `var(--rot,180deg)` ✓ all inline styles provide values anyway ✓.
**Now, the `pop` animation on heart-wrap — during pop, heart-wrap scale also scales... heart-wrap has no overflow (heart pokes within) ✓.
**Hmm — one more: `heartbeat` uses `linear` timing — with keyframe percentages forming the beat ✓.
**The idle `pulse` — `.pulse{ inset:-3px }` — inset relative to fx-back (= stage box) ✓ border-radius 999 ✓.
**When liked, pulse animation:none ✓.**
**What about `#like:checked + .stage .btn` transition of background** ✓ .35s.
**Also — during press (`:active`) BEFORE release, checkbox already toggles?? NO — checkbox toggles on click (mouseup) by default... Actually clicking a label: activation behavior fires on mouseup? For labels, clicking triggers input click on mousedown+up complete → checkbox state flips at mouseup. So squish during press, explosion on release ✓
Hmm wait — actually for `<label>`, activation occurs on click event (after mouseup) ✓. Good UX.
BUT — hmm, one caveat: on touch devices, tapping fires quickly ✓ fine.
**Now — the h1 em gradient + inline-block + emPop animation** — on check ✓.
**Let me reconsider the vertical rhythm** in 800px viewport:
- scene padding-top ~44
- header: badge 31 + h1 (54*1.06 ≈ 58 + margin 22) + sub (2 lines? max-width 46ch at 16.5px — text "One hidden checkbox, a mountain of keyframes, and a frankly excessive celebration. Press the button — then press it again." ≈ 120 chars → at 46ch/line → 3 lines ≈ 80px + margin 16) → header ≈ 31+22+58+16+80 ≈ 207
- hero: padding 26 top + button 64 + meta 38+22 + hint 18+18 + padding 8 ≈ 194
- footer 16 + padding 24
- total ≈ 207+194+40 = 441 → free ≈ 360 → hero margin auto: 180 above/below hero → button center ≈ 44+207+180+26+32 ≈ 489?? That's ~61% down. Hmm — flash gradient at 54% — slightly off; adjust flash center to 58%? Let me compute more carefully... it's within radial tolerance (400px radius gradient). I'll set flash center `50% 58%`. Eh — actually, let me reduce uncertainty: make the sub shorter (1-2 lines) — "One hidden checkbox. A mountain of keyframes. Zero JavaScript." Let me craft shorter sub: "One hidden checkbox, a pile of keyframes, and a truly excessive celebration." ≈ 76 chars → 2 lines at 46ch. header ≈ 31+22+58+16+54 ≈ 181. hero top ≈ 44+181+free/2... free = 800-44-181-194-40-24?? I'm overcomplicating — the flash is a soft radial wash; exact center doesn't matter. Keep 50% 55%.
Actually — better: make the flash follow the actual button? Can't without JS. It's fine — big soft radial covers the area.
**Mobile 375×667**: header ≈ badge 31 + h1 30*1.06*2 lines?? "You're gonna like this." at 30px in 335px width — "You're gonna" + "like this." — fits ~2 lines ≈ 64 + 22 margin; sub 14px 3 lines 70; header ≈ 31+22+64+16+70 = 203; hero 194; footer 40; total 437 + padding 44 ≈ 481 < 667 ✓ fits ✓. h1 might wrap — fine, add `text-wrap: balance` for pretty wrapping ✓ (Chrome supports).
Hmm — h1 could be one line at ~375? 30px font: "You're gonna like this." ≈ 23 chars × ~15px ≈ 345 > 335 → wraps to 2 lines ✓ balance makes it nice.
**Now — WRITE THE FINAL FILE.** Also double-check no `alert` etc — pure CSS, no JS at all ✓. No external resources ✓.
Let me also add a couple of nice touches:
1. `p` margins reset where needed (badge margin 0 ✓ set; sub ✓ set; hint ✓; meta ✓; foot ✓).
2. `h1` text-wrap balance ✓.
3. `.stage { border-radius: 999px }` not needed.
Also — should the `.hint` mention spacebar? "Click to like · click again to undo" — keep. Maybe add: "(tab + space works too)" — small a11y flex: `Click to like · click again to undo · Tab + Space works too`. Slightly long for mobile — 13px, fine one line? 58 chars × 6.5 ≈ 380 > 335 → wraps 2 lines — acceptable or shorten: "Click to like · click again to undo". Then footer covers the tech flex. Keep short.
**Footer**: "1 checkbox · 13 @keyframes · 0 lines of JavaScript" ✓.
Let me also double check the heart-wrap transition on uncheck (animation removal): specified transform none; before-change computed scale ~1.08-1.22 (heartbeat) → transition to scale(1) — per spec this SHOULD transition (before-change style includes animations)... I'm fairly confident Chrome handles animation-removal → transition correctly when the underlying specified value changed... hmm, actually there's a known Chrome behavior: transitions don't start from animation values in some cases (the "after-change style" for transition triggering uses the computed value which includes the running animation... when the class/state changes, both the animation removal and property change happen in the SAME style recalculation → transition sees before = with-animation value, after = without → should run). I recall this works. If not — snap under the squish ✓ either way fine.
Also — `.heart-wrap` transition includes overshoot bezier (1.5) — if it triggers, bouncy return ✓ cute.
**One more check — `#like:checked + .stage .heart` color + filter transition ✓.**
**`.t` base transitions** — t-like exit transition defined in checked rule ✓ discussed.
Also — one **BUG RISK**: `.txt { display:grid }` inside `.btn` (inline-flex) — grid item sizing ✓ width max-content ✓.
**Another BUG RISK**: `.stage` inline-flex — but the label contains fx-back (absolute, no size) → stage width = .btn width ✓ height = .btn height ✓.
**Also** — `.hero` has padding 26px 0 8px — hmm plus effects margin — fine.
**Let me also double check `.boom` left:var(--hx)** — --hx defined on :root = 45px ✓.
Wait — actually let me re-verify --hx with final button metrics: padding: 0 32px 0 26px; border 1px; gap 13px; heart-wrap 36px. From stage left edge (== btn border-box left): border 1 + padding-left 26 = 27 → heart-wrap center at 27 + 18 = 45 ✓✓.
**Now — a thought about `.fx-back`/`.fx-front` z-index with `.btn { z-index: 2 }`** — btn position relative ✓.
**Everything seems solid. Let me also add a `noscript`?** Not needed — there IS no script 😄. Could add `<!-- no <script> was written in the making of this page -->` comment — cute.
**Final copy polish:**
- title: "You're gonna like this — pure CSS like button"
- badge: "Pure CSS · Zero JavaScript"
- h1: "You're gonna <em>like</em> this."
- sub: "One hidden checkbox, a mountain of keyframes, and a frankly excessive celebration. Press the button — then press it again."
- hint: "Click to like · click again to undo · repeat until satisfied"
- foot: "1 checkbox · 13 @keyframes · 0 lines of JavaScript"
Counting keyframes again in final CSS (must match "13"):
1. flash
2. shake
3. breathe
4. emPop
5. ripple
6. pop
7. heartbeat
8. pulse
9. ghostUp
10. ring
11. fly
12. drop
13. rise
= 13 ✓
Now — one more **potential problem**: `body:has(#like:checked) .scene { animation: shake ... }` — .scene also has NO other transform ✓. BUT — will shake interfere with hero margin centering? No.
**AND** — a significant check: **`.scene` has `animation: shake` applied while checked — creating a stacking context during animation** — .flash z 40 vs scene z1 — flash is sibling of scene in body — scene z1 < flash 40 ✓ flash above ✓.
Also — **:has() + animation on EVERY check**: after first like, animation stays "applied" (state) but completed. Re-like after un-like → restarts ✓.
Hmm — one more: when checked and user clicks to uncheck mid-shake — shake removed → transform snaps mid-frame — visually a tiny jump — masked by squish ✓ ok.
**Also potential issue**: `.stage:active .btn { transform: scale(.94); }` — during press BEFORE release — with `.btn` transition transform .2s with overshoot bezier — press squish eases in .2s, release springs ✓.
**iOS Safari `svh`** — Chrome target ✓ but I'll use `min-height: 100svh` with fallback `100vh` line before it:
```css
min-height:100vh; min-height:100svh;
```
✓ safe.
**Let me reconsider `will-change` on .pt/.conf** — 24 elements — ok.
**One more idea for extra wow**: make the button emit a tiny **sparkle trail** on hover? skip — enough.
**Double-check no horizontal scrollbar at 1280×800 and 375**: body overflow hidden ✓.
Also — since body overflow hidden and content taller than viewport on tiny heights → clipped but centered flex — fine.
**Write final answer now.** I'll write the complete HTML with careful ordering. Let me be extra careful with:
1. All selectors matching DOM structure.
2. No typos in var names.
3. All keyframes defined.
4. Balanced braces.
Full file draft:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>You’re gonna like this — a pure-CSS like button</title>
<style>
/* ================================================================
THE LIKE BUTTON — pure CSS, zero JavaScript.
State lives in a hidden <input type="checkbox">;
every effect below is keyed off #like:checked.
================================================================ */
:root{
--font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--serif: Georgia, "Times New Roman", serif;
--red: #ff2e63;
--red-hi:#ff5c7a;
--pink: #ff8fae;
--gold: #ffd166;
--cyan: #4cc9f0;
--violet:#b388ff;
--ink: #f2f5fc;
--mut: #97a0b6;
--mut2: #707a94;
--hx: 45px; /* horizontal centre of the heart, inside the button */
}
*,*::before,*::after{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0;
overflow:hidden; /* the burst may leave the stage — never the page */
font-family:var(--font);
color:var(--ink);
background:#0a0c13;
-webkit-font-smoothing:antialiased;
}
::selection{ background:rgba(255,46,99,.4); }
/* ---------- ambient background ---------- */
.bg{
position:fixed; inset:0; z-index:0; pointer-events:none;
background:
radial-gradient(1000px 560px at 50% 116%, rgba(255,46,99,.11), transparent 62%),
radial-gradient(760px 420px at 50% -8%, rgba(96,130,255,.09), transparent 60%);
}
.bg::after{
content:""; position:absolute; inset:0;
background-image:radial-gradient(rgba(255,255,255,.055) 1px, transparent 1.4px);
background-size:26px 26px;
-webkit-mask-image:radial-gradient(85% 68% at 50% 42%, #000 25%, transparent 78%);
mask-image:radial-gradient(85% 68% at 50% 42%, #000 25%, transparent 78%);
}
/* full-page blush when the like lands */
.flash{
position:fixed; inset:0; z-index:40; pointer-events:none; opacity:0;
background:radial-gradient(600px 420px at 50% 55%, rgba(255,46,99,.22), rgba(255,46,99,.05) 45%, transparent 72%);
}
body:has(#like:checked) .flash{ animation:flash .8s ease-out; }
@keyframes flash{ 0%{opacity:0} 16%{opacity:1} 100%{opacity:0} }
/* ---------- page frame ---------- */
.scene{
position:relative; z-index:1;
min-height:100vh; min-height:100svh;
display:flex; flex-direction:column; align-items:center;
padding:clamp(22px,4vh,48px) 20px 24px;
text-align:center;
}
body:has(#like:checked) .scene{ animation:shake .55s cubic-bezier(.3,.6,.4,1); }
@keyframes shake{
0%,100%{ transform:translate(0,0) rotate(0deg); }
15%{ transform:translate(-7px,4px) rotate(-.5deg); }
30%{ transform:translate(6px,-5px) rotate(.45deg); }
45%{ transform:translate(-5px,3px) rotate(-.35deg); }
60%{ transform:translate(4px,-3px) rotate(.3deg); }
78%{ transform:translate(-3px,2px) rotate(-.18deg); }
92%{ transform:translate(2px,-1px) rotate(0deg); }
}
header{ display:flex; flex-direction:column; align-items:center; }
.badge{
display:inline-flex; align-items:center; gap:9px; margin:0;
padding:8px 16px; border:1px solid rgba(255,255,255,.13); border-radius:999px;
background:rgba(255,255,255,.04);
font-size:11px; font-weight:700; letter-spacing:.16em; text-transform:uppercase;
color:#aeb6c9;
}
.bdot{
width:7px; height:7px; border-radius:50%; background:var(--red);
box-shadow:0 0 12px rgba(255,46,99,.9);
animation:breathe 2.2s ease-in-out infinite;
}
@keyframes breathe{ 0%,100%{opacity:1; transform:scale(1)} 50%{opacity:.5; transform:scale(.75)} }
h1{
margin:22px 0 0; text-wrap:balance;
font-size:clamp(30px,5.6vw,54px); font-weight:800;
letter-spacing:-.028em; line-height:1.06;
}
h1 em{
display:inline-block; font-family:var(--serif); font-style:italic; font-weight:600;
padding:0 .05em;
background:linear-gradient(100deg,var(--red) 15%,var(--pink) 85%);
-webkit-background-clip:text; background-clip:text; color:transparent;
}
body:has(#like:checked) h1 em{ animation:emPop .85s cubic-bezier(.3,1.7,.4,1); }
@keyframes emPop{ 0%{transform:scale(1) rotate(0)} 45%{transform:scale(1.13) rotate(-2.5deg)} 100%{transform:scale(1) rotate(0)} }
.sub{
margin:16px auto 0; max-width:46ch;
font-size:clamp(14px,1.9vw,16.5px); line-height:1.65; color:var(--mut);
}
.hero{ margin:auto 0; display:flex; flex-direction:column; align-items:center; padding:26px 0 8px; }
.hint{ margin:18px 0 0; font-size:13px; color:var(--mut2); }
.foot{ margin:0; font-size:12px; letter-spacing:.03em; color:var(--mut2); }
/* ---------- the one and only input ---------- */
#like{ position:absolute; width:1px; height:1px; margin:-1px; opacity:0; pointer-events:none; }
#like:focus-visible + .stage .btn{ outline:2px solid var(--red-hi); outline-offset:5px; }
/* ---------- the button ---------- */
.stage{
position:relative; display:inline-flex; cursor:pointer;
user-select:none; -webkit-user-select:none;
-webkit-tap-highlight-color:transparent; touch-action:manipulation;
}
.fx-back,.fx-front{ position:absolute; inset:0; pointer-events:none; }
.fx-back{ z-index:1; }
.fx-front{ z-index:3; }
.boom{ position:absolute; left:var(--hx); top:50%; width:0; height:0; } /* the heart's exact centre */
.btn{
position:relative; z-index:2; overflow:hidden;
display:inline-flex; align-items:center; gap:13px;
height:64px; padding:0 32px 0 26px; border-radius:999px;
background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.02));
border:1px solid rgba(255,255,255,.16);
box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 14px 34px rgba(0,0,0,.45);
font-size:17px; font-weight:700; letter-spacing:.01em; color:var(--ink);
transition:transform .2s cubic-bezier(.34,1.56,.64,1),
background .35s ease, border-color .35s ease, box-shadow .35s ease;
}
.stage:hover .btn{
transform:translateY(-2px);
border-color:rgba(255,255,255,.3);
box-shadow:inset 0 1px 0 rgba(255,255,255,.09), 0 18px 40px rgba(0,0,0,.5), 0 6px 26px rgba(255,46,99,.14);
}
.stage:active .btn{ transform:scale(.94); } /* squish while pressed */
/* soft ripple on every press */
.btn::after{
content:""; position:absolute; inset:0; border-radius:inherit;
background:radial-gradient(closest-side circle at 50% 50%, rgba(255,255,255,.3), transparent 72%);
opacity:0; transform:scale(.55);
}
.stage:active .btn::after{ animation:ripple .5s ease-out; }
@keyframes ripple{ 0%{opacity:.85; transform:scale(.55)} 100%{opacity:0; transform:scale(1.12)} }
/* liked skin */
#like:checked + .stage .btn{
background:linear-gradient(180deg, rgba(255,46,99,.18), rgba(255,46,99,.06));
border-color:rgba(255,84,120,.55);
box-shadow:inset 0 1px 0 rgba(255,255,255,.12),
0 10px 34px rgba(255,46,99,.26),
0 0 0 1px rgba(255,46,99,.18);
}
/* ---------- the heart, drawn in CSS ---------- */
.heart{
--s:20px;
position:absolute; left:50%; top:50%;
width:var(--s); height:var(--s);
transform:translate(-50%,-50%) rotate(45deg);
background:currentColor; color:#97a1b8;
transition:color .38s ease, filter .45s ease;
}
.heart::before,.heart::after{
content:""; position:absolute; width:var(--s); height:var(--s);
border-radius:50%; background:currentColor;
}
.heart::before{ left:0; top:calc(var(--s) / -2); }
.heart::after { left:calc(var(--s) / -2); top:0; }
.heart-wrap{
position:relative; width:36px; height:36px; flex:none;
transition:transform .4s cubic-bezier(.3,1.5,.45,1);
}
#like:checked + .stage .heart{ color:var(--red); filter:drop-shadow(0 2px 10px rgba(255,46,99,.55)); }
#like:checked + .stage .heart-wrap{
animation:pop .85s cubic-bezier(.25,1,.4,1) both, heartbeat 1.7s linear .85s infinite;
}
@keyframes pop{
0% { transform:scale(1) rotate(0deg); }
28% { transform:scale(1.45) rotate(-8deg); }
44% { transform:scale(.82) rotate(5deg); }
58% { transform:scale(1.28) rotate(-3deg); }
72% { transform:scale(.96) rotate(1deg); }
86% { transform:scale(1.15) rotate(0deg); }
100%{ transform:scale(1.08) rotate(0deg); }
}
@keyframes heartbeat{
0%,100%{ transform:scale(1.08); }
14% { transform:scale(1.22); }
28% { transform:scale(1.08); }
42% { transform:scale(1.18); }
60% { transform:scale(1.08); }
}
/* ---------- Like → Liked ---------- */
.txt{ display:grid; overflow:hidden; }
.t{
grid-area:1/1; white-space:nowrap;
transition:transform .38s cubic-bezier(.55,0,.3,1), opacity .32s ease;
}
.t-liked{ color:var(--red-hi); transform:translateY(125%); opacity:0; }
#like:checked + .stage .t-like{
transform:translateY(-125%); opacity:0;
transition:transform .26s cubic-bezier(.5,0,.7,.4), opacity .2s ease;
}
#like:checked + .stage .t-liked{
transform:translateY(0); opacity:1;
transition:transform .52s cubic-bezier(.25,1.5,.4,1) .1s, opacity .3s ease .1s;
}
/* ---------- idle pulse & ghost heart (behind the button) ---------- */
.pulse{
position:absolute; inset:-3px; border-radius:999px;
border:1.5px solid rgba(255,84,120,.38); opacity:0;
animation:pulse 2.8s ease-out infinite;
}
#like:checked + .stage .pulse{ animation:none; opacity:0; }
@keyframes pulse{ 0%{transform:scale(1); opacity:.7} 70%,100%{transform:scale(1.3); opacity:0} }
.ghost{ position:absolute; left:var(--hx); top:50%; width:0; height:0; opacity:0; }
.ghost .heart{
--s:62px; color:var(--red);
filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8));
}
#like:checked + .stage .ghost{ animation:ghostUp 1.3s cubic-bezier(.22,.61,.31,1) .04s both; }
@keyframes ghostUp{
0% { transform:translate(0,0) scale(.2) rotate(0deg); opacity:0; }
16% { opacity:.55; }
38% { transform:translate(4px,-42px) scale(1.1) rotate(-9deg); opacity:.55; }
100%{ transform:translate(30px,-320px) scale(2) rotate(12deg); opacity:0; }
}
/* ---------- shockwave rings ---------- */
.ring{
position:absolute; left:-46px; top:-46px; width:92px; height:92px;
border-radius:50%; border:2.5px solid rgba(255,255,255,.85); opacity:0;
}
.r2{ border-color:var(--red); --rd:.09s; }
.r3{ border-color:var(--gold); --rd:.18s; }
#like:checked + .stage .ring{ animation:ring .78s cubic-bezier(.18,.65,.35,1) var(--rd,0s) both; }
@keyframes ring{
0% { transform:scale(.4); opacity:0; }
15% { opacity:.95; }
100%{ transform:scale(2.65); opacity:0; }
}
/* ---------- flying particles ---------- */
.pt{ position:absolute; left:0; top:0; opacity:0; will-change:transform,opacity; }
.pt.dot { width:var(--s,10px); height:var(--s,10px); border-radius:50%; background:var(--c,#fff); }
.pt.star{
width:var(--s,14px); height:var(--s,14px); background:var(--c,var(--gold));
clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%);
}
.pt.mh{ width:var(--s,12px); height:var(--s,12px); background:var(--c,var(--red)); }
.pt.mh::before,.pt.mh::after{
content:""; position:absolute; width:var(--s,12px); height:var(--s,12px);
border-radius:50%; background:var(--c,var(--red));
}
.pt.mh::before{ left:0; top:calc(var(--s,12px) / -2); }
.pt.mh::after { left:calc(var(--s,12px) / -2); top:0; }
#like:checked + .stage .pt{ animation:fly var(--t,.9s) cubic-bezier(.16,.68,.32,1) var(--d,0s) both; }
@keyframes fly{
0%{
transform:translate(-50%,-50%) translate(0,0) scale(.3) rotate(0deg);
opacity:0;
}
12%{ opacity:1; }
38%{
transform:translate(-50%,-50%) translate(calc(var(--dx,100px) * .38), calc(var(--dy,-100px) * .38)) scale(1.22) rotate(calc(var(--rot,180deg) * .38));
opacity:1;
}
76%{ opacity:1; }
100%{
transform:translate(-50%,-50%) translate(var(--dx,100px), var(--dy,-100px)) scale(.55) rotate(var(--rot,180deg));
opacity:0;
}
}
/* ---------- confetti with a gravity arc ---------- */
.conf{
position:absolute; left:0; top:0;
width:var(--w,8px); height:var(--h,12px); border-radius:2px;
background:var(--c,var(--gold)); opacity:0; will-change:transform;
}
#like:checked + .stage .conf{ animation:drop var(--t,1.3s) linear var(--d,.15s) both; }
@keyframes drop{
0%{
transform:translate(-50%,-50%) translate(0,0) rotate(0deg);
opacity:0;
animation-timing-function:cubic-bezier(.15,.7,.35,1); /* the toss */
}
10%{ opacity:1; }
32%{
transform:translate(-50%,-50%) translate(calc(var(--sw,30px) * .45), -95px) rotate(230deg);
opacity:1;
animation-timing-function:cubic-bezier(.5,0,.8,.5); /* gravity */
}
82%{ opacity:1; }
100%{
transform:translate(-50%,-50%) translate(var(--sw,30px), 240px) rotate(640deg);
opacity:0;
}
}
/* ---------- counter with a rolling digit ---------- */
.meta{ --row:1.5em; position:relative; margin:38px 0 0; font-size:15px; color:var(--mut); }
.counter{
display:inline-flex; align-items:center;
font-weight:700; color:#c6cede; font-variant-numeric:tabular-nums;
transition:color .45s ease;
}
.cs,.cl{ line-height:var(--row); }
.roll{ height:var(--row); overflow:hidden; }
.strip{ display:block; transition:transform .55s cubic-bezier(.3,.9,.3,1.12); }
.strip i{ display:block; height:var(--row); line-height:var(--row); font-style:normal; }
#like:checked ~ .meta .strip{ transform:translateY(calc(-1 * var(--row))); }
#like:checked ~ .meta .counter{ color:var(--red-hi); }
.plus{
position:absolute; left:calc(50% + 58px); top:-14px;
font-size:15px; font-weight:800; color:var(--red-hi);
text-shadow:0 0 14px rgba(255,46,99,.65);
opacity:0; pointer-events:none;
}
#like:checked ~ .meta .plus{ animation:rise .85s cubic-bezier(.2,.7,.4,1) .16s both; }
@keyframes rise{
0% { transform:translateY(6px); opacity:0; }
30% { opacity:1; }
100%{ transform:translateY(-30px); opacity:0; }
}
</style>
</head>
<body>
<div class="bg"></div>
<div class="flash"></div>
<main class="scene">
<header>
<p class="badge"><span class="bdot"></span>Pure CSS · Zero JavaScript</p>
<h1>You’re gonna <em>like</em> this.</h1>
<p class="sub">One hidden checkbox, a mountain of keyframes, and a frankly excessive celebration. Press the button — then press it again.</p>
</header>
<div class="hero">
<!-- the entire app is this one checkbox -->
<input type="checkbox" id="like" aria-label="Like this page">
<label class="stage" for="like">
<!-- layer 0 · behind the button -->
<span class="fx-back">
<span class="pulse"></span>
<span class="ghost"><i class="heart"></i></span>
</span>
<!-- layer 1 · the button -->
<span class="btn">
<span class="heart-wrap"><i class="heart"></i></span>
<span class="txt">
<span class="t t-like">Like</span>
<span class="t t-liked">Liked</span>
</span>
</span>
<!-- layer 2 · everything that explodes -->
<span class="fx-front">
<span class="boom">
<i class="ring r1"></i><i class="ring r2"></i><i class="ring r3"></i>
<!-- dots -->
<i class="pt dot" style="--dx:122px; --dy:-8px; --c:var(--gold); --s:10px; --t:.8s; --d:0s; --rot:200deg"></i>
...
```
(fill in all particles as planned)
Let me now write out all 16 pt + 8 conf elements concretely in final form.
Dots (10):
```html
<i class="pt dot" style="--dx:122px; --dy:-8px; --c:var(--gold); --s:10px; --t:.8s; --d:0s; --rot:200deg"></i>
<i class="pt dot" style="--dx:98px; --dy:-64px; --c:var(--cyan); --s:12px; --t:.9s; --d:.04s; --rot:-160deg"></i>
<i class="pt dot" style="--dx:46px; --dy:-106px; --c:var(--pink); --s:9px; --t:.85s; --d:0s; --rot:120deg"></i>
<i class="pt dot" style="--dx:-34px; --dy:-110px; --c:var(--violet); --s:11px; --t:.95s; --d:.05s; --rot:-220deg"></i>
<i class="pt dot" style="--dx:-96px; --dy:-64px; --c:var(--red); --s:10px; --t:.8s; --d:.02s; --rot:180deg"></i>
<i class="pt dot" style="--dx:-124px; --dy:-6px; --c:var(--gold); --s:13px; --t:.9s; --d:0s; --rot:-140deg"></i>
<i class="pt dot" style="--dx:-92px; --dy:58px; --c:var(--cyan); --s:9px; --t:.9s; --d:.06s; --rot:240deg"></i>
<i class="pt dot" style="--dx:96px; --dy:60px; --c:var(--pink); --s:11px; --t:.95s; --d:.03s; --rot:-200deg"></i>
<i class="pt dot" style="--dx:12px; --dy:106px; --c:var(--gold); --s:10px; --t:.85s; --d:.02s; --rot:160deg"></i>
<i class="pt dot" style="--dx:-14px; --dy:102px; --c:var(--violet); --s:9px; --t:.9s; --d:.05s; --rot:-120deg"></i>
```
Stars (4):
```html
<i class="pt star" style="--dx:150px; --dy:-58px; --c:var(--gold); --s:16px; --t:1s; --d:.02s; --rot:320deg"></i>
<i class="pt star" style="--dx:-148px; --dy:-62px; --c:var(--violet); --s:14px; --t:1.05s; --d:.06s; --rot:-280deg"></i>
<i class="pt star" style="--dx:80px; --dy:-140px; --c:var(--cyan); --s:13px; --t:.95s; --d:0s; --rot:240deg"></i>
<i class="pt star" style="--dx:-76px; --dy:-146px; --c:var(--pink); --s:15px; --t:1s; --d:.05s; --rot:-210deg"></i>
```
Mini hearts (2):
```html
<i class="pt mh" style="--dx:44px; --dy:-148px; --c:var(--red); --s:14px; --t:1s; --d:.06s; --rot:260deg"></i>
<i class="pt mh" style="--dx:-48px; --dy:-152px; --c:var(--pink); --s:12px; --t:1.05s; --d:.1s; --rot:-230deg"></i>
```
Confetti (8):
```html
<i class="conf" style="--c:var(--gold); --sw:34px; --t:1.25s; --d:.12s; --w:8px; --h:13px"></i>
<i class="conf" style="--c:var(--cyan); --sw:-30px; --t:1.4s; --d:.2s; --w:7px; --h:11px"></i>
<i class="conf" style="--c:var(--pink); --sw:46px; --t:1.3s; --d:.16s; --w:9px; --h:9px"></i>
<i class="conf" style="--c:var(--violet); --sw:-40px; --t:1.45s; --d:.26s; --w:7px; --h:12px"></i>
<i class="conf" style="--c:#ffffff; --sw:22px; --t:1.2s; --d:.1s; --w:6px; --h:10px"></i>
<i class="conf" style="--c:var(--gold); --sw:-24px; --t:1.35s; --d:.22s; --w:8px; --h:8px"></i>
<i class="conf" style="--c:var(--cyan); --sw:38px; --t:1.5s; --d:.3s; --w:7px; --h:13px"></i>
<i class="conf" style="--c:var(--pink); --sw:-34px; --t:1.28s; --d:.18s; --w:9px; --h:11px"></i>
```
Then close boom, fx-front, label. Then meta + hint. Then footer, close hero, scene, body, html.
Wait — order in hero: input, label, meta(p), hint(p) ✓ selectors `#like:checked ~ .meta` ✓ `#like:checked ~ .hint`? not used.
**Final sanity pass on subtle CSS points:**
1. `.pulse` — `inset:-3px` on fx-back ✓.
2. `#like:checked + .stage .pulse { animation: none; opacity: 0; }` ✓.
3. `.ghost .heart` — `--s:62px` overrides base 20 ✓. The `.heart` base `left:50%; top:50%` in 0×0 ghost = 0 ✓. BUT — `.heart` transition color/filter — ghost heart color: `.ghost .heart { color: var(--red) }` (0,2,0) vs `#like:checked + .stage .heart` (1,2,1)... wait `#like:checked + .stage .heart`: id(1) + pseudo-class(:checked counts as class-level) + .stage + .heart = specificity (1,3,0). `.ghost .heart` (0,2,0) → checked rule wins → same var(--red) ✓ no conflict.
4. `#like:checked + .stage .heart { filter: drop-shadow(...) }` (1,3,0) vs `.ghost .heart { filter: blur(.4px) drop-shadow(...) }` (0,2,0) → CHECKED WINS → ghost gets the SMALL drop-shadow, losing its big glow & blur when checked — hmm! The ghost's nice glow gets overridden at the moment it animates. Fix: `.ghost .heart` needs higher specificity or !important... simplest: `#like:checked + .stage .ghost .heart { color:var(--red); filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8)); }` — specificity (1,4,0) wins ✓. Or give ghost heart inline style? Cleaner: use the more specific selector. Also `.ghost .heart` base (unchecked): color var(--red) + big filter — fine (invisible anyway, opacity 0 on .ghost).
So:
```css
.ghost .heart{ --s:62px; color:var(--red); filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8)); }
#like:checked + .stage .ghost .heart{ filter:blur(.4px) drop-shadow(0 0 26px rgba(255,46,99,.8)); }
```
Second rule re-asserts for checked ✓.
5. **`.t-liked` vs `#like:checked + .stage .t-liked`** ✓ specificities fine (1,3,0) > (0,1,0) ✓.
6. **`h1 em` + `body:has(...) h1 em { animation }`** ✓.
7. **em gradient text + transform scale** — inline-block ✓.
8. **`.stage:active .btn::after` — animation restarts each press** ✓.
9. `input` `margin:-1px` — no layout impact ✓.
10. **`.meta` uses `--row` custom prop + `calc(-1 * var(--row))`** ✓.
11. **`.plus` left calc(50% + 58px)** — .meta is a `<p>` — width: as flex item in column with align-items:center → fit-content? Actually flex items size to content in cross axis?? In a column flex container, align-items:center makes items shrink-to-fit width ✓ (align-self center → width auto = fit-content? For a block-level flex item, cross-size auto = fit-content ✓ yes).
12. **`text-wrap:balance`** on h1 ✓ Chrome.
13. **`.foot`** — footer element class foot ✓.
14. The **`.btn` gap 13px** — heart-wrap 36 + text — visual ✓.
15. **Rings behind or above button?** They're in fx-front (z3) — above button — outline rings over the button briefly — looks like shockwave in front — good.
16. **Will the rings/particles be clipped by `.scene` overflow?** .scene has no overflow set → visible ✓; body overflow hidden clips only beyond viewport ✓.
17. **`min-height:100svh` with overflow hidden body** — .scene height = viewport exactly ✓.
18. **print?** n/a.
19. **The `label` wrapping `input`? NO — input is sibling ✓** (label for=) ✓.
20. **`.hero` margin auto — with .scene flex column: works ✓.**
21. **h1 em padding .05em** — prevents italic clipping ✓.
22. **`.sub` — uses `—` em dash ✓.
23. **badge text-transform uppercase — content "Pure CSS · Zero JavaScript"** displays uppercase ✓.
24. **Odd case: user clicks rapidly 5×** — animations restart each toggle ✓ chaotic joy ✓.
25. **`--hx:45px` used by .boom and .ghost ✓.**
One more thing — **`.heart-wrap` has `transition: transform .4s cubic-bezier(.3,1.5,.45,1)`** — the overshoot bezier on uncheck ✓ if triggered.
And **`.heart` inside `.heart-wrap`**: `.heart` absolute — but `.heart-wrap` is `position:relative` ✓.
**Ghost behind btn but pulse also behind** — pulse border visible around button edges (inset -3) ✓; ghost above pulse, below btn ✓.
**Now — one thing I want to double-check: the `fly` keyframes' `translate(calc(var(--dx) * .38), calc(var(--dy) * .38))`** — in the 38% frame I must ensure same function ORDER as other frames: `translate(-50%,-50%) translate(X,Y) scale() rotate()` ✓ consistent across 0/38/100 ✓.
**Confetti `drop`** — `translate(-50%,-50%) translate(X,Y) rotate()` ✓ 0/32/100 ✓.
Also hmm — **confetti initial position**: at boom center (heart, mid-button) — confetti pieces appear AT the heart then toss up — but they're opaque 8×13 rects materializing at 10% — slight pop-in; acceptable (explosion vibe).
**Possible visual annoyance: confetti falling ONTO the counter text** — z: fx-front z3 above meta ✓ momentary ✓.
I think we're set. Let me also reconsider — **should `.stage` get `margin` for breathing room from hero padding 26px top** ✓.
**Double-check `.boom` top:50%** — of fx-front = stage 64px → 32px ✓ = button vertical center ✓ heart-wrap center also 32 ✓ (heart-wrap 36 centered in 64 → 14..50, center 32 ✓).
**Heart in wrap visual offset** — discussed, ±4px — fine.
**Sub-pixel: border-radius inherit for .btn::after** ✓.
Now, is there **ANY JavaScript**? No `<script>` ✓.
**One last look at potential CSS errors:**
- `radial-gradient(closest-side circle at 50% 50%, rgba(255,255,255,.3), transparent 72%)` — syntax: `radial-gradient(closest-side at 50% 50%, ...)` — with keyword `circle`?? `radial-gradient(closest-side circle at ...)` is valid (extent keyword + shape) ✓. Hmm — `circle` + `closest-side` ✓ valid syntax.
- `mask-image: radial-gradient(...)` ✓.
- `clip-path: polygon(...)` ✓.
- `:has()` ✓ Chrome 105+.
- `100svh` ✓.
- `text-wrap: balance` ✓ Chrome 114+.
- Custom properties in keyframes ✓.
- `animation-timing-function` inside keyframes ✓.
**Font stack in var** ✓.
Also — I use `<i>` elements for particles — `<i>` is inline; set `display:block`? `.pt{ position:absolute }` → positioned → block-ish ✓ fine. `.ring` positioned ✓. `.conf` positioned ✓. `.strip i` display block ✓ set.
`.bdot` inline span with width/height — span is inline → width/height ignored! `.bdot` needs display:inline-block or it's flex item → blockified ✓ — .badge is `display:inline-flex` → its children are flex items → blockified ✓ width/height work ✓.
`.plus` absolute ✓.
**`.heart` is `<i>`** — inline but absolute → blockified ✓.
**`.ghost` span absolute ✓ contains heart `<i>` absolute ✓.**
**Check `.stage` label contains spans only — valid HTML ✓ (label can contain phrasing content — span/i are phrasing ✓; but `.btn` is a span with button role visually — fine).**
Hmm — semantic thought: should the "button" be a real `<button>`? Can't — label+checkbox pattern requires label. Fine.
**One more a11y touch**: `aria-label` on input ✓. Label content includes "LikeLiked" text — double announce... could add `aria-hidden="true"` to `.t-liked`?? But then unliked state announces "Like" ✓ liked announces... input aria-label overrides label content? If input has aria-label, the label's content is overridden for the input's accessible name — but the label ELEMENT itself is still... labels are announced for their control; with aria-label on input, name = aria-label ✓. The visual text inside label becomes hidden?? No — aria-label on input doesn't hide label text from AT necessarily (label content ignored when aria-label present). Good enough: aria-label="Like this page" ✓.
**Also `cursor:pointer` on stage ✓ and maybe `.btn` too (inherits)**.
I'm now fairly confident. Let me also add **`footer` in HTML — `<footer class="foot">`** ✓.
Let me also think again about **`.fx-back { z-index:1 } .btn { z-index:2 } .fx-front { z-index:3 }`** — all inside `.stage` — stage is inline-flex (no stacking context unless positioned+z — it IS positioned (relative) with z-index auto → NOT a stacking context... positioned with z auto → not stacking context (unless will-change etc). Children z-indexes participate in nearest ancestor stacking context — could they fight with .scene content? .scene has `animation: shake` — during animation, does it become a stacking context? Animating transform yes (animation of transform → will-change-ish → stacking context per spec: "any value other than none on transform... creates stacking context" — only if transform non-none: during shake animation, transform interpolates → non-none → stacking context ✓; when idle (animation done but still applied? animation fill... after finish without forwards fill, computed transform returns to none → stacking context gone). Either way, children z 1-3 vs scene siblings — all our z's are positive and above scene content pieces which are z auto — fine ✓.
Actually hmm — careful: fx children with z-index 1,2,3 in the ROOT stacking context (when scene not a context) — they'd paint above other scene content (header etc.) — fine ✓ desired.
**Great. Also — one final consideration about the screenshot at ~3s:** all animations idle: badge dot breathing, pulse ring pulsing ✓ static rest looks polished ✓.
Also let me reconsider the **pulse ring timing so the screenshot might catch it mid-pulse** — either way looks fine.
**Write it.** Actually — one more improvement for the initial paint "wow": subtle entrance animation for the hero (fade/rise on load)? A one-time entrance: `.scene > * { animation: enter .8s ... }` — adds keyframe #14 → footer count mismatch ("13"). Recount with entrance = 14 → update footer text. It's a nice polish — page elements fade/slide in on load. Let me add:
```css
@keyframes enter{ from{ opacity:0; transform:translateY(14px); } to{ opacity:1; transform:none; } }
header{ animation:enter .7s cubic-bezier(.2,.7,.3,1) both; }
.hero{ animation:enter .7s cubic-bezier(.2,.7,.3,1) .12s both; }
.foot{ animation:enter .7s cubic-bezier(.2,.7,.3,1) .24s both; }
```
Wait — .hero already has margin auto — animation transform on .hero fine. But careful: `body:has(#like:checked) .scene { animation: shake }` — .scene animation — separate element ✓ no conflict.
Hmm — but `.hero` transform animation + `.hero` children fx — during entrance, .hero transforms → fx contained ✓ fine (before any click).
14 @keyframes → footer "1 checkbox · 14 @keyframes · 0 lines of JavaScript".
Hmm — but `.hero` animation `both` keeps final `transform:none` ✓ fine.
Wait — one conflict: `.hero { margin:auto 0 }` + entrance animation — during animation opacity <1 — fine.
Also with entrance animations, .scene's justify via margins... fine.
Count check (final list): enter, breathe, emPop, flash, shake, ripple, pop, heartbeat, pulse, ghostUp, ring, fly, drop, rise = 14 ✓.
Footer: "1 checkbox · 14 @keyframes · 0 lines of JavaScript".
Hmm — should I verify the count matches EXACTLY what's in file? I'll count once more when writing final:
1. enter
2. breathe
3. emPop
4. flash
5. shake
6. ripple
7. pop
8. heartbeat
9. pulse
10. ghostUp
11. ring
12. fly
13. drop
14. rise
✓ 14.
Also — hmm, the entrance animation delays meaningful content? "Something meaningful visible on first paint" — elements animate in over .7s with .24s max delay — screenshot at 3s shows final state ✓. Even at 0s partially visible ✓.
**One more potential issue — `.stage:active` on touch**: touch-action manipulation ✓.
**Possible issue: on very first click, `:active` styles + `#like:checked` simultaneously at release — explosion begins while squish releasing — spring transform .2s ✓ smooth ✓.
Now — **also consider `overflow:hidden` on .btn + focus outline** — outline draws outside, not clipped by overflow? Overflow clips CONTENT, outline is not content — outlines are not clipped by the element's own overflow ✓ (they can be clipped by ancestors' overflow — body overflow hidden only at viewport edge ✓).
**Also — `.t` line-height**: inherited from .btn? .btn sets font-size 17 but not line-height → inherits body default (normal) ✓ fine.
Let me also double check **`.heart::before` positioning with --s**: `top: calc(var(--s) / -2)` → e.g. calc(20px / -2) = -10px ✓ valid (division by number ✓, negative result ✓).
And `.pt.mh::before { top: calc(var(--s,12px) / -2) }` ✓.
**Colors of dots vs bg** ✓ vivid.
**Final HTML assembly** — also add an HTML comment noting zero scripts ✓ cute.
Also — set `<html lang="en">` ✓.
Double-check: **no network requests** — fonts system ✓ no images ✓.
I think this is complete and robust. Let me also mentally simulate a click once more end-to-end:
1. mousedown on label → `:active` → btn scales to .94, ripple plays.
2. mouseup → click → checkbox checked.
3. Instantly: heart color #ff2e63 + glow (transition .38s), heart-wrap pop animation starts (scale 1→1.45 rotate → ...), t-like slides up out (.26s), t-liked springs in (.52s delayed .1s), btn background/border/shadow shift red (.35s), rings r1 starts immediately (scale .4→2.65 fade), dots/stars/mini-hearts fly out (staggered delays up to .1s, .8-1.05s), confetti tossed up & falls (1.2-1.5s), ghost heart launches upward (1.3s), scene shakes .55s, flash blushes .8s, h1 "like" pops, counter digit 7→8 rolls with overshoot, +1 rises .16s delayed, pulse ring stops.
4. ~.85s later: heartbeat loop begins on the heart.
5. Click again: everything reverses smoothly (transitions), animations removed; digit rolls back to 7; +1 doesn't re-run (uncheck); pulse resumes.
Hmm — on uncheck, `.plus` animation removed → snaps to opacity 0 base ✓ invisible ✓.
On uncheck, `.flash`/`.shake`/`emPop` removed → snap to base (opacity 0 / no transform) ✓ invisible ✓.
On uncheck, `.pt` animations removed → opacity 0 base ✓ invisible ✓.
One thing: on uncheck, `pop` fill both removed → heart-wrap transform back to base — transition maybe triggers bouncy return ✓.
Rapid double-click: check→uncheck quickly: explosion starts, then removed instantly (particles vanish mid-flight) + new squish... The "liked" burst disappearing when unliked is logically consistent ✓.
**Great — writing final output now.**
One more styling detail — the **`.btn` when liked and hovered**: hover rule (0,3,0) vs checked (1,3,0) — checked bg wins; hover transform still applies ✓.
Also the t-like/t-liked — while liked, `.t-like` hidden above; ✓.
Let me also reconsider **`.meta` margin-top 38px** — confetti falls from boom (y=32 in button) down 240px → passes through meta ✓ overlapping text momentarily — fine/fun.
And **`.hint` margin 18** ✓.
Also — hmm, `header` entrance animation `both` + `body:has(...) h1 em { animation: emPop }` — em inside h1 inside header; header animation on ancestor doesn't conflict with em's own ✓.
**Writing final code.** Some last CSS ordering: I'll put keyframes near their components for readability (already planned). The final file will be long (~450 lines) — that's fine and impressive.
Also let me make sure the **viewport meta** and no `user-scalable=no` (a11y) ✓.
Final check of `--dy` for confetti... `drop` has fixed -95px rise and 240px fall — independent of --dy ✓ (only --sw horizontal).
Also potential nit — `.conf` `--sw` negative values: calc(var(--sw) * .45) with -30px → -13.5px ✓.
DONE thinking — output the file. Let me write it carefully, no typos, balanced braces.
Actually — one more important verification on **`#like:checked + .stage`**: `+` requires label IMMEDIATELY after input — my hero DOM: input then label ✓ (whitespace between is fine).
And `#like:checked ~ .meta` ✓, `#like:checked ~ .hint` unused.
`#like:focus-visible + .stage .btn` ✓.
Also — set `.stage { border-radius:999px }`? unnecessary.
Writing now. I'll also include brief comments to make the source readable/judgeable.
One more paranoia — **`.boom` `left:var(--hx)` where --hx on :root** ✓ inherited ✓.
And — `.ghost { left: var(--hx) }` ✓.
Also — I realize I should double check the **`.heart` base color `#97a1b8`** — visible gray on dark button ✓.
**`.t-liked` color var(--red-hi)** ✓.
**Also — small type detail: `.btn` font-weight 700 ✓.**
Output the final file now.