# Register Page · Final Design Spec

Centered-card variant (Variant A). Friendly, inviting registration form for Familienarchiv — a collaborative archive of family letters from the 19th and 20th centuries. Designed for a dual audience (60+ and 25–42), with large 17px serif inputs, du-Anrede (informal you), German-first with DE/EN/ES toggle, live password-match feedback, mention-notification opt-in, and a post-submit success panel.

Final · Ready for implementation
Variant
A — Centered card · 640px white card on sand canvas
Fields
Vorname · Nachname · E-Mail · Passwort · Passwort bestätigen · Benachrichtigungen
Languages
DE (default) / EN / ES — header toggle; swaps all labels, hints, and validation messages live
Audience
60+ primary (48px touch targets, 17px serif, calm layout) · 25–42 secondary
📐 Mockup scale notice — all font-size, height, and padding values in the mockup CSS are scaled to ~55% of actual implementation values. Do not copy sizes from mockup CSS. Use the ⚙ Implementation Reference tables after each section.
1 Full page — desktop (1280px)
Default — form at rest, German language
localhost:3000/register
Ein Familienprojekt
Schön, dass du da bist.
Bereits 1.500 Briefe aus vielen Jahrzehnten sind im Archiv. Mit deinem Konto hilfst du mit, sie zu lesen, zu ordnen und für die nächsten Generationen zu bewahren.
Über dich
Vorname
Nachname
Konto
E-Mail-Adresse
Passwort
Mindestens 8 Zeichen.
Passwort bestätigen
Benachrichtigungen
Benachrichtige mich,
wenn jemand mich in einem Kommentar erwähnt oder mir auf einen Kommentar antwortet.
Implementation Reference — Page structureroutes/register/+page.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Page wrapper | `flex min-h-screen flex-col bg-canvas` | — | Same pattern as login page
Main centering wrapper | `flex flex-1 justify-center items-start px-8 pt-16` | 64px top padding | Items start (not center) so tall forms don't overflow
Content column | `w-full max-w-[640px]` | 640px max | Fixed max-width card column
Form card | `bg-surface border border-line rounded-sm shadow-sm p-10` | 40px padding | `rounded-sm` = 2px; `shadow-sm` = 0 1px 2px rgb(0 0 0/.05)

2 Header — anatomy & language toggle states
DE active (default)
localhost:3000/register
Active language: mint underline + white text. Inactive: 55% opacity.
EN active
localhost:3000/register?lang=en
Toggle switches all copy live (no page reload). All labels, hints, and validation messages switch language.
ES active
localhost:3000/register?lang=es
Implementation Reference — Headerroutes/AuthHeader.svelte (extend) or new RegisterHeader.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Header wrapper | `sticky top-0 z-50 bg-[#012851]` | — | Same as authenticated header but no nav links (unauthenticated page)
Mint accent stripe | `h-1 bg-accent` | 4px | `--c-accent: #a1dcd8`. Always at the very top of the header.
Nav bar | `h-16 flex items-center px-8` | 64px tall, 32px side padding | Max-width container inside: `max-w-screen-xl mx-auto w-full`
Wordmark | `font-sans text-xl font-bold tracking-[.15em] text-white uppercase` | 20px | Not a link on the register page (user is not yet logged in)
Lang toggle container | `ml-auto flex items-center gap-1` | 4px gap | —
Lang button — inactive | `font-sans text-xs font-bold tracking-[.12em] uppercase text-white/55 px-2.5 py-1.5 border-b-2 border-transparent transition-colors hover:text-white` | 12px, padding 6px 10px | Touch target: 12+6+6 = 24px tall, acceptable for a header toggle since it's not a primary action
Lang button — active | `text-white border-b-2 border-accent` | — | Mint underline border

3 Above-card — eyebrow, headline, subtext
German (DE)
Ein Familienprojekt
Schön, dass du da bist.
Bereits 1.500 Briefe aus vielen Jahrzehnten sind im Archiv. Mit deinem Konto hilfst du mit, sie zu lesen, zu ordnen und für die nächsten Generationen zu bewahren.
English (EN)
A family project
Glad to have you here.
Already 1,500 letters from many decades are in the archive. Your account helps read them, organise them, and keep them safe for the next generations.
Spanish (ES)
Un proyecto familiar
Qué bien tenerte aquí.
Ya hay 1.500 cartas de muchas décadas en el archivo. Con tu cuenta ayudas a leerlas, organizarlas y conservarlas para las próximas generaciones.
Implementation Reference — Above-cardroutes/register/+page.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Above-card wrapper | `text-center mb-9` | 36px bottom margin | —
Eyebrow label | `inline-block font-sans text-[11px] font-bold tracking-[.22em] uppercase text-ink-2 border-t border-b border-[#d4d2c5] px-3.5 py-1.5 mb-[18px]` | 11px / padding 6px 14px | i18n key: `m.register_eyebrow()`
Headline | `font-serif font-normal text-[46px] leading-[1.12] tracking-[-0.005em] text-ink mb-4` | 46px | i18n key: `m.register_headline()`. Tinos (serif), not bold — weight 400.
Subtext | `font-serif text-lg leading-relaxed text-ink-2 max-w-[540px] mx-auto` | 18px, max-width 540px | i18n key: `m.register_sub()`. `text-wrap: pretty` via inline style for better line breaks.

4 Form section — Über dich (name fields)
Empty
Über dich
Vorname
Nachname
Focused — Vorname
Über dich
Vorname
Nachname
Focus: navy border + soft navy glow ring.
Filled
Über dich
Vorname
Nachname
Error — submitted empty
Über dich
Vorname
Nachname
Red border only — no text error for empty name fields (border color communicates invalid state on submit).
Implementation Reference — Name fieldsroutes/register/+page.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Section caption wrapper | `flex items-center gap-3 mb-1` | — | Gap 12px between text and rule line
Section caption text | `font-sans text-[11px] font-bold tracking-[.18em] uppercase text-ink whitespace-nowrap` | 11px | i18n key: `m.register_section_about()`
Section caption rule | `flex-1 h-px bg-line` | 1px | —
2-column name grid | `grid grid-cols-2 gap-4 mt-4` | 16px gap, 16px top margin | On mobile (<480px): `grid-cols-1`
Field wrapper | `flex flex-col` | — | —
Field label | `block font-sans text-xs font-bold tracking-[.1em] uppercase text-ink-2 mb-2` | 12px, 8px bottom margin | i18n keys: `m.register_first_name()`, `m.register_last_name()`

Text input — default

`w-full border border-line bg-surface font-serif text-[17px] text-ink placeholder:text-ink-3 px-4 py-[14px] rounded-none outline-none focus-visible:ring-2 focus-visible:ring-focus-ring/15 focus-visible:border-ink transition-colors`

17px font / 48px tall (14+14+17+1px borders)

No border-radius (Tailwind's rounded-none keeps it at 2px via border-radius:2px from global reset — or use `rounded-sm`)

Text input — invalid | + `border-danger focus-visible:ring-danger/20` | — | Applied when `submitted && !field.trim()`
Placeholders | `placeholder:text-ink-3` | — | DE: "z.B. Frieda" / "z.B. Lehmann" · EN: "e.g. Frieda" / "e.g. Lehmann"

5 Form section — Konto (e-mail & passwords)
Default — empty
Konto
E-Mail-Adresse
Passwort
Mindestens 8 Zeichen.
Passwort bestätigen
Password valid + passwords match
Konto
E-Mail-Adresse
Passwort
Mindestens 8 Zeichen. ✓
Passwort bestätigen
Passwörter stimmen überein.
Green dot + green text when passwords match. Show button label changes to "Verbergen".
Password mismatch
Konto
E-Mail-Adresse
Passwort
Mindestens 8 Zeichen. ✓
Passwort bestätigen
Die beiden Passwörter stimmen noch nicht überein.
Red border on confirm field + red error text. No dot icon for errors.
Implementation Reference — Konto fieldsroutes/register/+page.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Email input | `w-full border border-line bg-surface font-serif text-[17px] text-ink placeholder:text-ink-3 px-4 py-[14px] rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-focus-ring/15 focus-visible:border-ink transition-colors` | 17px font / 48px tall | type="email", autocomplete="email"
Password input wrapper | `relative` | — | Contains input + show/hide button
Password input | Same as email input + `pr-[92px]` | 92px right padding to clear show/hide btn | type="password" toggles to type="text" on show
Show/hide button | `absolute right-0 inset-y-0 px-3.5 font-sans text-[11px] font-bold tracking-[.1em] uppercase text-ink-2 border-l border-line hover:text-ink transition-colors` | 11px font | type="button" (prevent form submit). Label: `m.password_show()` / `m.password_hide()`
Hint text — neutral | `font-sans text-[13px] text-ink-3 mt-1.5` | 13px, 6px top margin | i18n key: `m.register_pw_hint()` ("Mindestens 8 Zeichen.")
Hint text — success (match) | `font-sans text-[13px] text-[#0a6b56] mt-1.5 flex items-center gap-1.5` | — | Shown when `pw.length > 0 && pw === pw2`. Dot: `w-2 h-2 rounded-full bg-turquoise inline-block`
Hint text — error (mismatch) | `font-sans text-[13px] text-danger mt-1.5` | — | Shown when `pw2.length > 0 && pw !== pw2`. i18n key: `m.register_pw_match_no()`
Validation logic | — | — | pwValid: length ≥ 8. pwMatch: pw === pw2 && pw.length > 0. pwMismatch: pw2.length > 0 && pw !== pw2. Errors only shown after first submit attempt or onBlur.

6 Notification preference — CheckOption card
Unchecked
Benachrichtigungen
Benachrichtige mich,
wenn jemand mich in einem Kommentar erwähnt oder mir auf einen Kommentar antwortet.
Border: sand (#e4e2d7). Background: white. Checkbox: gray border, white fill.
Checked (default state)
Benachrichtigungen
Benachrichtige mich,
wenn jemand mich in einem Kommentar erwähnt oder mir auf einen Kommentar antwortet.
Default state: checked (opt-in by default). Border: navy. Background: mint tint (rgba(161,220,216,.18)).
Implementation Reference — CheckOption cardroutes/register/+page.svelte

Element | Tailwind classes / values | Real px | Notes
--- | --- | --- | ---
Card wrapper (label) | `<label>` with `flex gap-3.5 p-3.5 border rounded-sm cursor-pointer transition-colors items-start` | 14px gap, 14px 16px padding | The entire card is a `<label>` for large click target. Wrap real `<input type="checkbox">` visually hidden inside.
Card — unchecked | `border-line bg-surface` | — | —
Card — checked | `border-ink bg-[rgba(161,220,216,0.18)]` | — | Mint tint bg. No Tailwind utility exists — use inline style or arbitrary value.
Real checkbox input | `sr-only` (visually hidden, accessible) | — | Bind `checked` state to this. Default: `true`.
Custom checkbox box | `flex-shrink-0 w-[22px] h-[22px] rounded-sm border-2 flex items-center justify-center mt-px transition-all` | 22×22px | Min touch target met by the card itself.
Checkbox — unchecked | `border-gray-400 bg-surface` | — | —
Checkbox — checked | `border-ink bg-ink` | — | White checkmark SVG inside.
Checkmark SVG | 14×14px, path: M3 8.5l3.2 3.2L13 5, stroke white 2.4, round caps | 14px icon | Only rendered when checked.
Option title | `font-sans text-sm font-semibold text-ink` | 14px | i18n key: `m.register_notify_label()`
Option description | `font-serif text-[15px] leading-normal text-ink-2 mt-0.5` | 15px | i18n key: `m.register_notify_desc()`

7 Submit button & footer links
Default
Submit — hover
Bg darkens: #012851 → #01386f. No scale transform.
Loading (after submit)
Arrow icon replaced by spinner. Button opacity 0.85. cursor: wait. 850ms simulated delay then success.
Implementation Reference — Submit & footerroutes/register/+page.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Submit button | `w-full bg-primary text-primary-fg font-sans text-[13px] font-bold tracking-[.12em] uppercase py-4 px-6 rounded-sm flex items-center justify-center gap-2.5 transition-colors hover:bg-[#01386f] mt-8` | 13px font / 16px top+bottom padding / ~53px tall | Arrow icon: 16×16px SVG. Loading state: add `opacity-85 cursor-wait`, swap icon for spinner.
Loading spinner | `w-4 h-4 rounded-full border-2 border-white/30 border-t-white animate-spin` | 16px | Replaces arrow icon during loading state
Footer wrapper | `mt-6 flex flex-col gap-3.5 text-center` | 24px top margin, 14px gap | —
Privacy text | `font-serif text-sm text-ink-3 leading-relaxed` | 14px | Links within: `text-ink underline decoration-accent underline-offset-[3px] decoration-2`
Sign-in prompt | `font-sans text-[13px] text-ink-2` | 13px | i18n: `m.register_have_account()` + `m.register_sign_in()`
Sign-in link | `font-sans text-xs font-bold tracking-[.1em] uppercase text-ink underline decoration-accent underline-offset-[4px] decoration-2` | 12px | href="/login"

8 Success panel — post-submit state
Success — German
localhost:3000/register
Willkommen im Familienarchiv.
Wir haben dir eine E-Mail an frieda@beispiel.de geschickt, um deine Adresse zu bestätigen. Anschließend kannst du dich anmelden.
Success — English
localhost:3000/register?lang=en
Welcome to Familienarchiv.
We sent an email to frieda@example.com to confirm your address. After that, you can sign in.
Implementation Reference — Success panelroutes/register/+page.svelte

Element | Tailwind classes | Real px | Notes
--- | --- | --- | ---
Success wrapper | `text-center py-2` | 8px top/bottom padding | Replaces the entire `<form>` block. Same card container as the form.
Checkmark circle | `w-[72px] h-[72px] rounded-full bg-[rgba(161,220,216,0.35)] inline-flex items-center justify-center mb-6` | 72px | Checkmark SVG: 34×34px, stroke #012851 2.2px.
Success headline | `font-serif font-normal text-[30px] leading-tight text-ink mb-3.5` | 30px | i18n key: `m.register_success_h()`
Success body | `font-serif text-[17px] leading-[1.55] text-ink-2 max-w-[420px] mx-auto mb-7` | 17px, max 420px | i18n key: `m.register_success_b()`. Replace `{email}` token with entered email.
CTA button | Same as submit button — full-width navy | — | href="/login". i18n: `m.register_success_action()`
Resend link | `font-sans text-xs font-bold tracking-[.1em] uppercase text-ink-2 px-2.5 py-2.5 underline decoration-accent underline-offset-[4px] decoration-2` | 12px | i18n: `m.register_success_resend()`. Calls `/api/auth/resend-verification`. Container: `max-w-[320px] mx-auto flex flex-col gap-2.5`

9 Mobile layout — 320px
320px — form at rest
9:41
Ein Familienprojekt
Schön, dass du
da bist.
Bereits 1.500 Briefe sind im Archiv. Mit deinem Konto hilfst du mit, sie zu bewahren.
Über dich
Vorname
Nachname
Konto
E-Mail
Passwort
Passwort best.
Name grid collapses to single column. Subtext shortened. Card padding reduced. All inputs remain 17px on real device (browser zooms accordingly).
320px — filled, ready to submit
9:41
Schön, dass du
da bist.
Über dich
Vorname
Nachname
Konto
E-Mail
Passwort
✓ 8 Zeichen
Best.
Stimmen überein.
320px — success panel
9:41
Willkommen im Familienarchiv.
Wir haben dir eine E-Mail an frieda@b.de geschickt, um deine Adresse zu bestätigen.
Implementation Reference — Mobile breakpointsroutes/register/+page.svelte

Breakpoint | Change | Tailwind | Notes
--- | --- | --- | ---
Default (mobile-first, <480px) | Single-column name grid | `grid grid-cols-1 gap-4` | Start mobile-first; add `sm:grid-cols-2` at 480px+
sm: 480px+ | 2-column name grid | `sm:grid-cols-2` | Custom breakpoint or use `min-[480px]:grid-cols-2`
Default (mobile-first) | Card padding 24px | `p-6` | Reduced from desktop 40px
sm: 640px+ | Card padding 40px | `sm:p-10` | Full desktop padding
Default (mobile-first) | Main padding 32px top | `pt-8` | Reduced from desktop 64px
sm: 640px+ | Main padding 64px top | `sm:pt-16` | —
All breakpoints | Input font size 17px | `text-[17px]` | Do NOT reduce on mobile — large text is critical for the 60+ audience. The browser will zoom the viewport to show text clearly.
All breakpoints | Touch targets ≥ 44px | Button: `py-4` = 16px × 2 + 13px font = 45px ✓ | CheckOption card: entire card is the touch target (~52px+ tall) ✓

10 Implementation notes — i18n, accessibility, backend
i18n keys (Paraglide)
New keys — messages/de.json + en.json + es.json

Key | DE
--- | ---
`register_eyebrow` | Ein Familienprojekt
`register_headline` | Schön, dass du da bist.
`register_sub` | Bereits 1.500 Briefe aus vielen Jahrzehnten…
`register_section_about` | Über dich
`register_section_account` | Konto
`register_section_prefs` | Benachrichtigungen
`register_first_name` | Vorname
`register_last_name` | Nachname
`register_email` | E-Mail-Adresse
`register_password` | Passwort
`register_password_confirm` | Passwort bestätigen
`register_pw_hint` | Mindestens 8 Zeichen.
`register_pw_match_ok` | Passwörter stimmen überein.
`register_pw_match_no` | Die beiden Passwörter stimmen noch nicht überein.
`register_notify_label` | Benachrichtige mich,
`register_notify_desc` | wenn jemand mich in einem Kommentar erwähnt…
`register_submit` | Konto erstellen
`register_submit_loading` | Wird erstellt…
`register_have_account` | Du hast bereits ein Konto?
`register_sign_in` | Anmelden
`register_success_h` | Willkommen im Familienarchiv.
`register_success_b` | Wir haben dir eine E-Mail an {email} geschickt…
`register_success_action` | Zur Anmeldung
`register_success_resend` | E-Mail erneut senden
`register_privacy` | Mit dem Erstellen eines Kontos stimmst du der
`register_privacy_link` | Datenschutzerklärung
`register_terms_link` | Nutzungsbedingungen
`password_show` | Anzeigen
`password_hide` | Verbergen
`page_title_register` | Registrieren — Familienarchiv

Accessibility requirements
WCAG 2.2 checklist

Criterion | Implementation | Level
--- | --- | ---
1.4.3 Contrast (text) | Navy #012851 on white: 14.5:1 ✓. Gray #4b5563 on white: 7.6:1 ✓. Gray #6b7280 on white: 5.9:1 ✓ (AA only — use for large text / hints) | AA
2.4.2 Page title | `<svelte:head><title>{m.page_title_register()}</title>` | A
1.3.1 Form labels | Every input has `<label for="...">`. No placeholder-as-label. | A
2.4.3 Focus order | Tab order: DE/EN/ES → Vorname → Nachname → Email → Password → Confirm → Checkbox → Submit | A
2.4.7 Focus visible | `focus-visible:ring-2 focus-visible:ring-focus-ring/15` on all inputs. `focus-visible:ring-2 focus-visible:ring-white/80` on submit button. | AA
2.5.3 Touch target (2.2) | All buttons ≥ 44px tall. CheckOption card ≥ 44px. Show/hide button: 44px tall (same as input height). | AA
4.1.3 Status messages | Password match/mismatch feedback announced via `aria-live="polite"` region. | AA
1.4.4 Resize text | All sizes in rem/px that scale with browser zoom. 17px input font = 1.0625rem. | AA
Error identification | `aria-invalid="true"` on invalid inputs. `aria-describedby` linking to error message element. | A

Backend wiring
Route + API integration

Concern | Details
--- | ---
Route | `frontend/src/routes/register/+page.svelte` + `+page.server.ts`
Form action | POST `?/register` → `+page.server.ts` action
API endpoint | `POST /api/auth/register` — create new AppUser (first name, last name, email, password, notifyOnMention)
On success | Backend sends verification email. Frontend shows success panel with entered email.
Resend endpoint | `POST /api/auth/resend-verification` — body: `{ email }`
Client-side validation | Run before server submit: required fields, email format, password ≥ 8 chars, password match. Show errors inline on first submit attempt.
Server-side errors | Map backend error codes via `getErrorMessage(code)`: EMAIL_ALREADY_EXISTS → Paraglide key. Never display raw backend messages.
notifyOnMention field | CheckOption checked state maps to `notifyOnMention: boolean` in request body. Default: `true`.
Language preference | The active language is a client-side Paraglide state only (page.svelte `$state`). Not persisted until the user creates an account; can be added to the registration payload as `preferredLanguage: 'de' | 'en' | 'es'` if the backend supports it.
Auth header | Register page is unauthenticated. Use a minimal header (AuthHeader without nav links) or a dedicated RegisterHeader.

Implementation sequence:

- 1. Add `POST /api/auth/register` to Spring Boot backend (new controller + service method)
- 2. Add `POST /api/auth/resend-verification`
- 3. Regenerate TypeScript API types (`npm run generate:api`)
- 4. Implement `frontend/src/routes/register/+page.svelte`
- 5. Add i18n keys to all three message files
- 6. Add route to navigation (unauthenticated footer or login page link)