<section class="demo">
<article class="demo__card" id="card">
<span class="demo__shine" id="shine"></span>
<p class="demo__kicker">019 / Surface</p>
<h2>Hold the light</h2>
<p class="demo__meta">Tilt follows the pointer</p>
</article>
</section>.demo {
min-height: 100dvh;
display: grid;
place-items: center;
perspective: 920px;
background: #08080a;
color: #f4f4f2;
font-family: "Helvetica Neue", system-ui, sans-serif;
}
.demo__card {
position: relative;
width: min(280px, 78vw);
height: 340px;
padding: 24px;
display: flex;
flex-direction: column;
justify-content: space-between;
border-radius: 16px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(160deg, #1a1a22 0%, #101014 55%, #16120e 100%);
transform-style: preserve-3d;
will-change: transform;
}
.demo__shine {
position: absolute;
inset: -40%;
pointer-events: none;
background: radial-gradient(circle at 30% 20%, rgba(244, 244, 242, 0.22), transparent 42%);
mix-blend-mode: screen;
opacity: 0.7;
}
.demo__kicker,
.demo__meta {
margin: 0;
font-size: 10px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #5a5a62;
}
.demo__card h2 {
margin: 0;
font-size: 32px;
font-weight: 500;
letter-spacing: -0.04em;
line-height: 1.05;
}
@media (prefers-reduced-motion: reduce) {
.demo__card { transform: none !important; }
}const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const card = document.querySelector("#card");
const shine = document.querySelector("#shine");
const rotX = gsap.quickTo(card, "rotationX", { duration: 0.55, ease: "power3" });
const rotY = gsap.quickTo(card, "rotationY", { duration: 0.55, ease: "power3" });
gsap.from(card, { y: 18, opacity: 0, duration: 0.9, ease: "expo.out" });
if (!reduce) {
gsap.to(card, { y: -6, duration: 2.4, ease: "sine.inOut", yoyo: true, repeat: -1 });
}
card.addEventListener("pointermove", function (event) {
if (reduce) return;
const rect = card.getBoundingClientRect();
const px = (event.clientX - rect.left) / rect.width - 0.5;
const py = (event.clientY - rect.top) / rect.height - 0.5;
rotY(px * 16);
rotX(py * -12);
shine.style.background = "radial-gradient(circle at " + (px * 100 + 50) + "% " + (py * 100 + 50) + "%, rgba(244,244,242,0.28), transparent 42%)";
});
card.addEventListener("pointerleave", function () {
rotX(0);
rotY(0);
});