diff --git a/frontend/package.json b/frontend/package.json
index 2921914..eb67aa0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -30,6 +30,7 @@
"react-day-picker": "^10.0.1",
"react-dom": "^19.2.7",
"react-resizable-panels": "^4.11.2",
+ "react-router-dom": "^7.18.1",
"recharts": "3.8.0",
"shadcn": "^4.12.0",
"sonner": "^2.0.7",
diff --git a/frontend/src/Landing.tsx b/frontend/src/Landing.tsx
new file mode 100644
index 0000000..49d22c8
--- /dev/null
+++ b/frontend/src/Landing.tsx
@@ -0,0 +1,274 @@
+import { useCallback } from 'react';
+import { Link } from 'react-router-dom';
+import {
+ TrophyIcon,
+ ArrowRightIcon,
+ ChevronDownIcon,
+ EyeIcon,
+ GitMergeIcon,
+ UsersIcon,
+ BellIcon,
+ RadioIcon,
+ DatabaseIcon,
+ SparklesIcon,
+ PlayIcon,
+ MicIcon,
+} from 'lucide-react';
+
+/**
+ * Public marketing landing page for podman.live.
+ * Rendered at "/" without ClerkProvider — needs no secrets.
+ * The live app lives at "/app".
+ */
+export function Landing() {
+ const scrollToStory = useCallback(() => {
+ document.getElementById('story')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }, []);
+
+ return (
+
+ {/* ambient glows */}
+
+
+ {/* top nav */}
+
+
+ {/* hero */}
+
+
+
+ Top 3 — AI Engineer World's Fair Hackathon 2026
+
+
+
+
+ PodMan
+
+
+ The teammate that sees what git can't.
+
+
+ An ambient AI teammate that watches your pod's screens in realtime — catching merge
+ collisions, duplicated work, and missed handoffs before anyone pushes.
+
+
+ {/* award chips */}
+
+ {[
+ { label: '2nd Overall · 95+ teams', accent: 'text-amber-200 border-amber-300/25 bg-amber-400/10' },
+ { label: 'LiveKit Track — Winner', accent: 'text-sky-200 border-sky-300/25 bg-sky-400/10' },
+ { label: 'DigitalOcean — Best Use', accent: 'text-cyan-200 border-cyan-300/25 bg-cyan-400/10' },
+ ].map((c) => (
+
+ {c.label}
+
+ ))}
+
+
+ {/* CTAs */}
+
+
+ Read the story
+
+
+ Launch PodMan
+
+
+
+
+
+
+
+
+ {/* story */}
+
+
+
+ PodMan was built at the{' '}
+ AI Engineer World's Fair 2026 Hackathon (June
+ 27–28) against 95+ teams. It took{' '}
+ 2nd place overall , and won both the{' '}
+ LiveKit track and{' '}
+ DigitalOcean's Best Use award.
+
+
+ We shipped the live demo in a single sprint — a LiveKit room, Gemini watching the
+ screen-share, and a voice that speaks up the moment two engineers drift toward the same
+ code. There wasn't time on the clock to give it a proper home.
+
+
+ So this is that home — and we're actively building PodMan out from the hackathon
+ prototype into something a team can actually run every day.
+
+
+
+
+ {/* what it does */}
+
+
+ }
+ title="Ambient awareness"
+ body="Joins your pod's LiveKit room and samples every screen-share in realtime — no bots to invoke, no commands to remember."
+ />
+ }
+ title="Merge-collision radar"
+ body="Recognizes when two people are converging on the same files or feature and warns them before the conflict ever reaches a push."
+ />
+ }
+ title="Duplicated-work detection"
+ body="Notices when effort overlaps across the team and surfaces it early, so two engineers don't quietly build the same thing."
+ />
+ }
+ title="Handoff nudges"
+ body="Speaks up about missed handoffs and stalled threads — and learns which nudges actually helped, so it stays useful, not noisy."
+ />
+
+
+
+ {/* how it works */}
+
+
+ } name="LiveKit" note="Realtime transport for the shared room and voice delivery." />
+ } name="Gemini" note="Vision + event detection + the live voice that nudges the team." />
+ } name="MongoDB" note="Engineer state, ownership map, events, and which nudges landed." />
+
+
+ {['LiveKit', 'Gemini Live', 'MongoDB Atlas', 'Voyage embeddings', 'Modular MAX', 'Hermes ops'].map((t) => (
+
+ {t}
+
+ ))}
+
+
+
+ {/* demo */}
+
+
+
+
Watch the pod get its warning before the collision.
+
+
+
+ {/* final CTA */}
+
+
+
+
+ Put PodMan in your pod.
+
+
+ Open a room and let it watch the work. It's an early build we're actively
+ shaping — jump in and try it.
+
+
+ Launch PodMan
+
+
+
+
+ {/* footer */}
+
+
+
+
+
PodMan · podman.live
+
+
Built at AI Engineer World's Fair 2026 · 2nd Overall
+
+
+
+ );
+}
+
+function Section({
+ id,
+ eyebrow,
+ title,
+ children,
+}: {
+ id: string;
+ eyebrow: string;
+ title: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
{eyebrow}
+
{title}
+
{children}
+
+
+ );
+}
+
+function Feature({ icon, title, body }: { icon: React.ReactNode; title: string; body: string }) {
+ return (
+
+
+ {icon}
+
+
{title}
+
{body}
+
+ );
+}
+
+function Stack({ icon, name, note }: { icon: React.ReactNode; name: string; note: string }) {
+ return (
+
+
+ {icon}
+ {name}
+
+
{note}
+
+ );
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index e2082d5..8f63ca7 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,24 +1,53 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ClerkProvider } from '@clerk/react';
import { shadcn } from '@clerk/ui/themes';
import App from './App.js';
+import { Landing } from './Landing.js';
import './index.css';
import '@clerk/ui/themes/shadcn.css';
import { TooltipProvider } from '@/components/ui/tooltip';
const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
+const hasClerk = typeof clerkPublishableKey === 'string' && clerkPublishableKey.startsWith('pk_');
-if (!clerkPublishableKey) {
- throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY');
-}
-
-createRoot(document.getElementById('root')!).render(
-
-
+function AppRoute() {
+ if (!hasClerk) {
+ // Secrets not provisioned yet — show a graceful placeholder instead of crashing.
+ return (
+
+
+
PodMan is deploying
+
+ The live app is being provisioned. Check back shortly.
+
+
+ ← Back to home
+
+
+ );
+ }
+ return (
+
+ );
+}
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+ } />
+ } />
+
+
,
);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ffdb34b..f10ddf3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -154,6 +154,9 @@ importers:
react-resizable-panels:
specifier: ^4.11.2
version: 4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+ react-router-dom:
+ specifier: ^7.18.1
+ version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
recharts:
specifier: 3.8.0
version: 3.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(redux@5.0.1)
@@ -3838,6 +3841,10 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
@@ -5784,6 +5791,23 @@ packages:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
+ react-router-dom@7.18.1:
+ resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.18.1:
+ resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -5987,6 +6011,9 @@ packages:
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -10670,6 +10697,8 @@ snapshots:
cookie@0.7.2: {}
+ cookie@1.1.1: {}
+
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
@@ -12806,6 +12835,20 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
+ react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
+
+ react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
+ dependencies:
+ cookie: 1.1.1
+ react: 19.2.7
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 19.2.7(react@19.2.7)
+
react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7):
dependencies:
get-nonce: 1.0.1
@@ -13104,6 +13147,8 @@ snapshots:
set-blocking@2.0.0: {}
+ set-cookie-parser@2.7.2: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4