diff --git a/vibn-frontend/_marketing/components/new-site/index.tsx b/vibn-frontend/_marketing/components/new-site/index.tsx
new file mode 100644
index 00000000..b99f82e8
--- /dev/null
+++ b/vibn-frontend/_marketing/components/new-site/index.tsx
@@ -0,0 +1,2776 @@
+// Auto-ported from new-site
+"use client";
+import React, { useState, useEffect, useRef, Fragment } from "react";
+
+// --- tweaks-panel.jsx ---
+
+// tweaks-panel.jsx
+// Reusable Tweaks shell + form-control helpers.
+//
+// Owns the host protocol (listens for __activate_edit_mode / __deactivate_edit_mode,
+// posts __edit_mode_available / __edit_mode_set_keys / __edit_mode_dismissed) so
+// individual prototypes don't re-roll it. Ships a consistent set of controls so you
+// don't hand-draw , segmented radios, steppers, etc.
+//
+// Usage (in an HTML file that loads React + Babel):
+//
+// const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
+// "primaryColor": "#D97757",
+// "palette": ["#D97757", "#29261b", "#f6f4ef"],
+// "fontSize": 16,
+// "density": "regular",
+// "dark": false
+// }/*EDITMODE-END*/;
+//
+// function App() {
+// const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
+// return (
+//
+// Hello
+//
+//
+// setTweak('fontSize', v)} />
+// setTweak('density', v)} />
+//
+// setTweak('primaryColor', v)} />
+// setTweak('palette', v)} />
+// setTweak('dark', v)} />
+//
+//
+// );
+// }
+//
+// ─────────────────────────────────────────────────────────────────────────────
+
+const __TWEAKS_STYLE = `
+ .twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
+ max-height:calc(100vh - 32px);display:flex;flex-direction:column;
+ transform:scale(var(--dc-inv-zoom,1));transform-origin:bottom right;
+ background:rgba(250,249,247,.78);color:#29261b;
+ -webkit-backdrop-filter:blur(24px) saturate(160%);backdrop-filter:blur(24px) saturate(160%);
+ border:.5px solid rgba(255,255,255,.6);border-radius:14px;
+ box-shadow:0 1px 0 rgba(255,255,255,.5) inset,0 12px 40px rgba(0,0,0,.18);
+ font:11.5px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
+ .twk-hd{display:flex;align-items:center;justify-content:space-between;
+ padding:10px 8px 10px 14px;cursor:move;user-select:none}
+ .twk-hd b{font-size:12px;font-weight:600;letter-spacing:.01em}
+ .twk-x{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
+ width:22px;height:22px;border-radius:6px;cursor:default;font-size:13px;line-height:1}
+ .twk-x:hover{background:rgba(0,0,0,.06);color:#29261b}
+ .twk-body{padding:2px 14px 14px;display:flex;flex-direction:column;gap:10px;
+ overflow-y:auto;overflow-x:hidden;min-height:0;
+ scrollbar-width:thin;scrollbar-color:rgba(0,0,0,.15) transparent}
+ .twk-body::-webkit-scrollbar{width:8px}
+ .twk-body::-webkit-scrollbar-track{background:transparent;margin:2px}
+ .twk-body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15);border-radius:4px;
+ border:2px solid transparent;background-clip:content-box}
+ .twk-body::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.25);
+ border:2px solid transparent;background-clip:content-box}
+ .twk-row{display:flex;flex-direction:column;gap:5px}
+ .twk-row-h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
+ .twk-lbl{display:flex;justify-content:space-between;align-items:baseline;
+ color:rgba(41,38,27,.72)}
+ .twk-lbl>span:first-child{font-weight:500}
+ .twk-val{color:rgba(41,38,27,.5);font-variant-numeric:tabular-nums}
+
+ .twk-sect{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
+ color:rgba(41,38,27,.45);padding:10px 0 0}
+ .twk-sect:first-child{padding-top:0}
+
+ .twk-field{appearance:none;box-sizing:border-box;width:100%;min-width:0;height:26px;padding:0 8px;
+ border:.5px solid rgba(0,0,0,.1);border-radius:7px;
+ background:rgba(255,255,255,.6);color:inherit;font:inherit;outline:none}
+ .twk-field:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}
+ select.twk-field{padding-right:22px;
+ background-image:url("data:image/svg+xml;utf8, ");
+ background-repeat:no-repeat;background-position:right 8px center}
+
+ .twk-slider{appearance:none;-webkit-appearance:none;width:100%;height:4px;margin:6px 0;
+ border-radius:999px;background:rgba(0,0,0,.12);outline:none}
+ .twk-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
+ width:14px;height:14px;border-radius:50%;background:#fff;
+ border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
+ .twk-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;
+ background:#fff;border:.5px solid rgba(0,0,0,.12);box-shadow:0 1px 3px rgba(0,0,0,.2);cursor:default}
+
+ .twk-seg{position:relative;display:flex;padding:2px;border-radius:8px;
+ background:rgba(0,0,0,.06);user-select:none}
+ .twk-seg-thumb{position:absolute;top:2px;bottom:2px;border-radius:6px;
+ background:rgba(255,255,255,.9);box-shadow:0 1px 2px rgba(0,0,0,.12);
+ transition:left .15s cubic-bezier(.3,.7,.4,1),width .15s}
+ .twk-seg.dragging .twk-seg-thumb{transition:none}
+ .twk-seg button{appearance:none;position:relative;z-index:1;flex:1;border:0;
+ background:transparent;color:inherit;font:inherit;font-weight:500;min-height:22px;
+ border-radius:6px;cursor:default;padding:4px 6px;line-height:1.2;
+ overflow-wrap:anywhere}
+
+ .twk-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
+ background:rgba(0,0,0,.15);transition:background .15s;cursor:default;padding:0}
+ .twk-toggle[data-on="1"]{background:#34c759}
+ .twk-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
+ background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}
+ .twk-toggle[data-on="1"] i{transform:translateX(14px)}
+
+ .twk-num{display:flex;align-items:center;box-sizing:border-box;min-width:0;height:26px;padding:0 0 0 8px;
+ border:.5px solid rgba(0,0,0,.1);border-radius:7px;background:rgba(255,255,255,.6)}
+ .twk-num-lbl{font-weight:500;color:rgba(41,38,27,.6);cursor:ew-resize;
+ user-select:none;padding-right:8px}
+ .twk-num input{flex:1;min-width:0;height:100%;border:0;background:transparent;
+ font:inherit;font-variant-numeric:tabular-nums;text-align:right;padding:0 8px 0 0;
+ outline:none;color:inherit;-moz-appearance:textfield}
+ .twk-num input::-webkit-inner-spin-button,.twk-num input::-webkit-outer-spin-button{
+ -webkit-appearance:none;margin:0}
+ .twk-num-unit{padding-right:8px;color:rgba(41,38,27,.45)}
+
+ .twk-btn{appearance:none;height:26px;padding:0 12px;border:0;border-radius:7px;
+ background:rgba(0,0,0,.78);color:#fff;font:inherit;font-weight:500;cursor:default}
+ .twk-btn:hover{background:rgba(0,0,0,.88)}
+ .twk-btn.secondary{background:rgba(0,0,0,.06);color:inherit}
+ .twk-btn.secondary:hover{background:rgba(0,0,0,.1)}
+
+ .twk-swatch{appearance:none;-webkit-appearance:none;width:56px;height:22px;
+ border:.5px solid rgba(0,0,0,.1);border-radius:6px;padding:0;cursor:default;
+ background:transparent;flex-shrink:0}
+ .twk-swatch::-webkit-color-swatch-wrapper{padding:0}
+ .twk-swatch::-webkit-color-swatch{border:0;border-radius:5.5px}
+ .twk-swatch::-moz-color-swatch{border:0;border-radius:5.5px}
+
+ .twk-chips{display:flex;gap:6px}
+ .twk-chip{position:relative;appearance:none;flex:1;min-width:0;height:46px;
+ padding:0;border:0;border-radius:6px;overflow:hidden;cursor:default;
+ box-shadow:0 0 0 .5px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.06);
+ transition:transform .12s cubic-bezier(.3,.7,.4,1),box-shadow .12s}
+ .twk-chip:hover{transform:translateY(-1px);
+ box-shadow:0 0 0 .5px rgba(0,0,0,.18),0 4px 10px rgba(0,0,0,.12)}
+ .twk-chip[data-on="1"]{box-shadow:0 0 0 1.5px rgba(0,0,0,.85),
+ 0 2px 6px rgba(0,0,0,.15)}
+ .twk-chip>span{position:absolute;top:0;bottom:0;right:0;width:34%;
+ display:flex;flex-direction:column;box-shadow:-1px 0 0 rgba(0,0,0,.1)}
+ .twk-chip>span>i{flex:1;box-shadow:0 -1px 0 rgba(0,0,0,.1)}
+ .twk-chip>span>i:first-child{box-shadow:none}
+ .twk-chip svg{position:absolute;top:6px;left:6px;width:13px;height:13px;
+ filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
+`;
+
+// ── useTweaks ───────────────────────────────────────────────────────────────
+// Single source of truth for tweak values. setTweak persists via the host
+// (__edit_mode_set_keys → host rewrites the EDITMODE block on disk).
+function useTweaks(defaults) {
+ const [values, setValues] = useState(defaults);
+ // Accepts either setTweak('key', value) or setTweak({ key: value, ... }) so a
+ // useState-style call doesn't write a "[object Object]" key into the persisted
+ // JSON block.
+ const setTweak = React.useCallback((keyOrEdits, val) => {
+ const edits =
+ typeof keyOrEdits === "object" && keyOrEdits !== null
+ ? keyOrEdits
+ : { [keyOrEdits]: val };
+ setValues((prev) => ({ ...prev, ...edits }));
+ window.parent.postMessage({ type: "__edit_mode_set_keys", edits }, "*");
+ // Same-window signal so in-page listeners (deck-stage rail thumbnails)
+ // can react — the parent message only reaches the host, not peers.
+ window.dispatchEvent(new CustomEvent("tweakchange", { detail: edits }));
+ }, []);
+ return [values, setTweak];
+}
+
+// ── TweaksPanel ─────────────────────────────────────────────────────────────
+// Floating shell. Registers the protocol listener BEFORE announcing
+// availability — if the announce ran first, the host's activate could land
+// before our handler exists and the toolbar toggle would silently no-op.
+// The close button posts __edit_mode_dismissed so the host's toolbar toggle
+// flips off in lockstep; the host echoes __deactivate_edit_mode back which
+// is what actually hides the panel.
+function TweaksPanel({ title = "Tweaks", noDeckControls = false, children }) {
+ const [open, setOpen] = useState(false);
+ const dragRef = useRef(null);
+ // Auto-inject a rail toggle when a is on the page. The
+ // toggle drives the deck's per-viewer _railVisible via window message;
+ // state is mirrored from the same localStorage key the deck reads so
+ // the control reflects reality across reloads. The mechanism is the
+ // message — authors who want custom placement can post it directly
+ // and pass noDeckControls to suppress this one.
+ const hasDeckStage = React.useMemo(
+ () =>
+ typeof document !== "undefined" && !!document.querySelector("deck-stage"),
+ [],
+ );
+ // deck-stage enables its rail in connectedCallback, but this panel can
+ // mount before that element has upgraded. The initial read catches the
+ // common case; the listener covers mounting first. (Older deck-stage.js
+ // copies still wait for the host's __omelette_rail_enabled postMessage —
+ // same listener handles those.)
+ const [railEnabled, setRailEnabled] = useState(
+ () => hasDeckStage && !!document.querySelector("deck-stage")?._railEnabled,
+ );
+ useEffect(() => {
+ if (!hasDeckStage || railEnabled) return undefined;
+ const onMsg = (e) => {
+ if (e.data && e.data.type === "__omelette_rail_enabled")
+ setRailEnabled(true);
+ };
+ window.addEventListener("message", onMsg);
+ return () => window.removeEventListener("message", onMsg);
+ }, [hasDeckStage, railEnabled]);
+ const [railVisible, setRailVisible] = useState(() => {
+ try {
+ return localStorage.getItem("deck-stage.railVisible") !== "0";
+ } catch (e) {
+ return true;
+ }
+ });
+ const toggleRail = (on) => {
+ setRailVisible(on);
+ window.postMessage({ type: "__deck_rail_visible", on }, "*");
+ };
+ const offsetRef = useRef({ x: 16, y: 16 });
+ const PAD = 16;
+
+ const clampToViewport = React.useCallback(() => {
+ const panel = dragRef.current;
+ if (!panel) return;
+ const w = panel.offsetWidth,
+ h = panel.offsetHeight;
+ const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
+ const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
+ offsetRef.current = {
+ x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
+ y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
+ };
+ panel.style.right = offsetRef.current.x + "px";
+ panel.style.bottom = offsetRef.current.y + "px";
+ }, []);
+
+ useEffect(() => {
+ if (!open) return;
+ clampToViewport();
+ if (typeof ResizeObserver === "undefined") {
+ window.addEventListener("resize", clampToViewport);
+ return () => window.removeEventListener("resize", clampToViewport);
+ }
+ const ro = new ResizeObserver(clampToViewport);
+ ro.observe(document.documentElement);
+ return () => ro.disconnect();
+ }, [open, clampToViewport]);
+
+ useEffect(() => {
+ const onMsg = (e) => {
+ const t = e?.data?.type;
+ if (t === "__activate_edit_mode") setOpen(true);
+ else if (t === "__deactivate_edit_mode") setOpen(false);
+ };
+ window.addEventListener("message", onMsg);
+ window.parent.postMessage({ type: "__edit_mode_available" }, "*");
+ return () => window.removeEventListener("message", onMsg);
+ }, []);
+
+ const dismiss = () => {
+ setOpen(false);
+ window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*");
+ };
+
+ const onDragStart = (e) => {
+ const panel = dragRef.current;
+ if (!panel) return;
+ const r = panel.getBoundingClientRect();
+ const sx = e.clientX,
+ sy = e.clientY;
+ const startRight = window.innerWidth - r.right;
+ const startBottom = window.innerHeight - r.bottom;
+ const move = (ev) => {
+ offsetRef.current = {
+ x: startRight - (ev.clientX - sx),
+ y: startBottom - (ev.clientY - sy),
+ };
+ clampToViewport();
+ };
+ const up = () => {
+ window.removeEventListener("mousemove", move);
+ window.removeEventListener("mouseup", up);
+ };
+ window.addEventListener("mousemove", move);
+ window.addEventListener("mouseup", up);
+ };
+
+ if (!open) return null;
+ return (
+ <>
+
+
+
+ {title}
+ e.stopPropagation()}
+ onClick={dismiss}
+ >
+ ✕
+
+
+
+ {children}
+ {hasDeckStage && railEnabled && !noDeckControls && (
+
+
+
+ )}
+
+
+ >
+ );
+}
+
+// ── Layout helpers ──────────────────────────────────────────────────────────
+
+function TweakSection({ label, children }) {
+ return (
+ <>
+ {label}
+ {children}
+ >
+ );
+}
+
+function TweakRow({ label, value, children, inline = false }) {
+ return (
+
+
+ {label}
+ {value != null && {value} }
+
+ {children}
+
+ );
+}
+
+// ── Controls ────────────────────────────────────────────────────────────────
+
+function TweakSlider({
+ label,
+ value,
+ min = 0,
+ max = 100,
+ step = 1,
+ unit = "",
+ onChange,
+}) {
+ return (
+
+ onChange(Number(e.target.value))}
+ />
+
+ );
+}
+
+function TweakToggle({ label, value, onChange }) {
+ return (
+
+
+ {label}
+
+
onChange(!value)}
+ >
+
+
+
+ );
+}
+
+function TweakRadio({ label, value, options, onChange }) {
+ const trackRef = useRef(null);
+ const [dragging, setDragging] = useState(false);
+ // The active value is read by pointer-move handlers attached for the lifetime
+ // of a drag — ref it so a stale closure doesn't fire onChange for every move.
+ const valueRef = useRef(value);
+ valueRef.current = value;
+
+ // Segments wrap mid-word once per-segment width runs out. The track is
+ // ~248px (280 panel − 28 body pad − 4 seg pad), each button loses 12px
+ // to its own padding, and 11.5px system-ui averages ~6.3px/char — so 2
+ // options fit ~16 chars each, 3 fit ~10. Past that (or >3 options), fall
+ // back to a dropdown rather than wrap.
+ const labelLen = (o) => String(typeof o === "object" ? o.label : o).length;
+ const maxLen = options.reduce((m, o) => Math.max(m, labelLen(o)), 0);
+ const fitsAsSegments = maxLen <= ({ 2: 16, 3: 10 }[options.length] ?? 0);
+ if (!fitsAsSegments) {
+ // emits strings — map back to the original option value so the
+ // fallback stays type-preserving (numbers, booleans) like the segment path.
+ const resolve = (s) => {
+ const m = options.find(
+ (o) => String(typeof o === "object" ? o.value : o) === s,
+ );
+ return m === undefined ? s : typeof m === "object" ? m.value : m;
+ };
+ return (
+ onChange(resolve(s))}
+ />
+ );
+ }
+ const opts = options.map((o) =>
+ typeof o === "object" ? o : { value: o, label: o },
+ );
+ const idx = Math.max(
+ 0,
+ opts.findIndex((o) => o.value === value),
+ );
+ const n = opts.length;
+
+ const segAt = (clientX) => {
+ const r = trackRef.current.getBoundingClientRect();
+ const inner = r.width - 4;
+ const i = Math.floor(((clientX - r.left - 2) / inner) * n);
+ return opts[Math.max(0, Math.min(n - 1, i))].value;
+ };
+
+ const onPointerDown = (e) => {
+ setDragging(true);
+ const v0 = segAt(e.clientX);
+ if (v0 !== valueRef.current) onChange(v0);
+ const move = (ev) => {
+ if (!trackRef.current) return;
+ const v = segAt(ev.clientX);
+ if (v !== valueRef.current) onChange(v);
+ };
+ const up = () => {
+ setDragging(false);
+ window.removeEventListener("pointermove", move);
+ window.removeEventListener("pointerup", up);
+ };
+ window.addEventListener("pointermove", move);
+ window.addEventListener("pointerup", up);
+ };
+
+ return (
+
+
+
+ {opts.map((o) => (
+
+ {o.label}
+
+ ))}
+
+
+ );
+}
+
+function TweakSelect({ label, value, options, onChange }) {
+ return (
+
+ onChange(e.target.value)}
+ >
+ {options.map((o) => {
+ const v = typeof o === "object" ? o.value : o;
+ const l = typeof o === "object" ? o.label : o;
+ return (
+
+ {l}
+
+ );
+ })}
+
+
+ );
+}
+
+function TweakText({ label, value, placeholder, onChange }) {
+ return (
+
+ onChange(e.target.value)}
+ />
+
+ );
+}
+
+function TweakNumber({
+ label,
+ value,
+ min,
+ max,
+ step = 1,
+ unit = "",
+ onChange,
+}) {
+ const clamp = (n) => {
+ if (min != null && n < min) return min;
+ if (max != null && n > max) return max;
+ return n;
+ };
+ const startRef = useRef({ x: 0, val: 0 });
+ const onScrubStart = (e) => {
+ e.preventDefault();
+ startRef.current = { x: e.clientX, val: value };
+ const decimals = (String(step).split(".")[1] || "").length;
+ const move = (ev) => {
+ const dx = ev.clientX - startRef.current.x;
+ const raw = startRef.current.val + dx * step;
+ const snapped = Math.round(raw / step) * step;
+ onChange(clamp(Number(snapped.toFixed(decimals))));
+ };
+ const up = () => {
+ window.removeEventListener("pointermove", move);
+ window.removeEventListener("pointerup", up);
+ };
+ window.addEventListener("pointermove", move);
+ window.addEventListener("pointerup", up);
+ };
+ return (
+
+
+ {label}
+
+ onChange(clamp(Number(e.target.value)))}
+ />
+ {unit && {unit} }
+
+ );
+}
+
+// Relative-luminance contrast pick — checkmarks drawn over a swatch need to
+// read on both #111 and #fafafa without per-option configuration. Hex input
+// only (#rgb / #rrggbb); named or rgb()/hsl() colors fall through to "light".
+function __twkIsLight(hex) {
+ const h = String(hex).replace("#", "");
+ const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0");
+ const n = parseInt(x.slice(0, 6), 16);
+ if (Number.isNaN(n)) return true;
+ const r = (n >> 16) & 255,
+ g = (n >> 8) & 255,
+ b = n & 255;
+ return r * 299 + g * 587 + b * 114 > 148000;
+}
+
+const __TwkCheck = ({ light }) => (
+
+
+
+);
+
+// TweakColor — curated color/palette picker. Each option is either a single
+// hex string or an array of 1-5 hex strings; the card adapts — a lone color
+// renders solid, a palette renders colors[0] as the hero (left ~2/3) with the
+// rest stacked in a sharp column on the right. onChange emits the
+// option in the shape it was passed (string stays string, array stays array).
+// Without options it falls back to the native color input for back-compat.
+function TweakColor({ label, value, options, onChange }) {
+ if (!options || !options.length) {
+ return (
+
+
+ {label}
+
+
onChange(e.target.value)}
+ />
+
+ );
+ }
+ // Native emits lowercase hex per the HTML spec, so
+ // compare case-insensitively. String() guards JSON.stringify(undefined),
+ // which returns the primitive undefined (no .toLowerCase).
+ const key = (o) => String(JSON.stringify(o)).toLowerCase();
+ const cur = key(value);
+ return (
+
+
+ {options.map((o, i) => {
+ const colors = Array.isArray(o) ? o : [o];
+ const [hero, ...rest] = colors;
+ const sup = rest.slice(0, 4);
+ const on = key(o) === cur;
+ return (
+ onChange(o)}
+ >
+ {sup.length > 0 && (
+
+ {sup.map((c, j) => (
+
+ ))}
+
+ )}
+ {on && <__TwkCheck light={__twkIsLight(hero)} />}
+
+ );
+ })}
+
+
+ );
+}
+
+function TweakButton({ label, onClick, secondary = false }) {
+ return (
+
+ {label}
+
+ );
+}
+
+// --- primitives.jsx ---
+// Small shared primitives: logo, arrow icon, ambient glow, eyebrow, trust strip.
+
+// The "V_" mark — bold filled V + terminal-cursor underscore. Sized via the
+// outer .logo-mark; the SVG fills it. `stroke-linejoin="round"` + a thin
+// stroke on the filled paths softens the corners just enough.
+export function LogoMark({
+ size = 26,
+ blink = true,
+}: {
+ size?: number;
+ blink?: boolean;
+}) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export function Logo({ size = 26 }: { size?: number }) {
+ return (
+
+
+ vibn
+
+ );
+}
+
+function Arrow({ size = 14 }) {
+ return (
+
+
+
+ );
+}
+
+function Eyebrow({ children }) {
+ return {children}
;
+}
+
+// Soft radial glow blob for ambient backgrounds. Place absolutely positioned.
+function Glow({
+ color = "var(--accent-glow)",
+ size = 700,
+ opacity = 1,
+ style = {},
+}) {
+ return (
+
+ );
+}
+
+function TrustStrip({ items }) {
+ return (
+
+ {items.map((item, i) => (
+
+ {i > 0 && · }
+ {item}
+
+ ))}
+
+ );
+}
+
+// A subtle gradient hairline used inside cards & frames.
+function Hairline({ vertical = false, style = {} }) {
+ return (
+
+ );
+}
+
+// --- hero.jsx ---
+// Hero: the Reddit quote headline + prompt input.
+// Visitors can type into the prompt; cycling placeholders, suggestion chips, submit handler logs to console.
+
+const HERO_PLACEHOLDERS = [
+ "A booking site for my dog grooming business…",
+ "An invoice tracker for my freelance clients…",
+ "A members-only recipe site for my supper club…",
+ "A custom CRM for our 3-person real estate team…",
+ "A tip calculator app for our restaurant staff…",
+ "A waitlist site for my new ceramics studio…",
+];
+
+const HERO_CHIPS = [
+ "📋 Client intake form",
+ "📅 Booking site",
+ "🧾 Invoice tracker",
+ "🛒 Online store",
+ "📰 Email newsletter",
+];
+
+function Hero({ onStart, variant = "quote" }) {
+ const [text, setText] = useState("");
+ const [phIdx, setPhIdx] = useState(0);
+ const [phChars, setPhChars] = useState(0);
+ const [deleting, setDeleting] = useState(false);
+ const taRef = useRef(null);
+
+ // Type-on placeholder when textarea is empty.
+ useEffect(() => {
+ if (text.length > 0) return undefined;
+ const full = HERO_PLACEHOLDERS[phIdx];
+ const speed = deleting ? 18 : 38;
+ const t = setTimeout(() => {
+ if (!deleting) {
+ if (phChars < full.length) setPhChars(phChars + 1);
+ else setTimeout(() => setDeleting(true), 1700);
+ } else {
+ if (phChars > 0) setPhChars(phChars - 1);
+ else {
+ setDeleting(false);
+ setPhIdx((phIdx + 1) % HERO_PLACEHOLDERS.length);
+ }
+ }
+ }, speed);
+ return () => clearTimeout(t);
+ }, [text, phIdx, phChars, deleting]);
+
+ const placeholder = HERO_PLACEHOLDERS[phIdx].slice(0, phChars);
+
+ const submit = () => {
+ const value = text || HERO_PLACEHOLDERS[phIdx];
+ if (onStart) onStart(value);
+ };
+
+ const useChip = (chip) => {
+ const clean = chip.replace(/^[^\w]+/, "").trim();
+ setText(`Build me ${clean.toLowerCase()} for my business.`);
+ if (taRef.current) taRef.current.focus();
+ };
+
+ return (
+
+
+
+ {/* ambient glows behind hero */}
+
+
+
+
+
+
+ Live from minute one
+
+
+ {variant === "promise" ? (
+ <>
+
+ Every small business deserves their{" "}
+ perfect tool .
+
+
+ idea → live → marketed → customers
+
+
+ Vibn is the platform for people building them.
+
+ >
+ ) : (
+ <>
+
+
+ "
+
+ I built my product,
+
+ now what
+
+ ?"
+
+
+
+ posted 2 hours ago · r/SideProject
+
+
+ Keep vibing. All the way to launch.
+
+ Your AI handles the technical stuff, puts your idea online, and
+ helps you find your first customers.
+
+ >
+ )}
+
+ {/* Prompt */}
+
+
+
+
+
+
+
+ Screenshot
+
+
+ Voice
+
+
+ Templates
+
+
+
+ Start building
+
+
+
+
+
+ {/* Suggestion chips */}
+
+ {HERO_CHIPS.map((c) => (
+ useChip(c)}
+ >
+ {c}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+function PromptIcon({ name }) {
+ const props = {
+ width: 13,
+ height: 13,
+ viewBox: "0 0 16 16",
+ fill: "none",
+ stroke: "currentColor",
+ strokeWidth: 1.5,
+ strokeLinecap: "round",
+ strokeLinejoin: "round",
+ };
+ if (name === "paperclip")
+ return (
+
+
+
+ );
+ if (name === "mic")
+ return (
+
+
+
+
+ );
+ if (name === "grid")
+ return (
+
+
+
+
+
+
+ );
+ return null;
+}
+
+// --- wall.jsx ---
+// The Wall — recreates the moment the vibe dies. Faux chat from a "generic" AI
+// coding tool that hands back a homework list. Ends on the punchline.
+
+function Wall() {
+ return (
+
+
+
+
+
+
The wall
+
+ Keep vibing. All the way to launch .
+
+
+ Your AI handles the technical stuff, puts your idea online, and
+ helps you find customers. No extra tools. No headaches.
+
+
+ Does this look familiar?
+
+
+
+
+
+
+
+
+
+
+
untitled-project · main
+
generic ai coder · chat
+
+
+
+
+
YOU
+
+
You · just now
+
+ okay it works!! how do i put this online so my customers can
+ use it?
+
+
+
+
+
+
AI
+
+
Generic AI · just now
+
+ Great job 🎉 Your app is running locally. To take it live,
+ you'll need to set a few things up first:
+
+
+
+ Sign up for Supabase and create a project for your
+ database.↗ external
+
+
+ Configure authentication with Supabase Auth or Clerk
+ — pick one.↗ external
+
+
+ Create a GitHub repo , commit your code, and push it.
+ ↗ external
+
+
+ Deploy to Vercel : connect repo, configure framework
+ preset.↗ external
+
+
+ Add environment variables for your API keys and DB
+ url in the Vercel dashboard.
+ ↗ external
+
+
+ Set up DNS for your custom domain and verify
+ nameservers with your registrar.
+ ↗ external
+
+
+ Configure SSL / TLS certificates for HTTPS (or use
+ Vercel's automatic provisioning).
+ ↗ external
+
+
+ Set up Stripe if you want to take payments, and
+ configure webhooks.↗ external
+
+
+
↓ 23 more steps
+
+
+
+
+
YOU
+
+
You · now
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ And just like that — the vibe is gone.
+
+
+
+
+ );
+}
+
+// --- crossed.jsx ---
+// Crossed-out list — technical terms struck through, ending in "Your AI handles
+// all of it. You just keep building."
+
+const CROSSED_TERMS = [
+ "Databases",
+ "Auth providers",
+ "GitHub",
+ "Hosting",
+ "API keys",
+ "Environment variables",
+ "Deployment",
+ "Backend code",
+ "Servers",
+ "DNS records",
+ "SSL certificates",
+ "CORS errors",
+ "Webhooks",
+ "Build pipelines",
+ "package.json",
+ "npm install",
+];
+
+function CrossedOut() {
+ return (
+
+
+
+
+
+
What you don't have to learn
+
+ All the stuff that made you give up last time.
+
+
Forget every word on this list.
+
+
+
+ {CROSSED_TERMS.map((term, i) => (
+
+ {term}
+
+ ))}
+
+
+
+ Your AI handles all of it .
+
+ You just keep building.
+
+
+
+ );
+}
+
+// --- journey.jsx ---
+// The Journey — 4 steps from idea → first 100 customers. A visual marker shows
+// where most tools stop. Each step shows a tiny "demo" snippet of what Vibn does.
+
+const JOURNEY_STEPS = [
+ {
+ num: "01",
+ title: "You describe it.",
+ sub: "The AI builds it.",
+ body: "Talk to it like you'd talk to a friend who codes. It builds the screens, the buttons, the logic — whatever your idea needs.",
+ demo: "describe",
+ },
+ {
+ num: "02",
+ title: "It goes live.",
+ sub: "The AI puts it online.",
+ body: "Logins, saving your stuff, hosting — handled. You get a live link from minute one. Share it. Show your friends. It just works.",
+ demo: "live",
+ },
+ {
+ num: "03",
+ title: "It gets seen.",
+ sub: "The AI markets it.",
+ body: "Posts, emails, social — written, scheduled, and shipped on autopilot. The tone matches your brand because you trained it talking to your AI.",
+ demo: "seen",
+ },
+ {
+ num: "04",
+ title: "It gets customers.",
+ sub: "Your first 100.",
+ body: "Through our Google partnership, Vibn helps the right people find your product when they're searching for what you built.",
+ demo: "customers",
+ },
+];
+
+function Journey() {
+ return (
+
+
+
+
+
+
The journey
+
+ From idea to first 100 customers.
+
+ In one chat.
+
+
+ Other tools take you to step two and wave goodbye. Vibn keeps
+ building with you.
+
+
+
+
+ {/* "Where everyone else stops" marker, sits over the gap between cards 2 and 3 */}
+
+
+
↑ Where every other tool stops
+
+
+
+ {JOURNEY_STEPS.map((step, i) => (
+
= 2} />
+ ))}
+
+
+
+ One tool. One chat. From "wouldn't it be cool if…" to{" "}
+ real customers paying you money.
+
+
+
+ );
+}
+
+function StepCard({ step, stopped }) {
+ return (
+
+
+
{step.num}
+
{step.title}
+
{step.sub}
+
{step.body}
+
+
+
+ );
+}
+
+function StepDemo({ demo }) {
+ if (demo === "describe") {
+ return (
+
+
+ YOU
+
+ build a booking site for my dog grooming biz
+
+
+
+ VIBN
+ on it — designing screens…
+
+
+ VIBN
+ ✓ booking flow ready
+
+
+ );
+ }
+ if (demo === "live") {
+ return (
+
+
+ VIBN
+ put it online
+
+
+
+
+
+
+ pawsandposh.vibn.app
+
+
+
+ ✓ logins · ✓ saving · ✓ live
+
+
+ );
+ }
+ if (demo === "seen") {
+ return (
+
+
+ VIBN
+
+ draft a launch post for Instagram + email blast
+
+
+
+ ↳ scheduled for Tue 9:00 AM
+
+
+ ↳ scheduled for Thu 6:00 PM
+
+
+ ✓ 3 channels on autopilot
+
+
+ );
+ }
+ if (demo === "customers") {
+ return (
+
+
+
+ +47this week
+
+
+
+
+
+
+
+ found you via Google
+
+
+ ✓ tracking toward 100
+
+
+ );
+ }
+ return null;
+}
+
+// --- audience.jsx ---
+// Who it's for — three audience cards, each with a Reddit-style customer quote
+// and Vibn's answer.
+
+const AUDIENCE = [
+ {
+ label: "Small business owners",
+ icon: "shop",
+ quote:
+ "I'm paying $312/month for software that does 60% of what I need and zero of the rest.",
+ source: "u/coffeeshop_owner · r/smallbusiness",
+ answer:
+ "Build the tool that actually fits your shop — exactly your workflow, no monthly fee bleed.",
+ },
+ {
+ label: "Freelancers building for clients",
+ icon: "spark",
+ quote:
+ "My client wants a quote tool. I can mock the frontend in a day. The backend? Two weeks I don't have.",
+ source: "u/agency_of_one · r/freelance",
+ answer:
+ "Deliver the whole thing — login, data, hosting — in the same chat where you built the screens.",
+ },
+ {
+ label: "Anyone with an idea",
+ icon: "spark2",
+ quote:
+ "I built the homepage in an afternoon. Then the AI told me to 'just deploy it' and I cried.",
+ source: "u/first_time_builder · r/sideproject",
+ answer:
+ "No deploys. No GitHub. No fear. The thing you described is online, with logins, ready for users.",
+ },
+];
+
+function Audience() {
+ return (
+
+
+
+
+
+
Who Vibn is for
+
+ People who have an idea — not a stack.
+
+
+ If you've ever felt this, Vibn was built for you.
+
+
+
+
+ {AUDIENCE.map((a) => (
+
+
+
{a.label}
+
+
+ "{a.quote}"
— {a.source}
+
+
+
+ Vibn
+ {a.answer}
+
+
+ ))}
+
+
+
+ );
+}
+
+function AudienceIcon({ name }) {
+ const p = {
+ width: 20,
+ height: 20,
+ viewBox: "0 0 20 20",
+ fill: "none",
+ stroke: "currentColor",
+ strokeWidth: 1.5,
+ strokeLinecap: "round",
+ strokeLinejoin: "round",
+ };
+ if (name === "shop")
+ return (
+
+
+
+
+ );
+ if (name === "spark")
+ return (
+
+
+
+ );
+ if (name === "spark2")
+ return (
+
+
+
+
+ );
+ return null;
+}
+
+// --- closing.jsx ---
+// Closing CTA + Footer.
+
+function Closing() {
+ return (
+
+
+
+
+
+
+
+
+ If you can describe it,
+
+ you can build it.
+
+
+ And you can keep building it — all the way to customers.
+
+ No new tools. No homework. No going back to the wall.
+
+
+
+
+
+ );
+}
+
+export function Footer() {
+ return (
+
+
+
+
+
+
+
+
+ 🇨🇦 Built in Canada
+ ·
+ Your data stays safe
+ ·
+ No credit card to start
+
+
+
+
© 2026 Vibn Inc. · Made for makers, not engineers.
+
+
+
+
+ );
+}
+
+// --- app.jsx ---
+// App — composes the page. Includes the sticky nav, the success modal that
+// appears when the user submits the hero prompt, and the Tweaks panel.
+
+const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/ {
+ accent: ["#ff6b47", "#ffae9a", "#9c3a1f"],
+ heroVariant: "promise",
+ showStopMarker: true,
+ showLivePill: false,
+}; /*EDITMODE-END*/
+
+const ACCENT_PRESETS = {
+ coral: ["#ff6b47", "#ffae9a", "#9c3a1f"], // warm coral (default)
+ amber: ["#ffb347", "#ffd9a3", "#9c6e1f"], // soft amber
+ lime: ["#9ee649", "#d2f3a6", "#3f7a1c"], // electric lime
+ violet: ["#b07cff", "#dabfff", "#5a2fa3"], // violet
+};
+
+function applyAccent(arr) {
+ // arr[0] is the hero color we map to var(--accent); compute soft + glow + fg.
+ const hero = arr[0];
+ const soft = `${hero}24`; // 14% alpha
+ const glow = `${hero}59`; // 35% alpha
+ const root = document.documentElement;
+ root.style.setProperty("--accent", hero);
+ root.style.setProperty("--accent-soft", soft);
+ root.style.setProperty("--accent-glow", glow);
+ // Foreground on accent: derive a dark-on-accent for primary buttons.
+ root.style.setProperty("--accent-fg", "#1a0f0a");
+}
+
+function App() {
+ const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
+ const [scrolled, setScrolled] = useState(false);
+ const [showLaunch, setShowLaunch] = useState(null);
+
+ useEffect(() => {
+ applyAccent(t.accent);
+ }, [t.accent]);
+
+ useEffect(() => {
+ const onScroll = () => setScrolled(window.scrollY > 8);
+ onScroll();
+ window.addEventListener("scroll", onScroll, { passive: true });
+ return () => window.removeEventListener("scroll", onScroll);
+ }, []);
+
+ const handleStart = (prompt) => {
+ setShowLaunch(prompt || "Build me a tool for my business.");
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {showLaunch !== null && (
+ setShowLaunch(null)} />
+ )}
+
+
+
+ setTweak("accent", v)}
+ />
+
+
+ setTweak("heroVariant", v)}
+ />
+ setTweak("showLivePill", v)}
+ />
+
+
+ setTweak("showStopMarker", v)}
+ />
+
+
+
+ {/* Tweak-driven CSS overrides */}
+
+ >
+ );
+}
+
+export function Nav({ scrolled = false }: { scrolled?: boolean }) {
+ return (
+
+
+
+ );
+}
+
+// Modal that fires when the user submits the hero prompt. Reassures them their
+// vibe will be honored — playfully sells the rest of the flow.
+function LaunchModal({ prompt, onClose }) {
+ useEffect(() => {
+ const onKey = (e) => {
+ if (e.key === "Escape") onClose();
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [onClose]);
+
+ // Preserve prompt for onboarding seeding (T12)
+ useEffect(() => {
+ if (typeof window !== "undefined" && prompt) {
+ try {
+ localStorage.setItem("vibn:firstName", prompt);
+ } catch (err) {
+ console.error("Failed to save hero prompt to localStorage:", err);
+ }
+ }
+ }, [prompt]);
+
+ const [step, setStep] = useState(0);
+ useEffect(() => {
+ if (step >= 4) return undefined;
+ const t = setTimeout(() => setStep(step + 1), 700);
+ return () => clearTimeout(t);
+ }, [step]);
+
+ const [redirectCount, setRedirectCount] = useState(3);
+ useEffect(() => {
+ if (step < 4) return undefined;
+ if (redirectCount <= 0) {
+ window.location.href = "/auth";
+ return undefined;
+ }
+ const t = setTimeout(() => setRedirectCount(redirectCount - 1), 1000);
+ return () => clearTimeout(t);
+ }, [step, redirectCount]);
+
+ return (
+
+
+
+
e.stopPropagation()}>
+
+ ✕
+
+
+ Vibn is on it
+
+
Keep vibing — we've got the rest.
+
"{prompt}"
+
+
+ {[
+ "Drafting the screens",
+ "Setting up logins",
+ "Saving your stuff",
+ "Putting it online",
+ ].map((s, i) => (
+
+ {i < step ? (
+
+
+
+ ) : i === step ? (
+
+ ) : (
+
+
+
+ )}
+
{s}
+
+ ))}
+
+
+ {step === 4 ? (
+
+ ) : (
+
+ No homework · No setup · No new tools to learn
+
+ )}
+
+
+ );
+}
+
+export default function NewSite() {
+ return (
+
+ );
+}