Dating — Swipe Deck
A tactile, phone-framed swipe deck where draggable profile cards tilt and reveal LIKE or NOPE stamps as you fling them left or right. Each card stacks a full-bleed gradient photo, name and age, a short bio and interest chips, with the next card peeking behind. Action buttons for rewind, nope, super-like and like mirror the gestures, and a friendly empty state lands when the deck runs dry. Built for pointer and touch with spring-back physics and a live match toast.
MCP
Code
:root {
--bg: #fff5f6;
--surface: #ffffff;
--ink: #2a1a2e;
--ink-2: #5a4a55;
--muted: #8a7a85;
--coral: #ff5e6c;
--coral-d: #e63950;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--pink: #ff8fb1;
--grad: linear-gradient(135deg, #ff5e6c, #8b5cf6);
--line: rgba(42, 26, 46, 0.10);
--white: #fff;
--r-sm: 12px;
--r-md: 20px;
--r-lg: 28px;
--green: #22c55e;
--green-d: #16a34a;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px 16px;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 600px at 15% -10%, rgba(255, 143, 177, 0.35), transparent 60%),
radial-gradient(1000px 500px at 110% 20%, rgba(139, 92, 246, 0.28), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Phone frame */
.phone {
width: 100%;
max-width: 430px;
min-height: 760px;
background: var(--surface);
border-radius: 40px;
box-shadow:
0 2px 4px rgba(42, 26, 46, 0.06),
0 30px 70px -20px rgba(124, 58, 237, 0.35),
0 0 0 8px rgba(255, 255, 255, 0.7);
padding: 18px 18px 22px;
display: flex;
flex-direction: column;
gap: 16px;
position: relative;
overflow: hidden;
}
.phone::before {
content: "";
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
width: 118px;
height: 6px;
border-radius: 999px;
background: var(--line);
z-index: 5;
}
/* Top bar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 6px 0;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 12px;
background: var(--grad);
color: #fff;
font-size: 18px;
box-shadow: 0 8px 18px -6px rgba(230, 57, 80, 0.6);
}
.brand-name {
font-family: "Poppins", "Inter", sans-serif;
font-weight: 800;
font-size: 21px;
letter-spacing: -0.02em;
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.counter {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--muted);
background: var(--bg);
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--line);
}
.counter #remaining { color: var(--coral-d); font-weight: 700; }
/* Deck area */
.deck {
position: relative;
flex: 1;
min-height: 480px;
margin: 2px 2px 0;
}
/* Cards */
.card {
position: absolute;
inset: 0;
border-radius: var(--r-lg);
overflow: hidden;
background: var(--surface);
box-shadow:
0 10px 30px -10px rgba(42, 26, 46, 0.35),
0 2px 8px rgba(42, 26, 46, 0.08);
user-select: none;
-webkit-user-select: none;
cursor: grab;
touch-action: none;
will-change: transform;
transform-origin: center bottom;
}
.card.dragging { cursor: grabbing; }
.card:not(.top) { pointer-events: none; }
/* stacked scale for the cards behind */
.card.top { transform: translateY(0) scale(1); z-index: 3; }
.card.next { transform: translateY(14px) scale(0.955); z-index: 2; filter: saturate(0.9); }
.card.later { transform: translateY(28px) scale(0.91); z-index: 1; filter: saturate(0.8) brightness(0.98); }
.card.hidden-deep { opacity: 0; transform: translateY(28px) scale(0.9); }
.card.settle { transition: transform 0.45s cubic-bezier(0.18, 0.9, 0.28, 1.3); }
.card.stack-anim { transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.3, 1), filter 0.4s ease; }
.card-photo {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
}
.card-photo::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(20, 8, 24, 0.86) 0%,
rgba(20, 8, 24, 0.35) 34%,
rgba(20, 8, 24, 0) 58%
);
}
.card-top-fade {
position: absolute;
inset: 0 0 auto 0;
height: 90px;
background: linear-gradient(to bottom, rgba(20, 8, 24, 0.4), transparent);
z-index: 2;
}
.card-dist {
position: absolute;
top: 16px;
left: 16px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.32);
backdrop-filter: blur(6px);
padding: 6px 11px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.25);
}
.card-online {
position: absolute;
top: 16px;
right: 16px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.32);
backdrop-filter: blur(6px);
padding: 6px 11px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.25);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.35);
}
.card-info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
padding: 20px 22px 24px;
color: #fff;
}
.card-name {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
margin: 0 0 6px;
}
.card-name h2 {
font-family: "Poppins", "Inter", sans-serif;
font-size: 27px;
font-weight: 700;
letter-spacing: -0.02em;
margin: 0;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}
.card-name .age { font-size: 24px; font-weight: 500; opacity: 0.92; }
.card-name .verified {
font-size: 14px;
color: #7dd3fc;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.4));
}
.card-bio {
margin: 0 0 12px;
font-size: 14px;
line-height: 1.45;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.chip {
font-size: 12px;
font-weight: 600;
color: #fff;
background: rgba(255, 255, 255, 0.16);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.28);
padding: 6px 11px;
border-radius: 999px;
}
/* Stamps */
.stamp {
position: absolute;
top: 34px;
z-index: 4;
font-family: "Poppins", "Inter", sans-serif;
font-weight: 800;
font-size: 34px;
letter-spacing: 0.04em;
padding: 6px 16px;
border-radius: 12px;
border: 4px solid;
opacity: 0;
pointer-events: none;
}
.stamp-like {
left: 22px;
color: var(--green);
border-color: var(--green);
transform: rotate(-16deg);
text-shadow: 0 2px 12px rgba(34, 197, 94, 0.4);
}
.stamp-nope {
right: 22px;
color: var(--coral);
border-color: var(--coral);
transform: rotate(16deg);
}
.stamp-super {
left: 50%;
top: auto;
bottom: 130px;
transform: translateX(-50%) rotate(-6deg);
color: var(--violet);
border-color: var(--violet);
font-size: 26px;
}
/* Empty state */
.empty {
position: absolute;
inset: 0;
z-index: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 8px;
padding: 32px;
border-radius: var(--r-lg);
background: var(--bg);
border: 1.5px dashed var(--line);
animation: fadeIn 0.5s ease both;
}
.empty[hidden] { display: none; }
.empty-emoji { font-size: 52px; margin-bottom: 4px; }
.empty h2 {
font-family: "Poppins", "Inter", sans-serif;
font-size: 21px;
margin: 0;
color: var(--ink);
}
.empty p {
margin: 0 0 8px;
font-size: 14px;
color: var(--muted);
max-width: 260px;
}
.btn-reset {
font-family: inherit;
font-size: 14px;
font-weight: 700;
color: #fff;
background: var(--grad);
border: 0;
padding: 12px 24px;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 12px 24px -8px rgba(230, 57, 80, 0.6);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.btn-reset:hover { transform: translateY(-2px); box-shadow: 0 16px 30px -8px rgba(230, 57, 80, 0.7); }
.btn-reset:active { transform: translateY(0) scale(0.97); }
.btn-reset:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
/* Action bar */
.actions {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 6px 0 4px;
}
.act {
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: var(--surface);
cursor: pointer;
font-size: 22px;
line-height: 1;
box-shadow: 0 6px 18px -6px rgba(42, 26, 46, 0.28);
transition: transform 0.16s cubic-bezier(0.2, 0.8, 0.3, 1.4), box-shadow 0.16s ease, filter 0.16s ease;
}
.act:hover { transform: translateY(-4px) scale(1.06); }
.act:active { transform: translateY(0) scale(0.9); }
.act:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
.act:disabled { opacity: 0.4; cursor: not-allowed; transform: none; filter: grayscale(0.4); }
.act-rewind { width: 50px; height: 50px; color: #f59e0b; font-size: 20px; }
.act-nope { width: 62px; height: 62px; color: var(--coral-d); font-weight: 700; }
.act-super { width: 50px; height: 50px; color: var(--violet-d); }
.act-like { width: 62px; height: 62px; color: var(--green-d); }
.act-nope:hover { box-shadow: 0 10px 22px -6px rgba(230, 57, 80, 0.55); }
.act-like:hover { box-shadow: 0 10px 22px -6px rgba(34, 197, 94, 0.55); }
.act-super:hover { box-shadow: 0 10px 22px -6px rgba(124, 58, 237, 0.55); }
.act.pulse { animation: pop 0.4s ease; }
@keyframes pop {
0% { transform: scale(1); }
45% { transform: scale(1.28); }
100% { transform: scale(1); }
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 34px;
transform: translate(-50%, 24px);
background: var(--grad);
color: #fff;
font-weight: 700;
font-size: 14px;
padding: 13px 22px;
border-radius: 999px;
box-shadow: 0 18px 40px -12px rgba(124, 58, 237, 0.7);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.2, 0.8, 0.3, 1.3);
z-index: 50;
display: flex;
align-items: center;
gap: 9px;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast .toast-emoji { font-size: 18px; }
@media (max-width: 520px) {
body { padding: 0; place-items: stretch; }
.phone {
max-width: none;
min-height: 100vh;
border-radius: 0;
box-shadow: none;
padding: 12px 14px 18px;
}
.phone::before { display: none; }
.topbar { padding-top: 8px; }
.deck { min-height: 0; }
.card-name h2 { font-size: 25px; }
.toast { bottom: 96px; }
}
@media (prefers-reduced-motion: reduce) {
.card.settle, .card.stack-anim, .act, .toast, .btn-reset { transition-duration: 0.01ms; }
.act.pulse { animation: none; }
}(function () {
"use strict";
// --- Fictional profile data ---
var PROFILES = [
{
name: "Mara", age: 27, verified: true, online: true, dist: "2 km away",
bio: "Ceramicist chasing good light and better tacos. Ask me about kilns.",
interests: ["Pottery", "Film photography", "Tacos", "Vinyl"],
grad: "linear-gradient(150deg,#ff8fb1,#ff5e6c 55%,#8b5cf6)"
},
{
name: "Dev", age: 31, verified: false, online: false, dist: "5 km away",
bio: "Trail runner, terrible at chess, great at making playlists.",
interests: ["Trail running", "Chess", "Synthwave", "Cold brew"],
grad: "linear-gradient(150deg,#8b5cf6,#7c3aed 60%,#2a1a2e)"
},
{
name: "Priya", age: 29, verified: true, online: true, dist: "1 km away",
bio: "Weekend baker and full-time plant hoarder. My sourdough has a name.",
interests: ["Baking", "Plants", "Bouldering", "Podcasts"],
grad: "linear-gradient(150deg,#ffb199,#ff5e6c 50%,#e63950)"
},
{
name: "Theo", age: 34, verified: false, online: true, dist: "8 km away",
bio: "Jazz drummer by night, UX designer by day. Will duet you on karaoke.",
interests: ["Jazz", "Design", "Karaoke", "Ramen"],
grad: "linear-gradient(150deg,#a78bfa,#8b5cf6 55%,#ff5e6c)"
},
{
name: "Noor", age: 26, verified: true, online: false, dist: "3 km away",
bio: "Marine biologist. I will show you tide-pool photos, you have been warned.",
interests: ["Diving", "Oceans", "Watercolor", "Board games"],
grad: "linear-gradient(150deg,#7dd3fc,#8b5cf6 55%,#ff8fb1)"
},
{
name: "Léo", age: 30, verified: false, online: true, dist: "6 km away",
bio: "Cyclist and amateur cheesemonger. Yes, that is a real hobby.",
interests: ["Cycling", "Cheese", "Wine", "Sci-fi"],
grad: "linear-gradient(150deg,#fda4af,#e63950 55%,#7c3aed)"
}
];
var THRESHOLD = 110; // px to trigger a swipe
var MAX_ROT = 18; // deg
var deck = document.getElementById("deck");
var emptyEl = document.getElementById("empty");
var remainingEl = document.getElementById("remaining");
var toastEl = document.getElementById("toast");
var btnRewind = document.getElementById("rewind");
var btnNope = document.getElementById("nope");
var btnSuper = document.getElementById("super");
var btnLike = document.getElementById("like");
var btnReset = document.getElementById("reset");
var queue = []; // remaining profiles (top = last item)
var history = []; // {profile, dir} for rewind
var toastTimer;
// ---- rendering ----
function buildCard(profile) {
var card = document.createElement("article");
card.className = "card";
card.setAttribute("aria-label", profile.name + ", " + profile.age);
var chips = profile.interests.map(function (i) {
return '<span class="chip">' + i + "</span>";
}).join("");
card.innerHTML =
'<div class="card-photo" style="background-image:' + profile.grad + '"></div>' +
'<div class="card-top-fade"></div>' +
'<span class="card-dist">📍 ' + profile.dist + "</span>" +
(profile.online ? '<span class="card-online"><span class="dot"></span>Online</span>' : "") +
'<div class="stamp stamp-like">LIKE</div>' +
'<div class="stamp stamp-nope">NOPE</div>' +
'<div class="stamp stamp-super">SUPER</div>' +
'<div class="card-info">' +
'<div class="card-name"><h2>' + profile.name + "</h2>" +
'<span class="age">' + profile.age + "</span>" +
(profile.verified ? '<span class="verified" title="Verified">✔ verified</span>' : "") +
"</div>" +
'<p class="card-bio">' + profile.bio + "</p>" +
'<div class="chips">' + chips + "</div>" +
"</div>";
attachDrag(card);
return card;
}
function render() {
// remove existing cards (keep empty state node)
var cards = deck.querySelectorAll(".card");
cards.forEach(function (c) { c.remove(); });
// render up to 3 cards from top of queue (queue end = topmost)
var count = queue.length;
var visible = Math.min(3, count);
for (var i = 0; i < visible; i++) {
var profile = queue[count - 1 - i];
var card = buildCard(profile);
// insert so topmost is last in DOM order (higher paint, but z-index handles it)
deck.insertBefore(card, emptyEl);
if (i === 0) card.classList.add("top");
else if (i === 1) card.classList.add("next");
else card.classList.add("later");
requestAnimationFrame(function (c) {
return function () { c.classList.add("stack-anim"); };
}(card));
}
remainingEl.textContent = count;
emptyEl.hidden = count > 0;
btnRewind.disabled = history.length === 0;
var noCards = count === 0;
btnNope.disabled = noCards;
btnSuper.disabled = noCards;
btnLike.disabled = noCards;
}
function topCard() { return deck.querySelector(".card.top"); }
// ---- drag engine ----
function attachDrag(card) {
var startX = 0, startY = 0, curX = 0, curY = 0, active = false;
function down(e) {
if (!card.classList.contains("top")) return;
active = true;
card.classList.add("dragging");
card.classList.remove("settle");
var p = point(e);
startX = p.x; startY = p.y;
card.setPointerCapture && e.pointerId != null && card.setPointerCapture(e.pointerId);
}
function move(e) {
if (!active) return;
var p = point(e);
curX = p.x - startX;
curY = p.y - startY;
apply(card, curX, curY);
updateStamps(card, curX);
}
function up() {
if (!active) return;
active = false;
card.classList.remove("dragging");
if (curX > THRESHOLD) { flingOut(card, "like"); }
else if (curX < -THRESHOLD) { flingOut(card, "nope"); }
else { springBack(card); }
curX = 0; curY = 0;
}
card.addEventListener("pointerdown", down);
card.addEventListener("pointermove", move);
card.addEventListener("pointerup", up);
card.addEventListener("pointercancel", up);
card.addEventListener("pointerleave", function (e) { if (active && e.pointerType === "mouse") up(); });
}
function point(e) {
if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY };
return { x: e.clientX, y: e.clientY };
}
function apply(card, x, y) {
var rot = Math.max(-MAX_ROT, Math.min(MAX_ROT, x / 12));
card.style.transform = "translate(" + x + "px," + y + "px) rotate(" + rot + "deg)";
}
function updateStamps(card, x) {
var like = card.querySelector(".stamp-like");
var nope = card.querySelector(".stamp-nope");
var likeOp = Math.max(0, Math.min(1, x / THRESHOLD));
var nopeOp = Math.max(0, Math.min(1, -x / THRESHOLD));
like.style.opacity = likeOp;
nope.style.opacity = nopeOp;
}
function springBack(card) {
card.classList.add("settle");
card.style.transform = "translate(0,0) rotate(0deg)";
var like = card.querySelector(".stamp-like");
var nope = card.querySelector(".stamp-nope");
if (like) like.style.opacity = 0;
if (nope) nope.style.opacity = 0;
}
// ---- swipe out ----
function flingOut(card, dir, isSuper) {
var profile = queue.pop();
history.push({ profile: profile, dir: dir });
card.classList.add("settle");
card.classList.remove("top");
var dx = dir === "like" ? window.innerWidth : -window.innerWidth;
var rot = dir === "like" ? 26 : -26;
if (isSuper) {
var sstamp = card.querySelector(".stamp-super");
if (sstamp) sstamp.style.opacity = 1;
card.style.transform = "translateY(-140%) rotate(0deg)";
} else {
card.style.transform = "translate(" + dx + "px," + (-40) + "px) rotate(" + rot + "deg)";
}
card.style.opacity = "0";
// promote the cards behind
promoteStack();
window.setTimeout(function () { card.remove(); finalizeAfterSwipe(); }, 380);
// feedback
if (dir === "like") {
if (isSuper) {
toast("⭐", "Super Liked " + profile.name + "!");
} else if (Math.random() < 0.5) {
toast("💘", "It's a match with " + profile.name + "!");
} else {
toast("💚", "You liked " + profile.name);
}
} else {
toast("👋", "Passed on " + profile.name);
}
}
function promoteStack() {
var cards = deck.querySelectorAll(".card");
cards.forEach(function (c) {
if (c.classList.contains("next")) {
c.classList.remove("next");
c.classList.add("top");
} else if (c.classList.contains("later")) {
c.classList.remove("later");
c.classList.add("next");
}
});
}
function finalizeAfterSwipe() {
// add a fresh 3rd card if available and not already rendered
var rendered = deck.querySelectorAll(".card").length;
var total = queue.length;
if (total > rendered) {
var idx = total - 1 - rendered; // next unrendered from top
var profile = queue[idx];
var card = buildCard(profile);
deck.insertBefore(card, deck.firstChild);
card.classList.add("later");
requestAnimationFrame(function () { card.classList.add("stack-anim"); });
}
remainingEl.textContent = queue.length;
emptyEl.hidden = queue.length > 0;
btnRewind.disabled = history.length === 0;
var noCards = queue.length === 0;
btnNope.disabled = noCards;
btnSuper.disabled = noCards;
btnLike.disabled = noCards;
}
// ---- button-driven swipe ----
function programmaticSwipe(dir, isSuper) {
var card = topCard();
if (!card) return;
flingOut(card, dir, isSuper);
}
function rewind() {
if (history.length === 0) return;
var last = history.pop();
queue.push(last.profile);
render();
var top = topCard();
if (top) {
top.classList.add("settle");
top.style.transform = "translate(0,-16px) scale(1.02)";
requestAnimationFrame(function () {
top.style.transform = "translate(0,0) scale(1)";
});
}
toast("↺", "Brought back " + last.profile.name);
}
function resetDeck() {
queue = PROFILES.slice();
history = [];
render();
toast("✨", "Deck reshuffled");
}
// ---- toast ----
function toast(emoji, msg) {
if (!toastEl) return;
toastEl.innerHTML = '<span class="toast-emoji">' + emoji + "</span>" + msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("show");
}, 1900);
}
function pulse(btn) {
btn.classList.remove("pulse");
void btn.offsetWidth;
btn.classList.add("pulse");
}
// ---- wire up ----
btnNope.addEventListener("click", function () { pulse(btnNope); programmaticSwipe("nope"); });
btnLike.addEventListener("click", function () { pulse(btnLike); programmaticSwipe("like"); });
btnSuper.addEventListener("click", function () { pulse(btnSuper); programmaticSwipe("like", true); });
btnRewind.addEventListener("click", function () { pulse(btnRewind); rewind(); });
btnReset.addEventListener("click", resetDeck);
// keyboard shortcuts
document.addEventListener("keydown", function (e) {
if (e.key === "ArrowLeft") { programmaticSwipe("nope"); }
else if (e.key === "ArrowRight") { programmaticSwipe("like"); }
else if (e.key === "ArrowUp") { programmaticSwipe("like", true); }
else if (e.key.toLowerCase() === "r") { rewind(); }
});
// init
queue = PROFILES.slice();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dating — Swipe Deck</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Poppins:wght@600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="phone" aria-label="Dating app swipe deck">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">♥</span>
<span class="brand-name">Sparkly</span>
</div>
<p class="counter" aria-live="polite"><span id="remaining">6</span> nearby</p>
</header>
<section class="deck" id="deck" aria-label="Profile cards">
<!-- cards injected by script.js -->
<div class="empty" id="empty" hidden>
<div class="empty-emoji" aria-hidden="true">🌙</div>
<h2>You're all caught up</h2>
<p>No more people around you right now. Check back soon or widen your search.</p>
<button class="btn-reset" id="reset" type="button">Reshuffle deck</button>
</div>
</section>
<nav class="actions" aria-label="Swipe actions">
<button class="act act-rewind" id="rewind" type="button" aria-label="Rewind last profile" title="Rewind">↺</button>
<button class="act act-nope" id="nope" type="button" aria-label="Pass on this profile" title="Nope">✕</button>
<button class="act act-super" id="super" type="button" aria-label="Super like this profile" title="Super Like">★</button>
<button class="act act-like" id="like" type="button" aria-label="Like this profile" title="Like">♥</button>
</nav>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A mobile-app-style swipe deck rendered inside a centered ~430px phone frame. Each profile card fills the frame with a soft gradient portrait, a gradient scrim at the bottom, the person’s name and age, a one-line bio, a distance badge, and a row of interest chips. Cards are physically draggable: press and drag with a mouse or finger and the top card follows your pointer, rotating slightly and fading in a green LIKE stamp to the right or a coral NOPE stamp to the left. Release past the threshold to fling it off-screen; release short and it springs back with an elastic ease.
Below the deck sits a row of circular action buttons — rewind, nope, super-like, and like — that trigger the same animations programmatically, so the deck is fully usable without gestures and keyboard-friendly. A rewind button restores the last dismissed profile, and a live counter tracks how many people are left. Every like has a chance to surface a celebratory match toast.
When the last card leaves the stack, a warm empty state fades in with a reset button to reshuffle the deck. The whole thing is vanilla JS with a small pointer-events engine, spring-back physics, and no dependencies — drop the three files in and it just runs.