## 1. Data model and migration

- [x] 1.1 Add `host_name_1` (TEXT, NOT NULL), `host_name_2` (TEXT, nullable) and `host_phone` (VARCHAR(20), NOT NULL) to the `Event` model in `api/app/models/wedding.py`, with docstrings naming design D1 and D2 and stating that `host_name_2` is NULL — never `""` — when absent
- [x] 1.2 Write one Alembic revision that adds the three columns nullable, backfills them, then applies the NOT NULL constraints — design D5. Its docstring must state that downgrade destroys host data
- [x] 1.3 Backfill `host_phone` from the owning wedding's `host_contact_phone`, and write the sentinel `+880000000000` where that is NULL, with a comment explaining why a deliberately-unreachable number beats a blank or a plausible one
- [x] 1.4 Backfill `host_name_1` with the placeholder `Set host name` and leave `host_name_2` NULL. Comment why: the host is a parent, `bride_name`/`groom_name` are the couple, and writing those would state something the domain says is false
- [x] 1.5 Run `make migrate` against a database holding existing events and confirm the backfill populated every row before the constraints applied

## 2. API

- [x] 2.1 Add the three fields to `AdminEventRead` in `api/app/routers/admin_events.py`
- [x] 2.2 Add `host_name_1` and `host_phone` as required (`Field(min_length=1)`) and `host_name_2` as optional to `EventCreate`; add all three as optional-to-omit on `EventUpdate` while refusing an explicit empty value for the two required ones
- [x] 2.3 Normalise `host_phone` through `app.services.phone.normalize_phone` on both create and update, catching `InvalidPhoneNumberError` and raising a 422 that names the phone field — design D4, mirroring how `_clean_messages` surfaces its error
- [x] 2.4 Coerce a blank or whitespace-only `host_name_2` to `NULL` on both write paths, so absent has exactly one representation
- [x] 2.5 Expose the three fields on the event payload the invitation pages read (`api/app/schemas/invitation.py`), then run `make client` and commit the regenerated `web/lib/api/schema.d.ts` — CI fails on a stale client
- [x] 2.6 Update `api/scripts/seed.py` so every seeded event carries host details; update `api/scripts/walkthrough.py` so its event-creation step sends them

## 3. Admin UI

- [x] 3.1 Add `hostName1`, `hostName2` and `hostPhone` to the event view model(s) in `web/lib/view-models.ts`, listed explicitly like every other field (design D8)
- [x] 3.2 Add a Host section to `web/components/admin/EventEditorModal.tsx` positioned between Settings and Card design (design D9), with the two name inputs and the phone, the optional one labelled as optional
- [x] 3.3 Wire the host fields into the editor's staged save so they travel in the existing settings PATCH and are discarded when the modal is closed without saving
- [x] 3.4 Block Save in the editor when `host_name_1` or `host_phone` is empty, showing which field is required rather than failing on the server round-trip
- [x] 3.5 Add the same three fields to the Create event form in `web/components/admin/EventsView.tsx`, placed after Venue/Address and before Invitation messages (design D10), with the same required-field handling
- [x] 3.6 Surface the API's 422 for a malformed phone against the phone field in both forms, not as a generic form-level error

## 4. Invitation page

- [x] 4.1 Add the `Invited By` label and the `And` separator to both the `en` and `bn` dictionaries in `web/lib/i18n.ts`, as ordinary keys alongside `dear` and `dressCode` (design D11)
- [x] 4.2 Write the name-composition helper reading the separator from the dictionary: one name renders alone, two render joined by `And` in primary-then-secondary order (design D6)
- [x] 4.3 Render the host block at the end of the below-the-fold region in `web/components/InvitationShell.tsx` — the `Invited By` label above, names on one line, phone beneath as a `tel:` link, centred, server-rendered, wrapping rather than truncating
- [x] 4.4 Style the label subordinate to the names, so the names stay what a guest reads first
- [x] 4.5 Delete the trailing `couple.hostContactPhone` paragraph from `InvitationShell.tsx`, leaving `CoupleView.hostContactPhone` populated for the cancellation page and the RSVP-closed message (design D7)
- [x] 4.6 Confirm the block renders on both `/i/{token}` and `/e/{slug}`, and that `/e/{slug}` still carries no guest data

## 5. Tests

- [x] 5.1 API test: creating an event without `host_name_1`, or without `host_phone`, is rejected; creating one with both succeeds and stores them
- [x] 5.2 API test: a host phone in local Bangladeshi form is stored in E.164; an unparseable one returns a 422 naming the phone field
- [x] 5.3 API test: a blank `host_name_2` is stored as NULL, and clearing a required host field via PATCH is refused
- [x] 5.4 API test: two events of one wedding hold independent host details
- [x] 5.5 Migration test: an event belonging to a wedding with no `host_contact_phone` receives the sentinel, one with a phone receives that number, and every upgraded event receives the placeholder host name rather than the bride's or groom's
- [ ] 5.6 Render test: the block is labelled `Invited By`, one name renders alone, two render joined by `And`, and the phone is a `tel:` link — covering the composition helper directly

## 6. Delivery checks

- [x] 6.1 `make lint` and `make test` both clean
- [x] 6.2 `make walkthrough` passes end to end, creating an event with host details and opening a guest link that shows the host block
- [x] 6.3 Open a real invitation in `make dev` on a phone-width viewport and confirm the host block is last, centred, labelled `Invited By`, and dials on press
- [x] 6.4 Update `CLAUDE.md` — the host is per-event, and the wedding-wide `host_contact_phone` now serves only the cancellation and RSVP-closed surfaces
- [x] 6.5 Write the operator release note: every pre-existing event shows the placeholder `Set host name` and must be given its real host — the parent or parents hosting it — before guests see the invitation; any event still showing `+880000000000` has no real phone either
