## Purpose

Self-service cancellation: every reminder carries a cancel link that lets a guest withdraw with one confirmed tap, updating headcount instantly and stopping future reminders — while being immune to link-preview crawlers.

## ADDED Requirements

### Requirement: Cancel link in every message (FR-3.1, P0)
Every reminder and confirmation message SHALL contain a cancel link of the form `/i/{token}/cancel`.

#### Scenario: Reminder content
- **WHEN** any reminder or confirmation message is rendered
- **THEN** its body contains the guest's cancel URL

### Requirement: Confirmed, POST-only cancellation (FR-3.2, FR-3.3, P0)
The cancel page SHALL show what is being cancelled (event, date, party size) and require one explicit confirmation tap. The cancellation itself MUST be performed via POST with CSRF protection — never a bare GET — so WhatsApp/email link-preview bots cannot trigger it (§7.6).

#### Scenario: Guest confirms cancellation
- **WHEN** the guest opens the cancel link and taps the confirm button
- **THEN** a POST request performs the cancellation

#### Scenario: Link-preview crawler fetches cancel URL
- **WHEN** a bot performs a GET on `/i/{token}/cancel`
- **THEN** no state changes — the RSVP remains accepted

### Requirement: Cancellation effects (FR-3.5, P0)
On cancel: status SHALL become `cancelled`, the event headcount decrements immediately by the RSVP's party size, and all future reminder jobs for that invitation are marked `skipped`.

#### Scenario: Cancel updates counts and jobs
- **WHEN** a guest with party size 2 cancels
- **THEN** the accepted headcount drops by 2 immediately and their queued future reminder jobs are marked skipped

### Requirement: Re-accept and admin notification (FR-3.6, P1)
After cancelling, the guest SHALL see a "changed your mind?" re-accept option, and the admin dashboard SHALL surface a cancellation notification.

#### Scenario: Re-accept after cancel
- **WHEN** a cancelled guest taps re-accept
- **THEN** their invitation returns to `accepted` and headcount is restored

### Requirement: Cancellation blocked after event start (FR-3.7, P1)
Cancellation SHALL be blocked after the event's start time; the page shows a host contact number instead.

#### Scenario: Late cancel attempt
- **WHEN** a guest opens the cancel link after the event has started
- **THEN** no cancel action is offered and the contact number is displayed

### Requirement: Optional cancellation reason (FR-3.4, P2)
The cancel confirmation SHALL offer an optional one-line reason field; when provided, the reason is stored with the cancellation.

#### Scenario: Reason provided
- **WHEN** the guest enters a reason and confirms
- **THEN** the reason is stored with the cancellation history entry
