fix(ai): strip deepseek xml tags from chat history & secure git tools
This commit addresses the issue where DeepSeek's raw XML markup (like <tool_calls> and <think>) was leaking into chat history, causing hallucinations in subsequent turns. It also patches a vulnerability in the git commit tool where arbitrary shell injection was possible. Additionally, it includes UX copy and color contrast adjustments for the marketing homepage breadcrumbs.
This commit is contained in:
809
new-site/beta.jsx
Normal file
809
new-site/beta.jsx
Normal file
@@ -0,0 +1,809 @@
|
||||
// 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 />);
|
||||
Reference in New Issue
Block a user