Flow overview. Screens A and B live at /forgot-password; C and D at /reset-password/[token]. In the build only one is shown at a time and each is a centred card on a grey band. All noindex and excluded from the sitemap. Analytics: password_reset_requested, password_reset_completed.
Screen A, request. Single email field, validated on submit. Always moves to Screen B whether or not the account exists, to avoid revealing which emails are registered. Rate limit: 3 requests per hour per email and per IP. Button label "Send reset link", spinner while sending. Prefills email if arriving from the log in page.
Screen B, sent. Neutral wording ("If an account exists..."). Token is single use, random, stored hashed, valid 60 minutes. Resend has a 30 second cooldown and a counter. Hints help social-login users who have no password. Link to 14-contact.html for support.
Screen C, new password. Token checked on page load; if bad, show the error card. Password minimum 10 characters, common-password check, strength chips, show/hide toggle, confirm field must match. Inline errors on blur. Submit label "Save new password". Password manager attributes (autocomplete="new-password").
Screen D, done. Invalidate the token and all existing sessions and refresh tokens, send a confirmation email "Your Popdance password was changed. Not you? Contact us now". Link to 29-login.html. Optionally log the user in directly (decision for security review).
Expired or used link. Clear explanation and a way forward: request a new link, log in or sign up. Also shown for tampered tokens.
Mobile
Cards go full width, one per screen. Buttons are full width and fixed to the keyboard where practical. The email field uses the email keyboard; the password field offers autofill suggestions.
States
Default, validation error, sending, sent, resend cooldown, rate limited ("Too many requests. Try again in an hour or contact support"), token valid, token invalid or expired, password too weak, mismatch, saving, success, server error ("Something went wrong. Please try again."), logged-in user visiting (offer to change password in 28-customer-settings.html instead).