diff --git a/CLAUDE.md b/CLAUDE.md
index 79203a83..66994993 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,8 +80,7 @@ deliberate — it is the owner's own machine. Do not add a jail without being as
src/
├── server.tsx # the single entrypoint: routes, WS upgrades, static files
├── apps/
-│ ├── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard
-│ └── landing/ # marketing landing page
+│ └── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard
├── servers/
│ ├── hono.ts # router composition; everything under /api
│ ├── _middlewares/ # auth, body parsing, the permission gate, rate limiting
diff --git a/package.json b/package.json
index 0269b1f3..105c00ea 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,6 @@
"prebuild": "bun run ./scripts/prebuild.ts",
"build:web": "bun run ./scripts/build/web.ts",
"build:dashboard": "bun run ./scripts/build/dashboard.ts",
- "build:landing": "bun run ./scripts/build/landing.ts",
"db:gen": "cd src/databases/officer_db && bun run generate",
"db:push": "bun run scripts/gen-plugin-schemas.ts && cd src/databases/officer_db && bun run push",
"db:migrate": "cd src/databases/officer_db && bun run migrate",
diff --git a/scripts/build/landing.ts b/scripts/build/landing.ts
deleted file mode 100644
index dab79f37..00000000
--- a/scripts/build/landing.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env bun
-import plugin from 'bun-plugin-tailwind';
-import { existsSync } from 'fs';
-import { rm } from 'fs/promises';
-import path from 'path';
-
-const outdir = path.join(process.cwd(), 'dist_landing');
-
-if (existsSync(outdir)) {
- console.log(`🗑️ Cleaning previous build at ${outdir}`);
- await rm(outdir, { recursive: true, force: true });
-}
-
-console.log('\n🚀 Starting landing page build...\n');
-
-const start = performance.now();
-
-const entrypoints = [...new Bun.Glob('**.html').scanSync('src/apps/landing')]
- .map((a) => path.resolve('src/apps/landing', a))
- .filter((dir) => !dir.includes('node_modules'));
-console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? 'file' : 'files'} to process\n`);
-
-const formatFileSize = (bytes: number): string => {
- const units = ['B', 'KB', 'MB', 'GB'];
- let size = bytes;
- let unitIndex = 0;
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
- return `${size.toFixed(2)} ${units[unitIndex]}`;
-};
-
-const result = await Bun.build({
- entrypoints,
- outdir,
- plugins: [plugin],
- minify: true,
- target: 'browser',
- sourcemap: 'linked',
- define: {
- 'process.env.NODE_ENV': JSON.stringify('production'),
- },
-});
-
-const end = performance.now();
-
-const outputTable = result.outputs.map((output) => ({
- File: path.relative(process.cwd(), output.path),
- Type: output.kind,
- Size: formatFileSize(output.size),
-}));
-
-console.table(outputTable);
-console.log(`\n✅ Build completed in ${(end - start).toFixed(2)}ms\n`);
diff --git a/src/apps/landing/App.tsx b/src/apps/landing/App.tsx
deleted file mode 100644
index 3f332613..00000000
--- a/src/apps/landing/App.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { PixelGrid } from '../../workspaces/components/PixelGrid';
-import { DuckAvatar } from './DuckAvatar';
-import { WaitlistForm } from './WaitlistForm';
-
-const landscapeBg = '/landscape1.webp';
-const logoSrc = '/officer-logo.svg';
-
-export const App = () => {
- return (
-
- {/* Background */}
-
-
- {/* Logo */}
-
-
-

-
-
-

-
-
-
- {/* Waitlist form */}
-
-
-
-
- );
-};
diff --git a/src/apps/landing/DuckAvatar.tsx b/src/apps/landing/DuckAvatar.tsx
deleted file mode 100644
index 6e27d5dc..00000000
--- a/src/apps/landing/DuckAvatar.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Canvas, useThree } from '@react-three/fiber';
-import { OrbitControls, useGLTF, useAnimations } from '@react-three/drei';
-import { useEffect, useState, useRef } from 'react';
-
-function DuckModel({ onLoaded }: { onLoaded: () => void }) {
- const character = useGLTF('/duck3D/Character_output.glb');
- const animations = useGLTF('/duck3D/Meshy_Merged_Animations.glb');
- const meshRef = useRef(null);
- const { actions } = useAnimations(animations.animations, meshRef);
-
- useEffect(() => {
- onLoaded();
- }, [character, animations, onLoaded]);
-
- useEffect(() => {
- if (!actions) return;
- const action = actions['Walking'];
- if (!action) return;
- action.reset();
- action.setLoop(2201, Infinity);
- action.clampWhenFinished = false;
- action.fadeIn(0.4);
- action.play();
- }, [actions]);
-
- return ;
-}
-
-function Camera() {
- const { camera } = useThree();
- const controlsRef = useRef(null);
-
- useEffect(() => {
- if (controlsRef.current) {
- controlsRef.current.target.set(0.3, 3.29, -0.4);
- camera.position.set(2.89, 5.24, 7.38);
- controlsRef.current.update();
- }
- }, [camera]);
-
- return ;
-}
-
-export function DuckAvatar() {
- const [isLoaded, setIsLoaded] = useState(false);
-
- return (
-
-
-
-
-
- );
-}
diff --git a/src/apps/landing/WaitlistForm.tsx b/src/apps/landing/WaitlistForm.tsx
deleted file mode 100644
index aceb048a..00000000
--- a/src/apps/landing/WaitlistForm.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import type { CSSProperties, FormEvent } from 'react';
-import { useEffect, useRef, useState } from 'react';
-
-const cardStyle: CSSProperties = {
- backgroundColor: 'var(--card-bg)',
- backgroundImage: `
- linear-gradient(to right, var(--card-grid-color) 1px, transparent 1px),
- linear-gradient(to bottom, var(--card-grid-color) 1px, transparent 1px)
- `,
- backgroundSize: '20px 20px',
-};
-
-export function WaitlistForm() {
- const [email, setEmail] = useState('');
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [done, setDone] = useState(false);
- const [focused, setFocused] = useState(false);
-
- const inputRef = useRef(null);
- const wrapperRef = useRef(null);
- const isValid = email.trim().length > 0 && email.includes('@');
-
- useEffect(() => {
- const vv = window.visualViewport;
- if (!vv) return;
-
- const reset = () => {
- window.scrollTo(0, 0);
- document.documentElement.scrollTop = 0;
- document.body.scrollTop = 0;
- };
-
- vv.addEventListener('resize', reset);
- vv.addEventListener('scroll', reset);
- return () => {
- vv.removeEventListener('resize', reset);
- vv.removeEventListener('scroll', reset);
- };
- }, []);
-
- const handleFocus = () => setFocused(true);
-
- const handleBlur = () => {
- setFocused(false);
- window.scrollTo(0, 0);
- };
-
- const handleSubmit = async (ev: FormEvent) => {
- ev.preventDefault();
- inputRef.current?.blur();
- const trimmed = email.trim();
- if (!trimmed || isSubmitting) return;
-
- setIsSubmitting(true);
- try {
- await fetch('https://alpha.officer.dev/api/waitlist', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email: trimmed }),
- });
- setDone(true);
- } catch {
- setDone(true);
- } finally {
- setIsSubmitting(false);
- }
- };
-
- return (
-
- {done ? (
-
-
You're on the list!
-
-
- We'll notify {email} when Officer is ready.
-
-
-
- ) : (
- <>
-
Join the Waitlist
-
- Be the first to know when Officer launches.
-
-
- >
- )}
-
- );
-}
diff --git a/src/apps/landing/index.html b/src/apps/landing/index.html
deleted file mode 100644
index e8a8172d..00000000
--- a/src/apps/landing/index.html
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
- Officer Dev (Alpha)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/apps/landing/main.tsx b/src/apps/landing/main.tsx
deleted file mode 100644
index ed3b67f5..00000000
--- a/src/apps/landing/main.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { StrictMode } from 'react';
-import { createRoot } from 'react-dom/client';
-import { App } from './App';
-import './styles.css';
-
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-);
diff --git a/src/apps/landing/styles.css b/src/apps/landing/styles.css
deleted file mode 100644
index a20ed246..00000000
--- a/src/apps/landing/styles.css
+++ /dev/null
@@ -1,92 +0,0 @@
-@import "tailwindcss";
-@plugin 'tailwindcss-animate';
-
-@custom-variant dark (&:is(.dark *));
-
-@theme {
- --color-duck-yellow: var(--duck-yellow);
- --color-duck-orange: var(--duck-orange);
- --color-duck-teal: var(--duck-teal);
- --color-duck-forest: var(--duck-forest);
- --color-duck-dark: var(--duck-dark);
- --color-duck-beige: var(--duck-beige);
-
- --color-background: hsl(var(--background));
- --color-foreground: hsl(var(--foreground));
-
- --color-primary: hsl(var(--primary));
- --color-primary-foreground: hsl(var(--primary-foreground));
-
- --color-muted: hsl(var(--muted));
- --color-muted-foreground: hsl(var(--muted-foreground));
-
- --color-input: hsl(var(--input));
- --color-ring: hsl(var(--ring));
-
- --radius-lg: var(--radius);
- --radius-md: calc(var(--radius) - 2px);
- --radius-sm: calc(var(--radius) - 4px);
-}
-
-@layer base {
- :root {
- --duck-yellow: #F4C430;
- --duck-orange: #EA580C;
- --duck-teal: #0891B2;
- --duck-forest: #166534;
- --duck-dark: #14532D;
- --duck-beige: #E7D4B5;
-
- --pixel-grid-color: rgba(20, 83, 45, 0.1);
-
- --card-bg: rgba(255, 255, 255, 0.9);
- --card-grid-color: rgba(20, 83, 45, 0.08);
-
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
-
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
-
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
-
- --input: 214.3 31.8% 91.4%;
- --ring: 222.2 84% 4.9%;
-
- --radius: 0.5rem;
- }
-
- .dark {
- --duck-teal: #22d3ee;
- --duck-dark: #f1f5f9;
- --duck-forest: #4ade80;
- --duck-beige: #292524;
-
- --pixel-grid-color: rgba(200, 230, 210, 0.08);
-
- --card-bg: rgba(10, 20, 15, 0.9);
- --card-grid-color: rgba(200, 230, 210, 0.06);
-
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
-
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
-
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
-
- --input: 217.2 32.6% 17.5%;
- --ring: 212.7 26.8% 83.9%;
- }
-
- html, body {
- @apply bg-background text-foreground;
- margin: 0;
- overflow: hidden;
- position: fixed;
- width: 100%;
- height: 100%;
- }
-}