## Purpose

How the guest-facing invitation is composed: a compact personal greeting above artwork that dominates the screen, a single call to action below it, and an RSVP form that appears only when asked for — presented as a side drawer on wide screens and a bottom sheet on narrow ones.

## ADDED Requirements

### Requirement: Three-part invitation composition (P0)
The invitation SHALL be composed of a header carrying only the personal greeting, a body in which the card design is the dominant element, and a footer carrying a single primary call to action. The header SHALL NOT be visually more prominent than the body. The RSVP form SHALL NOT be rendered in the flow beneath the artwork.

#### Scenario: First screen
- **WHEN** a guest opens their invitation on any supported viewport
- **THEN** the greeting, the card, and the call to action are what they see, with no form controls other than the call to action

#### Scenario: Header restraint
- **WHEN** the greeting and the card are rendered together
- **THEN** the greeting occupies a minor share of the vertical space and the card the majority

#### Scenario: Supporting detail remains reachable
- **WHEN** the guest scrolls past the call to action
- **THEN** the countdown, date, venue, directions, calendar action, notes and host contact are all present and readable as text

### Requirement: Personalised greeting (P0)
On a tokenized route the header SHALL address the guest by the name held on their guest record, followed by a short invitation sentence selected by that guest's invitation type. The header is the **only** place a guest is addressed by name: the card below it is fixed for the event and carries no guest data, so a name SHALL NOT appear inside it.

#### Scenario: Named guest
- **WHEN** a guest whose record holds "Sarah Ahmed" opens their link
- **THEN** the header reads "Dear Sarah Ahmed," followed by the invitation sentence for their type

#### Scenario: Open link with no known guest
- **WHEN** a visitor opens the tokenless `/e/{slug}` route
- **THEN** the header renders the invitation sentence without a name line, and no guest data appears anywhere on the page

#### Scenario: Long name
- **WHEN** a guest's stored name is long enough to exceed one line at the narrowest supported width
- **THEN** the name wraps onto further lines without truncation, clipping, or horizontal scrolling, and the layout below it shifts down rather than overlapping

### Requirement: Guest invitation type (P0)
Each guest SHALL carry an invitation type of `single` or `family`, settable when adding a guest and changeable when editing one. Guest records created before this capability SHALL behave as `single`.

#### Scenario: Setting on creation
- **WHEN** an admin adds a guest and selects Family
- **THEN** that guest's invitation renders the family sentence

#### Scenario: Changing on an existing guest
- **WHEN** an admin changes a guest from Single to Family
- **THEN** the next load of that guest's invitation renders the family sentence, with no change to their token, RSVP, or headcount

#### Scenario: Pre-existing guest
- **WHEN** a guest record stored before this capability existed is loaded
- **THEN** it is treated as `single` and its invitation renders the single sentence

### Requirement: Customer-editable invitation messages (P0)
The wedding's settings SHALL expose exactly two editable sentences per supported locale — one for single guests, one for family guests. Each SHALL be limited to 100 characters, SHALL be stored and rendered as plain text, and SHALL have any markup, rich text, or runs of consecutive blank lines rejected or normalised away. The admin interface SHALL show a live character count against the limit.

#### Scenario: Customising a message
- **WHEN** an admin replaces the family message with "We would love for you and your family to celebrate with us"
- **THEN** family guests see that sentence and single guests are unaffected

#### Scenario: Over the limit
- **WHEN** an admin enters a message longer than 100 characters
- **THEN** the interface shows the count exceeding the limit and the API rejects the save

#### Scenario: Markup submitted
- **WHEN** a message containing HTML tags is submitted
- **THEN** it is rejected or stored with the markup stripped, and it is never rendered as markup to a guest

#### Scenario: Character counter
- **WHEN** an admin types a 42-character message
- **THEN** the interface displays the count against the maximum

### Requirement: Message defaults and fallbacks (P0)
Where a customer has not supplied a message for a given type and locale, the system SHALL render a built-in default. A guest SHALL never be shown an empty, null, or placeholder greeting.

#### Scenario: No customisation
- **WHEN** a wedding has never had its messages edited
- **THEN** single guests see "You are cordially invited" and family guests see "You and your family are cordially invited", each in the resolved locale

#### Scenario: Partial customisation
- **WHEN** only the English family message is customised
- **THEN** English family guests see the custom sentence and every other combination falls back to its default

### Requirement: Deferred RSVP surface (P0)
The RSVP form SHALL open only when the guest activates the call to action, SHALL appear as a drawer entering from the trailing edge on wide viewports and as a sheet rising from the bottom on narrow ones, and SHALL NOT navigate the guest away from the invitation.

#### Scenario: Wide viewport
- **WHEN** a guest on a 1280px viewport activates Accept
- **THEN** the form animates in from the right, the invitation remains visible, and the URL does not change

#### Scenario: Narrow viewport
- **WHEN** a guest on a 375px viewport activates Accept
- **THEN** the form rises from the bottom as a sheet, scrolls internally when its content exceeds the space, and the URL does not change

#### Scenario: Dismissal
- **WHEN** the RSVP surface is open
- **THEN** a visible close control and the Escape key each dismiss it, and dismissal never discards data the guest has already entered without warning them

#### Scenario: Decline path
- **WHEN** a guest declines instead of accepting
- **THEN** the existing decline behaviour, confirmation and re-accept option are unchanged

### Requirement: RSVP surface accessibility (P0)
While open, the RSVP surface SHALL trap focus, SHALL move focus to itself on open and return it to the call to action on close, SHALL be announced as a dialog, and SHALL prevent the page behind it from scrolling.

#### Scenario: Keyboard operation
- **WHEN** a guest opens the surface with a keyboard and presses Tab repeatedly
- **THEN** focus cycles within the surface and never reaches the invitation behind it

#### Scenario: Background scroll
- **WHEN** the surface is open and the guest scrolls
- **THEN** the content behind does not move

### Requirement: On-screen keyboard and safe areas (P0)
On touch devices the RSVP surface SHALL remain usable while the on-screen keyboard is open: the focused field SHALL stay visible, the submit control SHALL remain reachable, and the surface SHALL respect device safe-area insets and dynamic browser chrome.

#### Scenario: Keyboard opens
- **WHEN** a guest focuses a field near the bottom of the sheet and the keyboard appears
- **THEN** the field remains visible and the submit control is reachable by scrolling within the sheet

#### Scenario: Device with a home indicator
- **WHEN** the sheet is displayed on a device with bottom safe-area inset
- **THEN** no control is obscured by system chrome

### Requirement: Responsive integrity (P0)
The invitation SHALL hold its composition across viewport widths from 320px to 1440px and above, in portrait and landscape, and under browser zoom. At no supported size SHALL there be horizontal scrolling, clipped names, distorted or cropped artwork, overlapping sections, or controls outside the viewport.

#### Scenario: Narrowest supported width
- **WHEN** the invitation is viewed at 320px
- **THEN** no horizontal scrolling occurs and the greeting, card and call to action are all fully visible and operable

#### Scenario: Artwork proportions
- **WHEN** the invitation is viewed at any supported width
- **THEN** the card renders at its authored aspect ratio, neither stretched nor cropping its design

#### Scenario: Mobile landscape
- **WHEN** the invitation is viewed in landscape on a phone
- **THEN** the composition remains usable and the call to action is reachable without obstruction

### Requirement: Existing behaviour preserved (P0)
This composition SHALL NOT alter invitation link resolution, guest identification, RSVP submission or its stored data, cancellation, headcount, locale resolution, or admin authentication and permissions.

#### Scenario: Existing link
- **WHEN** a link issued before this change is opened
- **THEN** it resolves to the same guest and event, and submitting an RSVP produces the same stored result as before

#### Scenario: Already-responded guest
- **WHEN** a guest who has already accepted reopens their link
- **THEN** the greeting and card render, and their existing answer with update and cancel options is available from the call to action position instead of a blank form
