diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
new file mode 100644
index 00000000..551a1f34
--- /dev/null
+++ b/frontend/CLAUDE.md
@@ -0,0 +1,197 @@
+# Frontend — Familienarchiv
+
+## Overview
+
+SvelteKit 2 application providing the Familienarchiv web UI. Server-side rendered (SSR) where beneficial, with client-side interactivity for document viewing, transcription, annotation, and admin workflows.
+
+## Tech Stack
+
+- **Framework**: SvelteKit 2 with Svelte 5 (runes mode)
+- **Language**: TypeScript 5.9
+- **Styling**: Tailwind CSS 4.1 + custom brand utilities
+- **Build Tool**: Vite 7
+- **Adapter**: `@sveltejs/adapter-node` (Node.js server, not static)
+- **i18n**: Paraglide.js 2.5 (`@inlang/paraglide-js`) — German (default), English, Spanish
+- **API Client**: `openapi-fetch` + `openapi-typescript` (generated from backend OpenAPI spec)
+- **PDF Rendering**: `pdfjs-dist` (PDF.js)
+- **Testing**:
+ - Unit/Server: Vitest 4 (Node environment)
+ - Component: Vitest Browser Mode with Playwright (Chromium)
+ - E2E: Playwright (`frontend/e2e/`)
+
+## Project Structure
+
+```
+src/
+├── routes/ # SvelteKit file-based routing
+│ ├── +layout.svelte # Global layout: header, nav, auth state
+│ ├── +layout.server.ts # Loads current user, injects auth cookie
+│ ├── +page.svelte # Home / document search dashboard
+│ ├── documents/ # Document CRUD, detail, edit, upload
+│ ├── persons/ # Person directory, detail, edit, merge
+│ ├── briefwechsel/ # Bilateral conversation timeline
+│ ├── chronik/ # Unified activity feed
+│ ├── admin/ # User, group, tag, OCR, system management
+│ ├── api/ # Internal API proxies (server-side only)
+│ ├── login/ logout/ # Auth pages
+│ └── ...
+├── lib/
+│ ├── components/ # Reusable Svelte components
+│ │ ├── document/ # Document-specific components
+│ │ ├── chronik/ # Activity feed components
+│ │ └── user/ # User-related components
+│ ├── generated/ # Auto-generated API types (openapi-typescript)
+│ ├── server/ # Server-only utilities (db, auth helpers)
+│ ├── services/ # Client-side service logic
+│ ├── stores/ # Svelte stores (global state)
+│ ├── types.ts # Shared TypeScript types
+│ ├── errors.ts # Error code mapping (mirrors backend ErrorCode)
+│ ├── api.server.ts # Typed API client factory
+│ ├── utils.ts # Shared utilities
+│ ├── relativeTime.ts # Time formatting
+│ ├── search.ts # Search utilities
+│ └── paraglide/ # Generated i18n code
+├── hooks/ # SvelteKit hooks (handle, handleFetch)
+└── actions/ # Custom Svelte actions (click outside, etc.)
+```
+
+## API Client Pattern
+
+All server-side API calls use the typed client from `$lib/api.server.ts`:
+
+```typescript
+const api = createApiClient(fetch);
+const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
+
+// Always check via response.ok, NOT result.error
+if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+}
+return { person: result.data! };
+```
+
+Key rules:
+
+- Use `!result.response.ok` for error checking (not `if (result.error)` — breaks when spec has no error responses defined)
+- Cast errors as `result.error as unknown as { code?: string }` to extract backend error code
+- Use `result.data!` after an ok check
+
+For multipart/form-data (file uploads), bypass the typed client and use raw `fetch`.
+
+## Form Actions Pattern
+
+```typescript
+// +page.server.ts
+export const actions = {
+ default: async ({ request, fetch }) => {
+ const formData = await request.formData();
+ const name = formData.get('name') as string;
+ // ...
+ return fail(400, { error: 'message' }); // on error
+ throw redirect(303, '/target'); // on success
+ }
+};
+```
+
+## Date Handling
+
+- **Forms**: German format `dd.mm.yyyy` with auto-dot insertion via `handleDateInput()`. A hidden `` sends ISO to the backend.
+- **Display**: Always use `Intl.DateTimeFormat` with `T12:00:00` suffix to prevent UTC off-by-one:
+ ```typescript
+ new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(
+ new Date(doc.documentDate + 'T12:00:00')
+ );
+ ```
+
+## Styling Conventions (Tailwind CSS 4)
+
+Brand color utilities (defined in `layout.css`):
+
+| Class | Value | Usage |
+| ------------ | --------- | -------------------------------- |
+| `brand-navy` | `#002850` | Primary text, buttons, headers |
+| `brand-mint` | `#A6DAD8` | Accents, hover underlines, icons |
+| `brand-sand` | `#E4E2D7` | Page background, card borders |
+
+Typography:
+
+- `font-serif` (Merriweather) — body text, document titles, names
+- `font-sans` (Montserrat) — labels, metadata, UI chrome
+
+Card pattern for content sections:
+
+```svelte
+
+
Section
+
+
+```
+
+## Key UI Components
+
+| Component | Props | Description |
+| -------------------- | ---------------------------------------------------- | ------------------------------------- |
+| `PersonTypeahead` | `name`, `label`, `value`, `initialName`, `on:change` | Single-person selector with typeahead |
+| `PersonMultiSelect` | `selectedPersons` (bind) | Chip-based multi-person selector |
+| `TagInput` | `tags` (bind), `allowCreation?`, `on:change` | Tag chip input with typeahead |
+| `PdfViewer` | `url`, `annotations`, `on:annotation` | PDF rendering with annotation overlay |
+| `TranscriptionBlock` | `block`, `mode` | Read/edit transcription block |
+| `DocumentTopBar` | `document` | Responsive document metadata header |
+
+## How to Run
+
+### Development
+
+```bash
+cd frontend
+npm install
+npm run dev # Dev server on port 5173 (or 3000 if --port 3000)
+```
+
+### Build & Preview
+
+```bash
+npm run build # Production build
+npm run preview # Preview production build
+```
+
+### Code Quality
+
+```bash
+npm run lint # Prettier + ESLint check
+npm run format # Auto-fix formatting
+npm run check # svelte-check (type checking)
+```
+
+### Testing
+
+```bash
+npm run test # Vitest unit + server tests (headless)
+npm run test:coverage # Coverage report (server project only)
+npm run test:e2e # Playwright E2E tests
+npm run test:e2e:headed # Playwright E2E with visible browser
+npm run test:e2e:ui # Playwright UI mode
+```
+
+### Regenerate API Types
+
+Requires backend running with `--spring.profiles.active=dev`:
+
+```bash
+npm run generate:api
+```
+
+## Vite Proxy
+
+During development, `/api` calls are proxied to the Spring Boot backend. The proxy injects the `Authorization` header from the `auth_token` cookie automatically (see `vite.config.ts`).
+
+## i18n (Paraglide)
+
+Translations live in `messages/{de,en,es}.json`. The compiler generates type-safe helpers in `src/lib/paraglide/`. Run compilation manually with:
+
+```bash
+npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
+```
+
+Or let the Vite plugin handle it automatically during dev/build.
diff --git a/frontend/e2e/CLAUDE.md b/frontend/e2e/CLAUDE.md
new file mode 100644
index 00000000..b3e4c4a4
--- /dev/null
+++ b/frontend/e2e/CLAUDE.md
@@ -0,0 +1,141 @@
+# E2E Tests — Familienarchiv
+
+## Overview
+
+End-to-end tests for the Familienarchiv frontend using Playwright. These tests verify complete user flows across the full stack (SvelteKit frontend + Spring Boot backend + PostgreSQL + MinIO).
+
+## Tech Stack
+
+- **Test Runner**: Playwright (`@playwright/test`)
+- **Browser**: Chromium (desktop)
+- **Locale**: `de-DE` (ensures German language detection)
+- **Auth**: Shared session cookie stored after setup
+
+## Project Structure
+
+```
+frontend/e2e/
+├── auth.setup.ts # Authentication setup — logs in and saves session
+├── auth.spec.ts # Authentication flows (login, logout, register)
+├── admin.spec.ts # Admin panel CRUD operations
+├── annotations.spec.ts # Document annotation features
+├── bottom-panel.spec.ts # Bottom panel / transcription panel
+├── dashboard-*.spec.ts # Dashboard variants and screenshots
+├── documents.spec.ts # Document upload, edit, search
+├── focus-rings.spec.ts # Accessibility focus ring tests
+├── header.spec.ts # Navigation header
+├── history.spec.ts # Chronik / activity feed
+├── korrespondenz.spec.ts # Correspondence timeline
+├── lang.spec.ts # Language switching
+├── password-reset.spec.ts # Password reset flow
+├── permissions.spec.ts # Role-based access control
+├── persons.spec.ts # Person directory CRUD
+├── profile.spec.ts # User profile
+├── theme.spec.ts # Dark/light mode
+├── transcription.spec.ts # Transcription workflows
+├── accessibility.spec.ts # Axe accessibility scans
+├── fixtures/ # Test data fixtures
+└── helpers/ # Test helper utilities
+```
+
+## Authentication Strategy
+
+Tests share auth state via a stored session cookie:
+
+1. **Setup** (`auth.setup.ts`): Logs in with test credentials and saves `storageState` to `e2e/.auth/user.json`
+2. **Tests**: All test projects depend on `setup` and reuse the stored session
+
+This avoids re-logging in for every test, but means tests **must run sequentially** (`fullyParallel: false`, `workers: 1`).
+
+## Configuration
+
+Config lives in `frontend/playwright.config.ts`:
+
+| Setting | Value | Notes |
+| --------------- | ----------------------- | ------------------------------ |
+| `testDir` | `./e2e` | Test file location |
+| `fullyParallel` | `false` | Shared auth state |
+| `workers` | `1` | Sequential execution |
+| `screenshot` | `'on'` | Always capture |
+| `video` | `'retain-on-failure'` | Keep on failure |
+| `trace` | `'retain-on-failure'` | Keep on failure |
+| `baseURL` | `http://localhost:3000` | Overridable via `E2E_BASE_URL` |
+
+The `webServer` config auto-starts `npm run dev -- --port 3000` if no server is detected at the base URL.
+
+## How to Run
+
+### Prerequisites
+
+The full stack must be running (or the `webServer` config will start the frontend dev server):
+
+```bash
+# Start infrastructure
+docker-compose up -d
+
+# Ensure backend is healthy
+curl http://localhost:8080/actuator/health
+```
+
+### Run E2E Tests
+
+```bash
+cd frontend
+
+# Headless (CI mode)
+npm run test:e2e
+
+# With visible browser
+npm run test:e2e:headed
+
+# Interactive UI mode
+npm run test:e2e:ui
+
+# Run a specific test file
+npx playwright test documents.spec.ts
+
+# Run with a different base URL (e.g., docker frontend on 5173)
+E2E_BASE_URL=http://localhost:5173 npx playwright test
+```
+
+## Writing New E2E Tests
+
+1. Create a new `.spec.ts` file in `frontend/e2e/`
+2. Use the shared auth state (no manual login needed)
+3. Use page object patterns or helper functions from `helpers/`
+4. Add `test-data-id` attributes to components for stable selectors
+5. Run with `--debug` or `--ui` to troubleshoot
+
+### Example Test Pattern
+
+```typescript
+import { test, expect } from '@playwright/test';
+
+test('user can create a document', async ({ page }) => {
+ await page.goto('/documents/new');
+ await page.getByTestId('document-title').fill('Test Document');
+ await page.getByTestId('save-button').click();
+ await expect(page).toHaveURL(/\/documents\/[^/]+$/);
+});
+```
+
+## Accessibility Testing
+
+`accessibility.spec.ts` runs Axe scans on key pages. Violations fail the test.
+
+```bash
+npx playwright test accessibility.spec.ts
+```
+
+## Troubleshooting
+
+| Issue | Solution |
+| --------------------- | ---------------------------------------- |
+| Auth failures | Delete `e2e/.auth/user.json` and re-run |
+| Backend not reachable | Ensure `docker-compose up -d` is running |
+| Flaky tests | Increase timeout or add explicit waits |
+| Screenshots missing | Check `test-results/e2e/` |
+
+## CI Integration
+
+E2E tests are **not** currently run in CI (the pipeline stops at unit/component tests). To add them, extend `infra/gitea/workflows/ci.yml` with a Playwright job that starts the full Docker Compose stack first.
diff --git a/frontend/src/lib/components/StammbaumSidePanel.svelte b/frontend/src/lib/components/StammbaumSidePanel.svelte
new file mode 100644
index 00000000..56d758fb
--- /dev/null
+++ b/frontend/src/lib/components/StammbaumSidePanel.svelte
@@ -0,0 +1,160 @@
+
+
+
+
+
{node.displayName}
+ {#if node.birthYear || node.deathYear}
+
+ {node.birthYear ?? '?'}–{node.deathYear ?? ''}
+
+ {/if}
+
+
+ {#if error}
+
{error}
+ {:else if loading}
+
…
+ {:else}
+
+
+ {m.stammbaum_panel_direct_rels()}
+
+ {#if directRels.length === 0}
+ {m.person_relationships_empty()}
+ {:else}
+
+ {#each directRels as rel (rel.id)}
+ -
+
+ {chipLabel(rel)}
+
+
+ {otherName(rel)}
+
+
+ {/each}
+
+ {/if}
+
+
+ {#if topDerived.length > 0}
+
+
+ {m.stammbaum_panel_derived_rels()}
+
+
+ {#each topDerived as derived (derived.person.id)}
+ -
+
+ {inferredRelationshipLabel(derived.label)}
+
+
+ {derived.person.displayName}
+
+
+ {/each}
+
+
+ {/if}
+ {/if}
+
+
+
diff --git a/frontend/src/lib/components/StammbaumTree.svelte b/frontend/src/lib/components/StammbaumTree.svelte
new file mode 100644
index 00000000..73b625bc
--- /dev/null
+++ b/frontend/src/lib/components/StammbaumTree.svelte
@@ -0,0 +1,239 @@
+
+
+
diff --git a/frontend/src/routes/stammbaum/+page.server.ts b/frontend/src/routes/stammbaum/+page.server.ts
new file mode 100644
index 00000000..b35c75d5
--- /dev/null
+++ b/frontend/src/routes/stammbaum/+page.server.ts
@@ -0,0 +1,18 @@
+import { error, redirect } from '@sveltejs/kit';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+
+export async function load({ fetch }) {
+ const api = createApiClient(fetch);
+ const result = await api.GET('/api/network');
+
+ if (result.response.status === 401) throw redirect(302, '/login');
+
+ if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+ }
+
+ const network = result.data!;
+ return { nodes: network.nodes ?? [], edges: network.edges ?? [] };
+}
diff --git a/frontend/src/routes/stammbaum/+page.svelte b/frontend/src/routes/stammbaum/+page.svelte
new file mode 100644
index 00000000..6d472173
--- /dev/null
+++ b/frontend/src/routes/stammbaum/+page.svelte
@@ -0,0 +1,110 @@
+
+
+
+
+ {m.nav_stammbaum()}
+ {#if data.nodes.length > 0}
+
+
+
+
+ {/if}
+
+
+ {#if data.nodes.length === 0}
+
+ {:else}
+
+
+ (selectedId = id)}
+ />
+
+ {#if selectedNode}
+
+ {/if}
+
+ {/if}
+