onselect?.(item)}
+ >
+ {@render children(item, selected?.id === item.id)}
+
+{/each}
+```
+
+### Generic with Default Type
+
+```svelte
+
+```
+
+### Generic Select Component
+
+```svelte
+
+
+Loading...
{/if}
+{:then data}
+ {@render children(data)}
+{:catch err}
+ {#if error}{@render error(err)}{:else}>(obj: T): T {
+ return JSON.parse(JSON.stringify(obj)); // Forces serialization
+}
+
+export const load = async () => {
+ const user = await db.query.users.findFirst();
+ return { user: serialize(user) };
+};
+```
+
+## BigInt
+
+```typescript
+// +page.server.ts - WRONG
+export const load = async () => {
+ return {
+ userId: 123456789012345678901234567890n, // ERROR - can't serialize
+ };
+};
+
+// +page.server.ts - RIGHT
+export const load = async () => {
+ return {
+ userId: '123456789012345678901234567890', // String
+ };
+};
+```
+
+## Detecting Serialization Issues
+
+SvelteKit will throw an error if you try to return non-serializable
+data:
+
+```
+Error: Data returned from `load` while rendering / is not serializable:
+ - Cannot stringify arbitrary non-POJOs
+```
+
+## Testing Serialization
+
+```typescript
+const data = { user: new User() };
+
+try {
+ JSON.parse(JSON.stringify(data));
+ console.log('✅ Serializable');
+} catch (e) {
+ console.log('❌ Not serializable');
+}
+```
+
+## TypeScript Help
+
+Use `satisfies` to catch issues at compile time:
+
+```typescript
+import type { PageServerLoad } from './$types';
+
+export const load = (async () => {
+ return {
+ user: {
+ id: 1,
+ name: 'Alice',
+ createdAt: new Date(), // TypeScript allows this, but it's problematic
+ },
+ };
+}) satisfies PageServerLoad;
+```
+
+Better: Create a type that only allows primitives:
+
+```typescript
+type Serializable =
+ | string
+ | number
+ | boolean
+ | null
+ | { [key: string]: Serializable }
+ | Serializable[];
+
+export const load: PageServerLoad = async () => {
+ const data: Serializable = {
+ user: {
+ id: 1,
+ name: 'Alice',
+ createdAt: new Date().toISOString(), // Must be string
+ },
+ };
+
+ return data;
+};
+```
+
+## Quick Checklist
+
+Before returning from server load or form action:
+
+1. ✅ All values are string, number, boolean, null, array, or plain
+ object?
+2. ✅ No Date objects? (convert to `.toISOString()`)
+3. ✅ No undefined? (use null)
+4. ✅ No class instances? (convert to plain objects)
+5. ✅ No Map/Set? (convert to object/array)
+6. ✅ No functions?
+7. ✅ No BigInt? (convert to string)
diff --git a/.agents/skills/sveltekit-structure/README.md b/.agents/skills/sveltekit-structure/README.md
new file mode 100644
index 0000000..ede6b2d
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/README.md
@@ -0,0 +1,17 @@
+# Sveltekit Structure
+
+SvelteKit structure guidance. Use for routing, layouts, error
+handling, and SSR. Covers file naming (+page vs +layout vs +server),
+nested layouts, error boundaries, and hydration.
+
+## Structure
+
+- `SKILL.md` - Main skill instructions
+- `references/` - Detailed documentation loaded as needed
+- `scripts/` - Executable code for deterministic operations
+- `assets/` - Templates, images, or other resources
+
+## Usage
+
+This skill is automatically discovered by Claude when relevant to the
+task.
diff --git a/.agents/skills/sveltekit-structure/SKILL.md b/.agents/skills/sveltekit-structure/SKILL.md
new file mode 100644
index 0000000..9c049b6
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/SKILL.md
@@ -0,0 +1,79 @@
+---
+name: sveltekit-structure
+# IMPORTANT: Keep description on ONE line for Claude Code compatibility
+# prettier-ignore
+description: SvelteKit structure guidance. Use for routing, layouts, error handling, SSR, or svelte:boundary. Covers file naming, nested layouts, error boundaries, pending UI, and hydration.
+---
+
+# SvelteKit Structure
+
+## Quick Start
+
+**File types:** `+page.svelte` (page) | `+layout.svelte` (wrapper) |
+`+error.svelte` (error boundary) | `+server.ts` (API endpoint)
+
+**Routes:** `src/routes/about/+page.svelte` → `/about` |
+`src/routes/posts/[id]/+page.svelte` → `/posts/123`
+
+**Layouts:** Apply to all child routes. `+layout.svelte` at any level
+wraps descendants.
+
+## Example
+
+```
+src/routes/
+├── +layout.svelte # Root layout (all pages)
+├── +page.svelte # Homepage /
+├── about/+page.svelte # /about
+└── dashboard/
+ ├── +layout.svelte # Dashboard layout (dashboard pages only)
+ ├── +page.svelte # /dashboard
+ └── settings/+page.svelte # /dashboard/settings
+```
+
+```svelte
+
+
+
+
+{@render children()}
+
+```
+
+## Reference Files
+
+- [file-naming.md](references/file-naming.md) - File naming
+ conventions
+- [layout-patterns.md](references/layout-patterns.md) - Nested layouts
+- [error-handling.md](references/error-handling.md) - +error.svelte
+ placement
+- [svelte-boundary.md](references/svelte-boundary.md) -
+ Component-level error/pending
+- [ssr-hydration.md](references/ssr-hydration.md) - SSR and
+ browser-only code
+
+## Notes
+
+- Layouts: `{@render children()}` | Errors: +error.svelte _above_
+ failing route
+- Groups: `(name)` folders don't affect URL | Client-only: check
+ `browser`
+- **Last verified:** 2025-01-11
+
+
diff --git a/.agents/skills/sveltekit-structure/references/error-handling.md b/.agents/skills/sveltekit-structure/references/error-handling.md
new file mode 100644
index 0000000..c81a084
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/references/error-handling.md
@@ -0,0 +1,274 @@
+# Error Handling
+
+## Error Boundary Placement
+
+**Key rule:** `+error.svelte` must be _above_ the failing route in the
+hierarchy.
+
+```
+src/routes/
+├── +error.svelte # Catches errors in all routes below
+├── +page.svelte # If this errors → uses +error.svelte above
+└── admin/
+ ├── +error.svelte # Catches errors in admin routes
+ └── +page.svelte # If this errors → uses admin/+error.svelte
+```
+
+**Wrong:**
+
+```
+src/routes/dashboard/
+├── +layout.svelte # If this errors...
+└── +error.svelte # This won't catch it (too low)
+```
+
+**Right:**
+
+```
+src/routes/
+├── +error.svelte # Catches dashboard layout errors
+└── dashboard/
+ ├── +layout.svelte
+ └── +error.svelte # Catches dashboard page errors
+```
+
+## Error Propagation
+
+Errors bubble up to the nearest `+error.svelte`:
+
+```
+src/routes/
+├── +error.svelte # Level 1 (root fallback)
+└── blog/
+ ├── +error.svelte # Level 2 (blog fallback)
+ └── [slug]/
+ ├── +layout.server.ts # Error here → blog/+error.svelte
+ ├── +page.server.ts # Error here → blog/+error.svelte
+ └── +page.svelte # Error here → blog/+error.svelte
+```
+
+**If no error boundary exists at that level, it goes to the parent.**
+
+## Basic Error Page
+
+```svelte
+
+
+
+{$page.status}
{$page.error.message}
+```
+
+## Custom Error Data
+
+```typescript
+// +page.server.ts
+import { error } from '@sveltejs/kit';
+
+export const load = async ({ params }) => {
+ const post = await getPost(params.id);
+
+ if (!post) {
+ throw error(404, {
+ message: 'Post not found',
+ postId: params.id,
+ });
+ }
+
+ return { post };
+};
+```
+
+```svelte
+
+
+
+{$page.status}
+{$page.error.message}
+
+{#if $page.error.postId}
+ Could not find post with ID: {$page.error.postId}
+{/if}
+```
+
+## Status Code Specific Errors
+
+```svelte
+
+
+
+{#if $page.status === 404}
+ Page Not Found
+ The page you're looking for doesn't exist.
+ Go home
+{:else if $page.status === 403}
+ Access Denied
+ You don't have permission to view this page.
+{:else if $page.status === 401}
+ Unauthorized
+ Please log in to continue.
+ Login
+{:else if $page.status >= 500}
+ Server Error
+ Something went wrong on our end. Please try again later.
+{:else}
+ Error {$page.status}
+ {$page.error.message}
+{/if}
+```
+
+## Common Status Codes
+
+- **400** - Bad Request (malformed request)
+- **401** - Unauthorized (not logged in)
+- **403** - Forbidden (logged in but no permission)
+- **404** - Not Found (resource doesn't exist)
+- **500** - Internal Server Error (unhandled exception)
+- **503** - Service Unavailable (server overloaded/down)
+
+## Error in Layout vs Page
+
+**Layout error:**
+
+```typescript
+// src/routes/dashboard/+layout.server.ts
+export const load = async ({ locals }) => {
+ if (!locals.user) {
+ throw error(401, 'Not logged in'); // Caught by +error.svelte ABOVE dashboard
+ }
+ return { user: locals.user };
+};
+```
+
+**Page error:**
+
+```typescript
+// src/routes/dashboard/settings/+page.server.ts
+export const load = async () => {
+ throw error(404, 'Settings not found'); // Caught by nearest +error.svelte
+};
+```
+
+## Expected Errors vs Unexpected Errors
+
+**Expected (use error()):**
+
+```typescript
+// User requests invalid resource
+if (!post) throw error(404, 'Post not found');
+
+// User lacks permission
+if (post.authorId !== user.id) throw error(403, 'Not your post');
+```
+
+**Unexpected (let it bubble):**
+
+```typescript
+// Unhandled exceptions (DB connection fails, etc.)
+// Will show generic 500 error
+const posts = await db.query.posts.findMany(); // Might throw
+```
+
+## Handling Errors in handleError Hook
+
+```typescript
+// src/hooks.server.ts
+import type { HandleServerError } from '@sveltejs/kit';
+
+export const handleError: HandleServerError = ({ error, event }) => {
+ // Log to error tracking service
+ console.error('Error:', error, 'Path:', event.url.pathname);
+
+ // Return user-friendly message (don't expose internals)
+ return {
+ message: 'An unexpected error occurred',
+ code: error?.code ?? 'UNKNOWN',
+ };
+};
+```
+
+## Error in Load vs Error in Component
+
+**Load function error (recommended):**
+
+```typescript
+// +page.server.ts
+export const load = async ({ params }) => {
+ const user = await getUser(params.id);
+ if (!user) throw error(404, 'User not found'); // Shows +error.svelte
+ return { user };
+};
+```
+
+**Component error (not ideal):**
+
+```svelte
+
+
+
+{data.user.name}
+```
+
+**Best practice:** Validate and throw errors in `load` functions, not
+components.
+
+## Fallback Error Handling
+
+Always have a root `+error.svelte`:
+
+```svelte
+
+
+
+Oops! Something went wrong
+
+{#if dev}
+
+ {JSON.stringify($page.error, null, 2)}
+{:else}
+
+ We're sorry, but something unexpected happened.
+{/if}
+
+Go home
+```
+
+## Testing Error Boundaries
+
+```typescript
+// +page.server.ts
+export const load = async ({ url }) => {
+ // Test error page in dev
+ if (url.searchParams.has('test-error')) {
+ throw error(500, 'Test error');
+ }
+
+ return {};
+};
+```
+
+Visit `http://localhost:5173/page?test-error` to see error boundary.
+
+## Key Takeaways
+
+1. ✅ Place `+error.svelte` ABOVE failing routes
+2. ✅ Errors bubble up to nearest error boundary
+3. ✅ Always have a root `src/routes/+error.svelte`
+4. ✅ Throw errors in `load` functions, not components
+5. ✅ Use specific status codes (401, 403, 404, 500)
+6. ✅ Customize error pages per section (blog, admin, etc.)
+7. ✅ Use `handleError` hook for logging/monitoring
+8. ✅ Show detailed errors in dev, generic in production
diff --git a/.agents/skills/sveltekit-structure/references/file-naming.md b/.agents/skills/sveltekit-structure/references/file-naming.md
new file mode 100644
index 0000000..b38409e
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/references/file-naming.md
@@ -0,0 +1,69 @@
+# File Naming Conventions
+
+## Core Files
+
+| File | Purpose | Runs | Example |
+| ------------------- | --------------------- | --------------------- | --------------------------------------------- |
+| `+page.svelte` | Page component | Client & Server (SSR) | `/routes/about/+page.svelte` → `/about` |
+| `+page.ts` | Universal load | Client & Server | Data for +page.svelte |
+| `+page.server.ts` | Server load & actions | Server only | DB queries, form actions |
+| `+layout.svelte` | Layout wrapper | Client & Server | Wraps child routes |
+| `+layout.ts` | Layout universal load | Client & Server | Data for +layout.svelte |
+| `+layout.server.ts` | Layout server load | Server only | Auth, user data |
+| `+error.svelte` | Error boundary | Client & Server | Shown when error thrown |
+| `+server.ts` | API endpoint | Server only | `/routes/api/users/+server.ts` → `/api/users` |
+
+## Route Parameters
+
+| Pattern | Matches | Example |
+| -------------- | -------------- | ---------------------------------------------------------------- |
+| `[id]` | Single param | `/posts/[id]/+page.svelte` → `/posts/123` |
+| `[slug]` | Single param | `/blog/[slug]/+page.svelte` → `/blog/hello-world` |
+| `[[optional]]` | Optional param | `/search/[[query]]/+page.svelte` → `/search` or `/search/svelte` |
+| `[...rest]` | Rest params | `/docs/[...path]/+page.svelte` → `/docs/a/b/c` |
+
+## Route Groups
+
+| Pattern | Purpose | URL |
+| ------------- | ---------------------------- | ---------------------------------------------- |
+| `(group)` | Group routes (no URL impact) | `/(app)/dashboard/+page.svelte` → `/dashboard` |
+| `(marketing)` | Separate layouts | Different layout for marketing pages |
+
+## Special Files
+
+- `hooks.server.ts` - Server hooks (handle function, runs on every
+ request)
+- `hooks.client.ts` - Client hooks (runs in browser)
+- `app.html` - HTML template
+- `service-worker.ts` - Service worker
+- `params/*.ts` - Param validators
+
+## Common Patterns
+
+```
+src/routes/
+├── +layout.svelte # Root layout
+├── +layout.ts # Root data
+├── +page.svelte # Homepage
+├── +error.svelte # Root error boundary
+│
+├── (app)/ # App routes (grouped)
+│ ├── +layout.svelte # App layout (auth required)
+│ ├── dashboard/+page.svelte # /dashboard
+│ └── settings/+page.svelte # /settings
+│
+├── (marketing)/ # Marketing routes (grouped)
+│ ├── +layout.svelte # Marketing layout
+│ ├── about/+page.svelte # /about
+│ └── pricing/+page.svelte # /pricing
+│
+├── blog/
+│ ├── +page.svelte # /blog (list)
+│ └── [slug]/
+│ ├── +page.svelte # /blog/post-title
+│ └── +page.server.ts # Load post data
+│
+└── api/
+ └── posts/
+ └── +server.ts # API: GET/POST /api/posts
+```
diff --git a/.agents/skills/sveltekit-structure/references/layout-patterns.md b/.agents/skills/sveltekit-structure/references/layout-patterns.md
new file mode 100644
index 0000000..7b251d6
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/references/layout-patterns.md
@@ -0,0 +1,387 @@
+# Layout Patterns
+
+## Basic Layout
+
+```svelte
+
+
+
+
+{@render children()}
+
+```
+
+**Key points:**
+
+- Must declare `children` in `$props()`
+- Use `{@render children()}` to render nested content
+- Root layout wraps ALL pages
+
+## Nested Layouts
+
+Layouts inherit from parent layouts:
+
+```
+src/routes/
+├── +layout.svelte # Root layout (all pages)
+└── dashboard/
+ ├── +layout.svelte # Dashboard layout (dashboard pages only)
+ └── +page.svelte # Uses both layouts
+```
+
+**Result:** Root layout wraps dashboard layout wraps page.
+
+**Rendering order:**
+
+```
+Root +layout.svelte
+ └─ Dashboard +layout.svelte
+ └─ +page.svelte
+```
+
+### Example: Nested Layout Pattern
+
+```svelte
+
+
+
+
+
+ {@render children()}
+
+```
+
+```svelte
+
+
+
+
+
+
{@render children()}
+
+```
+
+```svelte
+Dashboard Home
+```
+
+**Rendered HTML structure:**
+
+```html
+
+```
+
+## Layout Groups
+
+Use `(groups)` to organize without affecting URLs:
+
+```
+src/routes/
+├── (marketing)/
+│ ├── +layout.svelte # Marketing layout
+│ ├── about/+page.svelte # /about (uses marketing layout)
+│ └── pricing/+page.svelte # /pricing (uses marketing layout)
+│
+└── (app)/
+ ├── +layout.svelte # App layout
+ ├── dashboard/+page.svelte # /dashboard (uses app layout)
+ └── settings/+page.svelte # /settings (uses app layout)
+```
+
+**Key points:**
+
+- Parentheses make groups invisible in URLs
+- `/about` route, NOT `/(marketing)/about`
+- Different layouts for different sections
+- Useful for authentication boundaries
+
+### Example: Marketing vs App Layouts
+
+```svelte
+
+
+
+
+
+{@render children()}
+
+
+```
+
+```svelte
+
+
+
+
+
+{@render children()}
+```
+
+## Reset Layout
+
+Use `@` in filename to break layout inheritance:
+
+**NOT RECOMMENDED** - This feature is deprecated in SvelteKit 2+.
+
+Instead, use layout groups to create separate hierarchies:
+
+```
+src/routes/
+├── +layout.svelte # Root layout (for most pages)
+├── (app)/
+│ └── +layout.svelte # App layout
+└── (public)/
+ └── +layout.svelte # Public layout (completely separate)
+```
+
+## Layout with Data Loading
+
+```svelte
+
+export const load = async ({ locals }) => {
+ // Available to all child routes
+ return {
+ user: locals.user,
+ };
+};
+```
+
+```svelte
+
+
+
+
+ {#if data.user}
+ Welcome, {data.user.name}
+ {:else}
+ Login
+ {/if}
+
+
+{@render children()}
+```
+
+## Conditional Layout Content
+
+```svelte
+
+
+
+{#if !$page.url.pathname.startsWith('/admin')}
+
+{/if}
+
+{@render children()}
+
+{#if !$page.url.pathname.includes('/checkout')}
+
+{/if}
+```
+
+**Better approach:** Use layout groups instead of conditionals.
+
+## Protected Layouts
+
+```typescript
+// src/routes/(app)/+layout.server.ts
+import { redirect } from '@sveltejs/kit';
+
+export const load = async ({ locals }) => {
+ if (!locals.user) {
+ throw redirect(303, '/login');
+ }
+
+ return {
+ user: locals.user,
+ };
+};
+```
+
+All routes under `(app)` group now require authentication.
+
+## Sharing Layout State
+
+```svelte
+
+
+
+{@render children()}
+```
+
+```svelte
+
+
+
+Hello, {user.name}
+```
+
+## Layout Slot Props (Snippets)
+
+```svelte
+
+
+
+
+
+
+ {#if header}
+
+ {/if}
+ {@render children()}
+
+
+```
+
+```svelte
+
+{#snippet header()}
+ Custom Dashboard Header
+{/snippet}
+
+Dashboard content
+```
+
+## Common Patterns
+
+### Pattern 1: Auth Boundary
+
+```
+src/routes/
+├── +layout.svelte # Root (no auth)
+├── (public)/
+│ ├── +layout.svelte # Public layout (landing pages)
+│ ├── +page.svelte # /
+│ └── about/+page.svelte # /about
+└── (protected)/
+ ├── +layout.server.ts # Check auth, redirect if not logged in
+ ├── +layout.svelte # Protected layout (app UI)
+ ├── dashboard/+page.svelte # /dashboard
+ └── settings/+page.svelte # /settings
+```
+
+### Pattern 2: Multi-tenant with Shared Root
+
+```
+src/routes/
+├── +layout.svelte # Root layout (shared)
+├── admin/
+│ ├── +layout.svelte # Admin layout
+│ └── users/+page.svelte # /admin/users
+└── client/
+ ├── +layout.svelte # Client layout
+ └── projects/+page.svelte # /client/projects
+```
+
+### Pattern 3: Progressive Enhancement
+
+```svelte
+
+
+
+
+ {#if $navigating}
+
+ {/if}
+
+ {@render children()}
+
+
+
+```
+
+## Layout Best Practices
+
+1. ✅ Keep root layout minimal (shared across ALL pages)
+2. ✅ Use layout groups for separate sections
+3. ✅ Load shared data in layout's load function
+4. ✅ Use context for sharing state with descendants
+5. ✅ Avoid too many nested layouts (max 2-3 levels)
+6. ✅ Don't put auth logic in root layout (use groups)
+7. ✅ Remember: layouts share data DOWN, not UP
+8. ❌ Avoid conditionals in layouts (use groups instead)
+
+## Debugging Layouts
+
+```svelte
+
+
+
+{@render children()}
+```
+
+## Key Takeaways
+
+- Layouts wrap all child routes
+- Nested layouts create hierarchy (root → section → page)
+- Groups `(name)` organize without affecting URLs
+- Load data in `+layout.server.ts` to share with children
+- Use context to share reactive state
+- Keep layouts simple and focused
diff --git a/.agents/skills/sveltekit-structure/references/ssr-hydration.md b/.agents/skills/sveltekit-structure/references/ssr-hydration.md
new file mode 100644
index 0000000..bc59f1e
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/references/ssr-hydration.md
@@ -0,0 +1,92 @@
+# SSR & Hydration
+
+## The Problem
+
+SvelteKit runs on server (SSR) then hydrates in browser. Code using
+browser APIs (`window`, `document`, `localStorage`) fails on server.
+
+## Solution: Check for Browser
+
+```typescript
+import { browser } from '$app/environment';
+
+// In load function
+export const load = async () => {
+ const theme = browser ? localStorage.getItem('theme') : 'light';
+ return { theme };
+};
+```
+
+```svelte
+
+
+```
+
+## Common Mistakes
+
+### ❌ Using window Without Check
+
+```typescript
+// WRONG - fails on server
+export const load = async () => {
+ const width = window.innerWidth; // ERROR on server
+ return { width };
+};
+
+// RIGHT
+import { browser } from '$app/environment';
+
+export const load = async () => {
+ const width = browser ? window.innerWidth : 1024;
+ return { width };
+};
+```
+
+### ❌ Accessing DOM in Load
+
+```typescript
+// WRONG
+export const load = async () => {
+ const el = document.getElementById('root'); // ERROR on server
+};
+
+// RIGHT - Do DOM stuff in onMount
+export const load = async () => {
+ return {};
+};
+// Then in component:
+onMount(() => {
+ const el = document.getElementById('root');
+});
+```
+
+## Disable SSR (Not Recommended)
+
+```typescript
+// +page.ts
+export const ssr = false; // Disables SSR for this page
+```
+
+Only use when absolutely necessary (e.g., heavy Canvas/WebGL).
diff --git a/.agents/skills/sveltekit-structure/references/svelte-boundary.md b/.agents/skills/sveltekit-structure/references/svelte-boundary.md
new file mode 100644
index 0000000..21eaf1b
--- /dev/null
+++ b/.agents/skills/sveltekit-structure/references/svelte-boundary.md
@@ -0,0 +1,150 @@
+# svelte:boundary Component
+
+> Available in Svelte 5.3+
+
+## Two Purposes
+
+1. **Error boundaries** - catch rendering errors
+2. **Pending UI** - show loading state while `await` resolves
+
+## Basic Error Boundary
+
+```svelte
+ console.error(e)}>
+
+
+ {#snippet failed(error, reset)}
+ Error: {error.message}
+
+ {/snippet}
+
+```
+
+## Pending UI (Loading States)
+
+> **⚠️ Known Bug:** `` + `{@const await}` causes
+> infinite navigation loops during client-side page transitions when
+> pages share async queries. See
+> [sveltejs/svelte#17717](https://github.com/sveltejs/svelte/issues/17717).
+> Use `{#await}` blocks until this is fixed.
+
+With `experimental.async: true` (Svelte 5.36+), `{@const await}` is
+possible but **not safe for pages with shared queries or navigation**:
+
+```svelte
+
+
+ {#snippet pending()}
+
+ {/snippet}
+
+ {@const data = await loadData()}
+
+
+```
+
+**Safe alternative:**
+
+```svelte
+{#await loadData()}
+
+{:then data}
+
+{:catch error}
+ Error: {error.message}
+{/await}
+```
+
+## Combined Error + Pending
+
+Use `svelte:boundary` for **error catching only**, with `{#await}` for
+async data:
+
+```svelte
+
+ {#snippet failed(error, reset)}
+ Failed to load user
+
+ {/snippet}
+
+ {#await fetchUser()}
+ Loading user...
+ {:then user}
+
+ {:catch error}
+ Error: {error.message}
+ {/await}
+
+```
+
+## What Gets Caught
+
+**Caught:**
+
+- Errors during rendering
+- Errors in `$effect`
+
+**NOT Caught:**
+
+- Event handler errors (`onclick`, etc.)
+- Errors after `setTimeout`
+- Async errors outside boundary's await
+
+## vs +error.svelte
+
+| Feature | svelte:boundary | +error.svelte |
+| -------- | ----------------------- | ------------- |
+| Scope | Component subtree | Route segment |
+| Reset | Built-in reset function | Navigate away |
+| Pending | Yes (pending snippet) | No |
+| Use case | Component-level | Page-level |
+
+## Error Tracking Integration
+
+```svelte
+ {
+ // Send to Sentry, LogRocket, etc.
+ errorTracker.captureException(error);
+ }}
+>
+
+
+ {#snippet failed(error, reset)}
+
+ {/snippet}
+
+```
+
+## Nested Boundaries
+
+Inner boundary catches first:
+
+```svelte
+
+
+ {#snippet failed(e)}
+ Outer caught: {e.message}
+ {/snippet}
+
+
+
+ {#snippet failed(e)}
+ Inner caught: {e.message}
+ {/snippet}
+
+
+
+
+```
+
+## Key Points
+
+- Use `svelte:boundary` for component-level error isolation
+- Use `+error.svelte` for route-level error pages
+- `pending` snippet shows only on **initial** load (not on refresh - no flicker)
+- `failed` snippet replaces content on error
+- `reset` function lets users retry
+- Errors in event handlers are NOT caught
+- Requires `experimental.async: true` in svelte.config.js for `{@const await}`
+- **⚠️ Bug:** `{@const await}` + navigation causes infinite loops ([#17717](https://github.com/sveltejs/svelte/issues/17717)) — use `{#await}` instead
diff --git a/.agents/skills/typescript-best-practices/SKILL.md b/.agents/skills/typescript-best-practices/SKILL.md
new file mode 100644
index 0000000..703db2b
--- /dev/null
+++ b/.agents/skills/typescript-best-practices/SKILL.md
@@ -0,0 +1,270 @@
+---
+name: typescript-best-practices
+description: Provides TypeScript patterns for type-first development, making illegal states unrepresentable, exhaustive handling, and runtime validation. Must use when reading or writing TypeScript/JavaScript files.
+---
+
+# TypeScript Best Practices
+
+## Pair with React Best Practices
+
+When working with React components (`.tsx`, `.jsx` files or `@react` imports), always load `react-best-practices` alongside this skill. This skill covers TypeScript fundamentals; React-specific patterns (effects, hooks, refs, component design) are in the dedicated React skill.
+
+## Type-First Development
+
+Types define the contract before implementation. Follow this workflow:
+
+1. **Define the data model** - types, interfaces, and schemas first
+2. **Define function signatures** - input/output types before logic
+3. **Implement to satisfy types** - let the compiler guide completeness
+4. **Validate at boundaries** - runtime checks where data enters the system
+
+### Make Illegal States Unrepresentable
+
+Use the type system to prevent invalid states at compile time.
+
+**Discriminated unions for mutually exclusive states:**
+```ts
+// Good: only valid combinations possible
+type RequestState =
+ | { status: 'idle' }
+ | { status: 'loading' }
+ | { status: 'success'; data: T }
+ | { status: 'error'; error: Error };
+
+// Bad: allows invalid combinations like { loading: true, error: Error }
+type RequestState = {
+ loading: boolean;
+ data?: T;
+ error?: Error;
+};
+```
+
+**Branded types for domain primitives:**
+```ts
+type UserId = string & { readonly __brand: 'UserId' };
+type OrderId = string & { readonly __brand: 'OrderId' };
+
+// Compiler prevents passing OrderId where UserId expected
+function getUser(id: UserId): Promise { /* ... */ }
+
+function createUserId(id: string): UserId {
+ return id as UserId;
+}
+```
+
+**Const assertions for literal unions:**
+```ts
+const ROLES = ['admin', 'user', 'guest'] as const;
+type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'
+
+// Array and type stay in sync automatically
+function isValidRole(role: string): role is Role {
+ return ROLES.includes(role as Role);
+}
+```
+
+**Required vs optional fields - be explicit:**
+```ts
+// Creation: some fields required
+type CreateUser = {
+ email: string;
+ name: string;
+};
+
+// Update: all fields optional
+type UpdateUser = Partial;
+
+// Database row: all fields present
+type User = CreateUser & {
+ id: UserId;
+ createdAt: Date;
+};
+```
+
+## Module Structure
+
+Prefer smaller, focused files: one component, hook, or utility per file. Split when a file handles multiple concerns or exceeds ~200 lines. Colocate tests with implementation (`foo.test.ts` alongside `foo.ts`). Group related files by feature rather than by type.
+
+## Functional Patterns
+
+- Prefer `const` over `let`; use `readonly` and `Readonly` for immutable data.
+- Use `array.map/filter/reduce` over `for` loops; chain transformations in pipelines.
+- Write pure functions for business logic; isolate side effects in dedicated modules.
+- Avoid mutating function parameters; return new objects/arrays instead.
+
+## Instructions
+
+- Enable `strict` mode; model data with interfaces and types. Strong typing catches bugs at compile time.
+- Every code path returns a value or throws; use exhaustive `switch` with `never` checks in default. Unhandled cases become compile errors.
+- Propagate errors with context; catching requires re-throwing or returning a meaningful result. Hidden failures delay debugging.
+- Handle edge cases explicitly: empty arrays, null/undefined inputs, boundary values. Defensive checks prevent runtime surprises.
+- Use `await` for async calls; wrap external calls with contextual error messages. Unhandled rejections crash Node processes.
+- Add or update focused tests when changing logic; test behavior, not implementation details.
+
+## Examples
+
+Explicit failure for unimplemented logic:
+```ts
+export function buildWidget(widgetType: string): never {
+ throw new Error(`buildWidget not implemented for type: ${widgetType}`);
+}
+```
+
+Exhaustive switch with never check:
+```ts
+type Status = "active" | "inactive";
+
+export function processStatus(status: Status): string {
+ switch (status) {
+ case "active":
+ return "processing";
+ case "inactive":
+ return "skipped";
+ default: {
+ const _exhaustive: never = status;
+ throw new Error(`unhandled status: ${_exhaustive}`);
+ }
+ }
+}
+```
+
+Wrap external calls with context:
+```ts
+export async function fetchWidget(id: string): Promise {
+ const response = await fetch(`/api/widgets/${id}`);
+ if (!response.ok) {
+ throw new Error(`fetch widget ${id} failed: ${response.status}`);
+ }
+ return response.json();
+}
+```
+
+Debug logging with namespaced logger:
+```ts
+import debug from "debug";
+
+const log = debug("myapp:widgets");
+
+export function createWidget(name: string): Widget {
+ log("creating widget: %s", name);
+ const widget = { id: crypto.randomUUID(), name };
+ log("created widget: %s", widget.id);
+ return widget;
+}
+```
+
+## Runtime Validation with Zod
+
+- Define schemas as single source of truth; infer TypeScript types with `z.infer<>`. Avoid duplicating types and schemas.
+- Use `safeParse` for user input where failure is expected; use `parse` at trust boundaries where invalid data is a bug.
+- Compose schemas with `.extend()`, `.pick()`, `.omit()`, `.merge()` for DRY definitions.
+- Add `.transform()` for data normalization at parse time (trim strings, parse dates).
+- Include descriptive error messages; use `.refine()` for custom validation logic.
+
+### Examples
+
+Schema as source of truth with type inference:
+```ts
+import { z } from "zod";
+
+const UserSchema = z.object({
+ id: z.string().uuid(),
+ email: z.string().email(),
+ name: z.string().min(1),
+ createdAt: z.string().transform((s) => new Date(s)),
+});
+
+type User = z.infer;
+```
+
+Return parse results to callers (never swallow errors):
+```ts
+import { z, SafeParseReturnType } from "zod";
+
+export function parseUserInput(raw: unknown): SafeParseReturnType {
+ return UserSchema.safeParse(raw);
+}
+
+// Caller handles both success and error:
+const result = parseUserInput(formData);
+if (!result.success) {
+ setErrors(result.error.flatten().fieldErrors);
+ return;
+}
+await submitUser(result.data);
+```
+
+Strict parsing at trust boundaries:
+```ts
+export async function fetchUser(id: string): Promise {
+ const response = await fetch(`/api/users/${id}`);
+ if (!response.ok) {
+ throw new Error(`fetch user ${id} failed: ${response.status}`);
+ }
+ const data = await response.json();
+ return UserSchema.parse(data); // throws if API contract violated
+}
+```
+
+Schema composition:
+```ts
+const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
+const UpdateUserSchema = CreateUserSchema.partial();
+const UserWithPostsSchema = UserSchema.extend({
+ posts: z.array(PostSchema),
+});
+```
+
+## Configuration
+
+- Load config from environment variables at startup; validate with Zod before use. Invalid config should crash immediately.
+- Define a typed config object as single source of truth; avoid accessing `process.env` throughout the codebase.
+- Use sensible defaults for development; require explicit values for production secrets.
+
+### Examples
+
+Typed config with Zod validation:
+```ts
+import { z } from "zod";
+
+const ConfigSchema = z.object({
+ PORT: z.coerce.number().default(3000),
+ DATABASE_URL: z.string().url(),
+ API_KEY: z.string().min(1),
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
+});
+
+export const config = ConfigSchema.parse(process.env);
+```
+
+Access config values (not process.env directly):
+```ts
+import { config } from "./config";
+
+const server = app.listen(config.PORT);
+const db = connect(config.DATABASE_URL);
+```
+
+## Optional: type-fest
+
+For advanced type utilities beyond TypeScript builtins, consider [type-fest](https://github.com/sindresorhus/type-fest):
+
+- `Opaque` - cleaner branded types than manual `& { __brand }` pattern
+- `PartialDeep` - recursive partial for nested objects
+- `ReadonlyDeep` - recursive readonly for immutable data
+- `LiteralUnion` - literals with autocomplete + string fallback
+- `SetRequired` / `SetOptional` - targeted field modifications
+- `Simplify` - flatten complex intersection types in IDE tooltips
+
+```ts
+import type { Opaque, PartialDeep, SetRequired } from 'type-fest';
+
+// Branded type (cleaner than manual approach)
+type UserId = Opaque;
+
+// Deep partial for patch operations
+type UserPatch = PartialDeep;
+
+// Make specific fields required
+type UserWithEmail = SetRequired, 'email'>;
+```
diff --git a/.gitignore b/.gitignore
index a461635..5ed83d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,6 @@ vite.config.ts.timestamp-*
# Test results
/test-results
/playwright-report
+
+# Skills CLI (actual files are in .agents/skills/)
+/.claude/skills
diff --git a/skills-lock.json b/skills-lock.json
new file mode 100644
index 0000000..89affbd
--- /dev/null
+++ b/skills-lock.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "skills": {
+ "drizzle-migrations": {
+ "source": "erichowens/some_claude_skills",
+ "sourceType": "github",
+ "computedHash": "d2af96b92935e26f53d97616aa51a02f114c9da8129c5943408d3b4b19a10423"
+ },
+ "svelte5-best-practices": {
+ "source": "ejirocodes/agent-skills",
+ "sourceType": "github",
+ "computedHash": "bfcbba3d355aede6f6be9720f195206d4dcaa7c152787ea00c06cb4a1ac44ba4"
+ },
+ "sveltekit-data-flow": {
+ "source": "spences10/svelte-skills-kit",
+ "sourceType": "github",
+ "computedHash": "1ef78409b38d1cb7324d8e0a78ac3f4303e0a9c5b5789bfc2d646c0806faa618"
+ },
+ "sveltekit-structure": {
+ "source": "spences10/svelte-skills-kit",
+ "sourceType": "github",
+ "computedHash": "a935fd51b32e4e37d23286c04748825a6a193ef74d9980385449aa4f80646f9d"
+ },
+ "typescript-best-practices": {
+ "source": "0xbigboss/claude-code",
+ "sourceType": "github",
+ "computedHash": "838d6737e59e8b15457a6af23112bc3a107a8ac098606a19a43c2be0db2dbe5a"
+ }
+ }
+}