---
id: scenario-timeline-page
status: not-started
depends_on: [scenario-dashboard-shell]
area: frontend
description: Extract the Cash Flow events tier (income, expenses, relocation events) from ScenarioForm + LifeEventsTimelineStrip into a standalone page at /scenario/:id/timeline with grouped signed event cards. Pre-polish — the passport ribbon ships in scenario-redesign-polish.
---

## Goal

Today, scenario events are edited inside `ScenarioForm.tsx` (`tier-cashflow`, lines 4592–6107) which embeds `LifeEventsTimelineStrip.tsx` (1849 lines) for both the visual strip and the editable rows. The redesign promotes this into its own page renamed **Timeline**, with:

- a **summary pill** ("N entries · YYYY–YYYY") in the page header;
- a filter segmented control (`All / Income / Expenses / Countries`);
- a sort control + an `+ Add… ▾` dropdown listing event kinds;
- (pre-passport-ribbon: the existing residency band stays for this spec);
- **section-grouped signed event cards**: `Income & benefits` and `Expenses`. Each card has glyph, name, kind pill (`Benefit` / `Income` / `Expense` / `Education`), years, sub-line, signed amount, and a per-card "life strip" bar.
- Sticky `+ Add` bar on mobile.

Source of truth: `specs/mocks/scenario-redesign/timeline.html` + `timeline.jsx` — `EventsDest`, `EventCard`, `EventsDestMobile`.

The **passport ribbon** for `relocation_events` ships in `scenario-redesign-polish`. This spec keeps the current residency display (`LifeEventsTimelineStrip`'s residency band) at the top of the timeline page.

## Acceptance Criteria

- [ ] New route `/scenario/:id/timeline` registered in `routes.tsx`. Add `'timeline'` to `SCENARIO_SECTIONS` (introduced by `scenario-dashboard-shell`), **not** `SCENARIO_TABS`.
- [ ] New component `frontend/src/components/scenario/TimelinePage.tsx`. Renders header (title "Timeline", subtitle from the mock, summary pill), toolbar (filter seg, sort select, `+ Add… ▾` menu), residency band (existing read-only band from `LifeEventsTimelineStrip` — wrapped, not replaced), and **3 grouped sections in this order**:
      | Section | Kinds rendered | Notes |
      |---|---|---|
      | `Income & benefits` | `income`, `benefit` | Signed `+` amount. `benefit` cards show SS-style "claim age" sub-line. |
      | `Expenses` | `expense`, `lifestyle`, plus the mock's `school`/`education` styling for college-type expense entries | Signed `−` amount. `lifestyle` cards render under this group with no extra sub-grouping. |
      | `Strategies` | `roth-conversion` | Unsigned (it's a tax-strategy event, not a cash flow). Section appears only when at least one entry exists, to keep the page quiet by default. |

      Country moves (`relocation`) are **not** rendered in the grouped sections — they live in the residency band at the top (and the passport ribbon once `scenario-redesign-polish` lands). Clicking a residency segment opens its inline editor.
- [ ] **`EventCard`** matches `EventCard` in `timeline.jsx`: top row glyph + name + kind pill + years span + sub-line + signed amount; below it a per-card life-strip showing when the event is active across the plan timespan. Expanded state shows an inline editable form (current `LifeEventsTimelineStrip` row editor — wrap, don't rewrite).
- [ ] Section header for each group shows count + a context-appropriate total + a `+ add` quick-add: `Income & benefits` → signed `+$NK/yr at peak`; `Expenses` → signed `−$NK/yr at peak` (lifestyle entries included in the total); `Strategies` → unsigned total of Roth conversion amounts across all years (e.g. `$240K over 8 yrs`), no `+`/`−` prefix.
- [ ] `+ Add… ▾` dropdown reuses the current `LifeEventsTimelineStrip` `onAddEvent(kind: StripAddKind)` add flow. The current `StripAddKind` union (`LifeEventsTimelineStrip.tsx:34`) is `'income' | 'expense' | 'lifestyle' | 'benefit' | 'relocation' | 'roth-conversion' | 'retirement-A' | 'retirement-B'`. The menu must surface every **life-event** creation kind (i.e. all `StripAddKind` values except the retirement markers — `retirement-A` and `retirement-B` are retirement-timing controls that live on Essentials, not Timeline) so no existing creation path is silently dropped:
      | Menu item | Group | StripAddKind |
      |---|---|---|
      | Income | top | `'income'` |
      | Benefit (SS / pension claim) | top | `'benefit'` |
      | Expense | top | `'expense'` |
      | Lifestyle change | top | `'lifestyle'` |
      | — separator — | | |
      | Country move | bottom | `'relocation'` |
      | One-time inflow | bottom | `'income'` with `begin === end` pre-filled |
      | Roth conversion | bottom | `'roth-conversion'` |

      `'retirement-A'` and `'retirement-B'` are **not** in this menu — retirement timing already lives on the Essentials page (`/scenario/:id/essentials`, retirement-year fields). The legacy add path for those markers is dropped intentionally because they're not life events; if the existing add UI surfaces them today, remove those menu entries from this page only (the underlying `StripAddKind` union members stay until `scenario-remove-slide-over` so the legacy slide-over remains unmodified). One-time inflow uses `'income'` with a single-year span (`additional_income` entry with 1-year duration) — no new union value, no new payload type, no engine changes.
- [ ] Filter segmented control filters cards in place (no re-fetch). The mock shows 4 buttons; this spec extends to **5** to cover all groups:
      | Button | Shows |
      |---|---|
      | `All` | Everything (residency band + all 3 grouped sections) |
      | `Income` | `Income & benefits` group only (kinds `income`, `benefit`) |
      | `Expenses` | `Expenses` group only (kinds `expense`, `lifestyle`, plus college-styled entries) |
      | `Strategies` | `Strategies` group only (kind `roth-conversion`) — hidden from the filter when the group is empty |
      | `Countries` | Residency band only; all 3 grouped sections hidden |
- [ ] Dashboard `TimelineTile` (from `scenario-dashboard-shell`) navigates to `/scenario/:id/timeline` instead of opening the slide-over.
- [ ] Mobile (`v4-mob-body` + `mobile-add-bar` in the mock): same content stacked vertically; sticky `+ Add event` bar at the bottom.
- [ ] Playwright test at `frontend/e2e/ui-contract/scenario-timeline-page.spec.ts`: create an event of each kind, edit one, delete one — verify dashboard tile reflects updated count + range.

## Out of Scope

- **Passport ribbon** for relocation events — `scenario-redesign-polish`. Until then, keep showing the existing `LifeEventsTimelineStrip` residency band as a thin strip at the top of the page.
- Adding new event kinds beyond what already exists.
- Removing `tier-cashflow` from `ScenarioForm.tsx` — `scenario-remove-slide-over`.
- Read-only timeline strip on `RunResult` — handoff §"What stays put" says it's optional going forward; do not remove it here.

## Danger Zones

- `LifeEventsTimelineStrip.tsx` is 1849 lines with the visual strip + the editable rows interleaved. Don't rewrite it. Treat it as a controlled black box: extract just the per-row editor and per-card span data via existing exported types (`StripMarker`, `StripSpan`, etc.) and compose the new page around them.
- Event kind taxonomy: the mock uses `kind: 'income' | 'benefit' | 'expense' | 'school'`. This spec renders **all** existing `StripAddKind` life-event kinds (`income`, `benefit`, `expense`, `lifestyle`, `relocation`, `roth-conversion`) with the section/grouping rules above; `lifestyle` borrows the `expense` visual treatment, `roth-conversion` borrows a neutral pill style, `relocation` stays in the residency band. The current data model may use different names internally (`pension`, `social_security`, `salary`, etc.); map them to the visual categories at render time. **Do not migrate the data model.**
- The "per-card life-strip" is a new visual that needs the same span-percentage math as `LifeEventsTimelineStrip`. Use the existing `pct(year)` / `widthPct(begin, end)` style helpers from the strip, not new ones.
- COL-index fetches (used by relocation events) must continue to work — wrap the same hooks; do not introduce a new fetch path.

## Agent Notes

- Mocks: `timeline.html` (open in browser) + `timeline.jsx`. The mock shows two grouped sections (`Income & benefits` and `Expenses`); this spec adds a third (`Strategies` for Roth conversions) per the Acceptance Criteria table. School/education events live under Expenses with the ochre `--color-event-education` color.
- Kind → token map: `income` → `--color-event-income`; `benefit` → `--color-event-benefit`; `expense` → `--color-event-expense`; `school`/`education` → `--color-event-education`; `relocation`/`life` → `--color-event-relocation`. The tokens are added in `scenario-dashboard-shell`.
- Summary pill copy: `{n} entries · {start}–{end}` where start/end come from the actual event year range.
- The "+ Add… ▾" dropdown matches the mock's `.add-menu`; reuse existing dropdown utilities if any.
