*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #070a12;
--text: #f0f4fb;
--muted: #8a95a8;
--accent: #86e8ff;
--neon-purple: #ae52ff;
}
html { background: var(--bg); color: var(--text); font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; }
body { overflow-x: hidden; }
.intro {
min-height: 70vh;
display: flex; flex-direction: column; align-items: center; justify-content: center;
text-align: center; padding: 2rem;
}
.eyebrow { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.18em; color: var(--accent); margin-bottom: 1rem; }
h1 {
font-size: clamp(2.2rem, 6vw, 4rem); font-weight: 700; letter-spacing: -0.03em;
background: linear-gradient(135deg, #fff, var(--accent), var(--neon-purple));
background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: clamp(0.9rem, 2vw, 1.05rem); color: var(--muted); max-width: 460px; margin-top: 0.75rem; line-height: 1.6; }
/* Stack section */
.stack-section { position: relative; min-height: 250vh; }
.perspective-container {
position: sticky; top: 0; height: 100vh;
display: flex; align-items: center; justify-content: center;
perspective: 1200px;
}
.card-stack {
position: relative;
width: 380px; height: 460px;
}
/* Cards */
.stack-card {
position: absolute; inset: 0;
will-change: transform, opacity;
}
.card-glass {
width: 100%; height: 100%;
background: linear-gradient(135deg,
hsla(var(--hue,200), 30%, 15%, 0.6),
hsla(var(--hue,200), 20%, 10%, 0.8)
);
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid hsla(var(--hue,200), 40%, 40%, 0.2);
border-radius: 20px;
padding: 2.5rem;
display: flex; flex-direction: column; justify-content: center;
box-shadow: 0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 hsla(var(--hue,200), 50%, 70%, 0.08);
}
.card-num {
font-size: 3rem; font-weight: 700; line-height: 1; margin-bottom: 1rem;
color: hsla(var(--hue,200), 60%, 60%, 0.2);
}
.card-glass h3 {
font-size: 1.3rem; font-weight: 600; margin-bottom: 0.6rem;
color: hsl(var(--hue,200) 70% 80%);
}
.card-glass p { font-size: 0.88rem; color: var(--muted); line-height: 1.55; margin-bottom: 1.2rem; }
.card-tags { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.card-tags span {
padding: 0.18rem 0.55rem; border-radius: 4px;
font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
background: hsla(var(--hue,200), 40%, 30%, 0.25);
color: hsl(var(--hue,200) 60% 70%);
}
/* Outro */
.outro {
min-height: 50vh; display: flex; flex-direction: column;
align-items: center; justify-content: center; text-align: center; padding: 2rem;
}
.outro h2 { font-size: clamp(1.8rem, 4vw, 3rem); font-weight: 700; letter-spacing: -0.02em; margin-bottom: 1.5rem; }
.btn-back {
display: inline-block; padding: 0.7rem 2rem; border-radius: 999px;
border: 1px solid rgba(134,232,255,0.3); color: var(--accent); text-decoration: none;
font: 600 0.85rem/1 'Inter', system-ui, sans-serif; transition: all 0.25s ease;
}
.btn-back:hover { background: rgba(134,232,255,0.08); border-color: var(--accent); }
/* Reduced motion */
.reduced-motion .stack-card { opacity: 1 !important; transform: none !important; }
@media (max-width: 640px) {
.card-stack { width: 300px; height: 400px; }
.card-glass { padding: 1.8rem; }
}
if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Lenis from 'lenis';
gsap.registerPlugin(ScrollTrigger);
initDemoShell({ title: 'Card Stack Cascade', category: 'scroll', tech: ['gsap', 'scrolltrigger', '3d-transforms'] });
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
const reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add('reduced-motion');
window.addEventListener('motion-preference', (e) => {
document.documentElement.classList.toggle('reduced-motion', e.detail.reduced);
ScrollTrigger.refresh();
});
const cards = gsap.utils.toArray('.stack-card');
const total = cards.length;
// Initial stacked position: all centered, slight y offsets, increasing scale-down
cards.forEach((card, i) => {
gsap.set(card, {
y: i * -8,
scale: 1 - (total - 1 - i) * 0.04,
rotateX: 0,
zIndex: i,
opacity: i === total - 1 ? 1 : 0.6,
});
});
if (!reduced) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.stack-section',
start: 'top top',
end: 'bottom bottom',
scrub: 1,
},
});
// Fan out: each card moves to its final position
cards.forEach((card, i) => {
const angle = (i - (total - 1) / 2) * 8; // spread angle
const xOff = (i - (total - 1) / 2) * 120; // horizontal spread
const yOff = Math.abs(i - (total - 1) / 2) * 30; // arc shape
const rot = (i - (total - 1) / 2) * 4; // slight rotation
tl.to(card, {
x: xOff,
y: yOff,
rotateY: angle,
rotateZ: rot,
scale: 1,
opacity: 1,
zIndex: i,
duration: 1,
ease: 'none',
}, 0);
});
// Second phase: cards settle into a grid-like arrangement
cards.forEach((card, i) => {
const col = i % 3;
const row = Math.floor(i / 3);
const gridX = (col - 1) * 200;
const gridY = row * 240 - 120;
tl.to(card, {
x: gridX,
y: gridY,
rotateY: 0,
rotateZ: 0,
rotateX: 0,
scale: 0.85,
duration: 1,
ease: 'none',
}, 1);
});
}
// Intro animations
if (!reduced) {
gsap.set('.intro .eyebrow', { opacity: 0, y: 20 });
gsap.set('.intro h1', { opacity: 0, y: 40 });
gsap.set('.intro .subtitle', { opacity: 0, y: 25 });
gsap.timeline({ defaults: { ease: 'expo.out' } })
.to('.intro .eyebrow', { opacity: 1, y: 0, duration: 0.7, delay: 0.3 })
.to('.intro h1', { opacity: 1, y: 0, duration: 0.9 }, '-=0.4')
.to('.intro .subtitle', { opacity: 1, y: 0, duration: 0.7 }, '-=0.5');
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Stack Cascade — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"/vendor/gsap/index.js","gsap/ScrollTrigger":"/vendor/gsap/ScrollTrigger.js","gsap/SplitText":"/vendor/gsap/SplitText.js","gsap/Flip":"/vendor/gsap/Flip.js","gsap/ScrambleTextPlugin":"/vendor/gsap/ScrambleTextPlugin.js","gsap/TextPlugin":"/vendor/gsap/TextPlugin.js","gsap/all":"/vendor/gsap/all.js","gsap/":"/vendor/gsap/","lenis":"/vendor/lenis/dist/lenis.mjs","three":"/vendor/three/build/three.module.js","three/addons/":"/vendor/three/examples/jsm/"}}</script>
<style>html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}</style>
</head>
<body>
<section class="intro">
<span class="eyebrow">Demo 05</span>
<h1>Card Stack Cascade</h1>
<p class="subtitle">Cards start stacked and fan out with 3D transforms as you scroll. Glassmorphism panels with perspective depth.</p>
</section>
<section class="stack-section">
<div class="perspective-container">
<div class="card-stack" id="card-stack">
<div class="stack-card" data-index="0" style="--hue: 200;">
<div class="card-glass">
<span class="card-num">01</span>
<h3>Scroll Animations</h3>
<p>Choreograph complex timelines driven by scroll position with GSAP ScrollTrigger.</p>
<div class="card-tags"><span>gsap</span><span>scrolltrigger</span></div>
</div>
</div>
<div class="stack-card" data-index="1" style="--hue: 260;">
<div class="card-glass">
<span class="card-num">02</span>
<h3>3D WebGL Scenes</h3>
<p>Build immersive particle tunnels, shader backgrounds, and product showcases with Three.js.</p>
<div class="card-tags"><span>three.js</span><span>webgl</span></div>
</div>
</div>
<div class="stack-card" data-index="2" style="--hue: 320;">
<div class="card-glass">
<span class="card-num">03</span>
<h3>View Transitions</h3>
<p>Seamless page transitions with shared-element morphing using the View Transitions API.</p>
<div class="card-tags"><span>view-transitions</span><span>css</span></div>
</div>
</div>
<div class="stack-card" data-index="3" style="--hue: 40;">
<div class="card-glass">
<span class="card-num">04</span>
<h3>Canvas Effects</h3>
<p>Bokeh particles, spotlights, noise overlays, and magnetic cursors with Canvas 2D.</p>
<div class="card-tags"><span>canvas</span><span>vanilla-js</span></div>
</div>
</div>
<div class="stack-card" data-index="4" style="--hue: 160;">
<div class="card-glass">
<span class="card-num">05</span>
<h3>Motion Design</h3>
<p>Shared tokens, easing systems, and accessibility-first motion primitives.</p>
<div class="card-tags"><span>design-system</span><span>a11y</span></div>
</div>
</div>
</div>
</div>
</section>
<section class="outro">
<h2>Stack complete.</h2>
<a href="/" class="btn-back">Back to Showcase</a>
</section>
<script type="module" src="script.js"></script>
</body>
</html>