Shuffle/docs/design/auth-screens.md
cschlaefke 7eaed46dc2
All checks were successful
CI / flutter (pull_request) Successful in 1m38s
CI / web-build (pull_request) Successful in 1m38s
CI / supabase (pull_request) Successful in 13s
CI / supabase-tests (pull_request) Successful in 35s
CI / e2e (pull_request) Successful in 2m58s
test(auth): address review findings on sign-in prompt (PR A)
Correct the focus comment (refs #61), test Space key and focus state,
prove scrollability at 200 percent text, drop needless minWidth, English
doc comment, allow OutlinedButton as secondary action in standalone cards,
English prompt text says plays instead of games.

Refs #49

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 01:25:22 +01:00

304 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Auth-Screens: Layout-Skizzen und Texte (Spec #1, Task U1)
Grundlage: `docs/specs/1-auth-registrierung-anmeldung.md` (§ 4.4 Meldungen, § 5.4 Screens, § 6 Plattform).
Komponenten: `app/lib/core/widgets/` (siehe `docs/design-system.md`). Alle Texte unten sind
**Vorschläge** und gehen über l10n (ARB); die ARB-Dateien pflegt der flutter-dev (A1/A2).
Schlüssel sind `lowerCamelCase` mit Präfix `auth`.
## 1. Gemeinsames Layout
- Alle sechs Screens nutzen `AppFormScaffold`. Kompakt (< 600 dp): Vollbild, scrollbar, Seitenrand 16 dp.
Ab 600 dp: zentrierte Karte, max. 480 dp breit (`AppBreakpoints.authCardMaxWidth`), Innenabstand 24 dp.
- Inhalt in einer Spalte, Abstand zwischen Feldern 16 dp, zwischen Gruppen 24 dp.
- Reihenfolge = Lesereihenfolge = Tab-Reihenfolge: Titel, Banner (falls vorhanden), Felder, Checkboxen,
Primärknopf, Sekundärlinks.
- Titel: `headlineSmall`, darunter optional ein Einleitungssatz in `bodyMedium`. Titel als Semantics-Überschrift
(`Semantics(header: true)`).
- Banner (`AppBanner`) stehen **über** den Feldern, direkt unter dem Titel. Fehler am Feld stehen unter dem Feld.
- Genau ein Primärknopf (`AppPrimaryButton`, volle Breite). Sekundäraktionen sind `TextButton`s (48 dp). Für eigenständige Karten (z. B. `SignInPrompt`) ist ein `OutlinedButton` als Sekundäraktion zulässig.
- Eingaben bleiben bei jedem Fehler erhalten. Während `submitting` sind Knopf und Felder gesperrt, der Knopf
zeigt den Spinner und den gesprochenen Text `authSubmitting`.
- Nach einem Fehler setzt der Screen den Fokus auf das erste fehlerhafte Feld; bei Banner-Fehlern bleibt der
Fokus am Knopf, das Banner wird als Live-Region angesagt.
- Enter im letzten Feld sendet ab (`TextInputAction.done`), sonst `next`.
### Semantics-Struktur (Regel, Issue #41)
- Überschrift, Text und Knopf sind **getrennte Semantics-Knoten**: `heading` (`AuthScreenTitle`), Text, `button`
mit Label (`AppPrimaryButton`, `TextButton`). Das gilt in jeder Breite, auch in der Karte ab 600 dp.
- Ursache des Fehlers: `Card` ist ein Semantics-Container ohne `explicitChildNodes`. Alle Kinder ohne eigenen
Container (Titel, `Text`, Knopf) werden in dessen einen Knoten gefaltet. Seiten mit Eingabefeld fielen nicht auf,
weil `AppTextField` & Co. eigene Container sind. Der Fehler trat auf allen Plattformen ab 600 dp (Karte) auf und wurde im E2E (Web) sichtbar; auf dem Handy ohne Karte nicht.
- Deshalb kapselt `AppFormScaffold` den Karteninhalt in `Semantics(explicitChildNodes: true)`. Neue Layouts mit
`Card`/`Semantics(container: true)` um Seiteninhalt müssen das ebenfalls tun; `MergeSemantics` nur gezielt für
zusammengehörige Einzelelemente, nie um ganze Seiten.
- Banner bleiben eigene Live-Regionen (`AppBanner`), ihre Aktion ist ein eigener Knopf-Knoten.
- Test: `expectSeparateSemantics` (`app/test/features/auth/presentation/auth_ui_support.dart`) im weiten Layout
(`wideScreenSize`); E2E klickt per `getByRole('button', …)`.
Legende der Skizzen: `[ ]` Feld, `[x]` Checkbox, `( )` Primärknopf, `<…>` Link/TextButton, `!` Banner.
## 2. Skizzen
### RegisterScreen (`/register`)
```
Konto erstellen (Titel)
! Fehler-Banner (nur bei registrationRejected / network / rateLimited / unexpected)
E-Mail
[ name@example.com ] Fehler: authEmailInvalid
Passwort
[ •••••••••• (Auge) ] Hinweis: authPasswordRule / Fehler je Regel
Anzeigename
[ Alex 4/30 ] Fehler: authDisplayNameEmpty | TooLong | Invalid
[x] Ich bin mindestens 16 Jahre alt Fehler: authAgeNotConfirmed
[x] Ich habe die <Datenschutzerklärung> gelesen und stimme den <Nutzungsbedingungen> zu
Fehler: authTermsNotAccepted
( Konto erstellen )
<Schon ein Konto? Anmelden>
```
### CheckEmailScreen (`/register/check-email`, Modus Bestätigung bzw. Reset)
```
Prüfe dein Postfach (Titel)
Wir haben dir eine E-Mail geschickt, falls ... (Text, bodyLarge)
Der Link ist 1 Stunde gültig. Die E-Mail kann einige Minuten dauern. Schau auch im Spam-Ordner nach.
! Info-Banner "E-Mail wurde erneut gesendet" (nach Resend, auch bei emailSendLimited)
! Fehler-Banner bei rateLimited / network
(nur wenn E-Mail fehlt, z. B. nach Reload:) E-Mail [ ]
( E-Mail erneut senden ) -> nur Modus Bestätigung, gesperrt solange Anfrage läuft
<Zurück zur Anmeldung> (Modus Reset: statt des Resend-Knopfs <Andere E-Mail-Adresse eingeben> -> /forgot-password)
```
Der Modus steckt im **Pfad**, nicht im `extra`: `/register/check-email` (Bestätigung, E-Mail als `extra`) und
`/forgot-password/check-email` (Reset, ohne `extra`). So überlebt der Modus einen Reload im Web. Im Modus Reset gibt es
keinen Resend (Spec § 5.4), Text `authCheckEmailBodyReset`, Gültigkeit und Zusatz „kann einige Minuten dauern“ wie im
Modus Bestätigung; Keys `check_email_different_address` (statt `check_email_resend`).
### LoginScreen (`/login`, umgesetzt in 01b)
```
Anmelden
! Fehler-Banner: authErrorInvalidCredentials | authErrorRateLimited | authErrorNetwork (+ "Erneut versuchen")
! Info-Banner authErrorEmailNotConfirmed + Aktion "Bestätigungs-E-Mail erneut senden"
! Info-Banner "Wir haben die E-Mail erneut gesendet ..." | Fehler-Banner (authErrorRateLimited / authErrorNetwork) nach dem Resend
E-Mail [ ] Fehler (nur lokal): authErrorEmailInvalid
Passwort [ ••••• (Auge) ] Fehler (nur lokal): authPasswordRequired
( Anmelden )
<Passwort vergessen?> -> /forgot-password
<Konto erstellen> -> /register
```
Umsetzungsentscheidungen:
- Falsche Zugangsdaten stehen nie am Feld, sondern nur im Banner; Text für falsche E-Mail und falsches Passwort
ist identisch (Krit. 7, Test vergleicht alle sichtbaren Texte). Felder zeigen nur lokale Fehler (leer / E-Mail-Syntax).
- Bei `emailNotConfirmed` merkt sich der Controller die E-Mail des Versuchs; "erneut senden" nutzt diese Adresse
(`ResendController`, `ResendOrigin.login`), nicht den aktuellen Feldinhalt. Dort wird `emailSendLimited` als
Drosselungshinweis gezeigt (F12). Während des Sendens ist die Aktion **deaktiviert, aber sichtbar** (`AppBanner.actionEnabled: false`):
kein Doppelversand, und der Fokus eines Screenreaders verschwindet nicht mit dem Knopf (Semantics: Knopf, nicht aktiviert).
- Ein neuer Anmeldeversuch setzt das Resend-Ergebnis zurück.
- `from` wird an „Konto erstellen“ und „Passwort vergessen?“ weitergegeben (`authLocation`, geprüft mit `safeFromPath`);
Register- und ForgotPassword-Screen geben es mit „Anmelden“ bzw. „Zurück zur Anmeldung“ an `/login` zurück.
- Erfolg: `context.go(<from oder />)`. `from` wird mit `safeFromPath` geprüft (nur interne Pfade, keine Auth-Seiten,
kein `//`, `/\` oder Schema); sonst Startseite. Zusätzlich leitet der Router bei Status `signedIn` von `/login` um.
- Autofill: `AutofillGroup`, E-Mail `username` + `email`, Passwort `password`; `TextInput.finishAutofillContext()` bei Erfolg.
- Neue l10n-Schlüssel: `authLoginTitle`, `authLoginSubmit`, `authLoginForgotPassword`, `authLoginCreateAccount`.
- Widget-Keys: `login_screen`, `login_email`, `login_password`, `login_submit`, `login_banner`,
`login_not_confirmed_banner`, `login_resent_banner`, `login_resend_error_banner`, `login_forgot_link`, `login_register_link`.
### ForgotPasswordScreen (`/forgot-password`)
```
Passwort zurücksetzen
Gib die E-Mail-Adresse deines Kontos ein. Wir senden dir einen Link zum Zurücksetzen.
E-Mail [ ] Fehler: authEmailInvalid
( Link senden )
<Zurück zur Anmeldung> -> /login (mit from)
```
Umsetzung: Nach jedem 200 **und** bei `emailSendLimited` (F12) geht es ohne Hinweis zu `/forgot-password/check-email`
(Antwort für bekannte und unbekannte Adresse sowie für die Mail-Sperre ist identisch, getestet per exaktem
Textvergleich). Sichtbare Fehler nur für lokale Syntax, `invalidEmail` (am Feld), `rateLimited`/`network`/`unexpected`
(Banner, bei den beiden letzten mit „Erneut versuchen“, während des Sendens deaktiviert). Die Adresse wird nicht an
die Folgeseite übergeben. Keys: `forgot_password_screen`, `forgot_password_email`, `forgot_password_submit`,
`forgot_password_banner`, `forgot_password_body`, `forgot_password_back`.
### EmailLinkScreen (`/auth/confirm`)
Zustände: `idle` (Knopf), `submitting`, `success` (nur Bestätigung), `error` (Link ungültig).
```
E-Mail-Adresse bestätigen | Neues Passwort festlegen (Titel je Typ)
Ein Klick genügt, um fortzufahren.
( E-Mail-Adresse bestätigen ) | ( Weiter zum neuen Passwort )
Erfolg: ! Info-Banner "E-Mail bestätigt" (+ Mobile-Web: "Du kannst dich jetzt auch in der App anmelden.")
Fehler: ! Fehler-Banner "Der Link ist abgelaufen ..." ( Neuen Link anfordern )
```
Typ Recovery (01c): Titel, Text und Knopf aus `authRecovery*`; das Einlösen passiert erst per Klick. Bei Erfolg zeigt die
Seite nichts weiter an, der Statuswechsel `passwordRecovery` leitet nach `/reset-password`. Ungültiger oder abgelaufener
Link (`otp_expired`) → Fehlerzustand mit „Neuen Link anfordern“ → `/forgot-password` (Bestätigung: `/login`).
### ResetPasswordScreen (`/reset-password`)
```
Neues Passwort festlegen
Neues Passwort
[ ••••••••• (Auge) ] Live-Regeln: authPasswordRule, Fehler je Regel / samePassword
( Passwort speichern )
<Abbrechen> -> signOut -> /login
Erfolg (Ansicht ersetzt das Formular, kein Abbrechen mehr):
Passwort geändert. (Hinweis authPasswordChanged)
( Weiter ) -> /
```
Umsetzungsentscheidungen (01c):
- **Der Redirect nach `userUpdated` wäre ohne Eingriff ein Fehler:** gotrue liefert `userUpdated` asynchron (auch nach der Rückkehr von `updateUser`); die Regel „angemeldet auf `/reset-password` → `/`“ hätte die Seite entfernt, bevor die Erfolgsmeldung
(Krit. 9) sichtbar ist. Lösung: `userUpdated` während der Recovery beendet den Guard **nicht**, sondern setzt
`AuthStatus.passwordRecovery(passwordChanged: true)`. Das Repository markiert die Änderung nach erfolgreichem `updateUser` zusätzlich selbst (idempotent), das Formular bleibt
bis zur Erfolgsansicht gesperrt. Erst „Weiter“ (`AuthRepository.finishPasswordRecovery()`, vor dem Wechsel ohne Wirkung) beendet den
Guard; der Fokus liegt beim Wechsel auf „Weiter“; der Router leitet dann nach `/`. Die Erfolgsansicht leitet sich aus dem Status ab (`passwordRecoveryCompleted`),
überlebt also einen Neuaufbau der Seite. Bewusst **kein** zeitgesteuerter Sprung (Screenreader, Lesezeit). Spec § 5.5
ist entsprechend ergänzt.
- „Abbrechen“ gibt es nur im Formular, nicht in der Erfolgsansicht (es würde eine gültige Sitzung abmelden).
- Passwortregel wie bei der Registrierung (10 Zeichen, Liste häufiger Passwörter; die Liste wird vor dem Absenden
geladen, Fallback nur Länge). `weak_password` und `same_password` stehen am Feld, alles andere im Banner.
- Hinweistext ehrlich zu F1: andere Geräte können bis zu etwa einer Stunde angemeldet bleiben; nichts von „sofort“.
- Keys: `reset_password_screen`, `reset_password_password`, `reset_password_submit`, `reset_password_cancel`,
`reset_password_banner`, `reset_password_success_banner`, `reset_password_continue`.
## 3. Textvorschläge
Ton: du-Form, kurz, sachlich-freundlich, keine Schuldzuweisung. Platzhalter in ICU-Syntax (`{n}`).
Neutrale Texte (Krit. 4, F12) verraten nie, ob eine Adresse registriert ist.
### 3.1 Meldungen aus § 4.4 (Fehlerabbildung)
| Schlüssel | de | en |
|---|---|---|
| `authErrorInvalidCredentials` | E-Mail-Adresse oder Passwort ist nicht korrekt. | Email address or password is incorrect. |
| `authErrorEmailNotConfirmed` | Bestätige zuerst deine E-Mail-Adresse. Wir haben dir dazu eine E-Mail geschickt. | Please confirm your email address first. We sent you an email for this. |
| `authActionResendConfirmation` | Bestätigungs-E-Mail erneut senden | Resend confirmation email |
| `authErrorRateLimited` | Zu viele Versuche. Bitte warte einige Minuten und versuche es erneut. | Too many attempts. Please wait a few minutes and try again. |
| `authErrorWeakPasswordLength` | Das Passwort ist zu kurz. | The password is too short. |
| `authErrorWeakPasswordCharacters` | Das Passwort ist zu einfach. Wähle ein anderes. | The password is too simple. Choose a different one. |
| `authErrorWeakPasswordPwned` | Dieses Passwort ist sehr verbreitet und leicht zu erraten. Wähle ein anderes. | This password is very common and easy to guess. Choose a different one. |
| `authErrorSamePassword` | Wähle ein anderes als dein bisheriges Passwort. | Choose a password different from your current one. |
| `authErrorLinkInvalidOrExpired` | Der Link ist abgelaufen oder wurde schon benutzt. | The link has expired or has already been used. |
| `authActionRequestNewLink` | Neuen Link anfordern | Request a new link |
| `authErrorEmailInvalid` | Gib eine gültige E-Mail-Adresse ein. | Enter a valid email address. |
| `authErrorRegistrationRejected` | Registrierung nicht möglich. Bitte prüfe deine Angaben. | Registration is not possible. Please check your details. |
| `authErrorNetwork` | Keine Verbindung. Für diesen Schritt brauchst du Internet. | No connection. You need internet access for this step. |
| `authErrorUnexpected` | Etwas ist schiefgelaufen. Bitte versuche es erneut. | Something went wrong. Please try again. |
| `authActionRetry` | Erneut versuchen | Try again |
`emailSendLimited` hat bewusst **keinen** eigenen Text: Register/Forgot/CheckEmail zeigen den neutralen Erfolgstext
(§ 3.3), nur der Resend im LoginScreen zeigt `authErrorRateLimited`.
### 3.2 Meldungen aus § 5.4 (Felder, Banner, Hinweise)
| Schlüssel | de | en |
|---|---|---|
| `authPasswordTooShort` | Mindestens 10 Zeichen – {missing, plural, =1{es fehlt noch 1 Zeichen} other{es fehlen noch {missing} Zeichen}}. | At least 10 characters – {missing, plural, =1{1 more character needed} other{{missing} more characters needed}}. |
| `authPasswordCommon` | Dieses Passwort ist sehr verbreitet und leicht zu erraten. Wähle ein anderes. | This password is very common and easy to guess. Choose a different one. |
| `authPasswordRule` | Mindestens 10 Zeichen. Kein sehr verbreitetes Passwort. | At least 10 characters. Not a very common password. |
| `authPasswordRequired` | Gib ein Passwort ein. | Enter a password. |
| `authDisplayNameEmpty` | Gib einen Anzeigenamen ein. | Enter a display name. |
| `authDisplayNameTooLong` | Der Anzeigename darf höchstens {max} Zeichen lang sein. | The display name can be at most {max} characters long. |
| `authDisplayNameInvalid` | Dieser Anzeigename ist nicht erlaubt. Verwende sichtbare Zeichen. | This display name is not allowed. Use visible characters. |
| `authAgeNotConfirmed` | Shuffle ist erst ab 16 Jahren nutzbar. | Shuffle can only be used from age 16. |
| `authTermsNotAccepted` | Bitte stimme Datenschutzerklärung und Nutzungsbedingungen zu. | Please agree to the privacy policy and the terms of use. |
| `authFieldEmailLabel` | E-Mail-Adresse | Email address |
| `authFieldPasswordLabel` | Passwort | Password |
| `authFieldNewPasswordLabel` | Neues Passwort | New password |
| `authFieldDisplayNameLabel` | Anzeigename | Display name |
| `authPasswordShow` | Passwort anzeigen | Show password |
| `authPasswordHide` | Passwort verbergen | Hide password |
| `authCheckboxAgeLabel` | Ich bin mindestens 16 Jahre alt | I am at least 16 years old |
| `authCheckboxTermsPrefix` | Ich habe die | I have read the |
| `authCheckboxTermsPrivacyLink` | Datenschutzerklärung | privacy policy |
| `authCheckboxTermsMiddle` | gelesen und stimme den | and agree to the |
| `authCheckboxTermsTermsLink` | Nutzungsbedingungen | terms of use |
| `authCheckboxTermsSuffix` | zu | (leer) |
| `authBannerTypeError` | Fehler | Error |
| `authBannerTypeInfo` | Hinweis | Note |
| `authSubmitting` | Bitte warten … | Please wait … |
Hinweis zur Checkbox „Nutzungsbedingungen“: Satzbau ist je Sprache verschieden. Der Satz wird deshalb aus den
Segmenten `Prefix`, `PrivacyLink`, `Middle`, `TermsLink`, `Suffix` zusammengesetzt (`AppCheckboxField.rich`);
im Englischen bleibt `Suffix` leer. Alternativ ein ICU-Satz mit Platzhaltern, den der Screen an den Linkstellen
teilt – Entscheidung beim flutter-dev (A2).
### 3.3 Screen-Texte
| Schlüssel | de | en |
|---|---|---|
| `authRegisterTitle` | Konto erstellen | Create account |
| `authRegisterSubmit` | Konto erstellen | Create account |
| `authRegisterHaveAccount` | Schon ein Konto? Anmelden | Already have an account? Sign in |
| `authCheckEmailTitle` | Prüfe dein Postfach | Check your inbox |
| `authCheckEmailBody` | Wir haben dir eine E-Mail geschickt, falls die Adresse noch nicht registriert ist. Folge dem Link darin, um fortzufahren. | We sent you an email if the address is not registered yet. Follow the link in it to continue. |
| `authCheckEmailBodyReset` | Wir haben dir eine E-Mail mit einem Link zum Zurücksetzen geschickt, falls zu dieser Adresse ein Konto besteht. | We sent you an email with a reset link if an account exists for this address. |
| `authCheckEmailDelayHint` | Die E-Mail kann einige Minuten dauern. Schau auch im Spam-Ordner nach. | The email can take a few minutes. Also check your spam folder. |
| `authCheckEmailValidity` | Der Link ist {duration} gültig. | The link is valid for {duration}. |
| `authCheckEmailValidityOneHour` | 1 Stunde | 1 hour |
| `authCheckEmailResend` | E-Mail erneut senden | Resend email |
| `authCheckEmailResent` | Wir haben die E-Mail erneut gesendet, falls es zu dieser Adresse etwas zu senden gibt. | We resent the email, if there is something to send for this address. |
| `authCheckEmailDifferentAddress` | Andere E-Mail-Adresse eingeben | Enter a different email address |
| `authBackToLogin` | Zurück zur Anmeldung | Back to sign in |
| `authLoginTitle` | Anmelden | Sign in |
| `authLoginSubmit` | Anmelden | Sign in |
| `authLoginForgotPassword` | Passwort vergessen? | Forgot password? |
| `authLoginCreateAccount` | Konto erstellen | Create account |
| `authForgotTitle` | Passwort zurücksetzen | Reset password |
| `authForgotBody` | Gib die E-Mail-Adresse deines Kontos ein. Wir senden dir einen Link zum Zurücksetzen. | Enter the email address of your account. We will send you a reset link. |
| `authForgotSubmit` | Link senden | Send link |
| `authConfirmTitle` | E-Mail-Adresse bestätigen | Confirm email address |
| `authConfirmBody` | Ein Klick genügt, um deine E-Mail-Adresse zu bestätigen. | One click is enough to confirm your email address. |
| `authConfirmSubmit` | E-Mail-Adresse bestätigen | Confirm email address |
| `authConfirmSuccess` | E-Mail bestätigt. | Email confirmed. |
| `authConfirmSuccessMobileWeb` | Du kannst dich jetzt auch in der App anmelden. | You can now also sign in in the app. |
| `authRecoveryTitle` | Neues Passwort festlegen | Set a new password |
| `authRecoveryBody` | Ein Klick genügt, um ein neues Passwort festzulegen. | One click is enough to set a new password. |
| `authRecoverySubmit` | Weiter zum neuen Passwort | Continue to new password |
| `authResetTitle` | Neues Passwort festlegen | Set a new password |
| `authResetSubmit` | Passwort speichern | Save password |
| `authResetCancel` | Abbrechen | Cancel |
| `authPasswordChanged` | Passwort geändert. Andere Geräte werden abgemeldet; das kann bis zu einer Stunde dauern. | Password changed. Other devices will be signed out; this can take up to an hour. |
## 4. Barrierefreiheit pro Screen (Checkliste für A2–A4 und T2)
- Jedes Feld hat ein sichtbares Label (kein Platzhalter-Label); Fehler per `errorText` (Icon + Text + Live-Region).
- Banner und Fehler werden beim Erscheinen vorgelesen (Live-Region); keine reinen Farb-Signale.
- Passwortfelder: `AppPasswordField` mit `isNewPassword: true` bei Register/Reset, sonst `false`; E-Mail mit
`AutofillHints.email` (Login: `AutofillHints.username`/`email`), `TextInputType.emailAddress`.
- Links in Checkbox-Sätzen (`AppCheckboxField.rich`) sind eigene Semantics-Knoten (`link: true`, Label = Linktext,
Tap), keine Custom Actions (Flutter-Web setzt diese nicht in ARIA um). Der Checkbox-Knoten trägt den ganzen Satz.
- Meldungen, die sich bei jedem Tastendruck ändern („es fehlen noch n“), sind während des Tippens keine Live-Region
(`liveError: false`) und werden beim Verlassen des Feldes angesagt; der sichtbare Text bleibt.
- Alle interaktiven Elemente >= 48 dp, Tab-Reihenfolge wie Lesereihenfolge, Fokus sichtbar.
- Test je Screen mit `textScaler` 2.0 (Android 320 x 640), `androidTapTargetGuideline`,
`labeledTapTargetGuideline`, `textContrastGuideline`, Hell und Dunkel (T2).
## 3. Anmelde-Hinweis auf der Startseite (`SignInPrompt`, Spec #49, PR A)
Karte auf der Startseite, nur im Status `signedOut` (Entscheidung in `showSignInPromptProvider`; Laden: kein Hinweis,
Stream-Fehler: Hinweis). Auf allen Plattformen inklusive Web.
```
Dein Konto (Überschrift, titleLarge, Semantics header)
Melde dich an, um deine Sammlung und Partien zu verwalten. (bodyMedium)
( Anmelden ) -> /login (AppPrimaryButton, volle Breite)
( Konto erstellen ) -> /register (OutlinedButton, volle Breite)
```
- Max. 480 dp breit (`AppBreakpoints.authCardMaxWidth`), zentriert unter dem Platzhaltertext; Innenabstand 24 dp, Knopfabstand 12 dp.
- Karteninhalt in `Semantics(explicitChildNodes: true)` (Regel aus Issue #41): Überschrift, Text und beide Knöpfe sind eigene Knoten.
- Lese- und Tab-Reihenfolge: Überschrift, Text, „Anmelden“, „Konto erstellen“. Kein `from`-Parameter.
- l10n: `signInPromptTitle`, `signInPromptBody`, `signInPromptSignIn`, `signInPromptRegister`.
- Keys: `sign_in_prompt`, `sign_in_prompt_title`, `sign_in_prompt_body`, `sign_in_prompt_sign_in`, `sign_in_prompt_register`.