diff --git a/PLAN_NEXT.md b/PLAN_NEXT.md
index 86024d1..f162c60 100644
--- a/PLAN_NEXT.md
+++ b/PLAN_NEXT.md
@@ -114,6 +114,19 @@ localStorage manager + toggle + inline `ColorSchemeScript` equivalent.
RJSF swap was import-only; SSR smoke test proved all 10 intake labels +
textarea/password/checkbox/slider widgets render. `npm run build` green.
+## 0.4 Landing page (conversion structure + screenshot automation)
+
+StartPage rebuilt along the 8-block anatomy (hero with outcome headline,
+fact bar, 3 benefits, real product screenshot, 3 steps, trust list, FAQ,
+final CTA, footer mini) — one CTA everywhere (`/access`), honest copy
+(no fake testimonials/logos/numbers; pricing section omitted, model open).
+Centralized hosting in Germany per operator decision (not self-hosted).
+`frontend/scripts/capture.mjs` + `npm run screenshots` (Playwright/Chromium
+in the frontend container) regenerates `public/screenshots/*.png`; the
+workflows shot auto-selects `intake_form` to show the generated RJSF form.
+Vite dev `server.watch` polling enabled (Windows bind-mount events often
+never reach the Linux watcher → stale modules otherwise).
+
## 1. Locked decisions (do not re-ask)
* Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`,
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a7f0deb..0221c78 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -22,6 +22,7 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
+ "playwright": "^1.63.0",
"typescript": "~5.6.0",
"vite": "^6.0.0"
}
@@ -1949,6 +1950,35 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/playwright": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
+ "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.63.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
+ "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9189d2b..5781b8b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "screenshots": "node scripts/capture.mjs"
},
"dependencies": {
"@mantine/core": "^9.6.1",
@@ -23,6 +24,7 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
+ "playwright": "^1.63.0",
"typescript": "~5.6.0",
"vite": "^6.0.0"
}
diff --git a/frontend/public/screenshots/hero-light.png b/frontend/public/screenshots/hero-light.png
new file mode 100644
index 0000000..b3b8418
Binary files /dev/null and b/frontend/public/screenshots/hero-light.png differ
diff --git a/frontend/public/screenshots/workflows-light.png b/frontend/public/screenshots/workflows-light.png
new file mode 100644
index 0000000..4adcd3e
Binary files /dev/null and b/frontend/public/screenshots/workflows-light.png differ
diff --git a/frontend/scripts/capture.mjs b/frontend/scripts/capture.mjs
new file mode 100644
index 0000000..5f8d856
--- /dev/null
+++ b/frontend/scripts/capture.mjs
@@ -0,0 +1,66 @@
+/* Repeatable landing-page screenshots (re-run after UI changes).
+ *
+ * Usage (frontend container, dev server running):
+ * npm run screenshots # light scheme (default)
+ * npm run screenshots -- --scheme=dark
+ *
+ * Captures real app states into public/screenshots/ (committed, so the
+ * landing page always shows current UI). Authenticated states are out of
+ * scope (magic-link login is not scriptable without SMTP).
+ */
+
+import { mkdirSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { chromium } from "playwright";
+
+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+const outDir = join(root, "public", "screenshots");
+const base = process.env.SCREENSHOT_BASE_URL ?? "http://localhost:3000";
+const schemeArg = process.argv.find((a) => a.startsWith("--scheme="));
+const scheme = schemeArg ? schemeArg.split("=")[1] : "light";
+
+const shots = [
+ { name: "hero", path: "/", fullPage: true },
+ {
+ name: "workflows",
+ path: "/app/workflows",
+ fullPage: false,
+ selectFlow: "intake_form",
+ },
+];
+
+const browser = await chromium.launch();
+try {
+ mkdirSync(outDir, { recursive: true });
+ const context = await browser.newContext({
+ viewport: { width: 1440, height: 900 },
+ colorScheme: scheme === "dark" ? "dark" : "light",
+ });
+ await context.addInitScript(
+ (value) => {
+ try {
+ localStorage.setItem("kirche-color-scheme", value);
+ } catch {
+ /* private mode */
+ }
+ },
+ scheme === "dark" ? "dark" : "light",
+ );
+ const page = await context.newPage();
+ for (const shot of shots) {
+ await page.goto(base + shot.path, { waitUntil: "networkidle" });
+ await page.waitForTimeout(1500);
+ if (shot.selectFlow) {
+ // Show the generated RJSF form instead of the default first flow.
+ await page.locator('[role="combobox"]').first().click();
+ await page.getByRole("option", { name: shot.selectFlow }).click();
+ await page.waitForTimeout(2000);
+ }
+ const file = join(outDir, `${shot.name}-${scheme}.png`);
+ await page.screenshot({ path: file, fullPage: shot.fullPage });
+ console.log(`wrote ${file}`);
+ }
+} finally {
+ await browser.close();
+}
diff --git a/frontend/src/pages/StartPage.tsx b/frontend/src/pages/StartPage.tsx
index 441e146..42b55f0 100644
--- a/frontend/src/pages/StartPage.tsx
+++ b/frontend/src/pages/StartPage.tsx
@@ -1,71 +1,280 @@
import { Link } from "react-router-dom";
-import { Button, Card, Grid, Group, Text, Title } from "@mantine/core";
import {
+ Accordion,
+ Anchor,
+ Badge,
+ Button,
+ Card,
+ Grid,
+ Group,
+ Image,
+ List,
+ Stack,
+ Text,
+ ThemeIcon,
+ Timeline,
+ Title,
+} from "@mantine/core";
+import {
+ IconCheck,
IconClipboardCheck,
+ IconFileText,
IconInbox,
IconKey,
+ IconLock,
+ IconServer,
IconSparkles,
+ IconUserCheck,
} from "@tabler/icons-react";
import { useAuth } from "../auth";
-const actions = [
+const CTA_LABEL = "Zugang anfragen";
+
+const facts = [
+ { icon: