Dating — Landing Casual
A playful casual-dating landing wrapped in a mobile phone frame — gradient hero with a live nearby-tonight counter, swipeable spotlight profile cards, animated vibe filters, social proof stats, and a big signup CTA with inline email validation and toast feedback.
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;
--shadow: 0 18px 50px rgba(124, 58, 237, 0.18);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1000px 600px at 15% -5%, #ffe3e8 0%, transparent 60%),
radial-gradient(900px 600px at 90% 10%, #ece0ff 0%, transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
h1, h2 { font-family: "Baloo 2", "Inter", sans-serif; margin: 0; letter-spacing: -0.01em; }
.stage {
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px 16px;
}
/* PHONE FRAME */
.phone {
width: 100%;
max-width: 430px;
background: #1a0f1e;
border-radius: 46px;
padding: 12px;
box-shadow: var(--shadow), 0 0 0 2px rgba(255, 255, 255, 0.06) inset;
position: relative;
}
.notch {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
width: 118px;
height: 26px;
background: #1a0f1e;
border-radius: 0 0 16px 16px;
z-index: 5;
}
.screen {
background: var(--bg);
border-radius: 36px;
overflow: hidden;
height: min(84vh, 860px);
overflow-y: auto;
scrollbar-width: none;
}
.screen::-webkit-scrollbar { display: none; }
/* HERO */
.hero {
background: var(--grad);
color: var(--white);
padding: 46px 22px 30px;
border-radius: 0 0 34px 34px;
position: relative;
overflow: hidden;
}
.hero::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(320px 180px at 85% 0%, rgba(255, 255, 255, 0.28), transparent 70%);
pointer-events: none;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
position: relative;
z-index: 1;
}
.logo { font-weight: 800; font-size: 20px; display: inline-flex; align-items: center; gap: 6px; }
.logo-mark { filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .25)); }
.ghost-btn {
font: inherit;
font-weight: 600;
color: var(--white);
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.4);
padding: 8px 16px;
border-radius: 999px;
cursor: pointer;
transition: background .18s, transform .12s;
}
.ghost-btn:hover { background: rgba(255, 255, 255, 0.28); }
.ghost-btn:active { transform: scale(.96); }
.hero-live {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
position: relative;
z-index: 1;
backdrop-filter: blur(4px);
}
.hero-live strong { font-weight: 800; }
.dot {
width: 9px; height: 9px; border-radius: 50%;
background: #7dffb0;
box-shadow: 0 0 0 0 rgba(125, 255, 176, 0.7);
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(125, 255, 176, 0.7); }
70% { box-shadow: 0 0 0 9px rgba(125, 255, 176, 0); }
100% { box-shadow: 0 0 0 0 rgba(125, 255, 176, 0); }
}
.hero-title { font-size: 44px; line-height: 1.02; margin: 16px 0 10px; font-weight: 800; position: relative; z-index: 1; }
.grad-text {
background: linear-gradient(90deg, #fff, #ffe08a);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero-sub { margin: 0 0 18px; font-size: 15px; max-width: 30ch; color: rgba(255, 255, 255, 0.92); position: relative; z-index: 1; }
.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; padding: 0; margin: 0 0 22px; position: relative; z-index: 1; }
.chip {
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.28);
padding: 7px 12px;
border-radius: 999px;
font-size: 12.5px;
font-weight: 600;
}
.cta-primary {
display: inline-block;
text-decoration: none;
font: inherit;
font-weight: 700;
font-size: 16px;
color: var(--coral-d);
background: var(--white);
border: none;
padding: 15px 26px;
border-radius: 999px;
cursor: pointer;
position: relative;
z-index: 1;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);
transition: transform .14s, box-shadow .14s;
}
.cta-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 30px rgba(0, 0, 0, 0.24); }
.cta-primary:active { transform: translateY(0) scale(.98); }
.hero-note { font-size: 12px; margin: 12px 0 0; color: rgba(255, 255, 255, 0.85); position: relative; z-index: 1; }
/* SECTIONS */
.section { padding: 26px 22px 4px; }
.section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.section-head h2 { font-size: 22px; font-weight: 800; }
.spot-nav { display: flex; gap: 8px; }
.round-btn {
width: 38px; height: 38px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--surface);
color: var(--violet-d);
font-size: 20px;
font-weight: 700;
cursor: pointer;
display: grid; place-items: center;
transition: transform .12s, background .18s, color .18s;
}
.round-btn:hover { background: var(--violet); color: #fff; }
.round-btn:active { transform: scale(.9); }
/* SPOTLIGHT CARD */
.spotlight { position: relative; }
.spot-card {
border-radius: var(--r-lg);
overflow: hidden;
position: relative;
aspect-ratio: 4 / 5;
color: #fff;
box-shadow: 0 12px 30px rgba(42, 26, 46, 0.16);
display: flex;
align-items: flex-end;
animation: cardIn .38s cubic-bezier(.22, 1, .36, 1);
}
@keyframes cardIn { from { opacity: 0; transform: translateY(12px) scale(.98); } to { opacity: 1; transform: none; } }
.spot-card::after {
content: "";
position: absolute; inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(20, 8, 24, 0.78) 100%);
}
.spot-badge {
position: absolute; top: 14px; left: 14px;
background: rgba(255, 255, 255, 0.9);
color: var(--coral-d);
font-size: 11.5px; font-weight: 700;
padding: 5px 11px; border-radius: 999px;
z-index: 2;
display: inline-flex; align-items: center; gap: 5px;
}
.spot-dist {
position: absolute; top: 14px; right: 14px;
background: rgba(20, 8, 24, 0.5);
backdrop-filter: blur(4px);
color: #fff;
font-size: 11.5px; font-weight: 600;
padding: 5px 11px; border-radius: 999px;
z-index: 2;
}
.spot-info { position: relative; z-index: 2; padding: 18px; width: 100%; }
.spot-name { font-family: "Baloo 2", sans-serif; font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 7px; }
.spot-verified { font-size: 13px; }
.spot-bio { font-size: 13px; margin: 3px 0 10px; color: rgba(255, 255, 255, 0.9); }
.spot-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.spot-tags span {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 4px 9px; border-radius: 999px; font-size: 11px; font-weight: 600;
}
.spot-like {
position: absolute; bottom: 16px; right: 16px; z-index: 3;
width: 50px; height: 50px; border-radius: 50%;
border: none; cursor: pointer;
background: var(--grad);
color: #fff; font-size: 22px;
display: grid; place-items: center;
box-shadow: 0 8px 18px rgba(230, 57, 80, 0.45);
transition: transform .14s;
}
.spot-like:hover { transform: scale(1.08); }
.spot-like:active { transform: scale(.88); }
.spot-like.liked { animation: pop .4s ease; }
@keyframes pop { 0% { transform: scale(1); } 45% { transform: scale(1.35); } 100% { transform: scale(1); } }
.dots { display: flex; gap: 6px; justify-content: center; margin-top: 12px; }
.dots button {
width: 7px; height: 7px; border-radius: 999px; border: none;
background: var(--line); cursor: pointer; padding: 0;
transition: width .2s, background .2s;
}
.dots button.on { width: 20px; background: var(--coral); }
/* VIBES */
.vibes { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
.vibes::-webkit-scrollbar { display: none; }
.vibe {
flex: 0 0 auto;
font: inherit; font-weight: 600; font-size: 13.5px;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line);
padding: 9px 15px; border-radius: 999px; cursor: pointer;
transition: all .18s;
white-space: nowrap;
}
.vibe:hover { border-color: var(--violet); color: var(--violet-d); }
.vibe.is-active { background: var(--grad); color: #fff; border-color: transparent; box-shadow: 0 6px 16px rgba(139, 92, 246, 0.3); }
.matches { list-style: none; padding: 0; margin: 16px 0 0; display: grid; gap: 10px; }
.match {
display: flex; align-items: center; gap: 12px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 11px 13px;
transition: transform .16s, box-shadow .16s;
animation: rowIn .32s ease both;
}
.match:hover { transform: translateY(-2px); box-shadow: 0 10px 22px rgba(42, 26, 46, 0.09); }
@keyframes rowIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.m-avatar {
width: 46px; height: 46px; border-radius: 14px; flex: 0 0 auto;
display: grid; place-items: center; font-size: 20px; color: #fff; font-weight: 700;
position: relative;
}
.m-avatar .on-dot {
position: absolute; bottom: -1px; right: -1px;
width: 12px; height: 12px; border-radius: 50%;
background: #22c55e; border: 2px solid var(--surface);
}
.m-body { flex: 1; min-width: 0; }
.m-name { font-weight: 700; font-size: 14.5px; display: flex; align-items: center; gap: 6px; }
.m-meta { font-size: 12.5px; color: var(--muted); }
.m-match {
font-size: 12px; font-weight: 700; color: var(--coral-d);
background: #ffe3e8; padding: 5px 10px; border-radius: 999px; flex: 0 0 auto;
}
/* STATS */
.stats-wrap { padding-top: 30px; }
.stats {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 12px;
box-shadow: 0 10px 26px rgba(42, 26, 46, 0.06);
}
.stat { text-align: center; }
.stat-num {
display: block; font-family: "Baloo 2", sans-serif; font-weight: 800; font-size: 24px;
background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent;
}
.stat-label { font-size: 11.5px; color: var(--muted); font-weight: 500; }
/* SIGNUP */
.signup { padding-top: 30px; }
.signup-card {
background: var(--grad);
color: #fff;
border-radius: var(--r-lg);
padding: 26px 22px;
text-align: center;
position: relative;
overflow: hidden;
}
.signup-card::before {
content: ""; position: absolute; inset: 0;
background: radial-gradient(260px 160px at 20% 0%, rgba(255, 255, 255, 0.25), transparent 70%);
}
.signup-card h2 { font-size: 24px; font-weight: 800; position: relative; }
.signup-card > p { font-size: 14px; margin: 8px 0 18px; color: rgba(255, 255, 255, 0.92); position: relative; }
.field { position: relative; text-align: left; margin-bottom: 12px; }
.field input {
width: 100%; font: inherit; font-size: 15px;
padding: 14px 16px; border-radius: 999px;
border: 2px solid transparent; background: rgba(255, 255, 255, 0.95); color: var(--ink);
outline: none;
}
.field input::placeholder { color: var(--muted); }
.field.invalid input { border-color: #ffd24d; animation: shake .4s; }
@keyframes shake { 0%, 100% { transform: translateX(0); } 20%, 60% { transform: translateX(-6px); } 40%, 80% { transform: translateX(6px); } }
.err { display: block; min-height: 15px; font-size: 12px; font-weight: 600; color: #fff4c2; padding: 4px 14px 0; }
.block { display: block; width: 100%; }
.avatars { display: flex; align-items: center; justify-content: center; gap: 0; margin-top: 16px; position: relative; }
.av {
width: 32px; height: 32px; border-radius: 50%; border: 2px solid #fff; margin-left: -8px;
}
.av:first-child { margin-left: 0; }
.a1 { background: linear-gradient(135deg, #ff8fb1, #ff5e6c); }
.a2 { background: linear-gradient(135deg, #8b5cf6, #6d28d9); }
.a3 { background: linear-gradient(135deg, #fbbf24, #f97316); }
.a4 { background: linear-gradient(135deg, #34d399, #059669); }
.av-more { font-size: 12.5px; font-weight: 600; margin-left: 10px; }
.foot { text-align: center; font-size: 12px; color: var(--muted); padding: 26px 22px 30px; }
.foot span { font-weight: 700; color: var(--ink-2); }
/* FOCUS */
a:focus-visible, button:focus-visible, input:focus-visible {
outline: 3px solid var(--violet);
outline-offset: 2px;
}
.signup-card input:focus-visible, .cta-primary:focus-visible { outline-color: #ffe08a; }
/* TOAST */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(24px);
background: var(--ink); color: #fff;
padding: 13px 20px; border-radius: 999px;
font-size: 14px; font-weight: 600;
box-shadow: 0 14px 34px rgba(42, 26, 46, 0.35);
opacity: 0; pointer-events: none; transition: opacity .25s, transform .25s;
z-index: 50; max-width: 90vw;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (max-width: 520px) {
.stage { padding: 0; }
.phone { max-width: 100%; border-radius: 0; padding: 0; box-shadow: none; }
.notch { display: none; }
.screen { border-radius: 0; height: auto; min-height: 100vh; overflow-y: visible; }
.hero { padding-top: 26px; }
.hero-title { font-size: 40px; }
.stat-num { font-size: 21px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
/* ---------- live count ticker ---------- */
var liveEl = document.getElementById("liveCount");
var live = 1284;
if (liveEl) {
setInterval(function () {
live += Math.floor(Math.random() * 7) - 2; // drift up-ish
if (live < 1180) live = 1180;
liveEl.textContent = live.toLocaleString();
}, 2200);
}
/* ---------- spotlight profiles ---------- */
var profiles = [
{
name: "Mia", age: 26, dist: "1.2 mi", badge: "🔥 Active now",
bio: "Rooftop cocktails > small talk. Show me your worst dad joke.",
tags: ["Cocktails", "Live music", "Spontaneous"],
grad: "linear-gradient(135deg,#ff5e6c,#ff8fb1)"
},
{
name: "Leo", age: 29, dist: "0.8 mi", badge: "✨ New here",
bio: "Taco crawl tonight? I know a place. Bring your appetite.",
tags: ["Foodie", "Vinyl", "Night owl"],
grad: "linear-gradient(135deg,#8b5cf6,#6d28d9)"
},
{
name: "Priya", age: 24, dist: "2.1 mi", badge: "🔥 Active now",
bio: "Trivia champ looking for a partner in (harmless) crime.",
tags: ["Trivia", "Coffee", "Adventures"],
grad: "linear-gradient(135deg,#fbbf24,#f97316)"
},
{
name: "Sam", age: 31, dist: "1.5 mi", badge: "💜 Popular",
bio: "Late-night ramen and even later conversations. You in?",
tags: ["Ramen", "Films", "No-strings"],
grad: "linear-gradient(135deg,#34d399,#0ea5e9)"
}
];
var spotlight = document.getElementById("spotlight");
var spotDots = document.getElementById("spotDots");
var idx = 0;
function renderSpot() {
var p = profiles[idx];
spotlight.innerHTML =
'<article class="spot-card" style="background:' + p.grad + '">' +
'<span class="spot-badge">' + p.badge + '</span>' +
'<span class="spot-dist">📍 ' + p.dist + '</span>' +
'<div class="spot-info">' +
'<div class="spot-name">' + p.name + ", " + p.age +
' <span class="spot-verified" title="Verified">✔️</span></div>' +
'<p class="spot-bio">' + p.bio + "</p>" +
'<div class="spot-tags">' + p.tags.map(function (t) {
return "<span>" + t + "</span>";
}).join("") + "</div>" +
"</div>" +
'<button class="spot-like" type="button" aria-label="Like ' + p.name + '">💗</button>' +
"</article>";
var likeBtn = spotlight.querySelector(".spot-like");
likeBtn.addEventListener("click", function () {
likeBtn.classList.remove("liked");
void likeBtn.offsetWidth; // reflow to restart animation
likeBtn.classList.add("liked");
toast("You liked " + p.name + " — they'll see it tonight 💗");
});
renderDots();
}
function renderDots() {
spotDots.innerHTML = profiles.map(function (_, i) {
return '<button type="button" role="tab" aria-label="Profile ' + (i + 1) +
'" class="' + (i === idx ? "on" : "") + '"></button>';
}).join("");
}
spotDots.addEventListener("click", function (e) {
var btns = Array.prototype.slice.call(spotDots.children);
var i = btns.indexOf(e.target);
if (i > -1) { idx = i; renderSpot(); }
});
document.getElementById("spotNext").addEventListener("click", function () {
idx = (idx + 1) % profiles.length; renderSpot();
});
document.getElementById("spotPrev").addEventListener("click", function () {
idx = (idx - 1 + profiles.length) % profiles.length; renderSpot();
});
renderSpot();
/* ---------- vibe filters + match list ---------- */
var allMatches = [
{ name: "Chloe", meta: "26 · 1.1 mi · online", match: 96, vibe: "tonight", emoji: "🍸", g: "linear-gradient(135deg,#ff5e6c,#ff8fb1)" },
{ name: "Deshawn", meta: "30 · 0.6 mi · online", match: 91, vibe: "tonight", emoji: "🎧", g: "linear-gradient(135deg,#8b5cf6,#6d28d9)" },
{ name: "Ana", meta: "27 · 2.4 mi · online", match: 88, vibe: "coffee", emoji: "☕", g: "linear-gradient(135deg,#fbbf24,#f97316)" },
{ name: "Ravi", meta: "28 · 1.9 mi · online", match: 84, vibe: "coffee", emoji: "📚", g: "linear-gradient(135deg,#34d399,#059669)" },
{ name: "Nadia", meta: "25 · 3.0 mi · online", match: 93, vibe: "adventure", emoji: "🎢", g: "linear-gradient(135deg,#0ea5e9,#6366f1)" },
{ name: "Theo", meta: "32 · 1.3 mi · online", match: 79, vibe: "adventure", emoji: "🏔️", g: "linear-gradient(135deg,#f43f5e,#a855f7)" },
{ name: "Jules", meta: "29 · 0.9 mi · online", match: 90, vibe: "nostrings", emoji: "🎈", g: "linear-gradient(135deg,#ec4899,#8b5cf6)" },
{ name: "Kai", meta: "27 · 2.2 mi · online", match: 82, vibe: "nostrings", emoji: "🌙", g: "linear-gradient(135deg,#22d3ee,#3b82f6)" }
];
var matchesEl = document.getElementById("matches");
var vibes = document.getElementById("vibes");
function renderMatches(vibe) {
var list = allMatches.filter(function (m) { return m.vibe === vibe; });
// pad with a top-tonight pick so every vibe shows 3
var extra = allMatches.filter(function (m) { return m.vibe !== vibe; })
.sort(function (a, b) { return b.match - a.match; }).slice(0, 3 - list.length);
list = list.concat(extra).slice(0, 3);
matchesEl.innerHTML = list.map(function (m, i) {
return '<li class="match" style="animation-delay:' + (i * 60) + 'ms">' +
'<span class="m-avatar" style="background:' + m.g + '">' + m.emoji +
'<span class="on-dot"></span></span>' +
'<span class="m-body">' +
'<span class="m-name">' + m.name + '</span>' +
'<span class="m-meta">' + m.meta + '</span>' +
'</span>' +
'<span class="m-match">' + m.match + '% match</span>' +
'</li>';
}).join("");
}
vibes.addEventListener("click", function (e) {
var btn = e.target.closest(".vibe");
if (!btn) return;
Array.prototype.forEach.call(vibes.children, function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
renderMatches(btn.dataset.vibe);
});
renderMatches("tonight");
/* ---------- count-up stats on scroll ---------- */
function formatNum(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
if (n >= 1000) return Math.round(n / 1000) + "K";
return String(n);
}
function countUp(el) {
var to = parseInt(el.dataset.to, 10);
var suffix = el.dataset.suffix || "";
var start = performance.now();
var dur = 1400;
function step(now) {
var p = Math.min((now - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
var val = Math.round(to * eased);
el.textContent = (to >= 1000 ? formatNum(val) : val) + suffix;
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
var statsWrap = document.getElementById("stats");
if ("IntersectionObserver" in window && statsWrap) {
var seen = false;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting && !seen) {
seen = true;
statsWrap.querySelectorAll(".stat-num").forEach(countUp);
io.disconnect();
}
});
}, { threshold: 0.4 });
io.observe(statsWrap);
} else if (statsWrap) {
statsWrap.querySelectorAll(".stat-num").forEach(countUp);
}
/* ---------- signup form ---------- */
var form = document.getElementById("signupForm");
var email = document.getElementById("email");
var field = email.closest(".field");
var errEl = document.getElementById("emailErr");
var joinBtn = document.getElementById("joinBtn");
var re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
form.addEventListener("submit", function (e) {
e.preventDefault();
var val = email.value.trim();
if (!re.test(val)) {
field.classList.remove("invalid");
void field.offsetWidth;
field.classList.add("invalid");
errEl.textContent = val ? "Hmm, that email looks off." : "Enter your email to join.";
email.focus();
return;
}
field.classList.remove("invalid");
errEl.textContent = "";
joinBtn.textContent = "You're in! 🎉";
joinBtn.disabled = true;
toast("Welcome! Finding people near you tonight… 💗");
setTimeout(function () {
joinBtn.textContent = "Join free tonight";
joinBtn.disabled = false;
email.value = "";
}, 3200);
});
email.addEventListener("input", function () {
if (field.classList.contains("invalid") && re.test(email.value.trim())) {
field.classList.remove("invalid");
errEl.textContent = "";
}
});
/* ---------- login button ---------- */
document.getElementById("loginBtn").addEventListener("click", function () {
toast("Log in coming soon — join free for now! ✨");
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tonight — Casual Dating, Made Fun</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=Baloo+2:wght@600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="stage">
<div class="phone" role="region" aria-label="Tonight dating app landing screen">
<div class="notch" aria-hidden="true"></div>
<div class="screen">
<!-- HERO -->
<header class="hero">
<div class="topbar">
<span class="logo"><span class="logo-mark" aria-hidden="true">💗</span> Tonight</span>
<button class="ghost-btn" type="button" id="loginBtn">Log in</button>
</div>
<div class="hero-live">
<span class="dot" aria-hidden="true"></span>
<span><strong id="liveCount">1,284</strong> people online near you</span>
</div>
<h1 class="hero-title">Meet someone<br /><span class="grad-text">tonight.</span></h1>
<p class="hero-sub">No essays. No games. Just real people nearby who are down to hang, right now.</p>
<ul class="chips" aria-label="What Tonight offers">
<li class="chip">⚡ Instant matches</li>
<li class="chip">📍 2 miles away</li>
<li class="chip">🍸 Meet tonight</li>
<li class="chip">🙈 No pressure</li>
</ul>
<a href="#signup" class="cta-primary">Get started — it's free</a>
<p class="hero-note">Free to join · 18+ · Delete anytime</p>
</header>
<!-- SPOTLIGHT -->
<section class="section" aria-label="Nearby spotlight">
<div class="section-head">
<h2>Nearby right now</h2>
<div class="spot-nav">
<button class="round-btn" id="spotPrev" type="button" aria-label="Previous profile">‹</button>
<button class="round-btn" id="spotNext" type="button" aria-label="Next profile">›</button>
</div>
</div>
<div class="spotlight" id="spotlight" aria-live="polite"></div>
<div class="dots" id="spotDots" role="tablist" aria-label="Spotlight profiles"></div>
</section>
<!-- VIBE FILTERS -->
<section class="section" aria-label="Find your vibe">
<div class="section-head"><h2>Pick your vibe</h2></div>
<div class="vibes" id="vibes" role="tablist" aria-label="Filter matches by vibe">
<button class="vibe is-active" role="tab" aria-selected="true" data-vibe="tonight" type="button">🔥 Tonight</button>
<button class="vibe" role="tab" aria-selected="false" data-vibe="coffee" type="button">☕ Coffee</button>
<button class="vibe" role="tab" aria-selected="false" data-vibe="adventure" type="button">🎢 Adventure</button>
<button class="vibe" role="tab" aria-selected="false" data-vibe="nostrings" type="button">🎈 No-strings</button>
</div>
<ul class="matches" id="matches" aria-live="polite"></ul>
</section>
<!-- STATS -->
<section class="section stats-wrap" aria-label="Community stats">
<div class="stats" id="stats">
<div class="stat"><span class="stat-num" data-to="4200000">0</span><span class="stat-label">Matches made</span></div>
<div class="stat"><span class="stat-num" data-to="93" data-suffix="%">0</span><span class="stat-label">Meet within 48h</span></div>
<div class="stat"><span class="stat-num" data-to="120" data-suffix="+">0</span><span class="stat-label">Cities live</span></div>
</div>
</section>
<!-- SIGNUP -->
<section class="section signup" id="signup" aria-label="Sign up">
<div class="signup-card">
<h2>Ready to make tonight count?</h2>
<p>Drop your email and we'll find people near you in seconds.</p>
<form id="signupForm" novalidate>
<div class="field">
<input id="email" type="email" inputmode="email" autocomplete="email"
placeholder="[email protected]" aria-label="Email address" required />
<span class="err" id="emailErr" role="alert"></span>
</div>
<button class="cta-primary block" id="joinBtn" type="submit">Join free tonight</button>
</form>
<div class="avatars" aria-hidden="true">
<span class="av a1"></span><span class="av a2"></span><span class="av a3"></span><span class="av a4"></span>
<span class="av-more">+2.4k joined today</span>
</div>
</div>
</section>
<footer class="foot">
<span>Tonight</span> · Made for fun, not forever · © 2026
</footer>
</div>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A high-energy landing page for a casual, meet-tonight style dating app, rendered inside a centered ~430px phone frame so it reads like a real product screen. The gradient hero leads with a nearby/tonight angle: a live counter ticks up the number of people online near you, chips advertise instant matches, and a pill CTA drops you straight into the email signup. Below the fold, a spotlight rail shows fictional profiles you can flip through, and a set of vibe filters (Tonight, Coffee, Adventure, No-strings) re-rank a mini match list with a smooth animation.
The signup card validates the email inline, shakes on an invalid entry, and fires a celebratory toast on success while the CTA morphs into a confirmation state. Social-proof stats count up when scrolled into view, and every card has hover, active, and keyboard-focus states.
All interactions are vanilla JS — a small toast() helper, an IntersectionObserver for the count-up stats, and event delegation for the filter chips and spotlight navigation. Fully responsive down to 360px, WCAG AA contrast, and keyboard-usable controls with visible focus rings.