810 lines
28 KiB
JavaScript
810 lines
28 KiB
JavaScript
// Beta signup — invite request flow with submit/confirmed states.
|
||
|
||
function Arrow({ size = 14 }) {
|
||
return (
|
||
<svg className="arrow" width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
function Glow({ color = "var(--accent-glow)", size = 700, opacity = 1, style = {} }) {
|
||
return (
|
||
<div aria-hidden="true" style={{
|
||
position: "absolute", width: size, height: size,
|
||
background: `radial-gradient(circle at center, ${color} 0%, transparent 62%)`,
|
||
filter: "blur(20px)", opacity, pointerEvents: "none", ...style,
|
||
}} />
|
||
);
|
||
}
|
||
|
||
const ROLES = [
|
||
{ value: "smb", label: "Small business owner", hint: "I run a shop, salon, studio, café…" },
|
||
{ value: "freelancer", label: "Freelancer / agency", hint: "I build tools for clients" },
|
||
{ value: "ideaperson", label: "I just have an idea", hint: "First-time builder, no code" },
|
||
];
|
||
|
||
const SOURCES = ["Reddit", "Twitter / X", "TikTok", "YouTube", "A friend", "Google", "Something else"];
|
||
|
||
const BENEFITS = [
|
||
{
|
||
icon: "lightning",
|
||
title: "First access",
|
||
body: "Skip the queue when public beta opens. You build before everyone else.",
|
||
},
|
||
{
|
||
icon: "gift",
|
||
title: "90 days of Pro, free",
|
||
body: "Full launch features — hosting, marketing, customer acquisition — on the house.",
|
||
},
|
||
{
|
||
icon: "chat",
|
||
title: "Direct line to the team",
|
||
body: "Private channel with the people building Vibn. Your feedback ships.",
|
||
},
|
||
];
|
||
|
||
function BetaApp() {
|
||
const [submitted, setSubmitted] = React.useState(false);
|
||
const [submitting, setSubmitting] = React.useState(false);
|
||
const [scrolled, setScrolled] = React.useState(false);
|
||
const [form, setForm] = React.useState({
|
||
email: "",
|
||
name: "",
|
||
build: "",
|
||
role: "smb",
|
||
source: "",
|
||
});
|
||
|
||
React.useEffect(() => {
|
||
const onScroll = () => setScrolled(window.scrollY > 8);
|
||
window.addEventListener("scroll", onScroll, { passive: true });
|
||
return () => window.removeEventListener("scroll", onScroll);
|
||
}, []);
|
||
|
||
const update = (k, v) => setForm((f) => ({ ...f, [k]: v }));
|
||
|
||
const valid = /\S+@\S+\.\S+/.test(form.email) && form.build.trim().length > 4;
|
||
|
||
const handleSubmit = (e) => {
|
||
e.preventDefault();
|
||
if (!valid || submitting) return;
|
||
setSubmitting(true);
|
||
setTimeout(() => {
|
||
setSubmitting(false);
|
||
setSubmitted(true);
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
}, 700);
|
||
};
|
||
|
||
// Stable "queue position" based on email — feels real, deterministic.
|
||
const queuePos = React.useMemo(() => {
|
||
let h = 7;
|
||
for (const c of form.email) h = (h * 31 + c.charCodeAt(0)) >>> 0;
|
||
return 2100 + (h % 900); // 2,100 – 2,999
|
||
}, [form.email]);
|
||
|
||
return (
|
||
<>
|
||
<BetaStyle />
|
||
<nav className={`nav${scrolled ? " scrolled" : ""}`}>
|
||
<div className="wrap nav-inner">
|
||
<a href="index.html" className="logo">
|
||
<span className="logo-mark">
|
||
<svg viewBox="0 0 36 32" width="74%" height="74%" fill="currentColor" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" aria-hidden="true">
|
||
<path d="M4 5 L10 5 L12 18 L14 5 L20 5 L12 27 Z" />
|
||
<rect x="22.5" y="23" width="9.5" height="3.8" rx="0.7" className="logo-caret" />
|
||
</svg>
|
||
</span>
|
||
<span>vibn</span>
|
||
</a>
|
||
<a href="index.html" className="nav-back">
|
||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
||
<path d="M13 8H3M7 4 3 8l4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
Back to home
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<main className="beta-main">
|
||
<Glow color="oklch(0.74 0.175 35 / 0.30)" size={1000}
|
||
style={{ top: "-280px", left: "50%", transform: "translateX(-50%)" }} />
|
||
<Glow color="oklch(0.45 0.10 35 / 0.20)" size={550}
|
||
style={{ top: "30%", left: "-180px" }} />
|
||
<Glow color="oklch(0.45 0.10 35 / 0.15)" size={500}
|
||
style={{ top: "20%", right: "-150px" }} />
|
||
|
||
<div className="wrap beta-wrap">
|
||
{submitted ? (
|
||
<Confirmed form={form} queuePos={queuePos} />
|
||
) : (
|
||
<>
|
||
<header className="beta-head">
|
||
<div className="eyebrow">Closed beta · invite-only</div>
|
||
<h1 className="beta-title">
|
||
Be one of the first to <em>vibe with Vibn</em>.
|
||
</h1>
|
||
<p className="beta-sub">
|
||
We're letting in <b>50 new builders a week</b>.
|
||
Tell us what you want to build — the most exciting ideas get the invite first.
|
||
</p>
|
||
</header>
|
||
|
||
<form className="beta-form" onSubmit={handleSubmit} noValidate>
|
||
<Field
|
||
label="01"
|
||
title="What's your email?"
|
||
hint="So we can send you the invite when it's your turn."
|
||
>
|
||
<input
|
||
type="email" required
|
||
className="f-input"
|
||
value={form.email}
|
||
onChange={(e) => update("email", e.target.value)}
|
||
placeholder="you@somewhere.com"
|
||
autoComplete="email"
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="02" title="What should we call you?" hint="Optional, but nice to know.">
|
||
<input
|
||
type="text"
|
||
className="f-input"
|
||
value={form.name}
|
||
onChange={(e) => update("name", e.target.value)}
|
||
placeholder="First name or handle"
|
||
autoComplete="given-name"
|
||
/>
|
||
</Field>
|
||
|
||
<Field
|
||
label="03"
|
||
title="What's the first thing you want to build?"
|
||
hint="Free-form. The vibe matters more than the spec."
|
||
required
|
||
>
|
||
<div className="f-prompt">
|
||
<textarea
|
||
className="f-textarea"
|
||
value={form.build}
|
||
onChange={(e) => update("build", e.target.value)}
|
||
placeholder="A booking site for my dog grooming business with reminders, payments and a wait list…"
|
||
rows={4}
|
||
/>
|
||
<div className="f-prompt-bar">
|
||
<span className="f-prompt-count">
|
||
{form.build.length > 0 ? `${form.build.length} chars` : "go wild"}
|
||
</span>
|
||
<span className="f-prompt-hint">
|
||
⌘ + Enter to submit the form
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Field>
|
||
|
||
<Field label="04" title="Which one are you?">
|
||
<div className="f-roles">
|
||
{ROLES.map((r) => (
|
||
<button
|
||
type="button" key={r.value}
|
||
className={`f-role${form.role === r.value ? " active" : ""}`}
|
||
onClick={() => update("role", r.value)}
|
||
>
|
||
<span className="f-role-label">{r.label}</span>
|
||
<span className="f-role-hint">{r.hint}</span>
|
||
<span className="f-role-check">
|
||
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
||
<path d="M3 7.2 5.8 10 11 4.2" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Field>
|
||
|
||
<Field label="05" title="How'd you hear about us?" hint="Optional. Helps us know what's working.">
|
||
<div className="f-chips">
|
||
{SOURCES.map((s) => (
|
||
<button
|
||
type="button" key={s}
|
||
className={`f-chip${form.source === s ? " active" : ""}`}
|
||
onClick={() => update("source", form.source === s ? "" : s)}
|
||
>{s}</button>
|
||
))}
|
||
</div>
|
||
</Field>
|
||
|
||
<div className="beta-submit">
|
||
<button
|
||
type="submit"
|
||
className="btn btn-primary beta-submit-btn"
|
||
disabled={!valid || submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<span className="spinner" /> Sending…
|
||
</>
|
||
) : (
|
||
<>Request my invite <Arrow /></>
|
||
)}
|
||
</button>
|
||
<p className="beta-fine mono">
|
||
No credit card · No spam, just one email when you're in · Unsubscribe anytime
|
||
</p>
|
||
</div>
|
||
</form>
|
||
</>
|
||
)}
|
||
|
||
{/* What you get — shown on both states */}
|
||
<section className="benefits">
|
||
<div className="benefits-head">
|
||
<div className="eyebrow">What you get on the inside</div>
|
||
</div>
|
||
<div className="benefits-grid">
|
||
{BENEFITS.map((b) => (
|
||
<div className="benefit" key={b.title}>
|
||
<div className="benefit-icon"><BenefitIcon name={b.icon} /></div>
|
||
<h3 className="benefit-title">{b.title}</h3>
|
||
<p className="benefit-body">{b.body}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<footer className="beta-footer">
|
||
<div className="wrap beta-footer-inner">
|
||
<span className="mono">🇨🇦 Built in Canada · Your data stays safe · No credit card to start</span>
|
||
<span className="mono">© 2026 Vibn Inc.</span>
|
||
</div>
|
||
</footer>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function Field({ label, title, hint, required, children }) {
|
||
return (
|
||
<div className="field">
|
||
<div className="field-meta">
|
||
<span className="field-num mono">{label}{required && <em>*</em>}</span>
|
||
<div className="field-text">
|
||
<div className="field-title">{title}</div>
|
||
{hint && <div className="field-hint">{hint}</div>}
|
||
</div>
|
||
</div>
|
||
<div className="field-body">{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function BenefitIcon({ name }) {
|
||
const p = { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none",
|
||
stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" };
|
||
if (name === "lightning") return <svg {...p}><path d="M11 2 4 11h5l-1 7 7-9h-5l1-7Z"/></svg>;
|
||
if (name === "gift") return <svg {...p}><rect x="3" y="7.5" width="14" height="10"/><path d="M3 11h14M10 7.5V18M7 7.5a2 2 0 1 1 3-2.5 2 2 0 1 1 3 2.5"/></svg>;
|
||
if (name === "chat") return <svg {...p}><path d="M3.5 11.5a6 6 0 1 1 3.4 5.4L3 18l1.1-3.9a6 6 0 0 1-.6-2.6Z"/></svg>;
|
||
return null;
|
||
}
|
||
|
||
// ── Submitted state ─────────────────────────────────────────────────────────
|
||
|
||
function Confirmed({ form, queuePos }) {
|
||
const [copied, setCopied] = React.useState(false);
|
||
// Fake-but-stable referral code
|
||
const ref = React.useMemo(() => {
|
||
const seed = form.email || form.name || "anon";
|
||
let h = 5;
|
||
for (const c of seed) h = (h * 33 + c.charCodeAt(0)) >>> 0;
|
||
return "v-" + h.toString(36).slice(0, 6);
|
||
}, [form.email, form.name]);
|
||
const link = typeof window !== "undefined" ? `${window.location.origin}/join?ref=${ref}` : `vibn.app/join?ref=${ref}`;
|
||
|
||
const copyLink = () => {
|
||
try { navigator.clipboard.writeText(link); } catch (e) { /* noop */ }
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 1800);
|
||
};
|
||
|
||
// Compute a queue progress bar percentage — visual feedback only
|
||
const pct = Math.max(2, Math.min(98, 100 - (queuePos - 2100) / 9));
|
||
|
||
return (
|
||
<div className="confirmed">
|
||
<div className="confirmed-head">
|
||
<div className="confirmed-badge">
|
||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||
<circle cx="16" cy="16" r="15" stroke="currentColor" strokeWidth="1.5" opacity=".25"/>
|
||
<path d="M10 16.5 14.5 21 22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
<div className="eyebrow">You're on the list</div>
|
||
<h1 className="beta-title" style={{ marginTop: 14 }}>
|
||
{form.name ? <>Welcome, <em>{form.name}</em>.</> : <>You're <em>in line</em>.</>}
|
||
</h1>
|
||
<p className="beta-sub">
|
||
We got your invite request — keep an eye on <b className="mono" style={{ fontWeight: 500 }}>{form.email}</b>.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="queue-card">
|
||
<div className="queue-top">
|
||
<div>
|
||
<div className="queue-label mono">your spot in line</div>
|
||
<div className="queue-num">#{queuePos.toLocaleString()}</div>
|
||
</div>
|
||
<div className="queue-rate">
|
||
<div className="queue-rate-num">50<small>/wk</small></div>
|
||
<div className="queue-rate-lbl mono">letting in</div>
|
||
</div>
|
||
</div>
|
||
<div className="queue-bar">
|
||
<div className="queue-bar-fill" style={{ width: `${pct}%` }} />
|
||
<div className="queue-bar-marker" style={{ left: `${pct}%` }}>
|
||
<span>You</span>
|
||
</div>
|
||
</div>
|
||
<div className="queue-foot mono">
|
||
You should hear from us in ~<b>{Math.ceil((queuePos - 50) / 50)} weeks</b>. Don't want to wait?
|
||
</div>
|
||
</div>
|
||
|
||
<div className="refer">
|
||
<div className="refer-head">
|
||
<div className="eyebrow">Skip the line</div>
|
||
<h3 className="refer-title">Send 3 friends — jump to the front.</h3>
|
||
<p className="refer-sub">Each friend who joins via your link bumps you up 500 spots.</p>
|
||
</div>
|
||
<div className="refer-row">
|
||
<div className="refer-link mono">
|
||
<span className="refer-prefix">vibn.app/join?ref=</span>
|
||
<b>{ref}</b>
|
||
</div>
|
||
<button type="button" className="btn btn-ghost refer-copy" onClick={copyLink}>
|
||
{copied ? "Copied!" : "Copy link"}
|
||
</button>
|
||
</div>
|
||
<div className="refer-share">
|
||
<a className="share-btn" href="#"><ShareIcon name="x"/> Share on X</a>
|
||
<a className="share-btn" href="#"><ShareIcon name="reddit"/> Post to Reddit</a>
|
||
<a className="share-btn" href="#"><ShareIcon name="mail"/> Email a friend</a>
|
||
</div>
|
||
</div>
|
||
|
||
{form.build && (
|
||
<div className="build-echo">
|
||
<div className="eyebrow">What we'll help you build first</div>
|
||
<div className="build-echo-quote">"{form.build}"</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ShareIcon({ name }) {
|
||
const p = { width: 14, height: 14, viewBox: "0 0 16 16", fill: "currentColor" };
|
||
if (name === "x") return <svg {...p}><path d="M9.2 7 13.7 2h-1.4L8.6 6.3 5.6 2H2l4.7 6.8L2 14h1.4l4.1-4.7 3.3 4.7H14L9.2 7Z"/></svg>;
|
||
if (name === "reddit") return <svg {...p}><circle cx="8" cy="9" r="6"/></svg>;
|
||
if (name === "mail") return <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="3.5" width="12" height="9" rx="1.5"/><path d="m3 5 5 3.8L13 5"/></svg>;
|
||
return null;
|
||
}
|
||
|
||
// ── Styles ──────────────────────────────────────────────────────────────────
|
||
|
||
function BetaStyle() {
|
||
return <style>{`
|
||
.beta-main { position: relative; padding-block: clamp(60px, 9vh, 100px); overflow: hidden; }
|
||
.beta-wrap { position: relative; max-width: 760px; }
|
||
|
||
.beta-head { text-align: center; margin-bottom: 56px; }
|
||
.beta-title {
|
||
margin-top: 18px;
|
||
font-size: clamp(40px, 6.4vw, 80px);
|
||
font-weight: 500; letter-spacing: -0.03em; line-height: 1.0;
|
||
text-wrap: balance;
|
||
}
|
||
.beta-title em {
|
||
font-style: normal;
|
||
color: var(--accent);
|
||
text-shadow: 0 0 40px var(--accent-glow);
|
||
}
|
||
.beta-sub {
|
||
margin-top: 22px;
|
||
font-size: clamp(16px, 1.6vw, 19px);
|
||
color: var(--fg-dim);
|
||
max-width: 540px; margin-inline: auto;
|
||
text-wrap: balance;
|
||
}
|
||
.beta-sub b { color: var(--fg); font-weight: 500; }
|
||
|
||
/* Form */
|
||
.beta-form {
|
||
display: flex; flex-direction: column;
|
||
gap: 28px;
|
||
padding: 36px clamp(20px, 4vw, 44px) 32px;
|
||
background: linear-gradient(180deg, oklch(0.20 0.009 60 / 0.6), oklch(0.17 0.008 60 / 0.6));
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 22px;
|
||
backdrop-filter: blur(20px);
|
||
position: relative;
|
||
box-shadow: 0 30px 80px -20px oklch(0 0 0 / 0.6);
|
||
}
|
||
.beta-form::before {
|
||
content: "";
|
||
position: absolute; left: 0; right: 0; top: 0; height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||
opacity: .6;
|
||
}
|
||
|
||
.field { display: flex; flex-direction: column; gap: 12px; }
|
||
.field-meta {
|
||
display: flex; align-items: flex-start; gap: 14px;
|
||
}
|
||
.field-num {
|
||
font-size: 11px; letter-spacing: 0.1em;
|
||
color: var(--fg-faint);
|
||
padding: 4px 8px;
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 6px;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
.field-num em {
|
||
font-style: normal;
|
||
color: var(--accent);
|
||
margin-left: 1px;
|
||
}
|
||
.field-text { flex: 1; }
|
||
.field-title {
|
||
font-size: 17px; font-weight: 500;
|
||
color: var(--fg);
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.field-hint {
|
||
margin-top: 2px;
|
||
font-size: 13px; color: var(--fg-mute);
|
||
}
|
||
.field-body { padding-left: 0; }
|
||
|
||
/* Inputs */
|
||
.f-input, .f-textarea {
|
||
width: 100%; box-sizing: border-box;
|
||
padding: 14px 16px;
|
||
background: oklch(0.16 0.008 60 / 0.8);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 12px;
|
||
color: var(--fg);
|
||
font: 16px/1.5 var(--font-sans);
|
||
outline: none;
|
||
transition: border-color .15s, background .15s, box-shadow .15s;
|
||
}
|
||
.f-input::placeholder, .f-textarea::placeholder { color: var(--fg-faint); }
|
||
.f-input:focus, .f-textarea:focus, .f-prompt:focus-within {
|
||
border-color: oklch(0.74 0.175 35 / 0.65);
|
||
background: oklch(0.18 0.009 60 / 0.95);
|
||
box-shadow: 0 0 0 3px oklch(0.74 0.175 35 / 0.15), 0 0 30px -10px var(--accent-glow);
|
||
}
|
||
.f-textarea { resize: vertical; min-height: 110px; }
|
||
|
||
.f-prompt {
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 12px;
|
||
background: oklch(0.16 0.008 60 / 0.8);
|
||
overflow: hidden;
|
||
transition: border-color .15s, box-shadow .15s, background .15s;
|
||
}
|
||
.f-prompt .f-textarea {
|
||
border: 0; background: transparent; border-radius: 0;
|
||
padding: 14px 16px 10px;
|
||
}
|
||
.f-prompt .f-textarea:focus { box-shadow: none; }
|
||
.f-prompt-bar {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 8px 14px 10px;
|
||
border-top: 1px solid var(--hairline);
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--fg-faint);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.f-prompt-count { color: var(--accent); }
|
||
|
||
/* Role cards */
|
||
.f-roles {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
.f-role {
|
||
position: relative;
|
||
text-align: left;
|
||
padding: 14px 18px 14px 16px;
|
||
background: oklch(0.16 0.008 60 / 0.6);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 12px;
|
||
display: flex; flex-direction: column; gap: 2px;
|
||
transition: border-color .15s, background .15s;
|
||
}
|
||
.f-role:hover { border-color: var(--hairline-2); }
|
||
.f-role.active {
|
||
border-color: var(--accent);
|
||
background: oklch(0.20 0.04 35 / 0.4);
|
||
box-shadow: 0 0 0 3px oklch(0.74 0.175 35 / 0.1);
|
||
}
|
||
.f-role-label {
|
||
font-size: 15px; font-weight: 500;
|
||
color: var(--fg);
|
||
}
|
||
.f-role-hint {
|
||
font-size: 13px; color: var(--fg-mute);
|
||
}
|
||
.f-role-check {
|
||
position: absolute; top: 50%; right: 16px;
|
||
transform: translateY(-50%);
|
||
width: 20px; height: 20px; border-radius: 50%;
|
||
border: 1.5px solid var(--hairline-2);
|
||
display: grid; place-items: center;
|
||
color: var(--accent-fg);
|
||
background: transparent;
|
||
transition: all .15s;
|
||
}
|
||
.f-role.active .f-role-check {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
}
|
||
.f-role-check svg { opacity: 0; transition: opacity .15s; }
|
||
.f-role.active .f-role-check svg { opacity: 1; }
|
||
|
||
/* Source chips */
|
||
.f-chips {
|
||
display: flex; flex-wrap: wrap; gap: 8px;
|
||
}
|
||
.f-chip {
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--hairline);
|
||
background: oklch(0.16 0.008 60 / 0.6);
|
||
color: var(--fg-dim);
|
||
font-size: 13px;
|
||
transition: border-color .15s, color .15s, background .15s;
|
||
}
|
||
.f-chip:hover { color: var(--fg); border-color: var(--hairline-2); }
|
||
.f-chip.active {
|
||
border-color: var(--accent);
|
||
background: oklch(0.20 0.04 35 / 0.4);
|
||
color: var(--fg);
|
||
}
|
||
|
||
.beta-submit {
|
||
display: flex; flex-direction: column; align-items: center; gap: 14px;
|
||
margin-top: 8px;
|
||
}
|
||
.beta-submit-btn {
|
||
width: 100%; max-width: 320px;
|
||
height: 56px; font-size: 16px;
|
||
}
|
||
.beta-fine {
|
||
font-size: 11px; color: var(--fg-faint);
|
||
letter-spacing: 0.03em; text-align: center;
|
||
text-wrap: balance;
|
||
}
|
||
|
||
.spinner {
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
border: 2px solid oklch(0 0 0 / 0.2);
|
||
border-top-color: var(--accent-fg);
|
||
animation: spin .9s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* Confirmed state */
|
||
.confirmed { display: flex; flex-direction: column; gap: 28px; }
|
||
.confirmed-head { text-align: center; }
|
||
.confirmed-badge {
|
||
display: inline-grid; place-items: center;
|
||
width: 64px; height: 64px;
|
||
border-radius: 50%;
|
||
color: var(--ok);
|
||
background: oklch(0.78 0.16 155 / 0.1);
|
||
border: 1px solid oklch(0.78 0.16 155 / 0.4);
|
||
box-shadow: 0 0 40px oklch(0.78 0.16 155 / 0.3);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.queue-card {
|
||
padding: 28px;
|
||
background: linear-gradient(180deg, oklch(0.20 0.009 60 / 0.6), oklch(0.17 0.008 60 / 0.6));
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 18px;
|
||
}
|
||
.queue-top {
|
||
display: flex; justify-content: space-between; align-items: flex-end;
|
||
gap: 14px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.queue-label, .queue-rate-lbl {
|
||
font-size: 11px; letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--fg-faint);
|
||
}
|
||
.queue-num {
|
||
font-family: var(--font-mono);
|
||
font-size: clamp(48px, 7vw, 76px);
|
||
font-weight: 500;
|
||
letter-spacing: -0.04em;
|
||
line-height: 1;
|
||
color: var(--accent);
|
||
text-shadow: 0 0 40px var(--accent-glow);
|
||
margin-top: 8px;
|
||
}
|
||
.queue-rate { text-align: right; }
|
||
.queue-rate-num {
|
||
font-family: var(--font-mono);
|
||
font-size: 26px; font-weight: 500;
|
||
color: var(--fg);
|
||
line-height: 1;
|
||
}
|
||
.queue-rate-num small {
|
||
color: var(--fg-mute); font-size: 13px; font-weight: 400;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.queue-bar {
|
||
position: relative;
|
||
height: 6px;
|
||
border-radius: 999px;
|
||
background: oklch(0.22 0.01 60);
|
||
overflow: visible;
|
||
margin: 36px 0 24px;
|
||
}
|
||
.queue-bar-fill {
|
||
position: absolute; left: 0; top: 0; bottom: 0;
|
||
border-radius: 999px;
|
||
background: linear-gradient(90deg, oklch(0.65 0.15 35), var(--accent));
|
||
box-shadow: 0 0 12px var(--accent-glow);
|
||
transition: width .8s cubic-bezier(.4,.1,.2,1);
|
||
}
|
||
.queue-bar-marker {
|
||
position: absolute; top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 14px; height: 14px;
|
||
border-radius: 50%;
|
||
background: var(--accent);
|
||
box-shadow: 0 0 0 3px var(--bg), 0 0 18px var(--accent-glow);
|
||
}
|
||
.queue-bar-marker span {
|
||
position: absolute; bottom: 100%; left: 50%;
|
||
transform: translate(-50%, -8px);
|
||
font-family: var(--font-mono);
|
||
font-size: 11px; color: var(--accent);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.queue-foot {
|
||
font-size: 12px; color: var(--fg-mute);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.queue-foot b { color: var(--fg); font-weight: 500; }
|
||
|
||
/* Refer */
|
||
.refer {
|
||
padding: 28px;
|
||
background: linear-gradient(180deg, oklch(0.20 0.009 60 / 0.6), oklch(0.17 0.008 60 / 0.6));
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 18px;
|
||
}
|
||
.refer-title {
|
||
margin-top: 12px;
|
||
font-size: 22px; font-weight: 500;
|
||
letter-spacing: -0.018em;
|
||
}
|
||
.refer-sub {
|
||
margin-top: 6px;
|
||
color: var(--fg-mute);
|
||
font-size: 14px;
|
||
}
|
||
.refer-row {
|
||
display: flex; gap: 8px; align-items: stretch;
|
||
margin-top: 18px;
|
||
}
|
||
.refer-link {
|
||
flex: 1;
|
||
padding: 12px 14px;
|
||
background: oklch(0.16 0.008 60);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
letter-spacing: 0.01em;
|
||
color: var(--fg-dim);
|
||
display: flex; align-items: center;
|
||
overflow: hidden; text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.refer-prefix { color: var(--fg-faint); }
|
||
.refer-link b { color: var(--accent); font-weight: 500; margin-left: 0; }
|
||
.refer-copy { height: auto; padding-inline: 18px; }
|
||
|
||
.refer-share {
|
||
display: flex; flex-wrap: wrap; gap: 8px;
|
||
margin-top: 14px;
|
||
}
|
||
.share-btn {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
padding: 8px 14px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--hairline);
|
||
background: oklch(0.16 0.008 60 / 0.5);
|
||
color: var(--fg-dim);
|
||
font-size: 13px;
|
||
transition: border-color .15s, color .15s;
|
||
}
|
||
.share-btn:hover { color: var(--fg); border-color: var(--hairline-2); }
|
||
|
||
.build-echo {
|
||
padding: 24px 28px;
|
||
border: 1px dashed var(--hairline);
|
||
border-radius: 16px;
|
||
}
|
||
.build-echo-quote {
|
||
margin-top: 12px;
|
||
font-size: 18px;
|
||
color: var(--fg);
|
||
font-style: italic;
|
||
letter-spacing: -0.005em;
|
||
text-wrap: balance;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* Benefits */
|
||
.benefits { margin-top: 64px; }
|
||
.benefits-head { text-align: center; margin-bottom: 26px; }
|
||
.benefits-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 14px;
|
||
}
|
||
@media (max-width: 720px) { .benefits-grid { grid-template-columns: 1fr; } }
|
||
.benefit {
|
||
padding: 24px;
|
||
background: linear-gradient(180deg, oklch(0.20 0.009 60 / 0.35), oklch(0.17 0.008 60 / 0.35));
|
||
border: 1px solid var(--hairline);
|
||
border-radius: 14px;
|
||
}
|
||
.benefit-icon {
|
||
width: 36px; height: 36px;
|
||
border-radius: 9px;
|
||
background: oklch(0.22 0.011 60);
|
||
border: 1px solid var(--hairline);
|
||
color: var(--accent);
|
||
display: grid; place-items: center;
|
||
margin-bottom: 14px;
|
||
}
|
||
.benefit-title {
|
||
font-size: 16px; font-weight: 500;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.benefit-body {
|
||
margin-top: 6px;
|
||
color: var(--fg-mute);
|
||
font-size: 13.5px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.beta-footer {
|
||
padding: 24px 0;
|
||
border-top: 1px solid var(--hairline);
|
||
background: oklch(0.14 0.008 60);
|
||
}
|
||
.beta-footer-inner {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
gap: 16px; flex-wrap: wrap;
|
||
font-size: 11px;
|
||
color: var(--fg-faint);
|
||
letter-spacing: 0.03em;
|
||
}
|
||
`}</style>;
|
||
}
|
||
|
||
ReactDOM.createRoot(document.getElementById("root")).render(<BetaApp />);
|