+
+// Good: Use useId hook
+import { useId } from 'react'
+
+function Input() {
+ const id = useId()
+ return
+}
+```
+
+### Invalid HTML Nesting
+
+```tsx
+// Bad: Invalid - div inside p
+
Content
+
+// Bad: Invalid - p inside p
+
Nested
+
+// Good: Valid nesting
+
+```
+
+### Third-party Scripts
+
+Scripts that modify DOM during hydration.
+
+```tsx
+// Good: Use next/script with afterInteractive
+import Script from 'next/script'
+
+export default function Page() {
+ return (
+
+ )
+}
+```
diff --git a/.agent/skills/next-best-practices/image.md b/.agent/skills/next-best-practices/image.md
new file mode 100644
index 000000000..aa9d28db9
--- /dev/null
+++ b/.agent/skills/next-best-practices/image.md
@@ -0,0 +1,173 @@
+# Image Optimization
+
+Use `next/image` for automatic image optimization.
+
+## Always Use next/image
+
+```tsx
+// Bad: Avoid native img
+
+
+// Good: Use next/image
+import Image from 'next/image'
+
+```
+
+## Required Props
+
+Images need explicit dimensions to prevent layout shift:
+
+```tsx
+// Local images - dimensions inferred automatically
+import heroImage from './hero.png'
+
+
+// Remote images - must specify width/height
+
+
+// Or use fill for parent-relative sizing
+
+
+
+```
+
+## Remote Images Configuration
+
+Remote domains must be configured in `next.config.js`:
+
+```js
+// next.config.js
+module.exports = {
+ images: {
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: 'example.com',
+ pathname: '/images/**',
+ },
+ {
+ protocol: 'https',
+ hostname: '*.cdn.com', // Wildcard subdomain
+ },
+ ],
+ },
+}
+```
+
+## Responsive Images
+
+Use `sizes` to tell the browser which size to download:
+
+```tsx
+// Full-width hero
+
+
+// Responsive grid (3 columns on desktop, 1 on mobile)
+
+
+// Fixed sidebar image
+
+```
+
+## Blur Placeholder
+
+Prevent layout shift with placeholders:
+
+```tsx
+// Local images - automatic blur hash
+import heroImage from './hero.png'
+
+
+// Remote images - provide blurDataURL
+
+
+// Or use color placeholder
+
+```
+
+## Priority Loading
+
+Use `priority` for above-the-fold images (LCP):
+
+```tsx
+// Hero image - loads immediately
+
+
+// Below-fold images - lazy loaded by default (no priority needed)
+
+```
+
+## Common Mistakes
+
+```tsx
+// Bad: Missing sizes with fill - downloads largest image
+
+
+// Good: Add sizes for proper responsive behavior
+
+
+// Bad: Using width/height for aspect ratio only
+
+
+// Good: Use actual display dimensions or fill with sizes
+
+
+// Bad: Remote image without config
+
+// Error: Invalid src prop, hostname not configured
+
+// Good: Add hostname to next.config.js remotePatterns
+```
+
+## Static Export
+
+When using `output: 'export'`, use `unoptimized` or custom loader:
+
+```tsx
+// Option 1: Disable optimization
+
+
+// Option 2: Global config
+// next.config.js
+module.exports = {
+ output: 'export',
+ images: { unoptimized: true },
+}
+
+// Option 3: Custom loader (Cloudinary, Imgix, etc.)
+const cloudinaryLoader = ({ src, width, quality }) => {
+ return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`
+}
+
+
+```
diff --git a/.agent/skills/next-best-practices/metadata.md b/.agent/skills/next-best-practices/metadata.md
new file mode 100644
index 000000000..5a0f6551c
--- /dev/null
+++ b/.agent/skills/next-best-practices/metadata.md
@@ -0,0 +1,301 @@
+# Metadata
+
+Add SEO metadata to Next.js pages using the Metadata API.
+
+## Important: Server Components Only
+
+The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.
+
+If the target page has `'use client'`:
+1. Remove `'use client'` if possible, move client logic to child components
+2. Or extract metadata to a parent Server Component layout
+3. Or split the file: Server Component with metadata imports Client Components
+
+## Static Metadata
+
+```tsx
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Page Title',
+ description: 'Page description for search engines',
+}
+```
+
+## Dynamic Metadata
+
+```tsx
+import type { Metadata } from 'next'
+
+type Props = { params: Promise<{ slug: string }> }
+
+export async function generateMetadata({ params }: Props): Promise
{
+ const { slug } = await params
+ const post = await getPost(slug)
+ return { title: post.title, description: post.description }
+}
+```
+
+## Avoid Duplicate Fetches
+
+Use React `cache()` when the same data is needed for both metadata and page:
+
+```tsx
+import { cache } from 'react'
+
+export const getPost = cache(async (slug: string) => {
+ return await db.posts.findFirst({ where: { slug } })
+})
+```
+
+## Viewport
+
+Separate from metadata for streaming support:
+
+```tsx
+import type { Viewport } from 'next'
+
+export const viewport: Viewport = {
+ width: 'device-width',
+ initialScale: 1,
+ themeColor: '#000000',
+}
+
+// Or dynamic
+export function generateViewport({ params }): Viewport {
+ return { themeColor: getThemeColor(params) }
+}
+```
+
+## Title Templates
+
+In root layout for consistent naming:
+
+```tsx
+export const metadata: Metadata = {
+ title: { default: 'Site Name', template: '%s | Site Name' },
+}
+```
+
+## Metadata File Conventions
+
+Reference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions
+
+Place these files in `app/` directory (or route segments):
+
+| File | Purpose |
+|------|---------|
+| `favicon.ico` | Favicon |
+| `icon.png` / `icon.svg` | App icon |
+| `apple-icon.png` | Apple app icon |
+| `opengraph-image.png` | OG image |
+| `twitter-image.png` | Twitter card image |
+| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
+| `robots.ts` / `robots.txt` | Robots directives |
+| `manifest.ts` / `manifest.json` | Web app manifest |
+
+## SEO Best Practice: Static Files Are Often Enough
+
+For most sites, **static metadata files provide excellent SEO coverage**:
+
+```
+app/
+├── favicon.ico
+├── opengraph-image.png # Works for both OG and Twitter
+├── sitemap.ts
+├── robots.ts
+└── layout.tsx # With title/description metadata
+```
+
+**Tips:**
+- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)
+- Static `title` and `description` in layout metadata is sufficient for most pages
+- Only use dynamic `generateMetadata` when content varies per page
+
+---
+
+# OG Image Generation
+
+Generate dynamic Open Graph images using `next/og`.
+
+## Important Rules
+
+1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)
+2. **No searchParams** - OG images can't access search params, use route params instead
+3. **Avoid Edge runtime** - Use default Node.js runtime
+
+```tsx
+// Good
+import { ImageResponse } from 'next/og'
+
+// Bad
+// import { ImageResponse } from '@vercel/og'
+// export const runtime = 'edge'
+```
+
+## Basic OG Image
+
+```tsx
+// app/opengraph-image.tsx
+import { ImageResponse } from 'next/og'
+
+export const alt = 'Site Name'
+export const size = { width: 1200, height: 630 }
+export const contentType = 'image/png'
+
+export default function Image() {
+ return new ImageResponse(
+ (
+
+ Hello World
+
+ ),
+ { ...size }
+ )
+}
+```
+
+## Dynamic OG Image
+
+```tsx
+// app/blog/[slug]/opengraph-image.tsx
+import { ImageResponse } from 'next/og'
+
+export const alt = 'Blog Post'
+export const size = { width: 1200, height: 630 }
+export const contentType = 'image/png'
+
+type Props = { params: Promise<{ slug: string }> }
+
+export default async function Image({ params }: Props) {
+ const { slug } = await params
+ const post = await getPost(slug)
+
+ return new ImageResponse(
+ (
+
+
{post.title}
+
{post.description}
+
+ ),
+ { ...size }
+ )
+}
+```
+
+## Custom Fonts
+
+```tsx
+import { ImageResponse } from 'next/og'
+import { join } from 'path'
+import { readFile } from 'fs/promises'
+
+export default async function Image() {
+ const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf')
+ const fontData = await readFile(fontPath)
+
+ return new ImageResponse(
+ (
+
+ Custom Font Text
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
+ }
+ )
+}
+```
+
+## File Naming
+
+- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)
+- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)
+
+## Styling Notes
+
+ImageResponse uses Flexbox layout:
+- Use `display: 'flex'`
+- No CSS Grid support
+- Styles must be inline objects
+
+## Multiple OG Images
+
+Use `generateImageMetadata` for multiple images per route:
+
+```tsx
+// app/blog/[slug]/opengraph-image.tsx
+import { ImageResponse } from 'next/og'
+
+export async function generateImageMetadata({ params }) {
+ const images = await getPostImages(params.slug)
+ return images.map((img, idx) => ({
+ id: idx,
+ alt: img.alt,
+ size: { width: 1200, height: 630 },
+ contentType: 'image/png',
+ }))
+}
+
+export default async function Image({ params, id }) {
+ const images = await getPostImages(params.slug)
+ const image = images[id]
+ return new ImageResponse(/* ... */)
+}
+```
+
+## Multiple Sitemaps
+
+Use `generateSitemaps` for large sites:
+
+```tsx
+// app/sitemap.ts
+import type { MetadataRoute } from 'next'
+
+export async function generateSitemaps() {
+ // Return array of sitemap IDs
+ return [{ id: 0 }, { id: 1 }, { id: 2 }]
+}
+
+export default async function sitemap({
+ id,
+}: {
+ id: number
+}): Promise {
+ const start = id * 50000
+ const end = start + 50000
+ const products = await getProducts(start, end)
+
+ return products.map((product) => ({
+ url: `https://example.com/product/${product.id}`,
+ lastModified: product.updatedAt,
+ }))
+}
+```
+
+Generates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.
diff --git a/.agent/skills/next-best-practices/parallel-routes.md b/.agent/skills/next-best-practices/parallel-routes.md
new file mode 100644
index 000000000..51e270d82
--- /dev/null
+++ b/.agent/skills/next-best-practices/parallel-routes.md
@@ -0,0 +1,287 @@
+# Parallel & Intercepting Routes
+
+Parallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.
+
+## File Structure
+
+```
+app/
+├── @modal/ # Parallel route slot
+│ ├── default.tsx # Required! Returns null
+│ ├── (.)photos/ # Intercepts /photos/*
+│ │ └── [id]/
+│ │ └── page.tsx # Modal content
+│ └── [...]catchall/ # Optional: catch unmatched
+│ └── page.tsx
+├── photos/
+│ └── [id]/
+│ └── page.tsx # Full page (direct access)
+├── layout.tsx # Renders both children and @modal
+└── page.tsx
+```
+
+## Step 1: Root Layout with Slot
+
+```tsx
+// app/layout.tsx
+export default function RootLayout({
+ children,
+ modal,
+}: {
+ children: React.ReactNode;
+ modal: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+ {modal}
+
+
+ );
+}
+```
+
+## Step 2: Default File (Critical!)
+
+**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.
+
+```tsx
+// app/@modal/default.tsx
+export default function Default() {
+ return null;
+}
+```
+
+Without this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.
+
+## Step 3: Intercepting Route (Modal)
+
+The `(.)` prefix intercepts routes at the same level.
+
+```tsx
+// app/@modal/(.)photos/[id]/page.tsx
+import { Modal } from '@/components/modal';
+
+export default async function PhotoModal({
+ params
+}: {
+ params: Promise<{ id: string }>
+}) {
+ const { id } = await params;
+ const photo = await getPhoto(id);
+
+ return (
+
+
+
+ );
+}
+```
+
+## Step 4: Full Page (Direct Access)
+
+```tsx
+// app/photos/[id]/page.tsx
+export default async function PhotoPage({
+ params
+}: {
+ params: Promise<{ id: string }>
+}) {
+ const { id } = await params;
+ const photo = await getPhoto(id);
+
+ return (
+
+
+
{photo.title}
+
+ );
+}
+```
+
+## Step 5: Modal Component with Correct Closing
+
+**Critical: Use `router.back()` to close modals, NOT `router.push()` or ` `.**
+
+```tsx
+// components/modal.tsx
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useCallback, useEffect, useRef } from 'react';
+
+export function Modal({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const overlayRef = useRef(null);
+
+ // Close on escape key
+ useEffect(() => {
+ function onKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Escape') {
+ router.back(); // Correct
+ }
+ }
+ document.addEventListener('keydown', onKeyDown);
+ return () => document.removeEventListener('keydown', onKeyDown);
+ }, [router]);
+
+ // Close on overlay click
+ const handleOverlayClick = useCallback((e: React.MouseEvent) => {
+ if (e.target === overlayRef.current) {
+ router.back(); // Correct
+ }
+ }, [router]);
+
+ return (
+
+
+ router.back()} // Correct!
+ className="absolute top-4 right-4"
+ >
+ Close
+
+ {children}
+
+
+ );
+}
+```
+
+### Why NOT `router.push('/')` or ` `?
+
+Using `push` or `Link` to "close" a modal:
+1. Adds a new history entry (back button shows modal again)
+2. Doesn't properly clear the intercepted route
+3. Can cause the modal to flash or persist unexpectedly
+
+`router.back()` correctly:
+1. Removes the intercepted route from history
+2. Returns to the previous page
+3. Properly unmounts the modal
+
+## Route Matcher Reference
+
+Matchers match **route segments**, not filesystem paths:
+
+| Matcher | Matches | Example |
+|---------|---------|---------|
+| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
+| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
+| `(..)(..)` | Two levels up | Rarely used |
+| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
+
+**Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment".
+
+## Handling Hard Navigation
+
+When users directly visit `/photos/123` (bookmark, refresh, shared link):
+- The intercepting route is bypassed
+- The full `photos/[id]/page.tsx` renders
+- Modal doesn't appear (expected behavior)
+
+If you want the modal to appear on direct access too, you need additional logic:
+
+```tsx
+// app/photos/[id]/page.tsx
+import { Modal } from '@/components/modal';
+
+export default async function PhotoPage({ params }) {
+ const { id } = await params;
+ const photo = await getPhoto(id);
+
+ // Option: Render as modal on direct access too
+ return (
+
+
+
+ );
+}
+```
+
+## Common Gotchas
+
+### 1. Missing `default.tsx` → 404 on Refresh
+
+Every `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).
+
+### 2. Modal Persists After Navigation
+
+You're using `router.push()` instead of `router.back()`.
+
+### 3. Nested Parallel Routes Need Defaults Too
+
+If you have `@modal` inside a route group, each level needs its own `default.tsx`:
+
+```
+app/
+├── (marketing)/
+│ ├── @modal/
+│ │ └── default.tsx # Needed!
+│ └── layout.tsx
+└── layout.tsx
+```
+
+### 4. Intercepted Route Shows Wrong Content
+
+Check your matcher:
+- `(.)photos` intercepts `/photos` from the same route level
+- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`
+
+### 5. TypeScript Errors with `params`
+
+In Next.js 15+, `params` is a Promise:
+
+```tsx
+// Correct
+export default async function Page({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+}
+```
+
+## Complete Example: Photo Gallery Modal
+
+```
+app/
+├── @modal/
+│ ├── default.tsx
+│ └── (.)photos/
+│ └── [id]/
+│ └── page.tsx
+├── photos/
+│ ├── page.tsx # Gallery grid
+│ └── [id]/
+│ └── page.tsx # Full photo page
+├── layout.tsx
+└── page.tsx
+```
+
+Links in the gallery:
+
+```tsx
+// app/photos/page.tsx
+import Link from 'next/link';
+
+export default async function Gallery() {
+ const photos = await getPhotos();
+
+ return (
+
+ {photos.map(photo => (
+
+
+
+ ))}
+
+ );
+}
+```
+
+Clicking a photo → Modal opens (intercepted)
+Direct URL → Full page renders
+Refresh while modal open → Full page renders
diff --git a/.agent/skills/next-best-practices/route-handlers.md b/.agent/skills/next-best-practices/route-handlers.md
new file mode 100644
index 000000000..25e6f4d79
--- /dev/null
+++ b/.agent/skills/next-best-practices/route-handlers.md
@@ -0,0 +1,146 @@
+# Route Handlers
+
+Create API endpoints with `route.ts` files.
+
+## Basic Usage
+
+```tsx
+// app/api/users/route.ts
+export async function GET() {
+ const users = await getUsers()
+ return Response.json(users)
+}
+
+export async function POST(request: Request) {
+ const body = await request.json()
+ const user = await createUser(body)
+ return Response.json(user, { status: 201 })
+}
+```
+
+## Supported Methods
+
+`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`
+
+## GET Handler Conflicts with page.tsx
+
+**A `route.ts` and `page.tsx` cannot coexist in the same folder.**
+
+```
+app/
+├── api/
+│ └── users/
+│ └── route.ts # /api/users
+└── users/
+ ├── page.tsx # /users (page)
+ └── route.ts # Warning: Conflicts with page.tsx!
+```
+
+If you need both a page and an API at the same path, use different paths:
+
+```
+app/
+├── users/
+│ └── page.tsx # /users (page)
+└── api/
+ └── users/
+ └── route.ts # /api/users (API)
+```
+
+## Environment Behavior
+
+Route handlers run in a **Server Component-like environment**:
+
+- Yes: Can use `async/await`
+- Yes: Can access `cookies()`, `headers()`
+- Yes: Can use Node.js APIs
+- No: Cannot use React hooks
+- No: Cannot use React DOM APIs
+- No: Cannot use browser APIs
+
+```tsx
+// Bad: This won't work - no React DOM in route handlers
+import { renderToString } from 'react-dom/server'
+
+export async function GET() {
+ const html = renderToString( ) // Error!
+ return new Response(html)
+}
+```
+
+## Dynamic Route Handlers
+
+```tsx
+// app/api/users/[id]/route.ts
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ const { id } = await params
+ const user = await getUser(id)
+
+ if (!user) {
+ return Response.json({ error: 'Not found' }, { status: 404 })
+ }
+
+ return Response.json(user)
+}
+```
+
+## Request Helpers
+
+```tsx
+export async function GET(request: Request) {
+ // URL and search params
+ const { searchParams } = new URL(request.url)
+ const query = searchParams.get('q')
+
+ // Headers
+ const authHeader = request.headers.get('authorization')
+
+ // Cookies (Next.js helper)
+ const cookieStore = await cookies()
+ const token = cookieStore.get('token')
+
+ return Response.json({ query, token })
+}
+```
+
+## Response Helpers
+
+```tsx
+// JSON response
+return Response.json({ data })
+
+// With status
+return Response.json({ error: 'Not found' }, { status: 404 })
+
+// With headers
+return Response.json(data, {
+ headers: {
+ 'Cache-Control': 'max-age=3600',
+ },
+})
+
+// Redirect
+return Response.redirect(new URL('/login', request.url))
+
+// Stream
+return new Response(stream, {
+ headers: { 'Content-Type': 'text/event-stream' },
+})
+```
+
+## When to Use Route Handlers vs Server Actions
+
+| Use Case | Route Handlers | Server Actions |
+|----------|----------------|----------------|
+| Form submissions | No | Yes |
+| Data mutations from UI | No | Yes |
+| Third-party webhooks | Yes | No |
+| External API consumption | Yes | No |
+| Public REST API | Yes | No |
+| File uploads | Both work | Both work |
+
+**Prefer Server Actions** for mutations triggered from your UI.
+**Use Route Handlers** for external integrations and public APIs.
diff --git a/.agent/skills/next-best-practices/rsc-boundaries.md b/.agent/skills/next-best-practices/rsc-boundaries.md
new file mode 100644
index 000000000..0c2208d32
--- /dev/null
+++ b/.agent/skills/next-best-practices/rsc-boundaries.md
@@ -0,0 +1,159 @@
+# RSC Boundaries
+
+Detect and prevent invalid patterns when crossing Server/Client component boundaries.
+
+## Detection Rules
+
+### 1. Async Client Components Are Invalid
+
+Client components **cannot** be async functions. Only Server Components can be async.
+
+**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`
+
+```tsx
+// Bad: async client component
+'use client'
+export default async function UserProfile() {
+ const user = await getUser() // Cannot await in client component
+ return {user.name}
+}
+
+// Good: Remove async, fetch data in parent server component
+// page.tsx (server component - no 'use client')
+export default async function Page() {
+ const user = await getUser()
+ return
+}
+
+// UserProfile.tsx (client component)
+'use client'
+export function UserProfile({ user }: { user: User }) {
+ return {user.name}
+}
+```
+
+```tsx
+// Bad: async arrow function client component
+'use client'
+const Dashboard = async () => {
+ const data = await fetchDashboard()
+ return {data}
+}
+
+// Good: Fetch in server component, pass data down
+```
+
+### 2. Non-Serializable Props to Client Components
+
+Props passed from Server → Client must be JSON-serializable.
+
+**Detect:** Server component passes these to a client component:
+- Functions (except Server Actions with `'use server'`)
+- `Date` objects
+- `Map`, `Set`, `WeakMap`, `WeakSet`
+- Class instances
+- `Symbol` (unless globally registered)
+- Circular references
+
+```tsx
+// Bad: Function prop
+// page.tsx (server)
+export default function Page() {
+ const handleClick = () => console.log('clicked')
+ return
+}
+
+// Good: Define function inside client component
+// ClientButton.tsx
+'use client'
+export function ClientButton() {
+ const handleClick = () => console.log('clicked')
+ return Click
+}
+```
+
+```tsx
+// Bad: Date object (silently becomes string, then crashes)
+// page.tsx (server)
+export default async function Page() {
+ const post = await getPost()
+ return // Date object
+}
+
+// PostCard.tsx (client) - will crash on .getFullYear()
+'use client'
+export function PostCard({ createdAt }: { createdAt: Date }) {
+ return {createdAt.getFullYear()} // Runtime error!
+}
+
+// Good: Serialize to string on server
+// page.tsx (server)
+export default async function Page() {
+ const post = await getPost()
+ return
+}
+
+// PostCard.tsx (client)
+'use client'
+export function PostCard({ createdAt }: { createdAt: string }) {
+ const date = new Date(createdAt)
+ return {date.getFullYear()}
+}
+```
+
+```tsx
+// Bad: Class instance
+const user = new UserModel(data)
+ // Methods will be stripped
+
+// Good: Pass plain object
+const user = await getUser()
+
+```
+
+```tsx
+// Bad: Map/Set
+
+
+// Good: Convert to array/object
+
+
+```
+
+### 3. Server Actions Are the Exception
+
+Functions marked with `'use server'` CAN be passed to client components.
+
+```tsx
+// Valid: Server Action can be passed
+// actions.ts
+'use server'
+export async function submitForm(formData: FormData) {
+ // server-side logic
+}
+
+// page.tsx (server)
+import { submitForm } from './actions'
+export default function Page() {
+ return // OK!
+}
+
+// ClientForm.tsx (client)
+'use client'
+export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise }) {
+ return
+}
+```
+
+## Quick Reference
+
+| Pattern | Valid? | Fix |
+|---------|--------|-----|
+| `'use client'` + `async function` | No | Fetch in server parent, pass data |
+| Pass `() => {}` to client | No | Define in client or use server action |
+| Pass `new Date()` to client | No | Use `.toISOString()` |
+| Pass `new Map()` to client | No | Convert to object/array |
+| Pass class instance to client | No | Pass plain object |
+| Pass server action to client | Yes | - |
+| Pass `string/number/boolean` | Yes | - |
+| Pass plain object/array | Yes | - |
diff --git a/.agent/skills/next-best-practices/runtime-selection.md b/.agent/skills/next-best-practices/runtime-selection.md
new file mode 100644
index 000000000..cec960d76
--- /dev/null
+++ b/.agent/skills/next-best-practices/runtime-selection.md
@@ -0,0 +1,39 @@
+# Runtime Selection
+
+## Use Node.js Runtime by Default
+
+Use the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.
+
+```tsx
+// Good: Default - no runtime config needed (uses Node.js)
+export default function Page() { ... }
+
+// Caution: Only if already used in project or specifically required
+export const runtime = 'edge'
+```
+
+## When to Use Each
+
+### Node.js Runtime (Default)
+
+- Full Node.js API support
+- File system access (`fs`)
+- Full `crypto` support
+- Database connections
+- Most npm packages work
+
+### Edge Runtime
+
+- Only for specific edge-location latency requirements
+- Limited API (no `fs`, limited `crypto`)
+- Smaller cold start
+- Geographic distribution needs
+
+## Detection
+
+**Before adding `runtime = 'edge'`**, check:
+1. Does the project already use Edge runtime?
+2. Is there a specific latency requirement?
+3. Are all dependencies Edge-compatible?
+
+If unsure, use Node.js runtime.
diff --git a/.agent/skills/next-best-practices/scripts.md b/.agent/skills/next-best-practices/scripts.md
new file mode 100644
index 000000000..4eeb744c8
--- /dev/null
+++ b/.agent/skills/next-best-practices/scripts.md
@@ -0,0 +1,141 @@
+# Scripts
+
+Loading third-party scripts in Next.js.
+
+## Use next/script
+
+Always use `next/script` instead of native `
+
+// Good: Next.js Script component
+import Script from 'next/script'
+
+
+```
+
+## Inline Scripts Need ID
+
+Inline scripts require an `id` attribute for Next.js to track them.
+
+```tsx
+// Bad: Missing id
+
+
+// Good: Has id
+
+
+// Good: Inline with id
+
+```
+
+## Don't Put Script in Head
+
+`next/script` should not be placed inside `next/head`. It handles its own positioning.
+
+```tsx
+// Bad: Script inside Head
+import Head from 'next/head'
+import Script from 'next/script'
+
+
+
+
+
+// Good: Script outside Head
+
+ Page
+
+
+```
+
+## Loading Strategies
+
+```tsx
+// afterInteractive (default) - Load after page is interactive
+
+
+// lazyOnload - Load during idle time
+
+
+// beforeInteractive - Load before page is interactive (use sparingly)
+// Only works in app/layout.tsx or pages/_document.js
+
+
+// worker - Load in web worker (experimental)
+
+```
+
+## Google Analytics
+
+Use `@next/third-parties` instead of inline GA scripts.
+
+```tsx
+// Bad: Inline GA script
+
+
+
+// Good: Next.js component
+import { GoogleAnalytics } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+ {children}
+
+
+ )
+}
+```
+
+## Google Tag Manager
+
+```tsx
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+```
+
+## Other Third-Party Scripts
+
+```tsx
+// YouTube embed
+import { YouTubeEmbed } from '@next/third-parties/google'
+
+
+
+// Google Maps
+import { GoogleMapsEmbed } from '@next/third-parties/google'
+
+
+```
+
+## Quick Reference
+
+| Pattern | Issue | Fix |
+|---------|-------|-----|
+| `
+
+// Good: Next.js Script component
+import Script from 'next/script'
+
+
+```
+
+## Inline Scripts Need ID
+
+Inline scripts require an `id` attribute for Next.js to track them.
+
+```tsx
+// Bad: Missing id
+
+
+// Good: Has id
+
+
+// Good: Inline with id
+
+```
+
+## Don't Put Script in Head
+
+`next/script` should not be placed inside `next/head`. It handles its own positioning.
+
+```tsx
+// Bad: Script inside Head
+import Head from 'next/head'
+import Script from 'next/script'
+
+
+
+
+
+// Good: Script outside Head
+
+ Page
+
+
+```
+
+## Loading Strategies
+
+```tsx
+// afterInteractive (default) - Load after page is interactive
+
+
+// lazyOnload - Load during idle time
+
+
+// beforeInteractive - Load before page is interactive (use sparingly)
+// Only works in app/layout.tsx or pages/_document.js
+
+
+// worker - Load in web worker (experimental)
+
+```
+
+## Google Analytics
+
+Use `@next/third-parties` instead of inline GA scripts.
+
+```tsx
+// Bad: Inline GA script
+
+
+
+// Good: Next.js component
+import { GoogleAnalytics } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+ {children}
+
+
+ )
+}
+```
+
+## Google Tag Manager
+
+```tsx
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+```
+
+## Other Third-Party Scripts
+
+```tsx
+// YouTube embed
+import { YouTubeEmbed } from '@next/third-parties/google'
+
+
+
+// Google Maps
+import { GoogleMapsEmbed } from '@next/third-parties/google'
+
+
+```
+
+## Quick Reference
+
+| Pattern | Issue | Fix |
+|---------|-------|-----|
+| `
+
+// Good: Next.js Script component
+import Script from 'next/script'
+
+
+```
+
+## Inline Scripts Need ID
+
+Inline scripts require an `id` attribute for Next.js to track them.
+
+```tsx
+// Bad: Missing id
+
+
+// Good: Has id
+
+
+// Good: Inline with id
+
+```
+
+## Don't Put Script in Head
+
+`next/script` should not be placed inside `next/head`. It handles its own positioning.
+
+```tsx
+// Bad: Script inside Head
+import Head from 'next/head'
+import Script from 'next/script'
+
+
+
+
+
+// Good: Script outside Head
+
+ Page
+
+
+```
+
+## Loading Strategies
+
+```tsx
+// afterInteractive (default) - Load after page is interactive
+
+
+// lazyOnload - Load during idle time
+
+
+// beforeInteractive - Load before page is interactive (use sparingly)
+// Only works in app/layout.tsx or pages/_document.js
+
+
+// worker - Load in web worker (experimental)
+
+```
+
+## Google Analytics
+
+Use `@next/third-parties` instead of inline GA scripts.
+
+```tsx
+// Bad: Inline GA script
+
+
+
+// Good: Next.js component
+import { GoogleAnalytics } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+ {children}
+
+
+ )
+}
+```
+
+## Google Tag Manager
+
+```tsx
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+```
+
+## Other Third-Party Scripts
+
+```tsx
+// YouTube embed
+import { YouTubeEmbed } from '@next/third-parties/google'
+
+
+
+// Google Maps
+import { GoogleMapsEmbed } from '@next/third-parties/google'
+
+
+```
+
+## Quick Reference
+
+| Pattern | Issue | Fix |
+|---------|-------|-----|
+| `
+
+// Good: Next.js Script component
+import Script from 'next/script'
+
+
+```
+
+## Inline Scripts Need ID
+
+Inline scripts require an `id` attribute for Next.js to track them.
+
+```tsx
+// Bad: Missing id
+
+
+// Good: Has id
+
+
+// Good: Inline with id
+
+```
+
+## Don't Put Script in Head
+
+`next/script` should not be placed inside `next/head`. It handles its own positioning.
+
+```tsx
+// Bad: Script inside Head
+import Head from 'next/head'
+import Script from 'next/script'
+
+
+
+
+
+// Good: Script outside Head
+
+ Page
+
+
+```
+
+## Loading Strategies
+
+```tsx
+// afterInteractive (default) - Load after page is interactive
+
+
+// lazyOnload - Load during idle time
+
+
+// beforeInteractive - Load before page is interactive (use sparingly)
+// Only works in app/layout.tsx or pages/_document.js
+
+
+// worker - Load in web worker (experimental)
+
+```
+
+## Google Analytics
+
+Use `@next/third-parties` instead of inline GA scripts.
+
+```tsx
+// Bad: Inline GA script
+
+
+
+// Good: Next.js component
+import { GoogleAnalytics } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+ {children}
+
+
+ )
+}
+```
+
+## Google Tag Manager
+
+```tsx
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+```
+
+## Other Third-Party Scripts
+
+```tsx
+// YouTube embed
+import { YouTubeEmbed } from '@next/third-parties/google'
+
+
+
+// Google Maps
+import { GoogleMapsEmbed } from '@next/third-parties/google'
+
+
+```
+
+## Quick Reference
+
+| Pattern | Issue | Fix |
+|---------|-------|-----|
+| `
+
+// Good: Next.js Script component
+import Script from 'next/script'
+
+
+```
+
+## Inline Scripts Need ID
+
+Inline scripts require an `id` attribute for Next.js to track them.
+
+```tsx
+// Bad: Missing id
+
+
+// Good: Has id
+
+
+// Good: Inline with id
+
+```
+
+## Don't Put Script in Head
+
+`next/script` should not be placed inside `next/head`. It handles its own positioning.
+
+```tsx
+// Bad: Script inside Head
+import Head from 'next/head'
+import Script from 'next/script'
+
+
+
+
+
+// Good: Script outside Head
+
+ Page
+
+
+```
+
+## Loading Strategies
+
+```tsx
+// afterInteractive (default) - Load after page is interactive
+
+
+// lazyOnload - Load during idle time
+
+
+// beforeInteractive - Load before page is interactive (use sparingly)
+// Only works in app/layout.tsx or pages/_document.js
+
+
+// worker - Load in web worker (experimental)
+
+```
+
+## Google Analytics
+
+Use `@next/third-parties` instead of inline GA scripts.
+
+```tsx
+// Bad: Inline GA script
+
+
+
+// Good: Next.js component
+import { GoogleAnalytics } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+ {children}
+
+
+ )
+}
+```
+
+## Google Tag Manager
+
+```tsx
+import { GoogleTagManager } from '@next/third-parties/google'
+
+export default function Layout({ children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+```
+
+## Other Third-Party Scripts
+
+```tsx
+// YouTube embed
+import { YouTubeEmbed } from '@next/third-parties/google'
+
+
+
+// Google Maps
+import { GoogleMapsEmbed } from '@next/third-parties/google'
+
+
+```
+
+## Quick Reference
+
+| Pattern | Issue | Fix |
+|---------|-------|-----|
+| `