## Why

Admin access today is a single flat allowlist: any admin who can sign in sees every event,
every guest and every headcount in the system. That was correct while the product was one
couple's wedding, and it is wrong now that an admin creates events for customers — a host
brought in to run their own Walima can read the entire guest list of someone else's Marriage.

Three things block closing that gap. There is no owner on `event`, so "their own events" is
not a question the database can answer. The role matrix carries three roles (`super_admin`,
`co_host`, `viewer`) whose distinctions no longer match how the product is used. And
`Action.MANAGE_ADMINS` exists in the policy matrix with no endpoint and no screen behind it,
so the only way to onboard a host is to insert a row by hand.

Sign-in itself is the fourth. Google is the only way in, which means a host must have a Google
account and the operator must wait on Google to onboard anybody. Adding username-and-password
sign-in alongside it lets a super admin create a working account outright and hand over a
temporary password — and it is what PRD FR-4.1 asked for before the 2026-08-07 owner decision
narrowed v1 to Google only.

## What Changes

### Access control

- **BREAKING** — collapse `AdminRole` from three members to two: `super_admin` and `host`.
  `co_host` and `viewer` are removed. Existing rows migrate: `co_host` → active `host`,
  `viewer` → inactive `host` (a viewer could only read, so silently granting them write and
  delete would be a privilege escalation performed by a migration).
- **BREAKING** — add `event.owner_admin_id`. A host sees, edits, sends to and deletes only
  the events they own and the guests underneath them; a super admin sees and does everything.
  Existing events backfill to the bootstrap super admin.
- Scope every admin surface that reads events, guests, headcounts, messages, exports, QR codes
  and card designs to the caller's visible-event set.

### Authentication

- **BREAKING** — reverses design decision D7 and its statement in `CLAUDE.md` that no password
  is stored anywhere. Admin accounts are now one of two kinds, fixed at creation: a **Google
  account**, or a **password account** signing in with a username and password.
- **BREAKING** — replace the `itsdangerous` signed-session cookie with a **JWT**, valid for
  **2 days** (up from 12 hours), carried in the same httpOnly cookie so it stays unreadable
  from JavaScript. The token carries identity only — never the role — so demotion and
  withdrawal keep taking effect on the very next request.
- A password change invalidates every token issued before it, so a compromised session cannot
  outlive the credential it was born from.
- First-time Google sign-in no longer rejects an unknown account. It creates an **inactive**
  account and shows an "awaiting approval" screen. Password accounts never self-register —
  they exist only because a super admin created them.
- Password sign-in is rate-limited and locks out after repeated failures. A password is
  something that can be guessed; a Google assertion is not.

### Account administration

- Add a **user configuration menu** for super admins: the pending queue, activate/deactivate,
  set role, transfer event ownership, and the last-active-super-admin guard enforced
  server-side.
- A super admin can **create and edit any account**, including creating a password account
  outright and **setting or resetting its temporary password**.
- An account signed in with a temporary password is **confined until it sets its own**: it
  holds a session, and every admin surface except changing that password refuses it.
- Add **profile settings** where any admin manages their own account, including changing their
  own password — password accounts only.
- For a Google account, a super admin may only activate, deactivate and assign a role. There is
  no password to set, and none of the password machinery applies.

### Dev and bootstrap

- Keep the username-only `/auth/dev` sign-in, **dev builds only**, unchanged in its gating
  (`AUTH_DEV_BYPASS` **and** `ENVIRONMENT != prod`, plus `assert_production_ready()` refusing
  to boot prod with the flag set). Promote it from an undocumented convenience to a specified,
  tested dev-only capability. It remains a shortcut past authentication, distinct from real
  password sign-in, and it never provisions an account.
- Seed the bootstrap super admin from `SEED_SUPER_ADMIN_EMAIL` in **production** as well as
  dev, so a fresh production database is not locked out with everyone in the pending queue and
  nobody able to approve them.
- Google sign-in was verified working while planning this change; nothing here repairs it.

## Capabilities

### New Capabilities
- `admin-user-management`: the super-admin screen for the admin roster — the pending-approval
  queue produced by first-time Google sign-in, creating and editing accounts of either kind,
  issuing temporary passwords, activation and deactivation, role assignment across the two
  roles, event-ownership transfer, and the invariants that protect the last active super admin.

### Modified Capabilities
- `admin-auth`: two roles instead of three and a rewritten capability matrix; two kinds of
  account and two sign-in paths; JWT sessions valid for two days; the temporary-password
  confinement; self-service password change; first-time Google sign-in provisions a pending
  account instead of rejecting it; the dev-only username sign-in becomes a specified
  requirement; the existing "Admin user management" requirement moves out to
  `admin-user-management`.
- `event-management`: events carry an owner; the visible-event set is defined here as the
  cross-cutting rule every admin surface obeys; hosts create events they then own; hosts
  delete only their own; super admins transfer ownership.
- `guest-management`: a guest's visibility and mutability follow the owner of the event that
  owns the guest, on both the per-event and the cross-event guest surfaces.
- `admin-dashboard`: counts, trends and event pickers reflect the caller's visible events
  only; navigation matches role, scope and account kind.

## Impact

**Database** — `admin_role` enum loses two members and gains one (a Postgres enum change, so
a real migration with a type swap, not an `ALTER TYPE ... ADD VALUE`); `event` gains
`owner_admin_id` (FK to `admin_user`, `ON DELETE RESTRICT`, indexed, backfilled then made
`NOT NULL`); `admin_user` gains `status` (replacing `is_active`, because pending and withdrawn
must be told apart), `auth_method`, `username`, `password_hash`, `password_set_at`,
`must_change_password`, `password_expires_at`, `failed_login_count`, `locked_until` and
`sessions_valid_from`.

**API** — 42 admin endpoints across `admin_events`, `admin_guests`, `admin_messaging`,
`admin_stats`, `admin_cards`, `admin_data` and `admin_wedding` currently resolve records by id
with no ownership check. Every one needs the scope applied at the query, not after it, or a
host learns an id exists by the difference between 403 and 404. Every one also gains the
temporary-password gate. `app/services/policy.py`'s matrix is rewritten; a new scope helper
joins it as the second half of the authorization story. New `/admin/users` router behind
`Action.MANAGE_ADMINS`, new password sign-in and password-change endpoints, and the session
layer moves from `itsdangerous` to JWT.

**Dependencies** — two new Python packages: `pyjwt` for tokens and `argon2-cffi` for password
hashing. `itsdangerous` is no longer used for the session.

**Configuration** — a dedicated JWT signing secret and the two-day lifetime, both added to
`assert_production_ready()` alongside `SEED_SUPER_ADMIN_EMAIL`.

**Frontend** — new user-configuration and profile screens under `web/app/admin/(protected)/`;
the sign-in page gains a password form, a pending state and a withdrawn state; a forced
password-change screen that cannot be navigated past; role-aware navigation reads the new
permission set; the generated client (`web/lib/api/schema.d.ts`) must be regenerated or CI
fails.

**Tests** — `test_policy.py` and `test_admin_route_guards.py` both assert the three-role
matrix by name and will fail until rewritten. Route guards need two more axes beyond "may this
role call this endpoint": "may this admin reach this record", and "is this admin confined to
changing their password".

**Ops** — `SEED_SUPER_ADMIN_EMAIL` becomes production-relevant. `make seed` and
`make walkthrough` both assume the dev bypass. Every admin's session is invalidated by the
cookie format change, so everyone signs in again once on deploy.
