CSS
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400&display=swap');
.scd-flash, .scd-flash *, .scd-flash *::before, .scd-flash *::after { box-sizing: border-box; margin: 0; padding: 0; }
.scd-flash {
position: relative;
min-height: 460px;
display: grid;
place-items: center;
background: #05060a;
font-family: 'Geist', sans-serif;
}
.scd-flash::before {
content: '';
position: absolute; inset: -30%;
background:
radial-gradient(40% 40% at 22% 28%, rgba(56,189,248,.30), transparent 70%),
radial-gradient(45% 45% at 78% 25%, rgba(168,85,247,.28), transparent 70%),
radial-gradient(50% 50% at 50% 82%, rgba(244,114,182,.24), transparent 70%);
filter: blur(26px);
animation: scd-flash-drift 19s ease-in-out infinite alternate;
pointer-events: none;
}
@keyframes scd-flash-drift {
0% { transform: translate(-3%,-2%) scale(1); }
100% { transform: translate(4%,3%) scale(1.1); }
}
.scd-flash::after {
content: '';
position: absolute; inset: 0;
background: repeating-linear-gradient(0deg, rgba(255,255,255,.02) 0 1px, transparent 1px 3px);
mix-blend-mode: overlay;
pointer-events: none;
}
.scd-flash__stage { position: relative; width: 280px; height: 380px; perspective: 1300px; z-index: 1; }
.scd-flash__counter {
position: absolute; top: -46px; left: 0; right: 0;
display: flex; align-items: center; justify-content: space-between;
font-family: 'Geist Mono', monospace;
font-size: .72rem; letter-spacing: .15em;
color: rgba(255,255,255,.55);
}
.scd-flash__prog { flex: 1; height: 3px; margin: 0 12px; background: rgba(255,255,255,.15); border-radius: 3px; overflow: hidden; }
.scd-flash__prog i { display: block; height: 100%; background: #fff; border-radius: 3px; transition: width .4s; box-shadow: 0 0 10px rgba(255,255,255,.6); }
.scd-flash__deck { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; }
.scd-flash__card {
position: absolute; inset: 0;
border-radius: 26px;
transform-style: preserve-3d;
cursor: grab;
user-select: none;
overflow: hidden;
box-shadow: 0 30px 60px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.09) inset;
transition: transform .5s cubic-bezier(.16,1,.3,1), opacity .5s, box-shadow .5s;
}
.scd-flash__card.scd-flash__card--drag { transition: none; cursor: grabbing; }
.scd-flash__grad { position: absolute; inset: 0; background-size: 200% 200%; animation: scd-flash-flow 8s ease infinite; }
@keyframes scd-flash-flow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.scd-flash__glow {
position: absolute; inset: 0;
background: radial-gradient(260px circle at var(--mx,50%) var(--my,40%), rgba(255,255,255,.4), transparent 60%);
mix-blend-mode: soft-light;
opacity: 0;
transition: opacity .4s;
}
.scd-flash__card:hover .scd-flash__glow { opacity: 1; }
.scd-flash__sheen {
position: absolute; top: -50%; left: -30%;
width: 55%; height: 200%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,.45), transparent);
transform: rotate(18deg) translateX(-260px);
transition: transform .9s cubic-bezier(.16,1,.3,1);
}
.scd-flash__verdict {
position: absolute; top: 24px;
padding: 8px 16px;
border-radius: 12px;
font-family: 'Geist Mono', monospace;
font-size: .85rem; font-weight: 700;
letter-spacing: .1em;
border: 2px solid;
opacity: 0;
transition: opacity .15s;
backdrop-filter: blur(4px);
}
.scd-flash__verdict--know { right: 22px; color: #86efac; border-color: #86efac; transform: rotate(10deg); }
.scd-flash__verdict--again { left: 22px; color: #fca5a5; border-color: #fca5a5; transform: rotate(-10deg); }
.scd-flash__face {
position: absolute; inset: 0;
padding: 30px;
display: flex; flex-direction: column; justify-content: space-between;
transform: translateZ(40px);
color: #fff;
}
.scd-flash__tag {
font-family: 'Geist Mono', monospace;
font-size: .62rem; letter-spacing: .22em; text-transform: uppercase;
padding: 6px 12px; border-radius: 999px;
background: rgba(255,255,255,.16);
border: 1px solid rgba(255,255,255,.24);
align-self: flex-start;
backdrop-filter: blur(6px);
}
.scd-flash__term {
font-family: 'Instrument Serif', serif;
font-size: 2.7rem; line-height: 1; letter-spacing: -.01em;
text-shadow: 0 3px 18px rgba(0,0,0,.3);
}
.scd-flash__def { font-size: .9rem; opacity: .9; line-height: 1.5; font-weight: 300; }
.scd-flash__fliphint { font-family: 'Geist Mono', monospace; font-size: .62rem; letter-spacing: .18em; opacity: .6; }
.scd-flash__card--g1 .scd-flash__grad { background: linear-gradient(135deg,#831843,#db2777 45%,#fb7185); }
.scd-flash__card--g2 .scd-flash__grad { background: linear-gradient(135deg,#0c4a6e,#0284c7 45%,#38bdf8); }
.scd-flash__card--g3 .scd-flash__grad { background: linear-gradient(135deg,#064e3b,#059669 50%,#34d399); }
.scd-flash__card--g4 .scd-flash__grad { background: linear-gradient(135deg,#581c87,#7c3aed 45%,#a78bfa); }
.scd-flash__hint {
position: absolute; bottom: -46px; left: 0; right: 0;
text-align: center;
font-family: 'Geist Mono', monospace;
color: rgba(255,255,255,.5);
font-size: .7rem;
letter-spacing: .12em;
}
.scd-flash__hint b { color: #86efac; }
.scd-flash__hint i { color: #fca5a5; font-style: normal; }
@media (prefers-reduced-motion: reduce) {
.scd-flash::before,
.scd-flash__grad,
.scd-flash__sheen,
.scd-flash__card,
.scd-flash__prog i { animation: none !important; transition: none !important; }
} JS
(() => {
const root = document.querySelector('.scd-flash');
if (!root) return;
const stage = root.querySelector('[data-scd-flash="stage"]');
const deck = root.querySelector('[data-scd-flash="deck"]');
const posEl = root.querySelector('[data-scd-flash="pos"]');
const barEl = root.querySelector('[data-scd-flash="bar"]');
if (!stage || !deck) return;
const total = deck.querySelectorAll('.scd-flash__card').length;
let done = 0;
function meter() {
const current = Math.min(done + 1, total);
if (posEl) posEl.textContent = String(current).padStart(2, '0') + ' / ' + String(total).padStart(2, '0');
if (barEl) barEl.style.width = (done / total * 100) + '%';
}
function layout(instant) {
const cards = [...deck.querySelectorAll('.scd-flash__card')];
cards.forEach((c, i) => {
const d = cards.length - 1 - i;
const tf = `translateY(${d * -14}px) translateZ(${-d * 30}px) scale(${1 - d * .05})`;
if (instant) {
const t = c.style.transition;
c.style.transition = 'none';
c.style.transform = tf;
void c.offsetWidth;
c.style.transition = t;
} else {
c.style.transform = tf;
}
c.style.zIndex = i + 1;
c.style.opacity = d > 3 ? 0 : 1;
c.style.filter = `brightness(${1 - d * .12})`;
});
}
function sendBack(card, dir) {
done++; meter();
card.style.transform = `translateX(${dir * 480}px) rotate(${dir * 22}deg)`;
card.style.opacity = '0';
setTimeout(() => {
card.style.transition = 'none';
deck.insertBefore(card, deck.firstElementChild);
card.style.opacity = '';
card.style.filter = '';
const k = card.querySelector('.scd-flash__verdict--know');
const a = card.querySelector('.scd-flash__verdict--again');
if (k) k.style.opacity = 0;
if (a) a.style.opacity = 0;
layout(true);
void card.offsetWidth;
card.style.transition = '';
if (done >= total) done = 0;
meter(); bind();
}, 420);
}
let activeTop = null;
let sx = 0, dx = 0, drag = false, moved = false;
let know = null, again = null;
function onDown(e) {
drag = true; moved = false;
sx = (e.touches ? e.touches[0].clientX : e.clientX);
if (activeTop) activeTop.classList.add('scd-flash__card--drag');
}
function onMove(e) {
if (!drag || !activeTop) return;
dx = (e.touches ? e.touches[0].clientX : e.clientX) - sx;
if (Math.abs(dx) > 5) moved = true;
activeTop.style.transform = `translateX(${dx}px) rotate(${dx * .05}deg)`;
if (know) know.style.opacity = Math.max(0, Math.min(1, dx / 90));
if (again) again.style.opacity = Math.max(0, Math.min(1, -dx / 90));
}
function onUp() {
if (!drag || !activeTop) return;
drag = false;
activeTop.classList.remove('scd-flash__card--drag');
if (!moved) { sendBack(activeTop, -1); return; }
if (Math.abs(dx) > 100) {
sendBack(activeTop, dx > 0 ? 1 : -1);
} else {
if (know) know.style.opacity = 0;
if (again) again.style.opacity = 0;
layout();
}
dx = 0;
}
function bind() {
const cards = [...deck.querySelectorAll('.scd-flash__card')];
const top = cards[cards.length - 1];
if (!top) return;
activeTop = top;
know = top.querySelector('.scd-flash__verdict--know');
again = top.querySelector('.scd-flash__verdict--again');
top.onmousedown = onDown;
top.ontouchstart = onDown;
}
root.addEventListener('mousemove', onMove);
root.addEventListener('touchmove', onMove, { passive: true });
root.addEventListener('mouseup', onUp);
root.addEventListener('touchend', onUp);
root.addEventListener('mouseleave', onUp);
stage.addEventListener('mousemove', (e) => {
stage.querySelectorAll('.scd-flash__card').forEach((c) => {
const r = c.getBoundingClientRect();
c.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
c.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%');
});
});
meter(); layout(); bind();
})();