diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 66ffaf49..19cddff6 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -12,7 +12,9 @@
// Install Domna's curated skill set (pinned to 0.0.5) into this workspace,
// then install npm deps. `gh repo clone` handles private-repo auth using
// the mounted host ~/.config/gh.
- "postCreateCommand": "gh repo clone Hestia-Homes/agentic-toolkit /tmp/agentic-toolkit -- --branch 0.0.8 --depth 1 && bash /tmp/agentic-toolkit/setup.sh && npm install",
+ // impeccable (design-guidance skill, github.com/pbakaus/impeccable) is on
+ // trial for the home-page redesign; drop its `skills add` step to remove it.
+ "postCreateCommand": "gh repo clone Hestia-Homes/agentic-toolkit /tmp/agentic-toolkit -- --branch 0.0.8 --depth 1 && bash /tmp/agentic-toolkit/setup.sh && npx --yes skills@latest add pbakaus/impeccable --agent claude-code --copy --global --yes && npm install",
"forwardPorts": ["frontend:3000", "pgadmin:80"],
diff --git a/.impeccable/critique/2026-07-07T13-05-05Z__portfolios-mock-html.md b/.impeccable/critique/2026-07-07T13-05-05Z__portfolios-mock-html.md
new file mode 100644
index 00000000..9d2e630f
--- /dev/null
+++ b/.impeccable/critique/2026-07-07T13-05-05Z__portfolios-mock-html.md
@@ -0,0 +1,67 @@
+---
+target: portfolios home redesign mockup (calm)
+total_score: 27
+p0_count: 0
+p1_count: 3
+timestamp: 2026-07-07T13-05-05Z
+slug: portfolios-mock-html
+---
+# Critique: Portfolios home redesign mockup (Calm direction)
+
+Method: dual-agent (A: design review · B: detector/mechanical evidence). Target: scratchpad/portfolios-mock.html (artifact 76ff144a). Browser visualization skipped: no browser automation in session.
+
+## Design Health Score: 27/40 — Acceptable, near Good
+
+| # | Heuristic | Score | Key Issue |
+|---|-----------|-------|-----------|
+| 1 | Visibility of System Status | 3 | Collapsed folders hide all state; a folded folder with 2 at-risk portfolios looks identical to a healthy one |
+| 2 | Match System / Real World | 3 | Mixed metaphor: star icon + "Pin" labels + "favourite" in notes — pick one word |
+| 3 | User Control and Freedom | 2 | No folder create/reorder/rename/delete anywhere; users can't control the page's primary organising structure |
+| 4 | Consistency and Standards | 3 | Card hover-lift implies whole-card clickability but only the name is a link; Pinned isn't collapsible while folders are |
+| 5 | Error Prevention | 3 | Low-stakes page; kebab actions undesigned so unassessable |
+| 6 | Recognition Rather Than Recall | 3 | Folders only discoverable via hidden kebab menu |
+| 7 | Flexibility and Efficiency | 2 | No "/" search shortcut, no bulk select/move, no collapse-all, no status sort |
+| 8 | Aesthetic and Minimalist Design | 3 | Pinned-card duplication; status pill outranks the name in reading order |
+| 9 | Error Recovery | 3 | No-results state names query + one-click clear (good); real page needs fetch-failure states |
+| 10 | Help and Documentation | 2 | One teaching empty state (pinning); nothing teaches folders |
+
+## Anti-Patterns Verdict
+
+Passes the product slop test (design review): no banned patterns, schema-honest fields, restrained navy palette, earned familiarity. Deterministic detector: exit 0, zero findings — but its clean bill covers only its own rule set; mechanical checks beyond it found the non-text contrast failures below.
+
+## Priority Issues
+
+- **[P1] Sectioned-folders IA collapses at ~15 folders × 60 portfolios.** All folders open by default, no rollup, no jump nav → endless undifferentiated scroll; collapsing destroys at-a-glance state. Fix: collapsed-by-default with persisted open state; folder-header rollups (count, Σ properties, worst-status chips like "2 at risk"); sticky folder index above ~6 folders; or move folders to a left filter rail entirely.
+- **[P1] Folder lifecycle undesigned.** No create, reorder (user's own question — order is hardcoded array), rename, delete. Fix: "New folder" affordance; folder-header kebab with Rename/Reorder/Remove; default alphabetical with per-user manual override (position column).
+- **[P1] Pin interaction breaks keyboard/SR users.** innerHTML re-render destroys the focused star (focus drops to body); no aria-live announcements for pin/search-count changes. Fix in real build: targeted DOM updates or focus restoration by data-id + polite live region.
+- **[P2] Non-text contrast failures (computed).** Idle star/kebab #9aa2b1 on white = 2.57:1; pinned gold star #f1bb06 on white = 1.77:1 (state nearly invisible to low-vision users); card border 1.24:1. Text pairs all pass 4.5:1 (lowest 5.59:1).
+- **[P2] Search is name-only, no keyboard path.** Doesn't match status/goal/folder; no "/" focus; header count stays global while filtering (should read "Showing X of Y").
+
+## Persona Red Flags
+
+**Alex (power user):** no keyboard route to search; only name text clickable despite whole-card hover-lift; organising 60 portfolios = 60 kebab round-trips (no bulk move); no collapse-all; no status sort; pinned duplication doubles scan work.
+
+**Sam (accessibility):** focus wiped on pin toggle; pinned state fails 3:1 non-text contrast; no announcements for dynamic changes; h2-in-summary risks odd NVDA announcements. Keeping: native details/summary, focus ring token, aria-pressed, reduced-motion handling.
+
+## Minor Observations
+
+- Empty folder renders a blank dashed box (empty string default) — needs teaching copy.
+- Overspend (cost > budget) clamps the bar silently and stays blue — a red state would serve "trustworthy numbers".
+- Status pill above the name makes colour the first read; name should lead on a dispatch page.
+- Dead CSS (.updated-inline); list view <900px drops all data columns.
+- "Hit the star" too casual for institutional register; unify Pinned/Starred/Favourites naming.
+- Nothing signals folders are per-user, not shared with teammates.
+- Mock file is a fragment (no doctype/lang/viewport) — supplied by the Artifact wrapper at publish, but the real page needs them natively; summary contains flow content (non-conforming, works in practice).
+- Real page needs skeleton sections, not spinners.
+
+## Strengths
+
+- Schema-honest cards ("No budget set" shown, £-of-£ bar only when both fields exist) — Design Principle 1 executed.
+- Trustworthy-numbers craft: tabular-nums, consistent £M/£k, honest >100% percentages.
+- 10-value status enum compressed to 8 clean pills, text + colour, never colour alone.
+
+## Questions to Consider
+
+- What if folders were a left filter rail instead of page sections? Solves order, scale, and jump-nav in one move.
+- What is a collapsed folder for, if collapsing hides all status? The header rollup is the reconciliation, not a nice-to-have.
+- Is pin-as-duplicate right, or should pinning float items within their folder?
diff --git a/.impeccable/critique/2026-07-07T16-54-50Z__src-app-components-home.md b/.impeccable/critique/2026-07-07T16-54-50Z__src-app-components-home.md
new file mode 100644
index 00000000..fd3e6334
--- /dev/null
+++ b/.impeccable/critique/2026-07-07T16-54-50Z__src-app-components-home.md
@@ -0,0 +1,67 @@
+---
+target: implemented portfolio home page
+total_score: 25
+p0_count: 0
+p1_count: 4
+timestamp: 2026-07-07T16-54-50Z
+slug: src-app-components-home
+---
+# Critique: Implemented portfolio home page (feature/home-portfolio-redesign)
+
+Method: dual-agent (A: design review · B: detector/mechanical evidence). Target: src/app/components/home/* + src/app/home/page.tsx. Browser evidence skipped: deployed preview is auth-gated (next-auth redirect) and no browser automation in session.
+
+## Design Health Score: 25/40 — Acceptable; solid skeleton, weak edges
+
+| # | Heuristic | Score | Key Issue |
+|---|-----------|:---:|-----------|
+| 1 | Visibility of System Status | 3 | Folder create has no pending state; appears only after refetch |
+| 2 | Match System / Real World | 4 | Enum→display translation, search in display language, honest "unfiled, not deleted" copy |
+| 3 | User Control and Freedom | 2 | No undo; rename input discards silently on blur; menus lack Escape/outside-click close |
+| 4 | Consistency and Standards | 2 | Three overlay vocabularies: Headless UI Dialog, window.confirm, bare details popovers |
+| 5 | Error Prevention | 3 | Confirm exists; no duplicate-folder-name check; blur-cancel loses typed renames |
+| 6 | Recognition Rather Than Recall | 3 | Folder ⋯ is opacity-0 hover-revealed — invisible on touch |
+| 7 | Flexibility and Efficiency | 2 | No shortcuts, no bulk actions, no URL state, no Show-all |
+| 8 | Aesthetic and Minimalist Design | 3 | Table crams folder into Updated cell |
+| 9 | Error Recovery | 1 | Rollback message renders ONLY into sr-only region — sighted users see changes silently vanish; createMutation swallows errors |
+| 10 | Help and Documentation | 2 | "Drag to reorder" hint hidden exactly where drag doesn't work (mobile) |
+
+## Anti-Patterns Verdict
+
+Not slop (A): no banned patterns, schema-honest, restrained register; failures are under-engineering (native confirm, raw details menus, hex drift), not AI over-decoration. Detector (B): zero findings on all four files — but regex mode is blind to the real issues below (false negatives, not false positives).
+
+## Where A and B converge (highest-confidence findings)
+
+- window.confirm at PortfolioHome.tsx:372 (A: register break; B: fact) — with B's irony: it's currently the page's most accessible dialog; the replacement must not regress focus handling.
+- details/summary menus have no Esc/outside-click close, no menu semantics; two can be open at once (A heuristic 3/4; B verified no such JS exists).
+- Invisible failure states: announceError feeds only the sr-only region (A P1; B verified role=status is sr-only).
+- Touch gaps: folder ⋯ trigger invisible without hover (B fact; A recognition issue).
+
+## B-only mechanical findings
+
+- Contrast FAILs: placeholder gray-400 on white 2.54:1 (Tailwind default, never overridden); gray-500 counts on active rail bg 3.97:1 and hover 4.11:1. All 8 pills PASS (6.37–8.57). Gold star fill 1.77:1 but aria-hidden decorative with adjacent label + 17.33:1 stroke carrying state.
+- Hit targets: FolderRail ⋯ 23×23 (FAIL 24px min); rail "New" ~20px tall (FAIL). CardMenu exactly 24. Star 25. Others pass.
+- Table th lack scope="col"; "Your folders" header is a div not a heading; ✓/⚠ glyphs rely on aria-label-on-span/title (unreliable AT support); folder-name input has focus:outline-none with no focus state remaining.
+- Hex drift: greys/blues inline (#c3ccd8 #e2e7ee #e4e9f1 #e9edf3 #f8fafc #eceefb) while brand palette exists in tailwind.config.
+- Hygiene clean: no TODOs, no !important, one justified inline style, z-20 only, no unused imports.
+
+## Priority Issues
+
+- [P1] Replace window.confirm with inline two-step confirm in the folder menu panel ("Unfiles 12 portfolios · Remove / Cancel", red confirm, focus moves to it, Escape cancels). Chosen over dialog (over-weights a non-destructive action; modal-as-first-thought ban) and undo-toast (needs toast system + server restore API since folder recreation mints a new id).
+- [P1] Make mutation failures visible: shared visible status + live region; createMutation needs error+pending handling.
+- [P1] List-view CardMenu is clipped by the table's overflow-x-auto container — menu can be invisible with focus inside it. Portal/fixed/popover required.
+- [P1] No bulk filing: organising 81 portfolios is one-at-a-time (~250+ clicks). Multi-select + "Move N to folder", and/or rail folders as drop targets.
+- [P2] Standardise one popover primitive (Esc, outside-click, menu semantics, single-open) — heals heuristic 4.
+
+## Persona Red Flags
+
+Alex: no keyboard path to search/open; view/search/sort lost on refresh (no URL state); Show-more ×3 with no Show-all; cards re-sort under cursor on star.
+Sam: details menus announce as disclosures not menus; clipped menu can contain focus while invisible; good: aria-pressed + live announcements on star, drag has menu fallback, focus-visible reveals hidden trigger.
+
+## Minor
+
+Hex drift → tokenise; "Folder order saved" announced before server confirms; star-teleport re-sort mid-scan; folder deserves own list-view column; NewPortfolioModal clashes (out of scope); no duplicate-name guard.
+
+## Questions
+
+- Rail folders as live drop targets — would drag-to-file make bulk-organise disappear?
+- At 81 portfolios, is this a browse surface or a dispatcher (recents + starred + command palette first)?
diff --git a/CONTEXT.md b/CONTEXT.md
index 46f0f7a9..33bf900a 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -104,6 +104,14 @@ _Avoid_: invasive (unused here), wet trades (a narrower, overlapping set: plaste
A Scenario constraint that takes a measure off the table for the optimiser. The *only* measure-constraint concept on a Scenario — there is no stored inclusion list; "only Solar PV + ASHP" is expressed by excluding everything else. An empty exclusion set is valid and means every measure is available.
_Avoid_: inclusions, allowed measures (as a stored concept — UI may present "allowed/excluded" toggles, but what's captured is the exclusion set)
+**Modelling run**:
+One user-initiated act of triggering modelling: a set of Scenarios crossed with one filter-defined property selection within a Portfolio. A durable record of *what was asked* — the Run filters, the Scenarios, who triggered it, when, and the matched-property count shown at preview — so Plans can be traced back to the selection that produced them, alongside how the work is going. A Scenario is the *question*; a Modelling run *asks* it (possibly again, possibly for a different property selection). Re-running an already-modelled property appends a newer Plan; latest wins on read.
+_Avoid_: job, batch, trigger request, modelling task (the pipeline's execution records)
+
+**Run filter**:
+The property-selecting constraints of a Modelling run: postcode, property type, and built form (each multi-select; tags later). An absent filter means *unconstrained* — "all postcodes" is the absence of a postcode filter, never an enumeration. Type and built form are the canonical enum vocabularies plus **Unknown**, which selects properties with no resolvable value at all. Resolution follows the Landlord-override rule: an override fact beats an EPC-derived value; the modelling backend is the single authority for resolving filters to properties (the app never re-implements the rule — matched counts come from the backend).
+_Avoid_: property query, segment, search (the postcode-search journey is unrelated)
+
## Lifecycle
A **BulkUpload** moves through these statuses:
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 00000000..c69b31cc
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,41 @@
+# Product
+
+## Register
+
+product
+
+## Users
+
+Three operator groups, all professionals in a task, none casual visitors:
+
+- **Domna internal team** — analysts/consultants running assessments and modelling on behalf of landlords. Power users; live in the tool daily.
+- **Housing-association staff** — landlord-side asset and sustainability managers tracking their portfolios' retrofit progress. Domain-expert but not tool-expert; visit weekly.
+- **Contractors/approvers** — external parties with scoped capabilities on specific portfolios; arrive with a narrow job (approve, quote, deliver).
+
+Shared context: desk work, large screens, often alongside spreadsheets and procurement documents. The home page's job is orientation and dispatch — find the right portfolio fast and understand its state at a glance.
+
+## Product Purpose
+
+Domna assesses UK housing portfolios for energy retrofit: ingesting property data (bulk uploads, postcode search), modelling retrofit interventions, and reporting outcomes (EPC uplift, CO2/energy savings, valuation impact) against budgets. Success looks like a landlord trusting Domna's numbers enough to commit retrofit budgets against them.
+
+## Brand Personality
+
+**Hypothesis (user undecided — validate against mockup variants):** calm, expert, institutional — quiet confidence appropriate to housing-sector procurement, with measured warmth from the brand tan/brown accents. Brand navy (`#14163d`) carries identity in small, deliberate doses; the surface stays neutral and legible. Not playful, not loud, never salesy inside the product.
+
+## Anti-references
+
+- **Generic SaaS admin templates** — Bootstrap/AdminLTE dashboards, hero-metric cards, widget walls.
+- **The current home page** — a bare centered grid of identical icon cards with no data, hierarchy, or organisation.
+- **Over-designed AI mockup output** — 2rem-rounded cards, giant soft shadows, decorative gradients, invented data the schema can't supply.
+
+## Design Principles
+
+1. **Show real state, not decoration.** Every element on a card earns its place by reflecting actual schema data (status, goal, properties, budget). No invented metrics.
+2. **Orientation before information.** The home page dispatches users to the right portfolio; scanability (favourites, folders, search, sort) beats per-card completeness.
+3. **Earned familiarity.** Standard affordances done precisely — the tool should disappear into the task for someone fluent in Linear/Stripe-class products.
+4. **Trustworthy numbers.** Figures are the product; typography and alignment treat numeric data with more care than any decorative element.
+5. **Consistency over surprise.** One component vocabulary across the app; delight lives in moments (empty states, transitions), not pages.
+
+## Accessibility & Inclusion
+
+WCAG 2.1 AA: ≥4.5:1 body-text contrast (≥3:1 large text), full keyboard operability, visible focus states, `prefers-reduced-motion` alternatives for all animation. B2B housing-sector procurement may require conformance evidence.
diff --git a/docs/adr/0008-modelling-runs-filters-to-distributor.md b/docs/adr/0008-modelling-runs-filters-to-distributor.md
new file mode 100644
index 00000000..bbe0985c
--- /dev/null
+++ b/docs/adr/0008-modelling-runs-filters-to-distributor.md
@@ -0,0 +1,118 @@
+# 8. Modelling runs pass filters to the distributor; the backend owns resolution
+
+Date: 2026-07-06
+
+## Status
+
+Accepted
+
+Numbering note: 0004–0006 are referenced by CONTEXT.md but were lost to the old
+`docs/adr/**` gitignore rule; 0008 avoids all collisions (0003 = app-authored
+scenarios, 0007 = postcode search).
+
+## Context
+
+Users can now add properties (postcode search, bulk upload) and author
+Scenarios (ADR-0003) before any modelling exists. The missing step is
+triggering modelling for *chosen* properties: many Scenarios × a subset of the
+portfolio, potentially 50,000 properties. The entry point on the Model side is
+a **distributor** lambda — it fans work out to modelling workers, it does not
+model synchronously. ADR-0003 recorded a known follow-up: the in-flight
+"modelling running" state is not representable by derivation and needs an
+explicit marker when trigger wiring lands. This is that wiring.
+
+## Decision
+
+- **A Modelling run is recorded as an app-created task** — the BulkUpload
+ trigger convention (`triggerAddressMatching`): Next.js inserts the `tasks`
+ row (service `modelling_run`, `source_id` = portfolio, status `waiting`)
+ whose **`inputs` column** (added for this — mirrors `sub_task.inputs`)
+ carries `{portfolio_id, scenario_ids, filters, previewed_property_count,
+ triggered_by}` as JSON, then passes `task_id` to the distributor, which
+ attaches its execution sub-tasks to the same task. **One new column, no new
+ table.** Run history and the per-scenario in-flight badge read the
+ portfolio's own `modelling_run` tasks (portfolio-scoped, a handful of rows)
+ and parse the app-authored inputs JSON; execution progress falls out of the
+ existing subtask-count summary pattern. Sub-task granularity is the
+ distributor's **batches**, so progress is shown as a percentage — batch
+ counts are internally consistent but never comparable to property or plan
+ numbers.
+- **The distributor (`POST /v1/modelling/trigger-run`) receives
+ `{task_id, portfolio_id, scenario_ids, filters}`** — filters, never
+ property-id lists. The payload stays tiny at any portfolio size; the task's
+ `inputs` is the durable copy of the same request (plus provenance the
+ distributor doesn't need).
+- **Dispatch failure marks the task `failed`** (an improvement on the
+ bulk-upload flow, which leaves failed triggers at `waiting`), so history
+ never shows a ghost run as pending.
+- **The modelling backend is the single authority for resolving filters to
+ properties.** Resolution precedence: landlord-override fact → EPC-derived
+ value (new-approach EPC graph, lodged over predicted, or legacy property-row
+ columns) → **Unknown**. The app never re-implements this rule.
+- **Preview counts are computed in-app** (amended 2026-07-06; originally a
+ backend dry-run endpoint): one grouped SQL statement resolves the filters —
+ override snapshot → EPC-derived (`propertyTypeSql`/`builtFormTypeSql` in
+ epcSources: new-approach graph, lodged over predicted, RdSAP codes mapped;
+ legacy property-row columns) → Unknown — and intersects the matched set
+ with `plan` for per-scenario already-modelled counts ("120 of 214 will be
+ re-modelled"). Both the app and the distributor read this database and
+ implement this documented rule; divergence is a bug in whichever side moved
+ without the other. Plan arithmetic is exact: one plan per
+ (scenario, property) per run.
+- **Filter options are resolution-free in the app**: the postcode multi-select
+ is one `SELECT DISTINCT postcode` per portfolio; type/built-form options are
+ the static canonical enums plus Unknown. Filters are bounded (cap the
+ postcode multi-select) as a UX guard; "all" is an absent filter, never an
+ enumeration.
+- **Run status is the task's status** — no correlation machinery needed: the
+ run and the execution share one record by construction. Scenario badges
+ gain a third derived state — "Modelling in progress" — and a per-portfolio
+ run history lists each run's filters, counts, initiator and derived status
+ with batch-based percentage progress.
+- **Concurrent runs warn, never block**: triggering a scenario with a run in
+ flight shows who started it and when, but proceeds. Plans append and
+ latest-per-property wins on read, so overlap is safe; a hard lock could leak
+ on a dead run.
+
+## Alternatives considered
+
+- **A dedicated `modelling_run` table (+ scenarios join table)**: relational
+ scenario linkage and typed columns for who/filters/counts, but a migration,
+ a second record system beside tasks, and correlation machinery (either the
+ backend stamping run ids into tasks, or the app storing returned task ids).
+ The task system already records app-initiated pipeline work with inputs,
+ progress and status — and the badge/history queries are portfolio-scoped
+ over a handful of rows, so the JSON parse costs nothing that matters.
+- **Pass resolved property ids to the distributor**: makes preview-vs-run
+ drift impossible by construction, but ~50k ids is a payload the async entry
+ point can't take, and it moves the resolution rule into the app — the
+ backend already owns it for modelling itself.
+- **App-side count queries mirroring the resolution rule**: no backend
+ dependency for the preview, but the overrides-else-EPC rule would live in
+ two codebases and the shown count could disagree with what runs.
+- **Status columns on the run row (two-writer, as bulk uploads)**: simple
+ reads, but a second source of truth that can silently never arrive; task
+ correlation reuses records the pipeline writes anyway.
+- **Blocking concurrent runs**: prevents accidental double-spend but a stalled
+ pipeline would lock the scenario until someone clears task rows by hand.
+
+## Consequences
+
+- One backend ask must land with the distributor: accept an app-created
+ `task_id` and attach execution subtasks to it (rather than creating its own
+ task, as the current `plan_engine` entrypoint does). Its filter resolution
+ must match the rule above — the app's preview is the reference
+ implementation.
+- The preview's already-modelled callout and matched counts are only as fresh
+ as trigger time; a property added between preview and trigger is picked up
+ by the run (filters re-resolve) — the run row records the previewed count so
+ discrepancies are visible after the fact.
+- Reporting/scenario UIs gain a third derived scenario state; anything
+ assuming "has a run row ⇒ has plans" is wrong (a dead run may produce none).
+- The tagging system (future) extends Run filters without touching the
+ contract shape: tags become one more optional filter key.
+- Known follow-up: **email notification on run completion**. The dispatch
+ confirmation sets a time expectation only; "we'll email you when results
+ are ready" was deliberately kept out of the UI copy until a completion
+ notification exists. The run_id → task correlation gives the completion
+ signal to hang it on.
diff --git a/docs/adr/0009-portfolio-organisation-is-per-user.md b/docs/adr/0009-portfolio-organisation-is-per-user.md
new file mode 100644
index 00000000..6262f607
--- /dev/null
+++ b/docs/adr/0009-portfolio-organisation-is-per-user.md
@@ -0,0 +1,51 @@
+# 9. Portfolio organisation (folders, starring) is per-user, not shared
+
+Date: 2026-07-07
+
+## Status
+
+Accepted
+
+## Context
+
+The home-page redesign introduced folders and starring so users can organise
+their portfolios. The obvious alternative was **shared, team-level
+organisation** — one folder structure per organisation that everyone sees, the
+way housing-association staff might expect from a shared drive.
+
+The user base spans three groups with conflicting mental models: Domna
+analysts touching many portfolios daily, housing-association asset managers
+visiting weekly, and contractors with narrow scoped access. One person's
+"SHDF Wave 3" triage grouping is another's noise; a contractor can see only a
+subset of any shared structure, which renders shared folders confusing
+(half-empty groups) or leaky (folder names disclose portfolios they can't
+see).
+
+## Decision
+
+Folders and starring belong to the individual user:
+
+- `user_portfolio_folders` and `user_portfolio_config` (migration 0264) key
+ everything on `user_id`. A composite FK `(folder_id, user_id) →
+ user_portfolio_folders(id, user_id)` makes cross-user filing impossible at
+ the database level.
+- `portfolioUsers` remains pure access control; the config layer never
+ grants or implies access. Config rows are created lazily and simply stop
+ rendering if access is revoked (they resurface on re-invite).
+- Folder deletion only unfiles; it is app-mediated (unfile + delete in one
+ transaction) because the composite FK is `NO ACTION`.
+- The UI states the model ("Folders are personal to you — teammates organise
+ their own") and folder URLs (`/home?folder=…`) resolve per-viewer.
+
+## Consequences
+
+- No permission model, naming disputes, or sync semantics for shared
+ structures — the feature stayed one schema PR and one page.
+- Teams cannot share a curated view. If that demand materialises, shared
+ organisation should be **additive** (e.g. org-level folders alongside
+ personal ones), not a mutation of this schema — the per-user tables and
+ the composite FK do not accommodate multi-owner folders.
+- Every user starts from an unorganised list and does their own filing;
+ drag-to-file exists to keep that cheap.
+- Support/debugging must remember that two users looking at "the same"
+ home page see different groupings by design.
diff --git a/docs/design/home-redesign/sample1.html b/docs/design/home-redesign/sample1.html
new file mode 100644
index 00000000..1a231709
--- /dev/null
+++ b/docs/design/home-redesign/sample1.html
@@ -0,0 +1,792 @@
+
+
+
+
+ No portfolios in {viewTitle} match “
+ {query.trim()}”.{" "}
+
+
+ ) : (
+
+ {activeView === "starred" ? (
+ <>
+
+ Nothing starred yet.
+ {" "}
+ Star a portfolio to keep your day-to-day work one click
+ away.
+ >
+ ) : activeView === "all" ? (
+ <>
+
+ No portfolios yet.
+ {" "}
+ Create your first portfolio to get started.
+ >
+ ) : (
+ <>
+
+ This folder is empty.
+ {" "}
+ Move portfolios here with the ⋯ menu on any card.
+ >
+ )}
+
+ Choose the scenarios you want answered, then the homes to include. The counts you see are
+ checked against your portfolio as you go — what you see is exactly what gets modelled.
+
+
+ {done ? (
+
+
+
+
+
+ Modelling started — you don't need to stay on this page
+
+
+ We're modelling {done.properties.toLocaleString()}{" "}
+ {done.properties === 1 ? "home" : "homes"} against {done.scenarios}{" "}
+ scenario{done.scenarios === 1 ? "" : "s"} —{" "}
+ {(done.properties * done.scenarios).toLocaleString()} plan
+ {done.properties * done.scenarios === 1 ? "" : "s"}. Results for a run this size
+ are typically ready within{" "}
+ ~{estimateRunMinutes(done.properties * done.scenarios)} minutes — progress
+ updates under Recent runs below as it happens.
+
+
+
+ setDone(null)}
+ className="rounded-xl border border-gray-200 bg-white px-4 py-2 text-sm font-semibold text-brandblue transition hover:bg-gray-50"
+ >
+ Set up another run
+
+
+ Back to portfolio
+
+
+
+
+ ) : (
+ <>
+ {/* Step 1 — scenarios */}
+
+
+ Step 1 · Scenarios
+
+
+ Which questions are we asking?
+
+
+ Every selected scenario runs against the same property selection.
+
+ {scenarios.length === 0 ? (
+
+ No scenarios yet —{" "}
+
+ create your first scenario
+ {" "}
+ and come back to run it.
+
+ Leave a filter untouched to include everything. What you've told us about a home
+ takes priority over its EPC; “Unknown” picks out homes where we have
+ neither.
+
+
+ {postcodesQuery.isError ? (
+
+
+ Postcode
+
+
+ We couldn't load your portfolio's postcodes.{" "}
+ postcodesQuery.refetch()}
+ className="font-semibold underline hover:text-red-800"
+ >
+ Try again
+
+
+ Filters are limited to {MAX_FILTER_POSTCODES} postcodes per run. Tags are coming —
+ you'll be able to save a selection and reuse it.
+
+
+
+ >
+ )}
+
+ {/* Recent runs — always visible: after a run is triggered this is where
+ its live status lands, so the banner above can say "watch below" */}
+
+
+
Recent runs
+
+ what was asked, by whom, and how it went
+
+
+ {history.isLoading ? (
+
Loading…
+ ) : !history.data?.length ? (
+
+ No runs yet — your first appears here the moment you trigger it.
+
+
+ {STATUS_LABELS[r.status.state]}
+
+ {/* Work is chunked into batches, so show a percentage —
+ the counts never line up with property/plan numbers */}
+ {r.status.state === "in_progress" && (
+ <>
+
+ {Math.round((100 * r.status.ready) / Math.max(1, r.status.total))}% done
+
+
+
+
+ >
+ )}
+
+ You're about to model {matched?.toLocaleString()} homes across{" "}
+ {scenarioIds.length} scenario{scenarioIds.length > 1 ? "s" : ""} —{" "}
+ {planTotal?.toLocaleString()} plans. A run this size can't be stopped
+ once it starts.
+