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.
| 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) |
| 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 |
| 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. |
| 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" |
| 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. |
| 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() |
| 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" |
| 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 |
| 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) ✓ |
| 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 |
| 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 |
| 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. |
POST /api/auth/register to Spring Boot backend (new controller + service method)POST /api/auth/resend-verificationnpm run generate:api)frontend/src/routes/register/+page.svelte