Signup now collects only email + displayName, creates an
unconfirmed user with empty password hash, mints an
EmailConfirmationToken, and dispatches a confirmation email.
Password is set during ConfirmUserEmail.
validates email present + format (wraps EmailAddress vo's
InvalidArgumentException as BadRequest), password present +
>= 8 chars, then ensures email not already registered. hashes
password through injected PasswordHasher and persists via
UserRepository->create with isAdmin=false (admins are seeder-
only per plan). throws DomainException on duplicate email so
the controller layer can map it to 409. all 18 tests pass.