diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index ee6c38e..312206c 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,210 +1,107 @@
# Portfolio Project Instructions
-This is Dennis Konkol's personal portfolio (dk0.dev) - a Next.js 15 portfolio with Directus CMS integration, n8n automation, and a "liquid" design system.
+Dennis Konkol's portfolio (dk0.dev) — Next.js 15, Directus CMS, n8n automation, "Liquid Editorial Bento" design system.
## Build, Test, and Lint
-### Development
```bash
-npm run dev # Full dev environment (Docker + Next.js)
-npm run dev:simple # Next.js only (no Docker dependencies)
-npm run dev:next # Plain Next.js dev server
+npm run dev:next # Plain Next.js dev server (no Docker)
+npm run build # Production build (standalone mode)
+npm run lint # ESLint (0 errors required, warnings OK)
+npm run lint:fix # Auto-fix lint issues
+npm run test # All Jest unit tests
+npx jest path/to/test.tsx # Run a single test file
+npm run test:watch # Watch mode
+npm run test:e2e # Playwright E2E tests
+npm run db:generate # Regenerate Prisma client after schema changes
```
-### Build & Deploy
-```bash
-npm run build # Production build (standalone mode)
-npm run start # Start production server
-```
+## Architecture
-### Testing
-```bash
-# Unit tests (Jest)
-npm run test # Run all unit tests
-npm run test:watch # Watch mode
-npm run test:coverage # With coverage report
+### Server/Client Component Split
-# E2E tests (Playwright)
-npm run test:e2e # Run all E2E tests
-npm run test:e2e:ui # Interactive UI mode
-npm run test:critical # Critical paths only
-npm run test:hydration # Hydration tests only
-```
+The homepage uses a **server component orchestrator** pattern:
-### Linting
-```bash
-npm run lint # Run ESLint
-npm run lint:fix # Auto-fix issues
-```
+- `app/_ui/HomePageServer.tsx` — async server component, fetches all translations in parallel via `Promise.all`, renders Hero directly, wraps client sections in `ScrollFadeIn`
+- `app/components/Hero.tsx` — **server component** (no `"use client"`), uses `getTranslations()` from `next-intl/server`
+- `app/components/ClientWrappers.tsx` — exports `AboutClient`, `ProjectsClient`, `ContactClient`, `FooterClient`, each wrapping their component in a scoped `NextIntlClientProvider` with only the needed translation keys
+- `app/components/ClientProviders.tsx` — root client wrapper, defers Three.js/WebGL via `requestIdleCallback` (5s timeout) to avoid blocking LCP
-### Database (Prisma)
-```bash
-npm run db:generate # Generate Prisma client
-npm run db:push # Push schema to database
-npm run db:studio # Open Prisma Studio
-npm run db:seed # Seed database
-```
+### SSR Animation Safety
-## Architecture Overview
+**Never use Framer Motion's `initial={{ opacity: 0 }}` on SSR-rendered elements** — it bakes `style="opacity:0"` into HTML, making content invisible if hydration fails.
-### Tech Stack
-- **Framework**: Next.js 15 (App Router), TypeScript 5.9
-- **Styling**: Tailwind CSS 3.4 with custom `liquid-*` color tokens
-- **Theming**: next-themes for dark mode (system/light/dark)
-- **Animations**: Framer Motion 12
-- **3D**: Three.js + React Three Fiber (shader gradient background)
-- **Database**: PostgreSQL via Prisma ORM
-- **Cache**: Redis (optional)
-- **CMS**: Directus (self-hosted, GraphQL, optional)
-- **Automation**: n8n webhooks (status, chat, hardcover, image generation)
-- **i18n**: next-intl (EN + DE)
-- **Monitoring**: Console error logging (development mode only)
-- **Deployment**: Docker (standalone mode) + Nginx
+Use `ScrollFadeIn` component instead (`app/components/ScrollFadeIn.tsx`): renders no inline style during SSR (content visible by default), applies opacity+transform only after `hasMounted` check, animates via IntersectionObserver + CSS transitions.
-### Key Directories
-```
-app/
- [locale]/ # i18n routes (en, de)
- page.tsx # Homepage sections
- projects/ # Project listing + detail pages
- api/ # API routes
- book-reviews/ # Book reviews from Directus
- hobbies/ # Hobbies from Directus
- n8n/ # n8n webhook proxies
- projects/ # Projects (PostgreSQL + Directus)
- tech-stack/ # Tech stack from Directus
- components/ # React components
-lib/
- directus.ts # Directus GraphQL client (no SDK)
- auth.ts # Auth + rate limiting
- translations-loader.ts # i18n loaders for server components
-prisma/
- schema.prisma # Database schema
-messages/
- en.json # English translations
- de.json # German translations
-```
+Framer Motion `AnimatePresence` is fine for modals/overlays that only render after user interaction.
### Data Source Fallback Chain
-The architecture prioritizes resilience with this fallback hierarchy:
-1. **Directus CMS** (if `DIRECTUS_STATIC_TOKEN` configured)
-2. **PostgreSQL** (for projects, analytics)
-3. **JSON files** (`messages/*.json`)
-4. **Hardcoded defaults**
-5. **Display key itself** (last resort)
-**Critical**: The site never crashes if external services (Directus, PostgreSQL, n8n, Redis) are unavailable. All API routes return graceful fallbacks.
+Every data fetch degrades gracefully — the site never crashes:
+
+1. **Directus CMS** → 2. **PostgreSQL** → 3. **JSON files** (`messages/*.json`) → 4. **Hardcoded defaults** → 5. **i18n key itself**
### CMS Integration (Directus)
-- GraphQL calls via `lib/directus.ts` (no Directus SDK)
-- Collections: `tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews`
-- Translations use Directus native system (M2O to `languages`)
+
+- GraphQL via `lib/directus.ts` — no Directus SDK, uses `directusRequest()` with 2s timeout
+- Returns `null` on failure (never throws)
- Locale mapping: `en` → `en-US`, `de` → `de-DE`
-- API routes export `runtime='nodejs'`, `dynamic='force-dynamic'` and include a `source` field in JSON responses (`directus|fallback|error`)
+- API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and return `source` field (`directus|fallback|error`)
### n8n Integration
-- Webhook base URL: `N8N_WEBHOOK_URL` env var
-- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
-- All endpoints have rate limiting and 10s timeout protection
-- Hardcover reading data cached for 5 minutes
+
+- Webhook proxies in `app/api/n8n/` (status, chat, hardcover, generate-image)
+- Auth: `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
+- All endpoints have rate limiting and 10s timeout
+- Hardcover reading data cached 5 minutes
## Key Conventions
-### i18n (Internationalization)
-- **Supported locales**: `en` (English), `de` (German)
-- **Primary source**: Static JSON files in `messages/en.json` and `messages/de.json`
-- **Optional override**: Directus CMS `messages` collection
-- **Server components**: Use `getHeroTranslations()`, `getNavTranslations()`, etc. from `lib/translations-loader.ts`
-- **Client components**: Use `useTranslations("key.path")` from next-intl
-- **Locale mapping**: Middleware defines `["en", "de"]` which must match `app/[locale]/layout.tsx`
+### i18n
-### Component Patterns
-- **Client components**: Mark with `"use client"` for interactive/data-fetching parts
-- **Data loading**: Use `useEffect` for client-side fetching on mount
-- **Animations**: Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp`
-- **Loading states**: Every async component needs a matching Skeleton component
+- Locales: `en`, `de` — defined in `middleware.ts`, must match `app/[locale]/layout.tsx`
+- Client components: `useTranslations("key.path")` from `next-intl`
+- Server components: `getTranslations("key.path")` from `next-intl/server`
+- Always add keys to both `messages/en.json` and `messages/de.json`
-### Design System ("Liquid Editorial Bento")
-- **Core palette**: Cream (`#fdfcf8`), Stone (`#0c0a09`), Emerald (`#10b981`)
-- **Custom colors**: Prefixed with `liquid-*` (sky, mint, lavender, pink, rose, peach, coral, teal, lime)
-- **Card style**: Gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`)
-- **Glassmorphism**: Use `backdrop-blur-sm` with `border-2` and `rounded-xl`
-- **Typography**: Headlines uppercase, tracking-tighter, with accent point at end
-- **Layout**: Bento Grid for new features (no floating overlays)
+### Design System
-### File Naming
-- **Components**: PascalCase in `app/components/` (e.g., `About.tsx`)
-- **API routes**: kebab-case directories in `app/api/` (e.g., `book-reviews/`)
-- **Lib utilities**: kebab-case in `lib/` (e.g., `email-obfuscate.ts`)
+- Custom Tailwind colors: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime`
+- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm`, `border-2`, `rounded-xl`
+- Typography: Headlines uppercase, `tracking-tighter`, accent dot at end (`.`)
+- Layout: Bento Grid, no floating overlays
+- Accessibility: Use `text-stone-600 dark:text-stone-400` (not `text-stone-400`) for body text — contrast ratio must be ≥4.5:1
### Code Style
-- **Language**: Code in English, user-facing text via i18n
-- **TypeScript**: No `any` types - use interfaces from `lib/directus.ts` or `app/_ui/`
-- **Error handling**: All API calls must catch errors with fallbacks
-- **Error logging**: Only in development mode (`process.env.NODE_ENV === "development"`)
-- **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`)
-- **No emojis**: Unless explicitly requested
-### Testing Notes
-- **Jest environment**: JSDOM with mocks for `window.matchMedia` and `IntersectionObserver`
-- **Playwright**: Uses plain Next.js dev server (no Docker) with `NODE_ENV=development` to avoid Edge runtime issues
-- **Transform**: ESM modules (react-markdown, remark-*, etc.) are transformed via `transformIgnorePatterns`
-- **After UI changes**: Run `npm run test` to verify no regressions
+- TypeScript: no `any` — use interfaces from `lib/directus.ts` or `types/`
+- Error logging: `console.error` only when `process.env.NODE_ENV === "development"`
+- File naming: PascalCase components (`About.tsx`), kebab-case API routes (`book-reviews/`), kebab-case lib utils
+- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`)
+- Every async component needs a Skeleton loading state
+
+### Testing
+
+- Jest with JSDOM; mocks for `window.matchMedia` and `IntersectionObserver` in `jest.setup.ts`
+- ESM modules transformed via `transformIgnorePatterns` (react-markdown, remark-*, etc.)
+- Server component tests: `const resolved = await Component({ props }); render(resolved)`
+- Test mocks for `next/image`: use `eslint-disable-next-line @next/next/no-img-element` on the `
` tag
### Docker & Deployment
-- **Standalone mode**: `next.config.ts` uses `output: "standalone"` for optimized Docker builds
-- **Branches**: `dev` → staging, `production` → live
-- **CI/CD**: Gitea Actions (`.gitea/workflows/`)
-- **Verify Docker builds**: Always test Docker builds after changes to `next.config.ts` or dependencies
+
+- `output: "standalone"` in `next.config.ts`
+- Entrypoint: `scripts/start-with-migrate.js` — waits for DB, runs migrations (non-fatal on failure), starts server
+- CI/CD: `.gitea/workflows/ci.yml` — `test-build` job (all branches), `deploy-dev` (dev only), `deploy-production` (production only)
+- Branches: `dev` → testing.dk0.dev, `production` → dk0.dev
+- Dev and production share the same PostgreSQL and Redis instances
## Common Tasks
### Adding a CMS-managed section
+
1. Define GraphQL query + types in `lib/directus.ts`
2. Create API route in `app/api//route.ts` with `runtime='nodejs'` and `dynamic='force-dynamic'`
-3. Create component in `app/components/.tsx`
-4. Add i18n keys to `messages/en.json` and `messages/de.json`
-5. Integrate into parent component
-
-### Adding i18n strings
-1. Add keys to both `messages/en.json` and `messages/de.json`
-2. Use `useTranslations("key.path")` in client components
-3. Use `getTranslations("key.path")` in server components
-
-### Working with Directus
-- All queries go through `directusRequest()` in `lib/directus.ts`
-- Uses GraphQL endpoint (`/graphql`) with 2s timeout
-- Returns `null` on failure (graceful degradation)
-- Translations filtered by `languages_code.code` matching Directus locale
-
-## Environment Variables
-
-### Required for CMS
-```bash
-DIRECTUS_URL=https://cms.dk0.dev
-DIRECTUS_STATIC_TOKEN=...
-```
-
-### Required for n8n features
-```bash
-N8N_WEBHOOK_URL=https://n8n.dk0.dev
-N8N_SECRET_TOKEN=...
-N8N_API_KEY=...
-```
-
-### Database & Cache
-```bash
-DATABASE_URL=postgresql://...
-REDIS_URL=redis://...
-```
-
-### Optional
-```bash
-NEXT_PUBLIC_BASE_URL=https://dk0.dev
-```
-
-## Documentation References
-- Operations guide: `docs/OPERATIONS.md`
-- Locale system: `docs/LOCALE_SYSTEM.md`
-- CMS guide: `docs/CMS_GUIDE.md`
-- Testing & deployment: `docs/TESTING_AND_DEPLOYMENT.md`
+3. Create component in `app/components/.tsx` with Skeleton loading state
+4. Add i18n keys to both `messages/en.json` and `messages/de.json`
+5. Create a `Client` wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider`
+6. Add to `HomePageServer.tsx` wrapped in `ScrollFadeIn`