planck flagged via mai report feedback (id 12301) after the B5+B6 verification round caught them: - §5.4 'INSERT into paliad.project_parties' → real table is paliad.parties - §5.4 'status=open' → real CHECK constraint allows pending/completed/cancelled/waived - §7.4 listed verfahrensablauf-detail-mode.ts as dead code, but builder imports filterByDetailMode from it; struck through with KEEP note. Code shipped (B5+B6) used the correct values throughout; this aligns the historical PRD with reality so a future reader doesn't repeat the verification time planck spent.
56 KiB
PRD — Procedures: Litigation Builder (m/paliad#153)
Task: t-paliad-339
Gitea: m/paliad#153
Inventor: edison (shift-1, Opus)
Date: 2026-05-27
Branch: mai/edison/inventor-prd-columnar
Status: Draft — DESIGN READY FOR REVIEW. Coder gate held.
Builds on (read before extending this PRD):
docs/design-procedures-workflow-tracker-2026-05-27.md— atlas's reverted tracker design (m/paliad#152). The anchor+scope idea did not land; understand why before re-proposing.docs/design-unified-procedural-events-tool-2026-05-27.md— cronus's U0-U4 catalog, currently live on main @ed3c5d1post-revert. Visual baseline for filter strip + tab control.docs/design-deadline-system-revision-2026-05-27.md— atlas Phase 2 model layer (scenario_flags SSoT, view-mode toggle, per-rule selection chips). Model layer is locked; this PRD is purely surface + new persistence tables.docs/design-fristenrechner-overhaul-2026-05-26.md— cronus 2026-05-26 inventor-pass (Mode A + B + result, shipped via t-paliad-322).
Predecessor takeaway (atlas's debrief on #152):
"When the architecture is novel, default to grilling m in prose FIRST. The doc rewrites cost a commit; the bigger cost would have been wasting m's question-batch on the wrong architecture."
Followed here. This PRD captures the architecture m chose through 20 chip-picker decisions across 5 batches, not an inventor-first strawman.
§0 Premises
§0.1 What is /tools/procedures today (live, post-revert)
The current page is cronus's 4-tab catalog (U0-U4, shipped via m/paliad#151):
- Sticky filter strip (search box + 4 chip rows: Forum / Verfahren / Ereignisart / Partei).
- 4 solid tabs:
Verfahren wählen/Direkt suchen/Geführt/Aus Akte. - Default-active tab = "Verfahren wählen" renders
VerfahrensablaufBody(the legacy Verfahrensablauf wizard: proceeding picker → perspective + date → 3-step wizard → result in 3-column "Spalten" or single-column "Zeitstrahl"). - Other 3 tab panels are stubs (search/wizard/akte never wired in U0-U3).
m's blocking feedback (verbatim, 2026-05-27 22:18):
I like to keep our current columnar layout with proactive / court / reactive. And it is good if we can select which side we want to simulate. […] There are basically three main approaches I see to this: Get an overview over proceedings, play around with options, build Scenarios. Another one where something specific happened and we just want to know what deadlines we need to note […]. A third one from a specific proceeding / case file where things take place / have taken place.
And the architecture-shifting follow-ups (2026-05-27 22:35-22:36, mid-grilling):
I would prefer to have an interface where not every constellation is in the URL by the way. That seems limiting. We could just have a litigation builder. Sometimes we build a full scenario with multiple instances etc, sometimes we just want the next step. we should have ways to save these "litigation constellations" where we save which proceedings we have and which state they are in, which submissions were or were not filed. A small Scenario DB could work, dont you think?
These three statements upgraded the brief from "redesign a catalog" to "build a Litigation Builder backed by a Scenario DB". The PRD below is shaped by them.
§0.2 Locked constraints (m's words, brief in #153)
- Columnar layout:
proaktiv | court | reaktiv(perspective-flippable). - Three approaches as entry modes: overview/scenarios, event-triggered, case-file driven.
- Filtering across all dimensions + text search.
- Optional follow-ups: toggleable, highlightable, with display-count setting.
- Modular where it actually helps (m: "I don't know — generally does not super apply here." — drop modular as a load-bearing goal).
- UPC v1, expand later.
§0.3 Live data the builder works against
Verified 2026-05-27 against paliad.sequencing_rules (231 published / 242 total):
- 110 chained (
parent_idnot null). - 78 trigger-rooted, 4 spawns (cross-PT), 47 court-set, 18 conditional.
- ~46 proceeding types total (UPC 35 / DE 5 / EPA 3 / DPMA 3). v1 focuses on UPC.
paliad.proceeding_types.kinddiscriminator (atlas's t-paliad-324) filters non-proceeding rows (phases/side_actions/meta) from the picker.paliad.deadlinescarries bothprocedural_event_idandsequencing_rule_id→ Akte actuals overlay is a direct join.paliad.projects.scenario_flagsjsonb (atlas P0) is the SSoT for project-level scenario state; the newpaliad.scenario_proceedings.scenario_flagsmirrors this shape per-proceeding-per-scenario.
§0.4 Scope (in / out)
In:
- Replace
/tools/procedureswith the Litigation Builder. - New
paliad.scenarios+paliad.scenario_proceedings+paliad.scenario_events+paliad.scenario_sharestables. - Promote-to-project flow (scenario →
paliad.projectsrow). - Bidirectional link from
/projects/{id}(button: "Im Builder öffnen" — exports project state to a builder session).
Out (deferred or owned elsewhere):
- Calculator (
pkg/litigationplanner.CalculateRule) — working. - Editorial backfill (curie's t-paliad-333 owns the 7 compound rules + R.109).
/admin/procedural-events(editor surface; different audience)./projects/{id}Verlauf / SmartTimeline (per-Akte actuals; sister tool).- youpc.org / Outlook / PDF export.
- Multi-jurisdiction expansion (DE/EPA/DPMA) — UPC v1 first.
- Cross-proceeding peer triggers (UPC-inf judgment → EPA opp choice deadline) — v1.1.
- Multi-user concurrent editing on the same scenario (out of scope; sharing is read-only).
§1 Goals
- One canvas, three entry modes. Unify the 3 approaches into a single Litigation Builder surface. The entry modes (
Übersicht / Ereignis / Aus Akte) shape the initial state of the canvas; once the user is working, the canvas itself is what they interact with. - Persisted constellations. A user can save a "litigation constellation" — multiple parallel proceedings with their flags, filed/skipped/planned event states, dates, and notes — as a named scenario. Scenarios live in the DB (not the URL).
- Auto-save by default. No "unsaved changes" modals. The active scenario auto-persists. Anonymous scratch scenarios convert to named ones when the user clicks "Benennen".
- Promote-to-project. A scenario can be turned into a real
paliad.projectsrow via a 3-step wizard. Procedural shape, placeholder parties, notes, and filed-state all carry over; the user fleshes out client-bound metadata during the wizard. - Share read-only with the team. Each scenario is private by default; explicit "An Team teilen" grants named HLC users read-only access. Original owner stays sole editor.
- Columnar geometry restored. The current "Spalten" view (claimant | court | defendant) returns as the canonical render — but now per-proceeding-triplet within a scenario, with perspective ("our side") flippable per proceeding so
proaktiv | court | reaktivreads correctly across multi-proceeding constellations. - Per-event-card optional horizon. Each event card on the canvas can dial in how many optional follow-ups to surface. Cards are the unit of optional-display control.
§2 User journeys
§2.1 Journey A — Cold-open builder ("Übersicht / Scenarios")
Persona: Dr. Becker, senior partner. Friday afternoon. New UPC matter not yet committed; she's briefing a client on Monday on the full procedural shape.
- Opens
/tools/procedures. No?scenarioparam. Cold-open canvas: empty workbench with a "Neues Szenario starten" CTA and a short list of her 5 most-recent scenarios. - Clicks the CTA → inline picker (Forum chip row → Verfahren chip row →
Hinzufügen). Picks UPC +upc.inf.cfi. - Canvas now renders one proceeding triplet (
proaktiv | court | reaktiv). Default perspective is empty (no party selected) — both sides render equally; the perspective radio in the page header sits unset. - She picks defendant perspective at the page header → triplet flips. The defendant column becomes
proaktiv(her side); claimant becomesreaktiv. - She adds a second proceeding via
+ Verfahren hinzufügenat the bottom: EPAepa.opp.opd. New triplet stacks below the first. New triplet's perspective defaults to "patentee" inheriting from her client's role across the two; she flips per-proceeding via the triplet header. - She turns on
with_ccron the UPC inf triplet's per-proceeding flag strip. The CCR child triplet auto-expands inline below the parent at the spawn node. - Auto-save kicks in (debounced 500ms). The page header shows "Gespeichert in Scratch · Benennen".
- She clicks "Benennen", enters "Becker — UPC + EPA defensive". Side panel "Meine Szenarien" updates.
- On Monday she opens the scenario from her recent list, walks the client through it, hits "Als Projekt anlegen" (when the client commits). 3-step wizard fires (§5.4).
§2.2 Journey B — Event-triggered lookup ("Ereignis")
Persona: Sandra, paralegal. Today: a Hinweisbeschluss arrived on a CMS queue. She doesn't know yet which Akte it belongs to.
- Opens
/tools/procedures. Picks "Ereignis" entry mode at the top. - Page-header search box auto-focuses. She types "Hinweis" → universal search drops down:
5 Ereignisse · 1 Szenario · 0 Akten. Picks the eventupc.inf.cfi.cmo_review(Antrag CMO-Überprüfung). - Canvas renders one triplet of
upc.inf.cfiwith the Hinweisbeschluss event card auto-anchored (lime band +━━ DU BIST HIER ━━divider above the next-coming events). - She reads the follow-ups: "Antrag auf CMO-Überprüfung (claimant, R.333.2 · 1 Monat)" and 2 optional follow-ups. The Stichtag input in the page header defaults to today; she leaves it.
- She doesn't save anything — this was a quick lookup. Scratch scenario auto-persists but she doesn't name it; it'll fall off her recent list after a while.
- Later she identifies the matter (HL-2024-001), switches to "Aus Akte" mode, and continues there.
§2.3 Journey C — Case-file driven ("Aus Akte")
Persona: Anna, senior associate. Working on HL-2024-001 (UPC infringement). The client just confirmed they want to file a CCR.
- Opens
/tools/procedures. Page-header Akte picker shows recent projects; she picks HL-2024-001. - Page header auto-fills: proceeding =
upc.inf.cfi, perspective = defendant (fromprojects.our_side), scenario_flags ={with_ccr: false}(current state). - Builder loads: one
upc.inf.cfitriplet, perspective-flipped. Event cards overlay actuals frompaliad.deadlines—Klageerhebungis filed (2026-01-15),Klageerwiderungis planned (2026-04-01, computed), others are planned. - She turns on
with_ccron the triplet's flag strip. The CCR child triplet expands inline. Crucially: the scenario is project-backed — the flag write also patchesprojects.scenario_flags(via existingPATCH /api/projects/{id}/scenario-flagsfrom atlas P0). When she walks away, the project's deadlines + flags reflect the builder's state. - She marks the
Widerklage auf Nichtigkeitevent card as "filed" with today's date. Builder writes apaliad.deadlinesrow withstatus='done'+completed_at=today, audit_reason "via Litigation Builder". Project's Verlauf reflects this. - The CCR child triplet's
Antrag Patentänderung (R.30)event card surfaces. She marks it "planned" and ticks the per-card optional horizon to "+2" → 2 more optional R.30-adjacent rules surface. - Exit: she closes the tab. Project state persists in
paliad.projects+paliad.deadlinesas before; the scenario row tracks the builder-session view (so when she returns, the canvas state is restored — including her per-card optional-horizon picks).
§2.4 Journey D — Promote scratch to a real project
Persona: Dr. Becker, follow-up from Journey A. The client committed; she wants to convert the scenario into a real matter.
- With "Becker — UPC + EPA defensive" loaded, she clicks "Als Projekt anlegen" in the page header.
- Wizard step 1: Bestätigen. Read-only summary of what's about to be promoted: 2 proceedings (UPC inf + EPA opp), CCR child, 3 scenario flags set, 0 events filed, 5 events planned, 2 notes. "Weiter".
- Wizard step 2: Parteien ergänzen. Each proceeding's parties section shows whatever placeholder names she sketched in the scenario ("Klg X" / "Bekl Y"). She edits each into the real names. (Per m's Q11 pick — full carry — placeholder strings come in; the wizard's job is to clean them.)
- Wizard step 3: Akte-Metadaten. Case number, client, litigation parent project (optional), our_side (auto-set from the scenario's primary triplet), team selection. "Anlegen".
- New
paliad.projectsrow written withorigin_scenario_id = <scenario.id>. Scenario row'sstatusflips topromoted,promoted_project_idpoints back. Builder navigates to/projects/<new-id>. - The scenario stays read-only in her "Meine Szenarien" list under "Promoted", reachable for historical reference (cf. "this is what we planned at briefing time").
§2.5 Journey E — Share a scenario with a colleague
Persona: Anna shares the HL-2024-001 builder session with Dr. Becker (her supervising partner) for review before committing to the CCR strategy.
- Anna opens the scenario, clicks "Teilen" in the page header.
- Side panel slides in with a user-picker (HLC user search). She picks "Dr. Becker", clicks "Schreibgeschützt teilen".
paliad.scenario_sharesrow written. Anna remains sole editor.- Dr. Becker opens the tool. Her side panel "Meine Szenarien" has a new bucket "Geteilt mit mir"; Anna's scenario is listed. She opens it: canvas renders the same view but every mutating affordance (add proceeding, flag toggle, file/skip, promote, share) is disabled. Watermark: "Geteilt von Anna · schreibgeschützt".
- Becker reads, drops Anna a note via existing comment infrastructure (out of scope — separate ticket). Decision made out-of-band. Anna proceeds.
§3 The canvas shape
§3.1 ASCII sketch
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Paliad · Verfahren & Fristen — Litigation Builder [Mein Konto ▾] │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ Szenario: [Becker — UPC + EPA def. ▼] Gespeichert ✓ · [Benennen] [Teilen] [Als Projekt] │
│ Akte: [— ohne — ▼] Stichtag: [2026-04-01] │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ Filter: [🔍 Klageerwiderung, Hinweis, HL-2024… ] │
│ Forum [● UPC] [DE] [EPA] [DPMA] Verfahren [● upc.inf.cfi …] │
│ Partei [Klg] [● Bekl] Ereignisart [filing] [hearing] [decision] │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ Einstieg: [ Übersicht ● ][ Ereignis ○ ][ Aus Akte ○ ] │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ upc.inf.cfi · Verletzungsverfahren UPC Bekl-Sicht [▾] [Detailgrad: Gewählt ▾]│
│ │ Optionen: ☑ with_ccr ☐ with_amend ☐ with_cci [─][×] │
│ ├─────────────────┬────────────────────┬─────────────────────────────────────────┤
│ │ Proaktiv (Bekl) │ Gericht │ Reaktiv (Klg) │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ │ Klageerw. │ │ │ │ Klageerh. │ │
│ │ │ R.23 │ │ │ │ R.13 │ │
│ │ │ planned │ │ │ │ filed │ │
│ │ │ 2026-04-01 │ │ │ │ 2026-01-15 │ │
│ │ │ +3 Optionen ▾│ │ │ │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │
│ │ │ ┌──────────────┐ │ │
│ │ │ │ Mündl. Verh. │ │ │
│ │ │ │ planned │ │ │
│ │ │ │ [Gericht] │ │ │
│ │ │ └──────────────┘ │ │
│ │ ━━━━━━━━━ DU BIST HIER (Klageerwiderung) ━━━━━━━━━ │
│ └─────────────────┴────────────────────┴─────────────────────────────────────────┘ │
│ │
│ ┌── (spawn child) upc.ccr.cfi · Widerklage auf Nichtigkeit Klg-Sicht [▾] ───────┐│
│ │ Optionen: ☐ with_amend [─][×]││
│ ├────────────────────┬─────────────────┬───────────────────────────────────────┐ ││
│ │ Proaktiv (Klg) │ Gericht │ Reaktiv (Bekl) │ ││
│ │ ┌─────────────┐ │ │ │ ││
│ │ │ CCR-Antrag │ │ │ │ ││
│ │ │ R.49 │ │ │ │ ││
│ │ │ planned │ │ │ │ ││
│ │ └─────────────┘ │ │ │ ││
│ └────────────────────┴─────────────────┴───────────────────────────────────────┘ ││
│ │
│ ┌─ epa.opp.opd · Einspruchsverfahren EPA PatInh-Sicht [▾] [Detailgrad: Gewählt ▾]│
│ │ Optionen: (keine flags für EPA Opp) [─][×] │
│ ├─────────────────┬────────────────────┬─────────────────────────────────────────┤
│ │ Proaktiv │ EPA │ Reaktiv (Einsprechende) │
│ │ ┌─────────────┐ │ │ │
│ │ │ Erwiderung │ │ │ │
│ │ │ R.79(1) EPÜ │ │ │ │
│ │ │ planned │ │ │ │
│ │ └─────────────┘ │ │ │
│ └─────────────────┴────────────────────┴─────────────────────────────────────────┘ │
│ │
│ [ + Verfahren hinzufügen ] │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
Side panel (collapsible, right-edge):
┌──── Meine Szenarien ────┐
│ ● Aktiv │
│ ▸ Becker — UPC+EPA def │ ← current
│ ▸ Test-CCR-Patent-X │
│ ○ Geteilt mit mir │
│ ▸ Becker UPC ply │
│ ○ Promoted │
│ ▸ HL-2023-118 │
│ ○ Archiviert (3) │
│ [+ Neues Szenario] │
└──────────────────────────┘
§3.2 What each element does
| Element | Read | Write | Persists in |
|---|---|---|---|
| Page-header scenario picker | Current scenarios.id + name |
Switch scenarios | URL ?scenario=<id> + DB |
[Benennen] button |
Anonymous → named | scenarios.name, status='active' |
DB |
[Teilen] button |
— | scenario_shares row(s) |
DB |
[Als Projekt] button |
— | Opens promote wizard | (wizard → DB on commit) |
| Akte picker | User's projects | Loads project state into builder | URL ?project=<id> + DB |
| Stichtag input | Scenario-level default | scenarios.stichtag |
DB |
| Filter strip (search + chips) | Free-text + dimension filters | UI state | URL ?q, ?forum, … per-mode |
| Einstieg mode radio | Current entry mode | Resets filter strip on change | URL ?mode= |
| Triplet header (jurisdiction badge + name + perspective + Detailgrad) | scenario_proceedings.{primary_party, detailgrad} |
Edit | DB |
| Triplet flag strip | scenario_proceedings.scenario_flags |
Toggle flags | DB |
| Event card (state, date, notes, optional-horizon) | scenario_events.* |
Edit per-card | DB |
+ Verfahren hinzufügen |
— | New scenario_proceedings row |
DB |
| Side panel | User's scenarios + shared scenarios | Switch + create + archive | DB |
§3.3 Columns: proaktiv | court | reaktiv
The 3-column layout returns as the canonical desktop shape. Per m's locked constraint (and brief #153), it is a stance grouping, not a sequence anchor — time flows top-to-bottom (chronological), columns express who is acting.
- Proaktiv: the column for events the active perspective's party initiates (their
primary_partymatches the event'sprimary_party). - Court: court-set events (
is_court_set=true), neutral column. - Reaktiv: the column for events the opposing party initiates.
The perspective is per-proceeding (per-triplet, via scenario_proceedings.primary_party). When no perspective is set (null), both party columns render equally with their natural party labels (Klg / Bekl), not Proaktiv / Reaktiv. This means kontextfrei browsing reads as "claimant column | court | defendant column" until the user picks a side.
This addresses m's reverted-design bug #3 verbatim: "Proaktiv/Gericht/Reaktiv columns are a stance grouping, not a sequence anchor." Time = vertical. Stance = horizontal. The triplet is the unit; multiple proceedings stack vertically.
§3.4 Event card anatomy
┌─────────────────────┐
│ Klageerwiderung │ ← event name (procedural_event.name)
│ R.23 │ ← rule code
│ planned │ ← state: planned / filed / skipped
│ 2026-04-01 │ ← date (computed for planned, actual for filed)
│ +3 Optionen ▾ │ ← per-card optional horizon (only when card has optionals)
└─────────────────────┘
State machine (m's Q10 pick — 3-state):
planned(default): future event, date is computed from anchor + duration_value + duration_unit. Click → choosefiledorskipped.filed: past event,actual_dateis set (defaults to computed, user can override). Visual: ✓ checkmark, slightly muted "past" tone.skipped: user chose not to file. Visual: strikethrough text + optionalskip_reason(textarea). Optional rules are commonly skipped without rationale; mandatory rules withskippedstate flag the scenario as "non-standard" but don't block.
No overdue state — user does the date arithmetic by eye against today. (Mandatory cards rendered in red when actual_date < today AND state=planned is a render hint, not a stored state.)
Per-card optional horizon (m's Q4 pick). Each card with children at priority IN ('optional','recommended-skip-by-default') carries a chip +N Optionen ▾. Default N=0 (hidden). Clicking opens an inline list of the optional children with +/- controls to surface/hide them on the canvas. Per-card horizon persists as scenario_events.horizon_optional int.
Filed-state cards persist the date in scenario_events.actual_date date. The card's notes field (textarea, lazy-loaded) lives in scenario_events.notes text.
§3.5 Court-set events
is_court_set=true rules don't compute a date until the court picks one. Card renders with [Gericht] badge in place of the date and a small "Datum eintragen" affordance. Clicking filed opens a date picker (date is required for filed state when is_court_set=true — the user is asserting "the court set this date").
Downstream events that anchor on a court-set event render their dates as [abhängig von <event>] until the court date is filed, then auto-recompute.
§3.6 Spawn (child) proceedings
When a triplet has a with_<flag> enabled and the flag's gating rule has is_spawn=true, the child proceeding (e.g. upc.ccr.cfi for with_ccr on upc.inf.cfi) renders inline as a child triplet immediately below the parent triplet in the canvas stack — visually nested via the spawn note in the parent triplet's header band.
scenario_proceedings.parent_scenario_proceeding_id FK self-references for the nesting; scenario_proceedings.spawn_anchor_event_id points at the gating sequencing_rule so the UI knows where in the parent the spawn happened.
The child triplet has its own perspective, scenario flags, Stichtag override, Detailgrad. It can itself spawn (depth N supported; today's data is 2-deep at most).
Cross-proceeding peer triggers (upc.inf judgment → epa.opp choice deadline) are out of scope for v1 (m's Q14 pick). v1 ships independent triplets stacked vertically; the user mentally tracks cross-dependencies. A future scenario_event_links table is the path to peer triggers in v1.1.
§4 Hard decisions table — m's 20 picks
| # | Topic | Pick | Locks |
|---|---|---|---|
| Q1 | Modular meaning | "doesn't super apply" — drop modular as a load-bearing goal | §0.2 |
| Q2 | Tab state semantics | Shared anchor + Akte across modes; filters reset per mode | §3.1, §3.2, §6 |
| Q3 | Case-file integration | Page-header Akte picker, persistent across modes | §3.1, §3.2, §2.3 |
| Q4 | Optional-display horizon | Per-event-card | §3.4 |
| Q5 | Builder shape | Unified builder, 3 entry modes (cold-open / event-triggered / Akte) | §0, §1, §2, §3 |
| Q6 | Scenario↔project relationship | Separate paliad.scenarios table + promote-to-project action |
§5, §2.4 |
| Q7 | Scenario contents | Multi-proceeding constellation per scenario | §3, §5 |
| Q8 | Save model | Auto-save active scenario + "Meine Szenarien" list | §1, §3, §6.4 |
| Q9 | Multi-proceeding render | Vertical stacked column-triplets | §3 |
| Q10 | Per-event state | 3-state: planned / filed / skipped (no overdue state) |
§3.4 |
| Q11 | Promote-to-project carry | Everything (incl. placeholder parties + free-form notes) | §2.4, §5.4 |
| Q12 | Sharing model | Private by default + explicit team-share (read-only) | §1, §5, §2.5 |
| Q13 | Scenario flags placement | Per-proceeding (each triplet owns its scenario_flags) |
§5.1 |
| Q14 | Cross-proceeding peer triggers | Out of scope for v1 (defer to v1.1) | §3.6, §7 |
| Q15 | Perspective scope | Per-proceeding (each triplet has its own primary_party) |
§3.3, §5.1 |
| Q16 | Add-proceeding flow | + Verfahren hinzufügen button below the last triplet, inline picker |
§3, §3.1 |
| Q17 | Cold-open canvas | Empty canvas + "Neues Szenario" CTA + recent-list | §2.1, §3 |
| Q18 | Search scope | Universal: events + scenarios + Akten, scoped by result type | §3.1, §6 |
| Q19 | Promote-to-project flow | 3-step wizard (Bestätigen → Parteien ergänzen → Akte-Metadaten) | §2.4, §5.4 |
| Q20 | Mobile treatment | Desktop v1, mobile basic-read (mutating actions prompt "Auf größerem Bildschirm öffnen") | §3, §7 |
§4.1 Divergences from inventor recommendations
Three picks diverged from my recommendation. Captured here so future readers (m, the coder) see the current design, not the strawman.
- Q1 — Modular. Inventor recommended "plug-in widgets". m: "I don't know — generally does not super apply here." Modular is dropped as a goal; the natural decomposition (BuilderCanvas → ProceedingTriplet → EventCard → ScenarioListPanel → PromoteWizard) is documented in §6.2 as build hygiene, not as a load-bearing constraint.
- Q10 — Event state. Inventor recommended 4-state (planned / filed / skipped / overdue). m picked 3-state — no
overdueenum. Rationale (interpreted):overdueis derived fromdate < today AND state=planned, not stored; this avoids stale state when the date is edited. - Q11 — Promote carry. Inventor recommended carrying procedural shape + flags + filed-state + notes but not placeholder parties/case_number/billing. m picked "everything carries" — placeholder parties come in. Mitigation: Q19's 3-step wizard's step 2 (Parteien ergänzen) gives the user a chance to clean placeholders before commit, so the safety net m wanted on Q11 is folded into Q19.
§4.2 Inventor picks not formally asked
A few decisions are inventor-set because they're either: (a) implementation details that don't change the architecture, or (b) clean defaults that match existing patterns. Listed here so they're visible; m can flag any.
- Detailgrad ("Gewählt" / "Alle Optionen") scope: per-proceeding (matches today's Verfahrensablauf pattern). State in
scenario_proceedings.detailgrad. - Akte picker shape: flat dropdown sorted by recently-viewed first, with a typeahead filter for case numbers/names. Same shape as today's project picker on /agenda.
- Notes: per-event-card (textarea on each card, lazy-loaded). Scenario-level notes also exist (
scenarios.notes text) for cross-cutting commentary. - Read-only shared state UI: every mutating affordance is disabled (greyed, no click handlers). Watermark "Geteilt von · schreibgeschützt" at the top of the canvas. No "Fork to my workspace" affordance in v1.
- URL contract: minimal, view-state only —
?scenario=<id>&mode=<entry>&event=<sequencing_rule_id>(deep-link to a specific anchor). Filter pills + chip state get URL params per active entry mode but explicitly NOT the constellation data (per m's "not every constellation in URL" guidance). The constellation lives inpaliad.scenario_*tables. - Auto-save granularity: debounced 500ms on every change. Indicator near scenario name:
Gespeichert ✓(last successful save < 5s ago),Speichert…(in flight),Letzte Speicherung fehlgeschlagen — erneut versuchen(on error). - Soft delete: archived scenarios stay in DB with
status='archived'. No hard delete in v1. - Audit: no audit log on scenario edits (they're exploratory). Audit on promote-to-project goes via the existing
projects.audit_log. - Concurrent editing: single-editor model. Owner is sole editor; shares are read-only. No locking / merge conflict UI needed in v1.
- Bilingual: German primary, English via existing
i18n.ts. Scenario names: user-chosen, any language. Skip reasons + notes: free-text, any language.
§5 Data model deltas
All new tables live in paliad.* schema, alongside existing paliad.projects / paliad.deadlines / paliad.sequencing_rules.
§5.1 New tables
-- Scenario header. One row per saved scenario (named or scratch).
CREATE TABLE paliad.scenarios (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
name text NOT NULL DEFAULT 'Unbenanntes Szenario',
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active','archived','promoted')),
origin_project_id uuid NULL REFERENCES paliad.projects(id) ON DELETE SET NULL,
-- set when scenario was exported from a project
promoted_project_id uuid NULL REFERENCES paliad.projects(id) ON DELETE SET NULL,
-- set when scenario was promoted to a project
stichtag date NULL,
-- scenario-level default Stichtag; per-triplet overrides take precedence
notes text NULL,
-- free-form scenario-level commentary
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX scenarios_owner_status_idx ON paliad.scenarios(owner_id, status);
CREATE INDEX scenarios_updated_idx ON paliad.scenarios(owner_id, updated_at DESC);
-- One row per proceeding inside a scenario. Multiple per scenario for
-- multi-proceeding constellations. parent_scenario_proceeding_id self-refs
-- for spawned children (CCR child of UPC inf etc.).
CREATE TABLE paliad.scenario_proceedings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scenario_id uuid NOT NULL REFERENCES paliad.scenarios(id) ON DELETE CASCADE,
proceeding_type_id uuid NOT NULL REFERENCES paliad.proceeding_types(id),
primary_party text NULL
CHECK (primary_party IN ('claimant','defendant')),
-- per-proceeding perspective; null = no perspective picked yet
scenario_flags jsonb NOT NULL DEFAULT '{}'::jsonb,
-- per-proceeding flags: {with_ccr: true, with_amend: false, …}
parent_scenario_proceeding_id uuid NULL REFERENCES paliad.scenario_proceedings(id) ON DELETE CASCADE,
-- self-ref for spawned children (CCR child of UPC inf etc.)
spawn_anchor_event_id uuid NULL REFERENCES paliad.sequencing_rules(id),
-- which rule of the parent caused this spawn (for UI placement)
ordinal int NOT NULL DEFAULT 0,
-- stack order on canvas (top to bottom)
stichtag date NULL,
-- per-proceeding Stichtag override; falls back to scenarios.stichtag
detailgrad text NOT NULL DEFAULT 'selected'
CHECK (detailgrad IN ('selected','all_options')),
appeal_target text NULL,
-- applies_to_target for appeal proceedings; null for non-appeal triplets
collapsed boolean NOT NULL DEFAULT false,
-- user-collapsed triplet header (UI state)
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX scenario_proceedings_scenario_idx ON paliad.scenario_proceedings(scenario_id, ordinal);
CREATE INDEX scenario_proceedings_parent_idx ON paliad.scenario_proceedings(parent_scenario_proceeding_id);
-- One row per event card on the canvas. Captures the card's state +
-- per-card attributes (filed date, skip reason, notes, optional horizon).
-- Most cards are sequencing-rule-backed; free-form events have a null
-- sequencing_rule_id and a non-null procedural_event_id (or text label).
CREATE TABLE paliad.scenario_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scenario_proceeding_id uuid NOT NULL REFERENCES paliad.scenario_proceedings(id) ON DELETE CASCADE,
sequencing_rule_id uuid NULL REFERENCES paliad.sequencing_rules(id),
procedural_event_id uuid NULL REFERENCES paliad.procedural_events(id),
-- one of {sequencing_rule_id, procedural_event_id, custom_label} must be set
custom_label text NULL,
-- free-form event name when neither sequencing_rule nor procedural_event apply
state text NOT NULL DEFAULT 'planned'
CHECK (state IN ('planned','filed','skipped')),
actual_date date NULL,
-- set when state='filed'; can also be set for state='planned' (court-set override)
skip_reason text NULL,
-- optional rationale when state='skipped'
notes text NULL,
-- per-card free-form
horizon_optional int NOT NULL DEFAULT 0,
-- per-card "show N more optionals" affordance
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (scenario_proceeding_id, sequencing_rule_id) WHERE sequencing_rule_id IS NOT NULL
);
CREATE INDEX scenario_events_proceeding_idx ON paliad.scenario_events(scenario_proceeding_id);
-- Read-only team shares. Owner is sole editor; shares grant view-only.
CREATE TABLE paliad.scenario_shares (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scenario_id uuid NOT NULL REFERENCES paliad.scenarios(id) ON DELETE CASCADE,
shared_with_user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
created_by uuid NOT NULL REFERENCES auth.users(id),
UNIQUE (scenario_id, shared_with_user_id)
);
CREATE INDEX scenario_shares_user_idx ON paliad.scenario_shares(shared_with_user_id);
§5.2 Additions to existing tables
-- One nullable FK on paliad.projects to track which scenario spawned this
-- project (set on promote-to-project). Auditable origin trail.
ALTER TABLE paliad.projects
ADD COLUMN origin_scenario_id uuid NULL
REFERENCES paliad.scenarios(id) ON DELETE SET NULL;
CREATE INDEX projects_origin_scenario_idx ON paliad.projects(origin_scenario_id)
WHERE origin_scenario_id IS NOT NULL;
No other changes to existing schema. paliad.deadlines continues to be the authoritative source for project-bound actuals; the builder writes to paliad.deadlines (not scenario_events) when working in Akte mode against a project-backed scenario.
§5.3 RLS
Same pattern as existing paliad.projects:
scenariosreadable byowner_idOR by users with a matchingscenario_shares.shared_with_user_idrow.scenarioswritable only byowner_id(and only whenstatus != 'promoted').scenario_proceedings+scenario_eventscascade from scenario visibility.scenario_sharesreadable byshared_with_user_idorcreated_by; writable only by the scenario owner.
Helper function paliad.can_see_scenario(scenario_id) mirrors the existing paliad.can_see_project(project_id) shape.
§5.4 Promote-to-project: data flow
[Wizard step 1: Bestätigen]
Read: scenarios + scenario_proceedings + scenario_events
Action: none (read-only summary)
[Wizard step 2: Parteien ergänzen]
Read: scenario_proceedings.scenario_flags (for hints about placeholder party names)
Action: builds an in-memory parties payload (per proceeding, per role)
[Wizard step 3: Akte-Metadaten]
Read: user's clients + litigations + project tree (existing /projects API)
Action: builds an in-memory project metadata payload
[Commit]
Transaction:
1. INSERT into paliad.projects (carrying step-2 + step-3 payloads, + scenario notes)
SET origin_scenario_id = <scenario.id>
2. INSERT into paliad.parties from step-2 payload
3. For each scenario_proceeding (depth-first, parent before child):
a. INSERT scenario_flags as projects.scenario_flags (parent-level only;
children become sub-projects via parent_project_id)
b. For each filed scenario_event: INSERT paliad.deadlines row with
status='done', completed_at=actual_date, audit_reason='via Litigation Builder promotion'
c. For each planned scenario_event: INSERT paliad.deadlines row with
status='pending', due_date=computed (or actual_date override)
d. Skipped events: not inserted (no deadline row)
4. UPDATE paliad.scenarios SET status='promoted', promoted_project_id=<new>
5. Navigate to /projects/<new>
The deadlines write uses existing POST /api/projects/{id}/deadlines/bulk semantics under the hood — no new bulk-deadline-from-scenario endpoint needed.
§6 Modular boundaries (light)
m said modular "doesn't super apply" — dropped as a load-bearing goal. The natural decomposition below is build-hygiene documentation, not a constraint the coder must enforce.
§6.1 Front-end components
| Component | File | Responsibility |
|---|---|---|
BuilderCanvas |
frontend/src/components/BuilderCanvas.tsx |
Root render of the builder. Receives the active scenario, renders triplet stack + cold-open empty state |
ProceedingTriplet |
frontend/src/components/ProceedingTriplet.tsx |
One proceeding's render: header strip (jurisdiction + name + perspective + Detailgrad + collapse + remove) + flag strip + 3 columns + spawn child triplets recursively |
EventCard |
frontend/src/components/EventCard.tsx |
One card in a column lane. State / date / optional-horizon / notes affordances |
ScenarioFlagsStrip |
frontend/src/components/ScenarioFlagsStrip.tsx |
Per-triplet flag toggles. Reads scenario_flag_catalog, applies to scenario_proceedings.scenario_flags |
AddProceedingPicker |
frontend/src/components/AddProceedingPicker.tsx |
Inline picker triggered by + Verfahren hinzufügen. Forum chip row → Verfahren chip row → Hinzufügen |
ScenarioListPanel |
frontend/src/components/ScenarioListPanel.tsx |
Side panel: Aktiv / Geteilt / Promoted / Archiviert buckets + new-scenario CTA |
PromoteToProjectWizard |
frontend/src/components/PromoteToProjectWizard.tsx |
3-step modal: Bestätigen / Parteien / Metadaten |
PageHeaderControls |
frontend/src/components/PageHeaderControls.tsx |
Scenario picker + Benennen/Teilen/Promote buttons + Akte picker + Stichtag input |
EntryModeChrome |
frontend/src/components/EntryModeChrome.tsx |
Cold-open / event-triggered / Akte mode radio; ephemeral UI affordance that fades into canvas state |
§6.2 Client TS files
Mirror the React-ish component split:
frontend/src/client/builder.ts— root orchestrator (auto-save loop, URL state, mode routing, scenario fetch)frontend/src/client/builder-scenario.ts— scenario CRUD against/api/scenariosfrontend/src/client/builder-event-card.ts— per-card state machine + optional-horizon controlfrontend/src/client/builder-promote-wizard.ts— 3-step wizard state machinefrontend/src/client/builder-search.ts— universal search (events + scenarios + Akten)frontend/src/client/builder-shares.ts— share-with-team UI
§6.3 Backend services + routes
| Service | File | Endpoints |
|---|---|---|
ScenarioService |
internal/services/scenario_service.go |
List / Get / Create / Update / Archive / Promote |
ScenarioProceedingService |
internal/services/scenario_proceeding_service.go |
Add / Remove / Update (flags, perspective, ordinal, detailgrad) |
ScenarioEventService |
internal/services/scenario_event_service.go |
List / Update state / Set date / Set notes / Set horizon |
ScenarioShareService |
internal/services/scenario_share_service.go |
List / Add / Remove shares |
ScenarioPromoteService |
internal/services/scenario_promote_service.go |
Wizard-driven transactional promote |
Routes (added under existing API namespace):
GET /api/scenarios — list user's scenarios (filtered by status)
POST /api/scenarios — create new scenario
GET /api/scenarios/{id} — get scenario + proceedings + events (deep)
PATCH /api/scenarios/{id} — update name / stichtag / notes / status
DELETE /api/scenarios/{id} — archive (soft delete; status='archived')
POST /api/scenarios/{id}/proceedings — add proceeding to scenario
PATCH /api/scenarios/{id}/proceedings/{pid} — update flags / perspective / ordinal / detailgrad
DELETE /api/scenarios/{id}/proceedings/{pid} — remove proceeding (cascades to events)
PATCH /api/scenarios/{id}/events/{eid} — update state / date / notes / horizon
POST /api/scenarios/{id}/shares — share with user (read-only)
DELETE /api/scenarios/{id}/shares/{sid} — revoke share
POST /api/scenarios/{id}/promote — promote to project (3-step wizard payload)
POST /api/scenarios/from-project/{project_id} — export project to a new scenario (what-if)
GET /api/search — universal search (events + scenarios + Akten)
Existing endpoints used unchanged:
GET /api/tools/fristenrechner/search?kind=events— for the events corpus.GET /api/projects— Akte picker source.POST /api/projects/{id}/deadlines/bulk— promotion writes deadlines through this.PATCH /api/projects/{id}/scenario-flags— Akte-mode flag sync.
§7 Migration plan from current live shape
Current live (/tools/procedures on main @ ed3c5d1) = cronus's U0-U4 4-tab catalog. Migration is a 6-slice train, every slice ships visibly. No feature flag (m's pattern preference per #152 Q7).
§7.1 Slice train
| Slice | What ships | DB | Visible to user |
|---|---|---|---|
| B0 — Scenario DB foundation | New tables (scenarios + scenario_proceedings + scenario_events + scenario_shares) + RLS + minimal API (list / create / get). Scenarios writable from a developer-only test route at first. | Mig #N (new tables + RLS + paliad.projects.origin_scenario_id) |
No user-visible change. |
| B1 — Builder shell + cold-open mode | New /tools/procedures page replaces the 4-tab catalog. Renders: page header (scenario picker + Akte picker + Stichtag + search), entry-mode radio (cold-open active), filter strip, empty canvas + "Neues Szenario starten" CTA + recent list. Add-proceeding picker works; first triplet renders with the existing Verfahrensablauf-core calc. Auto-save active scenario. Side panel "Meine Szenarien" with Aktiv bucket only. |
— | New page visible. Single triplet works end-to-end. |
| B2 — Multi-triplet + spawn nesting + per-event state | Vertical multi-triplet stack with + Verfahren hinzufügen. Per-triplet perspective + flag strip. Spawn child triplets render inline. Event cards get the 3-state machine (planned/filed/skipped) + date editor + per-card optional horizon chip. Page-header Stichtag drives default dates. |
— | Full scenario builder works without Akte integration. |
| B3 — Event-triggered mode + universal search | "Ereignis" entry mode wires the search box to land on a single-triplet anchored view (scratch scenario). Universal search returns events + scenarios + Akten with type-scoped result groups. Filter pills (forum/proc/party/kind) reset on mode switch. | — | Event lookup works. |
| B4 — Akte mode + project-backed scenarios | "Aus Akte" entry mode + page-header Akte picker. Loads project state into the builder (proceeding + perspective + scenario_flags + deadlines actuals). Akte-backed scenarios write through to paliad.deadlines + paliad.projects.scenario_flags; non-Akte scenarios write to paliad.scenario_events. Cross-surface scenario-flag-changed event listener reused from #152 T3. |
— | Akte integration works end-to-end. |
| B5 — Share + Promote-to-project wizard | "Teilen" button + user picker + share row. "Geteilt mit mir" bucket in side panel. "Als Projekt anlegen" opens the 3-step wizard (Bestätigen → Parteien ergänzen → Akte-Metadaten). Successful commit creates project + cascades deadlines + sets origin_scenario_id, navigates to /projects/{id}. "Promoted" bucket in side panel. |
— | Sharing + promotion work. |
| B6 — Mobile basic-read + cleanup + i18n polish | Mobile (<640px) shows scenarios + cards read-only; mutating affordances prompt "Auf größerem Bildschirm öffnen". Cleanup: delete dead U0-U4 catalog code (4-tab control, legacy verfahrensablauf.ts, etc.). All i18n keys finalised (DE + EN). |
— | Mobile works; codebase cleaner. |
§7.2 Why this train shape
- B0 is DB-only. The schema can land independently and be exercised via test routes / Supabase MCP before any UI sees it. Keeps mig risk isolated.
- B1-B2 are the MVP. After B2, a user can build and save a multi-proceeding scenario fully kontextfrei. That alone replaces 60% of today's catalog use.
- B3 adds the lookup path. After B3, "what's next after Klageerwiderung?" works without saving.
- B4 makes it real. Akte integration is the load-bearing piece for daily use; ships once the foundation is stable.
- B5 unlocks team value. Sharing + promotion are the difference between "personal tool" and "team tool". Ship after the core works.
- B6 is cleanup. Mobile read + dead code removal land last to avoid coupling to in-flight features.
§7.3 What stays unchanged
- URL
/tools/procedureskeeps it (the new builder lives there). - Sidebar entry "Verfahren & Fristen" keeps it.
- cmd-K palette keeps it.
/tools/fristenrechner+/tools/verfahrensablauflegacy redirects (from cronus's U4) stay alive: 301 →/tools/procedures(the builder).pkg/litigationplanner.CalculateRule— untouched./admin/procedural-events— untouched./projects/{id}Verlauf — untouched (new "Im Builder öffnen" button is the only addition).
§7.4 Cleanup at B6
Dead code to delete (verify with grep before deletion):
frontend/src/components/VerfahrensablaufBody.tsx(replaced by ProceedingTriplet)frontend/src/client/verfahrensablauf.ts(replaced by builder.ts orchestration)frontend/src/client/views/verfahrensablauf-state.ts(replaced by scenario-backed state)frontend/src/client/views/verfahrensablauf-state.test.ts— KEEP. Builder importsfrontend/src/client/verfahrensablauf-detail-mode.tsfilterByDetailModefrom it; per-triplet Detailgrad reuses this module.- Existing scratch tab content in
frontend/src/client/procedures.ts(4-tab toggling logic, mode routing)
Kept:
frontend/src/client/views/verfahrensablauf-core.ts(calculation engine; reused by EventCard + ProceedingTriplet)- Legacy URL redirects in Go (
/tools/fristenrechner+/tools/verfahrensablauf→/tools/procedures)
§8 Open follow-ups (out of scope for v1)
Tracked for v1.1 / future tickets:
- Cross-proceeding peer triggers (UPC-inf judgment → EPA opp choice deadline). New
paliad.scenario_event_linkstable. UI: trigger-picker chip on event cards. - DE / EPA / DPMA full expansion. v1 supports EPA + DPMA proceedings at the data layer (calc engine handles them), but the spawn flags and CCR-style nestings are UPC-specific. Other jurisdictions get proper coverage in v1.1.
- Scenario versioning / snapshots. m's Q8 alternative ("versioned snapshots") deferred. Add when scenarios start driving client briefings.
- Multi-user concurrent editing. Out of scope. Single-editor model with read-only shares is sufficient until usage shows otherwise.
- Fork-a-shared-scenario. Read-only sharing in v1 doesn't expose "fork into my workspace". Add when team usage demands it.
- Comments on scenarios / event cards. Out of scope (separate ticket).
- PDF export of a scenario for client briefings. Out of scope.
- Mobile-parity edits. v1.1 — full mobile interaction loop.
- Audit log on scenario edits. Out of scope (exploratory data).
- Cross-scenario comparison view. ("Compare planned vs actual" lives on the project page via promote-then-compare; explicit comparison tool is v2.)
§9 Synthesis links
- mBrian: file as
[synthesis]linkedtriggered_byt-paliad-339;related_toatlas's reverted tracker design, cronus's unified-procedural-events-tool design, atlas's deadline-system-revision. - Cross-refs in this repo:
docs/design-procedures-workflow-tracker-2026-05-27.md(atlas, reverted),docs/design-unified-procedural-events-tool-2026-05-27.md(cronus, live),docs/design-deadline-system-revision-2026-05-27.md(atlas Phase 2),docs/design-fristenrechner-overhaul-2026-05-26.md(cronus 2026-05-26). - Gitea: m/paliad#153 (this PRD), m/paliad#152 (atlas's tracker, reverted), m/paliad#151 (cronus U0-U4 shipped), m/paliad#149 (atlas Phase 2 in flight).
- Coder phase (deferred per inventor SKILL): runs after m ratifies this PRD. Slice ordering per §7.1. NOT edison (parked at DESIGN READY FOR REVIEW). NOT atlas (just-rejected tracker → framing bias). NOT cronus (parked on Fristenrechner inventor branch). A pattern-fluent Sonnet coder picks up B0 first.
§10 Coder hand-off notes
(Pre-emptive — for whoever picks up B0.)
- Migration number: check
internal/db/migrations/for the max slot at coder shift start. Two recent migrations (curie's t-paliad-336, ritchie's t-paliad-149 P0) are in flight; coordinate via paliadin/head before claiming a slot. - Akte integration nuance: when the builder is in Akte mode and the scenario is project-backed, writes flow to
paliad.deadlines/paliad.projects.scenario_flagsinstead ofpaliad.scenario_*tables — the scenario row itself just records the canvas view-state (which triplets are visible, ordinal, collapsed state, per-card horizon). This dual-write rule is the load-bearing complexity of B4; design tests for it explicitly. - Auto-save throttling: 500ms debounce per change. Avoid PATCH-per-keystroke on notes textareas (use blur-trigger + 2s debounce there).
- Search performance: universal search (events + scenarios + Akten) needs to stay snappy. Events corpus is ~3000 rows; scenarios/Akten are per-user. Use existing trgm indexes; avoid joining across all three for ranking.
- B5 transactional promotion: do the wizard's commit in a single Postgres transaction. If any of (project insert / parties / deadlines / scenario status update) fails, roll back atomically. No partial promotions.
- Mobile rendering: B6 is meant to be cheap. Column-triplet → CSS grid that collapses to single-column at
@media (max-width: 640px). Mutating affordances getpointer-events: none+ a click-handler that surfaces the "Auf größerem Bildschirm öffnen" toast — keeps the desktop interaction code paths unchanged. - i18n keys: every user-facing string gets
data-i18nfrom B1. Don't accumulate i18n debt across slices.