mirror of
https://github.com/hanzo-apps/waitlist.git
synced 2026-10-30 14:48:32 +00:00
Adds real social proof and a glossy depth pass on top of the existing
brand-neutral monochrome. Every new piece is configurable end-to-end.
Activity feed
• Mock-api: GET /v1/waitlist/activity?waitlist=&limit=&types= returning
most-recent events with a server-now timestamp so clients render
consistent relative times across clock skew. Ring-buffer last 200
events (env override: ACTIVITY_MAX_HISTORY). Cap on limit: 100
(env: ACTIVITY_LIMIT_CAP). Types filter (join,share,invite,referral)
is server-validated, unknown types silently dropped.
• join handler pushes a `join` event + a `referral` event when the
sender's refCode resolves. track-share pushes a `share` event the
same day-it-was-credited (skipped on duplicate same-day to match
the daily-cap economy). invite pushes one event per accepted email.
• Seeder synthesizes 30 backdated events on first boot so the ticker
is alive on first paint, not waiting on real traffic.
• Widget: <WaitlistActivity waitlist baseUrl limit types pollInterval
formatEvent formatRelative showHeading heading emptyLabel theme
className style /> — every visible knob is a prop. Polling interval
is configurable (0 disables, default 12s). Default formatter handles
copy/webshare/email/sms with natural copy ("copied their link",
"shared via system", "shared by email", "texted their link"); other
platforms get a clean platformLabel mapping (twitter→X, etc.).
Animated count-up
• useCountUp(target, { duration, ease, retrigger }) hook — 1200ms
cubic-out by default, honors prefers-reduced-motion, 0 disables.
• <WaitlistLeaderboard animateTotal={true | false | { duration }}/>
runs the count-up on the "N on the list" subtitle when the meta
first lands. Truly off-switchable.
Glossy aesthetic (all monochrome — no new color tokens)
• Cards: multi-layer shadow + a 1px inner highlight on the top edge
using inset color-mix(--hw-bg). Reads as real material without
breaking neutrality.
• Leaderboard rows: 6px radius, translateX(2px) on hover (skipped
under prefers-reduced-motion).
• Activity pulse: 8px accent dot with a soft expanding ring keyframe
(1.8s loop). Reduced-motion turns it off.
• Demo hero: subtle radial gradient halo via .section--glow::before
using color-mix(--fg 6%) to stay neutral. Disabled when the user
prefers reduced transparency.
• Topbar Join pill: inset top highlight + accentuated drop shadow
on hover for that glossy CTA look.
• Two-column board grid on desktop (leaderboard + Live activity),
stacks on <=880px.
• OpenGraph + Twitter card metadata on the demo for proper social
previews when someone tweets the page URL.
418 lines
8.4 KiB
CSS
418 lines
8.4 KiB
CSS
/* Demo site — Hanzo monochrome (Radix gray). */
|
|
|
|
:root {
|
|
--bg: #fcfcfc;
|
|
--bg-2: #f9f9f9;
|
|
--bg-3: #f0f0f0;
|
|
--fg: #202020;
|
|
--muted: #646464;
|
|
--border: #e8e8e8;
|
|
--border-strong: #d9d9d9;
|
|
|
|
--font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, sans-serif;
|
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
|
|
--container: 1100px;
|
|
--pad-x: clamp(20px, 4vw, 32px);
|
|
--pad-y: clamp(56px, 8vw, 96px);
|
|
--radius: 14px;
|
|
--radius-sm: 8px;
|
|
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #111111;
|
|
--bg-2: #191919;
|
|
--bg-3: #222222;
|
|
--fg: #eeeeee;
|
|
--muted: #b4b4b4;
|
|
--border: #2a2a2a;
|
|
--border-strong: #3a3a3a;
|
|
}
|
|
}
|
|
|
|
*,
|
|
*::before,
|
|
*::after { box-sizing: border-box; }
|
|
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-family: var(--font);
|
|
-webkit-font-smoothing: antialiased;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
|
|
::selection { background: var(--fg); color: var(--bg); }
|
|
|
|
a { color: inherit; text-decoration: none; }
|
|
a:hover { opacity: 0.72; }
|
|
|
|
button:focus-visible,
|
|
a:focus-visible,
|
|
[role="tab"]:focus-visible {
|
|
outline: 2px solid var(--fg);
|
|
outline-offset: 2px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* Container */
|
|
|
|
.container {
|
|
max-width: var(--container);
|
|
margin: 0 auto;
|
|
padding-left: var(--pad-x);
|
|
padding-right: var(--pad-x);
|
|
}
|
|
|
|
/* Sticky topbar with primary Join CTA */
|
|
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
background: color-mix(in srgb, var(--bg) 92%, transparent);
|
|
backdrop-filter: saturate(180%) blur(10px);
|
|
-webkit-backdrop-filter: saturate(180%) blur(10px);
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--fg);
|
|
}
|
|
|
|
.topbar__inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding-top: 14px;
|
|
padding-bottom: 14px;
|
|
}
|
|
|
|
.topbar__brand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: var(--fg);
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.topbar__wordmark {
|
|
font-size: 15px;
|
|
}
|
|
|
|
.topbar__brand:hover { opacity: 1; }
|
|
|
|
.topbar__cta { flex-shrink: 0; }
|
|
|
|
/* Primary CTA pill — used in topbar + FAB. */
|
|
|
|
.cta {
|
|
appearance: none;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.005em;
|
|
padding: 10px 18px;
|
|
border-radius: 999px;
|
|
border: 1px solid transparent;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: transform 120ms ease, background 120ms ease, color 120ms ease, box-shadow 200ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.cta:hover { transform: translateY(-1px); }
|
|
.cta:active { transform: none; }
|
|
|
|
.cta--solid {
|
|
background: var(--hw-accent, var(--fg));
|
|
color: var(--hw-accent-fg, var(--bg));
|
|
/* Subtle inner highlight on the top edge for that glossy feel. */
|
|
box-shadow:
|
|
inset 0 1px 0 0 color-mix(in srgb, var(--hw-accent-fg, var(--bg)) 18%, transparent),
|
|
0 1px 2px 0 color-mix(in srgb, var(--fg) 12%, transparent);
|
|
}
|
|
|
|
.cta--solid:hover {
|
|
box-shadow:
|
|
inset 0 1px 0 0 color-mix(in srgb, var(--hw-accent-fg, var(--bg)) 22%, transparent),
|
|
0 8px 22px 0 color-mix(in srgb, var(--fg) 22%, transparent);
|
|
}
|
|
|
|
.cta__rank {
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.cta__sep {
|
|
opacity: 0.45;
|
|
margin: 0 -2px;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.cta { transition: none; }
|
|
.cta:hover { transform: none; }
|
|
}
|
|
|
|
/* Sections */
|
|
|
|
.section {
|
|
padding-top: clamp(40px, 6vw, 72px);
|
|
padding-bottom: clamp(40px, 6vw, 72px);
|
|
}
|
|
|
|
.section--first {
|
|
/* compensate for sticky topbar so the headline isn't tucked under it */
|
|
padding-top: clamp(32px, 5vw, 56px);
|
|
}
|
|
|
|
/* Glossy radial halo behind the hero — purely additive, monochrome,
|
|
* disabled when the user prefers reduced motion or low contrast. */
|
|
.section--glow {
|
|
position: relative;
|
|
isolation: isolate;
|
|
}
|
|
|
|
.section--glow::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0 0 auto 0;
|
|
height: 520px;
|
|
background:
|
|
radial-gradient(120% 80% at 50% 0%,
|
|
color-mix(in srgb, var(--fg) 6%, transparent) 0%,
|
|
color-mix(in srgb, var(--fg) 0%, transparent) 60%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
mask-image: linear-gradient(180deg, #000 0%, #000 60%, transparent 100%);
|
|
-webkit-mask-image: linear-gradient(180deg, #000 0%, #000 60%, transparent 100%);
|
|
}
|
|
|
|
@media (prefers-reduced-transparency: reduce) {
|
|
.section--glow::before { display: none; }
|
|
}
|
|
|
|
/* Board grid: leaderboard left, live activity right, stack on mobile. */
|
|
.board-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr);
|
|
gap: clamp(16px, 2.4vw, 28px);
|
|
align-items: start;
|
|
}
|
|
|
|
@media (max-width: 880px) {
|
|
.board-grid {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|
|
|
|
.section--alt {
|
|
background: var(--bg-2);
|
|
border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
max-width: none;
|
|
padding-left: 0;
|
|
padding-right: 0;
|
|
}
|
|
|
|
.section--alt > * {
|
|
max-width: var(--container);
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
padding-left: var(--pad-x);
|
|
padding-right: var(--pad-x);
|
|
}
|
|
|
|
/* Section heads */
|
|
|
|
.section-head {
|
|
margin-bottom: clamp(24px, 4vw, 40px);
|
|
}
|
|
|
|
.section-head--center {
|
|
text-align: center;
|
|
}
|
|
|
|
.section-head--center .h1,
|
|
.section-head--center .h2 { margin-left: auto; margin-right: auto; }
|
|
|
|
.h1 {
|
|
font-size: clamp(36px, 6vw, 56px);
|
|
font-weight: 700;
|
|
letter-spacing: -0.035em;
|
|
line-height: 1.02;
|
|
margin: 0 0 14px;
|
|
}
|
|
|
|
.h1--lg {
|
|
font-size: clamp(40px, 7vw, 64px);
|
|
}
|
|
|
|
.h2 {
|
|
font-size: clamp(24px, 3vw, 36px);
|
|
font-weight: 600;
|
|
letter-spacing: -0.025em;
|
|
margin: 0 0 12px;
|
|
}
|
|
|
|
.h2--lg {
|
|
font-size: clamp(28px, 4vw, 44px);
|
|
letter-spacing: -0.03em;
|
|
}
|
|
|
|
.lede {
|
|
font-size: clamp(15px, 1.5vw, 17px);
|
|
color: var(--muted);
|
|
line-height: 1.55;
|
|
margin: 0;
|
|
max-width: 56ch;
|
|
}
|
|
|
|
.lede--center {
|
|
margin: 0 auto;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Join section: 2-col on desktop (intro | widget), stacked on mobile */
|
|
|
|
.join-wrap {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(360px, 440px);
|
|
gap: clamp(24px, 5vw, 64px);
|
|
align-items: start;
|
|
}
|
|
|
|
@media (max-width: 880px) {
|
|
.join-wrap {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
gap: 28px;
|
|
}
|
|
.join-intro { text-align: center; }
|
|
.join-intro .join-bullets { display: inline-block; text-align: left; }
|
|
}
|
|
|
|
.join-intro {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
padding-top: 6px;
|
|
}
|
|
|
|
.join-lede {
|
|
font-size: clamp(15px, 1.4vw, 16px);
|
|
color: var(--muted);
|
|
line-height: 1.55;
|
|
margin: 0;
|
|
max-width: 50ch;
|
|
}
|
|
|
|
.join-bullets {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.join-bullets li {
|
|
font-size: 14px;
|
|
color: var(--muted);
|
|
padding-left: 22px;
|
|
position: relative;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.join-bullets li::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 10px;
|
|
width: 12px;
|
|
height: 1px;
|
|
background: var(--border-strong);
|
|
}
|
|
|
|
.join-bullets li b {
|
|
color: var(--fg);
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.join-widget {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Footer */
|
|
|
|
.footer {
|
|
padding-top: clamp(32px, 5vw, 56px);
|
|
padding-bottom: clamp(32px, 5vw, 56px);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.footer__row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.footer__brand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: var(--fg);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.footer__meta { color: var(--muted); }
|
|
|
|
@media (max-width: 540px) {
|
|
.footer__row { flex-direction: column; text-align: center; gap: 10px; }
|
|
}
|
|
|
|
/* Make the leaderboard truly full-width inside the section container.
|
|
* Inside the join-widget column we want the natural ~440px widget width. */
|
|
.section .hanzo-waitlist { max-width: none; }
|
|
.join-widget .hanzo-waitlist { max-width: 440px; }
|
|
|
|
/* Smooth anchor scroll respecting the sticky topbar. */
|
|
html { scroll-padding-top: 76px; }
|
|
|
|
/* Brand presets — override --hw-* directly on the wrapper. Activated by
|
|
* pressing `b` to cycle through; primarily exists to demonstrate the
|
|
* brand-neutral CSS variable contract. */
|
|
.preset-a {
|
|
--hw-accent: #0a0a0a;
|
|
--hw-accent-fg: #fafafa;
|
|
--hw-radius: 12px;
|
|
}
|
|
|
|
.preset-b {
|
|
--hw-accent: #c9a253;
|
|
--hw-accent-fg: #0a0a0a;
|
|
--hw-radius: 4px;
|
|
}
|
|
|
|
.preset-c {
|
|
--hw-accent: #2a8a3a;
|
|
--hw-accent-fg: #ffffff;
|
|
--hw-radius: 24px;
|
|
}
|
|
|
|
.preset-d {
|
|
--hw-accent: #6a3d8f;
|
|
--hw-accent-fg: #ffffff;
|
|
--hw-radius: 6px;
|
|
}
|