diff --git a/apps/web/src/app/[locale]/(marketing)/page.tsx b/apps/web/src/app/[locale]/(marketing)/page.tsx
index aad02e5..31ff5c0 100644
--- a/apps/web/src/app/[locale]/(marketing)/page.tsx
+++ b/apps/web/src/app/[locale]/(marketing)/page.tsx
@@ -1,5 +1,6 @@
-import { Hero } from "~/modules/marketing/home/hero";
+import { HeroWithMesh } from "~/modules/marketing/home/hero-with-mesh";
import { Features } from "~/modules/marketing/home/features";
+import { WhereMeshFits } from "~/modules/marketing/home/where-mesh-fits";
import { WhatIsClaudemesh } from "~/modules/marketing/home/what-is-claudemesh";
import { Timeline } from "~/modules/marketing/home/timeline";
import { Pricing } from "~/modules/marketing/home/pricing";
@@ -16,8 +17,9 @@ const HomePage = () => {
className="bg-[var(--cm-bg)] text-[var(--cm-fg)] antialiased"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
-
+
+
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/clawd.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/clawd.tsx
new file mode 100644
index 0000000..03242b5
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/clawd.tsx
@@ -0,0 +1,52 @@
+import { fccTheme } from "./theme";
+
+export type ClawdPose = "default" | "arms-up" | "look-left" | "look-right";
+
+const APPLE_EYES: Record = {
+ default: " \u2597 \u2596 ",
+ "look-left": " \u2598 \u2598 ",
+ "look-right": " \u259d \u259d ",
+ "arms-up": " \u2597 \u2596 ",
+};
+
+export function Clawd({ pose = "default" }: { pose?: ClawdPose }) {
+ const monoStyle: React.CSSProperties = {
+ fontFamily: fccTheme.fontMono,
+ color: fccTheme.clawdBody,
+ lineHeight: 1,
+ letterSpacing: 0,
+ fontVariantLigatures: "none",
+ fontFeatureSettings: '"liga" 0, "calt" 0',
+ whiteSpace: "pre",
+ };
+
+ const eyesStyle: React.CSSProperties = {
+ backgroundColor: fccTheme.clawdBody,
+ color: fccTheme.clawdBackground,
+ };
+
+ const bodyRowStyle: React.CSSProperties = {
+ backgroundColor: fccTheme.clawdBody,
+ color: fccTheme.clawdBody,
+ };
+
+ return (
+
+
+ {"\u2597"}
+ {APPLE_EYES[pose]}
+ {"\u2596"}
+
+
{" "}
+
{"\u2598\u2598 \u259d\u259d"}
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/mesh-hero.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/mesh-hero.tsx
new file mode 100644
index 0000000..819139e
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/mesh-hero.tsx
@@ -0,0 +1,327 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { Session, type SessionEvent, type SessionStep } from "./session";
+import { fccTheme } from "./theme";
+
+type Point = { x: number; y: number };
+
+type SessionConfig = {
+ id: string;
+ /** Display name used by mesh-send `to` fields to route particles */
+ displayName: string;
+ title: string;
+ cwd: string;
+ script: SessionStep[];
+ startDelayMs?: number;
+ position: {
+ xPct: number;
+ yPct: number;
+ scale?: number;
+ rotate?: number;
+ opacity?: number;
+ zIndex?: number;
+ /** 0..1 — 1 is full color, 0 is grayscale */
+ saturate?: number;
+ /** pixels — adds depth-of-field bokeh blur to background peers */
+ blurPx?: number;
+ };
+};
+
+type ArcConfig = {
+ fromId: string;
+ toId: string;
+ triggerStepKind: "mesh-send";
+};
+
+type FlyingParticle = {
+ id: number;
+ fromId: string;
+ toId: string;
+ bornAt: number;
+};
+
+type MeshHeroProps = {
+ sessions: SessionConfig[];
+ arcs?: ArcConfig[];
+ width?: number;
+ height?: number;
+};
+
+const PARTICLE_LIFE_MS = 1100;
+const TRAIL_SEGMENTS = 18;
+const TRAIL_SPAN = 0.34;
+const ICON_W = 38;
+const ICON_H = 26;
+
+export function MeshHero({
+ sessions,
+ arcs = [],
+ width = 1440,
+ height = 720,
+}: MeshHeroProps) {
+ const containerRef = useRef(null);
+ const anchorsRef = useRef>({});
+ const [particles, setParticles] = useState([]);
+ const particleIdRef = useRef(0);
+ const [, forceTick] = useState(0);
+ const [reactions, setReactions] = useState<
+ Record
+ >({});
+ const reactionTimersRef = useRef>>(
+ {},
+ );
+ const arrivedParticlesRef = useRef>(new Set());
+
+ const bumpReaction = (
+ sessionId: string,
+ kind: "receive" | "send" | "arrive",
+ ) => {
+ setReactions((prev) => ({
+ ...prev,
+ [sessionId]: { nonce: (prev[sessionId]?.nonce ?? 0) + 1, kind },
+ }));
+ };
+
+ useEffect(() => {
+ let raf = 0;
+ const loop = () => {
+ forceTick((n) => (n + 1) % 1_000_000);
+ raf = requestAnimationFrame(loop);
+ };
+ raf = requestAnimationFrame(loop);
+ return () => cancelAnimationFrame(raf);
+ }, []);
+
+ useEffect(() => {
+ if (particles.length === 0) return;
+ const now = performance.now();
+ const next = particles.filter((p) => now - p.bornAt < PARTICLE_LIFE_MS);
+ if (next.length !== particles.length) setParticles(next);
+ });
+
+ const handleEvent = (e: SessionEvent) => {
+ if (e.kind !== "mesh-send") return;
+ // Resolve destination by matching the mesh-send `to` field against
+ // session displayNames. Fall back to the configured arcs if provided.
+ const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
+ const target = normalize(e.to);
+ const toSession = sessions.find(
+ (s) => normalize(s.displayName) === target,
+ );
+ let fromId = e.sessionId;
+ let toId = toSession?.id;
+ if (!toId) {
+ const arc = arcs.find((a) => a.fromId === e.sessionId);
+ if (!arc) return;
+ toId = arc.toId;
+ }
+ if (fromId === toId) return;
+ bumpReaction(fromId, "send");
+ const id = particleIdRef.current++;
+ setParticles((prev) => [
+ ...prev,
+ {
+ id,
+ fromId,
+ toId,
+ bornAt: performance.now(),
+ },
+ ]);
+ const timer = setTimeout(
+ () => bumpReaction(toId!, "arrive"),
+ PARTICLE_LIFE_MS - 60,
+ );
+ reactionTimersRef.current[`${id}`] = timer;
+ };
+
+ useEffect(() => {
+ return () => {
+ Object.values(reactionTimersRef.current).forEach(clearTimeout);
+ };
+ }, []);
+
+ const setAnchor = (id: string) => (el: HTMLDivElement | null) => {
+ if (!el || !containerRef.current) return;
+ const container = containerRef.current.getBoundingClientRect();
+ const rect = el.getBoundingClientRect();
+ anchorsRef.current[id] = {
+ x: rect.left - container.left + rect.width / 2,
+ y: rect.top - container.top + rect.height / 2,
+ };
+ };
+
+ const arcForParticle = (fromId: string, toId: string) => {
+ const from = anchorsRef.current[fromId];
+ const to = anchorsRef.current[toId];
+ if (!from || !to) return null;
+ const midX = (from.x + to.x) / 2;
+ const midY = (from.y + to.y) / 2 - Math.abs(to.x - from.x) * 0.08 - 30;
+ return { from, to, midX, midY };
+ };
+
+ return (
+
+ {sessions.map((s) => {
+ const left = (s.position.xPct / 100) * width;
+ const top = (s.position.yPct / 100) * height;
+ const scale = s.position.scale ?? 1;
+ const rotate = s.position.rotate ?? 0;
+ const opacity = s.position.opacity ?? 1;
+ const zIndex = s.position.zIndex ?? 1;
+ const saturate = s.position.saturate ?? 1;
+ const blurPx = s.position.blurPx ?? 0;
+ const filters = [
+ "drop-shadow(0 30px 50px rgba(0,0,0,0.6))",
+ saturate !== 1 ? `saturate(${saturate})` : "",
+ blurPx > 0 ? `blur(${blurPx}px)` : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {particles.map((p) => {
+ const arc = arcForParticle(p.fromId, p.toId);
+ if (!arc) return null;
+ const age = (performance.now() - p.bornAt) / PARTICLE_LIFE_MS;
+ if (age > 1) return null;
+ const head = Math.min(1, Math.max(0, age));
+
+ const pointAt = (t: number) => {
+ const tt = Math.max(0, Math.min(1, t));
+ const inv = 1 - tt;
+ return {
+ x:
+ inv * inv * arc.from.x +
+ 2 * inv * tt * arc.midX +
+ tt * tt * arc.to.x,
+ y:
+ inv * inv * arc.from.y +
+ 2 * inv * tt * arc.midY +
+ tt * tt * arc.to.y,
+ };
+ };
+
+ const trailNodes = Array.from({ length: TRAIL_SEGMENTS }, (_, i) => {
+ const frac = i / TRAIL_SEGMENTS;
+ const t = head - frac * TRAIL_SPAN;
+ if (t < 0) return null;
+ const pt = pointAt(t);
+ const falloff = Math.pow(1 - frac, 2.2);
+ return {
+ x: pt.x,
+ y: pt.y,
+ r: 2 + falloff * 5,
+ opacity: 0.75 * falloff,
+ };
+ }).filter((n): n is NonNullable => n !== null);
+
+ const headPt = pointAt(head);
+ const iconOpacity = Math.min(1, Math.sin(head * Math.PI) * 1.2 + 0.15);
+
+ return (
+
+ {trailNodes.map((n, i) => (
+
+ ))}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/message-row.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/message-row.tsx
new file mode 100644
index 0000000..0cec585
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/message-row.tsx
@@ -0,0 +1,139 @@
+import type { ReactNode } from "react";
+import { fccTheme } from "./theme";
+
+type BaseProps = { children: ReactNode };
+
+export function UserPromptRow({ children }: BaseProps) {
+ return (
+
+ {"\u003e"}
+ {children}
+
+ );
+}
+
+export function BashRunRow({
+ command,
+ lines,
+}: {
+ command: string;
+ lines?: string[];
+}) {
+ return (
+
+
+ {"\u25cf"}
+ Bash
+ ({command})
+
+ {lines?.map((l, i) => (
+
+
+ {"\u2514"}
+
+ {l}
+
+ ))}
+
+ );
+}
+
+export function BulletRow({
+ color = "success",
+ children,
+}: {
+ color?: "success" | "error" | "dim";
+ children: ReactNode;
+}) {
+ const c =
+ color === "error"
+ ? fccTheme.error
+ : color === "dim"
+ ? fccTheme.dim
+ : fccTheme.success;
+ return (
+
+ {"\u25cf"}
+ {children}
+
+ );
+}
+
+export function ToolUseRow({
+ name,
+ args,
+ result,
+}: {
+ name: string;
+ args?: string;
+ result?: string;
+}) {
+ return (
+
+
+ {"\u25cf"}
+ {name}
+ {args && ({args}) }
+
+ {result && (
+
+
+ {"\u2514"}
+
+ {result}
+
+ )}
+
+ );
+}
+
+export function AssistantTextRow({ children }: BaseProps) {
+ return (
+
+
+ {"\u25cf"}
+
+ {children}
+
+ );
+}
+
+export function MeshMessageRow({
+ direction,
+ peer,
+ message,
+}: {
+ direction: "out" | "in";
+ peer: string;
+ message: string;
+}) {
+ const arrow = direction === "out" ? "\u2192" : "\u2190";
+ return (
+
+ mesh
+ {arrow}
+
+ {peer}
+
+ :
+ {message}
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/prompt-input.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/prompt-input.tsx
new file mode 100644
index 0000000..3253339
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/prompt-input.tsx
@@ -0,0 +1,39 @@
+import { fccTheme } from "./theme";
+
+type PromptInputProps = {
+ value?: string;
+ caret?: boolean;
+};
+
+export function PromptInput({ value = "", caret = true }: PromptInputProps) {
+ return (
+
+ {"\u003e"}
+ {value}
+ {caret && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/session.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/session.tsx
new file mode 100644
index 0000000..de063b0
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/session.tsx
@@ -0,0 +1,398 @@
+"use client";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+// useRef is still used for onEventRef below
+import { fccTheme } from "./theme";
+import { TerminalWindow } from "./terminal-window";
+import { Welcome } from "./welcome";
+import {
+ AssistantTextRow,
+ BulletRow,
+ MeshMessageRow,
+ ToolUseRow,
+ UserPromptRow,
+} from "./message-row";
+import { PromptInput } from "./prompt-input";
+import { StatusBar } from "./status-bar";
+import { ThinkingSpinner } from "./thinking-spinner";
+
+export type SessionStep =
+ | { type: "user-input"; text: string; typeMs?: number }
+ | { type: "thinking"; durationMs: number; label?: string }
+ | { type: "assistant-text"; text: string; streamMs?: number }
+ | { type: "tool-use"; name: string; args?: string; result?: string }
+ | { type: "bullet"; text: string; color?: "success" | "error" | "dim" }
+ | {
+ type: "mesh-send";
+ to: string;
+ message: string;
+ }
+ | {
+ type: "mesh-receive";
+ from: string;
+ message: string;
+ }
+ | { type: "pause"; durationMs: number };
+
+export type SessionEvent =
+ | { kind: "mesh-send"; sessionId: string; to: string; message: string; stepIndex: number }
+ | { kind: "mesh-receive"; sessionId: string; from: string; message: string; stepIndex: number }
+ | { kind: "step-start"; sessionId: string; stepIndex: number }
+ | { kind: "script-complete"; sessionId: string };
+
+export type SessionReaction = "receive" | "send" | "arrive";
+
+export type SessionProps = {
+ sessionId: string;
+ script: SessionStep[];
+ title?: string;
+ cwd?: string;
+ width?: number;
+ height?: number;
+ contextPct?: number;
+ loop?: boolean;
+ startDelayMs?: number;
+ onEvent?: (event: SessionEvent) => void;
+ /**
+ * Bumps to trigger a reaction animation. Parent increments this to fire the
+ * matching effect — e.g. an "arrive" pulse when a mesh particle lands.
+ */
+ reactionNonce?: number;
+ reactionKind?: SessionReaction;
+};
+
+type RenderedStep =
+ | { kind: "user-input"; text: string; done: boolean }
+ | { kind: "thinking"; label: string }
+ | { kind: "assistant-text"; text: string; done: boolean }
+ | { kind: "tool-use"; name: string; args?: string; result?: string }
+ | { kind: "bullet"; text: string; color: "success" | "error" | "dim" }
+ | { kind: "mesh-send"; to: string; message: string }
+ | { kind: "mesh-receive"; from: string; message: string };
+
+export function Session({
+ sessionId,
+ script,
+ title,
+ cwd = "/Users/agutierrez",
+ width = 760,
+ height = 540,
+ contextPct = 6,
+ loop = true,
+ startDelayMs = 0,
+ onEvent,
+ reactionNonce = 0,
+ reactionKind = "receive",
+}: SessionProps) {
+ const [rendered, setRendered] = useState([]);
+ const [liveInput, setLiveInput] = useState("");
+ const [cycle, setCycle] = useState(0);
+ const onEventRef = useRef(onEvent);
+ onEventRef.current = onEvent;
+
+ const scriptKey = useMemo(
+ () => script.map((s) => s.type).join("|") + "::" + sessionId,
+ [script, sessionId],
+ );
+
+ useEffect(() => {
+ let cancelled = false;
+ setRendered([]);
+ setLiveInput("");
+
+ const wait = (ms: number) =>
+ new Promise((res) => {
+ const id = setTimeout(() => {
+ if (!cancelled) res();
+ }, ms);
+ void id;
+ });
+
+ const emit = (e: SessionEvent) => {
+ if (!cancelled) onEventRef.current?.(e);
+ };
+
+ const appendStep = (step: RenderedStep) => {
+ if (cancelled) return;
+ setRendered((prev) => [...prev, step]);
+ };
+
+ const updateLast = (mut: (s: RenderedStep) => RenderedStep) => {
+ if (cancelled) return;
+ setRendered((prev) => {
+ if (prev.length === 0) return prev;
+ const next = prev.slice();
+ next[next.length - 1] = mut(next[next.length - 1]);
+ return next;
+ });
+ };
+
+ const popLast = () => {
+ if (cancelled) return;
+ setRendered((prev) => prev.slice(0, -1));
+ };
+
+ const run = async () => {
+ if (startDelayMs > 0) await wait(startDelayMs);
+ for (let i = 0; i < script.length; i++) {
+ if (cancelled) return;
+ const step = script[i];
+ emit({ kind: "step-start", sessionId, stepIndex: i });
+
+ switch (step.type) {
+ case "pause":
+ await wait(step.durationMs);
+ break;
+
+ case "user-input": {
+ const ms = step.typeMs ?? 35;
+ for (let c = 1; c <= step.text.length; c++) {
+ if (cancelled) return;
+ const slice = step.text.slice(0, c);
+ if (!cancelled) setLiveInput(slice);
+ await wait(ms);
+ }
+ // Submit: clear the input box and push the prompt into scrollback
+ await wait(260);
+ if (cancelled) return;
+ setLiveInput("");
+ appendStep({ kind: "user-input", text: step.text, done: true });
+ await wait(140);
+ break;
+ }
+
+ case "thinking": {
+ appendStep({ kind: "thinking", label: step.label ?? "Thinking" });
+ await wait(step.durationMs);
+ popLast();
+ break;
+ }
+
+ case "assistant-text": {
+ appendStep({ kind: "assistant-text", text: "", done: false });
+ const ms = step.streamMs ?? 18;
+ for (let c = 1; c <= step.text.length; c++) {
+ if (cancelled) return;
+ const slice = step.text.slice(0, c);
+ updateLast((s) =>
+ s.kind === "assistant-text" ? { ...s, text: slice } : s,
+ );
+ await wait(ms);
+ }
+ updateLast((s) =>
+ s.kind === "assistant-text" ? { ...s, done: true } : s,
+ );
+ await wait(250);
+ break;
+ }
+
+ case "tool-use": {
+ appendStep({
+ kind: "tool-use",
+ name: step.name,
+ args: step.args,
+ result: step.result,
+ });
+ await wait(400);
+ break;
+ }
+
+ case "bullet": {
+ appendStep({
+ kind: "bullet",
+ text: step.text,
+ color: step.color ?? "success",
+ });
+ await wait(200);
+ break;
+ }
+
+ case "mesh-send": {
+ appendStep({
+ kind: "mesh-send",
+ to: step.to,
+ message: step.message,
+ });
+ emit({
+ kind: "mesh-send",
+ sessionId,
+ to: step.to,
+ message: step.message,
+ stepIndex: i,
+ });
+ await wait(350);
+ break;
+ }
+
+ case "mesh-receive": {
+ appendStep({
+ kind: "mesh-receive",
+ from: step.from,
+ message: step.message,
+ });
+ emit({
+ kind: "mesh-receive",
+ sessionId,
+ from: step.from,
+ message: step.message,
+ stepIndex: i,
+ });
+ await wait(350);
+ break;
+ }
+ }
+ }
+
+ if (cancelled) return;
+ emit({ kind: "script-complete", sessionId });
+
+ if (loop) {
+ await wait(2000);
+ if (cancelled) return;
+ setCycle((c) => c + 1);
+ }
+ };
+
+ void run();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [scriptKey, cycle, loop, script, sessionId, startDelayMs]);
+
+ const reactionClass = `fcc-react-${reactionKind}`;
+ const reactionKey = `${reactionKind}-${reactionNonce}`;
+
+ return (
+ 0 ? reactionClass : undefined}
+ style={{ willChange: "transform, filter" }}
+ >
+
+
+
+ [(base)
+ agutierrez@Mac
+ ~ %
+ claude
+
+
+
+
+
+ {rendered.map((s, i) => {
+ switch (s.kind) {
+ case "user-input":
+ return (
+
+ {s.text}
+ {!s.done && }
+
+ );
+ case "thinking":
+ return (
+
+
+
+ );
+ case "assistant-text":
+ return (
+
+ {s.text}
+ {!s.done && }
+
+ );
+ case "tool-use":
+ return (
+
+ );
+ case "bullet":
+ return (
+
+ {s.text}
+
+ );
+ case "mesh-send":
+ return (
+
+ );
+ case "mesh-receive":
+ return (
+
+ );
+ }
+ })}
+
+
+
+
+
+
+ );
+}
+
+function BlinkCursor() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/status-bar.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/status-bar.tsx
new file mode 100644
index 0000000..c80f691
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/status-bar.tsx
@@ -0,0 +1,55 @@
+import { fccTheme } from "./theme";
+
+type StatusBarProps = {
+ user?: string;
+ cwd?: string;
+ model?: string;
+ contextPct?: number;
+ errorNote?: string;
+ errorAction?: string;
+};
+
+export function StatusBar({
+ user = "agutierrez@Mac",
+ cwd = "~",
+ model = "Opus 4.6 (1M context)",
+ contextPct = 6,
+ errorNote,
+ errorAction,
+}: StatusBarProps) {
+ return (
+
+
+ {user}
+ {"\u007c"}
+ {cwd}
+ {"\u007c"}
+ {model}
+ {`[ctx:${contextPct}%]`}
+
+ {errorNote && (
+
+ {errorNote}
+ {errorAction && (
+ <>
+
+ {"\u00b7"}
+
+ {errorAction}
+ >
+ )}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/terminal-window.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/terminal-window.tsx
new file mode 100644
index 0000000..44d543a
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/terminal-window.tsx
@@ -0,0 +1,127 @@
+import type { ReactNode } from "react";
+import { fccTheme } from "./theme";
+
+type TerminalWindowProps = {
+ title?: string;
+ width?: number | string;
+ height?: number | string;
+ children: ReactNode;
+};
+
+export function TerminalWindow({
+ title = "agutierrez — \u2728 Initialize new coding project — node · claude — 80\u00d724",
+ width = 760,
+ height = 520,
+ children,
+}: TerminalWindowProps) {
+ return (
+
+ );
+}
+
+function TitleBar({ title }: { title: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+ );
+}
+
+function TrafficLight({ color }: { color: string }) {
+ return (
+
+ );
+}
+
+function FolderIcon() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/theme.ts b/apps/web/src/modules/marketing/home/fake-claude-code/theme.ts
new file mode 100644
index 0000000..d2b096d
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/theme.ts
@@ -0,0 +1,16 @@
+export const fccTheme = {
+ clawdBody: "rgb(215,119,87)",
+ clawdBackground: "rgb(0,0,0)",
+ text: "rgb(255,255,255)",
+ dim: "rgb(153,153,153)",
+ subtle: "rgb(80,80,80)",
+ success: "rgb(78,186,101)",
+ error: "rgb(255,107,128)",
+ claudeShimmer: "rgb(235,159,127)",
+ promptBorder: "rgb(136,136,136)",
+ bashBorder: "rgb(253,93,177)",
+ terminalBg: "rgb(0,0,0)",
+ terminalChrome: "rgb(55,55,57)",
+ fontMono:
+ "'Menlo', 'Monaco', 'SF Mono', 'JetBrains Mono', 'Consolas', ui-monospace, monospace",
+} as const;
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/thinking-spinner.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/thinking-spinner.tsx
new file mode 100644
index 0000000..2f7ae00
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/thinking-spinner.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { fccTheme } from "./theme";
+
+const FRAMES = ["\u2847", "\u284f", "\u285f", "\u287f", "\u28ff", "\u28f7", "\u28e7", "\u28c7"];
+
+type ThinkingSpinnerProps = {
+ label?: string;
+ intervalMs?: number;
+};
+
+export function ThinkingSpinner({
+ label = "Thinking",
+ intervalMs = 80,
+}: ThinkingSpinnerProps) {
+ const [i, setI] = useState(0);
+ useEffect(() => {
+ const id = setInterval(() => setI((n) => (n + 1) % FRAMES.length), intervalMs);
+ return () => clearInterval(id);
+ }, [intervalMs]);
+
+ return (
+
+ {FRAMES[i]}
+ {label}…
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/fake-claude-code/welcome.tsx b/apps/web/src/modules/marketing/home/fake-claude-code/welcome.tsx
new file mode 100644
index 0000000..8b13290
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/fake-claude-code/welcome.tsx
@@ -0,0 +1,53 @@
+import { Clawd, type ClawdPose } from "./clawd";
+import { fccTheme } from "./theme";
+
+type WelcomeProps = {
+ pose?: ClawdPose;
+ version?: string;
+ model?: string;
+ billing?: string;
+ cwd?: string;
+};
+
+export function Welcome({
+ pose = "default",
+ version = "2.1.101",
+ model = "Opus 4.6 (1M context)",
+ billing = "Claude Max",
+ cwd = "/Users/agutierrez",
+}: WelcomeProps) {
+ return (
+
+
+
+
+
+
+ Claude Code {" "}
+ v{version}
+
+
+ {model} · {billing}
+
+
{cwd}
+
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/features.tsx b/apps/web/src/modules/marketing/home/features.tsx
index 67d304c..3747340 100644
--- a/apps/web/src/modules/marketing/home/features.tsx
+++ b/apps/web/src/modules/marketing/home/features.tsx
@@ -3,6 +3,33 @@ import { useState } from "react";
import { Reveal, SectionIcon } from "./_reveal";
const FEATURES = [
+ {
+ key: "skills",
+ tab: "Skills",
+ title: "Publish a skill once, every peer invokes it",
+ body: "Write a skill in ~/.claude/skills/review-pr, share it to the mesh, and every teammate's Claude Code has /review-pr. Update the skill on your end → every peer auto-refreshes. No manual CLAUDE.md edits, no git pulls, no copy-paste.",
+ code: `share_skill(name: "review-pr", dir: "./.claude/skills/review-pr")
+mesh_skill_deploy("review-pr")
+list_skills() → all skills live on the mesh`,
+ },
+ {
+ key: "mcps",
+ tab: "MCPs",
+ title: "Share an MCP server once, every peer sees its tools",
+ body: "Register an MCP on your machine — Postgres, Stripe, internal API, whatever — then mesh_mcp_deploy it. Every peer's Claude Code auto-discovers the tools, with per-mesh scope and audit logs. Credentials never leave your machine.",
+ code: `mesh_mcp_register("postgres-prod", command: "npx mcp-postgres")
+mesh_mcp_deploy("postgres-prod")
+mesh_mcp_catalog() → every MCP live on the mesh`,
+ },
+ {
+ key: "commands",
+ tab: "Commands",
+ title: "Slash commands that travel with the mesh",
+ body: "Any slash command you've defined — /deploy, /audit, /review-pr — can be published to the mesh. Teammates invoke it from their own Claude Code. The command runs with your logic and rules, their context. Shared muscle memory, no copying files between repos.",
+ code: `share_skill(name: "deploy", kind: "command")
+// Peer B types /deploy in their session
+// → runs your publisher-side playbook in their repo`,
+ },
{
key: "groups",
tab: "Groups",
@@ -90,7 +117,7 @@ export const Features = () => {
className="mx-auto mt-4 max-w-xl text-center text-sm text-[var(--cm-fg-tertiary)]"
style={{ fontFamily: "var(--cm-font-sans)" }}
>
- 43 MCP tools. Groups, state, memory, files, databases, vectors, streams — all shipped.
+ Skills, MCPs, slash commands, groups, state, memory, files, databases, vectors, streams — every primitive meshed, end-to-end encrypted.
diff --git a/apps/web/src/modules/marketing/home/hero-mesh-animation.tsx b/apps/web/src/modules/marketing/home/hero-mesh-animation.tsx
new file mode 100644
index 0000000..e64c77b
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/hero-mesh-animation.tsx
@@ -0,0 +1,354 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { MeshHero } from "./fake-claude-code/mesh-hero";
+import type { SessionStep } from "./fake-claude-code/session";
+
+const NATURAL_W = 1600;
+const NATURAL_H = 860;
+
+const SCRIPT_A: SessionStep[] = [
+ { type: "pause", durationMs: 400 },
+ { type: "user-input", text: "share_skill /review-pr" },
+ { type: "mesh-send", to: "Lug Nut", message: "share_skill /review-pr" },
+ { type: "pause", durationMs: 1200 },
+ { type: "mesh-receive", from: "Mou", message: "postgres-prod MCP live" },
+ { type: "pause", durationMs: 800 },
+ {
+ type: "tool-use",
+ name: "mesh_tool_call",
+ args: "postgres-prod.query",
+ result: "142 rows",
+ },
+ { type: "pause", durationMs: 1100 },
+ { type: "mesh-send", to: "Mou", message: "thanks — skill in use" },
+ { type: "pause", durationMs: 2200 },
+];
+
+const SCRIPT_B: SessionStep[] = [
+ { type: "pause", durationMs: 700 },
+ { type: "mesh-receive", from: "Alexis", message: "/review-pr shared" },
+ { type: "pause", durationMs: 800 },
+ { type: "user-input", text: "/review-pr PR #142" },
+ { type: "thinking", durationMs: 700 },
+ {
+ type: "tool-use",
+ name: "Read",
+ args: "auth/middleware.ts",
+ result: "142 lines",
+ },
+ { type: "pause", durationMs: 800 },
+ { type: "mesh-send", to: "Mou", message: "found 2 issues in auth flow" },
+ { type: "pause", durationMs: 1500 },
+ { type: "mesh-receive", from: "Alexis", message: "thanks — skill in use" },
+ { type: "pause", durationMs: 1600 },
+];
+
+const SCRIPT_C: SessionStep[] = [
+ { type: "pause", durationMs: 300 },
+ { type: "user-input", text: "expose postgres to mesh" },
+ {
+ type: "tool-use",
+ name: "mesh_mcp_deploy",
+ args: "postgres-prod",
+ result: "exposed to 6 peers",
+ },
+ { type: "mesh-send", to: "Alexis", message: "postgres-prod MCP live" },
+ { type: "pause", durationMs: 1400 },
+ {
+ type: "mesh-receive",
+ from: "Lug Nut",
+ message: "found 2 issues in auth flow",
+ },
+ { type: "pause", durationMs: 700 },
+ { type: "assistant-text", text: "Patching issues via mesh." },
+ { type: "pause", durationMs: 900 },
+ {
+ type: "mesh-send",
+ to: "Lug Nut",
+ message: "fix pushed — rerun /review-pr",
+ },
+ { type: "pause", durationMs: 1800 },
+];
+
+const SCRIPT_PIP: SessionStep[] = [
+ { type: "pause", durationMs: 1200 },
+ { type: "mesh-receive", from: "Alexis", message: "share_skill /review-pr" },
+ { type: "pause", durationMs: 1600 },
+ { type: "mesh-send", to: "Alexis", message: "cache warm" },
+ { type: "pause", durationMs: 3200 },
+];
+
+const SCRIPT_RIPPLE: SessionStep[] = [
+ { type: "pause", durationMs: 2100 },
+ { type: "mesh-receive", from: "Mou", message: "postgres-prod MCP live" },
+ { type: "pause", durationMs: 1800 },
+ { type: "mesh-send", to: "Mou", message: "mirror ready" },
+ { type: "pause", durationMs: 3000 },
+];
+
+const SCRIPT_NEBULA: SessionStep[] = [
+ { type: "pause", durationMs: 2800 },
+ { type: "mesh-receive", from: "Lug Nut", message: "need security review" },
+ { type: "pause", durationMs: 1500 },
+ { type: "mesh-send", to: "Lug Nut", message: "reviewed — LGTM" },
+ { type: "pause", durationMs: 3000 },
+];
+
+const SCRIPT_JET: SessionStep[] = [
+ { type: "pause", durationMs: 1800 },
+ { type: "mesh-receive", from: "Alexis", message: "thanks — skill in use" },
+ { type: "pause", durationMs: 1800 },
+ { type: "mesh-send", to: "Alexis", message: "heartbeat ok" },
+ { type: "pause", durationMs: 3200 },
+];
+
+const SCRIPT_VELA: SessionStep[] = [
+ { type: "pause", durationMs: 900 },
+ { type: "mesh-send", to: "Lug Nut", message: "broker uptime 99.98" },
+ { type: "pause", durationMs: 2400 },
+ { type: "mesh-receive", from: "Mou", message: "postgres-prod MCP live" },
+ { type: "pause", durationMs: 3400 },
+];
+
+const SCRIPT_OREL: SessionStep[] = [
+ { type: "pause", durationMs: 2400 },
+ { type: "mesh-receive", from: "Alexis", message: "share_skill /review-pr" },
+ { type: "pause", durationMs: 1600 },
+ { type: "mesh-send", to: "Alexis", message: "mirrored downstream" },
+ { type: "pause", durationMs: 3000 },
+];
+
+type HeroMeshAnimationProps = {
+ /**
+ * `cover` — fill both width and height of the parent, overflow clipped (for
+ * use as a hero background). `contain` — fit within width, height scales
+ * proportionally (standalone use).
+ */
+ fit?: "cover" | "contain";
+};
+
+export function HeroMeshAnimation({ fit = "contain" }: HeroMeshAnimationProps) {
+ const outerRef = useRef(null);
+ const [fitScale, setFitScale] = useState(1);
+
+ useEffect(() => {
+ const el = outerRef.current;
+ if (!el) return;
+ const compute = (w: number, h: number) => {
+ if (fit === "cover") {
+ // Pick the larger ratio so the composition fills both dimensions.
+ // Never scale below 1 in cover mode — we want overflow if the parent
+ // is smaller than the natural size.
+ const s = Math.max(w / NATURAL_W, h / NATURAL_H);
+ setFitScale(Math.max(s, 0.001));
+ } else {
+ setFitScale(Math.min(1, w / NATURAL_W));
+ }
+ };
+ const ro = new ResizeObserver((entries) => {
+ const rect = entries[0]?.contentRect;
+ if (!rect) return;
+ compute(rect.width, rect.height);
+ });
+ ro.observe(el);
+ const rect = el.getBoundingClientRect();
+ compute(rect.width, rect.height);
+ return () => ro.disconnect();
+ }, [fit]);
+
+ const isCover = fit === "cover";
+ const scaledW = NATURAL_W * fitScale;
+ const scaledH = NATURAL_H * fitScale;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/modules/marketing/home/hero-with-mesh.tsx b/apps/web/src/modules/marketing/home/hero-with-mesh.tsx
new file mode 100644
index 0000000..482c736
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/hero-with-mesh.tsx
@@ -0,0 +1,118 @@
+import Link from "next/link";
+
+import { HeroMeshAnimation } from "./hero-mesh-animation";
+import { Reveal, SectionIcon } from "./_reveal";
+
+export const HeroWithMesh = () => {
+ return (
+
+ {/* Full-bleed mesh animation as hero background */}
+
+
+
+
+ {/* Radial vignette: dark where text sits, transparent at the edges
+ so the corner peers keep pulsing visibly */}
+
+ {/* Top/bottom fades so the animation bleeds into surrounding sections */}
+
+
+
+
+
+
+
+
+
+
+
+ Your Claude Code sessions{" "}
+ work alone.
+
+
+ claudemesh connects them.
+
+
+
+
+
+
+ Share context, files, skills, and MCPs across every Claude Code
+ session — encrypted, with zero setup. The broker routes ciphertext.
+ It never reads your messages.
+
+
+
+
+
+
+ Start free
+
+ →
+
+
+
+ $
+ curl -fsSL claudemesh.com/install | bash
+
+
+
+
+
+
+ Open-source CLI · Free during public beta ·{" "}
+
+ View source
+
+
+
+
+
+ );
+};
diff --git a/apps/web/src/modules/marketing/home/where-mesh-fits.tsx b/apps/web/src/modules/marketing/home/where-mesh-fits.tsx
new file mode 100644
index 0000000..0d3ccd3
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/where-mesh-fits.tsx
@@ -0,0 +1,128 @@
+import { Reveal, SectionIcon } from "./_reveal";
+
+type Card = {
+ label: string;
+ title: string;
+ theyDo: string;
+ weDo: string;
+ tone: "compare" | "claim";
+};
+
+const CARDS: Card[] = [
+ {
+ label: "vs. MCP",
+ title: "One Claude to its tools",
+ theyDo:
+ "MCP wires one Claude session to external services — GitHub, Postgres, a browser. The tool never knows who called it, never talks back, never sees other sessions.",
+ weDo: "claudemesh ships as an MCP server itself. We extend the model: publish an MCP once, every peer's Claude Code sees its tools. Credentials stay on the publisher's machine.",
+ tone: "compare",
+ },
+ {
+ label: "vs. Subagents",
+ title: "Helpers inside one session",
+ theyDo:
+ "Subagents spawn helper agents within a single Claude Code session. They share one context, one terminal, one machine. When the session closes, they're gone.",
+ weDo: "claudemesh connects full, independent Claude Code sessions across machines, across developers, across continents. Each peer keeps its own repo, its own perspective, its own scrollback.",
+ tone: "compare",
+ },
+ {
+ label: "vs. OpenClaw",
+ title: "Autonomous agents that run while you sleep",
+ theyDo:
+ "OpenClaw runs unattended. One agent brain, many subagents, 200+ LLMs on tap. It triages issues overnight, opens PRs, pokes CI, reacts to webhooks — all without a human in the loop. Different job, and a good one.",
+ weDo: "claudemesh is about the sessions you're actively running. When your Claude Code is open and you're shipping, the mesh wires your session to your teammates'. OpenClaw automates overnight; claudemesh meshes your work hours. They compose — put an OpenClaw instance on the mesh and it joins as just another peer.",
+ tone: "compare",
+ },
+ {
+ label: "What claudemesh is",
+ title: "The wire between Claude Code sessions",
+ theyDo:
+ "Every Claude Code session today is an island. Context dies with the terminal. Skills and MCPs are per-developer. Teammates relay insights through Slack.",
+ weDo: "claudemesh is one thing: a peer network for Claude Code. Share context, files, skills, MCPs, and slash commands across sessions — end-to-end encrypted. The broker routes ciphertext. It never reads your messages.",
+ tone: "claim",
+ },
+];
+
+export const WhereMeshFits = () => {
+ return (
+
+
+
+
+
+
+
+ Where claudemesh fits
+
+
+
+
+ A quick tour of what claudemesh is — and what it isn't. We
+ compose with the rest of the Claude Code ecosystem. We don't
+ replace any of it.
+
+
+
+
+ {CARDS.map((c) => {
+ const isClaim = c.tone === "claim";
+ return (
+
+
+
+ {c.label}
+
+
+ {c.title}
+
+
+ {c.theyDo}
+
+
+
+ {c.weDo}
+
+
+
+ );
+ })}
+
+
+
+ );
+};