diff --git a/CONTEXT.md b/CONTEXT.md
index de164f7b..60fc3694 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -101,9 +101,13 @@ A measure whose installation involves major internal works for residents (intern
_Avoid_: invasive (unused here), wet trades (a narrower, overlapping set: plaster-and-screed work)
**Measure exclusion**:
-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.
+A Scenario constraint that takes a measure off the table for the optimiser. The only measure-*exclusion* 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. (Exclusions decide *which* measures are available; **Fabric first** decides the *order* they are considered — the two compose.)
_Avoid_: inclusions, allowed measures (as a stored concept — UI may present "allowed/excluded" toggles, but what's captured is the exclusion set)
+**Fabric first**:
+A Scenario constraint that forces the optimiser to apply every viable **fabric measure** (insulation, glazing, ventilation) before it will consider heating or renewables — the heating/renewables tier is reached only if the Scenario's upgrade target is still unmet once fabric is exhausted. Unlike a Measure exclusion (which removes a measure entirely), it constrains the *order* measures are considered, not which are on the table; the two compose (an excluded fabric measure simply isn't among those forced in). Off by default, set at creation and immutable like the rest of the config. The modelling backend owns the ordering logic and the fabric/heating split — the app only records the flag (trigger-payload field `enforce_fabric_first`).
+_Avoid_: fabric only (the quick-start that *excludes* heating/renewables outright — fabric-first still allows them as a fallback), sequencing/phasing (reserve for PIBI installation order), enforce fabric first (a UI verb on the toggle, not the domain noun)
+
**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)
diff --git a/src/app/api/portfolio/[portfolioId]/scenarios/route.ts b/src/app/api/portfolio/[portfolioId]/scenarios/route.ts
index a2065311..98539171 100644
--- a/src/app/api/portfolio/[portfolioId]/scenarios/route.ts
+++ b/src/app/api/portfolio/[portfolioId]/scenarios/route.ts
@@ -29,6 +29,7 @@ export async function POST(
exclusions: Array.isArray(b.exclusions)
? b.exclusions.filter((k): k is string => typeof k === "string")
: [],
+ fabricFirst: b.fabricFirst === true,
});
if (!result.ok) {
diff --git a/src/app/portfolio/[slug]/(portfolio)/scenarios/[scenarioId]/page.tsx b/src/app/portfolio/[slug]/(portfolio)/scenarios/[scenarioId]/page.tsx
index 55fd7b98..a9e3101b 100644
--- a/src/app/portfolio/[slug]/(portfolio)/scenarios/[scenarioId]/page.tsx
+++ b/src/app/portfolio/[slug]/(portfolio)/scenarios/[scenarioId]/page.tsx
@@ -7,6 +7,7 @@ import { getScenarioWithPlansCount } from "@/lib/scenarios/queries";
import {
BandChip,
EPC_SOFT,
+ FabricFirstChip,
GOAL_SHORT_LABELS,
GoalIcon,
StatusPill,
@@ -114,7 +115,7 @@ export default async function ScenarioDetailPage(props: {
)}
-