Two issues from the first user test pass:
1. POST /admin/login was 500'ing on any password attempt that contained
non-ASCII characters (e.g. a smart-quote autofill from the browser
password manager). secrets.compare_digest(str, str) requires both
sides to be bytes or ASCII-only str; otherwise it raises TypeError.
Encoding both sides to UTF-8 bytes before the constant-time compare
makes the route degrade cleanly to 401 instead of 500.
2. Reconnecting an instructor while the session is in question_closed
left the dashboard stuck on "Reveal pending..." because
send_instructor_snapshot only replayed state + lobby_update +
full_leaderboard for closed sessions, not the question_open and
question_closed payloads needed to render the reveal card.
Now we replay question_open + question_closed + full_leaderboard
for the question_closed branch, so the SPA renders the full reveal
immediately on reconnect without waiting for the next event.