diff --git a/CLAUDE.md b/CLAUDE.md
index 38939fb..00d3107 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,168 +1,211 @@
-# CLAUDE.md
+# Agents Guide (Single Source of Truth)
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This document is the canonical reference for humans and `Agents` working in this repository. It standardizes stack, structure, workflows, and guardrails so contributions stay consistent and safe.
-## Project Overview
+## 1) Scope and goals
-Biohazard VFX is a modern Next.js 15 website for a visual effects studio, deployed to Cloudflare Workers using OpenNext. The site showcases portfolio work, services, and provides a multi-step contact form for client intake.
+* Make onboarding fast with a single place to look
+* Define conventions so changes are predictable
+* Provide exact commands that `Agents` can run without guesswork
+* Prevent accidental regressions in routing, theming, SEO, and deployment
-**Tech Stack:**
-- Next.js 15.5.4 with App Router
-- TypeScript (strict mode)
-- React 19.1.0
-- Tailwind CSS 4
-- shadcn/ui (New York style)
-- Framer Motion for animations
-- Cloudflare Workers deployment via OpenNext
+## 2) Tech stack
-## Development Commands
+* **Framework**: Next.js 15.5.4, React 19, TypeScript
+* **Styling**: Tailwind CSS 4, shadcn/ui
+* **Animation**: Framer Motion
+* **Forms**: react-hook-form + Zod
+* **Platform**: Cloudflare Workers via OpenNext
+* **Package manager**: npm
+* **Node**: LTS 20 or 22
-```bash
-# Development
-npm run dev # Start dev server with Turbopack
-npm run type-check # Run TypeScript compiler (no emit)
-npm run lint # Run ESLint
+## 3) Project layout
-# Building
-npm run build # Standard Next.js build
-npm run build:open-next # Build for Cloudflare Workers (runs next build + open-next build)
-
-# Production
-npm start # Start Next.js production server (not used for Cloudflare deployment)
+```
+root
+├─ src/
+│ ├─ app/ # App Router pages and layouts
+│ │ ├─ (marketing)/ # Example route groups
+│ │ ├─ api/ # Route handlers
+│ │ └─ layout.tsx # Root layout, see rules below
+│ ├─ components/ # Reusable UI
+│ ├─ data/ # JSON or TS data objects consumed by pages
+│ ├─ lib/ # Utilities, hooks, server actions
+│ ├─ styles/ # globals.css, tailwind utilities if applicable
+│ └─ types/ # Shared types
+├─ public/ # Static assets
+├─ next.config.ts
+├─ tailwind.config.ts
+├─ wrangler.toml # Cloudflare deploy config
+└─ package.json
```
-## Deployment (Cloudflare Workers)
+### Import aliases
-The site deploys to Cloudflare Workers, not Vercel. Critical deployment files:
+* Prefer absolute imports using `@` mapped to `src` via `tsconfig.json` paths.
-- **`wrangler.toml`**: Cloudflare Workers configuration with routes for biohazardvfx.com
-- **`open-next.config.ts`**: OpenNext adapter configuration for Cloudflare
-- **`next.config.ts`**: Ignores lint/TypeScript errors during build (required for deployment)
+## 4) Authoritative UI system
-**Deploy commands:**
-```bash
-npx opennextjs-cloudflare build # Build for Cloudflare
-npx wrangler deploy # Deploy to Cloudflare Workers
+* **Theme**: dark mode is the default. Do not introduce light-first designs without approval.
+* **Typography**: default to Geist and Geist Mono via CSS variables. If adding a new font, add it as a variable and document it here before use.
+* **Components**: use shadcn/ui primitives. Extend with local wrappers placed in `src/components/ui/`.
+* **Spacing and rhythm**: follow Tailwind 4 defaults. Prefer utility classes over custom CSS unless componentized.
+* **Animation**: keep motion subtle. Framer Motion only for meaningful transitions.
+
+## 5) Routing and layout rules
+
+* The **root layout** owns global providers, theme class, ``, and ``. Do not duplicate these in child layouts.
+* Pages live in `src/app`. Keep server components as the default. Promote to client component only when needed.
+* Metadata must be defined per route with the Next.js Metadata API.
+
+## 6) SEO and metadata
+
+* Use the Metadata API for title, description, Open Graph, and Twitter cards.
+* Add structured data with JSON-LD in the root layout or specific routes when required.
+* All pages must render a unique `title` and `description` suitable for indexing.
+
+## 7) Forms and validation
+
+* Use `react-hook-form` with Zod schemas.
+* Surface field-level errors and a generic submit error. Never swallow validation errors.
+
+## 8) Images and assets
+
+* Use Next Image component for remote images.
+* If a new external domain is introduced, add it to `next.config.ts` remote patterns and document it here.
+* Keep `public/` for truly static assets only.
+
+## 9) Environment variables
+
+Provide a `.env.sample` and keep it in sync. Typical keys:
+
+```
+NEXT_PUBLIC_SITE_URL=
+RESEND_API_KEY=
+CF_PAGES_URL=
```
-**Live URLs:**
-- Production: https://biohazardvfx.com
-- Worker: https://biohazard-vfx-website.nicholaivogelfilms.workers.dev
+Do not commit real secrets. `Agents` must fail a task rather than hardcode a secret.
-**Cloudflare-specific requirements:**
-- Requires `nodejs_compat` compatibility flag
-- Compatibility date: `2024-09-23` or later
-- Assets binding configured at `.open-next/assets`
+## 10) Local development
-## Architecture
+```
+# install
+npm ci
-### Path Aliases
-- `@/*` maps to `./src/*` (configured in tsconfig.json)
-- shadcn/ui aliases: `@/components`, `@/components/ui`, `@/lib/utils`
+# run dev server
+npm run dev
-### App Structure (Next.js App Router)
+# type checks and linting
+npm run typecheck
+npm run lint
-Pages are in `src/app/`:
-- `page.tsx` - Homepage with hero, featured projects, capabilities
-- `about/page.tsx` - Studio origins, values, testimonials
-- `services/page.tsx` - Service offerings with ServiceCard components
-- `portfolio/page.tsx` - Masonry grid portfolio layout
-- `contact/page.tsx` - Multi-step contact form
-- `layout.tsx` - Root layout with Navigation and Footer (removed from individual pages)
-
-**Important:** Navigation and Footer components are only in the root layout. Individual pages should NOT include them.
-
-### Data Files
-
-Content is separated from components in `src/data/`:
-- `projects.ts` - Project data with Project interface (id, title, description, category, thumbnailUrl, videoUrl, aspectRatio, featured, tags)
-- `services.ts` - Services data
-
-When adding new projects or services, update these files rather than hardcoding data in components.
-
-### Component Organization
-
-Custom components in `src/components/`:
-- **Layout:** Navigation, Footer
-- **Page sections:** Hero, ContactSection, MissionSection, BrandingSection
-- **Content display:** ProjectCard, ServiceCard, MasonryGrid, ProjectShowcase, VideoPlayer, VideoPreview
-- **Interactive:** MultiStepForm (4-step client intake), HorizontalAccordion
-- **Visual effects:** DepthMap, CursorDotBackground, ScrollProgressBar, SectionDivider
-- **Third-party:** InstagramFeed, ClientLogoGrid
-
-shadcn/ui components in `src/components/ui/`:
-- Uses New York style variant
-- Includes: accordion, button, card, dialog, hover-card, input, label, navigation-menu, progress, select, separator, textarea
-
-### Styling System
-
-**Fonts:** Nine Google Fonts preloaded in layout.tsx:
-- Geist Sans (primary), Geist Mono
-- Bebas Neue, Orbitron, Inter, JetBrains Mono, Space Mono, Rajdhani, Exo 2
-- Available as CSS variables: `--font-geist-sans`, `--font-bebas`, etc.
-
-**Theme:**
-- Dark mode by default (`className="dark"` on html element)
-- CSS variables in `src/app/globals.css`
-- shadcn/ui config in `components.json` (New York style, neutral base color)
-
-**Tailwind:**
-- Tailwind CSS 4 with PostCSS
-- Config: `tailwind.config.ts`
-- Global styles: `src/app/globals.css`
-
-### SEO & Metadata
-
-All pages include Next.js Metadata API:
-- Open Graph tags
-- Twitter cards
-- Canonical links
-- JSON-LD structured data (Organization schema in root layout)
-- metadataBase: `https://biohazardvfx.com`
-
-### Forms & Validation
-
-MultiStepForm component uses:
-- react-hook-form for form state
-- zod for validation (via @hookform/resolvers)
-- 4 steps: Project Type → Budget/Timeline → Project Details → Contact Info
-- Progress indicator using shadcn/ui Progress component
-
-### Images
-
-Next.js Image optimization configured in next.config.ts:
-- Remote patterns allowed: `images.unsplash.com`
-- `unoptimized: false` (optimization enabled)
-
-## Key Constraints
-
-1. **Cloudflare deployment:** Do not suggest Vercel-specific features that won't work with OpenNext
-2. **Build config:** Lint and TypeScript errors are ignored during build (required for deployment). Do not remove these settings from next.config.ts
-3. **Dark theme only:** Site uses dark mode by default. Do not create light mode variants unless explicitly requested
-4. **Layout structure:** Navigation and Footer are in root layout only. Do not add them to individual pages
-5. **Data separation:** Project and service data lives in `src/data/`. Keep content separate from components
-
-## Common Tasks
-
-**Add a new page:**
-1. Create directory in `src/app/`
-2. Add `page.tsx` with metadata export
-3. Update Navigation component at `src/components/Navigation.tsx`
-
-**Add a new shadcn/ui component:**
-```bash
-npx shadcn@latest add [component-name]
+# build and preview
+npm run build
+npm run start
```
-**Update project portfolio:**
-1. Edit `src/data/projects.ts`
-2. Add new Project object to projects array
-3. Ensure aspectRatio is set correctly for masonry layout
+Notes
-**Update services:**
-1. Edit `src/data/services.ts`
-2. Add service object with required fields
+* The Next build may be configured to ignore ESLint and TS errors for production bundling speed. CI still gates on `lint` and `typecheck` before merge.
+
+## 11) Deployment on Cloudflare Workers with OpenNext
+
+### Required wrangler.toml settings
+
+```
+name = "site-worker"
+main = ".open-next/worker/index.mjs"
+compatibility_date = "2024-09-23"
+compatibility_flags = ["nodejs_compat"]
+assets = { directory = ".open-next/assets" }
+```
+
+### Build and deploy
+
+```
+# produce OpenNext build artifacts
+npx open-next@latest build
+
+# deploy worker and assets
+npx wrangler deploy .open-next/worker
+```
+
+Guidelines
+
+* Always run `npm run typecheck` and `npm run lint` before build.
+* Ensure `assets.directory` matches the OpenNext output.
+* Keep the compatibility date at or after 2024-09-23.
+
+## 12) Branching, commits, and CI
+
+* **Default branch**: `main` is protected.
+* **Workflow**: feature branches -> PR -> required checks -> squash merge.
+* **Commit format**: Conventional Commits. Examples
+
+ * `feat: add contact form schema`
+ * `fix: correct Image remote pattern`
+ * `chore: bump dependencies`
+* **Required checks**
+
+ * `npm run lint`
+ * `npm run typecheck`
+ * `npm run build` can be optional locally if CI runs it, but must succeed before deploy.
+
+## 13) Testing
+
+* **Unit**: place tests close to sources, name with `.test.ts` or `.test.tsx`.
+* **E2E**: optional Playwright. If used, add a `playwright.config.ts` and a `npm run e2e` script.
+
+## 14) Data and content
+
+* Non-secret content belongs in `src/data` as TS modules or JSON. Keep it presentation-agnostic.
+* Do not fetch static project data in client components. Prefer server components or file imports.
+
+## 15) `Agents` operating rules
+
+1. Read this guide before making changes.
+2. Do not alter the root layout structure for global nav or footer. Extend only via component props or slots.
+3. Before adding a dependency, justify it in the PR description and update this document if it affects conventions.
+4. When creating pages, set Metadata and verify unique title and description.
+5. If a build ignores ESLint or TS errors, CI still blocks merges on `lint` and `typecheck`. Fix the errors instead of bypassing checks.
+6. Never commit secrets. Use `.env` and keep `.env.sample` current.
+7. If you change image domains or fonts, document the change here.
+8. Prefer small, reviewable PRs. Include screenshots for UI changes.
+9. **When adding files to `public/`**, always update the middleware whitelist in `src/middleware.ts` (line 8) to allow access to the new files.
+
+## 16) Common pitfalls
+
+* Adding a remote image domain but forgetting to allow it in `next.config.ts`.
+* Introducing a client component unnecessarily and breaking streaming or SSR.
+* Duplicating navigation inside nested layouts.
+* Styling drift by bypassing Tailwind utilities and shadcn primitives.
+* **⚠️ CRITICAL - Middleware Whitelist**: `src/middleware.ts` redirects ALL routes to `/` except explicitly whitelisted paths. When adding new static assets to `public/` (images, videos, PDFs, etc.), you MUST add the path to the middleware allowlist (line 8) or the file will return a 307 redirect to `/` instead of serving. Common symptom: video/image returns "text/html" Content-Type error.
+
+## 17) Quick command reference
+
+```
+# install deps
+npm ci
+
+# develop
+npm run dev
+
+# quality gates
+npm run lint
+npm run typecheck
+
+# build and preview
+npm run build
+npm run start
+
+# open-next build and deploy
+npx open-next@latest build
+npx wrangler deploy .open-next/worker
+```
+
+## 18) Change management
+
+* Any modification to guardrails in sections 4 to 12 requires a PR that updates this document.
+* Keep this file the single place that defines expectations for humans and `Agents`.
-**Modify theme colors:**
-1. Edit CSS variables in `src/app/globals.css`
-2. Theme uses neutral base color with CSS variables for theming
diff --git a/next.config.ts b/next.config.ts
index 7064ad7..45a46e6 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -20,6 +20,20 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
+ // Custom headers for video files
+ async headers() {
+ return [
+ {
+ source: "/:path*.mp4",
+ headers: [
+ {
+ key: "Content-Type",
+ value: "video/mp4",
+ },
+ ],
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/public/reel.mp4 b/public/reel.mp4
new file mode 100644
index 0000000..41a5f8f
Binary files /dev/null and b/public/reel.mp4 differ
diff --git a/src/components/ReelPlayer.tsx b/src/components/ReelPlayer.tsx
new file mode 100644
index 0000000..e703eb3
--- /dev/null
+++ b/src/components/ReelPlayer.tsx
@@ -0,0 +1,258 @@
+"use client";
+
+import { useRef, useState, useEffect } from "react";
+import { Play, Pause, Volume2, VolumeX, Maximize, AlertCircle } from "lucide-react";
+
+interface ReelPlayerProps {
+ src: string;
+ className?: string;
+}
+
+export function ReelPlayer({ src, className = "" }: ReelPlayerProps) {
+ const videoRef = useRef(null);
+ const progressBarRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [isMuted, setIsMuted] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [volume, setVolume] = useState(1);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ const handleLoadedMetadata = () => {
+ setDuration(video.duration);
+ };
+
+ const handleTimeUpdate = () => {
+ setCurrentTime(video.currentTime);
+ };
+
+ const handleEnded = () => {
+ setIsPlaying(false);
+ };
+
+ const handleError = (e: Event) => {
+ setIsLoading(false);
+ const videoEl = e.target as HTMLVideoElement;
+ const errorCode = videoEl.error?.code;
+ const errorMessage = videoEl.error?.message;
+
+ let userMessage = "Failed to load video. ";
+ switch (errorCode) {
+ case 1:
+ userMessage += "Video loading was aborted.";
+ break;
+ case 2:
+ userMessage += "Network error occurred.";
+ break;
+ case 3:
+ userMessage += "Video format not supported by your browser.";
+ break;
+ case 4:
+ userMessage += "Video source not found.";
+ break;
+ default:
+ userMessage += errorMessage || "Unknown error.";
+ }
+
+ setError(userMessage);
+ console.error("Video error:", errorCode, errorMessage);
+ };
+
+ const handleCanPlay = () => {
+ console.log("Video canplay event fired");
+ setIsLoading(false);
+ setError(null);
+ };
+
+ const handleLoadedData = () => {
+ console.log("Video loadeddata event fired");
+ setIsLoading(false);
+ };
+
+ video.addEventListener("loadedmetadata", handleLoadedMetadata);
+ video.addEventListener("timeupdate", handleTimeUpdate);
+ video.addEventListener("ended", handleEnded);
+ video.addEventListener("error", handleError);
+ video.addEventListener("canplay", handleCanPlay);
+ video.addEventListener("loadeddata", handleLoadedData);
+
+ // Check if video is already loaded (in case events fired before listeners attached)
+ if (video.readyState >= 3) {
+ // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA
+ console.log("Video already loaded, readyState:", video.readyState);
+ setIsLoading(false);
+ if (video.duration) {
+ setDuration(video.duration);
+ }
+ }
+
+ return () => {
+ video.removeEventListener("loadedmetadata", handleLoadedMetadata);
+ video.removeEventListener("timeupdate", handleTimeUpdate);
+ video.removeEventListener("ended", handleEnded);
+ video.removeEventListener("error", handleError);
+ video.removeEventListener("canplay", handleCanPlay);
+ video.removeEventListener("loadeddata", handleLoadedData);
+ };
+ }, []);
+
+ const togglePlay = async () => {
+ const video = videoRef.current;
+ if (!video || error) return;
+
+ try {
+ if (isPlaying) {
+ video.pause();
+ setIsPlaying(false);
+ } else {
+ await video.play();
+ setIsPlaying(true);
+ }
+ } catch (err) {
+ console.error("Play error:", err);
+ setError("Unable to play video. " + (err as Error).message);
+ setIsPlaying(false);
+ }
+ };
+
+ const toggleMute = () => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ video.muted = !isMuted;
+ setIsMuted(!isMuted);
+ };
+
+ const handleProgressClick = (e: React.MouseEvent) => {
+ const video = videoRef.current;
+ const progressBar = progressBarRef.current;
+ if (!video || !progressBar) return;
+
+ const rect = progressBar.getBoundingClientRect();
+ const clickX = e.clientX - rect.left;
+ const percentage = clickX / rect.width;
+ video.currentTime = percentage * video.duration;
+ };
+
+ const toggleFullscreen = () => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ if (!document.fullscreenElement) {
+ video.requestFullscreen();
+ } else {
+ document.exitFullscreen();
+ }
+ };
+
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ };
+
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
+
+ return (
+
+ {/* Video Element */}
+
+
+ {/* Loading State */}
+ {isLoading && !error && (
+
+
Loading video...
+
+ )}
+
+ {/* Error State */}
+ {error && (
+
+
+
+ {error}
+
+
+ Try refreshing the page or using a different browser.
+