diff --git a/docs/design-events-unification-2026-05-04.md b/docs/design-events-unification-2026-05-04.md
new file mode 100644
index 0000000..b90eadd
--- /dev/null
+++ b/docs/design-events-unification-2026-05-04.md
@@ -0,0 +1,629 @@
+# Design: Unify Fristen + Termine as filtered views of one Events page
+
+**Task:** t-paliad-109
+**Author:** cronus (inventor)
+**Date:** 2026-05-04
+**Status:** DRAFT — awaiting m's go/no-go on §F open questions
+**Branch:** `mai/cronus/design-unify-fristen`
+
+---
+
+## 0. Premise check (read this first)
+
+The task brief describes the current state as:
+
+> - `/deadlines` — backend `DeadlineService`, table `paliad.deadlines`
+> - `/agenda` — backend `AppointmentService`, table `paliad.appointments`
+
+The first half is correct. **The second half is wrong against the live codebase**, and the design has to start from the real shape:
+
+| Route | What it actually is today | Backend |
+|---|---|---|
+| `/deadlines` | Fristen list (table + 5 summary cards). Deadline-only. | `DeadlineService` |
+| `/appointments` | **Termine list (table + 3 summary cards). Appointment-only.** | `AppointmentService` |
+| `/agenda` | **Cross-type timeline (already unified)** — day-grouped feed with chip filters Beides / Nur Fristen / Nur Termine, range chips 7/14/30/90, event-type multi-select. | `AgendaService` (not `AppointmentService`) |
+
+So three list-ish surfaces exist, not two. The two table surfaces (`/deadlines` and `/appointments`) are the ones that diverge cosmetically and structurally; `/agenda` is a genuinely different visual paradigm (timeline grouped by day, no table) and a genuinely different backend (`AgendaService` already unions both event types).
+
+Sidebar today (`frontend/src/components/Sidebar.tsx:111–122`):
+
+```
+Übersicht: Dashboard, Agenda, Team
+Arbeit: Projekte, Fristen (/deadlines), Termine (/appointments)
+```
+
+So **Agenda is already a sibling overview-style entry** distinct from the work-day list pair Fristen/Termine. The design below treats the unification target as the **Fristen ↔ Termine list pair**, not the timeline. Whether `/agenda` collapses into the new shape is its own question (Q3 in §F).
+
+This premise correction was caught before locking the design — it determines the shape of A/B/C/D below. m should sanity-check it (Q1 in §F).
+
+---
+
+## 1. m's intent (as I read it)
+
+> "Fristen and Termine should be **two predefined filters of the same Events view**, sharing the same layout. The Dashboard should reflect the same model."
+
+Three things in that sentence:
+
+1. **Predefined filters** — the user-facing names "Fristen" and "Termine" stay; under the hood each is `?type=deadline` / `?type=appointment` of one Events page.
+2. **Same layout** — the table chrome, summary cards, filter row, "+ Neu" button all come from one component, not two.
+3. **Dashboard reflects the same model** — the deadline summary expands to a unified Events summary (or gains a parallel Termine block keyed off the same backend shape).
+
+The smallest-diff path that delivers that intent is **A1 + B1** below: keep the two URLs, render the same component, share one backend service that returns a discriminated `Event` row.
+
+---
+
+## 2. Recommended design (TL;DR)
+
+| Area | Recommendation | Smallest-diff alternative considered & rejected |
+|---|---|---|
+| **Routes** | Keep `/deadlines` and `/appointments` URLs; both render the same `EventsPage` component with `?type=deadline` or `?type=appointment` baked in by the handler. | A2 (introduce `/events`, redirect from old URLs) — louder external change, more deploy-time friction. |
+| **Sidebar** | No change. "Fristen" still links to `/deadlines`, "Termine" still links to `/appointments`. | Collapsing to a single "Ereignisse" entry — renames a thing users know; m's phrasing was "two predefined filters", not "one entry". |
+| **Page header** | Renders "Fristen" or "Termine" (driven by default type). Below: a 3-chip toggle (Fristen / Termine / Beides) lets users widen. | A header named "Events" — fights m's intent that the names stay. |
+| **Backend** | New `EventService.ListVisibleForUser` that union-loads from both tables and returns `[]EventListItem`. Sits next to `AgendaService` (which keeps the timeline shape). | Reusing `AgendaService` directly — its struct is timeline-shaped (no `EventTypeIDs`, no rule fields, hides completed deadlines). Extension is bigger than greenfield. |
+| **Endpoint** | New `GET /api/events?type=&status=&project_id=&event_type=&from=&to=`. Existing `/api/deadlines` and `/api/appointments` keep working until ~v2 cleanup. | Folding both old endpoints into `/api/events` — needless break for clients we still ship (calendars, dashboards, project-detail panes). |
+| **Detail pages** | **Stay separate** (`/deadlines/{id}`, `/appointments/{id}`). The unification is list-only. | Unify detail pages too — out of scope per task brief §13; deadline-edit and appointment-edit have nearly disjoint forms. |
+| **Dashboard** | Add a parallel **Termine summary** rail (Heute / Diese Woche / Später, mirroring `/appointments` summary today). Keep the deadline 5-bucket rail. Both rails read from `/api/events/summary` (new). | Cram appointments into the 5-bucket model — "Überfällig" doesn't really apply to past meetings; degrades meaning. |
+| **`/agenda`** | **Out of scope for this round.** Keep the timeline as-is; revisit in a follow-up once Events list is stable. | Retire `/agenda` now — too much UX surface area for one PR; m hasn't asked for it. |
+
+The rest of this doc is the detail behind those rows.
+
+---
+
+## 3. Section A — Information architecture
+
+### Q1. Canonical route
+
+**Recommendation: A1 — keep both URLs, share one component.**
+
+```
+GET /deadlines → renderEventsPage({ defaultType: "deadline" })
+GET /appointments → renderEventsPage({ defaultType: "appointment"})
+```
+
+Both handlers serve the same TSX page, both bundle the same `client/events.ts`. The only difference is a one-line attribute `
` (or `"appointment"`) read by `events.ts` on init.
+
+A `?type=` query param can override the default — that lets the 3-chip toggle ("Fristen / Termine / Beides") work without re-routing. The URL on `/deadlines?type=appointment` is mildly weird but harmless; the alternative is full `pushState` to switch routes when toggling, which fights browser history.
+
+Three options considered:
+
+| Option | Smallest diff? | Notes |
+|---|---|---|
+| **A1** Two routes, one component, default-type per handler | ✅ smallest | No redirect machinery, no broken bookmarks, no sidebar churn. |
+| A2 `/events` canonical + redirects | ✗ medium | 302 from `/deadlines` and `/appointments`. Every internal link, every bookmarked URL, every email-template link redirects once. Workable, but louder. |
+| A3 `/events` only + sidebar collapse to "Ereignisse" | ✗ largest | Renames a thing users know. Conflicts with m's "two predefined filters" framing. |
+
+### Q2. Branding
+
+**Recommendation: keep "Fristen" and "Termine" as user-facing names.**
+
+The page `` reads "Fristen" or "Termine" depending on the default type. The 3-chip toggle below the header is labeled `[Fristen] [Termine] [Beides]`. When the user is in "Beides" mode, the `` stays whichever they came from (don't rewrite it on toggle — would jitter). The page `` follows the same rule.
+
+Why not introduce "Events / Ereignisse" as a top-level label: m said "two predefined filters", not "one new concept". Calling the page "Events" while sidebar entries say "Fristen" / "Termine" creates a two-vocabulary problem; calling everything "Ereignisse" demands users learn a new label.
+
+The internal vocabulary in code (`EventService`, `EventListItem`, `/api/events`) stays English-Events per the system-language convention. User-facing strings stay German Fristen/Termine.
+
+### Q3. Sidebar nav
+
+**Recommendation: no change.**
+
+Sidebar today:
+```
+Arbeit:
+ Projekte /projects
+ Fristen /deadlines
+ Termine /appointments
+```
+
+Both entries continue to point at their existing URLs. The 3-chip toggle on the page is the gateway to "Beides".
+
+Collapsing to a single "Ereignisse" entry means losing the muscle-memory shortcut to the deadline-only or appointment-only view. The 3-chip toggle is one click further than a sidebar entry; for a high-frequency view that's a regression.
+
+If we ever want a single entry, the path is: ship the unification, watch usage, then collapse if telemetry says nobody uses one of the two pre-filtered URLs.
+
+---
+
+## 4. Section B — Data model
+
+### Q4. Backend service shape
+
+**Recommendation: B1 — new `EventService` that delegates internally.**
+
+```go
+// internal/services/event_service.go
+type EventService struct {
+ db *sqlx.DB
+ deadlines *DeadlineService
+ appointments *AppointmentService
+ eventTypes *EventTypeService
+}
+
+func NewEventService(db, d, a, et) *EventService
+
+type EventListFilter struct {
+ Type EventTypeFilter // "" | "deadline" | "appointment"
+ Status DeadlineStatusFilter // applies only to deadlines
+ ProjectID *uuid.UUID
+ EventTypeIDs []uuid.UUID // applies only to deadlines
+ IncludeUntyped bool // applies only to deadlines
+ AppointmentType *string // applies only to appointments
+ From *time.Time
+ To *time.Time
+}
+
+func (s *EventService) ListVisibleForUser(ctx, userID, filter) ([]EventListItem, error)
+func (s *EventService) SummaryCounts(ctx, userID, filter) (EventSummary, error)
+```
+
+Internally, `ListVisibleForUser`:
+
+1. If `filter.Type == "appointment"` → call only `AppointmentService.ListVisibleForUser`, project to `EventListItem`.
+2. If `filter.Type == "deadline"` → call only `DeadlineService.ListVisibleForUser`, project to `EventListItem`.
+3. If `filter.Type == ""` (Beides) → call both, merge, sort by canonical date.
+
+Status filter only takes effect when the result includes deadlines; when filter.Type=="appointment" with a Status set, the handler should return 400 (or quietly drop it — Q11 in §F).
+
+Three options considered:
+
+| Option | Notes |
+|---|---|
+| **B1** New `EventService` delegating to existing services | Single ownership, clean API surface. ~150 LoC. The two existing services keep their callers (project detail pages, dashboard subqueries). |
+| B2 Union at the handler layer | Filter logic split. Hard to test the merge. Same query gets duplicated for `/api/events/summary`. |
+| B3 Extend one of the existing services | Awkward — neither `DeadlineService.ListAllEvents` nor `AppointmentService.ListAllEvents` reads naturally. Adds an unrelated dep (each service would need to know about the other). |
+
+Why not reuse `AgendaService`: it's the right shape for timelines (`AgendaItem` with urgency annotation, completed-deadlines hidden, no rule/event-type fields on the row). Extending it to also feed the table view would require adding `EventTypeIDs`, `RuleCode`, `RuleName`, `Description`, `Notes`, an `IncludeCompleted` flag, and Status filtering — at which point it stops being agenda-shaped. Cleaner to leave `AgendaService` for the timeline and introduce a sibling.
+
+### Q5. The unified row type
+
+**Recommendation: discriminated tagged union with type-specific optional fields.**
+
+```ts
+// frontend type — same shape as Go's EventListItem JSON
+type EventListItem =
+ | DeadlineEvent
+ | AppointmentEvent;
+
+interface EventBase {
+ id: string;
+ type: "deadline" | "appointment";
+ title: string;
+ description?: string;
+ date: string; // ISO 8601 — canonical sort key (deadline: due_date 00:00 UTC; appointment: start_at)
+ date_label: string; // pre-formatted for table cell, e.g. "31.05.2026" or "31.05. 14:00–15:00"
+ urgency: "overdue" | "today" | "tomorrow" | "this_week" | "next_week" | "later" | "completed";
+ project_id?: string;
+ project_title?: string;
+ project_reference?: string;
+ project_type?: string;
+}
+
+interface DeadlineEvent extends EventBase {
+ type: "deadline";
+ due_date: string; // YYYY-MM-DD
+ status: "pending" | "completed";
+ completed_at?: string;
+ source: "manual" | "fristenrechner" | "import";
+ rule_id?: string;
+ rule_code?: string;
+ rule_name?: string;
+ rule_name_en?: string;
+ event_type_ids: string[];
+ has_ccr?: boolean; // condition_flag = 'with_ccr' (UPC_INF)
+}
+
+interface AppointmentEvent extends EventBase {
+ type: "appointment";
+ start_at: string;
+ end_at?: string;
+ location?: string;
+ appointment_type?: "hearing" | "meeting" | "consultation" | "deadline_hearing";
+}
+```
+
+Go-side mirror:
+
+```go
+type EventListItem struct {
+ ID uuid.UUID `json:"id"`
+ Type string `json:"type"` // "deadline" | "appointment"
+ Title string `json:"title"`
+ Description *string `json:"description,omitempty"`
+ Date time.Time `json:"date"`
+ DateLabel string `json:"date_label"`
+ Urgency string `json:"urgency"`
+ ProjectID *uuid.UUID `json:"project_id,omitempty"`
+ ProjectTitle *string `json:"project_title,omitempty"`
+ ProjectReference *string `json:"project_reference,omitempty"`
+ ProjectType *string `json:"project_type,omitempty"`
+
+ // Deadline-only (zero-valued / nil for appointments)
+ DueDate *string `json:"due_date,omitempty"`
+ Status *string `json:"status,omitempty"`
+ CompletedAt *time.Time `json:"completed_at,omitempty"`
+ Source *string `json:"source,omitempty"`
+ RuleID *uuid.UUID `json:"rule_id,omitempty"`
+ RuleCode *string `json:"rule_code,omitempty"`
+ RuleName *string `json:"rule_name,omitempty"`
+ RuleNameEN *string `json:"rule_name_en,omitempty"`
+ EventTypeIDs []uuid.UUID `json:"event_type_ids,omitempty"`
+ HasCCR *bool `json:"has_ccr,omitempty"`
+
+ // Appointment-only
+ StartAt *time.Time `json:"start_at,omitempty"`
+ EndAt *time.Time `json:"end_at,omitempty"`
+ Location *string `json:"location,omitempty"`
+ AppointmentType *string `json:"appointment_type,omitempty"`
+}
+```
+
+Why a flat struct with optionals instead of `Deadline *DeadlineFields; Appointment *AppointmentFields`: the agenda already proved (in `AgendaItem`) that flat-with-optionals reads cleaner across both Go service code and frontend rendering. The frontend type-narrows on `type === "deadline"` and TS infers the rest.
+
+JSON example — one of each:
+
+```json
+[
+ {
+ "id": "8c3a…",
+ "type": "deadline",
+ "title": "Statement of Defence",
+ "date": "2026-08-31T00:00:00Z",
+ "date_label": "31.08.2026",
+ "urgency": "next_week",
+ "project_id": "1f…",
+ "project_title": "Acme v. Foo",
+ "project_reference": "0001234.0000567",
+ "project_type": "case",
+ "due_date": "2026-08-31",
+ "status": "pending",
+ "source": "fristenrechner",
+ "rule_id": "…",
+ "rule_code": "RoP.023",
+ "rule_name": "Statement of Defence",
+ "event_type_ids": ["af…", "bd…"],
+ "has_ccr": false
+ },
+ {
+ "id": "9d4b…",
+ "type": "appointment",
+ "title": "Mündliche Verhandlung — Acme v. Foo",
+ "date": "2026-09-15T09:00:00Z",
+ "date_label": "15.09.2026 09:00–11:00",
+ "urgency": "later",
+ "project_id": "1f…",
+ "project_title": "Acme v. Foo",
+ "project_reference": "0001234.0000567",
+ "project_type": "case",
+ "start_at": "2026-09-15T09:00:00Z",
+ "end_at": "2026-09-15T11:00:00Z",
+ "location": "UPC LD München, Cincinnatistraße 64",
+ "appointment_type": "hearing"
+ }
+]
+```
+
+### Q6. Date semantics
+
+**Recommendation: one `date` column, one `date_label` column.**
+
+- `date` is always the canonical sort key, RFC3339 UTC.
+ - Deadlines: `2026-08-31T00:00:00Z` (midnight UTC of `due_date`).
+ - Appointments: `start_at` verbatim.
+- `date_label` is the pre-localized human string for the table cell.
+ - Deadlines: `"31.08.2026"` (no time component — deadlines are date-only).
+ - Appointments without `end_at`: `"15.09.2026 09:00"`.
+ - Appointments with `end_at` same-day: `"15.09.2026 09:00–11:00"`.
+ - Appointments with `end_at` next-day: `"15.09.2026 09:00 → 16.09.2026 11:00"`.
+
+The label is computed server-side so the table rows render identically across DE/EN (i18n only swaps date format) without each frontend pass having to special-case start/end vs single-date.
+
+The column header reads "Fällig / Beginn" (Fristen / Termine) in single-type mode and "Datum" in Beides mode (Q11 in §F asks m to confirm).
+
+---
+
+## 5. Section C — UI
+
+### Q7. The 5-bucket summary
+
+**Recommendation: bucket model is type-aware. Deadlines keep 5 buckets, Appointments use 3 buckets, "Beides" shows two rails.**
+
+The deadline 5-bucket model (Überfällig / Heute / Diese Woche / Nächste Woche / Erledigt — t-paliad-106) is genuinely deadline-shaped: "Überfällig" means **a deadline that passed without being completed**. Past appointments are not "overdue" — they've happened, and that's fine. So:
+
+| Mode | Bucket rail |
+|---|---|
+| `?type=deadline` (Fristen) | 5 cards — Überfällig / Heute / Diese Woche / Nächste Woche / Erledigt (today's behavior, unchanged). |
+| `?type=appointment` (Termine) | 3 cards — Heute / Diese Woche / Später (today's behavior on `/appointments`, unchanged). |
+| `?type=` (Beides) | **Two rails stacked**: Fristen rail (5 cards, deadlines only) on top, Termine rail (3 cards, appointments only) below. Each card filters to that bucket within its own type. |
+
+This stays honest about what each card means and avoids a stretched 4-column compromise that lies about appointments being "Überfällig". The Beides view does pay a vertical-space cost (~120px for the second rail); the alternative compromise feels worse.
+
+If m wants a *common* 4-bucket compromise (Heute / Diese Woche / Nächste Woche / Erledigt+Vergangen), Q12 asks. My recommendation is the two-rail approach.
+
+### Q8. Filter row
+
+**Recommendation: one filter row that shows the *union* of relevant filters; rules toggle visibility/active-state per type.**
+
+```
+[Type chips: Fristen | Termine | Beides ] ← driver
+Filters when type=deadline:
+ Status (single-select) | Projekt (single-select) | Typ (event-type multi-select)
+Filters when type=appointment:
+ Termin-Typ (single-select) | Projekt (single-select) | Von | Bis
+Filters when type=Beides:
+ Projekt (single-select) | Von | Bis | Typ (event-type multi-select, applies to deadlines only with a tooltip)
+ + a status selector that's disabled with hint "Nur Fristen"
+```
+
+Concretely:
+
+- **Projekt** (single-select) — always visible, always active. Same behavior as today.
+- **Status** (deadline-only) — visible in `?type=deadline` and `?type=`; in `?type=appointment` it's hidden. In Beides, it filters deadlines AND silently passes appointments through (with a tooltip explaining).
+- **Typ (event-type multi-select)** — visible in `?type=deadline` and `?type=`; hidden in `?type=appointment`. Today's `event_type_id` model is deadline-only.
+- **Termin-Typ** (hearing/meeting/consultation/deadline_hearing) — visible in `?type=appointment`; hidden in deadline-only mode and Beides (low value, would mean "only appointments of type X plus all deadlines" which is incoherent).
+- **Von / Bis** — already on `/appointments`. Add to the unified view across all type modes (gives users a way to scope deadlines too — currently deadlines don't have a date range filter, only buckets).
+
+Hidden, not greyed-out, when a filter doesn't apply. Greyed-out adds noise and invites confusion. Filters re-appear instantly on chip toggle (no page reload).
+
+### Q9. Columns that differ
+
+**Recommendation: type-conditional columns — visible whenever ≥1 row in the current view has data for that column.**
+
+Single-type mode is straightforward: render exactly today's columns.
+
+Beides mode: render the *union* of columns, but apply the existing **hide-on-uniform** pattern (`.entity-table--hide-event-type` from t-paliad-088, generalized):
+
+```
+| Type icon | Datum | Titel | Projekt | Regel¹ | Typ¹ | Ort² | Termin-Typ² | Status |
+ ¹ deadline-only — hidden in pure-appointment view
+ ² appointment-only — hidden in pure-deadline view
+```
+
+Cell content per column:
+
+| Column | Deadline row | Appointment row |
+|---|---|---|
+| Type icon | 🕐 (CLOCK) | 📅 (CALENDAR) |
+| Datum | "31.08.2026" | "15.09.2026 09:00–11:00" |
+| Titel | deadline title | appointment title |
+| Projekt | reference + title (or "—" for personal Termine) | same |
+| Regel | rule_code (e.g. "RoP.023") | empty (column shown only if any row has it) |
+| Typ | event-type chip cluster | empty |
+| Ort | empty | location text |
+| Termin-Typ | empty | "Verhandlung" / "Besprechung" / etc. |
+| Status | "Offen" / "Erledigt" / OVERDUE badge | empty (or maybe "vergangen" — Q14 in §F) |
+
+The CCR flag (UPC_INF condition_flag='with_ccr', t-paliad-086 PR-3) is a deadline detail that today shows as a small "CCR" pill on the deadline detail page. In the list view it stays as a row-level pill in the Titel cell — same as today on `/deadlines`.
+
+### Q10. The "+ Neu" button
+
+**Recommendation: type-aware default with a quick-switch dropdown.**
+
+In `?type=deadline`: button reads "Neue Frist" → `/deadlines/new`.
+In `?type=appointment`: button reads "Neuer Termin" → `/appointments/new`.
+In `?type=` (Beides): button reads "+ Neu" with a small dropdown caret → opens a 2-option menu (Neue Frist / Neuer Termin) that routes to the existing form pages.
+
+Why not a type-picker modal: it's an extra click for the common case (user knows what they're creating). Why not two side-by-side buttons in Beides mode: button-pair clutters the header and makes the "Beides" mode feel structurally different (it's just a filter view, not a different mode of being).
+
+Detail/create pages stay separate (per task brief §13 + §E13 below). The unification is list + filter, not form.
+
+---
+
+## 6. Section D — Dashboard
+
+### Q11. Termine on the Dashboard
+
+**Recommendation: Add a Termine summary rail; keep the deadline rail.**
+
+Today the Dashboard has:
+- 5-card deadline summary (Überfällig / Heute / Diese Woche / Nächste Woche / Erledigt) → links to `/deadlines?status=…`
+- "Kommende Fristen" + "Kommende Termine" two-column 7-day list (already cross-type)
+- Activity feed
+
+What to add:
+- **3-card Termine summary** (Heute / Diese Woche / Später) → links to `/appointments?range=today` etc.
+- Both card rails read from a new `GET /api/events/summary` that returns:
+
+```json
+{
+ "deadlines": { "overdue": 3, "today": 2, "this_week": 5, "next_week": 1, "completed_this_week": 2 },
+ "appointments": { "today": 1, "this_week": 4, "later": 12 }
+}
+```
+
+The two-column 7-day list stays — it's already cross-type and reads well. The activity feed stays.
+
+Visual ordering on Dashboard:
+```
+[Greeting]
+[Fristen summary — 5 cards]
+[Termine summary — 3 cards] ← new
+[Meine Akten matter card]
+[Kommende Fristen | Kommende Termine]
+[Letzte Aktivität]
+```
+
+The Termine rail goes directly under the Fristen rail because the two are conceptually the same "what's coming up?" question split by type.
+
+### Q12. Bucket-model translation
+
+**Recommendation: the buckets stay type-specific (no shared 4-bucket compromise).**
+
+Trying to fit appointments into the deadline 5-bucket model:
+
+| Deadline bucket | Appointment fit? |
+|---|---|
+| Überfällig (past, not completed) | ✗ — appointments either happened or didn't; "past" isn't urgent. |
+| Heute | ✓ |
+| Diese Woche | ✓ |
+| Nächste Woche | △ — `/appointments` today uses "Später" (anything past this week). The bucket is fine but the cutoff is different. |
+| Erledigt | △ — "vergangen" maybe, but the semantics differ. |
+
+The honest answer is the two surfaces have different time horizons (deadlines obsess over "overdue", appointments don't) and squeezing them into one bucket grid would erase that. The two-rail approach in §C7 is the cleanest expression.
+
+---
+
+## 7. Section E — Migration & rollout
+
+### Q13. Verlauf / detail-page links
+
+**Recommendation: detail pages stay type-specific. Verlauf links unchanged.**
+
+t-paliad-102 wired `eventDetailHref()` and `activityHref()` to point at `/deadlines/{id}` and `/appointments/{id}` based on event metadata. Those keep working — only the LIST view unifies. No frontend Verlauf change needed.
+
+If a future round wants to unify detail pages too, that's t-paliad-110 territory; the deadline-edit and appointment-edit forms are quite different (event_type chips, rule code, complete/reopen vs CalDAV time pickers, location, type dropdown).
+
+### Q14. Data migration
+
+**Recommendation: none. Both tables stay; only the read side joins.**
+
+`paliad.deadlines` and `paliad.appointments` keep their schemas. `EventService` reads from both and projects to `EventListItem` at request time. Migration 030+ stays untouched.
+
+The only schema-adjacent change worth flagging: when we add **per-row "Erledigt" semantics for appointments** (Q14 in §F asks), we'd need a new column `paliad.appointments.completed_at` or similar. Today there's no such concept (a past appointment is just past). I'd defer this to a follow-up unless m wants it now.
+
+### Rollout (PR shape)
+
+Single feature PR on `mai//events-unification`, ~5 commits:
+
+1. **Backend: EventService + endpoint.** New `internal/services/event_service.go` (delegating to existing services), new `internal/handlers/events.go` (`GET /api/events`, `GET /api/events/summary`), wire into `Services` struct.
+2. **Backend: EventService tests.** Unit tests for the merge/sort logic, type-filter, status-filter behavior, summary counts.
+3. **Frontend: shared EventsPage component + client/events.ts.** New `frontend/src/events.tsx` (the shared TSX), new `frontend/src/client/events.ts` (the runtime). Shared filter row, shared bucket-rail, shared table renderer.
+4. **Frontend: rewire `/deadlines` and `/appointments` handlers** to render `EventsPage` with the right `defaultType`. Drop `frontend/src/deadlines.tsx` + `frontend/src/appointments.tsx` (their build entries replaced by `events`). Update `bun build` config + Go template glue.
+5. **Frontend: Dashboard Termine summary rail.** Read `/api/events/summary`, render 3 cards under the existing Fristen rail.
+
+Plus i18n keys (DE+EN) for the new strings: type-chip labels, the 3-chip toggle, "+ Neu" dropdown labels, Dashboard Termine rail. Roughly ~12 new keys.
+
+Old endpoints (`GET /api/deadlines`, `GET /api/appointments`) **stay** — they're used by `/deadlines/calendar`, `/appointments/calendar`, `/projects/{id}` detail panes, mobile/PWA. Don't churn callers we don't have to.
+
+Estimated PR scope: ~600 LoC backend + ~900 LoC frontend (most of it consolidation, not new code) + ~150 LoC tests. Numbers approximate.
+
+---
+
+## 8. Mock — unified table layout
+
+ASCII mock of `?type=` (Beides) view, after 3-chip toggle, both rails visible:
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ Fristen [Kalender] [Neue Frist] │ ← H1 reflects entry route
+├─────────────────────────────────────────────────────────────────────────┤
+│ [⏰ Fristen] [📅 Termine] [Beides ●] │ ← 3-chip toggle
+├─────────────────────────────────────────────────────────────────────────┤
+│ Fristen auf einen Blick │
+│ ┌────────┬───────┬────────────┬────────────┬──────────┐ │
+│ │ 3 │ 2 │ 5 │ 1 │ 2 │ │
+│ │Überfäl.│ Heute │Diese Woche │Nächste W. │ Erledigt │ │
+│ └────────┴───────┴────────────┴────────────┴──────────┘ │
+│ Termine │
+│ ┌───────┬────────────┬─────────┐ │
+│ │ 1 │ 4 │ 12 │ │
+│ │ Heute │Diese Woche │ Später │ │
+│ └───────┴────────────┴─────────┘ │
+├─────────────────────────────────────────────────────────────────────────┤
+│ Projekt: [Alle ▾] Von: [____] Bis: [____] Typ: [Alle ▾] Status: [—] │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │ │ Datum │ Titel │ Projekt │ Regel │ Typ│Ort │T-Typ │ Status │
+├─┼─┼──────────────────────┼────────────────────┼───────────┼────────┼────┼─────────────────────┼───────────┼────────────┤
+│☐│⏰│ 28.05.2026 │ Statement of Def. │ ACM 0001 │RoP.023 │SoD │ │ │ Offen │
+│☐│⏰│ 31.05.2026 OVERDUE │ Reply to Defence │ ACM 0001 │RoP.029a│Repl│ │ │ Offen │
+│ │📅│ 01.06.2026 09:00–11:00│ MV Acme v. Foo │ ACM 0001 │ │ │UPC LD MUC, … │Verhandlung│ │
+│☐│⏰│ 03.06.2026 │ Schriftsatz Beweis │ ACM 0001 │ │SoD │ │ │ Offen │
+│ │📅│ 05.06.2026 14:00 │ Strategiebespr. │ — │ │ │Zoom │Besprechung│ │
+└─┴─┴──────────────────────┴────────────────────┴───────────┴────────┴────┴─────────────────────┴───────────┴────────────┘
+```
+
+In `?type=deadline` mode, the Termine summary rail and the Ort/T-Typ columns vanish; in `?type=appointment` mode, the Fristen rail vanishes plus Regel/Typ/Status; the table becomes today's pure deadline / pure appointment table.
+
+The leftmost ☐ column is the deadline-complete checkbox (deadline rows only — appointments don't have a complete affordance today; Q14 in §F asks).
+
+---
+
+## 9. Section F — Open questions for m
+
+These are blocking. I've put a recommendation under each so the decision is small.
+
+**Q1. Premise correction.** The task brief described `/agenda` as the appointment list backed by `AppointmentService`. The live system has `/appointments` as the appointment list and `/agenda` as a third pre-existing cross-type timeline view. The design above treats the unification target as **`/deadlines` ↔ `/appointments`**, with `/agenda` left alone. **Confirm this read.** *(Reco: confirm.)*
+
+**Q2. Sidebar.** Keep "Fristen" + "Termine" as separate sidebar entries, both pointing at the unified component? Or collapse to one "Ereignisse" entry? *(Reco: keep separate.)*
+
+**Q3. `/agenda` fate.** Out of scope for this round (timeline stays as-is) — confirm? Or do you want the timeline retired in favor of the new Events list + Beides toggle? If retired, the cards on Dashboard linking to `/agenda` need rerouting. *(Reco: leave as-is for this round.)*
+
+**Q4. Page header in Beides mode.** When the user toggles to Beides on `/deadlines`, does the `` stay "Fristen" (recommended), switch to "Ereignisse", or rewrite to "Fristen & Termine"? *(Reco: stay "Fristen" — the route owns the heading; the chip toggle is a within-page filter.)*
+
+**Q5. URL on type-chip toggle.** When the user toggles to "Beides" on `/deadlines`, the URL becomes `/deadlines?type=` — slightly weird. Acceptable, or should the toggle redirect to a canonical `/events` route? *(Reco: accept the weirdness; bookmarks survive.)*
+
+**Q6. "Neu" button in Beides mode.** Recommended: single button "+ Neu" with a 2-option dropdown (Neue Frist / Neuer Termin). Acceptable, or do you want two side-by-side buttons? *(Reco: dropdown.)*
+
+**Q7. Filter row visibility.** In Beides mode, deadline-only filters (Status, Typ multi-select) are visible-but-mark-deadline-only. Appointment-only filter (Termin-Typ) is hidden. Confirm this asymmetry. *(Reco: confirm.)*
+
+**Q8. Date range filter on deadlines.** Today `/deadlines` has no Von/Bis range — only buckets. Adding it as part of the unified filter row would slightly change deadline UX. OK? *(Reco: yes, gives users another way to scope; doesn't replace buckets.)*
+
+**Q9. Type icon column.** I'm proposing a leftmost type icon column (⏰ vs 📅) in Beides mode for at-a-glance. Useful or noise? *(Reco: useful in Beides; auto-hide in single-type mode.)*
+
+**Q10. Dashboard Termine summary cards.** Add a 3-card Termine rail (Heute / Diese Woche / Später) under the existing 5-card Fristen rail. Confirm. *(Reco: add it.)*
+
+**Q11. Status filter semantics in Beides.** When type=Beides and Status="Erledigt" is set, what should appointments do? Three options:
+- (a) Hide all appointments (status filter only matches completed deadlines).
+- (b) Show all appointments untouched, plus completed deadlines.
+- (c) Disable the Status selector with a tooltip "Status gilt nur für Fristen". *(Reco: c — simplest mental model.)*
+
+**Q12. 5-bucket vs 3-bucket vs shared-4-bucket.** I recommended the two-rail approach in Beides (deadline 5-bucket + appointment 3-bucket stacked). Are you OK with two rails, or do you want a single shared bucket model (e.g. drop "Überfällig" and use a 4-card Heute/Diese Woche/Nächste Woche/Erledigt+Vergangen across both)? *(Reco: two rails — honest about each type's semantics.)*
+
+**Q13. Date column header label in Beides.** "Datum" (generic) vs keeping "Fällig / Beginn" double-header. *(Reco: "Datum"; the type icon column tells users what it means.)*
+
+**Q14. "Erledigt" for appointments.** Today appointments have no completion concept — past appointments just exist. Do you want to add `appointments.completed_at` so users can mark a Verhandlung as "done" and have it leave the active table? Or leave appointments without that — they fall off the active range filter naturally? *(Reco: defer to a follow-up; not part of this unification.)*
+
+**Q15. API endpoint cohabitation.** Keep `GET /api/deadlines` and `GET /api/appointments` alongside the new `GET /api/events`? *(Reco: keep both; the calendar and project-detail pages still call them. Retire on a separate v2 cleanup once confidence is high.)*
+
+**Q16. Detail-page unification.** Out of scope per task brief §13. Confirm — I want to be sure m's "same Events view" framing didn't extend to detail pages. *(Reco: out of scope; deadline-edit and appointment-edit forms have nearly disjoint fields.)*
+
+**Q17. Granularity on event-type filter in Beides.** Event-type filter (multi-select chip cluster) only matches deadlines (appointments don't have event types). When applied in Beides, do appointments get included anyway, or do they get filtered out (logically: "show only events that have one of these types")? *(Reco: appointments pass through unchanged; the filter is a deadline-side narrower, not a global narrower. Tooltip clarifies.)*
+
+---
+
+## 10. Out of scope
+
+- Detail pages (`/deadlines/{id}`, `/appointments/{id}`) — stay separate.
+- `/agenda` timeline — stays as-is for this round.
+- `/deadlines/calendar` and `/appointments/calendar` — month-grid views; not affected by list unification.
+- Forms (`/deadlines/new`, `/appointments/new`) — stay separate.
+- Reminder service, CalDAV sync, project-detail panes — read from old endpoints; unaffected.
+- Adding `completed_at` to appointments — defer per Q14.
+
+---
+
+## 11. Files the implementer will touch
+
+(For the head's planning; not authoritative.)
+
+**New files:**
+- `internal/services/event_service.go`
+- `internal/services/event_service_test.go`
+- `internal/handlers/events.go`
+- `frontend/src/events.tsx`
+- `frontend/src/client/events.ts`
+
+**Modified:**
+- `internal/handlers/handlers.go` — wire new service + endpoints; rewire `/deadlines` and `/appointments` page handlers to render `events.tsx`.
+- `internal/handlers/dashboard.go` — extend payload with appointment summary (or call new `/api/events/summary`).
+- `frontend/src/dashboard.tsx` — add Termine 3-card rail.
+- `frontend/src/client/dashboard.ts` — fetch + render Termine summary.
+- `frontend/src/i18n.ts` (or wherever keys live) — ~12 new DE/EN keys.
+- `frontend/build.ts` — drop `deadlines.tsx`/`appointments.tsx` build entries; add `events.tsx`.
+
+**Deleted (replaced):**
+- `frontend/src/deadlines.tsx`
+- `frontend/src/client/deadlines.ts`
+- `frontend/src/appointments.tsx`
+- `frontend/src/client/appointments.ts`
+
+**Untouched:**
+- `internal/services/deadline_service.go` (still called by `EventService`)
+- `internal/services/appointment_service.go` (still called by `EventService`)
+- `internal/services/agenda_service.go` (still serves `/agenda` timeline)
+- All detail / form / calendar pages.
+
+---
+
+## 12. Inventor stays parked
+
+This is design-only per the inventor → coder gate. After m greenlights §F, head decides whether to load `/mai-coder` on me or assign elsewhere. cronus has the deepest event-types context (t-paliad-088) and bucket math context (t-paliad-106) so cronus or curie are natural fits, but the head decides.
+
+— cronus